From 574a9812351716e5cc56d8c487ddbd476c59aa67 Mon Sep 17 00:00:00 2001 From: ATM Platform Date: Tue, 16 Jun 2026 21:22:24 +0000 Subject: [PATCH] feat: provisionar cliente acme (silo completo via GitOps) --- tenants/acme/00-namespace-quota-netpol.yaml | 64 +++++++++ tenants/acme/05-sealed-db-credentials.yaml | 15 ++ tenants/acme/06-sealed-keycloak-admin.yaml | 15 ++ tenants/acme/10-postgres.yaml | 83 +++++++++++ tenants/acme/20-keycloak.yaml | 86 +++++++++++ tenants/acme/30-apps-stubs.yaml | 150 ++++++++++++++++++++ tenants/acme/35-realm-import-cm.yaml | 39 +++++ tenants/acme/40-middleware.yaml | 10 ++ tenants/acme/50-backup.yaml | 57 ++++++++ tenants/acme/realm/athleticmap-realm.json | 32 +++++ 10 files changed, 551 insertions(+) create mode 100644 tenants/acme/00-namespace-quota-netpol.yaml create mode 100644 tenants/acme/05-sealed-db-credentials.yaml create mode 100644 tenants/acme/06-sealed-keycloak-admin.yaml create mode 100644 tenants/acme/10-postgres.yaml create mode 100644 tenants/acme/20-keycloak.yaml create mode 100644 tenants/acme/30-apps-stubs.yaml create mode 100644 tenants/acme/35-realm-import-cm.yaml create mode 100644 tenants/acme/40-middleware.yaml create mode 100644 tenants/acme/50-backup.yaml create mode 100644 tenants/acme/realm/athleticmap-realm.json diff --git a/tenants/acme/00-namespace-quota-netpol.yaml b/tenants/acme/00-namespace-quota-netpol.yaml new file mode 100644 index 0000000..7266431 --- /dev/null +++ b/tenants/acme/00-namespace-quota-netpol.yaml @@ -0,0 +1,64 @@ +# Silo "demo" — isolamento do tenant (namespace + quota + limites + rede) +apiVersion: v1 +kind: Namespace +metadata: + name: acme-prod + labels: + name: acme-prod + tenant: acme + athleticmap.io/tier: pilot +--- +apiVersion: v1 +kind: ResourceQuota +metadata: + name: tenant-quota + namespace: acme-prod +spec: + hard: + requests.cpu: "2" + requests.memory: 2Gi + limits.cpu: "4" + limits.memory: 6Gi + pods: "20" + persistentvolumeclaims: "4" +--- +apiVersion: v1 +kind: LimitRange +metadata: + name: defaults + namespace: acme-prod +spec: + limits: + - type: Container + default: + cpu: 500m + memory: 512Mi + defaultRequest: + cpu: 100m + memory: 128Mi +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: deny-cross-tenant + namespace: acme-prod +spec: + podSelector: {} + policyTypes: [Ingress, Egress] + ingress: + - from: + - podSelector: {} + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + egress: + - to: # intra-namespace (pods) + ClusterIPs (VIP de service, pre-DNAT) + - podSelector: {} + - ipBlock: { cidr: 10.43.0.0/16 } + - to: # DNS (CoreDNS em kube-system) + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - { protocol: UDP, port: 53 } + - { protocol: TCP, port: 53 } diff --git a/tenants/acme/05-sealed-db-credentials.yaml b/tenants/acme/05-sealed-db-credentials.yaml new file mode 100644 index 0000000..59f688c --- /dev/null +++ b/tenants/acme/05-sealed-db-credentials.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: db-credentials + namespace: acme-prod +spec: + encryptedData: + password: AgBFRogG+gGWgmRSXwsb3PfXhORsSoHGN0nREc98Q4KmoP0f2l9pgN9sDsX0sXNU/x4LG6eSoC6gXArU1dRplw6MkijPtWfhXWDQE7hCmhVDhBp/F+3jqDjzX0Z/1q7e8yopZuqZH3uIIagQ/SMlBCzHsm6ZOW43LoWGvrHCX94QCL3ZzuTX7t1UtO5SbRj/ooUMLDC33LqSrDvH+aAEJMEQimOYT8gbE1W1CN1VYR2A2Rac8lOUyqIFUMwEuyMvws98UEAEecilQRlmGGQJj2ORgYjFQRZCF0jDXbLYJmUxnPVAA3un3tfgABLg/S6KU/fqvIazk+OCknwXmZyQdB35lJcXxMNPuMYQ0mIdaSCDTpuZzUIF/6cdijjB+FvNMdEjTlSLcHlkQT3ABemyCpCrA00Otg+wVle0RGvhaj/2fsEYe28hNc72QO2IL53Ggboraho0BiIaxcnwVovHXno+URO4HmQsg0iecfhS09trXOMPYTf+Ire0NLSGE06kZqvRnqtBWpQYbr0r4rRV5rdF7pRGIgl09txMQ5VCKdUgoufGgQMGOquVWIUnViHTBU63vnGsOeaAr0A8bt2jQ0k4AaG3NmoPP5ROgw5U4y8ddQ9J5p1yd0RkeZ8d3605vnPXEzNRSmLhaBpT6EHg5nwQ/1vpPt4Bnjuk5CKUY1q4aSDJAzRfPfful8/LJzAAINji0e9cdKMn4Mt0bAS9P1MMCHFsOLRLdFGIUO9lBvME6A== + template: + metadata: + creationTimestamp: null + name: db-credentials + namespace: acme-prod diff --git a/tenants/acme/06-sealed-keycloak-admin.yaml b/tenants/acme/06-sealed-keycloak-admin.yaml new file mode 100644 index 0000000..dd2e53c --- /dev/null +++ b/tenants/acme/06-sealed-keycloak-admin.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: keycloak-admin + namespace: acme-prod +spec: + encryptedData: + password: AgCD4+27tP4eujj+4aruY5qQy7JVfahQBasYRh5uz/PdYbRathBDGl5scygR0Zf+hg5Qaoem7VRO1ilBE3Uzjm/EvS2iD/pLasQP1n6ZJYgX91+HQnbORNjrTAKuZ/Jk/rzz3mcADdNl3LwEYy71iKELlmlV8zSmDgpzT8U6AkdoUc1xPErpzBch9ChpVnyTcnXz2rb8ni1zjzrG42p6lf42U9glTy/YvztouZiGggJahTPC5ChcHpWcj+1ILFt8/Lbovrv+8vODXzBg3Jt3Oe+CB0bUSVbSrMudCeFu+raBbfGiUZnpT6pBa4j8rjHL5qFmxxtjfHgNcrhJIi6b3KFt4yLRCM8c24wg29rKSRb8hTV2tLGuao4uAT9HxVW3x29hzgR+Lvj4yRGpKf7rzGJKkSF+dHrAbcaYTIWnYeMGGFKnT1edWFAB2JimDrnmjRJF8eGca/2JVL9P+mvElJetcVMbNd36310fE0Fu36Xlr/CK3gNGWCepsBdmIy7A38UlT7MArJ83bJXrKSFA0qdwZQ/oHB2WE3so/9YfPwJ8MNqv2ROf/MYRjlvL3Yx1K3GxzmR+L7nrEf/KrhsZOibVt81rHdtoH1/VZ/AeH3V2kYp9HPZi/yq1KrlzTuQWvL7mQrXqMC7GnNSzaD2fcTp0LGjshfJdzsIXW9mo5lBarWt6QW5VrR0vf74kHUGwPnS8CK5qVz8OufFHf1faN7q0qrVVjWrU46I= + template: + metadata: + creationTimestamp: null + name: keycloak-admin + namespace: acme-prod diff --git a/tenants/acme/10-postgres.yaml b/tenants/acme/10-postgres.yaml new file mode 100644 index 0000000..6e6edc0 --- /dev/null +++ b/tenants/acme/10-postgres.yaml @@ -0,0 +1,83 @@ +# PostgreSQL dedicado do tenant demo (banco da aplicação + banco do Keycloak) +apiVersion: v1 +kind: ConfigMap +metadata: + name: pg-initdb + namespace: acme-prod +data: + 01-keycloak.sql: | + CREATE DATABASE keycloak; +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: postgres-data + namespace: acme-prod +spec: + accessModes: [ReadWriteOnce] + storageClassName: local-path + resources: + requests: + storage: 5Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres + namespace: acme-prod +spec: + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: postgres:16 + ports: + - containerPort: 5432 + env: + - name: POSTGRES_DB + value: athleticmap + - name: POSTGRES_USER + value: atm + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: db-credentials + key: password + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + - name: initdb + mountPath: /docker-entrypoint-initdb.d + readinessProbe: + exec: + command: ["pg_isready", "-U", "atm", "-d", "athleticmap"] + initialDelaySeconds: 10 + periodSeconds: 5 + volumes: + - name: data + persistentVolumeClaim: + claimName: postgres-data + - name: initdb + configMap: + name: pg-initdb +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: acme-prod +spec: + selector: + app: postgres + ports: + - port: 5432 + targetPort: 5432 diff --git a/tenants/acme/20-keycloak.yaml b/tenants/acme/20-keycloak.yaml new file mode 100644 index 0000000..b168409 --- /dev/null +++ b/tenants/acme/20-keycloak.yaml @@ -0,0 +1,86 @@ +# Keycloak dedicado do tenant demo (IdP do silo) — modo dev, persistindo no Postgres +apiVersion: apps/v1 +kind: Deployment +metadata: + name: keycloak + namespace: acme-prod +spec: + replicas: 1 + selector: + matchLabels: + app: keycloak + template: + metadata: + labels: + app: keycloak + spec: + containers: + - name: keycloak + image: quay.io/keycloak/keycloak:26.0 + args: ["start-dev", "--import-realm"] + env: + - name: KC_DB + value: postgres + - name: KC_DB_URL + value: "jdbc:postgresql://postgres:5432/keycloak" + - name: KC_DB_USERNAME + value: atm + - name: KC_DB_PASSWORD + valueFrom: + secretKeyRef: + name: db-credentials + key: password + - name: KC_BOOTSTRAP_ADMIN_USERNAME + value: admin + - name: KC_BOOTSTRAP_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: keycloak-admin + key: password + - name: KC_HEALTH_ENABLED + value: "true" + - name: KC_HTTP_ENABLED + value: "true" + - name: KC_PROXY_HEADERS + value: xforwarded + - name: KC_HOSTNAME + value: "auth-acme.athleticmap.influxdigital.com.br" + - name: KC_HOSTNAME_STRICT + value: "true" + ports: + - containerPort: 8080 + - containerPort: 9000 + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + cpu: "1" + memory: 1Gi + readinessProbe: + httpGet: + path: /health/ready + port: 9000 + initialDelaySeconds: 30 + periodSeconds: 10 + failureThreshold: 40 + volumeMounts: + - name: realm-import + mountPath: /opt/keycloak/data/import + readOnly: true + volumes: + - name: realm-import + configMap: + name: kc-realm-import +--- +apiVersion: v1 +kind: Service +metadata: + name: keycloak + namespace: acme-prod +spec: + selector: + app: keycloak + ports: + - port: 8080 + targetPort: 8080 diff --git a/tenants/acme/30-apps-stubs.yaml b/tenants/acme/30-apps-stubs.yaml new file mode 100644 index 0000000..6eeaeec --- /dev/null +++ b/tenants/acme/30-apps-stubs.yaml @@ -0,0 +1,150 @@ +# Apps do tenant demo: +# - backend: Spring Boot OAuth2 Resource Server (imagem athletic-map-backend:1.2, porta 8083) +# - frontend: SPA OIDC Authorization Code + PKCE (keycloak-js) chamando /api/me +# - bff: stub (whoami) +--- +apiVersion: apps/v1 +kind: Deployment +metadata: { name: backend, namespace: acme-prod } +spec: + replicas: 1 + selector: { matchLabels: { app: backend } } + template: + metadata: { labels: { app: backend } } + spec: + containers: + - name: backend + image: docker.io/library/athletic-map-backend:1.2 + imagePullPolicy: Never + env: + - { name: ATM_JWK_SET_URI, value: "http://keycloak:8080/realms/athleticmap/protocol/openid-connect/certs" } + - { name: ATM_ISSUER, value: "https://auth-acme.athleticmap.influxdigital.com.br/realms/athleticmap" } + - { name: ATM_TENANT, value: "acme" } + ports: [{ containerPort: 8083 }] + readinessProbe: + httpGet: { path: /api/public/health, port: 8083 } + initialDelaySeconds: 20 + periodSeconds: 10 + failureThreshold: 24 +--- +apiVersion: v1 +kind: Service +metadata: { name: backend, namespace: acme-prod } +spec: + selector: { app: backend } + ports: [{ port: 80, targetPort: 8083 }] +--- +apiVersion: apps/v1 +kind: Deployment +metadata: { name: bff, namespace: acme-prod } +spec: + replicas: 1 + selector: { matchLabels: { app: bff } } + template: + metadata: { labels: { app: bff } } + spec: + containers: + - name: whoami + image: traefik/whoami:latest + args: ["--name", "athletic-map-bff acme (stub)"] + ports: [{ containerPort: 80 }] +--- +apiVersion: v1 +kind: Service +metadata: { name: bff, namespace: acme-prod } +spec: + selector: { app: bff } + ports: [{ port: 80, targetPort: 80 }] +--- +apiVersion: v1 +kind: ConfigMap +metadata: { name: frontend-index, namespace: acme-prod } +data: + index.html: | + + + Athletic Map — Acme + + +

Carregando…

+ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: { name: frontend, namespace: acme-prod } +spec: + replicas: 1 + selector: { matchLabels: { app: frontend } } + template: + metadata: + labels: { app: frontend } + annotations: { configVersion: "spa-2" } + spec: + containers: + - name: nginx + image: nginx:alpine + ports: [{ containerPort: 80 }] + volumeMounts: + - { name: html, mountPath: /usr/share/nginx/html } + volumes: + - name: html + configMap: { name: frontend-index } +--- +apiVersion: v1 +kind: Service +metadata: { name: frontend, namespace: acme-prod } +spec: + selector: { app: frontend } + ports: [{ port: 80, targetPort: 80 }] +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: demo + namespace: acme-prod + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.middlewares: acme-prod-redirect-https@kubernetescrd +spec: + ingressClassName: traefik + tls: + - hosts: + - acme.athleticmap.influxdigital.com.br + - auth-acme.athleticmap.influxdigital.com.br + secretName: acme-tls + rules: + - host: acme.athleticmap.influxdigital.com.br + http: + paths: + - { path: /api, pathType: Prefix, backend: { service: { name: backend, port: { number: 80 } } } } + - { path: /, pathType: Prefix, backend: { service: { name: frontend, port: { number: 80 } } } } + - host: auth-acme.athleticmap.influxdigital.com.br + http: + paths: + - { path: /, pathType: Prefix, backend: { service: { name: keycloak, port: { number: 8080 } } } } diff --git a/tenants/acme/35-realm-import-cm.yaml b/tenants/acme/35-realm-import-cm.yaml new file mode 100644 index 0000000..42301d0 --- /dev/null +++ b/tenants/acme/35-realm-import-cm.yaml @@ -0,0 +1,39 @@ +apiVersion: v1 +data: + athleticmap-realm.json: | + { + "realm": "athleticmap", + "enabled": true, + "displayName": "Athletic Map", + "loginWithEmailAllowed": true, + "roles": { + "realm": [ + { "name": "admin" }, + { "name": "atm_athlete" }, + { "name": "atm_trainer" }, + { "name": "atm_team_admin" }, + { "name": "atm_fed_admin" } + ] + }, + "clients": [ + { + "clientId": "spa", + "name": "Athletic Map SPA", + "publicClient": true, + "protocol": "openid-connect", + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "redirectUris": ["https://acme.athleticmap.influxdigital.com.br/*"], + "webOrigins": ["https://acme.athleticmap.influxdigital.com.br"], + "attributes": { + "pkce.code.challenge.method": "S256", + "post.logout.redirect.uris": "https://acme.athleticmap.influxdigital.com.br/*" + } + } + ] + } +kind: ConfigMap +metadata: + name: kc-realm-import + namespace: acme-prod diff --git a/tenants/acme/40-middleware.yaml b/tenants/acme/40-middleware.yaml new file mode 100644 index 0000000..ff82825 --- /dev/null +++ b/tenants/acme/40-middleware.yaml @@ -0,0 +1,10 @@ +# Redirect HTTP->HTTPS (Traefik). Aplicado por-ingress (nao afeta o desafio ACME do cert-manager). +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: redirect-https + namespace: acme-prod +spec: + redirectScheme: + scheme: https + permanent: true diff --git a/tenants/acme/50-backup.yaml b/tenants/acme/50-backup.yaml new file mode 100644 index 0000000..8ae637b --- /dev/null +++ b/tenants/acme/50-backup.yaml @@ -0,0 +1,57 @@ +# Backup diario do Postgres do tenant demo (pg_dump -> PVC, retencao 7 dias) +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: pg-backups + namespace: acme-prod +spec: + accessModes: [ReadWriteOnce] + storageClassName: local-path + resources: + requests: + storage: 5Gi +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: pg-backup + namespace: acme-prod +spec: + schedule: "0 2 * * *" + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + concurrencyPolicy: Forbid + jobTemplate: + spec: + template: + spec: + restartPolicy: OnFailure + containers: + - name: pg-backup + image: postgres:16 + env: + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: db-credentials + key: password + command: ["/bin/sh", "-c"] + args: + - | + set -eo pipefail + echo "aguardando postgres ficar acessivel..." + for i in $(seq 1 30); do pg_isready -h postgres -U atm -t 3 >/dev/null 2>&1 && break; sleep 2; done + TS=$(date +%Y%m%d-%H%M%S) + for DB in athleticmap keycloak; do + echo "dump $DB ..." + pg_dump -h postgres -U atm -d "$DB" | gzip > "/backups/${DB}-${TS}.sql.gz" + done + find /backups -name '*.sql.gz' -mtime +7 -delete + echo "backups atuais:"; ls -lh /backups | tail -20 + volumeMounts: + - name: backups + mountPath: /backups + volumes: + - name: backups + persistentVolumeClaim: + claimName: pg-backups diff --git a/tenants/acme/realm/athleticmap-realm.json b/tenants/acme/realm/athleticmap-realm.json new file mode 100644 index 0000000..1ae1105 --- /dev/null +++ b/tenants/acme/realm/athleticmap-realm.json @@ -0,0 +1,32 @@ +{ + "realm": "athleticmap", + "enabled": true, + "displayName": "Athletic Map", + "loginWithEmailAllowed": true, + "roles": { + "realm": [ + { "name": "admin" }, + { "name": "atm_athlete" }, + { "name": "atm_trainer" }, + { "name": "atm_team_admin" }, + { "name": "atm_fed_admin" } + ] + }, + "clients": [ + { + "clientId": "spa", + "name": "Athletic Map SPA", + "publicClient": true, + "protocol": "openid-connect", + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "redirectUris": ["https://acme.athleticmap.influxdigital.com.br/*"], + "webOrigins": ["https://acme.athleticmap.influxdigital.com.br"], + "attributes": { + "pkce.code.challenge.method": "S256", + "post.logout.redirect.uris": "https://acme.athleticmap.influxdigital.com.br/*" + } + } + ] +}