devops

Helm & GitOps

Helm chart structure, values, templating, upgrades, rollbacks, GitOps principles, and Argo CD


Helm Chart Structure

Helm is the package manager for Kubernetes. A chart is a collection of files that describe a related set of Kubernetes resources.

myapp/
├── Chart.yaml # chart metadata
├── values.yaml # default configuration values
├── templates/ # Kubernetes manifest templates
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── configmap.yaml
│ ├── secret.yaml
│ ├── hpa.yaml
│ ├── _helpers.tpl # reusable template snippets
│ └── NOTES.txt # printed after install
└── charts/ # chart dependencies

Chart.yaml

apiVersion: v2
name: myapp
description: My application Helm chart
type: application
version: 0.1.0 # chart version (semver)
appVersion: "1.2.3" # application version being packaged
dependencies:
- name: postgresql
version: "~13.0"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled

Values & Templating

values.yaml

# Default values
replicaCount: 2
image:
repository: myrepo/myapp
tag: "" # defaults to Chart.appVersion
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
ingress:
enabled: false
className: "nginx"
hosts: []
tls: []
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
postgresql:
enabled: true
auth:
database: myapp

Template Syntax

templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }} # helper function
labels:
{{- include "myapp.labels" . | nindent 4 }} # include + indent
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "myapp.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: {{ .Values.service.port }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- if .Values.env }}
env:
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- end }}

_helpers.tpl

{{/*
Expand the name of the chart.
*/}}
{{- define "myapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "myapp.labels" -}}
helm.sh/chart: {{ include "myapp.chart" . }}
{{ include "myapp.selectorLabels" . }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "myapp.selectorLabels" -}}
app.kubernetes.io/name: {{ include "myapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

Helm CLI Usage

Terminal window
# Install a chart
helm install myapp ./myapp
helm install myapp ./myapp --values custom-values.yaml
helm install myapp ./myapp --set image.tag=1.2.4
helm install myapp ./myapp --namespace production --create-namespace
# Install from public repo
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm install my-postgres bitnami/postgresql --version 13.2.0
# Render templates without installing (dry run)
helm template myapp ./myapp --values prod.yaml
helm install myapp ./myapp --dry-run
# List releases
helm list
helm list -n production
helm list --all-namespaces
# Status of a release
helm status myapp
# Get values of a release
helm get values myapp
helm get values myapp --all # includes defaults

Helm Upgrades & Rollbacks

Terminal window
# Upgrade a release
helm upgrade myapp ./myapp
helm upgrade myapp ./myapp --values prod.yaml --set image.tag=1.2.5
# Upgrade or install if not exists
helm upgrade --install myapp ./myapp --values prod.yaml
# Rollback to previous version
helm rollback myapp
# Rollback to specific revision
helm history myapp # see all revisions
helm rollback myapp 3 # roll back to revision 3
# Uninstall (removes all K8s resources but keeps history)
helm uninstall myapp
helm uninstall myapp --keep-history # keep history for rollback

Upgrade best practices:

Terminal window
# Always diff before upgrading
helm diff upgrade myapp ./myapp --values prod.yaml
# (requires helm-diff plugin: helm plugin install https://github.com/databus23/helm-diff)
# Check if upgrade would change anything
helm upgrade myapp ./myapp --dry-run --values prod.yaml

GitOps Principles

GitOps = operational model where Git is the single source of truth for your infrastructure and applications.

Core principles:

  1. Git as single source of truth — all desired state is in Git
  2. Declarative — describe what you want, not how to do it
  3. Automated sync — an operator continuously reconciles actual state with desired state
  4. Pull-based — the cluster pulls from Git, doesn’t get pushed to (more secure)
Developer → pushes to Git → [GitOps Operator watches repo] → applies changes to cluster
(Argo CD / Flux)

vs. traditional push-based CI/CD:

Developer → CI pipeline runs → pipeline pushes to cluster
(pipeline needs credentials, cluster is externally accessible)

Why GitOps is better for production:

  • Full audit trail (every change is a Git commit)
  • Easy rollback (revert the commit)
  • Drift detection (operator alerts if cluster diverges from git)
  • No pipeline credentials stored for cluster access

Argo CD

Argo CD is a declarative GitOps operator for Kubernetes. It watches Git repos and automatically syncs changes to the cluster.

Installation

Terminal window
# Install Argo CD
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Access UI
kubectl port-forward svc/argocd-server -n argocd 8080:443
# Get initial admin password
kubectl get secret argocd-initial-admin-secret -n argocd \
-o jsonpath='{.data.password}' | base64 -d
# Install argocd CLI
# Then login
argocd login localhost:8080

Application Manifest

argocd-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io # cascade delete
spec:
project: default
source:
repoURL: https://github.com/myorg/myapp-config
targetRevision: HEAD # or specific branch/tag/commit
path: kubernetes/production # path within repo
# If using Helm
helm:
valueFiles:
- values.yaml
- values-production.yaml
parameters:
- name: image.tag
value: "1.2.3"
destination:
server: https://kubernetes.default.svc # in-cluster
namespace: production
syncPolicy:
automated:
prune: true # delete resources removed from git
selfHeal: true # revert manual changes to cluster
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
Terminal window
# CLI operations
argocd app list
argocd app get myapp
argocd app sync myapp # manual sync
argocd app sync myapp --dry-run # preview sync
argocd app history myapp
argocd app rollback myapp 3 # rollback to history ID
argocd app diff myapp # diff between git and cluster

Sync, Drift Correction, Rollback via Git

Terminal window
# Sync — apply git state to cluster
argocd app sync myapp
# Argo CD detects drift automatically when:
# - Someone uses kubectl apply directly
# - Cluster state changes (pod restart changes something)
# With selfHeal=true, it automatically corrects drift
# Check if app is out of sync
argocd app get myapp | grep -E "Status|Sync"
# Sync Status: OutOfSync — someone changed the cluster directly
# Rollback (Argo CD)
argocd app history myapp
# ID DATE REVISION
# 1 2024-01-01 abc123 (HEAD)
# 2 2024-01-02 def456
argocd app rollback myapp 2
# GitOps rollback (preferred) — revert the git commit
git revert HEAD
git push
# Argo CD automatically syncs the revert

End-to-End CI/CD Pipeline with GitOps

Developer pushes code
GitHub Actions runs:
[Lint] → [Test] → [Build Docker image] → [Push to ECR with git-sha tag]
▼ (on success, update image tag in config repo)
Config Repo (separate repo for K8s manifests):
values-production.yaml:
image:
tag: "abc1234" ← updated by CI pipeline
Argo CD detects change in config repo
Argo CD syncs new image to cluster
Deployment rolls out → health checks pass → done
# In GitHub Actions — update image tag in config repo
- name: Update image tag in config repo
run: |
git clone https://x-access-token:${{ secrets.CONFIG_REPO_PAT }}@github.com/myorg/myapp-config
cd myapp-config
# Update the tag in values file
yq e '.image.tag = "${{ github.sha }}"' -i kubernetes/production/values.yaml
git config user.email "ci@myorg.com"
git config user.name "CI Pipeline"
git commit -am "ci: update myapp image to ${{ github.sha }}"
git push