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
- Bootstrap namespace and shared config
- Deploy API and internal Service
- Deploy frontend and external Service
- Test rollout, rollback, and cleanup
Runnable manifests are in day25/k8s-lab/.
One namespace, two Deployments, two Services
Manifest set
Apply order
kubectl apply -f k8s-lab/namespace.yaml
kubectl apply -f k8s-lab/config.yaml
kubectl apply -f k8s-lab/secret.yaml
kubectl apply -f k8s-lab/api-deployment.yaml
kubectl apply -f k8s-lab/api-service.yaml
kubectl apply -f k8s-lab/frontend-deployment.yaml
kubectl apply -f k8s-lab/frontend-service.yaml
Step 0: prepare the namespace and check the cluster
# namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: workloads-lab
What each command does
- Verify cluster context and node health first
- Create an isolated namespace for the lab
- Set the default namespace once before applying the rest
Everything after this stays isolated in workloads-lab.
Step 1: create shared config and secret data
# config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: workloads-lab
data:
API_BASE_URL: "http://api-service:9090"
FRONTEND_MESSAGE: "Frontend calling internal API"
---
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: app-secret
namespace: workloads-lab
type: Opaque
stringData:
DEMO_API_KEY: "lab-token-123"
Key idea
- Create shared values before workloads
- ConfigMap carries URL and UI message
- Secret carries the demo API token
Run
kubectl apply -f k8s-lab/config.yaml
kubectl apply -f k8s-lab/secret.yaml
kubectl get configmap,secret
Step 2: deploy the API and explain the Deployment fields
# api-deployment.yaml
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: nicholasjackson/fake-service:v0.26.1
env:
- name: NAME
value: "api"
- name: MESSAGE
value: "Kubernetes API v1"
- name: LISTEN_ADDR
value: "0.0.0.0:9090"
- name: DEMO_API_KEY
valueFrom:
secretKeyRef:
name: app-secret
key: DEMO_API_KEY
ports:
- containerPort: 9090
readinessProbe:
httpGet:
path: /
port: 9090
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
---
# api-service.yaml
apiVersion: v1
kind: Service
metadata:
name: api-service
namespace: workloads-lab
spec:
selector:
app: api
ports:
- port: 9090
targetPort: 9090
type: ClusterIP
What to notice
- Deployment owns availability
- Probe gates traffic
- Secret is injected explicitly
api-service:9090is the stable endpoint
Run
kubectl apply -f k8s-lab/api-deployment.yaml
kubectl apply -f k8s-lab/api-service.yaml
kubectl rollout status deploy/api
Step 3: deploy the frontend and expose the app externally
# frontend-deployment.yaml
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: nicholasjackson/fake-service:v0.26.2
env:
- name: NAME
value: "frontend"
- name: MESSAGE
valueFrom:
configMapKeyRef:
name: app-config
key: FRONTEND_MESSAGE
- name: LISTEN_ADDR
value: "0.0.0.0:9090"
- name: UPSTREAM_URIS
valueFrom:
configMapKeyRef:
name: app-config
key: API_BASE_URL
ports:
- containerPort: 9090
---
# frontend-service.yaml
apiVersion: v1
kind: Service
metadata:
name: frontend-service
namespace: workloads-lab
spec:
selector:
app: frontend
ports:
- port: 9090
targetPort: 9090
nodePort: 30090
type: NodePort
What to notice
- Frontend reads message and API URL from ConfigMap
- Browser reaches the frontend over NodePort
- Frontend reaches API over ClusterIP
Run
kubectl apply -f k8s-lab/frontend-deployment.yaml
kubectl apply -f k8s-lab/frontend-service.yaml
kubectl port-forward svc/frontend-service 8088:9090
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=nicholasjackson/fake-service:v0.26.2
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 stateset imagestarts the rolloutrollout undoreverses a bad release- Namespace delete removes the lab cleanly
Check
- Watch the new ReplicaSet appear
- Confirm
v0.26.2is live - Run rollback once
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.