GitHub = Source of Truth
Helm = Templates + Values
Argo CD = GitOps CD / Reconciliation
Kubernetes = Runtime
AWS Load Balancer Controller = Creates ALB
ECR = Stores Restaurant image
Use fake classroom secrets only; don't commit real credentials.
1. Architecture
Developer
│
│ git push
▼
GitHub Repository
│
│
├── Helm Chart
│ ├── templates/
│ └── values.yaml
│
▼
Argo CD
│
│ Helm rendering
│
│ Desired vs Actual
│
│ Reconciliation
▼
Kubernetes API
│
├── ConfigMap
├── ServiceAccount
├── Deployment
│ ↓
│ ReplicaSet
│ ↓
│ Pods
│
├── Service
├── HPA
├── PDB
└── Ingress
│
▼
AWS Load Balancer Controller
│
▼
ALB
│
▼
Restaurant Pods
2. Repository structure
Have students create:
restaurant-company-gitops/
│
├── argocd/
│ └── application.yaml
│
└── helm/
└── restaurant/
├── Chart.yaml
├── values.yaml
├── values-prod.yaml
│
└── templates/
├── namespace.yaml
├── configmap.yaml
├── serviceaccount.yaml
├── deployment.yaml
├── service.yaml
├── pdb.yaml
├── hpa.yaml
└── ingress.yaml
For the first production lab, I would not put a plaintext Secret in Git. Explain that production GitOps requires a proper secret-management pattern.
3. Chart.yaml
Create:
apiVersion: v2
name: restaurant-company
description: Production Helm chart for Restaurant Company
type: application
version: 0.1.0
appVersion: "1.0.0"
Explain:
version
↓
version of Helm chart
appVersion
↓
version of our application
4. values.yaml
This contains sensible defaults:
namespace:
name: restaurant-prod
replicaCount: 2
image:
repository: YOUR_AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/restaurant-company
tag: latest
pullPolicy: Always
container:
port: 80
serviceAccount:
name: restaurant-sa
service:
type: ClusterIP
port: 80
targetPort: 80
config:
appName: "Restaurant Company"
environment: "production"
logLevel: "info"
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
probes:
startup:
path: /
periodSeconds: 5
failureThreshold: 12
readiness:
path: /
periodSeconds: 5
failureThreshold: 3
liveness:
path: /
periodSeconds: 10
failureThreshold: 3
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
pdb:
enabled: true
minAvailable: 2
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
ingress:
enabled: true
className: alb
scheme: internet-facing
targetType: ip
Students must replace:
YOUR_AWS_ACCOUNT_ID
with their own account ID and make sure the ECR repository name is correct.
5. values-prod.yaml
Now show why Helm is useful.
replicaCount: 3
image:
tag: latest
config:
environment: "production"
logLevel: "info"
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 70
pdb:
enabled: true
minAvailable: 2
Explain:
values.yamlgives us defaults.values-prod.yamloverrides only production-specific values.
6. Namespace template
templates/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: {{ .Values.namespace.name }}
labels:
environment: {{ .Values.config.environment }}
app.kubernetes.io/managed-by: Helm
7. ConfigMap
templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: restaurant-config
namespace: {{ .Values.namespace.name }}
data:
APP_NAME: {{ .Values.config.appName | quote }}
APP_ENV: {{ .Values.config.environment | quote }}
LOG_LEVEL: {{ .Values.config.logLevel | quote }}
Explain:
values-prod.yaml
↓
Helm
↓
ConfigMap
↓
Pod environment
8. ServiceAccount
templates/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ .Values.serviceAccount.name }}
namespace: {{ .Values.namespace.name }}
Important teaching point:
This Kubernetes ServiceAccount does not automatically give the Pod AWS permissions. For AWS API access from EKS workloads, we'd separately configure the appropriate AWS workload identity mechanism.
9. Production Deployment
templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: restaurant
namespace: {{ .Values.namespace.name }}
labels:
app: restaurant
spec:
replicas: {{ .Values.replicaCount }}
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: {{ .Values.rollingUpdate.maxUnavailable }}
maxSurge: {{ .Values.rollingUpdate.maxSurge }}
selector:
matchLabels:
app: restaurant
template:
metadata:
labels:
app: restaurant
environment: {{ .Values.config.environment }}
spec:
serviceAccountName: {{ .Values.serviceAccount.name }}
terminationGracePeriodSeconds: 30
containers:
- name: restaurant
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.container.port }}
protocol: TCP
envFrom:
- configMapRef:
name: restaurant-config
resources:
requests:
cpu: {{ .Values.resources.requests.cpu | quote }}
memory: {{ .Values.resources.requests.memory | quote }}
limits:
cpu: {{ .Values.resources.limits.cpu | quote }}
memory: {{ .Values.resources.limits.memory | quote }}
startupProbe:
httpGet:
path: {{ .Values.probes.startup.path }}
port: http
periodSeconds: {{ .Values.probes.startup.periodSeconds }}
failureThreshold: {{ .Values.probes.startup.failureThreshold }}
readinessProbe:
httpGet:
path: {{ .Values.probes.readiness.path }}
port: http
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
livenessProbe:
httpGet:
path: {{ .Values.probes.liveness.path }}
port: http
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
This lets you review several concepts simultaneously:
Helm values
Deployment
Rolling Update
Resources
ConfigMap
ServiceAccount
Startup Probe
Readiness Probe
Liveness Probe
10. Service
templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: restaurant-service
namespace: {{ .Values.namespace.name }}
spec:
type: {{ .Values.service.type }}
selector:
app: restaurant
ports:
- name: http
protocol: TCP
port: {{ .Values.service.port }}
targetPort: {{ .Values.service.targetPort }}
Architecture:
Service
selector:
app=restaurant
│
▼
Pods
label:
app=restaurant
11. PodDisruptionBudget
templates/pdb.yaml
{{- if .Values.pdb.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: restaurant-pdb
namespace: {{ .Values.namespace.name }}
spec:
minAvailable: {{ .Values.pdb.minAvailable }}
selector:
matchLabels:
app: restaurant
{{- end }}
This introduces another Helm feature:
{{ if }}
Explain:
Helm can conditionally generate Kubernetes resources.
If:
pdb:
enabled: false
Helm doesn't render this PDB.
12. HPA
templates/hpa.yaml
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: restaurant-hpa
namespace: {{ .Values.namespace.name }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: restaurant
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
Remind them:
CPU-based HPA requires working resource metrics, commonly provided by Metrics Server, and CPU utilization percentage depends on CPU requests.
13. ALB Ingress
templates/ingress.yaml
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: restaurant-ingress
namespace: {{ .Values.namespace.name }}
annotations:
alb.ingress.kubernetes.io/scheme: {{ .Values.ingress.scheme | quote }}
alb.ingress.kubernetes.io/target-type: {{ .Values.ingress.targetType | quote }}
spec:
ingressClassName: {{ .Values.ingress.className }}
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: restaurant-service
port:
number: {{ .Values.service.port }}
{{- end }}
Important:
Creating this Ingress alone is not enough to create an AWS ALB. The AWS Load Balancer Controller must already be installed and correctly authorized in the EKS cluster.
Architecture:
Ingress YAML
↓
AWS Load Balancer Controller
↓
AWS API
↓
ALB
↓
Target Group
↓
Restaurant Pods
14. Validate the Helm chart
Before touching Argo CD:
cd helm/restaurant
Run:
helm lint .
Then:
helm template restaurant . \
-f values-prod.yaml
Tell students:
helm templatedoes not deploy the application. It shows us what Kubernetes YAML Helm generates.
You can save it for inspection:
helm template restaurant . \
-f values-prod.yaml \
> rendered.yaml
Then:
less rendered.yaml
This is a fantastic teaching step:
templates/
+
values.yaml
+
values-prod.yaml
↓
HELM
↓
rendered.yaml
↓
Normal Kubernetes manifests
Delete the generated file afterward if you don't want it committed:
rm rendered.yaml
15. Optional: Test Helm manually first
For learning, before introducing Argo CD, students can test:
helm upgrade --install restaurant . \
-f values-prod.yaml \
-n restaurant-prod \
--create-namespace
Then:
helm list -n restaurant-prod
And:
kubectl get all -n restaurant-prod
However, after switching this application to Argo CD management, stop manually deploying it with Helm.
That's important.
Learning Helm:
helm install / helm upgrade
↓
Production GitOps lesson:
git push → Argo CD
16. Clean up the manual Helm release before Argo CD
If you installed the application manually for the Helm exercise:
helm uninstall restaurant -n restaurant-prod
Verify:
helm list -n restaurant-prod
Then let Argo CD own the deployment.
This avoids teaching students to have both manual Helm and Argo CD competing to manage the same application.
17. Argo CD Application YAML
Now create:
argocd/application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: restaurant-company
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/YOUR_GITHUB_USERNAME/restaurant-company-gitops.git
targetRevision: main
path: helm/restaurant
helm:
valueFiles:
- values-prod.yaml
destination:
server: https://kubernetes.default.svc
namespace: restaurant-prod
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Students replace:
YOUR_GITHUB_USERNAME
with their repository owner.
18. Push everything to GitHub
From the repository root:
git add .
git commit -m "Add production Restaurant Helm GitOps configuration"
git push
Now Git contains:
GitHub
restaurant-company-gitops/
│
├── argocd/
│
│ └── application.yaml
│
│
└── helm/
└── restaurant/
├── Chart.yaml
├── values.yaml
├── values-prod.yaml
└── templates/
19. Bootstrap the Argo CD Application
This is the one initial kubectl apply students may see:
kubectl apply -f argocd/application.yaml
Explain carefully:
We are not manually deploying the Restaurant application with this command. We are registering an Argo CD Application that tells Argo CD where the Git source of truth lives.
After that, normal application changes happen through Git.
Check:
kubectl get applications -n argocd
Then open Argo CD.
20. What Argo CD now does
This is the key architecture:
GitHub
│
│ restaurant Helm chart
│
▼
Argo CD
│
├── reads Chart.yaml
├── reads values.yaml
├── applies values-prod.yaml overrides
│
▼
Helm rendering
│
▼
Kubernetes manifests
│
▼
Argo CD compares
│
├── Desired State — Git
│
└── Actual State — EKS
│
▼
Reconcile
│
▼
EKS
Students do not need to run:
helm upgrade restaurant ...
and do not run:
kubectl apply -f deployment.yaml
for normal GitOps changes.
21. GitOps Lab #1 — Scale 3 → 5
Current production value:
replicaCount: 3
Check:
kubectl get pods -n restaurant-prod
Now change values-prod.yaml:
replicaCount: 5
Then:
git add helm/restaurant/values-prod.yaml
git commit -m "Scale Restaurant Company to 5 replicas"
git push
Do NOT run Helm.
Do NOT run kubectl apply.
Watch:
kubectl get pods -n restaurant-prod -w
With auto-sync enabled:
values-prod.yaml
3 → 5
│
▼
git push
│
▼
GitHub
│
▼
Argo CD
│
▼
Helm render
│
▼
Deployment replicas: 5
│
▼
Kubernetes
│
▼
5 Pods
That connects the entire course.
22. GitOps Lab #2 — Configuration Drift
Git says:
replicaCount: 5
Now intentionally change the live cluster:
kubectl scale deployment restaurant \
--replicas=1 \
-n restaurant-prod
Watch:
kubectl get pods -n restaurant-prod -w
Because Argo CD has:
selfHeal: true
the important model is:
Git Desired State
5
≠
Live State
1
↓
CONFIGURATION DRIFT
↓
Argo CD selfHeal
↓
Live State reconciled
toward 5
Tell them:
This is why we call Git our Source of Truth.
23. GitOps Lab #3 — Bad Image
Now teach troubleshooting.
Change:
image:
tag: latest
to a tag that doesn't exist:
image:
tag: broken-version
Commit:
git add .
git commit -m "Demo broken Restaurant image"
git push
Argo CD may successfully reconcile the desired configuration while the workload becomes unhealthy.
Check:
kubectl get pods -n restaurant-prod
Then:
kubectl describe pod POD_NAME -n restaurant-prod
Likely:
ErrImagePull
ImagePullBackOff
Teach:
Argo CD:
"I deployed what Git requested."
Kubernetes:
"I cannot run this image."
Therefore:
SYNCED ≠ HEALTHY
Fix it in Git, not directly in the Deployment:
image:
tag: latest
Then:
git add .
git commit -m "Fix Restaurant image"
git push
Argo CD should reconcile the fix.
24. GitOps Lab #4 — PDB condition
Show them this:
pdb:
enabled: true
Render:
helm template restaurant helm/restaurant \
-f helm/restaurant/values-prod.yaml
PDB exists.
Change:
pdb:
enabled: false
Render again.
PDB disappears from the rendered desired manifests.
That's Helm conditional logic:
enabled: true
↓
Helm generates PDB
enabled: false
↓
Helm doesn't generate PDB
If this change is pushed while Argo CD prune is enabled, Argo CD can remove the previously managed PDB because it is no longer part of desired state.
That demonstrates Helm + Argo CD prune together.
25. Student troubleshooting checklist
Give them this exact order:
# Argo application
kubectl get applications -n argocd
# Namespace resources
kubectl get all -n restaurant-prod
# Pods
kubectl get pods -n restaurant-prod -o wide
# Deployment
kubectl describe deployment restaurant -n restaurant-prod
# Pod problems
kubectl describe pod POD_NAME -n restaurant-prod
# Application logs
kubectl logs POD_NAME -n restaurant-prod
# Service
kubectl get svc -n restaurant-prod
kubectl describe svc restaurant-service -n restaurant-prod
# Service endpoints
kubectl get endpointslice \
-n restaurant-prod \
-l kubernetes.io/service-name=restaurant-service
# HPA
kubectl get hpa -n restaurant-prod
# PDB
kubectl get pdb -n restaurant-prod
# Ingress
kubectl get ingress -n restaurant-prod
kubectl describe ingress restaurant-ingress -n restaurant-prod
Final diagram for the students
PRODUCTION GITOPS
Developer
│
git push
│
▼
GitHub
SOURCE OF TRUTH
│
│
┌─────────┴─────────┐
│ │
Helm Templates values-prod.yaml
│ │
└─────────┬─────────┘
│
▼
Argo CD
│
Helm Render
│
▼
Kubernetes YAML
│
Compare/Reconcile
│
▼
Kubernetes API
│
┌──────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
Deployment Service Ingress
│ │ │
▼ │ ▼
ReplicaSet │ AWS LB Controller
│ │ │
▼ │ ▼
Pods ◀───────────────┘ ALB
▲ │
└────────────────────────────────────┘
The four sentences I would require every student to know are:
Helm = package manager and templating for Kubernetes.
Argo CD = GitOps Continuous Delivery and reconciliation.
Git = Source of Truth / Desired State.
Kubernetes = Actual State and runtime.
And the production workflow they should remember is:
Change values/templates
↓
git commit
↓
git push
↓
Argo CD
↓
Helm render
↓
Compare desired vs actual
↓
Reconcile
↓
EKS
Once Argo CD owns this application, the normal workflow is not kubectl apply and not manual helm upgrade — it is git push.
Top comments (0)