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
# 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