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
- SecurityContext fixo e imutável
- ResourceQuota e LimitRange por namespace
- NetworkPolicy opcional por ambiente
- ServiceAccount criada apenas se necessária
- PodDisruptionBudget para garantir disponibilidade
- readOnlyRootFilesystem com tmpdir separado
- Impedir que aluno passe chaves arbitrárias
- Validação obrigatória de campo via required
- 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:
-
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. -
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
existingSecretfoi preenchido, usa o Secret existente; - se
existingSecretestá vazio, usa o valor direto informado empassword.
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 ---
Nenhum comentário para exibir
Nenhum comentário para exibir