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
|