Progress · 0/4 sections
📦 Week 3 — Advanced Kubernetes (Days 15–21)
10 min read · Days 15–21 · Notion
Core insight: Week 2 gave you the primitives. Week 3 gives you the patterns that make Kubernetes production-worthy: Helm for packaging, StatefulSets for databases, HPA for autoscaling, PVCs for storage, Network Policies for zero-trust, and Operators for complex lifecycle management.
📅 Day 15 — Helm: Kubernetes Package Manager
Why Helm
Raw Kubernetes manifests have no templating, no versioning, no dependency management. A real app has 20+ manifests. Helm packages them into a Chart with values you override per environment.
# Install Helm
brew install helm
# Essential commands
helm repo add stable https://charts.helm.sh/stable
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
# Search for charts
helm search repo postgres
helm search hub nginx
# Install a chart
helm install my-postgres bitnami/postgresql \
--namespace dev \
--create-namespace \
--set auth.password=secret123 \
--set primary.persistence.size=10Gi
# Upgrade a release
helm upgrade my-postgres bitnami/postgresql \
--namespace dev \
--set auth.password=newsecret
# Rollback
helm rollback my-postgres 1 --namespace dev
# List releases
helm list --all-namespaces
# Uninstall
helm uninstall my-postgres --namespace dev
# Render templates without installing (great for debugging)
helm template my-app ./charts/my-app -f values.prod.yaml
# Diff before upgrade (plugin)
helm plugin install https://github.com/databus23/helm-diff
helm diff upgrade my-postgres bitnami/postgresql --set auth.password=newWrite your own Helm chart
# Scaffold a new chart
helm create my-go-service
# Creates:
# my-go-service/
# Chart.yaml — chart metadata
# values.yaml — default values
# templates/ — kubernetes manifests with templating
# charts/ — chart dependencies# Chart.yaml
apiVersion: v2
name: my-go-service
description: Production Go backend service
type: application
version: 0.1.0 # chart version
appVersion: "1.0.0" # app version
dependencies:
- name: postgresql
version: "13.x.x"
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled# values.yaml — defaults
replicaCount: 2
image:
repository: myapp
tag: "1.0.0"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
targetPort: 8080
ingress:
enabled: true
className: nginx
host: api.myapp.com
tls: true
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
env:
PORT: "8080"
LOG_LEVEL: "info"
secrets:
dbPassword: ""
jwtKey: ""
postgresql:
enabled: true
auth:
database: appdb
username: appuser# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-go-service.fullname" . }}
labels:
{{- include "my-go-service.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "my-go-service.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "my-go-service.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
ports:
- containerPort: {{ .Values.service.targetPort }}
resources:
{{- toYaml .Values.resources | nindent 10 }}
env:
{{- range $key, $val := .Values.env }}
- name: {{ $key }}
value: {{ $val | quote }}
{{- end }}
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "my-go-service.fullname" . }}-secrets
key: db-password# values.prod.yaml — production overrides
replicaCount: 5
image:
tag: "2.1.0"
resources:
requests:
cpu: 500m
memory: 256Mi
limits:
cpu: 2
memory: 1Gi
env:
LOG_LEVEL: "warn"# Install in production with overrides
helm upgrade --install my-go-service ./charts/my-go-service \
-f values.prod.yaml \
--set image.tag=2.1.0 \
--namespace production \
--create-namespace \
--atomic # rollback automatically if hooks fail
--timeout 5m📅 Day 16 — StatefulSets & Persistent Volumes
Why StatefulSets for databases
- Pods in a Deployment are interchangeable (random names, any order)
- StatefulSet pods have: stable names (
db-0,db-1,db-2), stable network identity, ordered start/stop - Each pod gets its own PersistentVolumeClaim (separate storage)
# statefulset.yaml — PostgreSQL in Kubernetes
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: dev
spec:
serviceName: postgres-headless # must match headless service
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
env:
- name: POSTGRES_DB
value: appdb
- name: POSTGRES_USER
value: appuser
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: db-password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
ports:
- containerPort: 5432
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 1
memory: 1Gi
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
readinessProbe:
exec:
command: ["pg_isready", "-U", "appuser", "-d", "appdb"]
initialDelaySeconds: 15
periodSeconds: 10
volumeClaimTemplates: # each pod gets its own PVC
- metadata:
name: postgres-storage
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: standard # use cloud storage class in production
resources:
requests:
storage: 10Gi
---
# Headless service: stable DNS for each pod
apiVersion: v1
kind: Service
metadata:
name: postgres-headless
namespace: dev
spec:
clusterIP: None # headless: no VIP, returns pod IPs
selector:
app: postgres
ports:
- port: 5432
---
# Regular service for client access
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: dev
spec:
selector:
app: postgres
ports:
- port: 5432# DNS for StatefulSet pods:
# pod-0: postgres-0.postgres-headless.dev.svc.cluster.local
# pod-1: postgres-1.postgres-headless.dev.svc.cluster.local
# Scale StatefulSet (ordered: 0 then 1 then 2)
kubectl scale statefulset postgres --replicas=3
# Watch ordered startup
kubectl get pods -l app=postgres -w
# PVC is NOT deleted when StatefulSet is deleted
kubectl delete statefulset postgres
kubectl get pvc # still exists! Data is safe.PersistentVolume, PVC, StorageClass
# storageclass.yaml (production: AWS EBS)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
type: gp3
iops: "3000"
throughput: "125"
volumeBindingMode: WaitForFirstConsumer # provision in same AZ as pod
reclaimPolicy: Retain # NEVER delete data automatically in production
allowVolumeExpansion: true📅 Day 17 — HPA, VPA & Karpenter
Horizontal Pod Autoscaler (HPA)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-go-api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-go-api
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: AverageValue
averageValue: 200Mi
- type: Pods # custom metric from Prometheus
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "1000"
behavior:
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Pods
value: 4
periodSeconds: 60 # max 4 pods added per minute
scaleDown:
stabilizationWindowSeconds: 300 # wait 5 min before scaling down
policies:
- type: Percent
value: 10
periodSeconds: 60 # max 10% removed per minute# Enable metrics-server (required for CPU/memory HPA)
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
# For kind: add --kubelet-insecure-tls flag
kubectl patch deployment metrics-server -n kube-system \
--type='json' \
-p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--kubelet-insecure-tls"}]'
# Watch HPA in action
kubectl get hpa my-go-api-hpa -w
kubectl top pods
# Generate load to trigger scaling
kubectl run load-gen --image=busybox --rm -it -- \
sh -c "while true; do wget -q -O- http://my-go-api/; done"Vertical Pod Autoscaler (VPA)
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: my-go-api-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: my-go-api
updatePolicy:
updateMode: "Off" # Recommendation only (Off), or Auto (restarts pods)
resourcePolicy:
containerPolicies:
- containerName: api
minAllowed:
cpu: 50m
memory: 32Mi
maxAllowed:
cpu: 2
memory: 2Gi📅 Day 18 — Network Policies
# Default deny all ingress + egress (zero-trust baseline)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {} # applies to ALL pods
policyTypes:
- Ingress
- Egress
---
# Allow: api pods can receive from ingress controller
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-ingress-to-api
namespace: production
spec:
podSelector:
matchLabels:
app: my-go-api
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- port: 8080
---
# Allow: api can reach postgres
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-api-to-postgres
namespace: production
spec:
podSelector:
matchLabels:
app: postgres
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: my-go-api
ports:
- port: 5432
---
# Allow: api egress to kube-dns and external APIs
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-api-egress
namespace: production
spec:
podSelector:
matchLabels:
app: my-go-api
policyTypes:
- Egress
egress:
- to: # kube-dns
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- port: 53
protocol: UDP
- to: # postgres
- podSelector:
matchLabels:
app: postgres
ports:
- port: 5432
- to: # external HTTPS
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
ports:
- port: 443📅 Day 19 — Kustomize
# Kustomize: overlay system built into kubectl
# Base manifests + environment-specific patches
k8s/
base/
deployment.yaml
service.yaml
kustomization.yaml
overlays/
dev/
kustomization.yaml
patch-replicas.yaml
production/
kustomization.yaml
patch-replicas.yaml
patch-resources.yaml# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
commonLabels:
managed-by: kustomize# overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namePrefix: prod-
namespace: production
images:
- name: myapp
newTag: "2.1.0"
patchesStrategicMerge:
- patch-replicas.yaml
- patch-resources.yaml
configMapGenerator:
- name: app-config
literals:
- LOG_LEVEL=warn
- ENVIRONMENT=production# overlays/production/patch-replicas.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-go-api
spec:
replicas: 5# Apply production overlay
kubectl apply -k k8s/overlays/production
# Preview rendered output
kubectl kustomize k8s/overlays/production
# Diff before applying
kubectl diff -k k8s/overlays/production📅 Day 20 — Custom Resource Definitions (CRDs) & Operators
# CRD: extend the Kubernetes API with your own resource types
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: databases.mycompany.io
spec:
group: mycompany.io
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required: [engine, version, storage]
properties:
engine:
type: string
enum: [postgres, mysql]
version:
type: string
storage:
type: string
replicas:
type: integer
minimum: 1
maximum: 5
default: 1
scope: Namespaced
names:
plural: databases
singular: database
kind: Database
shortNames: [db]# Using the custom resource
apiVersion: mycompany.io/v1
kind: Database
metadata:
name: orders-db
namespace: production
spec:
engine: postgres
version: "16"
storage: 50Gi
replicas: 2# Real-world Operators to install and study
# Postgres Operator (Crunchy Data)
helm repo add crunchy https://charts.crunchydata.com/charts
helm install pgo crunchy/pgo -n postgres-operator --create-namespace
# Redis Operator
helm repo add redis-operator https://spotahome.github.io/redis-operator
helm install redis-operator redis-operator/redis-operator -n redis-operator --create-namespace
# Cert-Manager (manages TLS certificates)
helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --set installCRDs=true
# Watch an operator manage lifecycle
kubectl apply -f postgres-cluster.yaml
kubectl get pods -w # watch operator create primary + replicas automatically📅 Day 21 — Week 3 Project: Production Helm Chart
Build a complete Helm chart for your Go service:
charts/my-go-service/
Chart.yaml
values.yaml
values.dev.yaml
values.prod.yaml
templates/
_helpers.tpl
deployment.yaml
service.yaml
ingress.yaml
hpa.yaml
vpa.yaml
configmap.yaml
secret.yaml
serviceaccount.yaml
rbac.yaml
networkpolicy.yaml
pdb.yaml # PodDisruptionBudget
NOTES.txt# templates/pdb.yaml — ensure at least 1 pod survives disruptions
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "my-go-service.fullname" . }}-pdb
spec:
minAvailable: 1
selector:
matchLabels:
{{- include "my-go-service.selectorLabels" . | nindent 6 }}Deliverables:
helm install dev ./charts/my-go-service -f values.dev.yamlworkshelm install prod ./charts/my-go-service -f values.prod.yamlworks- HPA scales from 2 to 8 replicas under load test
- Network policies isolate each tier
- StatefulSet PostgreSQL with data persisting across pod restarts
helm test my-go-serviceruns connectivity tests
⚠️ Common mistakes Week 3
Mistake 1
❌ No PodDisruptionBudget. Node drain (maintenance, upgrades) kills ALL replicas simultaneously. Service down for minutes.
✅ PDB with minAvailable: 1 guarantees at least one pod always running during voluntary disruptions.
Mistake 2
❌ HPA without stabilization window. Rapid scale-down causes flapping: 10 pods → 2 pods → 10 pods every few minutes under variable load.
✅ Set scaleDown.stabilizationWindowSeconds: 300. Kubernetes waits 5 minutes of consistently low usage before scaling down.
Mistake 3
❌ No NetworkPolicy. A compromised pod can freely talk to every other service in the cluster.
✅ Start with default-deny-all, then explicitly allow required paths. Treat the cluster network like a firewall.