Run production apps, not just containers
A 60-minute session covering Deployments, Services, ConfigMaps, Secrets, resource limits, and rolling updates through one end-to-end Kubernetes app lab.
Agenda and learning outcomes
Agenda
By the end, learners should be able to
- Explain why Pods alone are not an operations model
- Choose the right workload controller for stateless vs stateful vs node-level jobs
- Expose applications safely with the right Service type
- Externalize config and protect secrets
- Set resource requests and limits to avoid noisy-neighbor failures
- Use rolling updates and rollout history to ship safely
Pods are runtime units, not deployment strategy
Pod
- Smallest deployable unit in Kubernetes
- One or more tightly coupled containers
- Ephemeral by design: if it dies, Kubernetes can replace it with a new Pod
- IP address changes across restarts
Why not create Pods directly?
- No built-in desired state beyond that one object
- No rollout strategy, rollback history, or replica management
- Operationally fragile for production workloads
- Correct default: use a controller such as Deployment, StatefulSet, or DaemonSet
Rule of thumb
Choose the right workload primitive
Deployment
- Best default for stateless services
- Manages ReplicaSets
- Supports rolling updates and rollback
StatefulSet
- For databases and clustered stateful systems
- Stable pod names such as `db-0`, `db-1`
- Stable persistent volumes
DaemonSet
- Runs one pod on every node
- Ideal for log collectors and monitoring agents
- Examples: Fluent Bit, node exporters
ReplicaSet in context
ReplicaSets are rarely authored directly. A Deployment creates and owns them to guarantee that the requested replica count is continuously enforced.
Services give ephemeral Pods a stable address
Why Services exist
- Pods come and go, so Pod IPs are not a reliable endpoint
- A Service adds a stable virtual IP and DNS name
- Kube-proxy routes traffic to healthy matching Pods
Service types
- ClusterIP: internal-only, default choice for service-to-service traffic
- NodePort: exposes a port on every node, useful for demos and testing
- LoadBalancer: cloud-managed external traffic entrypoint
- Ingress: L7 routing by host/path to multiple Services
Separate application config from the image
ConfigMap
- Non-sensitive configuration
- Feature flags, base URLs, environment labels, config files
- Can be injected as environment variables or mounted as files
Secret
- Sensitive values such as tokens, passwords, certificates
- Base64 encoded in manifest form
- Use RBAC, encryption at rest, and external secret managers in production
Production guidance
Kubernetes Secrets are not magically safe because they are encoded. Treat them as sensitive data, enable encryption at rest, and prefer systems like External Secrets Operator with Azure Key Vault, AWS Secrets Manager, or HashiCorp Vault.
Resource requests and limits protect the cluster
Requests
- Minimum CPU and memory reserved for scheduling
- Used by the scheduler to place Pods
- Without requests, scheduling becomes guesswork
Limits
- Upper bound for CPU and memory consumption
- CPU can be throttled
- Memory overuse can lead to OOMKilled containers
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
Rolling updates reduce deployment risk
What a Deployment gives you
- Gradual replacement of old Pods with new Pods
- Health-driven progression using readiness probes
- Rollout history and rollback if the new version fails
Key commands
kubectl apply -f app.yaml
kubectl rollout status deploy/web
kubectl rollout history deploy/web
kubectl rollout undo deploy/web
Scenario: deploy a full app to Kubernetes
Application topology
Lab flow
- Step 0: verify cluster access and create a dedicated namespace
- Step 1: create ConfigMap and Secret for runtime configuration
- Step 2: deploy the API using a Deployment and ClusterIP Service
- Step 3: deploy the frontend and expose it outside the cluster
- Step 4: test scaling, rolling update, rollback, and cleanup
Step 0: prepare the namespace and check the cluster
kubectl config current-context
kubectl get nodes
kubectl create namespace workloads-lab
kubectl get ns workloads-lab
kubectl config set-context --current \
--namespace=workloads-lab
What each command does
kubectl config current-context: verify the active clusterkubectl get nodes: confirm schedulable worker nodeskubectl create namespace workloads-lab: isolate the lab objectskubectl config set-context --current --namespace=workloads-lab: set the default namespace
Clean namespace scoping keeps the demo safe and makes cleanup simple.
Step 1: create shared config and secret data
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: workloads-lab
data:
APP_MODE: "demo"
API_BASE_URL: "http://api-service:8080"
---
apiVersion: v1
kind: Secret
metadata:
name: app-secret
namespace: workloads-lab
type: Opaque
stringData:
DB_PASSWORD: "ChangeMe123!"
API_TOKEN: "demo-token-123"
Key idea
- Create config first so Pods start with required values
ConfigMapstores non-sensitive runtime settingsSecretstores credentials and tokensstringDatakeeps the manifest easier to author
Run
kubectl apply -f config.yaml
kubectl get configmap,secret
kubectl describe configmap app-config
Step 2: deploy the API and explain the Deployment fields
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: workloads-lab
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: ghcr.io/example/demo-api:v1
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secret
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
---
apiVersion: v1
kind: Service
metadata:
name: api-service
namespace: workloads-lab
spec:
selector:
app: api
ports:
- port: 8080
targetPort: 8080
type: ClusterIP
What to notice
replicas: 2keeps the API available during restartsselectormust match Pod labels exactlyreadinessProbeblocks traffic until the app is readyenvFrominjects ConfigMap and Secret valuesapi-servicebecomes the stable internal endpoint
Run
kubectl apply -f api.yaml
kubectl get deploy,rs,pods,svc
kubectl describe deployment api
kubectl logs deploy/api --tail=20
Step 3: deploy the frontend and expose the app externally
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: workloads-lab
spec:
replicas: 1
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: ghcr.io/example/demo-frontend:v1
env:
- name: API_BASE_URL
value: "http://api-service:8080"
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: frontend-service
namespace: workloads-lab
spec:
selector:
app: frontend
ports:
- port: 80
targetPort: 80
nodePort: 30080
type: NodePort
What to notice
- The frontend is also a stateless Deployment
- It reaches the API through the Service name, not a Pod IP
NodePortis fine for labs; useLoadBalancerorIngressin production- This step connects internal service discovery to external access
Run
kubectl apply -f frontend.yaml
kubectl get pods,svc
kubectl get endpoints
kubectl port-forward svc/frontend-service 8088:80
Step 4: scale, roll forward, rollback, and clean up
kubectl scale deployment/api --replicas=3
kubectl get pods -w
kubectl set image deployment/api \
api=ghcr.io/example/demo-api:v2
kubectl rollout status deployment/api
kubectl rollout history deployment/api
kubectl rollout undo deployment/api
kubectl delete namespace workloads-lab
What to notice
scalechanges desired state at the controller levelset imagetriggers a rolling updaterollout statusis the primary health check during releaserollout historyshows revision historyrollout undoreverts a failed release- Deleting the namespace removes the entire lab cleanly
Check
- Watch Pods and ReplicaSets before and after the update
- Confirm the new image revision is active
- Try rollback if the rollout stalls
Common mistakes in workload design
Mistake 1
Creating naked Pods and calling that a deployment model.
Mistake 2
Hardcoding passwords or URLs into images instead of externalizing them.
Mistake 3
Skipping requests, limits, readiness checks, and rollout observation.