Ir para o conteúdo principal

Exemplos de uso do Helm

Helm — Templates Dinâmicos: Situações Reais de Uso

Exemplos de projetos reais em produção: Bitnami, Jaeger, Ory Hydra, Sentry, Fluent Bit, Grafana, Sitewards, Werf, Elasticsearch Curator, MySQL HA, Neo4j, Codecentric, Apache Superset, HMCTS/chart-job, e documentação oficial do Helm.

Ordenados do mais útil para o cenário da UFSC (multi-tenant, controle de acesso, RKE2, alunos + devs) para o mais específico.


Índice

Controle, segurança e isolamento

  1. SecurityContext fixo e imutável
  2. ResourceQuota e LimitRange por namespace
  3. NetworkPolicy opcional por ambiente
  4. ServiceAccount criada apenas se necessária
  5. PodDisruptionBudget para garantir disponibilidade
  6. readOnlyRootFilesystem com tmpdir separado
  7. Impedir que aluno passe chaves arbitrárias
  8. Validação obrigatória de campo via required
  9. Validação de combinação com fail

Secrets e configuração sensível 10. Secret gerado uma vez e nunca sobrescrito 11. Secrets do values.yaml codificados em base64 automaticamente 12. Variáveis de ambiente via Secret existente ou valor direto 13. Grupos de credenciais ativáveis por helper 14. envFrom: injetar ConfigMap inteiro como variáveis 15. envFrom: injetar Secret inteiro como variáveis 16. extraEnv: variáveis extras sem alterar o template

Deploy e ciclo de vida 17. Hook de migração de banco antes do deploy 18. Hook de backup antes de upgrade 19. Hook de validação pós-instalação 20. Reiniciar pods quando ConfigMap muda 21. HPA só em produção 22. CronJob com schedule configurável 23. Deploy condicional por versão do cluster 24. Estratégia de update configurável 25. terminationGracePeriodSeconds configurável

Saúde e observabilidade 26. Liveness e Readiness probes configuráveis 27. Startup probe para apps lentos na inicialização 28. NOTES.txt dinâmico com URL do serviço 29. helm test: validação automática pós-deploy

Ingress e exposição 30. Ingress que só existe se pedido 31. TLS no Ingress que só aparece se configurado 32. Múltiplos hosts no Ingress por laço 33. Annotations dinâmicas vindas de mapa

Configuração de aplicação 34. Variáveis de ambiente por laço 35. ConfigMap gerado a partir de mapa no values.yaml 36. Arquivo de configuração montado via ConfigMap 37. extraVolumes e extraVolumeMounts 38. Sidecars por laço 39. Init container condicional 40. Init containers extras por laço 41. Múltiplos Services de uma lista


1. SecurityContext fixo e imutável

Origem: todos os charts do Bitnami, helm/examples oficial Referência: https://github.com/helm/examples | https://github.com/bitnami/charts

O RKE2 aplica restricted:latest PodSecurity. Se o aluno puder sobrescrever o securityContext, pode tentar rodar como root.

Os campos de segurança são hardcoded no template — não existem no values.yaml. O usuário não tem como passá-los.

# templates/deployment.yaml
spec:
  template:
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 101
        runAsGroup: 101
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: app
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: [ALL]

Recurso usado: ausência de variável — bloqueio por omissão no values.yaml


2. ResourceQuota e LimitRange por namespace

Origem: oneuptime.com blog (2026) Referência: https://oneuptime.com/blog/post/2026-02-09-helm-namespace-creation-labeling/view

Sem cotas, um único projeto consome todos os recursos. Com alunos em paralelo, um deploy mal configurado afeta todos.

O chart cria ResourceQuota e LimitRange no namespace. O LimitRange garante limites padrão mesmo em containers sem resources declarado.

# values.yaml
resourceQuota:
  enabled: true
  hard:
    pods: "10"
    requests.cpu: "2"
    limits.cpu: "4"
    limits.memory: "4Gi"
limitRange:
  enabled: true
  limits:
    - type: Container
      default:
        cpu: 200m
        memory: 128Mi
      max:
        cpu: "1"
        memory: 512Mi
# templates/resourcequota.yaml
{{- if .Values.resourceQuota.enabled }}
apiVersion: v1
kind: ResourceQuota
metadata:
  name: {{ include "app.fullname" . }}-quota
spec:
  hard:
    {{- toYaml .Values.resourceQuota.hard | nindent 4 }}
{{- end }}
# templates/limitrange.yaml
{{- if .Values.limitRange.enabled }}
apiVersion: v1
kind: LimitRange
metadata:
  name: {{ include "app.fullname" . }}-limits
spec:
  limits:
    {{- range .Values.limitRange.limits }}
    - type: {{ .type }}
      {{- with .default }}
      default:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .max }}
      max:
        {{- toYaml . | nindent 8 }}
      {{- end }}
    {{- end }}
{{- end }}

Recurso usado: if + range + with + toYaml


3. NetworkPolicy opcional por ambiente

Origem: support.tools, mlkmhd/kubernetes-network-policy-helm-chart Referência: https://support.tools/integrating-network-policy-with-helm-for-enhanced-kubernetes-security/ | https://github.com/mlkmhd/kubernetes-network-policy-helm-chart

Sem NetworkPolicy, os pods do namespace podem ficar acessíveis para outros workloads do cluster quando há serviços ou portas expostas.

Até três políticas são geradas: deny-all de entrada padrão, allow para o ingress e, se configurado, allow para métricas do Prometheus.

# values.yaml
networkPolicy:
  enabled: true
  allowedNamespace: ingress-nginx
  metricsPort: 9090
# templates/networkpolicy.yaml
{{- if .Values.networkPolicy.enabled }}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: {{ .Release.Name }}-default-deny
spec:
  podSelector: {}
  policyTypes: [Ingress]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: {{ .Release.Name }}-allow-ingress
spec:
  podSelector:
    matchLabels:
      {{- include "app.selectorLabels" . | nindent 6 }}
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: {{ .Values.networkPolicy.allowedNamespace }}
      ports:
        - protocol: TCP
          port: {{ .Values.service.port }}
{{- if .Values.networkPolicy.metricsPort }}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: {{ .Release.Name }}-allow-metrics
spec:
  podSelector:
    matchLabels:
      {{- include "app.selectorLabels" . | nindent 6 }}
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: monitoring
      ports:
        - protocol: TCP
          port: {{ .Values.networkPolicy.metricsPort }}
{{- end }}
{{- end }}

Recurso usado: if gerando múltiplos recursos com ---


4. ServiceAccount criada apenas se necessária

Origem: helm/examples oficial, Bitnami nginx Referência: https://github.com/helm/examples/blob/main/charts/hello-world/templates/serviceaccount.yaml | https://github.com/bitnami/charts/blob/main/bitnami/nginx/README.md

Criar ServiceAccount desnecessária polui o namespace e aumenta a superfície de ataque.

Só cria se serviceAccount.create: true. Um helper resolve o nome correto em cada caso — criada usa o nome completo do chart; não criada usa default.

# values.yaml
serviceAccount:
  create: false
  name: ""
  annotations: {}
  automountToken: false
# templates/_helpers.tpl
{{- define "app.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
  {{- default (include "app.fullname" .) .Values.serviceAccount.name }}
{{- else }}
  {{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
# templates/serviceaccount.yaml
{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
  name: {{ include "app.serviceAccountName" . }}
  {{- with .Values.serviceAccount.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
automountServiceAccountToken: {{ .Values.serviceAccount.automountToken }}
{{- end }}

Recurso usado: if + with + define/include + default


5. PodDisruptionBudget para garantir disponibilidade

Origem: codecentric/helm-charts (keycloak), fluent/helm-charts Referência: https://github.com/codecentric/helm-charts/blob/master/charts/keycloak/templates/poddisruptionbudget.yaml | https://github.com/fluent/helm-charts/blob/main/charts/fluent-bit/values.yaml

Durante helm upgrade ou drain de node, o Kubernetes pode derrubar todos os pods de uma vez, causando downtime.

PDB só é criado se habilitado e com mais de uma réplica — com réplica única causaria deadlock no drain.

# values.yaml
replicaCount: 2
podDisruptionBudget:
  enabled: true
  maxUnavailable: "30%"
# templates/poddisruptionbudget.yaml
{{- if and .Values.podDisruptionBudget.enabled (gt (int .Values.replicaCount) 1) }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: {{ include "app.fullname" . }}
spec:
  selector:
    matchLabels:
      {{- include "app.selectorLabels" . | nindent 6 }}
  {{- if .Values.podDisruptionBudget.maxUnavailable }}
  maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }}
  {{- else }}
  minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
  {{- end }}
{{- end }}

Recurso usado: if com and + gt + int


6. readOnlyRootFilesystem com tmpdir separado

Origem: bambash/helm-cronjobs, padrão de segurança de containers Referência: https://github.com/bambash/helm-cronjobs/blob/master/templates/cronjob.yaml

readOnlyRootFilesystem: true é boa prática de segurança, mas apps que escrevem em /tmp quebram.

O template detecta readOnlyRootFilesystem ativo e adiciona automaticamente um emptyDir montado em /tmp.

# values.yaml
securityContext:
  readOnlyRootFilesystem: true
mountTmpDir: true
# templates/deployment.yaml
{{- $readOnlyFS := .Values.securityContext.readOnlyRootFilesystem | default false }}
{{- $addTmpDir  := and .Values.mountTmpDir $readOnlyFS }}
containers:
  - name: app
    securityContext:
      readOnlyRootFilesystem: {{ $readOnlyFS }}
    {{- if $addTmpDir }}
    volumeMounts:
      - name: tmp
        mountPath: /tmp
    {{- end }}
{{- if $addTmpDir }}
volumes:
  - name: tmp
    emptyDir: {}
{{- end }}

Recurso usado: variáveis locais + and + if encadeados


7. Impedir que aluno passe chaves arbitrárias

Origem: values.schema.json — padrão recomendado pela documentação oficial Referência: https://helm.sh/docs/topics/charts/#schema-files

Um aluno pode tentar passar hostNetwork: true ou nodeSelector para ganhar acesso privilegiado.

additionalProperties: false no schema rejeita qualquer chave não declarada — antes de chegar no cluster.

{
  "$schema": "http://json-schema.org/schema#",
  "additionalProperties": false,
  "properties": {
    "image":        { "type": "object" },
    "replicaCount": { "type": "integer", "minimum": 1, "maximum": 2 },
    "resources":    { "type": "object" },
    "ingress":      { "type": "object" },
    "env":          { "type": "array" }
  }
}

Tentativa de hostNetwork: true retorna:

Error: values don't meet the specifications of the schema(s):
- (root): Additional property hostNetwork is not allowed

Recurso usado: values.schema.json com additionalProperties: false


8. Validação obrigatória de campo via required

Origem: bitnami/charts, helm documentação oficial Referência: https://helm.sh/docs/howto/charts_tips_and_tricks/

O aluno esquece de preencher o hostname. O deploy roda com campo vazio e o Ingress fica inválido.

required falha imediatamente com mensagem clara se o campo estiver vazio.

# templates/ingress.yaml
spec:
  rules:
    - host: {{ required "ingress.host é obrigatório. Exemplo: meu-projeto.inf.ufsc.br" .Values.ingress.host | quote }}
# templates/deployment.yaml
image: "registry.setic.ufsc.br/{{ required "image.repository é obrigatório" .Values.image.repository }}:{{ .Values.image.tag }}"

Recurso usado: required


9. Validação de combinação com fail

Origem: Ory Hydra k8s Referência: https://github.com/ory/k8s/blob/master/helm/charts/hydra/templates/_helpers.tpl

O aluno coloca replicaCount: 0 ou TLS habilitado sem host. Combinações inválidas só aparecem como erro de runtime.

fail para o deploy com mensagem específica quando a combinação de valores é inválida.

# templates/_helpers.tpl
{{- if and .Values.ingress.tls (not .Values.ingress.host) }}
{{- fail "ingress.host é obrigatório quando ingress.tls está ativo" }}
{{- end }}

{{- if eq (int .Values.replicaCount) 0 }}
{{- fail "replicaCount não pode ser zero. Use o job stop-producao para parar o serviço." }}
{{- end }}

Recurso usado: fail + if + and + not


10. Secret gerado uma vez e nunca sobrescrito

Origem: Baeldung Ops, documentação oficial Helm "Tips and Tricks" Referência: https://www.baeldung.com/ops/helm-charts-best-practices | https://helm.sh/docs/howto/charts_tips_and_tricks/

Charts que criam bancos precisam de senha inicial. Um helm upgrade não pode gerar nova senha — o banco perderia o acesso.

lookup verifica se o Secret já existe. Na primeira instalação gera senha aleatória; em upgrades reutiliza a existente.

# templates/secret.yaml
{{- $secret := lookup "v1" "Secret" .Release.Namespace "meu-sistema-db" }}
{{- if $secret }}
apiVersion: v1
kind: Secret
metadata:
  name: meu-sistema-db
data:
  password: {{ $secret.data.password }}
{{- else }}
apiVersion: v1
kind: Secret
metadata:
  name: meu-sistema-db
data:
  password: {{ randAlphaNum 32 | b64enc }}
{{- end }}

Recurso usado: lookup + randAlphaNum + b64enc


11. Secrets do values.yaml codificados em base64 automaticamente

Origem: oneuptime.com blog, buildkite chart Referência: https://oneuptime.com/blog/post/2026-01-19-kubernetes-helm-charts-from-scratch/view

No campo data, Kubernetes espera valores em base64. Pedir que o aluno já passe em base64 é uma UX ruim e propensa a erro.

O template recebe valores em texto puro e aplica b64enc automaticamente.

# values.yaml
secrets:
  DB_PASSWORD: "minha-senha-em-texto-puro"
  API_KEY: "chave-secreta"
# templates/secret.yaml
{{- if .Values.secrets }}
apiVersion: v1
kind: Secret
metadata:
  name: {{ include "app.fullname" . }}
type: Opaque
data:
  {{- range $key, $value := .Values.secrets }}
  {{ $key }}: {{ $value | b64enc | quote }}
  {{- end }}
{{- end }}

Recurso usado: if + range sobre mapa + b64enc


12. Variáveis de ambiente via Secret existente ou valor direto

Origem: Jaeger helm-charts, Ory Hydra k8s
Referência: https://github.com/jaegertracing/helm-charts/blob/main/charts/jaeger/templates/_helpers.tpl | https://github.com/ory/k8s/blob/master/helm/charts/hydra/templates/_helpers.tpl

Algumas aplicações precisam receber senhas, tokens ou chaves como variáveis de ambiente.

Existem dois jeitos comuns de fazer isso:

  1. Usar um Secret que já existe no Kubernetes
    Melhor opção para produção. A senha fica guardada em um Secret e o chart apenas aponta para ele.

  2. Passar o valor direto pelo values.yaml
    Mais simples para teste ou laboratório, mas menos seguro, porque a senha aparece no arquivo de configuração do Helm.

O problema é que o template não deve escolher sempre um único modo.

Se ele sempre usar valueFrom.secretKeyRef, o pod falha quando o Secret não existe.

Se ele sempre usar value, a senha fica exposta diretamente no manifesto do pod.

Por isso, o template verifica:

  • se existingSecret foi preenchido, usa o Secret existente;
  • se existingSecret está vazio, usa o valor direto informado em password.

Opção 1 — usando um Secret já existente

# values.yaml — usando um Secret já existente
storage:
  cassandra:
    existingSecret: "cassandra-credentials"
    password: ""

Nesse caso, o chart vai buscar a senha dentro do Secret chamado cassandra-credentials.

A senha não fica escrita diretamente no values.yaml.

Opção 2 — usando senha direta no values.yaml

# values.yaml — usando senha direta
storage:
  cassandra:
    existingSecret: ""
    password: "minha-senha"

Nesse caso, como existingSecret está vazio, o chart usa diretamente o valor de password.

É mais simples para teste, mas não é o ideal para produção.

Template com escolha automática

# templates/deployment.yaml
{{- if .Values.storage.cassandra.existingSecret }}
- name: CASSANDRA_PASSWORD
  valueFrom:
    secretKeyRef:
      name: {{ .Values.storage.cassandra.existingSecret }}
      key: password
{{- else }}
- name: CASSANDRA_PASSWORD
  value: {{ .Values.storage.cassandra.password | quote }}
{{- end }}

Lógica em português simples

Se existingSecret foi preenchido:
    usa o Secret já existente no Kubernetes

Senão:
    usa a senha escrita diretamente no values.yaml

Recurso usado: if/else

Resumo: esse padrão deixa o chart mais flexível. Em produção, ele pode usar um Secret já existente. Em testes ou laboratórios, ele também permite passar a senha direto pelo values.yaml.


13. Grupos de credenciais ativáveis por helper

Origem: Yogesh Bangar / Medium — deployment.yaml com 130+ linhas refatorado Referência: https://medium.com/@yogeshbbangar/unlocking-the-power-of-helper-tpl-in-helm-charts-a-real-world-example-757050227581

Uma app usa DB, Kafka, Redis — cada um com credenciais. Nem todo ambiente usa todos os serviços.

O usuário habilita/desabilita grupos. O helper só injeta os ativos.

# values.yaml
externalSecrets:
  databaseCredentials:
    enabled: true
    name: meu-sistema-db
  kafkaCredentials:
    enabled: false
  redisCredentials:
    enabled: true
    name: meu-sistema-redis
# templates/_helpers.tpl
{{- define "app.envVars" -}}
{{- if .Values.externalSecrets.databaseCredentials.enabled }}
- name: DB_PASSWORD
  valueFrom:
    secretKeyRef:
      name: {{ .Values.externalSecrets.databaseCredentials.name }}
      key: db_password
{{- end }}
{{- if .Values.externalSecrets.redisCredentials.enabled }}
- name: REDIS_PASSWORD
  valueFrom:
    secretKeyRef:
      name: {{ .Values.externalSecrets.redisCredentials.name }}
      key: redis_password
{{- end }}
{{- end }}
# templates/deployment.yaml
env:
  {{- include "app.envVars" . | nindent 6 }}

Recurso usado: if por grupo + define/include


14. envFrom: injetar ConfigMap inteiro como variáveis

Origem: Werf/website (Rails), Neo4j-helm Referência: https://werf.io/guides/rails/200_real_apps/080_config.html | https://neo4j.com/labs/neo4j-helm/1.0.0/operations/

Uma app tem dezenas de variáveis não-sensíveis. Listá-las uma a uma com env: é verboso.

Um ConfigMap guarda todas. O envFrom injeta o ConfigMap inteiro com uma linha.

# values.yaml
config:
  LOG_LEVEL: "info"
  NODE_ENV: "production"
  MAX_CONNECTIONS: "100"
# templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "app.fullname" . }}
data:
  {{- range $key, $value := .Values.config }}
  {{ $key }}: {{ $value | quote }}
  {{- end }}
# templates/deployment.yaml
containers:
  - name: app
    envFrom:
      - configMapRef:
          name: {{ include "app.fullname" . }}

Recurso usado: range sobre mapa + envFrom


15. envFrom: injetar Secret inteiro como variáveis

Origem: oneuptime.com blog, Neo4j-helm Referência: https://oneuptime.com/blog/post/2026-01-19-kubernetes-helm-charts-from-scratch/view | https://neo4j.com/labs/neo4j-helm/1.0.0/operations/

Igual ao anterior, mas para variáveis sensíveis. Passar cada senha com valueFrom.secretKeyRef é verboso.

O chart cria um Secret com todos os valores. O envFrom.secretRef injeta todos de uma vez.

# values.yaml
secrets:
  DB_PASSWORD: "senha-do-banco"
  SMTP_PASSWORD: "senha-email"
# templates/deployment.yaml
containers:
  - name: app
    envFrom:
      - configMapRef:
          name: {{ include "app.fullname" . }}
      {{- if .Values.secrets }}
      - secretRef:
          name: {{ include "app.fullname" . }}
      {{- end }}

Recurso usado: if + envFrom.secretRef


16. extraEnv: variáveis extras sem alterar o template

Origem: helm/charts issue #11981, keycloak chart, criblio/helm-charts Referência: https://github.com/helm/charts/issues/11981 | https://hackernoon.com/the-art-of-the-helm-chart-patterns-from-the-official-kubernetes-charts-8a7cafa86d12

O usuário precisa adicionar variáveis que o chart não previu, sem alterar o template.

O chart expõe extraEnv como lista livre. O usuário passa qualquer variável no formato Kubernetes nativo.

# values.yaml
extraEnv:
  - name: DEBUG
    value: "true"
  - name: FEATURE_FLAG_X
    valueFrom:
      configMapKeyRef:
        name: feature-flags
        key: x
# templates/deployment.yaml
containers:
  - name: app
    env:
      # variáveis base do chart ...
      {{- with .Values.extraEnv }}
      {{- toYaml . | nindent 12 }}
      {{- end }}

Recurso usado: with + toYaml


17. Hook de migração de banco antes do deploy

Origem: Sentry helm/charts, Werf/website (Laravel), oneuptime.com Referência: https://github.com/helm/charts/blob/master/stable/sentry/templates/hooks/db-init.job.yaml | https://oneuptime.com/blog/post/2026-01-17-helm-hooks-pre-post-install-upgrade/view | https://rtfm.co.ua/en/kubernetes-running-sql-migrations-with-kubernetes-job-and-helm-hook/

helm upgrade substitui pods imediatamente. Se a nova versão espera colunas inexistentes, a app quebra.

Job com helm.sh/hook: pre-upgrade,pre-install roda antes de qualquer recurso. Se falhar, o deploy para.

# values.yaml
migrations:
  enabled: true
  command: ["python", "manage.py", "migrate", "--noinput"]
  timeoutSeconds: 300
# templates/db-migration-hook.yaml
{{- if .Values.migrations.enabled }}
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "app.fullname" . }}-migrate
  annotations:
    "helm.sh/hook": pre-upgrade,pre-install
    "helm.sh/hook-weight": "-5"
    "helm.sh/hook-delete-policy": before-hook-creation
spec:
  backoffLimit: 0
  activeDeadlineSeconds: {{ .Values.migrations.timeoutSeconds | default 300 }}
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          command: {{ toJson .Values.migrations.command }}
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: {{ include "app.fullname" . }}-db-secret
                  key: url
{{- end }}

Os hooks têm peso (hook-weight) — menor valor roda primeiro. Os tipos disponíveis: pre-install, post-install, pre-upgrade, post-upgrade, pre-delete, post-delete, pre-rollback, post-rollback.

Recurso usado: if + toJson + anotações helm.sh/hook


18. Hook de backup antes de upgrade

Origem: oneuptime.com blog Referência: https://oneuptime.com/blog/post/2026-01-17-helm-hooks-pre-post-install-upgrade/view

Fazer backup antes de cada upgrade manualmente é um passo esquecível.

Job com helm.sh/hook: pre-upgrade dispara o backup automaticamente.

# values.yaml
backup:
  enabled: true
  pvcName: app-backups
# templates/backup-hook.yaml
{{- if .Values.backup.enabled }}
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "app.fullname" . }}-pre-upgrade-backup
  annotations:
    "helm.sh/hook": pre-upgrade
    "helm.sh/hook-weight": "-10"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  backoffLimit: 1
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: backup
          image: postgres:15
          command:
            - /bin/sh
            - -c
            - |
              TIMESTAMP=$(date +%Y%m%d_%H%M%S)
              pg_dump "$DATABASE_URL" > /backups/backup_$TIMESTAMP.sql
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: {{ include "app.fullname" . }}-db
                  key: url
          volumeMounts:
            - name: backups
              mountPath: /backups
      volumes:
        - name: backups
          persistentVolumeClaim:
            claimName: {{ .Values.backup.pvcName }}
{{- end }}

O peso -10 roda antes da migração (peso -5) — a ordem é garantida.

Recurso usado: if + helm.sh/hook-weight


19. Hook de validação pós-instalação

Origem: HMCTS/chart-job, sagar-jadhav/helm-chart-hooks-example Referência: https://github.com/hmcts/chart-job | https://github.com/sagar-jadhav/helm-chart-hooks-example | https://helm.sh/docs/topics/charts_hooks/

Como confirmar que o deploy funcionou além de os pods estarem Running? A app pode subir mas retornar 500.

Job com helm.sh/hook: post-install,post-upgrade faz um curl na rota de healthcheck. Se falhar, o Helm marca o release como falhado.

# templates/post-install-test-hook.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "app.fullname" . }}-post-install-validate
  annotations:
    "helm.sh/hook": post-install,post-upgrade
    "helm.sh/hook-weight": "5"
    "helm.sh/hook-delete-policy": hook-succeeded
spec:
  backoffLimit: 3
  template:
    spec:
      restartPolicy: OnFailure
      containers:
        - name: validate
          image: curlimages/curl:latest
          command:
            - /bin/sh
            - -c
            - |
              curl --fail --max-time 10 \
                http://{{ include "app.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port }}/healthz

Recurso usado: helm.sh/hook: post-install


20. Reiniciar pods quando ConfigMap muda

Origem: documentação oficial do Helm "Tips and Tricks" Referência: https://helm.sh/docs/howto/charts_tips_and_tricks/

helm upgrade atualiza o ConfigMap mas não reinicia os pods. A app continua com a config antiga.

Annotation com SHA256 do ConfigMap no Deployment. Quando o conteúdo muda, o hash muda, o Kubernetes detecta e dispara rollout.

# templates/deployment.yaml
spec:
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
        checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}

Recurso usado: include + sha256sum


21. HPA só em produção

Origem: helm/examples, k8saas documentation Referência: https://doc.kaas.thalesdigital.io/docs/BestPractices/BYOHelmchart | https://github.com/helm/examples

HPA em desenvolvimento esconde problemas de performance e consome recursos desnecessariamente.

HPA é opcional e controlado por flag.

# values.yaml
environment: producao
autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70
# templates/hpa.yaml
{{- if and .Values.autoscaling.enabled (eq .Values.environment "producao") }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: {{ include "app.fullname" . }}
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: {{ include "app.fullname" . }}
  minReplicas: {{ .Values.autoscaling.minReplicas }}
  maxReplicas: {{ .Values.autoscaling.maxReplicas }}
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}

Recurso usado: if + and + eq


22. CronJob com schedule configurável

Origem: bambash/helm-cronjobs, HMCTS/chart-job, elasticsearch-curator Referência: https://github.com/bambash/helm-cronjobs | https://github.com/hmcts/chart-job | https://github.com/helm/charts/blob/master/stable/elasticsearch-curator/templates/cronjob.yaml

Jobs agendados têm horários diferentes por ambiente — backup meia-noite em produção, a cada minuto em dev.

O schedule, a política de concorrência e o estado de suspensão são parâmetros.

# values.yaml
cronjob:
  enabled: true
  schedule: "0 0 * * *"
  jobRestartPolicy: Never
  suspend: false
  concurrencyPolicy: Forbid
  command: ["./scripts/backup.sh"]
# templates/cronjob.yaml
{{- if .Values.cronjob.enabled }}
apiVersion: batch/v1
kind: CronJob
metadata:
  name: {{ include "app.fullname" . }}-cronjob
spec:
  schedule: {{ .Values.cronjob.schedule | quote }}
  suspend: {{ .Values.cronjob.suspend }}
  concurrencyPolicy: {{ .Values.cronjob.concurrencyPolicy }}
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: {{ .Values.cronjob.jobRestartPolicy }}
          containers:
            - name: job
              image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
              command: {{ toJson .Values.cronjob.command }}
{{- end }}

Recurso usado: if + toJson


23. Deploy condicional por versão do cluster

Origem: bitnami/charts, documentação oficial Helm .Capabilities Referência: https://helm.sh/docs/chart_template_guide/builtin_objects/

APIs do Kubernetes mudam entre versões. Ingress usava v1beta1 até o 1.18 e passou para v1 no 1.19.

O template consulta a versão real do cluster e escolhe a API correta.

# templates/ingress.yaml
{{- if semverCompare ">=1.19" .Capabilities.KubeVersion.Version }}
apiVersion: networking.k8s.io/v1
{{- else }}
apiVersion: networking.k8s.io/v1beta1
{{- end }}
kind: Ingress
metadata:
  name: {{ include "app.fullname" . }}
spec:
  {{- if semverCompare ">=1.18" .Capabilities.KubeVersion.Version }}
  ingressClassName: {{ .Values.ingress.className }}
  {{- end }}

Recurso usado: semverCompare + .Capabilities.KubeVersion


24. Estratégia de update configurável

Origem: bjw-s/helm-charts common library, bitnami/charts Referência: https://raw.githubusercontent.com/bjw-s/helm-charts/main/charts/library/common/values.yaml

Deployments com PVC precisam de Recreate para evitar conflito de montagem. Apps sem PVC usam RollingUpdate.

A estratégia é configurável.

# values.yaml
updateStrategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 1
    maxUnavailable: 0
# templates/deployment.yaml
spec:
  strategy:
    type: {{ .Values.updateStrategy.type }}
    {{- if eq .Values.updateStrategy.type "RollingUpdate" }}
    rollingUpdate:
      maxSurge: {{ .Values.updateStrategy.rollingUpdate.maxSurge }}
      maxUnavailable: {{ .Values.updateStrategy.rollingUpdate.maxUnavailable }}
    {{- end }}

Recurso usado: if + eq


25. terminationGracePeriodSeconds configurável

Origem: Grafana tempo-distributed, fluent-bit helm-charts Referência: https://github.com/grafana/helm-charts/issues/3611 | https://github.com/fluent/helm-charts/blob/main/charts/fluent-bit/values.yaml

Apps com conexões de longa duração (websockets, streaming) precisam de mais tempo para fechar graciosamente. 30s padrão pode não ser suficiente.

# values.yaml
terminationGracePeriodSeconds: 60
# templates/deployment.yaml
spec:
  template:
    spec:
      {{- if .Values.terminationGracePeriodSeconds }}
      terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }}
      {{- end }}

Recurso usado: if


26. Liveness e Readiness probes configuráveis

Origem: sitewards/helm-chart, Grafana helm-charts issue #3185, k8saas Referência: https://github.com/sitewards/helm-chart/blob/master/chart/templates/deployment.yaml | https://github.com/grafana/helm-charts/issues/3185 | https://doc.kaas.thalesdigital.io/docs/BestPractices/BYOHelmchart

Apps diferentes têm rotas de healthcheck diferentes e tempos de inicialização diferentes.

Os parâmetros das probes são totalmente configuráveis. O bloco só aparece se habilitado.

# values.yaml
livenessProbe:
  enabled: true
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 15
  periodSeconds: 20
  failureThreshold: 3
readinessProbe:
  enabled: true
  httpGet:
    path: /ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 10
  failureThreshold: 3
# templates/deployment.yaml
{{- if .Values.livenessProbe.enabled }}
livenessProbe:
  httpGet:
    path: {{ .Values.livenessProbe.httpGet.path }}
    port: {{ .Values.livenessProbe.httpGet.port }}
  initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
  periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
  failureThreshold: {{ .Values.livenessProbe.failureThreshold }}
{{- end }}
{{- if .Values.readinessProbe.enabled }}
readinessProbe:
  httpGet:
    path: {{ .Values.readinessProbe.httpGet.path }}
    port: {{ .Values.readinessProbe.httpGet.port }}
  initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
  periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
  failureThreshold: {{ .Values.readinessProbe.failureThreshold }}
{{- end }}

Recurso usado: if com valores aninhados


27. Startup probe para apps lentos na inicialização

Origem: k8saas documentation, Andrew Lock blog Referência: https://doc.kaas.thalesdigital.io/docs/BestPractices/BYOHelmchart | https://andrewlock.net/deploying-asp-net-core-applications-to-kubernetes-part-6-adding-health-checks-with-liveness-readiness-and-startup-probes/

Apps Java/Spring que demoram 60+ segundos causam loops de restart se a livenessProbe disparar antes de estarem prontas.

startupProbe dá um budget de tempo para a inicialização. failureThreshold * periodSeconds = tempo máximo.

# values.yaml
startupProbe:
  enabled: true
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 30
  periodSeconds: 10
# templates/deployment.yaml
{{- if .Values.startupProbe.enabled }}
startupProbe:
  httpGet:
    path: {{ .Values.startupProbe.httpGet.path }}
    port: {{ .Values.startupProbe.httpGet.port }}
  failureThreshold: {{ .Values.startupProbe.failureThreshold }}
  periodSeconds: {{ .Values.startupProbe.periodSeconds }}
{{- end }}

Recurso usado: if


28. NOTES.txt dinâmico com URL do serviço

Origem: helm create padrão, codersociety best practices Referência: https://codersociety.com/blog/articles/helm-best-practices | https://helm.sh/docs/topics/charts/

Após helm install, o usuário não sabe como acessar o serviço.

NOTES.txt é um template que imprime instruções corretas com base nos valores reais do deploy.

# templates/NOTES.txt
Aplicação {{ .Release.Name }} implantada com sucesso!

{{- if .Values.ingress.enabled }}
Acesso externo:
  URL: http{{ if .Values.ingress.tls }}s{{ end }}://{{ .Values.ingress.host }}
{{- else }}
Acesso via port-forward:
  kubectl port-forward -n {{ .Release.Namespace }} svc/{{ include "app.fullname" . }} 8080:{{ .Values.service.port }}
  Acesse: http://localhost:8080
{{- end }}

Verificar status:
  helm status {{ .Release.Name }} -n {{ .Release.Namespace }}
  kubectl get pods -n {{ .Release.Namespace }}

Recurso usado: if/else + variáveis de Release + include


29. helm test: validação automática pós-deploy

Origem: HMCTS/chart-job, codersociety best practices Referência: https://helm.sh/docs/topics/chart_tests/ | https://codersociety.com/blog/articles/helm-best-practices

Como rodar testes automatizados após o deploy sem criar infraestrutura separada?

Pod com helm.sh/hook: test roda apenas quando o usuário executa helm test. Diferente dos hooks de ciclo de vida, este é sob demanda.

# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
  name: {{ include "app.fullname" . }}-test-connection
  annotations:
    "helm.sh/hook": test
spec:
  restartPolicy: Never
  containers:
    - name: wget
      image: busybox
      command:
        - wget
        - --spider
        - --server-response
        - 'http://{{ include "app.fullname" . }}:{{ .Values.service.port }}/healthz'

Executar:

helm test <release> -n <namespace>

Recurso usado: helm.sh/hook: test


30. Ingress que só existe se pedido

Origem: praticamente todo chart do Bitnami Referência: https://github.com/bitnami/charts/blob/main/bitnami/nginx/values.yaml

Apps internas não precisam de Ingress. Criar sempre seria um risco de segurança.

# templates/ingress.yaml
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "app.fullname" . }}
spec:
  ingressClassName: {{ .Values.ingress.className }}
  rules:
    - host: {{ .Values.ingress.host | quote }}
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: {{ include "app.fullname" . }}
                port:
                  number: {{ .Values.service.port }}
{{- end }}

Recurso usado: if


31. TLS no Ingress que só aparece se configurado

Origem: Bitnami nginx, keycloak, kibana Referência: https://github.com/bitnami/charts/blob/main/bitnami/keycloak/values.yaml

Bloco tls: com referência a Secret inexistente quebra o Ingress.

# templates/ingress.yaml (trecho)
spec:
  {{- if .Values.ingress.tls }}
  tls:
    - hosts:
        - {{ .Values.ingress.host | quote }}
      secretName: {{ printf "%s-tls" .Values.ingress.host }}
  {{- end }}
  rules:
    - host: {{ .Values.ingress.host | quote }}

Recurso usado: if aninhado + printf


32. Múltiplos hosts no Ingress por laço

Origem: nginx-ingress, Bitnami em geral Referência: https://helm.sh/docs/chart_template_guide/control_structures/

App responde por vários domínios. YAML estático exigiria duplicação manual.

# templates/ingress.yaml
spec:
  rules:
    {{- range .Values.ingress.hosts }}
    - host: {{ .host | quote }}
      http:
        paths:
          {{- range .paths }}
          - path: {{ .path }}
            pathType: {{ .pathType }}
            backend:
              service:
                name: {{ include "app.fullname" $ }}
                port:
                  number: {{ $.Values.service.port }}
          {{- end }}
    {{- end }}

$ acessa o escopo raiz de dentro do range.

Recurso usado: range aninhado


33. Annotations dinâmicas vindas de mapa

Origem: Bitnami nginx, fluent/helm-charts Referência: https://github.com/bitnami/charts/blob/main/bitnami/nginx/values.yaml | https://github.com/fluent/helm-charts/blob/main/charts/fluent-bit/values.yaml

O chart não pode prever quais annotations cada usuário vai precisar (cert-manager, prometheus, datadog).

# values.yaml
ingress:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
# templates/ingress.yaml
metadata:
  {{- with .Values.ingress.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}

with garante que o bloco não aparece se o mapa estiver vazio.

Recurso usado: with + toYaml


34. Variáveis de ambiente por laço

Origem: Yogesh Bangar / Medium, comunidade DEV.to Referência: https://dev.to/manjunath_kotabal_3d1e736/advanced-concepts-in-helm-templates-1oo6

Listar variáveis no template cria acoplamento — cada nova variável exige alterar o chart.

# values.yaml
env:
  - name: NODE_ENV
    value: production
  - name: LOG_LEVEL
    value: warn
# templates/deployment.yaml
env:
  {{- range .Values.env }}
  - name: {{ .name }}
    value: {{ .value | quote }}
  {{- end }}

Recurso usado: range sobre lista de objetos


35. ConfigMap gerado a partir de mapa no values.yaml

Origem: oneuptime.com blog, Werf/website Referência: https://oneuptime.com/blog/post/2026-01-19-kubernetes-helm-charts-from-scratch/view

O conteúdo do ConfigMap muda por ambiente. Com YAML estático, um arquivo diferente por ambiente.

# values.yaml
config:
  LOG_LEVEL: "info"
  NODE_ENV: "production"
  MAX_CONNECTIONS: "100"
# templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "app.fullname" . }}
data:
  {{- range $key, $value := .Values.config }}
  {{ $key }}: {{ $value | quote }}
  {{- end }}

Recurso usado: range sobre mapa com $key, $value


36. Arquivo de configuração montado via ConfigMap

Origem: Werf/website, criblio/helm-charts Referência: https://werf.io/guides/rails/200_real_apps/080_config.html

Apps leem configuração de arquivos (nginx.conf, app.properties). Embutir no template hardcoda o conteúdo.

O arquivo inteiro vem do values.yaml como string multiline e é montado como arquivo no pod.

# values.yaml
configFiles:
  nginx.conf: |
    server {
      listen 80;
      location / {
        proxy_pass http://localhost:8080;
      }
    }
# templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "app.fullname" . }}-files
data:
  {{- range $filename, $content := .Values.configFiles }}
  {{ $filename }}: |-
{{ $content | nindent 4 }}
  {{- end }}
# templates/deployment.yaml
volumes:
  - name: config-files
    configMap:
      name: {{ include "app.fullname" . }}-files
containers:
  - name: app
    volumeMounts:
      - name: config-files
        mountPath: /etc/nginx/conf.d

Recurso usado: range sobre mapa + string multiline


37. extraVolumes e extraVolumeMounts

Origem: apache/superset issue #16359, criblio/helm-charts, univention/common-helm Referência: https://github.com/apache/superset/issues/16359 | https://github.com/criblio/helm-charts/blob/master/common_docs/EXTRA_EXAMPLES.md

O usuário precisa montar um Secret ou ConfigMap externo que o chart não previu.

O chart expõe extraVolumes e extraVolumeMounts como listas livres no formato Kubernetes nativo.

# values.yaml
extraVolumes:
  - name: custom-certs
    secret:
      secretName: corporate-ca-certs
extraVolumeMounts:
  - name: custom-certs
    mountPath: /etc/ssl/certs/corporate
    readOnly: true
# templates/deployment.yaml
volumes:
  - name: config
    configMap:
      name: {{ include "app.fullname" . }}
  {{- with .Values.extraVolumes }}
  {{- toYaml . | nindent 2 }}
  {{- end }}
containers:
  - name: app
    volumeMounts:
      - name: config
        mountPath: /etc/config
      {{- with .Values.extraVolumeMounts }}
      {{- toYaml . | nindent 6 }}
      {{- end }}

Recurso usado: with + toYaml para listas livres


38. Sidecars por laço

Origem: Bitnami kafka, criblio/helm-charts Referência: https://github.com/bitnami/charts/blob/main/bitnami/kafka/README.md | https://github.com/criblio/helm-charts/blob/master/common_docs/EXTRA_EXAMPLES.md

Apps precisam de containers auxiliares (log exporter, metrics proxy). Hardcodar no template engessa o chart.

# values.yaml
sidecars:
  - name: log-exporter
    image: fluent/fluent-bit:2.1
    ports:
      - containerPort: 2020
# templates/deployment.yaml
containers:
  - name: app
    image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
  {{- range .Values.sidecars }}
  - name: {{ .name }}
    image: {{ .image }}
    {{- if .ports }}
    ports:
      {{- range .ports }}
      - containerPort: {{ .containerPort }}
      {{- end }}
    {{- end }}
  {{- end }}

Recurso usado: range + if interno


39. Init container condicional

Origem: elasticsearch-curator chart, bitnami/redis Referência: https://github.com/helm/charts/blob/master/stable/elasticsearch-curator/templates/cronjob.yaml

Algumas apps precisam de init container para ajustar permissões de volume. Para outras é desperdício.

# values.yaml
volumePermissions:
  enabled: true
  image: bitnami/minideb:buster
# templates/deployment.yaml
{{- if .Values.volumePermissions.enabled }}
initContainers:
  - name: volume-permissions
    image: {{ .Values.volumePermissions.image }}
    command: ["/bin/sh", "-c", "chown -R 1001:1001 /data"]
    volumeMounts:
      - name: data
        mountPath: /data
{{- end }}

Recurso usado: if


40. Init containers extras por laço

Origem: elasticsearch-curator chart, sendblocks/generic-helm-chart Referência: https://github.com/helm/charts/blob/master/stable/elasticsearch-curator/templates/cronjob.yaml

O usuário precisa de init containers personalizados sem alterar o chart.

# values.yaml
extraInitContainers:
  wait-for-db:
    image: busybox
    command: ['sh', '-c', 'until nc -z db-service 5432; do echo waiting; sleep 2; done']
# templates/deployment.yaml
{{- if or .Values.volumePermissions.enabled .Values.extraInitContainers }}
initContainers:
  {{- if .Values.volumePermissions.enabled }}
  - name: volume-permissions
    # ... init container padrão do chart
  {{- end }}
  {{- range $key, $value := .Values.extraInitContainers }}
  - name: {{ $key }}
    {{- toYaml $value | nindent 4 }}
  {{- end }}
{{- end }}

Recurso usado: if + range + toYaml sobre mapa


41. Múltiplos Services de uma lista

Origem: Baeldung Helm best practices Referência: https://www.baeldung.com/ops/helm-charts-best-practices

App expõe web, admin e métricas em portas diferentes. Um template por Service seria repetitivo.

# values.yaml
services:
  - name: web
    type: ClusterIP
    port: 80
    targetPort: 8080
  - name: metrics
    type: ClusterIP
    port: 9090
    targetPort: 9090
# templates/services.yaml
{{- range .Values.services }}
---
apiVersion: v1
kind: Service
metadata:
  name: {{ include "app.fullname" $ }}-{{ .name }}
  labels:
    {{- include "app.labels" $ | nindent 4 }}
spec:
  type: {{ .type }}
  ports:
    - port: {{ .port }}
      targetPort: {{ .targetPort }}
  selector:
    {{- include "app.selectorLabels" $ | nindent 4 }}
{{- end }}

Recurso usado: range gerando múltiplos recursos com ---