Basis Kubernetes Deploy
Padrões da Basis para infra/deploy de aplicações no K8s. Pareada com basis-spring-app (esta documenta o lado ops; aquela documenta o lado dev).
1. Convenções globais
- Registry:
basis-registry.basis.com.br/<app>/<componente>
- Tag CalVer:
YYYY.MM.DD.Seq — Seq é o CI_PIPELINE_IID do GitLab: contador por projeto, contíguo. Não é o CI_PIPELINE_ID, que é global da instância. A tag de produção leva o prefixo production-
- Namespace: um por aplicação (ex:
identity-hub, licitacao)
- Vhost RabbitMQ: um por aplicação, nome curto (ex:
identityhub)
2. Layout do kustomize
manifests/<app>/
├── base/
│ ├── apps/<componente>/ # deployment, service, kustomization, env files
│ ├── rabbitmq/ # vhost, user, filas
│ └── ...
└── overlays/
├── staging/
└── production/
- Overlays carregam
images: (tag CalVer), replicas:, secretGenerator behavior: replace
- Generators de env vars: SEMPRE
envs:, NUNCA files: (gotcha: files: cria 1 chave com nome inválido como env var, envFrom ignora silenciosamente)
3. ArgoCD + Image Updater
- App-of-apps no repo
iac/argocd-apps
- Image Updater anota a Application com regex CalVer e write-back via Git commit (atualiza
overlays/<env>/kustomization.yaml)
- O write-back sai como commit no repo de manifests, ArgoCD detecta e sincroniza
4. Operadores
Usamos um operador por dependência. Cada app roda sua própria instância (postgres, minio, mariadb, redis) — exceção: RabbitMQ é cluster compartilhado, isolamento por vhost.
| Operador |
Dependência |
Padrão de secret |
| Zalando |
PostgreSQL |
<user>.<cluster>.credentials.postgresql.acid.zalan.do (chaves: username, password) — gerenciado pelo operador, referenciar direto |
| RabbitMQ |
RabbitmqCluster + Topology |
<app>-rabbitmq (chaves: username, password) — criado por nós via secretGenerator.literals |
| Minio |
Bucket por aplicação |
gerenciado pelo operador |
| MariaDB |
DB por aplicação |
gerenciado pelo operador |
| ot-container-kit |
Redis cluster |
gerenciado pelo operador |
Princípio: o nome do secret é estável entre ambientes (definido em base/), só o valor muda (overlay com behavior: replace).
5. Topology RabbitMQ via CRDs
Quando a app gerencia a topologia via K8s (não via SCS auto-bind):
- Exchange
<app>.<flow> (topic), Queue <app>.<flow>.<group>, Binding com routingKey: "#"
- DLX único
DLX (topic) compartilhado entre flows da app
- DLQ
<queue>.dlq, binding com routingKey: <destination> (combina com x-dead-letter-routing-key do main queue)
- Main queue:
arguments.x-dead-letter-exchange: DLX + x-dead-letter-routing-key: <destination>
Espelha o que o SCS faria com auto-bind-dlq no default — mas com K8s como source of truth.
6. Onde a CI termina e o deploy começa
A fronteira é a imagem no registry com a tag de produção (production-<calver>). Quem a
coloca lá é o job promote-production da pipeline; daí em diante quem age é o Image Updater
(§3).
Tudo do lado GitLab — ci/pipeline.toml, o template compartilhado, as funções do
orchestrator Dagger, Sonar, MR, convenção de branch e de commit — está em basis-ci-gitlab.
Sintoma que atravessa a fronteira e engana: "a versão nova não subiu em produção". Antes de
investigar Application ou sync, confirme que a imagem existe no registry com a tag esperada.
Quase sempre o promote não rodou, ou a tag não casa com o allow-tags da Application.
7. Interface com basis-spring-app
Convenção da Basis: prefixo das ConfigurationProperties da app é sempre application.*, não o nome da app.
- Spring relaxed binding:
application.opnsense.base-url ← APPLICATION_OPNSENSE_BASE_URL
- Para infra Spring puro:
spring.datasource.* ← SPRING_DATASOURCE_*, spring.rabbitmq.* ← SPRING_RABBITMQ_*
- Não redeclarar defaults Spring (ex:
port: 5672 em rabbitmq)
- Não criar env var customizado se relaxed binding cobre (ex:
RABBITMQ_HOST → use SPRING_RABBITMQ_HOST)
Regra prática para deployment.yaml:
envFrom (ConfigMap/Secret) para o conjunto de chaves APPLICATION_* da app
env: explícito quando precisa secretKeyRef de operador (ex: postgres credentials)
8. Ingress e DNS
Duas coisas que o Ingress precisa ter e falham em silêncio quando faltam — e uma terceira
que ele não deve ter, porque a plataforma já resolve.
ingressClassName: traefik. É a única IngressClass que existe no cluster —
kubectl get ingressclass devolve uma linha só. Não há alternativa a escolher, e qualquer
outro valor é aceito pela API sem reclamar e nunca é atendido: o kubectl get ingress
mostra o recurso normal, o ADDRESS fica vazio e nada roteia. Vale para erro de digitação
e para replace que concatenou em vez de substituir: a API aceita qualquer string, e o
sintoma chega como "o app está fora", não como manifesto inválido
create-aws-record: "true" nas annotations sempre que o host tiver de resolver de fora.
É por essa annotation que o external-dns cria o registro na zona da AWS. Sem ela o
Ingress está correto e o nome não resolve — o sintoma chega como "o DNS não propagou", que
manda investigar o lado errado por um bom tempo
Não declare tls: nem annotation de cert-manager. HTTPS é automático e todos os
sistemas rodam nele. O cert-manager emite um certificado curinga do ambiente pelo
ClusterIssuer letsencrypt-route53 (desafio DNS-01 na Route53), e um TLSStore chamado
default o registra como certificado padrão do Traefik. O controller termina TLS para
qualquer host servido, sem o Ingress pedir nada:
kubectl get clusterissuer # letsencrypt-route53
kubectl get certificate -A | grep default-certificate # o curinga, READY=True
kubectl get tlsstore -n kube-system default -o jsonpath='{.spec}'
Nada disso mora neste repositório — é plataforma, montada uma vez por cluster. Onde um
Ingress de aplicação tem tls:, foi decisão pontual daquele serviço (certificado próprio,
host fora do curinga); não é o padrão e não é para copiar
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: <app>
namespace: <app>
annotations:
create-aws-record: "true" # o host precisa resolver de fora
spec: # sem bloco tls: — o HTTPS vem do certificado padrão
ingressClassName: traefik
rules:
- host: <app>.basis.com.br # staging: <app>.stg.basis.com.br
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: <app>
port:
name: http
De onde copiar: manifests/foundation/overlays/production/apps/editor-bpmn/ingress.yaml.
Copie o Ingress daí, não do app que você estava usando de modelo para outra coisa.
Referência boa para uma parte do manifesto não é referência boa para o resto dele — foi
assim, herdando o Ingress de um app cujo deployment servia de exemplo, que nasceram os
dois erros desta seção.
References disponíveis
references/rabbitmq-topology-pattern.md — Padrão Exchange/Queue/Binding limpos + Policy pra DLX/routing-key/TTL, DLX único compartilhado, migração de args→Policy
references/kustomize-base-example.md — Layout completo de base/ + overlays, envs: em generators, hash suffix vs disableNameSuffixHash
references/operator-secret-cheatsheet.md — Padrão de Secret para Postgres-Zalando, RabbitMQ topology, Minio, MariaDB, Redis-otk, com snippets de valueFrom
references/argocd-image-updater.md — Application yaml + anotações Image Updater, regex CalVer, fluxo end-to-end e promote
Snippets em references/ são auto-contidos (cópias, não ponteiros) pra não quebrarem com refactors.
1---2name: basis-k8s-deploy3description: Use when deploying or configuring a Basis application on Kubernetes — kustomize base+overlays layout, ArgoCD with Image Updater, CalVer image tags, operator-managed dependencies (Postgres/RabbitMQ/Minio/MariaDB/Redis) and their secret patterns. Activate for any change under iac/argocd-apps/manifests/, ArgoCD app definitions, or when wiring secrets/env vars from K8s into a Spring app. Prefer `basis-ci-gitlab` for anything on the GitLab side — pipeline, `ci/pipeline.toml`, Dagger, Sonar, merge requests, branch and commit conventions, and how the image gets built and promoted. The boundary is the image in the registry carrying its production tag; before that, `basis-ci-gitlab`, from there on, this skill.4---56# Basis Kubernetes Deploy78Padrões da Basis para infra/deploy de aplicações no K8s. Pareada com `basis-spring-app` (esta documenta o lado ops; aquela documenta o lado dev).910## 1. Convenções globais1112- **Registry**: `basis-registry.basis.com.br/<app>/<componente>`13- **Tag CalVer**: `YYYY.MM.DD.Seq` — `Seq` é o `CI_PIPELINE_IID` do GitLab: contador **por projeto**, contíguo. Não é o `CI_PIPELINE_ID`, que é global da instância. A tag de produção leva o prefixo `production-`14- **Namespace**: um por aplicação (ex: `identity-hub`, `licitacao`)15- **Vhost RabbitMQ**: um por aplicação, nome curto (ex: `identityhub`)1617## 2. Layout do kustomize1819```20manifests/<app>/21├── base/22│ ├── apps/<componente>/ # deployment, service, kustomization, env files23│ ├── rabbitmq/ # vhost, user, filas24│ └── ...25└── overlays/26 ├── staging/27 └── production/28```2930- Overlays carregam `images:` (tag CalVer), `replicas:`, `secretGenerator behavior: replace`31- **Generators de env vars**: SEMPRE `envs:`, NUNCA `files:` (gotcha: `files:` cria 1 chave com nome inválido como env var, `envFrom` ignora silenciosamente)3233## 3. ArgoCD + Image Updater3435- App-of-apps no repo `iac/argocd-apps`36- Image Updater anota a Application com regex CalVer e write-back via Git commit (atualiza `overlays/<env>/kustomization.yaml`)37- O write-back sai como commit no repo de manifests, ArgoCD detecta e sincroniza3839## 4. Operadores4041Usamos um operador por dependência. Cada app roda sua própria instância (postgres, minio, mariadb, redis) — exceção: RabbitMQ é cluster compartilhado, isolamento por vhost.4243| Operador | Dependência | Padrão de secret |44|----------|-------------|------------------|45| Zalando | PostgreSQL | `<user>.<cluster>.credentials.postgresql.acid.zalan.do` (chaves: `username`, `password`) — gerenciado pelo operador, referenciar direto |46| RabbitMQ | RabbitmqCluster + Topology | `<app>-rabbitmq` (chaves: `username`, `password`) — criado por nós via `secretGenerator.literals` |47| Minio | Bucket por aplicação | gerenciado pelo operador |48| MariaDB | DB por aplicação | gerenciado pelo operador |49| ot-container-kit | Redis cluster | gerenciado pelo operador |5051Princípio: o **nome** do secret é estável entre ambientes (definido em `base/`), só o **valor** muda (overlay com `behavior: replace`).5253## 5. Topology RabbitMQ via CRDs5455Quando a app gerencia a topologia via K8s (não via SCS auto-bind):5657- Exchange `<app>.<flow>` (topic), Queue `<app>.<flow>.<group>`, Binding com `routingKey: "#"`58- DLX único `DLX` (topic) compartilhado entre flows da app59- DLQ `<queue>.dlq`, binding com `routingKey: <destination>` (combina com `x-dead-letter-routing-key` do main queue)60- Main queue: `arguments.x-dead-letter-exchange: DLX` + `x-dead-letter-routing-key: <destination>`6162Espelha o que o SCS faria com `auto-bind-dlq` no default — mas com K8s como source of truth.6364## 6. Onde a CI termina e o deploy começa6566A fronteira é a **imagem no registry com a tag de produção** (`production-<calver>`). Quem a67coloca lá é o job `promote-production` da pipeline; daí em diante quem age é o Image Updater68(§3).6970Tudo do lado GitLab — `ci/pipeline.toml`, o template compartilhado, as funções do71orchestrator Dagger, Sonar, MR, convenção de branch e de commit — está em **`basis-ci-gitlab`**.7273Sintoma que atravessa a fronteira e engana: *"a versão nova não subiu em produção"*. Antes de74investigar Application ou sync, confirme que a imagem existe no registry com a tag esperada.75Quase sempre o `promote` não rodou, ou a tag não casa com o `allow-tags` da Application.7677## 7. Interface com `basis-spring-app`7879Convenção da Basis: prefixo das ConfigurationProperties da app é sempre `application.*`, não o nome da app.8081- Spring relaxed binding: `application.opnsense.base-url` ← `APPLICATION_OPNSENSE_BASE_URL`82- Para infra Spring puro: `spring.datasource.*` ← `SPRING_DATASOURCE_*`, `spring.rabbitmq.*` ← `SPRING_RABBITMQ_*`83- **Não** redeclarar defaults Spring (ex: `port: 5672` em rabbitmq)84- **Não** criar env var customizado se relaxed binding cobre (ex: `RABBITMQ_HOST` → use `SPRING_RABBITMQ_HOST`)8586Regra prática para deployment.yaml:87- `envFrom` (ConfigMap/Secret) para o conjunto de chaves `APPLICATION_*` da app88- `env:` explícito quando precisa `secretKeyRef` de operador (ex: postgres credentials)8990## 8. Ingress e DNS9192Duas coisas que o Ingress precisa ter e falham em silêncio quando faltam — e uma terceira93que ele **não** deve ter, porque a plataforma já resolve.9495- **`ingressClassName: traefik`.** É a única IngressClass que existe no cluster —96 `kubectl get ingressclass` devolve uma linha só. Não há alternativa a escolher, e qualquer97 outro valor é aceito pela API sem reclamar e nunca é atendido: o `kubectl get ingress`98 mostra o recurso normal, o `ADDRESS` fica vazio e nada roteia. Vale para erro de digitação99 e para replace que concatenou em vez de substituir: a API aceita qualquer string, e o100 sintoma chega como "o app está fora", não como manifesto inválido101- **`create-aws-record: "true"` nas annotations** sempre que o host tiver de resolver de fora.102 É por essa annotation que o **external-dns** cria o registro na zona da AWS. Sem ela o103 Ingress está correto e o nome não resolve — o sintoma chega como "o DNS não propagou", que104 manda investigar o lado errado por um bom tempo105- **Não declare `tls:` nem annotation de cert-manager.** HTTPS é automático e todos os106 sistemas rodam nele. O **cert-manager** emite um certificado curinga do ambiente pelo107 `ClusterIssuer letsencrypt-route53` (desafio DNS-01 na Route53), e um `TLSStore` chamado108 `default` o registra como certificado padrão do Traefik. O controller termina TLS para109 qualquer host servido, sem o Ingress pedir nada:110111 ```bash112 kubectl get clusterissuer # letsencrypt-route53113 kubectl get certificate -A | grep default-certificate # o curinga, READY=True114 kubectl get tlsstore -n kube-system default -o jsonpath='{.spec}'115 ```116117 Nada disso mora neste repositório — é plataforma, montada uma vez por cluster. Onde um118 Ingress de aplicação tem `tls:`, foi decisão pontual daquele serviço (certificado próprio,119 host fora do curinga); não é o padrão e não é para copiar120121```yaml122apiVersion: networking.k8s.io/v1123kind: Ingress124metadata:125 name: <app>126 namespace: <app>127 annotations:128 create-aws-record: "true" # o host precisa resolver de fora129spec: # sem bloco tls: — o HTTPS vem do certificado padrão130 ingressClassName: traefik131 rules:132 - host: <app>.basis.com.br # staging: <app>.stg.basis.com.br133 http:134 paths:135 - path: /136 pathType: Prefix137 backend:138 service:139 name: <app>140 port:141 name: http142```143144**De onde copiar:** `manifests/foundation/overlays/production/apps/editor-bpmn/ingress.yaml`.145146Copie o Ingress daí, não do app que você estava usando de modelo para outra coisa.147Referência boa para uma parte do manifesto não é referência boa para o resto dele — foi148assim, herdando o Ingress de um app cujo *deployment* servia de exemplo, que nasceram os149dois erros desta seção.150151## References disponíveis152153- [`references/rabbitmq-topology-pattern.md`](references/rabbitmq-topology-pattern.md) — Padrão Exchange/Queue/Binding limpos + Policy pra DLX/routing-key/TTL, DLX único compartilhado, migração de args→Policy154- [`references/kustomize-base-example.md`](references/kustomize-base-example.md) — Layout completo de `base/` + overlays, `envs:` em generators, hash suffix vs `disableNameSuffixHash`155- [`references/operator-secret-cheatsheet.md`](references/operator-secret-cheatsheet.md) — Padrão de Secret para Postgres-Zalando, RabbitMQ topology, Minio, MariaDB, Redis-otk, com snippets de `valueFrom`156- [`references/argocd-image-updater.md`](references/argocd-image-updater.md) — Application yaml + anotações Image Updater, regex CalVer, fluxo end-to-end e promote157158Snippets em `references/` são auto-contidos (cópias, não ponteiros) pra não quebrarem com refactors.