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