Skip to main content
mastering ckad certified kubernetes application developer

Solutions: Tasks 11–20

20 min read Chapter 87 of 87
Summary

Full solutions for Tasks 11–20 of Mock Exam...

Full solutions for Tasks 11–20 of Mock Exam 2: init container data handoff, DaemonSet with control-plane tolerations, TLS Ingress with self-signed certificate, OOMKilled diagnosis and fix, ServiceAccount RBAC binding, Deployment-to-Ingress chain with rewrite annotation, nodeSelector mismatch resolution, Job backoffLimit failure observation and fix, blue-green deployment Service selector switchover, and a comprehensive full-stack aggregate task assembling namespace, quota, ConfigMap, Secret, Deployment, Service, and Ingress.

Solutions: Tasks 11–20

These solutions continue from Section 1. Each solution includes the fastest approach, complete YAML, verification, and a time-saving tip to help you shave minutes off your exam performance.


Solution 11 — Multi-Container Pod with Init Container

Time target: 4 minutes

Time-saving tip: Write init container YAML from memory — init containers follow the same structure as regular containers but appear under spec.initContainers instead of spec.containers. Start with kubectl run app-with-init --image=busybox:1.36 --dry-run=client -o yaml to get the base Pod structure, then add the init container block manually.

Step 1: Create the namespace

kubectl create namespace init-ns

Step 2: Create the Pod

apiVersion: v1
kind: Pod
metadata:
  name: app-with-init
  namespace: init-ns
spec:
  initContainers:
  - name: config-downloader
    image: busybox:1.36
    command: ["sh", "-c", "echo 'server.port=8080' > /shared/config.txt && echo 'Config downloaded'"]
    volumeMounts:
    - name: shared-data
      mountPath: /shared
  containers:
  - name: app
    image: busybox:1.36
    command: ["sh", "-c", "cat /config/config.txt && sleep 3600"]
    volumeMounts:
    - name: shared-data
      mountPath: /config
  volumes:
  - name: shared-data
    emptyDir: {}
kubectl apply -f app-with-init.yaml

Step 3: Verify

Wait for the Pod to reach Running:

kubectl get pod app-with-init -n init-ns

Check the main container logs:

kubectl logs app-with-init -c app -n init-ns

Expected output: server.port=8080

Confirm the init container completed:

kubectl get pod app-with-init -n init-ns -o jsonpath='{.status.initContainerStatuses[0].state}'

Expected: {"terminated":{"exitCode":0,...,"reason":"Completed",...}}

Common Pitfalls

  • Mounting the volume at different paths but referencing the wrong path in the main container. The init container writes to /shared/config.txt, and the volume is mounted at /config in the main container. The file appears as /config/config.txt. If the mount paths don’t align with the file access paths, the main container finds an empty directory.
  • Placing initContainers inside containers. They are sibling fields under spec, not nested. spec.initContainers is separate from spec.containers.
  • Forgetting to share the same volume. Both the init container and the main container must reference the same volume name. If they use different volume names, they mount different (empty) volumes.

Solution 12 — DaemonSet with Control-Plane Tolerations

Time target: 4 minutes

Time-saving tip: There is no imperative command to create a DaemonSet. Start with a Deployment dry-run (kubectl create deployment log-agent --image=busybox:1.36 --dry-run=client -o yaml), then change kind: Deployment to kind: DaemonSet, remove the replicas field and the strategy field, and add tolerations. This is faster than writing a DaemonSet from scratch.

Step 1: Create the namespace

kubectl create namespace daemon-ns

Step 2: Create the DaemonSet

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: log-agent
  namespace: daemon-ns
spec:
  selector:
    matchLabels:
      app: log-agent
  template:
    metadata:
      labels:
        app: log-agent
    spec:
      tolerations:
      - key: "node-role.kubernetes.io/control-plane"
        operator: "Exists"
        effect: "NoSchedule"
      containers:
      - name: log-agent
        image: busybox:1.36
        command: ["sh", "-c", "while true; do echo 'Collecting logs from $(hostname)'; sleep 60; done"]
kubectl apply -f log-agent.yaml

Step 3: Verify

kubectl get daemonset log-agent -n daemon-ns

Expected: DESIRED equals the total number of nodes (workers + control-plane), and READY matches.

kubectl get pods -n daemon-ns -o wide

At least one Pod must be running on a node with the control-plane role. Check the NODE column against:

kubectl get nodes

Common Pitfalls

  • Using operator: "Equal" without specifying a value. The Exists operator matches any value for the given key. If you use Equal, you must provide the exact value of the taint, which varies by cluster. Exists is the reliable choice when the taint value is unknown.
  • Leaving the replicas field in the manifest. DaemonSets do not have a replicas field. If you converted from a Deployment template and forgot to remove it, the API rejects the manifest.
  • Not checking that the DaemonSet Pod count matches the node count. If DESIRED is less than the total node count, a taint you did not account for is blocking scheduling on some nodes.

Solution 13 — Ingress with TLS

Time target: 5 minutes

Time-saving tip: Generate the TLS cert and Secret with two quick commands. Do not overthink the certificate details — for the exam, any self-signed cert works. The key task is wiring the Secret into the Ingress correctly.

Step 1: Create the namespace

kubectl create namespace tls-ns

Step 2: Generate the TLS certificate

openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout tls.key -out tls.crt \
  -subj "/CN=secure-app.example.com"

Step 3: Create the TLS Secret

kubectl create secret tls tls-secret --cert=tls.crt --key=tls.key -n tls-ns

Step 4: Create the Deployment

kubectl create deployment secure-app --image=nginx:1.25 --replicas=2 -n tls-ns

Step 5: Create the Service

kubectl expose deployment secure-app --name=secure-app-svc --port=80 --target-port=80 -n tls-ns

Step 6: Create the Ingress

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: secure-app-ingress
  namespace: tls-ns
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - secure-app.example.com
    secretName: tls-secret
  rules:
  - host: secure-app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: secure-app-svc
            port:
              number: 80
kubectl apply -f secure-app-ingress.yaml

Step 7: Verify

kubectl describe ingress secure-app-ingress -n tls-ns

Expected: TLS section shows secure-app.example.com terminated with Secret tls-secret. Rules section shows the path / routing to secure-app-svc:80.

kubectl get secret tls-secret -n tls-ns -o yaml

The Secret type must be kubernetes.io/tls with tls.crt and tls.key data fields.

Common Pitfalls

  • Creating the Secret with kubectl create secret generic instead of kubectl create secret tls. A generic Secret does not have the kubernetes.io/tls type and may not have the correct key names (tls.crt and tls.key). Use kubectl create secret tls to get the right type automatically.
  • Mismatched host between TLS and rules. The hostname in the tls[].hosts list must exactly match the hostname in rules[].host. A mismatch causes TLS termination to fail for that host.
  • Forgetting ingressClassName. Without this field, the Ingress may not be picked up by any Ingress controller, depending on cluster configuration.
  • Swapping --cert and --key arguments. Providing the key file as --cert and the cert file as --key creates a Secret with invalid TLS data.

Solution 14 — Pod with Ephemeral Volume and OOMKilled Behavior

Time target: 4 minutes

Time-saving tip: Use the polinux/stress image directly — no need to install stress inside a container at runtime. Set up the Pod with limits you know will trigger OOMKilled, observe it, then create the fixed version.

Step 1: Create the namespace

kubectl create namespace oom-ns

Step 2: Create the memory-hog Pod

apiVersion: v1
kind: Pod
metadata:
  name: memory-hog
  namespace: oom-ns
spec:
  containers:
  - name: stress
    image: polinux/stress
    command: ["stress"]
    args: ["--vm", "1", "--vm-bytes", "128M", "--vm-hang", "0"]
    resources:
      requests:
        memory: "64Mi"
        cpu: "100m"
      limits:
        memory: "100Mi"
        cpu: "200m"
    volumeMounts:
    - name: tmp-data
      mountPath: /tmp/data
  volumes:
  - name: tmp-data
    emptyDir: {}
kubectl apply -f memory-hog.yaml

Step 3: Observe OOMKilled

The container requests 128Mi of memory but the limit is 100Mi. The kernel’s OOM killer terminates the process.

kubectl get pod memory-hog -n oom-ns

After a few seconds, the status shows OOMKilled or CrashLoopBackOff (with OOMKilled as the last termination reason).

kubectl get pod memory-hog -n oom-ns -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'

Expected output: OOMKilled

Step 4: Create the fixed Pod

apiVersion: v1
kind: Pod
metadata:
  name: memory-ok
  namespace: oom-ns
spec:
  containers:
  - name: stress
    image: polinux/stress
    command: ["stress"]
    args: ["--vm", "1", "--vm-bytes", "128M", "--vm-hang", "0"]
    resources:
      requests:
        memory: "128Mi"
        cpu: "100m"
      limits:
        memory: "256Mi"
        cpu: "200m"
    volumeMounts:
    - name: tmp-data
      mountPath: /tmp/data
  volumes:
  - name: tmp-data
    emptyDir: {}
kubectl apply -f memory-ok.yaml
kubectl get pod memory-ok -n oom-ns

Expected: Running status that persists.

Common Pitfalls

  • Setting limits equal to the stress allocation. If you set memory: 128Mi as the limit and the stress tool allocates exactly 128Mi, the container may still be OOMKilled due to the process’s own overhead (stack, heap, shared libraries). The limit must be comfortably above the allocation.
  • Forgetting the emptyDir volume. The task requires an ephemeral volume mount at /tmp/data. Missing it loses partial credit even if the OOMKilled behavior is observed correctly.
  • Not checking lastState vs state. The Pod restarts after OOMKilled, so the current state may show Running (during the next attempt) or Waiting (during backoff). The OOMKilled reason is in lastState.terminated.reason.

Solution 15 — ServiceAccount with Role Binding

Time target: 5 minutes

Time-saving tip: Use imperative commands for all three RBAC resources. The sequence is: kubectl create sa, kubectl create role, kubectl create rolebinding. These three commands take under 30 seconds combined.

Step 1: Create the namespace

kubectl create namespace rbac-ns

Step 2: Create the ServiceAccount

kubectl create serviceaccount pod-reader-sa -n rbac-ns

Step 3: Create the Role

kubectl create role pod-reader-role \
  --verb=get,list,watch \
  --resource=pods \
  -n rbac-ns

Step 4: Create the RoleBinding

kubectl create rolebinding pod-reader-binding \
  --role=pod-reader-role \
  --serviceaccount=rbac-ns:pod-reader-sa \
  -n rbac-ns

Note the --serviceaccount format: namespace:name.

Step 5: Create the Pod

apiVersion: v1
kind: Pod
metadata:
  name: reader-pod
  namespace: rbac-ns
spec:
  serviceAccountName: pod-reader-sa
  containers:
  - name: kubectl
    image: bitnami/kubectl:latest
    command: ["sh", "-c", "kubectl get pods -n rbac-ns && sleep 3600"]
kubectl apply -f reader-pod.yaml

Step 6: Verify access

kubectl logs reader-pod -n rbac-ns

Expected: a list of Pods in rbac-ns (at minimum, reader-pod itself).

Step 7: Verify restricted access

kubectl exec reader-pod -n rbac-ns -- kubectl get pods -n default

Expected: Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:rbac-ns:pod-reader-sa" cannot list resource "pods" in API group "" in the namespace "default"

Common Pitfalls

  • Using ClusterRole and ClusterRoleBinding instead of Role and RoleBinding. The task specifies namespace-scoped access. A ClusterRoleBinding would grant access across all namespaces, which violates the requirement that the ServiceAccount cannot list Pods in other namespaces.
  • Wrong --serviceaccount format. The format is namespace:serviceAccountName. Omitting the namespace or reversing the order causes the binding to fail silently — the RoleBinding is created but binds to a non-existent ServiceAccount reference.
  • Forgetting serviceAccountName in the Pod spec. Without it, the Pod uses the default ServiceAccount, which does not have the pod-reader permissions.
  • Using automountServiceAccountToken: false. If this is set (sometimes as a cluster default), the Pod cannot authenticate to the API server. The kubectl get pods command inside the container fails with a connection error instead of a Forbidden error.

Solution 16 — Deployment → Service → Ingress Chain

Time target: 4 minutes

Time-saving tip: Chain imperative commands. Create the Deployment, expose it, then write only the Ingress YAML. Two imperative commands plus one small YAML file — total time under 3 minutes.

Step 1: Create the namespace

kubectl create namespace app-stack

Step 2: Create the Deployment

kubectl create deployment frontend --image=nginx:1.25 --replicas=3 -n app-stack

Step 3: Expose with a Service

kubectl expose deployment frontend --name=frontend-svc --port=80 --target-port=80 -n app-stack

Step 4: Verify endpoints

kubectl get endpoints frontend-svc -n app-stack

Three IP addresses must appear in the endpoint list.

Step 5: Create the Ingress

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: frontend-ingress
  namespace: app-stack
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - host: frontend.example.com
    http:
      paths:
      - path: /app
        pathType: Prefix
        backend:
          service:
            name: frontend-svc
            port:
              number: 80
kubectl apply -f frontend-ingress.yaml

Step 6: Verify

kubectl describe ingress frontend-ingress -n app-stack

Expected: Rules section shows frontend.example.com/appfrontend-svc:80. Annotations section shows the rewrite-target annotation.

Common Pitfalls

  • Omitting the rewrite-target annotation. Without it, requests to /app are forwarded to the backend as /app, but nginx serves content at /. The response is a 404. The rewrite-target: / annotation strips the path prefix before forwarding.
  • Using pathType: Exact instead of Prefix. Exact matches only /app literally, not /app/ or /app/anything. The task specifies Prefix.
  • Forgetting ingressClassName. Required in clusters with multiple Ingress controllers or where no default class is configured.

Solution 17 — Fix: Pod Stuck Pending (nodeSelector Mismatch)

Time target: 3 minutes

Time-saving tip: When a Pod is Pending, run kubectl describe pod and read the Events section first. The scheduler message tells you exactly what label or resource is missing. This avoids guessing.

Step 1: Create namespace and apply the Pod

kubectl create namespace schedule-ns

Apply the Pod manifest from the task.

Step 2: Diagnose

kubectl describe pod picky-pod -n schedule-ns

Events section shows:

Warning  FailedScheduling  ...  0/N nodes are available: N node(s) didn't match Pod's node affinity/selector. preemption: ...

The Pod requires a node with label disktype=ssd, but no node has that label.

Step 3: Find a node and apply the label

kubectl get nodes --show-labels

Pick any worker node (e.g., worker-1):

kubectl label node worker-1 disktype=ssd

Step 4: Wait for the Pod to schedule

kubectl get pod picky-pod -n schedule-ns -w

The Pod transitions from Pending to ContainerCreating to Running.

Step 5: Optional cleanup

kubectl label node worker-1 disktype-

The trailing - removes the label. The Pod continues running because nodeSelector is evaluated at scheduling time, not continuously.

Common Pitfalls

  • Editing the Pod’s nodeSelector instead of labeling the node. Both approaches work, but editing a running Pod’s nodeSelector requires deleting and recreating the Pod (Pods are immutable for most spec fields). Labeling the node is faster and non-disruptive.
  • Labeling the wrong node. On multi-node clusters, label a worker node. Labeling a control-plane node that has taints may not help if the Pod lacks the corresponding tolerations.
  • Typo in the label key or value. The label must be exactly disktype=ssd (matching the Pod’s nodeSelector). disk-type=ssd or disktype=SSD do not match.

Solution 18 — Job with backoffLimit and Failure Handling

Time target: 4 minutes

Time-saving tip: Create the failing Job from a YAML manifest (copy from the task), watch it fail with kubectl get pods -w, then delete it and modify the command for the success version. The knowledge being tested is behavioral — understanding backoffLimit mechanics — not YAML complexity.

Step 1: Create the namespace

kubectl create namespace job-ns

Step 2: Create the failing Job

Apply the manifest from the task directly:

apiVersion: batch/v1
kind: Job
metadata:
  name: failing-job
  namespace: job-ns
spec:
  backoffLimit: 3
  template:
    spec:
      containers:
      - name: worker
        image: busybox:1.36
        command: ["sh", "-c", "echo 'Attempting task...' && exit 1"]
      restartPolicy: Never
kubectl apply -f failing-job.yaml

Step 3: Watch the failure behavior

kubectl get pods -n job-ns -w

Four Pods are created in sequence. Each one runs, prints “Attempting task…”, exits with code 1, and transitions to Error status. The back-off delay increases between each attempt.

Step 4: Verify the Job failed

kubectl describe job failing-job -n job-ns

The conditions section shows:

Type    Status  Reason
Failed  True    BackoffLimitExceeded
kubectl get job failing-job -n job-ns

The COMPLETIONS column shows 0/1 and no new Pods are being created.

Step 5: Delete and create the fixed Job

kubectl delete job failing-job -n job-ns
apiVersion: batch/v1
kind: Job
metadata:
  name: success-job
  namespace: job-ns
spec:
  backoffLimit: 3
  template:
    spec:
      containers:
      - name: worker
        image: busybox:1.36
        command: ["sh", "-c", "echo 'Task completed' && exit 0"]
      restartPolicy: Never
kubectl apply -f success-job.yaml

Step 6: Verify success

kubectl get job success-job -n job-ns

Expected: COMPLETIONS shows 1/1.

kubectl logs job/success-job -n job-ns

Expected output: Task completed

Common Pitfalls

  • Confusing backoffLimit with total attempts. backoffLimit: 3 means 3 retries after the first attempt, for a total of 4 Pod creations. The naming is misleading — it limits the number of back-offs (retries), not the total number of runs.
  • Using restartPolicy: Always. Jobs require either Never or OnFailure. Using Always causes an API validation error. With Never, each failed attempt creates a new Pod. With OnFailure, the same Pod restarts in-place (useful to avoid Pod proliferation).
  • Not deleting the failed Job before creating the success Job. If you try to apply a Job with the same name without deleting the old one, the API rejects it because Job names must be unique within a namespace.

Solution 19 — Blue-Green Deployment

Time target: 5 minutes

Time-saving tip: Create both Deployments and the Service before testing. The actual switchover is a single kubectl patch command. The whole task can be done in under 4 minutes if you write the YAML quickly.

Step 1: Create the namespace

kubectl create namespace bg-ns

Step 2: Create the blue Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-blue
  namespace: bg-ns
spec:
  replicas: 3
  selector:
    matchLabels:
      app: webapp
      version: blue
  template:
    metadata:
      labels:
        app: webapp
        version: blue
    spec:
      containers:
      - name: echo
        image: hashicorp/http-echo:0.2.3
        args: ["-listen=:5678", "-text=blue"]
        ports:
        - containerPort: 5678
kubectl apply -f app-blue.yaml

Step 3: Create the Service pointing to blue

apiVersion: v1
kind: Service
metadata:
  name: webapp-svc
  namespace: bg-ns
spec:
  selector:
    app: webapp
    version: blue
  ports:
  - port: 80
    targetPort: 5678
kubectl apply -f webapp-svc.yaml

Step 4: Verify blue

kubectl run test-blue --image=curlimages/curl --rm -it --restart=Never -n bg-ns -- curl http://webapp-svc.bg-ns.svc.cluster.local

Expected output: blue

Step 5: Create the green Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-green
  namespace: bg-ns
spec:
  replicas: 3
  selector:
    matchLabels:
      app: webapp
      version: green
  template:
    metadata:
      labels:
        app: webapp
        version: green
    spec:
      containers:
      - name: echo
        image: hashicorp/http-echo:0.2.3
        args: ["-listen=:5678", "-text=green"]
        ports:
        - containerPort: 5678
kubectl apply -f app-green.yaml

Wait for green Pods to be ready:

kubectl rollout status deployment/app-green -n bg-ns

Step 6: Switch the Service to green

kubectl patch service webapp-svc -n bg-ns -p '{"spec":{"selector":{"version":"green"}}}'

This changes only the version selector value. The app: webapp selector remains, so the Service now matches green Pods (which have both app: webapp and version: green).

Step 7: Verify green

kubectl run test-green --image=curlimages/curl --rm -it --restart=Never -n bg-ns -- curl http://webapp-svc.bg-ns.svc.cluster.local

Expected output: green

Step 8: Confirm both Deployments are running

kubectl get deployments -n bg-ns

Both app-blue and app-green must show 3/3 ready replicas. The blue Deployment remains available for instant rollback by patching the Service selector back to version: blue.

Common Pitfalls

  • Removing the app: webapp label from the selector when patching. The kubectl patch command with -p '{"spec":{"selector":{"version":"green"}}}' merges the new selector with the existing one. It does not replace the entire selector. The result is app: webapp, version: green, which is correct. If you used kubectl edit and accidentally deleted the app: webapp line, no Pods match.
  • Using different labels on blue and green Deployments. Both must share the app: webapp label. The version label is what differentiates them. Without the common app label, the Service cannot target both with a single selector pattern.
  • Not waiting for green Pods to be ready before switching. If you switch the Service while green Pods are still starting, the curl test may fail or return connection errors. Always confirm all green replicas are ready before patching the Service.
  • Deleting the blue Deployment after switchover. The task requires both Deployments to remain running. Blue serves as the rollback target.

Solution 20 — Full-Stack Aggregate Task

Time target: 8 minutes

Time-saving tip: Work through this task in layers: namespace and quota first, then data resources (ConfigMap and Secret), then the Deployment (which consumes them), then the Service, then the Ingress. Each layer depends on the previous one. Use imperative commands wherever possible to save time — ConfigMap, Secret, Deployment, and Service all support imperative creation.

Step 1: Create namespace and ResourceQuota

kubectl create namespace fullstack-ns
apiVersion: v1
kind: ResourceQuota
metadata:
  name: stack-quota
  namespace: fullstack-ns
spec:
  hard:
    pods: "10"
    requests.cpu: "2"
    requests.memory: 2Gi
kubectl apply -f stack-quota.yaml

Verify:

kubectl describe resourcequota stack-quota -n fullstack-ns

Step 2: Create the ConfigMap

kubectl create configmap app-settings \
  --from-literal=APP_ENV=production \
  --from-literal=LOG_LEVEL=warn \
  --from-literal=MAX_CONNECTIONS=100 \
  -n fullstack-ns

Step 3: Create the Secret

kubectl create secret generic app-secrets \
  --from-literal=API_KEY=super-secret-key-2026 \
  -n fullstack-ns

Step 4: Create the Deployment

Because of the ResourceQuota on requests.cpu and requests.memory, every container must specify resource requests. The Deployment manifest:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: fullstack-app
  namespace: fullstack-ns
spec:
  replicas: 3
  selector:
    matchLabels:
      app: fullstack
      tier: frontend
  template:
    metadata:
      labels:
        app: fullstack
        tier: frontend
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
          limits:
            cpu: "200m"
            memory: "256Mi"
        envFrom:
        - configMapRef:
            name: app-settings
        env:
        - name: API_KEY
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: API_KEY
kubectl apply -f fullstack-app.yaml

Wait for the rollout:

kubectl rollout status deployment/fullstack-app -n fullstack-ns

Step 5: Create the Service

kubectl expose deployment fullstack-app --name=fullstack-svc --port=80 --target-port=80 -n fullstack-ns

The kubectl expose command automatically picks up the app: fullstack selector from the Deployment. Verify it also includes tier: frontend or confirm endpoints are populated:

kubectl get endpoints fullstack-svc -n fullstack-ns

Three IP addresses must appear.

Note: kubectl expose uses all the Deployment’s selector labels. If the generated Service selector is app: fullstack, tier: frontend, the Service still matches the correct Pods. However, the task only requires the selector to include app: fullstack. Both configurations are valid as long as the endpoints resolve correctly.

Step 6: Create the Ingress

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: fullstack-ingress
  namespace: fullstack-ns
spec:
  ingressClassName: nginx
  rules:
  - host: fullstack.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: fullstack-svc
            port:
              number: 80
kubectl apply -f fullstack-ingress.yaml

Step 7: Full verification

# All resources exist
kubectl get quota,configmap,secret,deployment,service,ingress -n fullstack-ns

Expected: stack-quota, app-settings, app-secrets, fullstack-app, fullstack-svc, and fullstack-ingress all present.

# Deployment has 3 ready replicas
kubectl get deployment fullstack-app -n fullstack-ns

Expected: 3/3 in the READY column.

# Environment variables are set
kubectl exec deploy/fullstack-app -n fullstack-ns -- env | grep -E "APP_ENV|LOG_LEVEL|MAX_CONNECTIONS|API_KEY"

Expected:

APP_ENV=production
LOG_LEVEL=warn
MAX_CONNECTIONS=100
API_KEY=super-secret-key-2026
# Service has 3 endpoints
kubectl get endpoints fullstack-svc -n fullstack-ns

Expected: 3 IP:port entries.

# Ingress is configured
kubectl describe ingress fullstack-ingress -n fullstack-ns

Expected: fullstack.example.com/fullstack-svc:80.

Common Pitfalls

  • Forgetting resource requests because of the ResourceQuota. When a ResourceQuota specifies requests.cpu or requests.memory, every container in the namespace must declare those fields. Pods without resource requests are rejected by the admission controller. This catches candidates who use kubectl create deployment imperatively without adding resources afterward.
  • Using envFrom for the Secret. The task specifies envFrom with configMapRef for the ConfigMap but individual secretKeyRef for the Secret. Mixing them up (using envFrom with secretRef for the Secret) technically works but does not match the requirements.
  • Deploying resources in the wrong namespace. With 6 resources to create, forgetting -n fullstack-ns on one command puts that resource in the default namespace. The verification commands check fullstack-ns exclusively.
  • Not verifying end-to-end after each layer. Check the Deployment status before creating the Service. Check the endpoints before creating the Ingress. Catching errors early prevents cascading debugging time.
  • Exceeding the ResourceQuota. Three replicas at 100m CPU and 128Mi memory each total 300m CPU and 384Mi memory — well within the 2 CPU and 2Gi limits. But if you set higher values per container (e.g., 1 CPU each for 3 replicas = 3 CPU), the Deployment fails to create Pods because the quota caps total CPU requests at 2.