DEV Community

Aisalkyn Aidarova
Aisalkyn Aidarova

Posted on

Production Lab: Restaurant Company — Helm + Argo CD + EKS

GitHub = Source of Truth
Helm = Templates + Values
Argo CD = GitOps CD / Reconciliation
Kubernetes = Runtime
AWS Load Balancer Controller = Creates ALB
ECR = Stores Restaurant image
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Explain:

version
↓
version of Helm chart

appVersion
↓
version of our application
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Students must replace:

YOUR_AWS_ACCOUNT_ID
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Explain:

values.yaml gives us defaults. values-prod.yaml overrides 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
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

Explain:

values-prod.yaml
      ↓
Helm
      ↓
ConfigMap
      ↓
Pod environment
Enter fullscreen mode Exit fullscreen mode

8. ServiceAccount

templates/serviceaccount.yaml

apiVersion: v1
kind: ServiceAccount

metadata:
  name: {{ .Values.serviceAccount.name }}
  namespace: {{ .Values.namespace.name }}
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

This lets you review several concepts simultaneously:

Helm values
Deployment
Rolling Update
Resources
ConfigMap
ServiceAccount
Startup Probe
Readiness Probe
Liveness Probe
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

Architecture:

Service
selector:
app=restaurant
       │
       ▼
Pods
label:
app=restaurant
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

This introduces another Helm feature:

{{ if }}
Enter fullscreen mode Exit fullscreen mode

Explain:

Helm can conditionally generate Kubernetes resources.

If:

pdb:
  enabled: false
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

14. Validate the Helm chart

Before touching Argo CD:

cd helm/restaurant
Enter fullscreen mode Exit fullscreen mode

Run:

helm lint .
Enter fullscreen mode Exit fullscreen mode

Then:

helm template restaurant . \
  -f values-prod.yaml
Enter fullscreen mode Exit fullscreen mode

Tell students:

helm template does 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
Enter fullscreen mode Exit fullscreen mode

Then:

less rendered.yaml
Enter fullscreen mode Exit fullscreen mode

This is a fantastic teaching step:

templates/
      +
values.yaml
      +
values-prod.yaml
      ↓
     HELM
      ↓
rendered.yaml
      ↓
Normal Kubernetes manifests
Enter fullscreen mode Exit fullscreen mode

Delete the generated file afterward if you don't want it committed:

rm rendered.yaml
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then:

helm list -n restaurant-prod
Enter fullscreen mode Exit fullscreen mode

And:

kubectl get all -n restaurant-prod
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Verify:

helm list -n restaurant-prod
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Students replace:

YOUR_GITHUB_USERNAME
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now Git contains:

GitHub

restaurant-company-gitops/
        │
        ├── argocd/
        │
        │    └── application.yaml
        │
        │
        └── helm/
             └── restaurant/
                  ├── Chart.yaml
                  ├── values.yaml
                  ├── values-prod.yaml
                  └── templates/
Enter fullscreen mode Exit fullscreen mode

19. Bootstrap the Argo CD Application

This is the one initial kubectl apply students may see:

kubectl apply -f argocd/application.yaml
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Students do not need to run:

helm upgrade restaurant ...
Enter fullscreen mode Exit fullscreen mode

and do not run:

kubectl apply -f deployment.yaml
Enter fullscreen mode Exit fullscreen mode

for normal GitOps changes.


21. GitOps Lab #1 — Scale 3 → 5

Current production value:

replicaCount: 3
Enter fullscreen mode Exit fullscreen mode

Check:

kubectl get pods -n restaurant-prod
Enter fullscreen mode Exit fullscreen mode

Now change values-prod.yaml:

replicaCount: 5
Enter fullscreen mode Exit fullscreen mode

Then:

git add helm/restaurant/values-prod.yaml

git commit -m "Scale Restaurant Company to 5 replicas"

git push
Enter fullscreen mode Exit fullscreen mode

Do NOT run Helm.

Do NOT run kubectl apply.

Watch:

kubectl get pods -n restaurant-prod -w
Enter fullscreen mode Exit fullscreen mode

With auto-sync enabled:

values-prod.yaml

3 → 5
 │
 ▼
git push
 │
 ▼
GitHub
 │
 ▼
Argo CD
 │
 ▼
Helm render
 │
 ▼
Deployment replicas: 5
 │
 ▼
Kubernetes
 │
 ▼
5 Pods
Enter fullscreen mode Exit fullscreen mode

That connects the entire course.


22. GitOps Lab #2 — Configuration Drift

Git says:

replicaCount: 5
Enter fullscreen mode Exit fullscreen mode

Now intentionally change the live cluster:

kubectl scale deployment restaurant \
  --replicas=1 \
  -n restaurant-prod
Enter fullscreen mode Exit fullscreen mode

Watch:

kubectl get pods -n restaurant-prod -w
Enter fullscreen mode Exit fullscreen mode

Because Argo CD has:

selfHeal: true
Enter fullscreen mode Exit fullscreen mode

the important model is:

Git Desired State
5

      ≠

Live State
1

      ↓

CONFIGURATION DRIFT

      ↓

Argo CD selfHeal

      ↓

Live State reconciled
toward 5
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

to a tag that doesn't exist:

image:
  tag: broken-version
Enter fullscreen mode Exit fullscreen mode

Commit:

git add .

git commit -m "Demo broken Restaurant image"

git push
Enter fullscreen mode Exit fullscreen mode

Argo CD may successfully reconcile the desired configuration while the workload becomes unhealthy.

Check:

kubectl get pods -n restaurant-prod
Enter fullscreen mode Exit fullscreen mode

Then:

kubectl describe pod POD_NAME -n restaurant-prod
Enter fullscreen mode Exit fullscreen mode

Likely:

ErrImagePull
ImagePullBackOff
Enter fullscreen mode Exit fullscreen mode

Teach:

Argo CD:
"I deployed what Git requested."

Kubernetes:
"I cannot run this image."
Enter fullscreen mode Exit fullscreen mode

Therefore:

SYNCED ≠ HEALTHY
Enter fullscreen mode Exit fullscreen mode

Fix it in Git, not directly in the Deployment:

image:
  tag: latest
Enter fullscreen mode Exit fullscreen mode

Then:

git add .
git commit -m "Fix Restaurant image"
git push
Enter fullscreen mode Exit fullscreen mode

Argo CD should reconcile the fix.


24. GitOps Lab #4 — PDB condition

Show them this:

pdb:
  enabled: true
Enter fullscreen mode Exit fullscreen mode

Render:

helm template restaurant helm/restaurant \
  -f helm/restaurant/values-prod.yaml
Enter fullscreen mode Exit fullscreen mode

PDB exists.

Change:

pdb:
  enabled: false
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
       ▲                                    │
       └────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)