Final Lab¶
Duration: 45-60 minutes
Overview¶
This comprehensive lab ties together everything from Part 1. You'll deploy a complete 3-tier application (frontend, backend, database) to Kubernetes, converting it from a Docker Compose configuration.
Learning Objectives¶
- Convert a complete Docker Compose application to Kubernetes
- Apply all concepts learned: Pods, Deployments, Services, ConfigMaps, Secrets, Storage
- Implement health checks and resource limits
- Set up networking between components
- Verify end-to-end functionality
- Troubleshoot issues
Application Architecture¶
flowchart TB
Frontend["Frontend<br/>(nginx)<br/>nginx serving static files<br/>Port: 80"]
Backend["Backend<br/>(nginx)<br/>API server<br/>Port: 8080"]
Database["Database<br/>(postgres)<br/>PostgreSQL<br/>Port: 5432"]
Frontend --> Backend
Backend --> Database
Starting Point: Docker Compose¶
Here's the application in Docker Compose:
services:
frontend:
image: nginx:1.25-alpine
ports:
- "8080:80"
volumes:
- ./frontend/html:/usr/share/nginx/html:ro
environment:
- BACKEND_URL=http://backend:8080
depends_on:
- backend
networks:
- app-network
backend:
image: nginx:1.25-alpine
ports:
- "8081:80"
volumes:
- ./backend/config:/etc/nginx/conf.d:ro
environment:
- DB_HOST=database
- DB_PORT=5432
- DB_NAME=appdb
- DB_USER=appuser
depends_on:
- database
networks:
- app-network
database:
image: postgres:16-alpine
environment:
- POSTGRES_DB=appdb
- POSTGRES_USER=appuser
- POSTGRES_PASSWORD=secret123
volumes:
- db-data:/var/lib/postgresql/data
networks:
- app-network
networks:
app-network:
volumes:
db-data:
Your Mission¶
Convert this Docker Compose application to Kubernetes manifests and deploy it.
Requirements¶
1. Namespace¶
- Create namespace:
myapp - All resources should be in this namespace
2. Database (PostgreSQL)¶
- Use a StatefulSet (persistent identity)
- 1 replica
- Persistent storage: 1Gi PVC
- Create Secret for database credentials
- Expose via ClusterIP Service on port 5432
- Resource limits: 256Mi memory, 250m CPU
- Health checks: PostgreSQL readiness
3. Backend API¶
- Use a Deployment
- 2 replicas
- ConfigMap for non-sensitive configuration
- Secret for database password
- Environment variables for database connection
- Expose via ClusterIP Service on port 8080
- Resource limits: 128Mi memory, 100m CPU
- Liveness and readiness probes
4. Frontend¶
- Use a Deployment
- 3 replicas
- ConfigMap for nginx configuration
- Expose via NodePort Service on port 30080
- Resource limits: 64Mi memory, 50m CPU
- Health checks on port 80
5. Best Practices¶
- Use appropriate labels
- Include annotations for documentation
- Use specific image tags
- Run as non-root where possible
- Implement proper health checks
- Set resource requests and limits
Lab Tasks¶
Task 1: Create Namespace¶
Create a namespace for the application with appropriate labels.
Task 2: Create Database Secret¶
Create a Secret containing database credentials.
Task 3: Create Database PVC¶
Create a PersistentVolumeClaim for database storage.
Task 4: Deploy PostgreSQL¶
Create a StatefulSet for PostgreSQL with:
- PVC mounted at
/var/lib/postgresql/data - Environment variables from Secret
- Resource limits
- Readiness probe
Task 5: Create Database Service¶
Expose PostgreSQL with a headless Service.
Task 6: Create Backend ConfigMap¶
Create a ConfigMap with backend configuration.
Task 7: Deploy Backend¶
Create a Deployment for the backend with:
- Environment variables from ConfigMap and Secret
- Resource limits
- Health probes
Task 8: Create Backend Service¶
Expose the backend via ClusterIP Service.
Task 9: Create Frontend ConfigMap¶
Create a ConfigMap for frontend configuration (nginx config).
Task 10: Deploy Frontend¶
Create a Deployment for the frontend with:
- ConfigMap volume mount
- Resource limits
- Health checks
Task 11: Create Frontend Service¶
Expose the frontend via NodePort Service on port 30080.
Task 12: Verify End-to-End¶
Test the complete application:
- Access frontend from browser
- Verify frontend can reach backend
- Verify backend can reach database
- Check logs from all components
Task 13: Scale and Update¶
- Scale backend to 4 replicas
- Update frontend image version
- Verify zero-downtime rolling update
Hints¶
Hint for Task 1: Create Namespace
Hint for Task 2: Create Database Secret
Hint for Task 3: Create Database PVC
Hint for Task 4: Deploy PostgreSQL StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: database
namespace: myapp
labels:
app: database
tier: data
spec:
serviceName: database
replicas: 1
selector:
matchLabels:
app: database
template:
metadata:
labels:
app: database
tier: data
spec:
containers:
- name: postgres
image: postgres:16-alpine
ports:
- containerPort: 5432
name: postgres
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: db-credentials
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: db-credentials
key: database
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "250m"
readinessProbe:
exec:
command:
- sh
- -c
- pg_isready -U $POSTGRES_USER -d $POSTGRES_DB
initialDelaySeconds: 10
periodSeconds: 5
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: postgres-pvc
Hint for Task 5: Create Database Service
Hint for Task 6: Create Backend ConfigMap
Hint for Task 7: Deploy Backend
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
namespace: myapp
labels:
app: backend
tier: api
spec:
replicas: 2
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
tier: api
spec:
containers:
- name: backend
image: nginx:1.25-alpine
ports:
- containerPort: 80
name: http
env:
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: backend-config
key: db.host
- name: DB_PORT
valueFrom:
configMapKeyRef:
name: backend-config
key: db.port
- name: DB_NAME
valueFrom:
secretKeyRef:
name: db-credentials
key: database
- name: DB_USER
valueFrom:
secretKeyRef:
name: db-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
Hint for Task 8: Create Backend Service
Hint for Task 9: Create Frontend ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: frontend-config
namespace: myapp
data:
index.html: |
<!DOCTYPE html>
<html>
<head>
<title>My Application</title>
<style>
body { font-family: Arial, sans-serif; margin: 50px; }
.container { max-width: 800px; margin: 0 auto; }
h1 { color: #333; }
.status { padding: 20px; background: #f0f0f0; border-radius: 5px; }
button { padding: 10px 20px; font-size: 16px; cursor: pointer; }
</style>
</head>
<body>
<div class="container">
<h1>Welcome to My Kubernetes Application!</h1>
<div class="status">
<h2>Application Status</h2>
<p>Frontend: Running on Kubernetes</p>
<p>Backend API: <span id="backend-status">Checking...</span></p>
<button onclick="checkBackend()">Test Backend Connection</button>
</div>
<h2>Architecture</h2>
<ul>
<li>Frontend: nginx (3 replicas)</li>
<li>Backend: nginx (2 replicas)</li>
<li>Database: PostgreSQL (StatefulSet)</li>
</ul>
</div>
<script>
function checkBackend() {
document.getElementById('backend-status').textContent = 'Connected!';
}
checkBackend();
</script>
</body>
</html>
Hint for Task 10: Deploy Frontend
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: myapp
labels:
app: frontend
tier: web
spec:
replicas: 3
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
tier: web
spec:
containers:
- name: frontend
image: nginx:1.25-alpine
ports:
- containerPort: 80
name: http
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
resources:
requests:
memory: "32Mi"
cpu: "25m"
limits:
memory: "64Mi"
cpu: "50m"
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: html
configMap:
name: frontend-config
items:
- key: index.html
path: index.html
Hint for Task 11: Create Frontend Service
Verification Steps¶
# 1. Check all resources are created
kubectl get all -n myapp
# 2. Check PVC is bound
kubectl get pvc -n myapp
# 3. Check pods are running
kubectl get pods -n myapp
# 4. Check services
kubectl get svc -n myapp
# 5. Check database is ready
kubectl exec -n myapp statefulset/database -- psql -U appuser -d appdb -c '\l'
# 6. Check backend can see database
kubectl exec -n myapp deployment/backend -- printenv | grep DB_
# 7. Access frontend
curl http://localhost:30080
# or in browser: http://localhost:30080
# 8. Check logs
kubectl logs -n myapp -l app=frontend --tail=20
kubectl logs -n myapp -l app=backend --tail=20
kubectl logs -n myapp -l app=database --tail=20
Testing and Validation¶
1. Scale Backend¶
2. Rolling Update¶
# Update frontend image
kubectl set image deployment/frontend frontend=nginx:1.25.3-alpine -n myapp
# Watch the rollout
kubectl rollout status deployment/frontend -n myapp
# Check rollout history
kubectl rollout history deployment/frontend -n myapp
3. Test Database Persistence¶
# Write data to database
kubectl exec -n myapp statefulset/database -- psql -U appuser -d appdb -c "CREATE TABLE test (id serial PRIMARY KEY, name VARCHAR(50));"
kubectl exec -n myapp statefulset/database -- psql -U appuser -d appdb -c "INSERT INTO test (name) VALUES ('Kubernetes');"
# Delete and recreate StatefulSet
kubectl delete pod database-0 -n myapp
# Wait for pod to restart
kubectl wait --for=condition=Ready pod/database-0 -n myapp --timeout=60s
# Verify data persisted
kubectl exec -n myapp statefulset/database -- psql -U appuser -d appdb -c "SELECT * FROM test;"
Troubleshooting¶
If something doesn't work:
# Check pod status
kubectl get pods -n myapp
# Describe pod for events
kubectl describe pod <pod-name> -n myapp
# Check logs
kubectl logs <pod-name> -n myapp
# Check service endpoints
kubectl get endpoints -n myapp
# Test DNS resolution
kubectl run test --image=busybox -n myapp --rm -it -- nslookup database.myapp.svc.cluster.local
# Test connectivity
kubectl run test --image=curlimages/curl -n myapp --rm -it -- curl http://backend:8080
Cleanup¶
Success Criteria¶
You've successfully completed this lab when:
- All pods are running (frontend: 3, backend: 2, database: 1)
- Frontend is accessible at
http://localhost:30080 - Backend can connect to database
- Data persists in PostgreSQL after pod restart
- You can scale deployments up and down
- Rolling updates work without downtime
- All health checks are passing
Bonus Challenges¶
- Add resource quotas to the namespace
- Implement HorizontalPodAutoscaler for frontend and backend
- Add Ingress instead of NodePort
- Create a Redis cache layer between backend and database
- Add monitoring with Prometheus annotations
- Implement NetworkPolicies to restrict traffic
Key Takeaways¶
- Docker Compose services map to Kubernetes Deployments + Services
- Volumes in Compose become PVCs in Kubernetes
- Environment variables use ConfigMaps and Secrets
- Services provide DNS-based discovery
- StatefulSets provide persistent identity for databases
- Health checks ensure traffic only goes to ready pods
- Resource limits prevent noisy neighbor problems
- Labels enable powerful selection and organization
Congratulations¶
You've completed Part 1 of the workshop! You've learned:
- Kubernetes architecture and concepts
- Pods, Deployments, Services, and StatefulSets
- ConfigMaps, Secrets, and persistent storage
- Namespaces and resource management
- kubectl and k9s for cluster interaction
- Best practices for manifest organization
Ready for Part 2? Continue to advanced topics including:
- Ingress and load balancing
- Helm package management
- GitOps with Flux
- Monitoring and logging
- Advanced deployment strategies
Next Steps¶
- Review any concepts that were challenging
- Experiment with the deployed application
- Try the bonus challenges
- Take a break before Part 2!