Missão Helm: industrializar uma implantação multi-ambiente
15 min
Projeto 13 — Helm no Kubernetes · Nível intermédio → avançado · Duração estimada: 4 a 6 h
Parte de uma aplicação composta por dois serviços Python (um portail visual e uma api backend) e vai implantá-la três vezes, lado a lado — em DEV (azul), STAGING (laranja), PROD (verde) — com um único Chart Helm e três ficheiros de valores. No fim, fará um helm upgrade e depois um helm rollback, e reparará três templates cheios de erros reais vistos em empresa.
Trabalha numa equipa em que cada nova versão de uma aplicação deve passar por três ambientes:
DEV — caixa de areia dos programadores, dados descartáveis, uma só réplica.
STAGING — pré-produção, dados de teste, duas réplicas para validar a escalabilidade.
PROD — produção, dados reais, três réplicas no mínimo, nenhum tempo de inatividade tolerado.
Hoje, a equipa copia-cola os mesmos manifestos YAML para cada ambiente, a alterar à mão os valores diferentes. Resultado: os ficheiros divergem, um patch aplicado em dev não chega a prod, e uma implantação pede meia jornada.
A sua missão: industrializar tudo isto com Helm. Um só Chart, três ficheiros de valores, um comando por ambiente. Provará que funciona ao mostrar três painéis lado a lado no navegador — cada um com a sua cor, o seu número de Pods e a sua própria mensagem.
Conceitos essenciais antes de começar
Este documento é autónomo. Não precisa de nenhuma referência externa para o terminar.
1. O papel do Helm numa frase
O Helm gera manifestos Kubernetes a partir de templates e de variáveis. Lá onde kubectl apply toma um YAML estático, o Helm toma um template YAML e um ficheiro de valores, produz o YAML final e depois aplica-o como uma unidade versionada a que se chama uma release.
2. Anatomia de um Chart
mon-chart/├── Chart.yaml # metadata (nom, version)├── values.yaml # valeurs PAR DEFAUT└── templates/ # templates YAML ├── deployment.yaml ├── service.yaml └── _helpers.tpl # fonctions de template partagees (nom prefixe _)
Os ficheiros cujo nome começa por _ não produzem nenhum manifesto: servem para definir «helpers» reutilizáveis via {{ include "nom" . }}.
3. A sintaxe dos templates (Go template)
Escrito
Renderizado
{{ .Values.portail.replicas }}
O valor definido em values.yaml
{{ .Release.Name }}
O nome que passou a helm install (ex.: hedge-dev)
{{ .Chart.Name }}
O nome do chart (definido em Chart.yaml)
{{ .Chart.AppVersion }}
A versão da aplicação (definida em Chart.yaml)
{{ include "hedge.labels" . }}
Chamada de um helper definido em _helpers.tpl
{{- ... -}}
O - suprime os espaços antes/depois da renderização
Uma release é uma instalação de um chart. O mesmo chart pode ser instalado várias vezes, cada um com um nome de release diferente (hedge-dev, hedge-staging, hedge-prod) — é o fundamento do multiambiente.
{{ .Release.Name }} muda em cada install, {{ .Chart.Name }} permanece idêntico. Retenha este contraste: é central.
6. A regra de ouro do seletor imutável
O campo spec.selector.matchLabels de um Deployment fica fixo de uma vez por todas na criação. Se o seu template puser neste campo um valor que pode mudar (como uma versão, um ambiente, uma data), o primeiro helm install funciona, mas o primeiro helm upgrade falha com:
spec.selector: Invalid value: ...: field is immutable
Regra a gravar em pedra: em matchLabels, coloque apenas coisas que NUNCA mudarão para esta instância — tipicamente name, instance, component.
A arquitetura alvo: DEV / STAGING / PROD
Vai implantar o mesmo chart em três namespaces distintos, cada um com os seus parâmetros:
Parâmetro
DEV
STAGING
PROD
Namespace
hedge-dev
hedge-staging
hedge-prod
Release name
hedge-dev
hedge-staging
hedge-prod
Cor do bandeau
azul#2563eb
laranja#ea580c
verde#16a34a
Mensagem
«Ambiente de desenvolvimento…»
«Pré-produção — dados de teste apenas»
«Produção — cada ação tem um impacto real»
Réplicas portail
1
2
3
Réplicas api
1
2
3
Porta exposta (NodePort)
30130
30131
30132
URL de teste
http://localhost:30130
http://localhost:30131
http://localhost:30132
No fim do TP, abre três separadores lado a lado e vê três painéis coloridos de forma diferente, cada um a mostrar o seu ambiente, a sua versão, os seus Pods e o estado do seu backend.
Organização dos ficheiros
Parte da árvore seguinte — o ANEXO fornece o conteúdo exato de cada ficheiro:
projet13-kubernetes-helm-tp/├── 00-ENONCE.md <- este documento│├── apps/ <- O CODIGO (ANEXO A) — NAO MODIFICAR│ ├── portail/│ │ ├── app.py│ │ ├── requirements.txt│ │ └── Dockerfile│ └── api/│ ├── app.py│ ├── requirements.txt│ └── Dockerfile│├── chart/ <- O CHART A COMPLETAR│ ├── Chart.yaml <- esqueleto (ANEXO B)│ ├── values.yaml <- valores por omissao (ANEXO B)│ ││ ├── environments/ <- A SUA VEZ│ │ ├── values-dev.yaml <- esqueleto TODO (ANEXO B)│ │ ├── values-staging.yaml <- esqueleto TODO (ANEXO B)│ │ └── values-prod.yaml <- esqueleto TODO (ANEXO B)│ ││ ├── templates/ <- A SUA VEZ│ │ ├── _helpers.tpl <- esqueleto TODO (ANEXO B)│ │ ├── portail-deployment.yaml <- esqueleto TODO (ANEXO B)│ │ ├── portail-service.yaml <- esqueleto TODO (ANEXO B)│ │ ├── api-deployment.yaml <- esqueleto TODO (ANEXO B)│ │ └── api-service.yaml <- esqueleto TODO (ANEXO B)│ ││ └── casses/ <- FORNECIDOS mas DEFEITUOSOS (ANEXO C)│ ├── casse-1-configmap.yaml│ ├── casse-2-worker-deployment.yaml│ └── casse-3-cache-deployment.yaml│├── outils/│ └── valider.ps1 <- FORNECIDO (ANEXO D)│└── RAPPORT.md <- A REDIGIR por si
Ponto crucial: os ficheiros de chart/casses/não estão em chart/templates/. O Helm portanto não os carrega automaticamente. A missão 6 pedir-lhe-á que os copie um a um para templates/ a fim de observar o erro, e depois que os repare antes de os guardar.
As regras do jogo
Proibição absoluta de modificar a pasta apps/. O código da aplicação já está escrito — você é o DevOps, não o programador.
Só modifica os ficheiros da pasta chart/.
Nenhuma configuração de ambiente escrita em concreto num template: replicas, nodePort, cor, mensagem, ambiente — tudo deve vir de um .Values.*.
Os 3 ficheiros values-<env>.yaml devem diferir unicamente pelos valores que distinguem DEV, STAGING e PROD. Um ficheiro values-prod.yaml que redefine inutilmente image.repository ou service.targetPort é um erro — essas coisas vêm de values.yaml.
Trabalha no Kubernetes integrado no Docker Desktop.
Preparação
Pré-requisitos — a verificar uma só vez
O Docker Desktop está arrancado e o Kubernetes está ativado (Settings → Kubernetes → Enable Kubernetes).
O Docker Desktop tem pelo menos 4 Go de RAM atribuídos (Settings → Resources → Memory ≥ 4 GB). Este TP faz correr 12 Pods em simultâneo (1+1 + 2+2 + 3+3).
portail-deployment.yaml — um Deployment que usa .Values.portail.replicas, .Values.portail.image.*, e injeta as variáveis de ambiente ENVIRONMENT, APP_VERSION, THEME_COLOR, BANNIERE_MESSAGE, BACKEND_URL, REPLICAS_INFO.
portail-service.yaml — um Service NodePort que aponta para os Pods do portal.
api-deployment.yaml — um Deployment para a api (variáveis ENVIRONMENT, APP_VERSION).
api-service.yaml — um Service ClusterIP.
Ponto-chave: a variável BACKEND_URL do portal deve conter o nome do Service api construído com {{ .Release.Name }} (por exemplo http://hedge-dev-api), não um nome em concreto.
Validação:
powershell
helm template check .\chart -f .\chart\environments\values-dev.yaml# doit afficher 2 Deployments + 2 Services, tous prefixes par "check-"
Missão 3 — Escrever helpers limpos (15 pontos)
Complete chart/templates/_helpers.tpl com três helpers:
kubectl get pods -n hedge-dev -l app.kubernetes.io/component=portail
Consulte o histórico:
powershell
helm history hedge-dev -n hedge-dev
Vê pelo menos 2 revisões.
Anule o upgrade ao voltar à revisão 1:
powershell
helm rollback hedge-dev 1 -n hedge-dev
Verifique que se voltou a 1 só Pod portal, e que o histórico mostra uma nova revisão do tipo Rollback:
powershell
kubectl get pods -n hedge-dev -l app.kubernetes.io/component=portailhelm history hedge-dev -n hedge-dev
Pergunta a tratar no relatório: qual é a diferença fundamental entre helm upgrade --set portail.replicas=5 e kubectl scale deploy/hedge-dev-portail --replicas=5? Por que o Helm prefere que se passe por ele?
A pasta chart/casses/ contém três templates já escritos que compilam mas introduzem cada um um erro real encontrado em empresa. Deve, para cada um:
copiá-lo para chart/templates/ ;
reproduzir o sintoma descrito no topo do ficheiro ;
diagnosticar a causa ao ler a mensagem de erro ;
reparar (ao modificar o template em chart/templates/, não o original em casses/) ;
provar que a avaria desapareceu.
Ficheiro
Componente acrescentado
Natureza do erro
casse-1-configmap.yaml
Um ConfigMap global
Colisão de nome entre releases
casse-2-worker-deployment.yaml
Um Deploymentworker
Seletor imutável violado no primeiro helm upgrade
casse-3-cache-deployment.yaml
Um Deploymentcache
Caminho de valor errado (erro de digitação silencioso)
Truque de inquérito:
powershell
# rendu SEC d'un seul template (n'installe rien)helm template hedge-dev .\chart -f .\chart\environments\values-dev.yaml ` --show-only templates/casse-3-cache-deployment.yaml --debug
Este comando imprime exatamente o que o Helm enviaria ao Kubernetes. É a sua primeira ferramenta de diagnóstico — use-a sem moderação.
Missão 7 — Bónus: o refinamento (5 pontos)
À escolha, um só basta:
a) Acrescente um hook pre-install (Job) que mostra Bienvenue dans <environnement> nos logs Helm. A release deve esperar o fim do Job antes de continuar.
b) Torne o número de réplicas dinâmico com um valor values.yaml que tem uma estrutura aninhada (por exemplo portail.autoscaling.enabled, portail.autoscaling.min, portail.autoscaling.max) e faça gerar condicionalmente um HorizontalPodAutoscaler segundo .enabled.
c) Acrescente um NOTES.txt em templates/ que mostra, depois de cada helm install, o URL exato para abrir o painel (com o nodePort certo segundo os Values).
A pasta chart/ completa, em estado de funcionamento (helm lint limpo).
Um RAPPORT.md contendo:
a saída de helm list -A a mostrar as suas 3 releases ;
uma captura por ambiente (3 painéis coloridos) ;
o histórico completo de hedge-dev (com upgrade + rollback) ;
para cada avaria da missão 6: comando de diagnóstico, causa, correção, prova ;
as suas respostas às questões de reflexão.
A saída final de .\outils\valider.ps1.
Questões de reflexão
Por que o campo spec.selector.matchLabels é imutável no Kubernetes? Que problema resolve esta restrição?
Tem 3 ambientes hoje. Amanhã, a equipa DevSecOps pede um 4.º («pre-prod»). Que ficheiros cria e quais não toca?
Qual é a diferença entre helm upgrade --set replicas=5 e kubectl scale, do ponto de vista da rastreabilidade e do rollback?
O portal mostra «backend OK» — por que esta informação é mais fiável do que um simples kubectl get svc api?
O que acontece se apagar um Pod com kubectl delete pod, quando foi criado por um Deployment via Helm? O Helm está ao corrente da «perda»?
Um colega propõe-lhe pôr app.kubernetes.io/version: {{ .Chart.AppVersion }} no matchLabels de um Deployment. O que lhe responde?
Cotação
Elemento
Pontos
Missão 1 — Chart válido e lint limpo
10
Missão 2 — Templating portail + api
20
Missão 3 — Helpers e labels reutilizáveis
15
Missão 4 — Três ambientes lado a lado
20
Missão 5 — Upgrade + rollback rastreados
10
Missão 6 — Diagnóstico + reparação das 3 avarias
20
Qualidade do relatório e justificação das escolhas
5
Bónus — Missão 7
+5
Total
100 (+5)
Penalizações:
−10 por valor de ambiente escrito em concreto num template (replicas: 3 literal em vez de .Values....).
−5 por redefinição inútil num values-<env>.yaml (um valor que não tem razão para diferir entre ambientes).
−10 por alteração de um ficheiro de apps/.
Caixa de ferramentas Helm
powershell
# ANALYSE (aucun deploiement)helm lint .\chart # syntaxe + best practiceshelm template <release> .\chart -f <values.yaml> # rendu complethelm template <release> .\chart -f <values.yaml> --show-only templates/<fichier> # rendu cibléhelm template <release> .\chart -f <values.yaml> --debug # avec traceshelm show values .\chart # valeurs par défaut# DEPLOIEMENThelm install <release> .\chart -f <values.yaml> -n <ns> --create-namespacehelm upgrade <release> .\chart -f <values.yaml> -n <ns>helm upgrade <release> .\chart -f <values.yaml> --set portail.replicas=5 -n <ns>helm rollback <release> <revision> -n <ns>helm uninstall <release> -n <ns># OBSERVATIONhelm list -A # toutes les releaseshelm status <release> -n <ns>helm history <release> -n <ns>helm get values <release> -n <ns> # les valeurs activeshelm get manifest <release> -n <ns> # les manifestes appliques
Os 3 reflexos em caso de erro:
Começar sempre por helm template — é uma renderização SECA, sem risco, que mostra exatamente o que vai ser enviado ao Kubernetes.
Ler o caminho na mensagem de erro — o Helm dá sempre o ficheiro + a linha + o caminho .Values.* falhoso.
helm get manifest mostra o que está atualmente no cluster (útil para comparar com o que o seu novo template gera).
ANEXO A — As aplicações
Não modifique nenhum destes ficheiros. Recopie-os tal qual nos caminhos indicados.
A.1 — O portal (painel multiambiente)
Todos os valores mostrados vêm de variáveis de ambiente injetadas pelo Helm. A mesma imagem comporta-se de forma diferente segundo os env: do Deployment.
Ficheiro: apps/portail/app.py
python
"""Portal — painel multiambiente.Este Pod mostra o ambiente em que corre (DEV / STAGING / PROD),a versao da aplicacao, o numero de replicas e o estado do backend.Todos os valores mostrados vem de VARIAVEIS DE AMBIENTE injetadaspelo Helm a partir de values-<env>.yaml. O mesmo codigo adapta-se a cadaambiente sem nenhuma alteracao."""import osimport socketimport timeimport urllib.errorimport urllib.requestfrom flask import Flask, jsonify, requestapp = Flask(__name__)DEMARRAGE = time.time()def cfg(): return { "env": os.environ.get("ENVIRONMENT", "desconhecido"), "version": os.environ.get("APP_VERSION", "0.0.0"), "theme": os.environ.get("THEME_COLOR", "#64748b"), "message": os.environ.get("BANNIERE_MESSAGE", "Implantado com Helm"), "backend_url": os.environ.get("BACKEND_URL", "http://api"), "replicas_info": os.environ.get("REPLICAS_INFO", "?"), "pod": socket.gethostname(), "uptime": int(time.time() - DEMARRAGE), }def tester_backend(url): try: with urllib.request.urlopen(url + "/ping", timeout=1.5) as reponse: corps = reponse.read(200).decode("utf-8", "ignore") return "ok", corps.strip() except urllib.error.HTTPError as err: return "http", "HTTP %s" % err.code except Exception as err: return "ko", type(err).__name__@app.route("/health")def health(): return "OK", 200@app.route("/api-json")def api_json(): """Rota util para a validacao automatica.""" c = cfg() etat, detail = tester_backend(c["backend_url"]) return jsonify(pod=c["pod"], env=c["env"], version=c["version"], backend=etat, backend_detail=detail, uptime=c["uptime"])@app.route("/")def accueil(): c = cfg() etat, detail = tester_backend(c["backend_url"]) # ... (modelo HTML completo no ficheiro — nao repetido aqui para permanecer legivel)
O ficheiro completo é fornecido em apps/portail/app.py.
Recopie estes ficheiros e depois substitua cada TODO pelo valor certo.
As linhas precedidas de # ? são perguntas a decidir: cabe-lhe a si completar o código.
Ficheiro: chart/Chart.yaml
yaml
# ? Preencha os campos obrigatorios de um Chart Helm.# ? apiVersion deve valer v2 (a v1 esta obsoleta desde o Helm 3).# ? type e "application" (por oposicao a "library").apiVersion: TODOname: hedgedescription: TODOtype: TODOversion: 0.1.0appVersion: "1.0.0"
Ficheiro: chart/values.yaml
yaml
# values.yaml — valores por omissao do chart hedge.# Cada ambiente fornece um ficheiro values-<env>.yaml que SOBREPOE# estes valores por cima. Mantenha este ficheiro NEUTRO (nenhum valor especifico# de um ambiente).environment: defaultbanniere: message: "Chart Helm — application multi-environnement" couleur: "#64748b"portail: image: repository: hedge-portail tag: "1.0" pullPolicy: IfNotPresent replicas: 1 service: type: NodePort port: 80 targetPort: 5000 nodePort: 30130api: image: repository: hedge-api tag: "1.0" pullPolicy: IfNotPresent replicas: 1 service: type: ClusterIP port: 80 targetPort: 8000
Ficheiro: chart/templates/_helpers.tpl
yaml
{{/*Nome completo de um recurso : "<release>-<composant>".Uso : {{ include "hedge.fullname" (dict "root" . "composant" "portail") }}*/}}{{- define "hedge.fullname" -}}{{- printf "TODO" .root.Release.Name .composant | trunc 63 | trimSuffix "-" -}}{{- end -}}{{/*Labels comuns a todos os recursos.Uso : {{ include "hedge.labels" (dict "root" . "composant" "portail") | nindent 4 }}*/}}{{- define "hedge.labels" -}}# ? preencha os 7 labels pedidos na Missao 3app.kubernetes.io/name: TODOapp.kubernetes.io/instance: TODO# ... continue ...{{- end -}}{{/*Selector labels : subconjunto ESTAVEL dos labels.Colocar aqui APENAS labels que NUNCA mudarao para uma instancia.*/}}{{- define "hedge.selectorLabels" -}}# ? os 3 labels ESTRITAMENTE imutaveis apenas{{- end -}}
Ficheiro: chart/templates/portail-deployment.yaml
yaml
# ? Deployment do portal. Use :# - .Values.portail.replicas# - .Values.portail.image.{repository,tag,pullPolicy}# - .Values.portail.service.targetPort# - .Values.environment# - .Values.banniere.{couleur,message}# - .Chart.AppVersion (pour APP_VERSION)# - O NOME do Service api construido com .Release.Name (para BACKEND_URL)apiVersion: apps/v1kind: Deploymentmetadata: name: TODO labels: TODOspec: replicas: TODO selector: matchLabels: TODO template: metadata: labels: TODO spec: containers: - name: portail image: TODO imagePullPolicy: TODO ports: - containerPort: TODO env: - name: ENVIRONMENT value: TODO # ? acrescente APP_VERSION, THEME_COLOR, BANNIERE_MESSAGE, # BACKEND_URL, REPLICAS_INFO readinessProbe: httpGet: path: /health port: TODO initialDelaySeconds: 3 periodSeconds: 5
Ficheiro: chart/templates/portail-service.yaml
yaml
# ? Service para o portal. Tipo NodePort. Use a condicao# {{- if eq .Values.portail.service.type "NodePort" }} ... {{- end }}# para incluir "nodePort" APENAS se for mesmo um NodePort.apiVersion: v1kind: Servicemetadata: name: TODO labels: TODOspec: type: TODO selector: TODO ports: - port: TODO targetPort: TODO # ? nodePort apenas se type == NodePort
Ficheiro: chart/templates/api-deployment.yaml
yaml
# ? Mesma estrutura que portail-deployment.yaml, mas :# - composant = "api"# - variaveis de ambiente : ENVIRONMENT e APP_VERSION apenas# - porta do contentor = .Values.api.service.targetPort (8000)apiVersion: apps/v1kind: Deploymentmetadata: name: TODOspec: # ... (estrutura semelhante ao portal) ...
Ficheiro: chart/templates/api-service.yaml
yaml
# ? Service ClusterIP para a api. Uma so porta. Sem nodePort.apiVersion: v1kind: Servicemetadata: name: TODOspec: type: TODO selector: TODO ports: - port: TODO targetPort: TODO
Ficheiro: chart/environments/values-dev.yaml
yaml
# ? Ambiente DEV : 1 replica, bandeau azul #2563eb, NodePort 30130.environment: TODObanniere: message: TODO couleur: TODOportail: replicas: TODO service: nodePort: TODOapi: replicas: TODO
Ficheiro: chart/environments/values-staging.yaml
yaml
# ? Ambiente STAGING : 2 replicas, bandeau laranja #ea580c, NodePort 30131.environment: TODO# ... complete no modelo de values-dev.yaml ...
Cada ficheiro abaixo encontra-se em chart/casses/. Não modifique os originais — copie-os para chart/templates/, reproduza o erro e depois corrija a cópia.
Ficheiro: chart/casses/casse-1-configmap.yaml
yaml
# AVARIA 1# Sintoma : instale hedge-dev, depois tente instalar hedge-staging# NO MESMO NAMESPACE. A segunda instalacao falha com :# ConfigMap "hedge-config" ... exists and cannot be imported ...apiVersion: v1kind: ConfigMapmetadata: name: hedge-config labels: {{- include "hedge.labels" (dict "root" . "composant" "config") | nindent 4 }}data: timezone: "America/Toronto" langue: "fr-CA"
O ficheiro outils/valider.ps1 é fornecido tal qual. Não dá nenhuma solução — apenas uma pontuação e o primeiro ponto a corrigir. Execute-o a qualquer momento:
powershell
.\outils\valider.ps1
O que o script verifica:
Missão
Critérios automáticos
1
helm lint passa, Chart.yaml tem apiVersion: v2, type: application
2
helm template produz mesmo 2 Deployments e 2 Services, nomes prefixados pela release
3
_helpers.tpl define os 3 helpers, labels standard presentes
4
Os 3 ficheiros values-<env>.yaml existem com os valores certos (env, replicas, porta, cor), as 3 releases estão implantadas
5
A release hedge-dev tem ≥ 2 revisões e um rollback no histórico
6
Nenhum hedge-config em concreto, nenhum label variável em matchLabels, nenhum .Values.portal (com erro de digitação)
O script não executa ele próprio nenhum comando helm install: cabe-lhe a si implantar antes de validar.
Curso criado pelo Dr. Haythem REHOUMA — Desenvolvimento e implantação de soluções de dados