Skip to main content

Best practices


1 - Structure and organization

my-chart/
├── Chart.yaml # Métadonnées obligatoires
├── Chart.lock # Lock des dépendances
├── values.yaml # Valeurs par défaut documentées
├── values.schema.json # Validation des values
├── .helmignore # Fichiers à ignorer
├── README.md # Documentation utilisateur
├── CHANGELOG.md # Historique des changements
├── LICENSE # Licence
├── charts/ # Dépendances
├── crds/ # CRDs (si nécessaire)
├── templates/
│ ├── NOTES.txt # Instructions post-install
│ ├── _helpers.tpl # Fonctions helper
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── configmap.yaml
│ ├── secret.yaml
│ ├── serviceaccount.yaml
│ ├── hpa.yaml
│ ├── pdb.yaml
│ ├── networkpolicy.yaml
│ ├── hooks/
│ │ └── pre-upgrade-job.yaml
│ └── tests/
│ └── test-connection.yaml
└── ci/
├── ci-values.yaml # Values pour CI
└── test-values.yaml # Values pour tests

1.2 Consistent naming

# Conventions de nommage
# - Nom du chart: kebab-case (my-app)
# - Fichiers templates: kebab-case (my-deployment.yaml)
# - Helpers: chart-name.helper-name (my-app.labels)
# - Releases: env-app-component (prod-api-backend)

# _helpers.tpl
{{- define "my-app.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{- define "my-app.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}

2 - Values and configuration

2.1 Organizing values

# values.yaml bien organisé

# ========================================
# Global configuration
# ========================================
global:
imageRegistry: ""
imagePullSecrets: []
storageClass: ""

# ========================================
# Application
# ========================================
replicaCount: 1

image:
repository: nginx
tag: ""
pullPolicy: IfNotPresent

# ========================================
# Naming
# ========================================
nameOverride: ""
fullnameOverride: ""

# ========================================
# Service Account
# ========================================
serviceAccount:
# -- Specifies whether a service account should be created
create: true
# -- Annotations to add to the service account
annotations: {}
# -- The name of the service account to use
name: ""

# ========================================
# Security
# ========================================
podSecurityContext:
fsGroup: 1000

securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true

# ========================================
# Networking
# ========================================
service:
type: ClusterIP
port: 80

ingress:
enabled: false
className: ""
annotations: {}
hosts: []
tls: []

# ========================================
# Resources
# ========================================
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi

# ========================================
# Scaling
# ========================================
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 10
targetCPUUtilizationPercentage: 80

# ========================================
# Scheduling
# ========================================
nodeSelector: {}
tolerations: []
affinity: {}

# ========================================
# Dependencies
# ========================================
postgresql:
enabled: false
redis:
enabled: false

2.2 Documenting values

# Utiliser les commentaires helm-docs
# -- Nombre de réplicas du deployment
replicaCount: 1

image:
# -- Image repository
repository: nginx
# -- Image tag (defaults to chart appVersion)
tag: ""
# -- Image pull policy
# @default -- IfNotPresent
pullPolicy: IfNotPresent

# -- (list) Image pull secrets
# @default -- []
imagePullSecrets: []

2.3 Validation with JSON Schema

// values.schema.json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["replicaCount", "image"],
"properties": {
"replicaCount": {
"type": "integer",
"minimum": 0,
"description": "Number of replicas"
},
"image": {
"type": "object",
"required": ["repository"],
"properties": {
"repository": {
"type": "string",
"minLength": 1
},
"tag": {
"type": "string"
},
"pullPolicy": {
"type": "string",
"enum": ["Always", "IfNotPresent", "Never"]
}
}
},
"service": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["ClusterIP", "NodePort", "LoadBalancer"]
},
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535
}
}
}
}
}

3 - Templates

3.1 Standard labels

# _helpers.tpl
{{- define "my-app.labels" -}}
helm.sh/chart: {{ include "my-app.chart" . }}
{{ include "my-app.selectorLabels" . }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: {{ .Chart.Name }}
{{- with .Values.commonLabels }}
{{ toYaml . }}
{{- end }}
{{- end }}

{{- define "my-app.selectorLabels" -}}
app.kubernetes.io/name: {{ include "my-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

3.2 Conditional generation

# Toujours vérifier si une ressource doit être créée
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
# ...
{{- end }}

# Vérifier les valeurs optionnelles
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 2 }}
{{- end }}

# Valeurs par défaut sûres
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"

3.3 DRY (Don't Repeat Yourself)

# _helpers.tpl - Helper réutilisable pour les probes
{{- define "my-app.probes" -}}
{{- if .Values.probes.liveness.enabled }}
livenessProbe:
{{- if .Values.probes.liveness.httpGet }}
httpGet:
path: {{ .Values.probes.liveness.httpGet.path }}
port: {{ .Values.probes.liveness.httpGet.port | default "http" }}
{{- end }}
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds | default 10 }}
periodSeconds: {{ .Values.probes.liveness.periodSeconds | default 10 }}
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds | default 5 }}
failureThreshold: {{ .Values.probes.liveness.failureThreshold | default 3 }}
{{- end }}
{{- if .Values.probes.readiness.enabled }}
readinessProbe:
{{- if .Values.probes.readiness.httpGet }}
httpGet:
path: {{ .Values.probes.readiness.httpGet.path }}
port: {{ .Values.probes.readiness.httpGet.port | default "http" }}
{{- end }}
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds | default 5 }}
periodSeconds: {{ .Values.probes.readiness.periodSeconds | default 5 }}
{{- end }}
{{- end }}

# Utilisation dans deployment.yaml
containers:
- name: {{ .Chart.Name }}
{{- include "my-app.probes" . | nindent 10 }}

4 - Security

4.1 Pod Security Standards

# values.yaml
podSecurityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault

securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
capabilities:
drop:
- ALL

4.2 Network Policies

# templates/networkpolicy.yaml
{{- if .Values.networkPolicy.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "my-app.fullname" . }}
spec:
podSelector:
matchLabels:
{{- include "my-app.selectorLabels" . | nindent 6 }}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
ports:
- protocol: TCP
port: {{ .Values.service.port }}
egress:
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: postgresql
ports:
- protocol: TCP
port: 5432
# Permettre DNS
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
{{- end }}

4.3 Secrets management

# Ne jamais hardcoder de secrets dans values.yaml
# Utiliser des références externes

# Option 1: Secret externe
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.existingSecret | default (include "my-app.fullname" .) }}
key: db-password

# Option 2: Helm secrets plugin (valeurs chiffrées)
# values.yaml.dec -> values.yaml (chiffré avec sops)

# Option 3: External Secrets Operator
# templates/external-secret.yaml
{{- if .Values.externalSecrets.enabled }}
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: {{ include "my-app.fullname" . }}
spec:
refreshInterval: 1h
secretStoreRef:
name: {{ .Values.externalSecrets.secretStore }}
kind: SecretStore
target:
name: {{ include "my-app.fullname" . }}-secrets
data:
- secretKey: db-password
remoteRef:
key: {{ .Values.externalSecrets.path }}
property: password
{{- end }}

5 - CI/CD

5.1 Linting and validation

# .github/workflows/lint-test.yaml
name: Lint and Test Charts

on:
pull_request:
paths:
- 'charts/**'

jobs:
lint-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Helm
uses: azure/setup-helm@v3
with:
version: v3.13.0

- name: Set up chart-testing
uses: helm/chart-testing-[email protected]

- name: Lint charts
run: ct lint --config ct.yaml

- name: Create kind cluster
uses: helm/kind-[email protected]

- name: Install and test charts
run: ct install --config ct.yaml

5.2 chart-testing configuration

# ct.yaml
remote: origin
target-branch: main
chart-dirs:
- charts
chart-repos:
- bitnami=https://charts.bitnami.com/bitnami
validate-maintainers: false
check-version-increment: true

5.3 Automatic release

# .github/workflows/release.yaml
name: Release Charts

on:
push:
branches:
- main

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Configure Git
run: |
git config user.name "$GITHUB_ACTOR"
git config user.email "[email protected]"

- name: Install Helm
uses: azure/setup-helm@v3

- name: Run chart-releaser
uses: helm/chart-releaser-[email protected]
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

6 - Documentation

6.1 README with helm-docs

Create a README.md.gotmpl file that will be used by helm-docs to automatically generate the documentation:

Template structure:

  • chart.name: Chart name
  • chart.description: Chart description
  • chart.requirementsSection: Dependencies section
  • chart.valuesSection: Automatically generated values table

Example README.md.gotmpl template:

The template uses Go template directives such as:

  • template "chart.name" . to insert the name
  • template "chart.description" . for the description
  • template "chart.valuesSection" . for the values table
# Générer le README automatiquement
helm-docs --chart-search-root=charts

# Ou pour un chart spécifique
helm-docs --chart-search-root=charts/my-app

6.2 Informative NOTES.txt

The templates/NOTES.txt file displays information after installation:

Typical content:

  • Release name and version
  • Access instructions (URL if ingress is enabled, port-forward otherwise)
  • Links to the documentation

Go template elements used:

TemplateDescription
.Chart.NameChart name
.Release.NameRelease name
.Chart.VersionChart version
.Chart.AppVersionApplication version
.Values.ingress.enabledCheck whether ingress is enabled
.Values.service.portService port

NOTES.txt best practices:

  • Display version information
  • Provide access instructions according to the configuration
  • Include useful links (docs, issues)
  • Use emojis for readability

7 - Pre-release checklist

## Pre-release Checklist

### Chart.yaml
- [ ] Version incrémentée (SemVer)
- [ ] appVersion mis à jour
- [ ] Description claire
- [ ] Maintainers à jour

### values.yaml
- [ ] Valeurs par défaut sensées
- [ ] Commentaires explicatifs
- [ ] Pas de secrets en dur

### Templates
- [ ] Labels standards appliqués
- [ ] Resources requests/limits définis
- [ ] Security context configuré
- [ ] Probes configurées

### Tests
- [ ] helm lint passe
- [ ] helm template fonctionne
- [ ] Tests unitaires passent
- [ ] Tests d'intégration passent

### Documentation
- [ ] README à jour
- [ ] CHANGELOG mis à jour
- [ ] NOTES.txt informatif

Summary

In this chapter, we covered the best practices:

  • Structure and organization of charts
  • Well-documented and validated values
  • DRY and maintainable templates
  • Security (Pod Security, Network Policies)
  • CI/CD with automatic linting and tests
  • Complete documentation

Next step

In the next chapter, we will put things into practice with Exercises and Projects.

→ Next chapter: Exercises and Projects


← Back to the table of contents