Helm Chart Avanzato: Hooks, Tests e Dependencies Kubernetes

2026-03-20 · 11 min read · gen:2m 33s · tok:19947
#helm #kubernetes #devops #intermediate-tutorial #italiano #microservizi

Guida pratica a Helm hooks, tests e dependencies per deploy Kubernetes affidabili. Pattern concreti per orchestrare microservizi in produzione.

Helm Chart Avanzato: Padroneggiare Hooks, Tests e Dependencies per Deploy Kubernetes Affidabili

Hai un chart Helm che funziona in sviluppo. Poi arriva il momento di andare in produzione: devi eseguire migrazioni database prima del deploy, validare che i servizi dipendenti siano raggiungibili, gestire rollback quando qualcosa va storto. Il tuo values.yaml diventa un mostro ingestibile e i deploy falliscono alle 3 di notte.

Questo articolo ti mostra come superare questi problemi reali. Non teoria astratta, ma pattern concreti che uso quotidianamente per orchestrare deploy di architetture microservizi con decine di componenti interdipendenti.

Prerequisiti

  • Kubernetes cluster funzionante (minikube, kind o cloud provider)
  • Helm 3.12+ installato
  • FamiliaritĂ  con la sintassi base dei chart Helm
  • Conoscenza di base di Go templates
  • kubectl configurato e funzionante
1
2
3
4
5
6
# Verifica versioni
helm version --short
# v3.14.0+g3fc9f4b

kubectl version --client
# Client Version: v1.29.0

Architettura e Concetti Chiave

Prima di scrivere codice, capiamo come Helm orchestra il ciclo di vita di un release e dove si inseriscono hooks, test e dependencies.

flowchart TD
    A[helm install / upgrade] --> B[Resolve Dependencies]
    B --> PG[postgresql]
    B --> RD[redis]
    B --> RMQ[rabbitmq]
    PG & RD & RMQ --> C["pre-install / pre-upgrade hooks"]
    C --> H1["weight: -10 → DB Migration"]
    C --> H2["weight: 0 → Cache Warmup"]
    C --> H3["weight: 10 → Notification"]
    H1 & H2 & H3 --> D[Deploy Resources]
    D --> E["post-install / post-upgrade hooks"]
    E --> F{helm test}
    F -->|pass| G[âś… Release Complete]
    F -->|fail| H[❌ Rollback Decision]

Hooks: Job Kubernetes eseguiti in momenti specifici del ciclo di vita. Perfetti per migrazioni, seed data, cleanup.

Tests: Pod che validano lo stato del release dopo il deploy. Helm li esegue con helm test e riporta successo/fallimento.

Dependencies: Chart esterni che il tuo chart richiede. Helm li scarica, li configura e li installa nell’ordine corretto.

Implementazione Passo-Passo

Hooks Pre-Install e Pre-Upgrade per Migrazioni Database

Il caso d’uso piĂą comune: eseguire migrazioni database prima che l’applicazione parta. Vediamo un’implementazione robusta con gestione errori e retry.

Struttura del chart:

myapp/
├── Chart.yaml
├── values.yaml
├── templates/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── hooks/
│   │   ├── db-migration.yaml
│   │   ├── seed-data.yaml
│   │   └── pre-rollback-backup.yaml
│   └── tests/
│       ├── test-connection.yaml
│       └── test-api-health.yaml
└── charts/

Hook di migrazione database:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# templates/hooks/db-migration.yaml
{{- if .Values.migrations.enabled }}
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "myapp.fullname" . }}-db-migrate
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
  annotations:
    # Hook pre-install e pre-upgrade
    "helm.sh/hook": pre-install,pre-upgrade
    # Weight negativo = esegue prima di altri hook
    "helm.sh/hook-weight": "-5"
    # Elimina il job precedente prima di crearne uno nuovo,
    # poi elimina dopo successo (mantieni in caso di fallimento per debug)
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  # Timeout massimo per la migrazione
  activeDeadlineSeconds: {{ .Values.migrations.timeout | default 600 }}
  # Numero di retry in caso di fallimento
  backoffLimit: {{ .Values.migrations.retries | default 3 }}
  template:
    metadata:
      labels:
        {{- include "myapp.selectorLabels" . | nindent 8 }}
        app.kubernetes.io/component: migration
    spec:
      restartPolicy: Never
      # Init container che aspetta il database
      initContainers:
        - name: wait-for-db
          image: busybox:1.36
          command:
            - sh
            - -c
            - |
              echo "Attendo che PostgreSQL sia raggiungibile..."
              until nc -z {{ .Values.database.host }} {{ .Values.database.port }}; do
                echo "Database non ancora pronto, riprovo in 5 secondi..."
                sleep 5
              done
              echo "Database raggiungibile!"
      containers:
        - name: migrate
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          command:
            - /bin/sh
            - -c
            - |
              set -e
              echo "=== Inizio migrazione database ==="
              echo "Versione applicazione: {{ .Values.image.tag }}"
              echo "Database: {{ .Values.database.host }}:{{ .Values.database.port }}/{{ .Values.database.name }}"
              
              # Esegui le migrazioni con output dettagliato
              ./migrate -path /migrations -database "$DATABASE_URL" up
              
              RESULT=$?
              if [ $RESULT -eq 0 ]; then
                echo "=== Migrazione completata con successo ==="
              else
                echo "=== ERRORE: Migrazione fallita con codice $RESULT ==="
                exit $RESULT
              fi
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: {{ include "myapp.fullname" . }}-db-credentials
                  key: url
            {{- if .Values.migrations.dryRun }}
            - name: DRY_RUN
              value: "true"
            {{- end }}
          resources:
            {{- toYaml .Values.migrations.resources | nindent 12 }}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
{{- end }}

đź’ˇ Tip: Usa hook-delete-policy: before-hook-creation,hook-succeeded per mantenere i job falliti. Ti permette di ispezionare i log con kubectl logs job/myapp-db-migrate quando qualcosa va storto.

Hook per seed data con dipendenza dalla migrazione:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# templates/hooks/seed-data.yaml
{{- if and .Values.seed.enabled (not .Values.global.production) }}
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "myapp.fullname" . }}-seed-data
  annotations:
    "helm.sh/hook": post-install
    # Weight maggiore = esegue DOPO la migrazione (weight -5)
    "helm.sh/hook-weight": "5"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  activeDeadlineSeconds: 300
  backoffLimit: 2
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: seed
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          command:
            - /bin/sh
            - -c
            - |
              set -e
              echo "Caricamento dati di seed per ambiente {{ .Release.Namespace }}..."
              
              # Verifica che le migrazioni siano state eseguite
              ./cli db:check-migrations
              
              # Carica i dati appropriati per l'ambiente
              {{- if eq .Release.Namespace "staging" }}
              ./cli db:seed --dataset=staging
              {{- else }}
              ./cli db:seed --dataset=development
              {{- end }}
              
              echo "Seed completato!"
          envFrom:
            - secretRef:
                name: {{ include "myapp.fullname" . }}-db-credentials
{{- end }}

⚠️ Warning: Non eseguire mai seed data in produzione! Nota il controllo (not .Values.global.production) che disabilita questo hook negli ambienti di produzione.

Hook pre-rollback per backup:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# templates/hooks/pre-rollback-backup.yaml
{{- if .Values.backup.beforeRollback }}
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "myapp.fullname" . }}-pre-rollback-backup
  annotations:
    "helm.sh/hook": pre-rollback
    "helm.sh/hook-weight": "-10"
    "helm.sh/hook-delete-policy": before-hook-creation
spec:
  activeDeadlineSeconds: 900
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: backup
          image: postgres:15-alpine
          command:
            - /bin/sh
            - -c
            - |
              set -e
              TIMESTAMP=$(date +%Y%m%d_%H%M%S)
              BACKUP_NAME="pre_rollback_${TIMESTAMP}.sql.gz"
              
              echo "Creazione backup pre-rollback: $BACKUP_NAME"
              
              pg_dump -h "$DB_HOST" -U "$DB_USER" -d "$DB_NAME" | gzip > /backup/$BACKUP_NAME
              
              # Upload su S3 (opzionale)
              {{- if .Values.backup.s3.enabled }}
              aws s3 cp /backup/$BACKUP_NAME s3://{{ .Values.backup.s3.bucket }}/rollbacks/$BACKUP_NAME
              {{- end }}
              
              echo "Backup completato: $BACKUP_NAME"
          env:
            - name: DB_HOST
              value: {{ .Values.database.host | quote }}
            - name: DB_USER
              valueFrom:
                secretKeyRef:
                  name: {{ include "myapp.fullname" . }}-db-credentials
                  key: username
            - name: DB_NAME
              value: {{ .Values.database.name | quote }}
            - name: PGPASSWORD
              valueFrom:
                secretKeyRef:
                  name: {{ include "myapp.fullname" . }}-db-credentials
                  key: password
          volumeMounts:
            - name: backup-volume
              mountPath: /backup
      volumes:
        - name: backup-volume
          emptyDir:
            sizeLimit: 5Gi
{{- end }}

Test Suite Helm per Validazione Post-Deploy

I test Helm sono Pod che verificano lo stato del release. A differenza degli hook, non bloccano il deploy ma validano che tutto funzioni correttamente dopo l’installazione.

Test di connettivitĂ  database:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# templates/tests/test-db-connection.yaml
apiVersion: v1
kind: Pod
metadata:
  name: {{ include "myapp.fullname" . }}-test-db
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
    "helm.sh/hook-weight": "-5"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  restartPolicy: Never
  containers:
    - name: test-db
      image: postgres:15-alpine
      command:
        - /bin/sh
        - -c
        - |
          set -e
          echo "=== Test connessione database ==="
          
          # Test 1: Connessione base
          echo "Test 1: Verifica connessione..."
          pg_isready -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME"
          
          # Test 2: Query di verifica
          echo "Test 2: Esecuzione query di test..."
          RESULT=$(psql -h "$DB_HOST" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM schema_migrations;")
          echo "Migrazioni applicate: $RESULT"
          
          # Test 3: Verifica tabelle critiche
          echo "Test 3: Verifica schema..."
          psql -h "$DB_HOST" -U "$DB_USER" -d "$DB_NAME" -c "\dt" | grep -q "users" || {
            echo "ERRORE: Tabella 'users' non trovata!"
            exit 1
          }
          
          echo "=== Tutti i test database superati ==="
      env:
        - name: DB_HOST
          value: {{ .Values.database.host | quote }}
        - name: DB_PORT
          value: {{ .Values.database.port | quote }}
        - name: DB_USER
          valueFrom:
            secretKeyRef:
              name: {{ include "myapp.fullname" . }}-db-credentials
              key: username
        - name: DB_NAME
          value: {{ .Values.database.name | quote }}
        - name: PGPASSWORD
          valueFrom:
            secretKeyRef:
              name: {{ include "myapp.fullname" . }}-db-credentials
              key: password

Test API health check completo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# templates/tests/test-api-health.yaml
apiVersion: v1
kind: Pod
metadata:
  name: {{ include "myapp.fullname" . }}-test-api
  annotations:
    "helm.sh/hook": test
    "helm.sh/hook-weight": "0"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  restartPolicy: Never
  containers:
    - name: test-api
      image: curlimages/curl:8.5.0
      command:
        - /bin/sh
        - -c
        - |
          set -e
          API_URL="http://{{ include "myapp.fullname" . }}:{{ .Values.service.port }}"
          
          echo "=== Test API Health ==="
          echo "Endpoint: $API_URL"
          
          # Attendi che il servizio sia pronto
          echo "Attendo che il servizio sia raggiungibile..."
          for i in $(seq 1 30); do
            if curl -sf "$API_URL/health" > /dev/null 2>&1; then
              break
            fi
            echo "Tentativo $i/30..."
            sleep 2
          done
          
          # Test 1: Health endpoint
          echo "Test 1: Health check..."
          HEALTH=$(curl -sf "$API_URL/health")
          echo "Response: $HEALTH"
          echo "$HEALTH" | grep -q '"status":"healthy"' || {
            echo "ERRORE: Health check fallito!"
            exit 1
          }
          
          # Test 2: Ready endpoint (include check dipendenze)
          echo "Test 2: Readiness check..."
          READY=$(curl -sf "$API_URL/ready")
          echo "Response: $READY"
          
          # Verifica che tutte le dipendenze siano OK
          echo "$READY" | grep -q '"database":"connected"' || {
            echo "ERRORE: Database non connesso!"
            exit 1
          }
          
          {{- if .Values.redis.enabled }}
          echo "$READY" | grep -q '"redis":"connected"' || {
            echo "ERRORE: Redis non connesso!"
            exit 1
          }
          {{- end }}
          
          # Test 3: Verifica configurazione (solo staging/dev)
          {{- if not .Values.global.production }}
          echo "Test 3: Verifica configurazione..."
          CONFIG=$(curl -sf "$API_URL/debug/config")
          echo "$CONFIG" | grep -q '"environment":"{{ .Release.Namespace }}"' || {
            echo "WARNING: Environment mismatch"
          }
          {{- end }}
          
          # Test 4: Metriche Prometheus disponibili
          {{- if .Values.metrics.enabled }}
          echo "Test 4: Metriche Prometheus..."
          curl -sf "$API_URL/metrics" | grep -q "http_requests_total" || {
            echo "ERRORE: Metriche non disponibili!"
            exit 1
          }
          {{- end }}
          
          echo "=== Tutti i test API superati ==="

Test di integrazione multi-servizio:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# templates/tests/test-integration.yaml
{{- if .Values.tests.integration.enabled }}
apiVersion: v1
kind: Pod
metadata:
  name: {{ include "myapp.fullname" . }}-test-integration
  annotations:
    "helm.sh/hook": test
    "helm.sh/hook-weight": "10"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  restartPolicy: Never
  containers:
    - name: test-integration
      image: "{{ .Values.tests.integration.image }}"
      command:
        - /bin/sh
        - -c
        - |
          set -e
          echo "=== Test di Integrazione ==="
          
          # Esegui test suite con pytest/jest/go test
          cd /tests
          
          {{- if eq .Values.tests.integration.framework "pytest" }}
          pytest integration/ \
            --tb=short \
            --junitxml=/results/integration.xml \
            -v
          {{- else if eq .Values.tests.integration.framework "jest" }}
          npm test -- --ci --reporters=default --reporters=jest-junit
          {{- end }}
          
          echo "=== Test di integrazione completati ==="
      env:
        - name: API_BASE_URL
          value: "http://{{ include "myapp.fullname" . }}:{{ .Values.service.port }}"
        - name: TEST_TIMEOUT
          value: "{{ .Values.tests.integration.timeout | default 30 }}"
      volumeMounts:
        - name: test-results
          mountPath: /results
  volumes:
    - name: test-results
      emptyDir: {}
{{- end }}

Gestione Dependencies Multi-Servizio

Per architetture microservizi, devi orchestrare multiple dipendenze. Ecco un Chart.yaml completo con gestione avanzata:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# Chart.yaml
apiVersion: v2
name: myapp
description: Applicazione principale con dipendenze complete
type: application
version: 1.2.0
appVersion: "2.1.0"

dependencies:
  # PostgreSQL - sempre richiesto
  - name: postgresql
    version: "13.2.x"
    repository: "https://charts.bitnami.com/bitnami"
    condition: postgresql.enabled
    # Alias permette di avere multiple istanze dello stesso chart
    alias: db-primary
    
  # PostgreSQL replica per read-only (opzionale)
  - name: postgresql
    version: "13.2.x"
    repository: "https://charts.bitnami.com/bitnami"
    condition: postgresql.replica.enabled
    alias: db-replica
    
  # Redis per caching
  - name: redis
    version: "18.x.x"
    repository: "https://charts.bitnami.com/bitnami"
    condition: redis.enabled
    
  # RabbitMQ per message queue
  - name: rabbitmq
    version: "12.x.x"
    repository: "https://charts.bitnami.com/bitnami"
    condition: rabbitmq.enabled
    # Tags permettono di abilitare gruppi di dipendenze
    tags:
      - messaging
      - async-processing
      
  # Elasticsearch per search/logging
  - name: elasticsearch
    version: "19.x.x"
    repository: "https://charts.bitnami.com/bitnami"
    condition: elasticsearch.enabled
    tags:
      - search
      - logging
      
  # Chart interno per microservizio auth
  - name: auth-service
    version: "1.x.x"
    repository: "file://../auth-service"
    condition: authService.enabled

Values.yaml con override per le dipendenze:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# values.yaml
global:
  production: false
  imageRegistry: ""
  storageClass: "standard"
  
# Configurazione applicazione principale
image:
  repository: mycompany/myapp
  tag: "2.1.0"
  pullPolicy: IfNotPresent

service:
  port: 8080
  type: ClusterIP

# === CONFIGURAZIONE DIPENDENZE ===

# PostgreSQL primario
postgresql:
  enabled: true
  
db-primary:
  # Override specifici per il chart postgresql con alias db-primary
  auth:
    postgresPassword: ""  # SarĂ  sovrascritto da secret esterno
    username: "myapp"
    password: ""
    database: "myapp"
  primary:
    persistence:
      enabled: true
      size: 50Gi
    resources:
      requests: { memory: 512Mi, cpu: 250m }
      limits:   { memory: 2Gi, cpu: 1000m }

# Redis (aliasato di default, nessun alias qui)
redis:
  enabled: true
  architecture: replication
  auth:
    enabled: true
    password: ""

# Gruppi di dipendenze abilitabili via tag
tags:
  messaging: true
  async-processing: true
  search: false
  logging: false

# Microservizio interno
authService:
  enabled: true
  replicaCount: 2

⚠️ helm dependency update riscrive Chart.lock. Committa il lockfile. Senza, due sviluppatori che lanciano helm dependency update a settimane di distanza risolvono postgresql: 13.2.x a patch diverse e mandano in produzione database sottilmente diversi.

Prima di ogni install (o build CI) scarica e “vendorizza” i subchart:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Scarica i chart dichiarati in Chart.yaml dentro charts/
helm dependency update ./myapp

# Ispeziona l'albero risolto e quali condizioni sono attive
helm dependency list ./myapp
# NAME           VERSION   REPOSITORY                           STATUS
# postgresql     13.2.x    https://charts.bitnami.com/bitnami   ok
# postgresql     13.2.x    https://charts.bitnami.com/bitnami   unpacked (condition è false)
# redis          18.x.x    https://charts.bitnami.com/bitnami   ok
# rabbitmq       12.x.x    https://charts.bitnami.com/bitnami   ok
# elasticsearch  19.x.x    https://charts.bitnami.com/bitnami   unpacked (tag search è false)

Condition vs. Tag

Usa condition quando un singolo booleano attiva un subchart. Usa tags quando un interruttore deve attivare o disattivare un gruppo di chart insieme (vedi messaging e search sopra). Se condition e tag si applicano allo stesso chart, vince condition.

Passare valori dal subchart al parent

Il parent può leggere i valori che un subchart esporta, evitando di duplicare i dati di connessione:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# charts/postgresql/values.yaml (child)
exports:
  dbConnection:
    host: db-primary-postgresql
    port: 5432

# Chart.yaml (parent) — importa il blocco esportato dal child
dependencies:
  - name: postgresql
    version: "13.2.x"
    repository: "https://charts.bitnami.com/bitnami"
    alias: db-primary
    import-values:
      - child: exports.dbConnection
        parent: database

Dopo l’import, {{ .Values.database.host }} nel parent vale quello che il subchart ha esportato.

Configurazione per Produzione

Hooks e test ripagano solo se il comando di deploy tratta un hook fallito come un release fallito. In CI/CD deploya sempre con --atomic:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
helm upgrade --install myapp ./myapp \
  --namespace myapp \
  --create-namespace \
  --atomic \
  --timeout 10m \
  --wait \
  --set-string db-primary.auth.postgresPassword="$DB_ADMIN_PASSWORD" \
  --set-string db-primary.auth.password="$DB_APP_PASSWORD" \
  --values values.yaml \
  --values values.production.yaml

Cosa ti dĂ  ogni flag:

  • --atomic — se una risorsa o un hook pre-* fallisce, Helm fa il rollback dell’intero release alla revisione precedente. Senza, una migrazione fallita ti lascia su un release applicato a metĂ .
  • --wait — Helm si blocca finchĂ© Deployment, StatefulSet e Job non sono ready. --atomic lo implica, ma esplicitarlo rende l’intento chiaro.
  • --timeout 10m — il limite per ogni fase di attesa (hook, risorse, test). Le migrazioni su tabelle grandi hanno bisogno di margine.
  • --set-string per i secret — tiene le password fuori da values.yaml e dalla history git. Meglio ancora: usa un Secret esistente via db-primary.auth.existingSecret.

Lancia la test suite come stage separato della pipeline, così un deploy “verde” che fallisce la validazione blocca comunque il rilascio:

1
helm test myapp --namespace myapp --logs --timeout 5m

đź’ˇ --logs porta l’output di ogni Pod di test nel terminale (e nel log CI) anche quando il Pod viene cancellato subito dopo da hook-delete-policy: hook-succeeded. Senza, un test fallito in CI ti lascia solo un exit code.

Errori Comuni e Troubleshooting

Le risorse degli hook non vengono ripulite da helm uninstall. Helm non traccia le risorse create dagli hook come parte del release. Un Job hook senza hook-delete-policy sopravvive all’uninstall e va in collisione con l’install successivo. Metti sempre almeno before-hook-creation, e aggiungi hook-succeeded per tenere pulito il cluster preservando i job falliti da ispezionare.

Un hook di migrazione fallito non fa il rollback dello schema. --atomic fa il rollback delle risorse Kubernetes, non del database. Helm non sa cosa ha fatto il container di migrazione. Accoppia ogni migrazione forward con un hook pre-rollback (mostrato sopra) e rendi le migrazioni transazionali dove il tuo engine lo permette.

I test passano in locale ma si bloccano in CI. I Pod di test con restartPolicy: Never che vanno in errore restano in stato Error per sempre se helm test non riceve --timeout. Impostalo, e metti backoffLimit sui test basati su Job.

Le CRD non si aggiornano. I file sotto crds/ vengono installati una volta sola, prima degli hook, e non sono mai aggiornati o cancellati da Helm su upgrade o uninstall. Se un subchart alza la versione di una CRD, devi fare kubectl apply a mano.

helm dependency update fallisce dietro proxy o in cluster air-gapped. Vendorizza i charts/*.tgz in git, oppure pushali su un registry OCI (helm push) e referenzia repository oci:// così le build non escono su internet.

Diagnostica utile:

1
2
3
4
5
6
7
8
9
# I manifest esatti che Helm applicherĂ , hook inclusi
helm template ./myapp --values values.yaml | less

# Cosa è successo nell'ultimo release
helm history myapp -n myapp
helm status myapp -n myapp --show-resources

# Log di un Job hook fallito e preservato
kubectl logs -n myapp job/myapp-db-migrate

Performance e ScalabilitĂ 

  • Gli hook girano in sequenza e bloccano il release. Dieci hook pre-upgrade da dieci secondi l’uno aggiungono due minuti di finestra di downtime a ogni deploy. Accorpa gli step indipendenti in un unico Job hook, e tieni hook separati solo per ciò che deve davvero essere ordinato.
  • Raggruppa per weight, non per numero. Hook con lo stesso weight girano comunque un oggetto alla volta, ma eviti un nuovo ciclo di wait-for-ready per ogni bucket di weight.
  • Tieni le test suite sotto il minuto. helm test è nel percorso critico della pipeline. Sposta i test di integrazione esaustivi su un job separato contro il release deployato; in-chart lascia solo smoke test (connettivitĂ , versione schema, sanitĂ  config).
  • Grafi di dipendenze grandi rallentano ogni comando. helm dependency update riscarica e ri-pacchettizza l’intero albero. In CI, cachea la cartella charts/ con chiave sull’hash di Chart.lock.
  • Preferisci i registry OCI per le dipendenze. Il pull di chart oci:// è una singola richiesta autenticata per chart e sfrutta il layer caching, contro gli index file HTTP che Helm ri-scarica e ri-parsa.
  • ttlSecondsAfterFinished sui Job hook evita che il namespace accumuli centinaia di Pod di migrazione completati lungo un ciclo di release intenso.

Conclusioni e Next Steps

Ora hai i tre pilastri di un chart Helm production-grade che lavorano insieme: hook che eseguono migrazioni e backup in modo atomico dentro il lifecycle del release, test suite che validano un deploy prima di dichiararlo riuscito, e un grafo di dipendenze che compone microservizi con condition, tag e import di valori.

Il pattern che li lega è trattare il release come una transazione: deploy con --atomic --wait, hook pre-* che fanno da gate, test che confermano, e uno step fallito che fa tornare tutto a una revisione nota-buona.

Prossimi passi:

  • Aggiungi un hook post-upgrade che scalda le cache o triggera un purge CDN, così la prima richiesta utente non è quella che paga il cold start.
  • Sposta i secret fuori da --set-string con External Secrets Operator o Sealed Secrets.
  • Collega helm test a un tool di progressive delivery (Argo Rollouts o Flagger) così la validazione fa da gate a una canary invece che a un cutover completo.
  • Pubblica parent chart e subchart su un registry OCI e consumali per digest per deploy completamente riproducibili.

Risorse Aggiuntive