From 3c804aedf8eeb6eb40da8c14939cf89e082ab6ab Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Tue, 17 Mar 2026 18:11:53 +0000 Subject: [PATCH] extract dbaas, authentik, crowdsec from platform into independent stacks [ci skip] Phase 1 of platform stack split for parallel CI applies. All 3 modules were fully independent (no cross-module refs). State migrated via terraform state mv. All 3 stacks applied with zero changes (dbaas had pre-existing ResourceQuota drift). Woodpecker pipeline updated to run extracted stacks in parallel. --- .woodpecker/default.yml | 11 +- stacks/authentik/main.tf | 25 + stacks/authentik/modules/authentik/main.tf | 88 ++ .../authentik/modules/authentik/pgbouncer.ini | 14 + .../authentik/modules/authentik/pgbouncer.tf | 140 +++ .../authentik/modules/authentik/userlist.txt | 1 + .../authentik/modules/authentik/values.yaml | 73 ++ stacks/authentik/secrets | 1 + stacks/authentik/terragrunt.hcl | 8 + stacks/authentik/tiers.tf | 10 + stacks/crowdsec/main.tf | 30 + .../crowdsec/crowdsec-ingress-bouncer.yaml | 44 + stacks/crowdsec/modules/crowdsec/main.tf | 376 ++++++ stacks/crowdsec/modules/crowdsec/values.yaml | 226 ++++ stacks/crowdsec/secrets | 1 + stacks/crowdsec/terragrunt.hcl | 8 + stacks/crowdsec/tiers.tf | 10 + stacks/dbaas/main.tf | 27 + stacks/dbaas/modules/dbaas/chart_values.tpl | 16 + stacks/dbaas/modules/dbaas/cluster.yaml | 30 + stacks/dbaas/modules/dbaas/main.tf | 1091 +++++++++++++++++ .../modules/dbaas/mysql_chart_values.yaml | 14 + .../dbaas/postgres/postgres_Dockerfile | 30 + stacks/dbaas/modules/dbaas/versions.tf | 9 + stacks/dbaas/secrets | 1 + stacks/dbaas/terragrunt.hcl | 8 + stacks/dbaas/tiers.tf | 10 + stacks/platform/main.tf | 61 +- 28 files changed, 2306 insertions(+), 57 deletions(-) create mode 100644 stacks/authentik/main.tf create mode 100644 stacks/authentik/modules/authentik/main.tf create mode 100644 stacks/authentik/modules/authentik/pgbouncer.ini create mode 100644 stacks/authentik/modules/authentik/pgbouncer.tf create mode 100644 stacks/authentik/modules/authentik/userlist.txt create mode 100644 stacks/authentik/modules/authentik/values.yaml create mode 120000 stacks/authentik/secrets create mode 100644 stacks/authentik/terragrunt.hcl create mode 100644 stacks/authentik/tiers.tf create mode 100644 stacks/crowdsec/main.tf create mode 100644 stacks/crowdsec/modules/crowdsec/crowdsec-ingress-bouncer.yaml create mode 100644 stacks/crowdsec/modules/crowdsec/main.tf create mode 100644 stacks/crowdsec/modules/crowdsec/values.yaml create mode 120000 stacks/crowdsec/secrets create mode 100644 stacks/crowdsec/terragrunt.hcl create mode 100644 stacks/crowdsec/tiers.tf create mode 100644 stacks/dbaas/main.tf create mode 100644 stacks/dbaas/modules/dbaas/chart_values.tpl create mode 100644 stacks/dbaas/modules/dbaas/cluster.yaml create mode 100644 stacks/dbaas/modules/dbaas/main.tf create mode 100644 stacks/dbaas/modules/dbaas/mysql_chart_values.yaml create mode 100644 stacks/dbaas/modules/dbaas/postgres/postgres_Dockerfile create mode 100644 stacks/dbaas/modules/dbaas/versions.tf create mode 120000 stacks/dbaas/secrets create mode 100644 stacks/dbaas/terragrunt.hcl create mode 100644 stacks/dbaas/tiers.tf diff --git a/.woodpecker/default.yml b/.woodpecker/default.yml index d51df71f..127bbedb 100644 --- a/.woodpecker/default.yml +++ b/.woodpecker/default.yml @@ -32,9 +32,9 @@ steps: kubernetes: resources: requests: - memory: 1Gi - limits: memory: 2Gi + limits: + memory: 4Gi commands: - "apk update && apk add curl unzip git openssh-client" # Install Terraform @@ -45,8 +45,13 @@ steps: - "chmod 755 /usr/local/bin/terragrunt" # Source Vault token - "source .vault-env" - # Apply platform stack (core infrastructure services) + # Apply extracted stacks in parallel (slow modules) + - "cd stacks/dbaas && terragrunt apply --non-interactive -auto-approve &" + - "cd stacks/authentik && terragrunt apply --non-interactive -auto-approve &" + - "cd stacks/crowdsec && terragrunt apply --non-interactive -auto-approve &" + # Apply platform stack (remaining core infrastructure services) - "cd stacks/platform && terragrunt apply --non-interactive -auto-approve" + - "wait" - name: cleanup-and-push image: alpine diff --git a/stacks/authentik/main.tf b/stacks/authentik/main.tf new file mode 100644 index 00000000..6b3e1f9f --- /dev/null +++ b/stacks/authentik/main.tf @@ -0,0 +1,25 @@ +# ============================================================================= +# Authentik Stack — Identity provider (SSO) +# ============================================================================= + +variable "tls_secret_name" { type = string } +variable "redis_host" { type = string } + +data "vault_kv_secret_v2" "secrets" { + mount = "secret" + name = "platform" +} + +locals { + homepage_credentials = jsondecode(data.vault_kv_secret_v2.secrets.data["homepage_credentials"]) +} + +module "authentik" { + source = "./modules/authentik" + tier = local.tiers.cluster + tls_secret_name = var.tls_secret_name + secret_key = data.vault_kv_secret_v2.secrets.data["authentik_secret_key"] + postgres_password = data.vault_kv_secret_v2.secrets.data["authentik_postgres_password"] + redis_host = var.redis_host + homepage_token = try(local.homepage_credentials["authentik"]["token"], "") +} diff --git a/stacks/authentik/modules/authentik/main.tf b/stacks/authentik/modules/authentik/main.tf new file mode 100644 index 00000000..3c5506a4 --- /dev/null +++ b/stacks/authentik/modules/authentik/main.tf @@ -0,0 +1,88 @@ +variable "tls_secret_name" {} +variable "secret_key" {} +variable "postgres_password" {} +variable "tier" { type = string } +variable "redis_host" { type = string } +variable "homepage_token" { + type = string + default = "" + sensitive = true +} + + +module "tls_secret" { + source = "../../../../modules/kubernetes/setup_tls_secret" + namespace = kubernetes_namespace.authentik.metadata[0].name + tls_secret_name = var.tls_secret_name +} + +resource "kubernetes_namespace" "authentik" { + metadata { + name = "authentik" + labels = { + tier = var.tier + "resource-governance/custom-quota" = "true" + } + } +} + +resource "kubernetes_resource_quota" "authentik" { + metadata { + name = "authentik-quota" + namespace = kubernetes_namespace.authentik.metadata[0].name + } + spec { + hard = { + "requests.cpu" = "16" + "requests.memory" = "16Gi" + "limits.memory" = "96Gi" + pods = "50" + } + } +} + +resource "helm_release" "authentik" { + namespace = kubernetes_namespace.authentik.metadata[0].name + create_namespace = true + name = "goauthentik" + + repository = "https://charts.goauthentik.io/" + chart = "authentik" + # version = "2025.8.1" + version = "2025.10.3" + atomic = true + timeout = 6000 + + values = [templatefile("${path.module}/values.yaml", { postgres_password = var.postgres_password, secret_key = var.secret_key, redis_host = var.redis_host })] +} + + +module "ingress" { + source = "../../../../modules/kubernetes/ingress_factory" + namespace = kubernetes_namespace.authentik.metadata[0].name + name = "authentik" + service_name = "goauthentik-server" + tls_secret_name = var.tls_secret_name + extra_annotations = { + "gethomepage.dev/enabled" = "true" + "gethomepage.dev/name" = "Authentik" + "gethomepage.dev/description" = "Identity provider" + "gethomepage.dev/icon" = "authentik.png" + "gethomepage.dev/group" = "Identity & Security" + "gethomepage.dev/pod-selector" = "" + "gethomepage.dev/widget.type" = "authentik" + "gethomepage.dev/widget.url" = "http://goauthentik-server.authentik.svc.cluster.local" + "gethomepage.dev/widget.key" = var.homepage_token + } +} + +module "ingress-outpost" { + source = "../../../../modules/kubernetes/ingress_factory" + namespace = kubernetes_namespace.authentik.metadata[0].name + name = "authentik-outpost" + host = "authentik" + service_name = "ak-outpost-authentik-embedded-outpost" + port = 9000 + ingress_path = ["/outpost.goauthentik.io"] + tls_secret_name = var.tls_secret_name +} diff --git a/stacks/authentik/modules/authentik/pgbouncer.ini b/stacks/authentik/modules/authentik/pgbouncer.ini new file mode 100644 index 00000000..e3dc4e4f --- /dev/null +++ b/stacks/authentik/modules/authentik/pgbouncer.ini @@ -0,0 +1,14 @@ +[databases] +authentik = host=postgresql.dbaas port=5432 dbname=authentik user=authentik password=${password} + +[pgbouncer] +listen_addr = 0.0.0.0 +listen_port = 6432 +auth_type = md5 +auth_file = /etc/pgbouncer/userlist.txt +pool_mode = transaction +max_client_conn = 200 +default_pool_size = 20 +reserve_pool_size = 5 +reserve_pool_timeout = 5 +ignore_startup_parameters = extra_float_digits diff --git a/stacks/authentik/modules/authentik/pgbouncer.tf b/stacks/authentik/modules/authentik/pgbouncer.tf new file mode 100644 index 00000000..16126a16 --- /dev/null +++ b/stacks/authentik/modules/authentik/pgbouncer.tf @@ -0,0 +1,140 @@ +resource "kubernetes_config_map" "pgbouncer_config" { + metadata { + name = "pgbouncer-config" + namespace = "authentik" + } + + data = { + "pgbouncer.ini" = templatefile("${path.module}/pgbouncer.ini", { password = var.postgres_password }) + } +} + +# --- 2️⃣ Secret for user credentials --- +resource "kubernetes_secret" "pgbouncer_auth" { + metadata { + name = "pgbouncer-auth" + namespace = "authentik" + } + + data = { + "userlist.txt" = templatefile("${path.module}/userlist.txt", { password = var.postgres_password }) + } + + type = "Opaque" +} + +# --- 3️⃣ Deployment --- +resource "kubernetes_deployment" "pgbouncer" { + metadata { + name = "pgbouncer" + namespace = "authentik" + labels = { + app = "pgbouncer" + tier = var.tier + } + } + + spec { + replicas = 3 + + selector { + match_labels = { + app = "pgbouncer" + } + } + + template { + metadata { + labels = { + app = "pgbouncer" + } + } + + spec { + affinity { + pod_anti_affinity { + required_during_scheduling_ignored_during_execution { + label_selector { + match_expressions { + key = "component" + operator = "In" + values = ["server"] + } + } + topology_key = "kubernetes.io/hostname" + } + } + } + container { + name = "pgbouncer" + image = "edoburu/pgbouncer:latest" + image_pull_policy = "IfNotPresent" + + port { + container_port = 6432 + } + + volume_mount { + name = "config" + mount_path = "/etc/pgbouncer/pgbouncer.ini" + sub_path = "pgbouncer.ini" + } + + volume_mount { + name = "auth" + mount_path = "/etc/pgbouncer/userlist.txt" + sub_path = "userlist.txt" + } + + env { + name = "DATABASES_AUTHENTIK" + value = "host=postgres port=5432 dbname=authentik user=authentik password=${var.postgres_password}" + } + } + + volume { + name = "config" + config_map { + name = kubernetes_config_map.pgbouncer_config.metadata[0].name + } + } + + volume { + name = "auth" + secret { + secret_name = kubernetes_secret.pgbouncer_auth.metadata[0].name + } + } + dns_config { + option { + name = "ndots" + value = "2" + } + } + } + } + } + depends_on = [kubernetes_secret.pgbouncer_auth] +} + +# --- 4️⃣ Service --- +resource "kubernetes_service" "pgbouncer" { + metadata { + name = "pgbouncer" + namespace = "authentik" + } + + spec { + selector = { + app = "pgbouncer" + } + + port { + port = 6432 + target_port = 6432 + protocol = "TCP" + } + + type = "ClusterIP" + } +} diff --git a/stacks/authentik/modules/authentik/userlist.txt b/stacks/authentik/modules/authentik/userlist.txt new file mode 100644 index 00000000..024c3864 --- /dev/null +++ b/stacks/authentik/modules/authentik/userlist.txt @@ -0,0 +1 @@ +"authentik" "${password}" diff --git a/stacks/authentik/modules/authentik/values.yaml b/stacks/authentik/modules/authentik/values.yaml new file mode 100644 index 00000000..e50719e9 --- /dev/null +++ b/stacks/authentik/modules/authentik/values.yaml @@ -0,0 +1,73 @@ +authentik: + log_level: warning + # log_level: trace + secret_key: "${secret_key}" + # This sends anonymous usage-data, stack traces on errors and + # performance data to authentik.error-reporting.a7k.io, and is fully opt-in + error_reporting: + enabled: true + postgresql: + # host: postgresql.dbaas + host: pgbouncer.authentik + port: 6432 + user: authentik + password: ${postgres_password} + redis: + host: ${redis_host} + +server: + replicas: 3 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 0 + maxUnavailable: 1 + resources: + requests: + cpu: 100m + memory: 1Gi + limits: + memory: 1Gi + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app.kubernetes.io/component: server + ingress: + enabled: false + # hosts: + # - authentik.viktorbarzin.me + podAnnotations: + diun.enable: true + diun.include_tags: "^202[0-9].[0-9]+.*$" # no need to annotate the worker as it uses the same image + pdb: + enabled: true + minAvailable: 2 +global: + addPrometheusAnnotations: true + +worker: + replicas: 3 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 0 + maxUnavailable: 1 + resources: + requests: + cpu: 100m + memory: 896Mi + limits: + memory: 896Mi + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app.kubernetes.io/component: worker + pdb: + enabled: true + maxUnavailable: 1 diff --git a/stacks/authentik/secrets b/stacks/authentik/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/authentik/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/authentik/terragrunt.hcl b/stacks/authentik/terragrunt.hcl new file mode 100644 index 00000000..4f16dddf --- /dev/null +++ b/stacks/authentik/terragrunt.hcl @@ -0,0 +1,8 @@ +include "root" { + path = find_in_parent_folders() +} + +dependency "infra" { + config_path = "../infra" + skip_outputs = true +} diff --git a/stacks/authentik/tiers.tf b/stacks/authentik/tiers.tf new file mode 100644 index 00000000..eb0f8083 --- /dev/null +++ b/stacks/authentik/tiers.tf @@ -0,0 +1,10 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +locals { + tiers = { + core = "0-core" + cluster = "1-cluster" + gpu = "2-gpu" + edge = "3-edge" + aux = "4-aux" + } +} diff --git a/stacks/crowdsec/main.tf b/stacks/crowdsec/main.tf new file mode 100644 index 00000000..9ab93237 --- /dev/null +++ b/stacks/crowdsec/main.tf @@ -0,0 +1,30 @@ +# ============================================================================= +# CrowdSec Stack — Security/WAF +# ============================================================================= + +variable "tls_secret_name" { type = string } +variable "mysql_host" { type = string } + +data "vault_kv_secret_v2" "secrets" { + mount = "secret" + name = "platform" +} + +locals { + homepage_credentials = jsondecode(data.vault_kv_secret_v2.secrets.data["homepage_credentials"]) +} + +module "crowdsec" { + source = "./modules/crowdsec" + tier = local.tiers.cluster + tls_secret_name = var.tls_secret_name + mysql_host = var.mysql_host + homepage_username = local.homepage_credentials["crowdsec"]["username"] + homepage_password = local.homepage_credentials["crowdsec"]["password"] + enroll_key = data.vault_kv_secret_v2.secrets.data["crowdsec_enroll_key"] + db_password = data.vault_kv_secret_v2.secrets.data["crowdsec_db_password"] + crowdsec_dash_api_key = data.vault_kv_secret_v2.secrets.data["crowdsec_dash_api_key"] + crowdsec_dash_machine_id = data.vault_kv_secret_v2.secrets.data["crowdsec_dash_machine_id"] + crowdsec_dash_machine_password = data.vault_kv_secret_v2.secrets.data["crowdsec_dash_machine_password"] + slack_webhook_url = data.vault_kv_secret_v2.secrets.data["alertmanager_slack_api_url"] +} diff --git a/stacks/crowdsec/modules/crowdsec/crowdsec-ingress-bouncer.yaml b/stacks/crowdsec/modules/crowdsec/crowdsec-ingress-bouncer.yaml new file mode 100644 index 00000000..fe8ba008 --- /dev/null +++ b/stacks/crowdsec/modules/crowdsec/crowdsec-ingress-bouncer.yaml @@ -0,0 +1,44 @@ +controller: + extraVolumes: + - name: crowdsec-bouncer-plugin + emptyDir: {} + extraInitContainers: + - name: init-clone-crowdsec-bouncer + image: crowdsecurity/lua-bouncer-plugin + imagePullPolicy: IfNotPresent + env: + - name: API_URL + value: "http://crowdsec-service.crowdsec.svc.cluster.local:8080" # crowdsec lapi service-name + - name: API_KEY + value: "" # generated with `cscli bouncers add -n + - name: BOUNCER_CONFIG + value: "/crowdsec/crowdsec-bouncer.conf" + - name: CAPTCHA_PROVIDER + value: "recaptcha" # valid providers are recaptcha, hcaptcha, turnstile + - name: SECRET_KEY + value: "" # If you want captcha support otherwise remove this ENV VAR + - name: SITE_KEY + value: "" # If you want captcha support otherwise remove this ENV VAR + - name: BAN_TEMPLATE_PATH + value: /etc/nginx/lua/plugins/crowdsec/templates/ban.html + - name: CAPTCHA_TEMPLATE_PATH + value: /etc/nginx/lua/plugins/crowdsec/templates/captcha.html + command: + [ + "sh", + "-c", + "sh /docker_start.sh; mkdir -p /lua_plugins/crowdsec/; cp -R /crowdsec/* /lua_plugins/crowdsec/", + ] + volumeMounts: + - name: crowdsec-bouncer-plugin + mountPath: /lua_plugins + extraVolumeMounts: + - name: crowdsec-bouncer-plugin + mountPath: /etc/nginx/lua/plugins/crowdsec + subPath: crowdsec + config: + plugins: "crowdsec" + lua-shared-dicts: "crowdsec_cache: 50m" + server-snippet: | + lua_ssl_trusted_certificate "/etc/ssl/certs/ca-certificates.crt"; # If you want captcha support otherwise remove this line + resolver local=on ipv6=off; diff --git a/stacks/crowdsec/modules/crowdsec/main.tf b/stacks/crowdsec/modules/crowdsec/main.tf new file mode 100644 index 00000000..9141be3a --- /dev/null +++ b/stacks/crowdsec/modules/crowdsec/main.tf @@ -0,0 +1,376 @@ +variable "tls_secret_name" {} +variable "homepage_username" {} +variable "homepage_password" {} +variable "db_password" {} +variable "enroll_key" {} +variable "crowdsec_dash_api_key" { + type = string + sensitive = true +} +variable "crowdsec_dash_machine_id" { type = string } # used for web dash +variable "crowdsec_dash_machine_password" { + type = string + sensitive = true +} +variable "tier" { type = string } +variable "slack_webhook_url" { type = string } +variable "mysql_host" { type = string } + +module "tls_secret" { + source = "../../../../modules/kubernetes/setup_tls_secret" + namespace = kubernetes_namespace.crowdsec.metadata[0].name + tls_secret_name = var.tls_secret_name +} + +resource "kubernetes_namespace" "crowdsec" { + metadata { + name = "crowdsec" + labels = { + tier = var.tier + "resource-governance/custom-quota" = "true" + } + } +} + +resource "kubernetes_config_map" "crowdsec_custom_scenarios" { + metadata { + name = "crowdsec-custom-scenarios" + namespace = kubernetes_namespace.crowdsec.metadata[0].name + labels = { + "app.kubernetes.io/name" = "crowdsec" + } + } + + data = { + "http-403-abuse.yaml" = <<-YAML + type: leaky + name: crowdsecurity/http-403-abuse + description: "Detect IPs triggering too many HTTP 403s in NGINX ingress logs" + filter: "evt.Meta.log_type == 'http_access-log' && evt.Parsed.status == '403'" + groupby: "evt.Meta.source_ip" + leakspeed: "2s" + capacity: 10 + blackhole: 5m + labels: + service: http + behavior: abusive_403 + remediation: true + YAML + "http-429-abuse.yaml" : <<-YAML + type: leaky + name: crowdsecurity/http-429-abuse + description: "Detect IPs repeatedly triggering rate-limit (HTTP 429)" + filter: "evt.Meta.log_type == 'http_access-log' && evt.Parsed.status == '429'" + groupby: "evt.Meta.source_ip" + leakspeed: "10s" + capacity: 5 + blackhole: 1m + labels: + service: http + behavior: rate_limit_abuse + remediation: true + YAML + } +} + +# Whitelist for trusted IPs that should never be blocked +resource "kubernetes_config_map" "crowdsec_whitelist" { + metadata { + name = "crowdsec-whitelist" + namespace = kubernetes_namespace.crowdsec.metadata[0].name + labels = { + "app.kubernetes.io/name" = "crowdsec" + } + } + + data = { + "whitelist.yaml" = <<-YAML + name: crowdsecurity/whitelist-trusted-ips + description: "Whitelist for trusted IPs that should never be blocked" + whitelist: + reason: "Trusted IP - never block" + ip: + - "176.12.22.76" + YAML + } +} + + +resource "helm_release" "crowdsec" { + namespace = kubernetes_namespace.crowdsec.metadata[0].name + create_namespace = true + name = "crowdsec" + atomic = true + version = "0.21.0" + + repository = "https://crowdsecurity.github.io/helm-charts" + chart = "crowdsec" + + values = [templatefile("${path.module}/values.yaml", { homepage_username = var.homepage_username, homepage_password = var.homepage_password, DB_PASSWORD = var.db_password, ENROLL_KEY = var.enroll_key, SLACK_WEBHOOK_URL = var.slack_webhook_url, mysql_host = var.mysql_host })] + timeout = 900 + wait = true + wait_for_jobs = true +} + + +# Deployment for my custom dashboard that helps me unblock myself when I blocklist myself +resource "kubernetes_deployment" "crowdsec-web" { + metadata { + name = "crowdsec-web" + namespace = kubernetes_namespace.crowdsec.metadata[0].name + labels = { + app = "crowdsec_web" + "kubernetes.io/cluster-service" = "true" + tier = var.tier + } + } + spec { + replicas = 1 + strategy { + type = "RollingUpdate" + } + selector { + match_labels = { + app = "crowdsec_web" + } + } + template { + metadata { + labels = { + app = "crowdsec_web" + "kubernetes.io/cluster-service" = "true" + } + } + spec { + priority_class_name = "tier-1-cluster" + container { + name = "crowdsec-web" + image = "viktorbarzin/crowdsec_web" + env { + name = "CS_API_URL" + value = "http://crowdsec-service.crowdsec.svc.cluster.local:8080/v1" + } + env { + name = "CS_API_KEY" + value = var.crowdsec_dash_api_key + } + env { + name = "CS_MACHINE_ID" + value = var.crowdsec_dash_machine_id + } + env { + name = "CS_MACHINE_PASSWORD" + value = var.crowdsec_dash_machine_password + } + port { + name = "http" + container_port = 8000 + protocol = "TCP" + } + resources { + requests = { + cpu = "15m" + memory = "128Mi" + } + limits = { + memory = "128Mi" + } + } + } + dns_config { + option { + name = "ndots" + value = "2" + } + } + } + } + } +} + +resource "kubernetes_service" "crowdsec-web" { + metadata { + name = "crowdsec-web" + namespace = kubernetes_namespace.crowdsec.metadata[0].name + labels = { + "app" = "crowdsec_web" + } + } + + spec { + selector = { + app = "crowdsec_web" + } + port { + port = "80" + target_port = "8000" + } + } +} +module "ingress" { + source = "../../../../modules/kubernetes/ingress_factory" + namespace = kubernetes_namespace.crowdsec.metadata[0].name + name = "crowdsec-web" + protected = true + tls_secret_name = var.tls_secret_name + exclude_crowdsec = true + rybbit_site_id = "d09137795ccc" +} + +# CronJob to import public blocklists into CrowdSec +# https://github.com/wolffcatskyy/crowdsec-blocklist-import +# Uses kubectl exec to run in an existing CrowdSec agent pod that's already registered +resource "kubernetes_cron_job_v1" "crowdsec_blocklist_import" { + metadata { + name = "crowdsec-blocklist-import" + namespace = kubernetes_namespace.crowdsec.metadata[0].name + labels = { + app = "crowdsec-blocklist-import" + tier = var.tier + } + } + + spec { + # Run daily at 4 AM + schedule = "0 4 * * *" + timezone = "Europe/London" + concurrency_policy = "Forbid" + successful_jobs_history_limit = 3 + failed_jobs_history_limit = 3 + + job_template { + metadata { + labels = { + app = "crowdsec-blocklist-import" + } + } + + spec { + backoff_limit = 3 + template { + metadata { + labels = { + app = "crowdsec-blocklist-import" + } + } + + spec { + service_account_name = kubernetes_service_account.blocklist_import.metadata[0].name + restart_policy = "OnFailure" + + container { + name = "blocklist-import" + image = "bitnami/kubectl:latest" + + command = ["/bin/bash", "-c"] + args = [ + <<-EOF + set -e + + echo "Finding CrowdSec agent pod..." + AGENT_POD=$(kubectl get pods -n crowdsec -l k8s-app=crowdsec,type=agent -o jsonpath='{.items[0].metadata.name}') + + if [ -z "$AGENT_POD" ]; then + echo "ERROR: Could not find CrowdSec agent pod" + exit 1 + fi + + echo "Using agent pod: $AGENT_POD" + + # Download the import script + echo "Downloading blocklist import script..." + curl -fsSL -o /tmp/import.sh \ + https://raw.githubusercontent.com/wolffcatskyy/crowdsec-blocklist-import/main/import.sh + chmod +x /tmp/import.sh + + # Copy script to agent pod and execute + echo "Copying script to agent pod and executing..." + kubectl cp /tmp/import.sh crowdsec/$AGENT_POD:/tmp/import.sh + + kubectl exec -n crowdsec "$AGENT_POD" -- /bin/bash -c ' + set -e + + # Run with native mode since we are inside the CrowdSec container + export MODE=native + export DECISION_DURATION=24h + export FETCH_TIMEOUT=60 + export LOG_LEVEL=INFO + + /tmp/import.sh + + # Cleanup + rm -f /tmp/import.sh + ' + + echo "Blocklist import completed successfully!" + EOF + ] + } + } + } + } + } + } +} + +# Service account for the blocklist import job (needs kubectl exec permissions) +resource "kubernetes_service_account" "blocklist_import" { + metadata { + name = "crowdsec-blocklist-import" + namespace = kubernetes_namespace.crowdsec.metadata[0].name + } +} + +resource "kubernetes_role" "blocklist_import" { + metadata { + name = "crowdsec-blocklist-import" + namespace = kubernetes_namespace.crowdsec.metadata[0].name + } + + rule { + api_groups = [""] + resources = ["pods"] + verbs = ["get", "list"] + } + rule { + api_groups = [""] + resources = ["pods/exec"] + verbs = ["create"] + } +} + +resource "kubernetes_role_binding" "blocklist_import" { + metadata { + name = "crowdsec-blocklist-import" + namespace = kubernetes_namespace.crowdsec.metadata[0].name + } + + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "Role" + name = kubernetes_role.blocklist_import.metadata[0].name + } + + subject { + kind = "ServiceAccount" + name = kubernetes_service_account.blocklist_import.metadata[0].name + namespace = kubernetes_namespace.crowdsec.metadata[0].name + } +} + +# Custom ResourceQuota for CrowdSec — needs more than default 1-cluster quota +# because it runs DaemonSet agents (1 per worker node) + 3 LAPI replicas + web UI +resource "kubernetes_resource_quota" "crowdsec" { + metadata { + name = "crowdsec-quota" + namespace = kubernetes_namespace.crowdsec.metadata[0].name + } + spec { + hard = { + "requests.cpu" = "4" + "requests.memory" = "8Gi" + "limits.memory" = "16Gi" + pods = "30" + } + } +} diff --git a/stacks/crowdsec/modules/crowdsec/values.yaml b/stacks/crowdsec/modules/crowdsec/values.yaml new file mode 100644 index 00000000..fcfbb3af --- /dev/null +++ b/stacks/crowdsec/modules/crowdsec/values.yaml @@ -0,0 +1,226 @@ +# values from - https://github.com/crowdsecurity/helm-charts/blob/main/charts/crowdsec/values.yaml +container_runtime: containerd + +agent: + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + memory: 512Mi + priorityClassName: "tier-1-cluster" + # To specify each pod you want to process it logs (pods present in the node) + acquisition: + # The namespace where the pod is located + - namespace: traefik + # The pod name + podName: traefik-* + # as in crowdsec configuration, we need to specify the program name so the parser will match and parse logs + program: traefik + # Those are ENV variables + env: + # As it's a test, we don't want to share signals with CrowdSec so disable the Online API. + # - name: DISABLE_ONLINE_API + # value: "true" + # As we are running Traefik, we want to install the Traefik collection + - name: COLLECTIONS + value: "crowdsecurity/traefik crowdsecurity/base-http-scenarios crowdsecurity/http-cve" + - name: SCENARIOS + value: "" + # value: "crowdsecurity/http-crawl-aggressive" + # Mount custom scenarios into /etc/crowdsec/scenarios + extraVolumeMounts: + - name: custom-scenarios + mountPath: /etc/crowdsec/scenarios/http-403-abuse.yaml + subPath: "http-403-abuse.yaml" + readonly: true + - name: custom-scenarios + mountPath: /etc/crowdsec/scenarios/http-429-abuse.yaml + subPath: "http-429-abuse.yaml" + readonly: true + - name: whitelist + mountPath: /etc/crowdsec/parsers/s02-enrich/whitelist.yaml + subPath: "whitelist.yaml" + readonly: true + extraVolumes: + - name: custom-scenarios + configMap: + name: crowdsec-custom-scenarios + - name: whitelist + configMap: + name: crowdsec-whitelist +lapi: + resources: + requests: + cpu: 25m + memory: 128Mi + limits: + memory: 1Gi + startupProbe: + httpGet: + path: /health + port: 8080 + failureThreshold: 30 + periodSeconds: 10 + priorityClassName: "tier-1-cluster" + replicas: 3 + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app.kubernetes.io/name: crowdsec + type: lapi + pdb: + enabled: true + maxUnavailable: 1 + extraSecrets: + dbPassword: "${DB_PASSWORD}" + storeCAPICredentialsInSecret: true + persistentVolume: + config: + enabled: false + data: + enabled: false + env: + - name: ENROLL_KEY + value: "${ENROLL_KEY}" + - name: ENROLL_INSTANCE_NAME + value: "k8s-cluster" + - name: ENROLL_TAGS + value: "k8s linux" + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: crowdsec-lapi-secrets + key: dbPassword + # As it's a test, we don't want to share signals with CrowdSec, so disable the Online API. + # - name: DISABLE_ONLINE_API + # value: "true" + dashboard: + enabled: true + env: + - name: MB_DB_TYPE + value: "mysql" + - name: MB_DB_DBNAME + value: crowdsec-metabase + - name: MB_DB_USER + value: "crowdsec" + - name: MB_DB_PASS + value: "${DB_PASSWORD}" + - name: MB_DB_HOST + value: "${mysql_host}" + + - name: MB_EMAIL_SMTP_USERNAME + value: "info@viktorbarzin.me" + - name: MB_EMAIL_FROM_ADDRESS + value: "info@viktorbarzin.me" + - name: MB_EMAIL_SMTP_HOST + value: "mailserver.mailserver.svc.cluster.local" + - name: MB_EMAIL_SMTP_PASSWORD + value: "" # Ignore for now as it's unclear what notifications we can get + - name: MB_EMAIL_SMTP_PORT + value: "587" + - name: MB_EMAIL_SMTP_SECURITY + value: "starttls" + ingress: + enabled: true + annotations: + nginx.ingress.kubernetes.io/backend-protocol: "HTTP" + #nginx.ingress.kubernetes.io/auth-url: "https://oauth2.viktorbarzin.me/oauth2/auth" + nginx.ingress.kubernetes.io/auth-url: "http://ak-outpost-authentik-embedded-outpost.authentik.svc.cluster.local:9000/outpost.goauthentik.io/auth/nginx" + # nginx.ingress.kubernetes.io/auth-signin: "https://oauth2.viktorbarzin.me/oauth2/start?rd=/redirect/$http_host$escaped_request_uri" + nginx.ingress.kubernetes.io/auth-signin: "https://authentik.viktorbarzin.me/outpost.goauthentik.io/start?rd=$scheme%3A%2F%2F$host$escaped_request_uri" + nginx.ingress.kubernetes.io/auth-response-headers: "Set-Cookie,X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid" + nginx.ingress.kubernetes.io/auth-snippet: "proxy_set_header X-Forwarded-Host $http_host;" + gethomepage.dev/enabled: "true" + gethomepage.dev/description: "Web Application Firewall" + gethomepage.dev/icon: "crowdsec.png" + gethomepage.dev/name: "CrowdSec" + gethomepage.dev/group: "Identity & Security" + gethomepage.dev/widget.type: "crowdsec" + gethomepage.dev/widget.url: "http://crowdsec-service.crowdsec.svc.cluster.local:8080" + gethomepage.dev/widget.username: "${homepage_username}" + gethomepage.dev/widget.password: "${homepage_password}" + gethomepage.dev/pod-selector: "" + ingressClassName: "nginx" + host: "crowdsec.viktorbarzin.me" + tls: + - hosts: + - crowdsec.viktorbarzin.me + secretName: "tls-secret" + metrics: + enabled: true + strategy: + type: RollingUpdate + +config: + # Custom profiles: captcha for rate limiting, ban for attacks + profiles.yaml: | + # Captcha for rate limiting and 403 abuse (user can unblock themselves) + name: captcha_remediation + filters: + - Alert.Remediation == true && Alert.GetScope() == "Ip" && Alert.GetScenario() in ["crowdsecurity/http-429-abuse", "crowdsecurity/http-403-abuse", "crowdsecurity/http-crawl-non_statics", "crowdsecurity/http-sensitive-files"] + decisions: + - type: captcha + duration: 4h + notifications: + - slack_alerts + on_success: break + --- + # Default: Ban for serious attacks (CVE exploits, scanners, brute force) + name: default_ip_remediation + filters: + - Alert.Remediation == true && Alert.GetScope() == "Ip" + decisions: + - type: ban + duration: 4h + notifications: + - slack_alerts + on_success: break + --- + name: default_range_remediation + filters: + - Alert.Remediation == true && Alert.GetScope() == "Range" + decisions: + - type: ban + duration: 4h + notifications: + - slack_alerts + on_success: break + + config.yaml.local: | + db_config: + type: mysql + user: crowdsec + password: ${DB_PASSWORD} + db_name: crowdsec + host: ${mysql_host} + port: 3306 + api: + server: + auto_registration: # Activate if not using TLS for authentication + enabled: true + token: "$${REGISTRATION_TOKEN}" # /!\ do not change + allowed_ranges: # /!\ adapt to the pod IP ranges used by your cluster + - "127.0.0.1/32" + - "192.168.0.0/16" + - "10.0.0.0/8" + - "172.16.0.0/12" + + notifications: + slack.yaml: | + type: slack + name: slack_alerts + log_level: info + format: | + :rotating_light: *CrowdSec Alert* + {{range .}} + *Scenario:* {{.Alert.Scenario}} + *Source IP:* {{.Alert.Source.IP}} ({{.Alert.Source.Cn}}) + *Decisions:* + {{range .Alert.Decisions}} - {{.Type}} for {{.Duration}} (scope: {{.Scope}}, value: {{.Value}}) + {{end}} + {{end}} + webhook: ${SLACK_WEBHOOK_URL} diff --git a/stacks/crowdsec/secrets b/stacks/crowdsec/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/crowdsec/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/crowdsec/terragrunt.hcl b/stacks/crowdsec/terragrunt.hcl new file mode 100644 index 00000000..4f16dddf --- /dev/null +++ b/stacks/crowdsec/terragrunt.hcl @@ -0,0 +1,8 @@ +include "root" { + path = find_in_parent_folders() +} + +dependency "infra" { + config_path = "../infra" + skip_outputs = true +} diff --git a/stacks/crowdsec/tiers.tf b/stacks/crowdsec/tiers.tf new file mode 100644 index 00000000..eb0f8083 --- /dev/null +++ b/stacks/crowdsec/tiers.tf @@ -0,0 +1,10 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +locals { + tiers = { + core = "0-core" + cluster = "1-cluster" + gpu = "2-gpu" + edge = "3-edge" + aux = "4-aux" + } +} diff --git a/stacks/dbaas/main.tf b/stacks/dbaas/main.tf new file mode 100644 index 00000000..9825da78 --- /dev/null +++ b/stacks/dbaas/main.tf @@ -0,0 +1,27 @@ +# ============================================================================= +# DBaaS Stack — MySQL + PostgreSQL + pgAdmin +# ============================================================================= + +variable "tls_secret_name" { type = string } +variable "nfs_server" { type = string } +variable "prod" { + type = bool + default = false +} + +data "vault_kv_secret_v2" "secrets" { + mount = "secret" + name = "platform" +} + +module "dbaas" { + source = "./modules/dbaas" + prod = var.prod + tls_secret_name = var.tls_secret_name + nfs_server = var.nfs_server + dbaas_root_password = data.vault_kv_secret_v2.secrets.data["dbaas_root_password"] + postgresql_root_password = data.vault_kv_secret_v2.secrets.data["dbaas_postgresql_root_password"] + pgadmin_password = data.vault_kv_secret_v2.secrets.data["dbaas_pgadmin_password"] + kube_config_path = var.kube_config_path + tier = local.tiers.cluster +} diff --git a/stacks/dbaas/modules/dbaas/chart_values.tpl b/stacks/dbaas/modules/dbaas/chart_values.tpl new file mode 100644 index 00000000..06062b62 --- /dev/null +++ b/stacks/dbaas/modules/dbaas/chart_values.tpl @@ -0,0 +1,16 @@ +tls: + useSelfSigned: true +credentials: + root: + password: ${root_password} + user: root +serverInstances: 1 +podSpec: + containers: + - name: mysql + resources: + requests: + memory: "1024Mi" # adapt to your needs + cpu: "100m" # adapt to your needs + limits: + memory: "2048Mi" # adapt to your needs diff --git a/stacks/dbaas/modules/dbaas/cluster.yaml b/stacks/dbaas/modules/dbaas/cluster.yaml new file mode 100644 index 00000000..9659b05f --- /dev/null +++ b/stacks/dbaas/modules/dbaas/cluster.yaml @@ -0,0 +1,30 @@ +apiVersion: mysql.presslabs.org/v1alpha1 +kind: MysqlCluster +metadata: + name: mysql-cluster + namespace: dbaas +spec: + mysqlVersion: "5.7" + replicas: 1 + secretName: cluster-secret + mysqlConf: + # read_only: 0 # mysql forms a single transaction for each sql statement, autocommit for each statement + # automatic_sp_privileges: "ON" # automatically grants the EXECUTE and ALTER ROUTINE privileges to the creator of a stored routine + # auto_generate_certs: "ON" # Auto Generation of Certificate + # auto_increment_increment: 1 # Auto Incrementing value from +1 + # auto_increment_offset: 1 # Auto Increment Offset + # binlog-format: "STATEMENT" # contains various options such ROW(SLOW,SAFE) STATEMENT(FAST,UNSAFE), MIXED(combination of both) + # wait_timeout: 31536000 # 28800 number of seconds the server waits for activity on a non-interactive connection before closing it, You might encounter MySQL server has gone away error, you then tweak this value acccordingly + # interactive_timeout: 28800 # The number of seconds the server waits for activity on an interactive connection before closing it. + # max_allowed_packet: "512M" # Maximum size of MYSQL Network protocol packet that the server can create or read 4MB, 8MB, 16MB, 32MB + # max-binlog-size: 1073741824 # binary logs contains the events that describe database changes, this parameter describe size for the bin_log file. + # log_output: "TABLE" # Format in which the logout will be dumped + # master-info-repository: "TABLE" # Format in which the master info will be dumped + # relay_log_info_repository: "TABLE" # Format in which the relay info will be dumped + volumeSpec: + persistentVolumeClaim: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi diff --git a/stacks/dbaas/modules/dbaas/main.tf b/stacks/dbaas/modules/dbaas/main.tf new file mode 100644 index 00000000..1c94052e --- /dev/null +++ b/stacks/dbaas/modules/dbaas/main.tf @@ -0,0 +1,1091 @@ +# DB as a service. Installs MySQL operator +variable "tls_secret_name" {} +variable "tier" { type = string } +variable "dbaas_root_password" {} +variable "cluster_master_service" { + default = "mysql" +} +variable "postgresql_root_password" {} +variable "pgadmin_password" {} +variable "prod" { + default = false + type = bool +} +variable "nfs_server" { type = string } +variable "kube_config_path" { + type = string + sensitive = true +} + +resource "kubernetes_namespace" "dbaas" { + metadata { + name = "dbaas" + labels = { + tier = var.tier + "resource-governance/custom-quota" = "true" + } + } +} + +resource "kubernetes_resource_quota" "dbaas" { + metadata { + name = "dbaas-quota" + namespace = kubernetes_namespace.dbaas.metadata[0].name + } + spec { + hard = { + "requests.cpu" = "8" + "requests.memory" = "20Gi" + "limits.memory" = "20Gi" + pods = "30" + } + } +} + +module "tls_secret" { + source = "../../../../modules/kubernetes/setup_tls_secret" + namespace = kubernetes_namespace.dbaas.metadata[0].name + tls_secret_name = var.tls_secret_name +} + + +#### MYSQL — InnoDB Cluster via MySQL Operator +# +# 3 MySQL servers with Group Replication + 1 MySQL Router for auto-failover. +# Operator installed in mysql-operator namespace (toleration for control-plane). +# Init containers are slow (~20 min each) due to mysqlsh plugin loading. + +resource "kubernetes_namespace" "mysql_operator" { + metadata { + name = "mysql-operator" + labels = { + tier = "1-cluster" + } + } +} + +resource "helm_release" "mysql_operator" { + namespace = kubernetes_namespace.mysql_operator.metadata[0].name + create_namespace = false + name = "mysql-operator" + timeout = 300 + + repository = "https://mysql.github.io/mysql-operator/" + chart = "mysql-operator" + version = "2.2.7" + + values = [yamlencode({ + resources = { + requests = { + cpu = "100m" + memory = "512Mi" + } + limits = { + memory = "512Mi" + } + } + })] +} + +# The mysql-sidecar ClusterRole created by the Helm chart is missing +# namespace and CRD list/watch permissions needed by the kopf framework +# in the sidecar container. Without these, the sidecar enters degraded +# mode and never completes InnoDB cluster join operations. +resource "kubernetes_cluster_role" "mysql_sidecar_extra" { + metadata { + name = "mysql-sidecar-extra" + } + rule { + api_groups = [""] + resources = ["namespaces"] + verbs = ["list", "watch"] + } + rule { + api_groups = ["apiextensions.k8s.io"] + resources = ["customresourcedefinitions"] + verbs = ["list", "watch"] + } +} + +resource "kubernetes_cluster_role_binding" "mysql_sidecar_extra" { + metadata { + name = "mysql-sidecar-extra" + } + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "ClusterRole" + name = kubernetes_cluster_role.mysql_sidecar_extra.metadata[0].name + } + subject { + kind = "ServiceAccount" + name = "mysql-cluster-sa" + namespace = kubernetes_namespace.dbaas.metadata[0].name + } +} + +resource "helm_release" "mysql_cluster" { + namespace = kubernetes_namespace.dbaas.metadata[0].name + create_namespace = false + name = "mysql-cluster" + timeout = 900 + + repository = "https://mysql.github.io/mysql-operator/" + chart = "mysql-innodbcluster" + version = "2.2.7" + + values = [yamlencode({ + serverInstances = 3 + routerInstances = 1 + serverVersion = "8.4.4" + + credentials = { + root = { + user = "root" + password = var.dbaas_root_password + host = "%" + } + } + + tls = { + useSelfSigned = true + } + + datadirVolumeClaimTemplate = { + storageClassName = "iscsi-truenas" + resources = { + requests = { + storage = "30Gi" + } + } + } + + serverConfig = { + "my.cnf" = <<-EOT + [mysqld] + skip-name-resolve + # Auto-recovery after crashes: rejoin group without manual intervention + group_replication_autorejoin_tries=2016 + group_replication_exit_state_action=OFFLINE_MODE + group_replication_member_expel_timeout=30 + group_replication_unreachable_majority_timeout=60 + group_replication_start_on_boot=ON + # Cap XCom cache to prevent unbounded growth (default 1GB causes OOM) + group_replication_message_cache_size=134217728 + # Reduce log buffer (16MB sufficient for this workload, was 64MB) + innodb_log_buffer_size=16777216 + # Limit connections (peak usage ~40, no need for 151) + max_connections=80 + EOT + } + + resources = { + requests = { + cpu = "250m" + memory = "2Gi" + } + limits = { + memory = "4Gi" + } + } + + podSpec = { + affinity = { + nodeAffinity = { + requiredDuringSchedulingIgnoredDuringExecution = { + nodeSelectorTerms = [{ + matchExpressions = [{ + key = "kubernetes.io/hostname" + operator = "NotIn" + values = ["k8s-node1", "k8s-node2"] + }] + }] + } + } + podAntiAffinity = { + requiredDuringSchedulingIgnoredDuringExecution = [{ + labelSelector = { + matchLabels = { + "component" = "mysqld" + } + } + topologyKey = "kubernetes.io/hostname" + }] + } + } + containers = [{ + name = "mysql" + resources = { + requests = { + memory = "2Gi" + cpu = "250m" + } + limits = { + memory = "4Gi" + } + } + }] + initContainers = [ + { + name = "fixdatadir" + resources = { + requests = { memory = "64Mi", cpu = "25m" } + limits = { memory = "64Mi" } + } + }, + { + name = "initconf" + resources = { + requests = { memory = "256Mi", cpu = "50m" } + limits = { memory = "256Mi" } + } + }, + { + name = "initmysql" + resources = { + requests = { memory = "512Mi", cpu = "250m" } + limits = { memory = "512Mi" } + } + } + ] + } + })] + + depends_on = [helm_release.mysql_operator] +} + +# Compatibility service: mysql.dbaas points at InnoDB Cluster mysqld pods +# When router is available it handles failover, but we fall back to direct +# mysqld access to avoid total outage during partial cluster failures +resource "kubernetes_service" "mysql" { + metadata { + name = var.cluster_master_service + namespace = kubernetes_namespace.dbaas.metadata[0].name + } + spec { + publish_not_ready_addresses = true # bypass InnoDB Cluster readiness gate during partial failures + selector = { + "component" = "mysqld" + "mysql.oracle.com/cluster" = "mysql-cluster" + "mysql.oracle.com/cluster-role" = "PRIMARY" + } + port { + port = 3306 + target_port = 3306 + } + } + + depends_on = [helm_release.mysql_cluster] +} + +module "nfs_mysql_backup" { + source = "../../../../modules/kubernetes/nfs_volume" + name = "dbaas-mysql-backup" + namespace = kubernetes_namespace.dbaas.metadata[0].name + nfs_server = var.nfs_server + nfs_path = "/mnt/main/mysql-backup" +} + +module "nfs_pgadmin" { + source = "../../../../modules/kubernetes/nfs_volume" + name = "dbaas-pgadmin" + namespace = kubernetes_namespace.dbaas.metadata[0].name + nfs_server = var.nfs_server + nfs_path = "/mnt/main/postgresql/pgadmin" +} + +module "nfs_postgresql_backup" { + source = "../../../../modules/kubernetes/nfs_volume" + name = "dbaas-postgresql-backup" + namespace = kubernetes_namespace.dbaas.metadata[0].name + nfs_server = var.nfs_server + nfs_path = "/mnt/main/postgresql-backup" +} + +resource "kubernetes_cron_job_v1" "mysql-backup" { + metadata { + name = "mysql-backup" + namespace = kubernetes_namespace.dbaas.metadata[0].name + } + spec { + concurrency_policy = "Replace" + failed_jobs_history_limit = 5 + schedule = "0 0 * * *" + # schedule = "* * * * *" + starting_deadline_seconds = 10 + successful_jobs_history_limit = 10 + job_template { + metadata {} + spec { + backoff_limit = 3 + ttl_seconds_after_finished = 10 + template { + metadata {} + spec { + container { + name = "mysql-backup" + image = "mysql" + # TODO: would be nice to rotate at some point... Current size is 11MB so not really needed atm + command = ["/bin/bash", "-c", <<-EOT + set -euxo pipefail + export now=$(date +"%Y_%m_%d_%H_%M") + mysqldump --all-databases -u root -p${var.dbaas_root_password} --host mysql.dbaas.svc.cluster.local > /backup/dump_$now.sql + + # Rotate - delete last log file + cd /backup + find . -name "dump_*.sql" -type f -mtime +14 -delete # 14 day retention of backups + echo Done + EOT + ] + # To restore (from outside of the cluster): + # run kubectl port-forward to pod e.g.: + # > kb port-forward mysql-647cfd4969-46rmw --address 0.0.0.0 3307:3306 + # run mysql import (and specify non-localhost address to avoid using unix socket): (password is in tfvars) + # > mysql -u root -p --host 10.0.10.10 --port 3307 < /mnt/nfs/2024_01_06_13_54.sql + volume_mount { + name = "mysql-backup" + mount_path = "/backup" + } + } + volume { + name = "mysql-backup" + persistent_volume_claim { + claim_name = module.nfs_mysql_backup.claim_name + } + } + } + } + } + } + } +} + +# resource "kubernetes_persistent_volume" "mysql" { +# metadata { +# name = "mysql-pv" +# } +# spec { +# capacity = { +# "storage" = "10Gi" +# } +# access_modes = ["ReadWriteOnce"] +# persistent_volume_source { +# iscsi { +# target_portal = "iscsi.viktorbarzin.lan:3260" +# iqn = "iqn.2020-12.lan.viktorbarzin:storage:dbaas:mysql" +# lun = 0 +# fs_type = "ext4" +# } +# } +# } +# } + + +# resource "helm_release" "mysql" { +# namespace = kubernetes_namespace.dbaas.metadata[0].name +# create_namespace = false +# name = "mysql" + +# repository = "https://presslabs.github.io/charts" +# chart = "mysql-operator" +# # version = "v0.5.0-rc.3" + +# values = [templatefile("${path.module}/mysql_chart_values.yaml", { secretName = var.tls_secret_name })] +# atomic = true + +# depends_on = [kubernetes_namespace.dbaas] +# } + +# # resource "helm_release" "mysql" { +# # namespace = kubernetes_namespace.dbaas.metadata[0].name +# # create_namespace = false +# # name = "mysql-operator" + +# # repository = "https://mysql.github.io/mysql-operator/" +# # chart = "mysql-operator" +# # atomic = true +# # depends_on = [kubernetes_namespace.dbaas] +# # } + +# # resource "helm_release" "innodb-cluster" { +# # namespace = kubernetes_namespace.dbaas.metadata[0].name +# # create_namespace = false +# # name = var.cluster_master_service + +# # repository = "https://mysql.github.io/mysql-operator/" +# # chart = "mysql-innodbcluster" +# # atomic = true +# # depends_on = [kubernetes_namespace.dbaas] +# # values = [templatefile("${path.module}/chart_values.tpl", { root_password = var.dbaas_root_password })] +# # } + +# resource "kubernetes_persistent_volume" "mysql-operator" { +# metadata { +# name = "mysql-operator-pv" +# } +# spec { +# capacity = { +# "storage" = "1Gi" +# } +# access_modes = ["ReadWriteOnce"] +# persistent_volume_source { +# iscsi { +# target_portal = "iscsi.viktorbarzin.lan:3260" +# iqn = "iqn.2020-12.lan.viktorbarzin:storage:dbaas:operator" +# lun = 0 +# fs_type = "ext4" +# } +# } +# } +# } + +resource "kubernetes_secret" "cluster-password" { + metadata { + name = "cluster-secret" + namespace = kubernetes_namespace.dbaas.metadata[0].name + annotations = { + "reloader.stakater.com/match" = "true" + } + } + type = "Opaque" + data = { + "ROOT_PASSWORD" = var.dbaas_root_password + } +} + +# resource "kubernetes_ingress_v1" "dbaas" { +# metadata { +# name = "orchestrator-ingress" +# namespace = kubernetes_namespace.dbaas.metadata[0].name +# annotations = { +# "kubernetes.io/ingress.class" = "nginx" +# "nginx.ingress.kubernetes.io/auth-tls-verify-client" = "on" +# "nginx.ingress.kubernetes.io/auth-tls-secret" = "default/ca-secret" +# } +# } + +# spec { +# tls { +# hosts = ["db.viktorbarzin.me"] +# secret_name = var.tls_secret_name +# } +# rule { +# host = "db.viktorbarzin.me" +# http { +# path { +# path = "/" +# backend { +# service { +# name = "mysql-mysql-operator" +# port { +# number = 80 +# } +# } +# } +# } +# } +# } +# } +# } + + +# PHPMyAdmin instance +resource "kubernetes_deployment" "phpmyadmin" { + metadata { + name = "phpmyadmin" + namespace = kubernetes_namespace.dbaas.metadata[0].name + labels = { + "app" = "phpmyadmin" + tier = var.tier + + } + annotations = { + "reloader.stakater.com/search" = "true" + } + } + spec { + replicas = "1" + selector { + match_labels = { + "app" = "phpmyadmin" + } + } + template { + metadata { + labels = { + "app" = "phpmyadmin" + } + } + spec { + container { + name = "phpmyadmin" + image = "phpmyadmin/phpmyadmin:5.2.3" + port { + container_port = 80 + } + env { + name = "PMA_HOST" + value = var.cluster_master_service + } + env { + name = "PMA_PORT" + value = "3306" + } + env { + name = "MYSQL_ROOT_PASSWORD" + value_from { + secret_key_ref { + name = "cluster-secret" + key = "ROOT_PASSWORD" + } + } + } + env { + name = "UPLOAD_LIMIT" + value = "300M" + } + resources { + requests = { + cpu = "15m" + memory = "128Mi" + } + limits = { + memory = "128Mi" + } + } + } + dns_config { + option { + name = "ndots" + value = "2" + } + } + } + } + } +} + +resource "kubernetes_service" "phpmyadmin" { + metadata { + name = "pma" + namespace = kubernetes_namespace.dbaas.metadata[0].name + } + spec { + selector = { + "app" = "phpmyadmin" + } + port { + name = "web" + port = 80 + } + } +} +module "ingress" { + source = "../../../../modules/kubernetes/ingress_factory" + namespace = kubernetes_namespace.dbaas.metadata[0].name + name = "pma" + tls_secret_name = var.tls_secret_name + protected = true + extra_annotations = {} + rybbit_site_id = "942c76b8bd4d" + custom_content_security_policy = "script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' https://rybbit.viktorbarzin.me" +} + + +# resource "kubectl_manifest" "mysql-cluster" { +# yaml_body = <<-YAML +# apiVersion: mysql.presslabs.org/v1alpha1 +# kind: MysqlCluster +# metadata: +# name: mysql-cluster +# namespace = kubernetes_namespace.dbaas.metadata[0].name +# spec: +# mysqlVersion: "5.7" +# replicas: 1 +# secretName: cluster-secret +# mysqlConf: +# # read_only: 0 # mysql forms a single transaction for each sql statement, autocommit for each statement +# # automatic_sp_privileges: "ON" # automatically grants the EXECUTE and ALTER ROUTINE privileges to the creator of a stored routine +# # auto_generate_certs: "ON" # Auto Generation of Certificate +# # auto_increment_increment: 1 # Auto Incrementing value from +1 +# # auto_increment_offset: 1 # Auto Increment Offset +# # binlog-format: "STATEMENT" # contains various options such ROW(SLOW,SAFE) STATEMENT(FAST,UNSAFE), MIXED(combination of both) +# # wait_timeout: 31536000 # 28800 number of seconds the server waits for activity on a non-interactive connection before closing it, You might encounter MySQL server has gone away error, you then tweak this value acccordingly +# # interactive_timeout: 28800 # The number of seconds the server waits for activity on an interactive connection before closing it. +# # max_allowed_packet: "512M" # Maximum size of MYSQL Network protocol packet that the server can create or read 4MB, 8MB, 16MB, 32MB +# # max-binlog-size: 1073741824 # binary logs contains the events that describe database changes, this parameter describe size for the bin_log file. +# # log_output: "TABLE" # Format in which the logout will be dumped +# # master-info-repository: "TABLE" # Format in which the master info will be dumped +# # relay_log_info_repository: "TABLE" # Format in which the relay info will be dumped +# volumeSpec: +# persistentVolumeClaim: +# accessModes: +# - ReadWriteOnce +# resources: +# requests: +# storage: 10Gi +# YAML +# depends_on = [helm_release.mysql] +# # manifest = { +# # apiVersion = "mysql.presslabs.org/v1alpha1" +# # kind = "MysqlCluster" +# # metadata = { +# # name = "mysql-cluster" +# # namespace = kubernetes_namespace.dbaas.metadata[0].name +# # } +# # spec = { +# # mysqlVersion = "5.7" +# # replicas = 1 +# # secretName = "cluster-secret" +# # mysqlConf = { +# # read_only = 0 +# # } +# # volumeSpec = { +# # persistentVolumeClaim = { +# # resources = { +# # requests = { +# # storage = "10Gi" +# # } +# # } +# # } +# # } +# # } +# # } +# } + + +# For some unknwown reason not all CRDs are installed. Add them manually +# resource "kubectl_manifest" "mysql-user" { +# yaml_body = <<-EOF +# apiVersion: apiextensions.k8s.io/v1 +# kind: CustomResourceDefinition +# metadata: +# annotations: +# controller-gen.kubebuilder.io/version: v0.5.0 +# helm.sh/hook: crd-install +# name: mysqlusers.mysql.presslabs.org +# labels: +# app: mysql-operator +# spec: +# group: mysql.presslabs.org +# names: +# kind: MysqlUser +# listKind: MysqlUserList +# plural: mysqlusers +# singular: mysqluser +# scope:namespace = kubernetes_namespace.dbaas.metadata[0].name +# versions: +# - additionalPrinterColumns: +# - description: The user status +# jsonPath: .status.conditions[?(@.type == 'Ready')].status +# name: Ready +# type: string +# - jsonPath: .spec.clusterRef.name +# name: Cluster +# type: string +# - jsonPath: .spec.user +# name: UserName +# type: string +# - jsonPath: .metadata.creationTimestamp +# name: Age +# type: date +# name: v1alpha1 +# schema: +# openAPIV3Schema: +# description: MysqlUser is the Schema for the MySQL User API +# properties: +# apiVersion: +# description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' +# type: string +# kind: +# description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' +# type: string +# metadata: +# type: object +# spec: +# description: MysqlUserSpec defines the desired state of MysqlUserSpec +# properties: +# allowedHosts: +# description: AllowedHosts is the allowed host to connect from. +# items: +# type: string +# type: array +# clusterRef: +# description: ClusterRef represents a reference to the MySQL cluster. This field should be immutable. +# properties: +# name: +# description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?' +# type: string +# namespace = kubernetes_namespace.dbaas.metadata[0].name +# description:namespace = kubernetes_namespace.dbaas.metadata[0].name +# type: string +# type: object +# password: +# description: Password is the password for the user. +# properties: +# key: +# description: The key of the secret to select from. Must be a valid secret key. +# type: string +# name: +# description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?' +# type: string +# optional: +# description: Specify whether the Secret or its key must be defined +# type: boolean +# required: +# - key +# type: object +# permissions: +# description: Permissions is the list of roles that user has in the specified database. +# items: +# description: MysqlPermission defines a MySQL schema permission +# properties: +# permissions: +# description: Permissions represents the permissions granted on the schema/tables +# items: +# type: string +# type: array +# schema: +# description: Schema represents the schema to which the permission applies +# type: string +# tables: +# description: Tables represents the tables inside the schema to which the permission applies +# items: +# type: string +# type: array +# required: +# - permissions +# - schema +# - tables +# type: object +# type: array +# resourceLimits: +# additionalProperties: +# anyOf: +# - type: integer +# - type: string +# pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ +# x-kubernetes-int-or-string: true +# description: 'ResourceLimits allow settings limit per mysql user as defined here: https://dev.mysql.com/doc/refman/5.7/en/user-resources.html' +# type: object +# user: +# description: User is the name of the user that will be created with will access the specified database. This field should be immutable. +# type: string +# required: +# - allowedHosts +# - clusterRef +# - password +# - user +# type: object +# status: +# description: MysqlUserStatus defines the observed state of MysqlUser +# properties: +# allowedHosts: +# description: AllowedHosts contains the list of hosts that the user is allowed to connect from. +# items: +# type: string +# type: array +# conditions: +# description: Conditions represents the MysqlUser resource conditions list. +# items: +# description: MySQLUserCondition defines the condition struct for a MysqlUser resource +# properties: +# lastTransitionTime: +# description: Last time the condition transitioned from one status to another. +# format: date-time +# type: string +# lastUpdateTime: +# description: The last time this condition was updated. +# format: date-time +# type: string +# message: +# description: A human readable message indicating details about the transition. +# type: string +# reason: +# description: The reason for the condition's last transition. +# type: string +# status: +# description: Status of the condition, one of True, False, Unknown. +# type: string +# type: +# description: Type of MysqlUser condition. +# type: string +# required: +# - lastTransitionTime +# - message +# - reason +# - status +# - type +# type: object +# type: array +# type: object +# type: object +# served: true +# storage: true +# subresources: +# status: {} +# EOF +# } + +#### POSTGRESQL — CloudNativePG Cluster +# +# Migrated from single NFS-backed pod to CNPG on local-path storage. +# CNPG Cluster is managed via kubectl apply (not kubernetes_manifest) +# because the CNPG webhook mutates the spec on apply, causing +# Terraform provider "inconsistent result" errors. +# +# Rollback: apply old deployment yaml, revert service selector to app=postgresql. + +# Ensure the CNPG cluster manifest exists (idempotent kubectl apply) +resource "null_resource" "pg_cluster" { + triggers = { + instances = "2" + image = "ghcr.io/cloudnative-pg/postgis:16" + storage_size = "20Gi" + storage_class = "iscsi-truenas" + memory_limit = "512Mi" + + } + + provisioner "local-exec" { + command = <<-EOT + kubectl --kubeconfig ${var.kube_config_path} apply -f - <<'EOF' + apiVersion: postgresql.cnpg.io/v1 + kind: Cluster + metadata: + name: pg-cluster + namespace: dbaas + spec: + instances: 2 + imageName: ghcr.io/cloudnative-pg/postgis:16 + postgresql: + parameters: + search_path: '"$user", public' + enableAlterSystem: true + enableSuperuserAccess: true + storage: + size: 20Gi + storageClass: iscsi-truenas + resources: + requests: + cpu: "50m" + memory: "512Mi" + limits: + memory: "512Mi" + EOF + EOT + } +} + +# Service that maintains the original postgresql.dbaas endpoint, +# now pointing at the CNPG primary pod instead of the old deployment. +resource "kubernetes_service" "postgresql" { + metadata { + name = "postgresql" + namespace = kubernetes_namespace.dbaas.metadata[0].name + } + spec { + selector = { + "cnpg.io/cluster" = "pg-cluster" + "cnpg.io/instanceRole" = "primary" + } + port { + name = "postgresql" + port = 5432 + target_port = 5432 + } + } +} + +# Old PostgreSQL deployment — kept commented for rollback reference +# resource "kubernetes_deployment" "postgres" { +# metadata { +# name = "postgresql" +# namespace = kubernetes_namespace.dbaas.metadata[0].name +# labels = { tier = var.tier } +# } +# spec { +# replicas = 0 # scaled to 0 during CNPG migration +# selector { match_labels = { app = "postgresql" } } +# strategy { type = "Recreate" } +# template { +# metadata { labels = { app = "postgresql" } } +# spec { +# container { +# image = "viktorbarzin/postgres:16-master" +# name = "postgresql" +# env { name = "POSTGRES_PASSWORD"; value = var.postgresql_root_password } +# env { name = "POSTGRES_USER"; value = "root" } +# port { container_port = 5432; protocol = "TCP"; name = "postgresql" } +# volume_mount { name = "postgresql-persistent-storage"; mount_path = "/var/lib/postgresql/data" } +# } +# volume { +# name = "postgresql-persistent-storage" +# nfs { path = "/mnt/main/postgresql/data"; server = var.nfs_server } +# } +# } +# } +# } +# } + +#### PGADMIN + +resource "kubernetes_deployment" "pgadmin" { + metadata { + name = "pgadmin" + namespace = kubernetes_namespace.dbaas.metadata[0].name + annotations = { + "reloader.stakater.com/search" = "true" + } + labels = { + tier = var.tier + } + } + spec { + selector { + match_labels = { + app = "pgadmin" + } + } + template { + metadata { + labels = { + app = "pgadmin" + } + } + spec { + container { + image = "dpage/pgadmin4" + name = "pgadmin" + env { + name = "PGADMIN_DEFAULT_EMAIL" + value = "me@viktorbarzin.me" + } + env { + name = "PGADMIN_DEFAULT_PASSWORD" + # Changed at startup + value = var.pgadmin_password + } + port { + container_port = 80 + name = "web" + } + volume_mount { + name = "pgadmin" + mount_path = "/var/lib/pgadmin/" + } + + resources { + requests = { + cpu = "25m" + memory = "512Mi" + } + limits = { + memory = "512Mi" + } + } + + } + volume { + name = "pgadmin" + # config_map { + # name = "pgadmin-config" + # } + persistent_volume_claim { + claim_name = module.nfs_pgadmin.claim_name + } + } + dns_config { + option { + name = "ndots" + value = "2" + } + } + } + } + } +} +resource "kubernetes_service" "pgadmin" { + metadata { + name = "pgadmin" + namespace = kubernetes_namespace.dbaas.metadata[0].name + } + spec { + selector = { + "app" = "pgadmin" + } + port { + name = "pgadmin" + port = 80 + } + } +} +module "ingress-pgadmin" { + source = "../../../../modules/kubernetes/ingress_factory" + namespace = kubernetes_namespace.dbaas.metadata[0].name + name = "pgadmin" + tls_secret_name = var.tls_secret_name + protected = true + rybbit_site_id = "7cef78e30485" +} + + +resource "kubernetes_cron_job_v1" "postgresql-backup" { + metadata { + name = "postgresql-backup" + namespace = kubernetes_namespace.dbaas.metadata[0].name + } + spec { + concurrency_policy = "Replace" + failed_jobs_history_limit = 5 + schedule = "0 0 * * *" + # schedule = "* * * * *" + starting_deadline_seconds = 10 + successful_jobs_history_limit = 10 + job_template { + metadata {} + spec { + backoff_limit = 3 + ttl_seconds_after_finished = 10 + template { + metadata {} + spec { + container { + name = "postgresql-backup" + image = "postgres:16.4-bullseye" + env { + name = "PGPASSWORD" + value_from { + secret_key_ref { + name = "pg-cluster-superuser" + key = "password" + } + } + } + command = ["/bin/bash", "-c", <<-EOT + set -euxo pipefail + export now=$(date +"%Y_%m_%d_%H_%M") + PGPASSWORD=$PGPASSWORD pg_dumpall -h postgresql.dbaas -U postgres > /backup/dump_$now.sql + + # Rotate - delete last log file + cd /backup + find . -name "dump_*.sql" -type f -mtime +7 -delete # 7 day retention of backups + echo Done + EOT + ] + volume_mount { + name = "postgresql-backup" + mount_path = "/backup" + } + } + volume { + name = "postgresql-backup" + persistent_volume_claim { + claim_name = module.nfs_postgresql_backup.claim_name + } + } + } + } + } + } + } +} diff --git a/stacks/dbaas/modules/dbaas/mysql_chart_values.yaml b/stacks/dbaas/modules/dbaas/mysql_chart_values.yaml new file mode 100644 index 00000000..5dbeb1e8 --- /dev/null +++ b/stacks/dbaas/modules/dbaas/mysql_chart_values.yaml @@ -0,0 +1,14 @@ +--- +orchestrator: + # persistence: + # enabled: false + ingress: + enable: false + hosts: + - host: db.viktorbarzin.me + paths: + - path: / + tls: + - secretName: ${secretName} + hosts: + - db.viktorbarzin.me diff --git a/stacks/dbaas/modules/dbaas/postgres/postgres_Dockerfile b/stacks/dbaas/modules/dbaas/postgres/postgres_Dockerfile new file mode 100644 index 00000000..d622979e --- /dev/null +++ b/stacks/dbaas/modules/dbaas/postgres/postgres_Dockerfile @@ -0,0 +1,30 @@ +# Use the PostGIS image as the base +FROM pgvector/pgvector:0.8.0-pg16 as binary +FROM postgis/postgis:16-master +COPY --from=binary /pgvecto-rs-binary-release.deb /tmp/vectors.deb +RUN apt-get install -y /tmp/vectors.deb && rm -f /tmp/vectors.deb + +# Install necessary packages +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + libpq-dev \ + wget \ + git \ + postgresql-server-dev-16 \ + postgresql-16-pgvector \ + # Clean up to reduce layer size + && rm -rf /var/lib/apt/lists/* \ + && cd /tmp \ + && git clone --branch v0.8.0 https://github.com/pgvector/pgvector.git \ + && cd pgvector \ + && make \ + && make install \ + # Clean up unnecessary files + && cd - \ + && apt-get purge -y --auto-remove build-essential postgresql-server-dev-16 libpq-dev wget git \ + && rm -rf /tmp/pgvector + +# Copy initialization scripts +#COPY ./docker-entrypoint-initdb.d/ /docker-entrypoint-initdb.d/ +CMD ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", "search_path=\"$user\", public, vectors", "-c", "logging_collector=on"] \ No newline at end of file diff --git a/stacks/dbaas/modules/dbaas/versions.tf b/stacks/dbaas/modules/dbaas/versions.tf new file mode 100644 index 00000000..f020aef4 --- /dev/null +++ b/stacks/dbaas/modules/dbaas/versions.tf @@ -0,0 +1,9 @@ +# terraform { +# required_providers { +# kubectl = { +# source = "gavinbunney/kubectl" +# version = ">= 1.10.0" +# } +# } +# required_version = ">= 0.13" +# } diff --git a/stacks/dbaas/secrets b/stacks/dbaas/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/dbaas/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/dbaas/terragrunt.hcl b/stacks/dbaas/terragrunt.hcl new file mode 100644 index 00000000..4f16dddf --- /dev/null +++ b/stacks/dbaas/terragrunt.hcl @@ -0,0 +1,8 @@ +include "root" { + path = find_in_parent_folders() +} + +dependency "infra" { + config_path = "../infra" + skip_outputs = true +} diff --git a/stacks/dbaas/tiers.tf b/stacks/dbaas/tiers.tf new file mode 100644 index 00000000..eb0f8083 --- /dev/null +++ b/stacks/dbaas/tiers.tf @@ -0,0 +1,10 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +locals { + tiers = { + core = "0-core" + cluster = "1-cluster" + gpu = "2-gpu" + edge = "3-edge" + aux = "4-aux" + } +} diff --git a/stacks/platform/main.tf b/stacks/platform/main.tf index 490b4aca..cf8f805c 100644 --- a/stacks/platform/main.tf +++ b/stacks/platform/main.tf @@ -2,15 +2,18 @@ # Platform Stack — Core & Cluster Services # ============================================================================= # -# This stack groups ~22 core/cluster services that form the platform layer. +# This stack groups core/cluster services that form the platform layer. # These services are always present (no DEFCON gating) and provide the # foundational infrastructure that application stacks depend on. # # Services included: -# metallb, dbaas, cloudflared, infra-maintenance, -# redis, traefik, technitium, headscale, authentik, rbac, k8s-portal, -# crowdsec, monitoring, vaultwarden, reverse-proxy, metrics-server, vpa, +# metallb, cloudflared, infra-maintenance, +# redis, traefik, technitium, headscale, rbac, k8s-portal, +# monitoring, vaultwarden, reverse-proxy, metrics-server, vpa, # nvidia, kyverno, uptime-kuma, wireguard, xray, mailserver +# +# Extracted to independent stacks: +# dbaas, authentik, crowdsec # ============================================================================= # ----------------------------------------------------------------------------- @@ -31,10 +34,6 @@ variable "postgresql_host" { type = string } variable "mysql_host" { type = string } variable "ollama_host" { type = string } variable "mail_host" { type = string } -variable "prod" { - type = bool - default = false -} variable "k8s_ca_cert" { type = string default = "" @@ -88,21 +87,6 @@ module "metallb" { tier = local.tiers.core } -# ----------------------------------------------------------------------------- -# DBaaS — MySQL + PostgreSQL + pgAdmin -# ----------------------------------------------------------------------------- -module "dbaas" { - source = "./modules/dbaas" - prod = var.prod - tls_secret_name = var.tls_secret_name - nfs_server = var.nfs_server - dbaas_root_password = data.vault_kv_secret_v2.secrets.data["dbaas_root_password"] - postgresql_root_password = data.vault_kv_secret_v2.secrets.data["dbaas_postgresql_root_password"] - pgadmin_password = data.vault_kv_secret_v2.secrets.data["dbaas_pgadmin_password"] - kube_config_path = var.kube_config_path - tier = local.tiers.cluster -} - # ----------------------------------------------------------------------------- # Redis — Shared Redis instance # ----------------------------------------------------------------------------- @@ -153,19 +137,6 @@ module "headscale" { tier = local.tiers.core } -# ----------------------------------------------------------------------------- -# Authentik — Identity provider (SSO) -# ----------------------------------------------------------------------------- -module "authentik" { - source = "./modules/authentik" - tier = local.tiers.cluster - tls_secret_name = var.tls_secret_name - secret_key = data.vault_kv_secret_v2.secrets.data["authentik_secret_key"] - postgres_password = data.vault_kv_secret_v2.secrets.data["authentik_postgres_password"] - redis_host = var.redis_host - homepage_token = try(local.homepage_credentials["authentik"]["token"], "") -} - # ----------------------------------------------------------------------------- # RBAC — Kubernetes OIDC RBAC (depends on Authentik) # ----------------------------------------------------------------------------- @@ -187,24 +158,6 @@ module "k8s-portal" { k8s_ca_cert = var.k8s_ca_cert } -# ----------------------------------------------------------------------------- -# CrowdSec — Security/WAF -# ----------------------------------------------------------------------------- -module "crowdsec" { - source = "./modules/crowdsec" - tier = local.tiers.cluster - tls_secret_name = var.tls_secret_name - mysql_host = var.mysql_host - homepage_username = local.homepage_credentials["crowdsec"]["username"] - homepage_password = local.homepage_credentials["crowdsec"]["password"] - enroll_key = data.vault_kv_secret_v2.secrets.data["crowdsec_enroll_key"] - db_password = data.vault_kv_secret_v2.secrets.data["crowdsec_db_password"] - crowdsec_dash_api_key = data.vault_kv_secret_v2.secrets.data["crowdsec_dash_api_key"] - crowdsec_dash_machine_id = data.vault_kv_secret_v2.secrets.data["crowdsec_dash_machine_id"] - crowdsec_dash_machine_password = data.vault_kv_secret_v2.secrets.data["crowdsec_dash_machine_password"] - slack_webhook_url = data.vault_kv_secret_v2.secrets.data["alertmanager_slack_api_url"] -} - # ----------------------------------------------------------------------------- # Monitoring — Prometheus / Grafana / Loki stack # -----------------------------------------------------------------------------