Core Kubernetes Workloads, ConfigMaps & Secrets
Get the application running on Kubernetes with its configuration and secrets outside the image.
- Time
- 31 min
- Level
- Intermediate
- Objectives
- 4 objectives
- Cost
- Low cost
Where this fits in the platform
Already built
This lab adds
- Configuration and secrets kept out of the image
Before you start
Cost — Low cost
Depends on an existing cluster. The Kubernetes objects here cost nothing; the EKS cluster underneath them is $0.10/hour. If you created it in the EKS lab, destroy it when you finish this one.
Nothing to pay in the browser. Open the terminal runs this against a simulated cloud — the same API calls and the same commands, with no account and no bill. The figure above applies only if you build it in your own.
The scenario#
The image has the database hostname compiled in, so staging and production are different builds of the same commit. The password is in the same file. There are no probes, so a hung process keeps receiving traffic, and no limits, so one leaking container takes the node with it.
Hands-on environment
Run this lab in a real terminal, free and in your browser. The environment is temporary and yours alone — break it as much as you like.
Open the terminalOpens in Killercoda, in a new tab — keep this page open for the steps.
Run it on your own machine
Run this lab on your own machine. One command starts the environment, with everything the lab needs already installed:
You will need:
- docker
- kubectl
- kind
git clone https://github.com/EgyKode/EgyKode-lab.git
cd EgyKode-lab
./egykode start k8s
./egykode shellYou need Docker and Git installed. Everything else runs inside the environment. The first start downloads it and takes a few minutes; later starts are seconds.
Not sure what you already have? Run: npm run doctor — it checks and changes nothing.
Run it on AWS
This lab builds real cloud infrastructure, so it needs your own AWS account. Follow the cost and cleanup notes above — the resources are yours, and so is the bill.
Anything you tick here is your own record. EgyKode cannot see inside that terminal, so the success criteria stay self-assessed even when the environment checks your work for you.
A namespace, and why not default
Step 1 of 4
What you are building#
Namespace: platform
|
+-- ConfigMap non-secret config -> env vars
+-- Secret credentials -> env vars, RBAC-scoped
+-- Deployment 2 replicas, probes, limits
+-- Service ClusterIP, stable nameBuild it#
What you are proving: You can isolate an application in its own namespace, and say what using default costs you
This step settles no success criterion on its own.
apiVersion: v1
kind: Namespace
metadata:
name: platform
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/warn: restrictedA namespace is the unit RBAC, quotas and NetworkPolicies attach to. Everything
in default shares one blast radius and cannot be granted separately, which is
why the first real thing you create is a namespace.
What you are proving: You can keep environment-specific values out of the image, and know what makes a change actually reach the Pods
Marking this settles success criteria 1 and 4.
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: platform
data:
LOG_LEVEL: "info"
DB_HOST: "postgres.platform.svc.cluster.local"
FEATURE_CHECKOUT: "true"
---
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
namespace: platform
type: Opaque
stringData: # plain text here; Kubernetes encodes it
DB_PASSWORD: "change-me"
API_KEY: "change-me"Use stringData when writing YAML by hand. data requires base64 and creates
a step where people paste the wrong thing — and base64 is encoding, not
encryption either way. Anyone with get secrets reads it:
kubectl get secret app-secrets -n platform -o jsonpath='{.data.DB_PASSWORD}' | base64 -dWhat a Secret buys you over a ConfigMap is that RBAC can withhold it
separately, kubectl describe does not print it, and the kubelet mounts it as
tmpfs.
What you are proving: You can set requests and limits, state the QoS class they produce, and use a readiness probe to take a Pod out of rotation without restarting it
Marking this settles success criteria 2 and 3.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: platform
spec:
replicas: 2
selector:
matchLabels: { app: api }
template:
metadata:
labels: { app: api }
spec:
containers:
- name: api
image: registry.example.com/api:1.4.2 # never :latest
ports:
- containerPort: 8000
envFrom:
- configMapRef: { name: app-config }
- secretRef: { name: app-secrets }
resources:
requests: { cpu: "100m", memory: "128Mi" }
limits: { cpu: "500m", memory: "256Mi" }
startupProbe: # slow starts do not count as failures
httpGet: { path: /healthz, port: 8000 }
failureThreshold: 30
periodSeconds: 2
readinessProbe: # may I receive traffic?
httpGet: { path: /healthz, port: 8000 }
periodSeconds: 5
livenessProbe: # should I be killed?
httpGet: { path: /healthz, port: 8000 }
periodSeconds: 10
failureThreshold: 3
lifecycle:
preStop:
exec: { command: ["sh", "-c", "sleep 5"] }
terminationGracePeriodSeconds: 30The three probes answer three different questions, and conflating them causes outages:
| Probe | Asks | On failure |
|---|---|---|
startupProbe | Has it finished booting? | Holds the other two off |
readinessProbe | Can it serve now? | Removed from the Service. Not restarted. |
livenessProbe | Is it wedged? | Container killed |
A liveness probe pointed at an endpoint that touches the database turns a database blip into every Pod restarting at once. Liveness should test the process, readiness should test whether it can do useful work.
Requests versus limits, and QoS:
requestsis what the scheduler reserves — too high and Pods stayPending.limitsis the ceiling. Exceeding memory is an immediate OOM kill; exceeding CPU is throttling, not a kill.- Equal requests and limits gives
GuaranteedQoS, which is evicted last. Requests only givesBurstable. Neither givesBestEffort, evicted first.
kubectl get pod -n platform -o jsonpath='{.items[*].status.qosClass}'What you are proving: You can put a stable name in front of a set of Pods that come and go
This step settles no success criterion on its own.
apiVersion: v1
kind: Service
metadata:
name: api
namespace: platform
spec:
selector: { app: api }
ports:
- port: 80
targetPort: 8000Verify it worked#
# The config is injected, not baked in
kubectl exec -n platform deploy/api -- env | grep -E 'LOG_LEVEL|DB_HOST'
# Endpoints exist — this is what a Service actually resolves to
kubectl get endpointslices -n platform -l kubernetes.io/service-name=api
# Readiness removes a Pod without restarting it
kubectl exec -n platform deploy/api -- touch /tmp/unhealthy # if your app honours it
kubectl get pods -n platform # READY 0/1, RESTARTS unchanged
kubectl get endpointslices -n platform -l kubernetes.io/service-name=api # one fewer address
# QoS is what you intended
kubectl get pods -n platform -o custom-columns=NAME:.metadata.name,QOS:.status.qosClass
# Reachable by name from inside the cluster
kubectl run curl --rm -it --image=curlimages/curl --restart=Never -n platform -- \
curl -s -o /dev/null -w '%{http_code}\n' http://api/The readiness test is the one worth doing carefully: seeing READY 0/1 with
RESTARTS 0 and one fewer endpoint is the difference between understanding
readiness and having copied it.
CreateContainerConfigError
A ConfigMap or Secret named in envFrom does not exist in that namespace.
kubectl describe pod names it.
CrashLoopBackOff immediately after a config change
The app read a value it cannot parse. kubectl logs --previous reads the dead
container.
Pods restart under load with no error in the logs
OOM killed. kubectl get pod -o jsonpath='{.status.containerStatuses[0].lastState}'
shows reason: OOMKilled. Raise the memory limit or fix the leak.
The Service returns nothing but Pods are healthy
The selector does not match the Pod labels, or readiness is failing. The EndpointSlice tells you which in one command.
Everything is slow but nothing is failing
CPU throttling at the limit. Exceeding a CPU limit throttles rather than kills, so it presents as latency, not as an error.
A config change did nothing
Environment variables are read once at process start. Restart the Pods, or mount the ConfigMap as a file and watch it.
Clean up#
Destructive — This removes real resources. Check which environment you are in first.
kubectl delete namespace platformCost of this lab: Free on kind or minikube. On EKS you are paying for the cluster either way; these objects add nothing.
Success criteria
0 of 4
The concept behind it
Next up
Lab 35 of 59 on the project path