Helm Charts with Python — Deep Dive
Chart structure for a Python service
A production Helm chart for a FastAPI or Django service typically includes more than the basics. Here’s a real-world layout:
my-python-app/
├── Chart.yaml
├── values.yaml
├── values-staging.yaml
├── values-production.yaml
├── templates/
│ ├── _helpers.tpl
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── hpa.yaml
│ ├── configmap.yaml
│ ├── secret.yaml
│ ├── serviceaccount.yaml
│ ├── pdb.yaml
│ └── tests/
│ └── test-connection.yaml
└── charts/
└── postgresql/
Advanced templating techniques
Named templates for reuse
The _helpers.tpl file defines reusable template fragments:
{{- define "myapp.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "myapp.selectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
Conditional resources
Python services often need different resources per environment:
# templates/hpa.yaml
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "myapp.fullname" . }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "myapp.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPU }}
{{- end }}
Deployment template with Python-specific concerns
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }}
labels:
{{- include "myapp.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: {{ .Values.service.port }}
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "myapp.fullname" . }}-secret
key: database-url
- name: WORKERS
value: {{ .Values.gunicorn.workers | quote }}
- name: WORKER_CLASS
value: {{ .Values.gunicorn.workerClass | quote }}
livenessProbe:
httpGet:
path: {{ .Values.probes.liveness.path }}
port: {{ .Values.service.port }}
initialDelaySeconds: {{ .Values.probes.liveness.initialDelay }}
periodSeconds: 10
readinessProbe:
httpGet:
path: {{ .Values.probes.readiness.path }}
port: {{ .Values.service.port }}
initialDelaySeconds: {{ .Values.probes.readiness.initialDelay }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
The checksum/config annotation forces a pod restart when ConfigMap contents change — critical for Python apps that read config at startup.
Values file for a Python web service
# values.yaml
replicaCount: 2
image:
repository: ghcr.io/myorg/myapp
tag: "latest"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 8000
gunicorn:
workers: 4
workerClass: "uvicorn.workers.UvicornWorker"
probes:
liveness:
path: /health
initialDelay: 10
readiness:
path: /ready
initialDelay: 5
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: "1"
memory: 512Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
targetCPU: 70
postgresql:
enabled: true
auth:
database: myapp
username: myapp
Generating charts with Python
When managing many microservices, manual chart maintenance doesn’t scale. Python can generate charts from service definitions:
import yaml
from pathlib import Path
from dataclasses import dataclass, field
@dataclass
class ServiceSpec:
name: str
port: int = 8000
replicas: int = 2
cpu_request: str = "250m"
memory_request: str = "256Mi"
health_path: str = "/health"
dependencies: list[str] = field(default_factory=list)
def generate_chart(spec: ServiceSpec, output_dir: Path) -> None:
chart_dir = output_dir / spec.name
templates_dir = chart_dir / "templates"
templates_dir.mkdir(parents=True, exist_ok=True)
# Chart.yaml
chart_meta = {
"apiVersion": "v2",
"name": spec.name,
"version": "0.1.0",
"appVersion": "1.0.0",
"description": f"Helm chart for {spec.name}",
}
if spec.dependencies:
chart_meta["dependencies"] = [
{"name": dep, "version": ">=0.1.0", "repository": "file://../" + dep}
for dep in spec.dependencies
]
(chart_dir / "Chart.yaml").write_text(yaml.dump(chart_meta))
# values.yaml
values = {
"replicaCount": spec.replicas,
"image": {
"repository": f"ghcr.io/myorg/{spec.name}",
"tag": "latest",
},
"service": {"port": spec.port},
"resources": {
"requests": {
"cpu": spec.cpu_request,
"memory": spec.memory_request,
}
},
}
(chart_dir / "values.yaml").write_text(yaml.dump(values))
print(f"Generated chart for {spec.name} at {chart_dir}")
# Usage
specs = [
ServiceSpec("user-service", port=8001, dependencies=["postgresql"]),
ServiceSpec("order-service", port=8002, replicas=3),
ServiceSpec("notification-service", port=8003, cpu_request="100m"),
]
for spec in specs:
generate_chart(spec, Path("./charts"))
Testing Helm charts
helm template (local rendering)
helm template myrelease ./my-python-app -f values-staging.yaml
This renders all templates without deploying — essential for catching syntax errors.
helm unittest (plugin)
# templates/tests/deployment_test.yaml
suite: deployment tests
templates:
- deployment.yaml
tests:
- it: should set correct replica count
set:
replicaCount: 5
asserts:
- equal:
path: spec.replicas
value: 5
- it: should use uvicorn worker class
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: WORKER_CLASS
value: "uvicorn.workers.UvicornWorker"
Python-based chart validation
import subprocess
import yaml
def validate_chart(chart_path: str, values_file: str) -> list[str]:
"""Render chart and validate the output."""
result = subprocess.run(
["helm", "template", "test", chart_path, "-f", values_file],
capture_output=True, text=True
)
if result.returncode != 0:
return [f"Template error: {result.stderr}"]
errors = []
for doc in yaml.safe_load_all(result.stdout):
if doc is None:
continue
kind = doc.get("kind", "")
if kind == "Deployment":
containers = (
doc.get("spec", {})
.get("template", {})
.get("spec", {})
.get("containers", [])
)
for c in containers:
if "resources" not in c:
errors.append(
f"Container {c['name']} missing resource limits"
)
if "livenessProbe" not in c:
errors.append(
f"Container {c['name']} missing liveness probe"
)
return errors
CI/CD automation with Helm
A typical GitHub Actions workflow for a Python service with Helm:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push image
run: |
docker build -t ghcr.io/myorg/myapp:${{ github.sha }} .
docker push ghcr.io/myorg/myapp:${{ github.sha }}
- name: Deploy with Helm
run: |
helm upgrade --install myapp ./chart \
--set image.tag=${{ github.sha }} \
-f chart/values-production.yaml \
--wait --timeout 5m \
--atomic
The --atomic flag ensures that if the deployment fails health checks, Helm automatically rolls back to the previous release. This is Python-app-friendly because slow startups (common with Django) won’t leave the cluster in a half-deployed state.
Tradeoffs and alternatives
| Approach | Strengths | Weaknesses |
|---|---|---|
| Helm | Mature ecosystem, rollback, OCI registry | Go templating is awkward, YAML indentation issues |
| Kustomize | No templating engine, pure overlays | Less flexible for complex variations |
| CDK8s (Python) | Real programming language, type safety | Extra build step, smaller community |
| Pulumi K8s | Full Python, state management | Heavier, requires Pulumi runtime |
For Python teams, CDK8s is increasingly attractive because you write Kubernetes resources in Python with full IDE support, then synthesize to YAML or Helm-compatible output.
The one thing to remember: Production Helm charts for Python services succeed when they encode operational knowledge — health probes, resource limits, config checksums, and rollback safety — into the chart itself, making deployments self-documenting and self-protecting.
See Also
- Python Ansible Automation How Python powers Ansible to automatically set up and manage hundreds of servers without logging into each one
- Python Docker Compose Orchestration How Python developers use Docker Compose to run multiple services together like a conductor leading an orchestra
- Python Etcd Distributed Config How Python applications use etcd to share configuration across many servers and react to changes instantly
- Python Nomad Job Scheduling How Python developers use HashiCorp Nomad to run their programs across many computers without managing each one
- Python Pulumi Infrastructure How Python developers use Pulumi to build cloud infrastructure using the same language they already know