Kubernetes Final Project

Final Project: Deploy a Production-Ready Web App

This capstone project pulls the full Kubernetes training path together. You will deploy a small production-style web application, validate it, break and fix it, and review it against a practical production readiness checklist.

Scenario

You have been asked to deploy a small internal web application to Kubernetes. The application must be reachable through Ingress, configured through Kubernetes objects instead of hardcoded image changes, deployed with basic operational guardrails, and validated before it is considered ready.

This is not meant to be a complex software development project. The application will use nginx and a custom index.html file from a ConfigMap so the focus stays on Kubernetes operations.

Final boss mindset: The goal is not just to paste YAML. The goal is to prove you understand how the objects work together, how to validate them, and how to recover when something breaks.

What you will build

  • A dedicated Namespace
  • A ConfigMap containing custom web content
  • A Secret containing a fake application token
  • A dedicated ServiceAccount
  • A Deployment with two replicas
  • Resource requests and limits
  • Startup, readiness, and liveness probes
  • A ClusterIP Service
  • An Ingress route
  • Basic NetworkPolicy examples
  • A validation and troubleshooting checklist

Architecture

Browser or curl
  sends request to finalboss.localhost
    Ingress controller receives traffic
      Ingress routes to Service
        Service routes to ready Pods
          Pods run nginx
            nginx serves index.html from ConfigMap

Project requirements

  • All application resources must run in a Namespace named final-boss-lab.
  • The Deployment must be named production-webapp.
  • The Deployment must run two replicas.
  • The web content must come from a ConfigMap.
  • The workload must reference a Secret.
  • The workload must use a dedicated ServiceAccount.
  • The container must define CPU and memory requests and limits.
  • The container must define startup, readiness, and liveness probes.
  • The app must be exposed internally with a ClusterIP Service.
  • The app must be reachable through Ingress using finalboss.localhost.
  • You must validate rollout status, endpoints, logs, events, and browser or curl access.
  • You must intentionally break and fix at least one part of the deployment.

Before you start

This project assumes Docker Desktop is running, your kind cluster exists, kubectl is installed, your current context is kind-stealth-k8s-lab, and the NGINX Ingress Controller is installed from the Ingress lesson.

kubectl config current-context
kubectl get nodes
kubectl get pods -n ingress-nginx
kubectl get service -n ingress-nginx

If the ingress controller is not installed, install it:

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.12.1/deploy/static/provider/kind/deploy.yaml

kubectl wait --namespace ingress-nginx --for=condition=ready pod --selector=app.kubernetes.io/component=controller --timeout=180s

Step 1: Create the Namespace

Create final-boss-namespace.yaml:

apiVersion: v1
kind: Namespace
metadata:
  name: final-boss-lab
  labels:
    app.kubernetes.io/part-of: kubernetes-final-project
    environment: training
kubectl apply -f final-boss-namespace.yaml
kubectl get namespace final-boss-lab --show-labels

Step 2: Create the ConfigMap web content

Create final-boss-configmap.yaml:

apiVersion: v1
kind: ConfigMap
metadata:
  name: production-webapp-content
  namespace: final-boss-lab
  labels:
    app: production-webapp
data:
  index.html: |
    <!DOCTYPE html>
    <html>
    <head>
      <title>Kubernetes Final Boss</title>
      <style>
        body {
          font-family: Arial, sans-serif;
          background: #08111f;
          color: #f8fafc;
          margin: 0;
          padding: 4rem;
        }
        .card {
          max-width: 760px;
          border: 1px solid #334155;
          border-radius: 18px;
          padding: 2rem;
          background: #111827;
        }
        h1 { color: #93c5fd; }
        code { color: #86efac; }
      </style>
    </head>
    <body>
      <div class="card">
        <h1>Kubernetes Final Boss Complete</h1>
        <p>This page is being served by nginx inside Kubernetes.</p>
        <p>The web content came from a <code>ConfigMap</code>.</p>
        <p>The workload is running behind a <code>Deployment</code>, <code>Service</code>, and <code>Ingress</code>.</p>
      </div>
    </body>
    </html>
kubectl apply -f final-boss-configmap.yaml
kubectl describe configmap production-webapp-content -n final-boss-lab

Step 3: Create the Secret

Create final-boss-secret.yaml:

apiVersion: v1
kind: Secret
metadata:
  name: production-webapp-secret
  namespace: final-boss-lab
  labels:
    app: production-webapp
type: Opaque
stringData:
  APP_TOKEN: fake-final-boss-token
  APP_MODE: production-style-training
kubectl apply -f final-boss-secret.yaml
kubectl describe secret production-webapp-secret -n final-boss-lab
Security note: This is a fake Secret for training. Do not commit real passwords, tokens, keys, or certificates into Git in plain text.

Step 4: Create the ServiceAccount

Create final-boss-serviceaccount.yaml:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: production-webapp-sa
  namespace: final-boss-lab
  labels:
    app: production-webapp
kubectl apply -f final-boss-serviceaccount.yaml
kubectl get serviceaccounts -n final-boss-lab

Step 5: Create a read-only Role and RoleBinding

This simple app does not need Kubernetes API access, but this step shows how you would grant limited permissions to a workload identity when needed.

Create final-boss-rbac.yaml:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: production-webapp-readonly
  namespace: final-boss-lab
rules:
- apiGroups: [""]
  resources: ["configmaps"]
  verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: production-webapp-readonly-binding
  namespace: final-boss-lab
subjects:
- kind: ServiceAccount
  name: production-webapp-sa
  namespace: final-boss-lab
roleRef:
  kind: Role
  name: production-webapp-readonly
  apiGroup: rbac.authorization.k8s.io
kubectl apply -f final-boss-rbac.yaml

kubectl auth can-i list configmaps --as=system:serviceaccount:final-boss-lab:production-webapp-sa -n final-boss-lab

kubectl auth can-i delete pods --as=system:serviceaccount:final-boss-lab:production-webapp-sa -n final-boss-lab

kubectl auth can-i list secrets --as=system:serviceaccount:final-boss-lab:production-webapp-sa -n final-boss-lab

Step 6: Create the Deployment

Create final-boss-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: production-webapp
  namespace: final-boss-lab
  labels:
    app: production-webapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: production-webapp
  template:
    metadata:
      labels:
        app: production-webapp
    spec:
      serviceAccountName: production-webapp-sa
      containers:
      - name: nginx
        image: nginx:1.27
        ports:
        - containerPort: 80
        env:
        - name: APP_MODE
          valueFrom:
            secretKeyRef:
              name: production-webapp-secret
              key: APP_MODE
        - name: APP_TOKEN
          valueFrom:
            secretKeyRef:
              name: production-webapp-secret
              key: APP_TOKEN
        volumeMounts:
        - name: web-content
          mountPath: /usr/share/nginx/html
          readOnly: true
        startupProbe:
          httpGet:
            path: /
            port: 80
          failureThreshold: 30
          periodSeconds: 2
        readinessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 3
          periodSeconds: 5
          failureThreshold: 3
        livenessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 10
          periodSeconds: 10
          failureThreshold: 3
        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
          limits:
            cpu: "500m"
            memory: "256Mi"
      volumes:
      - name: web-content
        configMap:
          name: production-webapp-content
kubectl apply -f final-boss-deployment.yaml
kubectl rollout status deployment/production-webapp -n final-boss-lab
kubectl get deployments -n final-boss-lab
kubectl get pods -n final-boss-lab -o wide

Step 7: Inspect the Deployment and Pods

kubectl describe deployment production-webapp -n final-boss-lab
kubectl get pods -n final-boss-lab
kubectl describe pod <pod-name> -n final-boss-lab

Confirm the Pod uses the dedicated ServiceAccount, mounts the ConfigMap, references the Secret, defines requests and limits, and has startup, readiness, and liveness probes.

Step 8: Create the Service

Create final-boss-service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: production-webapp
  namespace: final-boss-lab
  labels:
    app: production-webapp
spec:
  type: ClusterIP
  selector:
    app: production-webapp
  ports:
  - name: http
    port: 80
    targetPort: 80
    protocol: TCP
kubectl apply -f final-boss-service.yaml
kubectl get service production-webapp -n final-boss-lab
kubectl describe service production-webapp -n final-boss-lab
kubectl get endpoints production-webapp -n final-boss-lab

Step 9: Test the Service with port-forward

kubectl port-forward -n final-boss-lab service/production-webapp 8080:80

Open:

http://localhost:8080

You should see the custom Kubernetes Final Boss page. Press Ctrl+C to stop port-forwarding.

Step 10: Create the Ingress

Create final-boss-ingress.yaml:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: production-webapp
  namespace: final-boss-lab
  labels:
    app: production-webapp
spec:
  ingressClassName: nginx
  rules:
  - host: finalboss.localhost
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: production-webapp
            port:
              number: 80
kubectl apply -f final-boss-ingress.yaml
kubectl get ingress -n final-boss-lab
kubectl describe ingress production-webapp -n final-boss-lab

Step 11: Test the Ingress with curl

In one PowerShell window, port-forward to the ingress controller:

kubectl port-forward -n ingress-nginx service/ingress-nginx-controller 8080:80

In a second PowerShell window, test with the Host header:

curl.exe -H "Host: finalboss.localhost" http://localhost:8080

Step 12: Test the Ingress in a browser

Open Notepad as Administrator and edit:

C:\Windows\System32\drivers\etc\hosts

Add:

127.0.0.1 finalboss.localhost

With the ingress controller port-forward still running, open:

http://finalboss.localhost:8080

When testing is complete, remove the temporary hosts file entry.

Step 13: Add NetworkPolicy examples

This project includes NetworkPolicy manifests so you practice the production pattern. Enforcement depends on whether your cluster networking plugin supports NetworkPolicy.

Create final-boss-networkpolicy.yaml:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: final-boss-lab
spec:
  podSelector: {}
  policyTypes:
  - Ingress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-webapp-http
  namespace: final-boss-lab
spec:
  podSelector:
    matchLabels:
      app: production-webapp
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: ingress-nginx
    ports:
    - protocol: TCP
      port: 80
kubectl apply -f final-boss-networkpolicy.yaml
kubectl get networkpolicies -n final-boss-lab
kubectl describe networkpolicy allow-webapp-http -n final-boss-lab

Step 14: Validate the full deployment

kubectl get all -n final-boss-lab
kubectl get configmaps -n final-boss-lab
kubectl get secrets -n final-boss-lab
kubectl get serviceaccounts -n final-boss-lab
kubectl get role,rolebinding -n final-boss-lab
kubectl get ingress -n final-boss-lab
kubectl get networkpolicies -n final-boss-lab
kubectl get endpoints production-webapp -n final-boss-lab
kubectl rollout status deployment/production-webapp -n final-boss-lab
kubectl get events -n final-boss-lab --sort-by=.metadata.creationTimestamp

Step 15: Break the Service selector on purpose

Edit final-boss-service.yaml and change:

selector:
  app: production-webapp

to:

selector:
  app: does-not-match

Apply it and inspect endpoints:

kubectl apply -f final-boss-service.yaml
kubectl get endpoints production-webapp -n final-boss-lab
kubectl describe service production-webapp -n final-boss-lab

The endpoint list should be empty because the Service selector no longer matches the Pod labels.

Step 16: Fix the Service selector

Change the selector back to app: production-webapp, then run:

kubectl apply -f final-boss-service.yaml
kubectl get endpoints production-webapp -n final-boss-lab

Step 17: Break the readiness probe on purpose

Edit final-boss-deployment.yaml and change the readiness probe path to /not-ready, then apply it:

kubectl apply -f final-boss-deployment.yaml
kubectl rollout status deployment/production-webapp -n final-boss-lab

Troubleshoot readiness failures:

kubectl get pods -n final-boss-lab
kubectl describe pod <pod-name> -n final-boss-lab
kubectl get endpoints production-webapp -n final-boss-lab
kubectl get events -n final-boss-lab --sort-by=.metadata.creationTimestamp

Step 18: Fix the readiness probe

Change the readiness probe path back to /, then run:

kubectl apply -f final-boss-deployment.yaml
kubectl rollout status deployment/production-webapp -n final-boss-lab
kubectl get pods -n final-boss-lab
kubectl get endpoints production-webapp -n final-boss-lab

Step 19: Simulate a bad image rollout

kubectl set image deployment/production-webapp nginx=nginx:not-a-real-final-boss-tag -n final-boss-lab
kubectl rollout status deployment/production-webapp -n final-boss-lab
kubectl get pods -n final-boss-lab
kubectl describe pod <bad-pod-name> -n final-boss-lab

Step 20: Recover with rollout undo

kubectl rollout undo deployment/production-webapp -n final-boss-lab
kubectl rollout status deployment/production-webapp -n final-boss-lab
kubectl get pods -n final-boss-lab
kubectl get endpoints production-webapp -n final-boss-lab

Step 21: Run the production readiness review

Workload design

  • Is the app managed by a Deployment?
  • Are there at least two replicas?
  • Are labels and selectors consistent?
  • Is the image pinned to a specific version?

Configuration

  • Does web content come from a ConfigMap?
  • Is sensitive-style configuration stored in a Secret?
  • Are real secrets excluded from Git?

Identity and access

  • Does the workload use a dedicated ServiceAccount?
  • Are RBAC permissions limited?
  • Can you prove permissions with kubectl auth can-i?

Resources and health

  • Are CPU and memory requests set?
  • Are CPU and memory limits set?
  • Are startup, readiness, and liveness probes configured?
  • Do the Pods become Ready?

Networking

  • Does the Service have endpoints?
  • Does port-forward to the Service work?
  • Does Ingress route to the Service?
  • Does curl or browser access work with the expected hostname?

Security and policy

  • Are NetworkPolicy manifests present?
  • Do you understand whether your local CNI enforces them?
  • Are Secrets protected from casual display in logs?

Operations

  • Can you check rollout status?
  • Can you read logs?
  • Can you read events?
  • Can you troubleshoot missing endpoints?
  • Can you recover from a bad image rollout?

Final scoring rubric

  • Pass: The app deploys, Service endpoints exist, and Service port-forward works.
  • Strong pass: Ingress works, probes are healthy, resources are set, and rollback works.
  • Final boss complete: You can explain every object, intentionally break and fix Service selectors and readiness, recover from a bad rollout, and walk through the production readiness checklist without guessing.

Step 22: Clean up

Stop any active port-forward sessions with Ctrl+C. Then delete the Namespace and everything inside it:

kubectl delete namespace final-boss-lab

Confirm cleanup:

kubectl get namespaces
kubectl get pods -A

Remove the temporary hosts file entry if you added one:

127.0.0.1 finalboss.localhost

What you just proved

  • You can create organized resources in a Namespace.
  • You can deploy a replicated workload.
  • You can use ConfigMaps and Secrets.
  • You can use a dedicated ServiceAccount and limited RBAC.
  • You can set resource requests and limits.
  • You can configure health probes.
  • You can expose the app with a Service and route traffic through Ingress.
  • You can apply NetworkPolicy patterns.
  • You can validate rollout, logs, events, and endpoints.
  • You can troubleshoot broken selectors, probes, and rollouts.
  • You can review production readiness before calling the workload done.
Bottom line: Kubernetes is not just a collection of objects. It is an operating model. This final project proves you can connect workload design, configuration, networking, security, health, troubleshooting, and production readiness into one complete deployment.

Final reflection questions

  1. Which object actually ran the application containers?
  2. Which object kept two replicas running?
  3. Which object gave the Pods stable internal access?
  4. Which object routed HTTP traffic into the cluster?
  5. Which object provided the custom web page?
  6. Which object stored fake sensitive configuration?
  7. Which identity did the workload run as?
  8. What happened when the Service selector was wrong?
  9. What happened when the readiness probe was wrong?
  10. How did you recover from a bad image rollout?
  11. What would you change before deploying a real internet-facing app?
  12. Which part of the production readiness checklist would be hardest in your own environment?
Answer key
  1. The Pods ran the application containers.
  2. The Deployment, through a ReplicaSet, kept two replicas running.
  3. The Service gave the Pods stable internal access.
  4. The Ingress routed HTTP traffic into the cluster through the ingress controller.
  5. The ConfigMap provided the custom web page.
  6. The Secret stored fake sensitive configuration.
  7. The workload used the production-webapp-sa ServiceAccount.
  8. The Service had no endpoints because it did not match the Pod labels.
  9. The Pods could be Running but not Ready, and Service endpoints could disappear or rollout could stall.
  10. By using kubectl rollout undo deployment/production-webapp -n final-boss-lab.
  11. Examples: real TLS, real DNS, external secret manager, image scanning, monitoring, alerting, backup/restore if state exists, stronger policies, and production ingress/load balancer configuration.
  12. Answers will vary. The important part is identifying the operational gap honestly.
← Previous: Production Readiness Checklist Series index Training path complete →