Difficulty: Intermediate | Time: 60 minutes | Focus: Deployments, Services, security contexts, scaling
Objectives
By the end of this lab, you will be able to create a local Kubernetes cluster using kind, deploy a CleanStart-based application to Kubernetes, create Services and Ingress for network access, apply security contexts including non-root execution, read-only filesystems, and dropped capabilities, scale deployments up and down, debug deployments with logs and port-forward, and test the full application deployment.
Prerequisites
Required: Docker 20.10 or newer, kind 0.20 or newer, kubectl 1.27 or newer, curl for testing, 4 GB free RAM for the kind cluster, and either Linux or macOS.
Installation:
Install kind:
# macOSbrew install kind # Linuxcurl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64chmod +x ./kindsudo mv ./kind /usr/local/bin/kind # Verifykind --versionInstall kubectl:
# macOSbrew install kubectl # Linuxcurl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl # Verifykubectl version --clientStep 1: Create Working Directory
mkdir -p ~/labs/lab-05-kubernetes-deploymentcd ~/labs/lab-05-kubernetes-deploymentStep 2: Create the Application
Create app.py:
cat > app.py << 'EOF'from http.server import HTTPServer, BaseHTTPRequestHandlerfrom datetime import datetimeimport osimport jsonimport sys class RequestHandler(BaseHTTPRequestHandler): def do_GET(self): if self.path == '/': self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() response = { "service": "lab-05-app", "timestamp": datetime.now().isoformat(), "version": "1.0.0", "hostname": os.getenv('HOSTNAME', 'unknown'), "pod_name": os.getenv('POD_NAME', 'unknown'), "namespace": os.getenv('NAMESPACE', 'default') } self.wfile.write(json.dumps(response).encode()) elif self.path == '/health': self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() response = {"status": "ok", "uptime_seconds": 1234} self.wfile.write(json.dumps(response).encode()) elif self.path == '/ready': self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() response = {"ready": True} self.wfile.write(json.dumps(response).encode()) else: self.send_response(404) self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(json.dumps({"error": "Not found"}).encode()) def log_message(self, format, *args): sys.stdout.write("[%s] %s\n" % (datetime.now().isoformat(), format%args)) if __name__ == '__main__': port = int(os.getenv('PORT', '8000')) print(f"Starting server on port {port}") server = HTTPServer(('0.0.0.0', port), RequestHandler) server.serve_forever()EOFCreate Dockerfile:
cat > Dockerfile << 'EOF'FROM registry.cleanstart.com/cleanstart/python:3.12 WORKDIR /app COPY app.py . EXPOSE 8000 ENV PORT=8000 HEALTHCHECK --interval=10s --timeout=5s --start-period=5s --retries=3 \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1 CMD ["python", "app.py"]EOFStep 3: Build and Push the Image to Kind
Build the Docker image:
docker build -t lab-05-app:latest .Expected output:
[+] Building 5.3s (6/6) FINISHEDCreate a kind cluster:
cat > kind-config.yaml << 'EOF'kind: ClusterapiVersion: kind.x-k8s.io/v1alpha4name: lab-05nodes:- role: control-plane- role: workerEOFCreate the cluster:
kind create cluster --config kind-config.yamlExpected output:
Creating cluster "lab-05" ... ✓ Ensuring node image (kindest/node:v1.27.0) 🖼 ✓ Preparing nodes 📦 ✓ Writing configuration 📜 ✓ Starting control-plane 🎛 ✓ Ready! 📦 Set kubectl context to "kind-lab-05"Verify the cluster:
kubectl cluster-infoExpected output:
Kubernetes control plane is running at https://127.0.0.1:xxxxxCoreDNS is running at https://127.0.0.1:xxxxx/api/v1/namespaces/kube-system/services/coredns...Load the image into the kind cluster:
kind load docker-image lab-05-app:latest --name lab-05Expected output:
Image: "lab-05-app:latest" loaded into kind cluster "lab-05"Step 4: Create Namespace
Create a namespace for the lab:
kubectl create namespace lab-05Expected output:
namespace/lab-05 createdSet the namespace as default for this session:
kubectl config set-context --current --namespace=lab-05Step 5: Create a Deployment
Create deployment.yaml:
cat > deployment.yaml << 'EOF'apiVersion: apps/v1kind: Deploymentmetadata: name: lab-05-app namespace: lab-05 labels: app: lab-05-appspec: replicas: 2 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 selector: matchLabels: app: lab-05-app template: metadata: labels: app: lab-05-app spec: securityContext: runAsNonRoot: true runAsUser: 65532 fsGroup: 65532 containers: - name: app image: lab-05-app:latest imagePullPolicy: Never ports: - name: http containerPort: 8000 protocol: TCP env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true runAsNonRoot: true runAsUser: 65532 capabilities: drop: - ALL livenessProbe: httpGet: path: /health port: http initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: path: /ready port: http initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 2 volumeMounts: - name: tmp mountPath: /tmp resources: requests: memory: "64Mi" cpu: "100m" limits: memory: "256Mi" cpu: "500m" volumes: - name: tmp emptyDir: medium: Memory sizeLimit: 100MiEOFApply the deployment:
kubectl apply -f deployment.yamlExpected output:
deployment.apps/lab-05-app createdWatch the deployment roll out:
kubectl rollout status deployment/lab-05-appExpected output (wait ~20 seconds):
Waiting for rollout to finish: 1 of 2 updated replicas are available...Waiting for rollout to finish: 1 of 2 updated replicas are available...deployment "lab-05-app" successfully rolled outVerify the pods:
kubectl get podsExpected output:
NAME READY STATUS RESTARTS AGElab-05-app-abcd1234-5xyz9 1/1 Running 0 30slab-05-app-abcd1234-7abc2 1/1 Running 0 30sStep 6: Create a Service
Create service.yaml:
cat > service.yaml << 'EOF'apiVersion: v1kind: Servicemetadata: name: lab-05-app namespace: lab-05 labels: app: lab-05-appspec: type: ClusterIP selector: app: lab-05-app ports: - name: http port: 8000 targetPort: http protocol: TCPEOFApply the service:
kubectl apply -f service.yamlExpected output:
service/lab-05-app createdVerify the service:
kubectl get serviceExpected output:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGElab-05-app ClusterIP 10.96.123.45 <none> 8000/TCP 10sStep 7: Test with Port-Forward
Forward traffic from localhost to the service:
kubectl port-forward service/lab-05-app 8000:8000 &Wait a moment, then test the endpoint:
curl http://localhost:8000/Expected output:
{ "service": "lab-05-app", "timestamp": "2024-03-22T15:30:45.123456", "version": "1.0.0", "hostname": "lab-05-app-abcd1234-5xyz9", "pod_name": "lab-05-app-abcd1234-5xyz9", "namespace": "lab-05"}Test the health endpoint:
curl http://localhost:8000/healthExpected output:
{"status": "ok", "uptime_seconds": 1234}Stop port-forward:
kill %1Step 8: View Logs
View logs from a specific pod:
POD=$(kubectl get pods -o jsonpath='{.items[0].metadata.name}')kubectl logs $PODExpected output:
Starting server on port 8000[2024-03-22T15:30:45...] 127.0.0.1 "GET / HTTP/1.1" 200 -[2024-03-22T15:30:46...] 127.0.0.1 "GET /health HTTP/1.1" 200 -View logs from all pods:
kubectl logs -l app=lab-05-app --all-containers=trueStep 9: Scale the Deployment
Scale up to 5 replicas:
kubectl scale deployment lab-05-app --replicas=5Expected output:
deployment.apps/lab-05-app scaledWatch the scaling:
kubectl get pods --watchExpected output (shows 5 pods running):
NAME READY STATUS RESTARTS AGElab-05-app-abcd1234-5xyz9 1/1 Running 0 2m30slab-05-app-abcd1234-7abc2 1/1 Running 0 2m30slab-05-app-abcd1234-8def3 1/1 Running 0 10slab-05-app-abcd1234-9ghi4 1/1 Running 0 10slab-05-app-abcd1234-1jkl5 1/1 Running 0 5sScale back down to 2:
kubectl scale deployment lab-05-app --replicas=2Exit watch mode: press Ctrl+C
Step 10: Create an Ingress (Advanced)
Create ingress.yaml:
cat > ingress.yaml << 'EOF'apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: lab-05-app namespace: lab-05spec: rules: - host: "localhost" http: paths: - path: / pathType: Prefix backend: service: name: lab-05-app port: number: 8000EOFApply the ingress:
kubectl apply -f ingress.yamlExpected output:
ingress.networking.k8s.io/lab-05-app createdNote: Ingress may not be immediately accessible in kind without additional configuration. Port-forward is the recommended method for testing.
Step 11: Test Deployment Rollout
Edit the deployment to trigger a rollout:
# Update the image tag (this will trigger a rollout)kubectl set image deployment/lab-05-app \ app=lab-05-app:latestWatch the rollout:
kubectl rollout status deployment/lab-05-appExpected output:
Waiting for rollout to finish: 1 of 2 updated replicas are available...deployment "lab-05-app" successfully rolled outView rollout history:
kubectl rollout history deployment/lab-05-appExpected output:
deployment.apps/lab-05-appREVISION CHANGE-CAUSE1 kubectl apply --filename=deployment.yaml2 <none>Step 12: Inspect Security Context
Verify security context is applied:
kubectl get pod -o jsonpath='{.items[0].spec.securityContext}'Expected output:
{ "fsGroup": 65532, "runAsNonRoot": true, "runAsUser": 65532}Verify container security context:
kubectl get pod -o jsonpath='{.items[0].spec.containers[0].securityContext}'Expected output:
{ "allowPrivilegeEscalation": false, "capabilities": { "drop": ["ALL"] }, "readOnlyRootFilesystem": true, "runAsNonRoot": true, "runAsUser": 65532}Verification Checklist
Confirm all of the following: Directory ~/labs/lab-05-kubernetes-deployment created, app.py created with HTTP endpoints, Dockerfile created using CleanStart base, Docker image built successfully, kind cluster created with kind create cluster, image loaded into kind cluster, namespace lab-05 created, deployment.yaml applied successfully, 2 pods running or more after scaling, kubectl rollout status shows successful rollout, curl http://localhost:8000/ works via port-forward, pod logs show HTTP requests, deployment scaled to 5 replicas and back to 2, security context shows runAsUser: 65532, and security context shows readOnlyRootFilesystem: true.
If all items are checked, you've successfully completed Lab 05.
What You Learned
You learned how to create kind clusters as local Kubernetes environments for testing, create and manage Kubernetes Deployments as resources for managing pod replicas, and understand how Replica Sets handle automatic pod management and scaling. You learned about Services as the Kubernetes networking abstraction layer, liveness and readiness probes as health checks for container orchestration, and security contexts as pod and container-level security policies. You discovered rolling updates as a zero-downtime deployment strategy, port-forwarding as a method for temporary network access to services, and scaling as horizontal pod autoscaling through manual operations in this lab. Finally, you learned about rollout history as a mechanism for tracking deployment changes.
Cleanup
Delete the namespace (includes all resources):
kubectl delete namespace lab-05Delete the kind cluster:
kind delete cluster --name lab-05Remove lab directory:
rm -rf ~/labs/lab-05-kubernetes-deploymentNext Lab
Proceed to Lab 06: CI/CD Pipeline to automate image building and testing with GitHub Actions.
Real-World Best Practices
Always use security contexts in production deployments, set resource requests and limits, implement liveness and readiness probes, and use rolling updates for zero-downtime deployments. Additionally, monitor logs and metrics, use Horizontal Pod Autoscaling (HPA) for dynamic scaling, implement network policies to restrict traffic, and use Kubernetes namespaces for multi-tenancy.
Estimated Time: 60 minutes | Hands-on: ~50 minutes | Reading: ~10 minutes
