docs: add Kubernetes CKS study notes

This commit is contained in:
2026-08-04 23:18:45 +02:00
parent a6ee7a2b07
commit 91a1849009
57 changed files with 8313 additions and 0 deletions

View File

@@ -0,0 +1,141 @@
# Assignment
We are working on an application that will be deployed on multiple containers within a pod on Kubernetes cluster. There is a requirement to share a volume among the containers to save some temporary data. The Nautilus DevOps team is developing a similar template to replicate the scenario. Below you can find more details about it.
Create a pod named volume-share-nautilus.
For the first container, use image ubuntu with latest tag only and remember to mention the tag i.e ubuntu:latest, container should be named as volume-container-nautilus-1, and run a sleep command for it so that it remains in running state. Volume volume-share should be mounted at path /tmp/beta.
For the second container, use image ubuntu with the latest tag only and remember to mention the tag i.e ubuntu:latest, container should be named as volume-container-nautilus-2, and again run a sleep command for it so that it remains in running state. Volume volume-share should be mounted at path /tmp/apps.
Volume name should be volume-share of type emptyDir.
After creating the pod, exec into the first container i.e volume-container-nautilus-1, and just for testing create a file beta.txt with the content Welcome to xFusionCorp Industries under the mounted path of first container i.e /tmp/beta.
The file beta.txt should be present under the mounted path /tmp/apps on the second container volume-container-nautilus-2 as well, since they are using a shared volume.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
# Solution
# Kubernetes Shared-Volume Pod — `volume-share-nautilus`
Two `ubuntu:latest` containers sharing one `emptyDir` volume mounted at different paths, then a
test proving a file written in one appears in the other. Applied inline via a heredoc — no
manifest file on disk.
## Step 1 — Apply the pod (heredoc → kubectl)
```bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: volume-share-nautilus
spec:
volumes:
- name: volume-share
emptyDir: {}
containers:
- name: volume-container-nautilus-1
image: ubuntu:latest
command:
- /bin/sh
- -c
- "sleep infinity"
volumeMounts:
- name: volume-share
mountPath: /tmp/beta
- name: volume-container-nautilus-2
image: ubuntu:latest
command:
- /bin/sh
- -c
- "sleep infinity"
volumeMounts:
- name: volume-share
mountPath: /tmp/apps
EOF
```
## Step 2 — Write the test file in container 1
```bash
kubectl exec -it volume-share-nautilus -c volume-container-nautilus-1 -- \
bash -c 'echo "Welcome to xFusionCorp Industries" > /tmp/beta/beta.txt'
```
## Step 3 — Confirm it appears in container 2
```bash
kubectl exec -it volume-share-nautilus -c volume-container-nautilus-2 -- \
cat /tmp/apps/beta.txt
```
Expected output: `Welcome to xFusionCorp Industries`
## How it works
### The heredoc apply pattern
- **`kubectl apply -f -`** reads from **stdin**; nothing written to disk.
- **`<<'EOF'` (delimiter quoted)** keeps the manifest literal — the right default for k8s YAML.
### One volume, two mount points
The heart of this task: a **single** `emptyDir` volume (`volume-share`) declared once under
`spec.volumes`, then mounted into **both** containers via `volumeMounts` — but at **different
paths**:
- container 1 sees it at `/tmp/beta`
- container 2 sees it at `/tmp/apps`
Both `volumeMounts` reference the same volume `name: volume-share`, so they're two windows onto
the **same underlying storage**. A write through one window is instantly visible through the
other — which is why `beta.txt` created at `/tmp/beta/beta.txt` shows up at
`/tmp/apps/beta.txt`. The mount paths are just where each container chooses to see the shared
data; the bytes are shared.
### `emptyDir` — pod-lifetime shared scratch
- **`emptyDir: {}`** creates an empty directory when the pod is scheduled to a node, shared by
all containers in the pod, and deleted when the pod is removed. It's the standard choice for
ephemeral data shared between containers in a pod — exactly this "temporary data" scenario.
### Why the `sleep` command
`ubuntu:latest` has no long-running default process — it would start, find nothing to do, and
exit, crash-looping the pod. Giving each container `sleep infinity` (via `/bin/sh -c`) provides
a foreground process that never returns, so both containers stay **Running** and remain
available to `exec` into. `sleep infinity` is cleaner than a fixed duration (which would
eventually exit); `tail -f /dev/null` or a `while true` loop are equivalent.
### `kubectl exec -c` targets a specific container
Because the pod is multi-container, `kubectl exec` **requires** `-c <container>` to say which
one — without it, kubectl defaults to the first container but warns; being explicit avoids
ambiguity. Step 2 targets container 1 to write; Step 3 targets container 2 to read.
## Verify
```bash
# Both containers up
kubectl get pod volume-share-nautilus # READY should be 2/2
# The shared file, read from container 2's mount path
kubectl exec volume-share-nautilus -c volume-container-nautilus-2 -- cat /tmp/apps/beta.txt
```
Expected — `volume-share-nautilus` `READY 2/2 Running`, and the `cat` returning
`Welcome to xFusionCorp Industries` from container 2's `/tmp/apps` path.
> If a container is in `CrashLoopBackOff`, its `sleep` command didn't take — confirm with
> `kubectl get pod volume-share-nautilus -o jsonpath='{.spec.containers[*].command}'`.

View File

@@ -0,0 +1,120 @@
# Assignment
Last week, the Nautilus DevOps team deployed a redis app on Kubernetes cluster, which was working fine so far. This morning one of the team members was making some changes in this existing setup, but he made some mistakes and the app went down. We need to fix this as soon as possible. Please take a look.
The deployment name is redis-deployment. The pods are not in running state right now, so please look into the issue and fix the same.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
# Solution
```bash
# - incorrect config map name
# - incorrect image on redis
```
# Troubleshoot Deployment — `redis-deployment`
The redis pods won't start after a bad edit. This walks the diagnosis, then the fix for whichever
root cause `describe` reveals.
## Step 1 — Diagnose
```bash
# Deployment + pod state
kubectl get deployment redis-deployment
kubectl get pods -l app=redis # adjust selector if different
# The authoritative source: per-container state + Events at the bottom
kubectl describe pod -l app=redis
# (or describe a specific pod name from the get output)
# The full spec, to spot the bad edit
kubectl get deployment redis-deployment -o yaml
```
Read the **container state** and **Events** in `describe`. The status tells you the class of bug:
| Symptom in `describe` | Root cause |
|-----------------------|------------|
| `ErrImagePull` / `ImagePullBackOff` | **Image name/tag typo** (e.g. `redis:alpin``redis:alpine`). |
| `CreateContainerConfigError` | **Bad reference** — a `configMapKeyRef` / `configMap` volume / `secretKeyRef` pointing at a name or key that doesn't exist. |
| `CrashLoopBackOff` | Container starts then exits — bad command/args or a config the app rejects. |
| `Pending` / `FailedScheduling` | An unschedulable request (e.g. `resources.requests` too high, bad nodeSelector). |
## Step 2 — Fix (match to what you found)
### A) Image typo → correct the image
```bash
# Confirm the container name and the wrong image
kubectl get deployment redis-deployment \
-o jsonpath='{.spec.template.spec.containers[0].name}{" "}{.spec.template.spec.containers[0].image}{"\n"}'
# Fix it (use the real container name = left of '='; correct the image/tag)
kubectl set image deployment/redis-deployment redis-container=redis:alpine
```
### B) Bad configMap / volume reference → correct the name
If a `configMap` volume or `configMapKeyRef` points at a mistyped name, fix it in place:
```bash
kubectl edit deployment redis-deployment
# find the wrong reference (e.g. name: redis-cofig) and correct it (redis-config),
# matching an existing ConfigMap:
kubectl get configmaps
```
### C) Unschedulable resource request → lower it
```bash
# Example: a request of "2" CPU on a small node leaves the pod Pending
kubectl edit deployment redis-deployment
# correct spec.template.spec.containers[].resources.requests to a sane value
```
Any of these edits changes the pod template and triggers a fresh rollout automatically.
```bash
kubectl rollout status deployment/redis-deployment
```
## How it works
### Why `describe` is the first move, not a guess
A broken pod advertises its exact failure in its container state and Events. `ImagePullBackOff`
names the image it couldn't pull (revealing a typo); `CreateContainerConfigError` names the
missing ConfigMap/Secret; `FailedScheduling` states why no node fits. Reading that first tells you
*which* field the "mistake" touched, so you fix one thing instead of shotgunning changes.
### Why in-place edits over re-apply here
The fix is a single-field correction on a deployment you didn't author and whose full spec you may
not have cleanly. `kubectl set image` (for the image) and `kubectl edit` (for a reference or
resource value) touch exactly the broken field and leave the rest intact — safer than
reconstructing the whole manifest. Each edit updates the pod template, so the Deployment rolls out
corrected pods on its own.
### Why the pods recover automatically
A Deployment continuously reconciles toward its spec. Once the template is valid (real image,
existing ConfigMap, schedulable requests), the ReplicaSet successfully creates pods and they reach
`Running` — no manual pod deletion needed, though you can delete a stuck pod to speed replacement.
## Verify
```bash
kubectl get deployment redis-deployment # READY should match desired
kubectl get pods -l app=redis # all Running
kubectl describe deployment redis-deployment | sed -n '/Events/,$p'
```
Expected — `redis-deployment` `READY N/N`, all pods `Running`, and no recurring error events.
> Paste the output of `kubectl get deployment redis-deployment -o yaml` if you want the exact
> one-line patch — the fix depends on which field was mistyped.

View File

@@ -0,0 +1,143 @@
# Assignment
One of the DevOps team members was trying to install a WordPress website on a LAMP stack, which is deployed on a Kubernetes cluster. It was working well, and we could see the installation page a few hours ago. However, something seems to have gone wrong with the stack after the website went down. Please look into the issue and fix it:
FYI, the deployment name is lamp-wp and it is using a service named lamp-service. Apache is using the default HTTP port, and the NodePort is 30008. From the application logs, it has been identified that the application is facing some issues connecting to the database, in addition to other problems. Additionally, there are some environment variables associated with the pods, such as MYSQL_ROOT_PASSWORD, MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD, and MYSQL_HOST
Also, do not attempt to delete or modify any other existing components, such as deployment names, service names, types, labels, secrets and so on.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
# Solution
# Troubleshoot LAMP WordPress — `lamp-wp` / `lamp-service`
The WordPress site is down after a bad change. The hints point to **two+ problems**: a DB-connection
issue (env vars) and "other problems" (typically the service port). This diagnoses, then fixes
surgically — **without** touching deployment/service names, types, labels, or secrets, as required.
## Step 1 — Diagnose
```bash
# Pod + container state
kubectl get pods -l app=lamp-wp # adjust selector to match
kubectl describe pod -l app=lamp-wp
# App logs (the DB connection error shows here)
kubectl logs -l app=lamp-wp -c <httpd-or-php-container>
kubectl logs -l app=lamp-wp -c <mysql-container>
# Full specs — find the mismatches
kubectl get deployment lamp-wp -o yaml
kubectl get service lamp-service -o yaml
kubectl get secrets # note the secret + keys the env vars use
```
Look for these specific mismatches:
1. **Service port** — Apache serves on **80** (default HTTP). The Service's `targetPort` must be
`80`; if the bad edit set it to something else (e.g. `8080`), the NodePort routes to a dead
port. `nodePort` stays `30008`.
2. **`MYSQL_HOST`** — in the WordPress/PHP container this must equal the **DB service name** (check
`kubectl get svc` for the MySQL service). If it points at the wrong host/IP, WordPress can't
reach the database — the logged connection error.
3. **DB credential env consistency** — the WordPress container's `MYSQL_ROOT_PASSWORD`,
`MYSQL_DATABASE`, `MYSQL_USER`, `MYSQL_PASSWORD` must match what the **MySQL** container was
initialized with (usually both pull from the same Secret keys). A key mismatch means WordPress
authenticates with wrong credentials.
## Step 2 — Fix (surgical; names/types/labels/secrets untouched)
### Service targetPort → 80
```bash
kubectl patch service lamp-service \
--type=json \
-p='[{"op":"replace","path":"/spec/ports/0/targetPort","value":80}]'
```
### `MYSQL_HOST` → the correct DB service name
```bash
# Find the MySQL service name first
kubectl get svc
# Set MYSQL_HOST on the WordPress/PHP container (use its real container name)
kubectl set env deployment/lamp-wp \
--containers='<httpd-or-php-container>' \
MYSQL_HOST=<mysql-service-name>
```
### Any wrong credential env / secret key reference
If an env var references a wrong Secret key, correct the **reference** (not the Secret) in place:
```bash
kubectl edit deployment lamp-wp
# fix the mistyped valueFrom.secretKeyRef.key / .name to match `kubectl get secret <name> -o yaml`
```
Each change updates the pod template and triggers a fresh rollout:
```bash
kubectl rollout status deployment/lamp-wp
```
## How it works
### Two independent failure planes
This stack breaks in two places that must both be right:
- **Network path** — `<node-ip>:30008` (nodePort) → Service `port`**`targetPort` 80** → Apache.
If `targetPort` doesn't match Apache's listen port, the page is unreachable even when the pod is
perfectly healthy. That's the "other problem" beyond the DB.
- **App→DB path** — WordPress connects to MySQL over the cluster network using `MYSQL_HOST` plus the
credential env vars. `MYSQL_HOST` must resolve to the MySQL **Service** (stable DNS name), and the
credentials must match what MySQL was initialized with. A wrong host or mismatched credential is
the logged "can't connect to database" error.
Fixing one without the other leaves the site down, which is why the task hints at multiple issues.
### Why surgical edits, not re-apply
The task forbids changing deployment/service names, types, labels, and secrets. Re-applying a
hand-built manifest risks altering those by omission. `kubectl patch` (targetPort), `kubectl set
env` (one env var on one container), and `kubectl edit` (a single reference) each touch exactly the
broken field and leave every protected component intact. This is the safe way to honor the "don't
modify other components" rule.
### Why `MYSQL_HOST` is a Service name
Pod IPs are ephemeral; a Service gives MySQL a stable DNS name inside the cluster. WordPress must
target that name so it keeps resolving across pod restarts — hardcoding an IP or using a wrong name
breaks on the first reschedule, which is exactly the kind of thing a bad manual edit introduces.
## Verify
```bash
# Pods running
kubectl get pods -l app=lamp-wp
# Service targetPort is 80, nodePort 30008
kubectl get service lamp-service \
-o jsonpath='{.spec.ports[0].targetPort}{" "}{.spec.ports[0].nodePort}{"\n"}'
# WordPress env has correct MYSQL_HOST
kubectl set env deployment/lamp-wp --list | grep MYSQL_HOST
# App reachable / DB connected (no more DB error in logs)
kubectl logs -l app=lamp-wp -c <httpd-or-php-container> | tail
curl -sI http://<node-ip>:30008 | head -n1
```
Expected — pods `Running`, service `targetPort 80` / `nodePort 30008`, `MYSQL_HOST` set to the
MySQL service name, no DB-connection errors in the logs, and the WordPress installation/login page
loading at `<node-ip>:30008`.
> Paste `kubectl get deployment lamp-wp -o yaml` and `kubectl get service lamp-service -o yaml`
> (plus `kubectl get secret <name> -o yaml`) and I'll give you the exact patches — the precise fix
> depends on which values were changed.

View File

@@ -0,0 +1,120 @@
# Assignment
Nautilus developers need access to the last 24 hours of logs so that they can trace issues and bugs. Therefore, we need to ship the access and error logs for the web server to a log-aggregation service. Following the separation of concerns principle, we implement the Sidecar pattern by deploying a second container that ships the error and access logs from nginx. Nginx does one thing, and it does it well - serving web pages. The second container also specializes in its task - shipping logs. Since containers are running on the same Pod, we can use a shared emptyDir volume to read and write logs.
Create a pod named webserver.
Create an emptyDir volume named shared-logs.
Create a regular container in the webserver pod from the nginx:latest image named nginx-container, and an init container from the ubuntu:latest image named sidecar-container.
Add the following command to the sidecar-container "sh","-c","while true; do cat /var/log/nginx/access.log /var/log/nginx/error.log; sleep 30; done"
Mount the shared-logs volume in both containers at /var/log/nginx. Ensure all containers are in a running state.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
# Solution
# Kubernetes Sidecar Logging Pod — `webserver`
An nginx container plus a log-shipping `sidecar-container`, sharing an `emptyDir` volume at
`/var/log/nginx`. The sidecar is defined as a **native sidecar** (an init container with
`restartPolicy: Always`) so it keeps running without blocking the pod. Applied inline via a
heredoc — no manifest file on disk.
## Apply (heredoc → kubectl)
```bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: webserver
spec:
volumes:
- name: shared-logs
emptyDir: {}
initContainers:
- name: sidecar-container
image: ubuntu:latest
restartPolicy: Always
command:
- sh
- -c
- "while true; do cat /var/log/nginx/access.log /var/log/nginx/error.log; sleep 30; done"
volumeMounts:
- name: shared-logs
mountPath: /var/log/nginx
containers:
- name: nginx-container
image: nginx:latest
volumeMounts:
- name: shared-logs
mountPath: /var/log/nginx
EOF
```
## How it works
### The catch: "init container" that must keep running
The task calls `sidecar-container` an **init container**, gives it an infinite
`while true` loop, and requires **all containers to be running**. Those are contradictory for a
*classic* init container: a normal init container must run to **completion** before the main
container starts. An init container with `while true` never completes, so the pod would hang in
`Init:0/1` forever and nginx would never start.
### The resolution: a native sidecar
Kubernetes 1.28+ supports **native sidecar containers** — an entry under `initContainers` that
carries **`restartPolicy: Always`**. That one field changes the semantics:
- it's still declared under `initContainers` (satisfying "init container"),
- but with `restartPolicy: Always`, Kubernetes **starts it and moves on** without waiting for it
to complete, and **keeps it running** for the pod's lifetime alongside the main container.
So nginx starts normally, the sidecar runs continuously tailing the logs, and the pod reaches a
fully `Running` state — meeting both "init container" and "all containers running." This is
exactly the sidecar pattern the task describes, implemented the modern, native way.
### The shared volume
- **`shared-logs` (`emptyDir`)** is declared once and mounted at **`/var/log/nginx` in both
containers**. nginx writes `access.log` and `error.log` there (its default log directory);
the sidecar reads from the same path because it's the same underlying storage. This is the
"read and write logs via a shared emptyDir" mechanism the task calls for — separation of
concerns: nginx serves, the sidecar ships logs, they meet only at the shared volume.
### The sidecar command
`sh -c "while true; do cat .../access.log .../error.log; sleep 30; done"` continuously dumps the
two log files every 30 seconds (standing in for shipping them to an aggregator). The infinite
loop is what requires the native-sidecar treatment above — without `restartPolicy: Always` under
`initContainers`, this loop would block the pod.
> Note: nginx creates `access.log`/`error.log` on first request; until then the sidecar's `cat`
> may print "No such file or directory" to its own stdout, which is harmless — it keeps looping.
## Verify
```bash
# Pod fully up: nginx running AND the native sidecar running
kubectl get pod webserver
# Confirm the sidecar is an init container with restartPolicy: Always
kubectl get pod webserver \
-o jsonpath='{.spec.initContainers[0].name}{" restartPolicy="}{.spec.initContainers[0].restartPolicy}{"\n"}'
# Both mounts point at /var/log/nginx
kubectl get pod webserver \
-o jsonpath='{range .spec.containers[*]}{.name}{" "}{.volumeMounts[0].mountPath}{"\n"}{end}'
```
Expected — `webserver` `Running` with the nginx container ready; the jsonpath printing
`sidecar-container restartPolicy=Always`; and the mount path `/var/log/nginx` on the containers.
> If the pod sits in `Init:0/1`, the sidecar is being treated as a **classic** init container —
> the `restartPolicy: Always` line is missing or the cluster predates native sidecars (needs
> Kubernetes 1.28+). Confirm the field made it into the spec with the second command above.

View File

@@ -0,0 +1,121 @@
# Assignment
Some of the Nautilus team developers are developing a static website and they want to deploy it on Kubernetes cluster. They want it to be highly available and scalable. Therefore, based on the requirements, the DevOps team has decided to create a deployment for it with multiple replicas. Below you can find more details about it:
Create a deployment using nginx image with latest tag only and remember to mention the tag i.e nginx:latest. Name it as nginx-deployment. The container should be named as nginx-container, also make sure replica counts are 3.
Create a NodePort type service named nginx-service. The nodePort should be 30011.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
# Solution
# Kubernetes Deployment + NodePort Service — `nginx-deployment` / `nginx-service`
A 3-replica nginx Deployment exposed by a NodePort Service on port `30011`. Both objects in one
multi-document heredoc — no manifest file on disk.
## Apply (heredoc → kubectl)
```bash
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx-container
image: nginx:latest
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: NodePort
selector:
app: nginx
ports:
- port: 80
targetPort: 80
nodePort: 30011
EOF
```
## How it works
### The heredoc apply pattern
- **`kubectl apply -f -`** reads from **stdin**; `---` separates the two documents, applied in
order.
- **`<<'EOF'` (delimiter quoted)** keeps the manifest literal — the right default for k8s YAML.
### The Deployment
- **`apps/v1` / `kind: Deployment`** — manages a ReplicaSet, which keeps the desired number of
pods running and self-heals; the basis for the "highly available and scalable" requirement.
- **`metadata.name: nginx-deployment`** and container **`name: nginx-container`** — exactly as
required.
- **`replicas: 3`** — three identical pods spread across the cluster for availability.
- **`image: nginx:latest`** — tag stated explicitly, as required.
- **`selector.matchLabels: app: nginx`** must equal **`template.metadata.labels: app: nginx`** —
a Deployment finds the pods it owns by this label match; a mismatch makes the API reject the
manifest.
### The Service ties to pods by label, not to the Deployment
- **`type: NodePort`** opens a port on every node that forwards into the cluster.
- **`selector: app: nginx`** — this is what connects the Service to the Deployment's pods. The
Service targets any pod labeled `app: nginx` — which is exactly what the Deployment stamps on
its three pods — and load-balances across them. The Service has no direct reference to the
Deployment; the shared `app: nginx` label is the entire link. A mismatched selector would
leave the Service with **zero endpoints**.
- **Port fields:**
- `port: 80` — the Service's own ClusterIP port.
- `targetPort: 80` — the container port traffic is forwarded to (nginx serves on 80).
- `nodePort: 30011` — the port opened on each node for external access. `30011` is inside the
valid range (`3000032767`), so the API accepts it.
Traffic path: `<node-ip>:30011` → Service `:80` → one of the three pods `:80`.
### Why the label must be consistent in three places
`app: nginx` appears in the Deployment selector, the pod template labels, and the Service
selector. The first two tie the Deployment to its pods; the third ties the Service to those same
pods. Keeping the label identical across all three is what makes the whole chain — Deployment →
pods → Service — connect.
## Verify
```bash
# Deployment with 3 ready replicas
kubectl get deployment nginx-deployment
# Service is NodePort on 30011
kubectl get service nginx-service
# Endpoints populated = selector matched the pods (key check)
kubectl get endpoints nginx-service
# Reachable via the node port
curl -s http://<node-ip>:30011 | head -n 5
```
Expected — deployment `READY 3/3`, `nginx-service` of type `NodePort` showing `80:30011/TCP`,
and `kubectl get endpoints nginx-service` listing three pod IPs. Empty endpoints ⇒ selector/label
mismatch (check `kubectl get pods --show-labels`).

View File

@@ -0,0 +1,123 @@
# Assignment
The Nautilus DevOps team is working on to setup some pre-requisites for an application that will send the greetings to different users. There is a sample deployment, that needs to be tested. Below is a scenario which needs to be configured on Kubernetes cluster. Please find below more details about it.
Create a pod named print-envars-greeting.
Configure spec as, the container name should be print-env-container and use bash image.
Create three environment variables:
a. GREETING and its value should be Welcome to
b. COMPANY and its value should be DevOps
c. GROUP and its value should be Industries
Use command ["/bin/sh", "-c", 'echo "$(GREETING) $(COMPANY) $(GROUP)"'] (please use this exact command), also set its restartPolicy policy to Never to avoid crash loop back.
You can check the output using kubectl logs -f print-envars-greeting command.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
# Solution
# Kubernetes Pod with Env Vars — `print-envars-greeting`
A one-shot pod that echoes three environment variables using Kubernetes' `$(VAR)` substitution.
Applied inline via a heredoc — no manifest file on disk.
## Apply (heredoc → kubectl)
```bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: print-envars-greeting
spec:
restartPolicy: Never
containers:
- name: print-env-container
image: bash
command: ["/bin/sh", "-c", 'echo "$(GREETING) $(COMPANY) $(GROUP)"']
env:
- name: GREETING
value: "Welcome to"
- name: COMPANY
value: "DevOps"
- name: GROUP
value: "Industries"
EOF
```
## Check the output
```bash
kubectl logs -f print-envars-greeting
```
Expected: `Welcome to DevOps Industries`
## How it works
### The heredoc apply pattern — quoting matters a lot here
- **`kubectl apply -f -`** reads from **stdin**; nothing written to disk.
- **`<<'EOF'` (delimiter quoted)** is critical for this task. The command contains `$(GREETING)`,
`$(COMPANY)`, `$(GROUP)`. If the heredoc delimiter were **unquoted**, the jump-host's shell
would interpret `$(...)` as **command substitution** and try to run `GREETING` as a command —
mangling the manifest before kubectl ever sees it. Quoting `EOF` passes the `$(...)` through
literally so Kubernetes (not the shell) handles the expansion.
### `$(VAR)` is Kubernetes substitution, not shell substitution
The exact command uses `$(GREETING)`, not `${GREETING}` or `$GREETING`. In a container's
`command`/`args`, **Kubernetes itself** expands `$(VAR)` references using the container's declared
`env` **before** the process starts. So Kubernetes rewrites the command to
`echo "Welcome to DevOps Industries"`, and `/bin/sh` then just echoes that literal string.
This is a subtle but important distinction:
- **`$(VAR)`** — expanded by **Kubernetes** from the `env` list at container start.
- **`${VAR}` / `$VAR`** — expanded by the **shell** at runtime.
Both would produce the same output here (since `sh` also has the env vars), but the task pins the
`$(VAR)` form, and it's Kubernetes doing the substitution. A gotcha to know: `$(VAR)` only
resolves if a matching `env` entry exists; an undefined `$(FOO)` is left **literal** rather than
erroring.
### The env vars
Three `env` entries — `GREETING="Welcome to"`, `COMPANY="DevOps"`, `GROUP="Industries"` — supply
the values Kubernetes substitutes into the command. Values are quoted because they contain spaces
(`Welcome to`).
### `restartPolicy: Never`
The command echoes once and exits `0` — a completed run, not a long-running process. With the
default `restartPolicy: Always`, Kubernetes would see the container exit and keep restarting it,
driving `CrashLoopBackOff` (even though it "succeeded"). `Never` tells Kubernetes not to restart
it, so the pod ends in `Completed` state cleanly. This is why the task specifies it.
### The `bash` image
`image: bash` pulls the official BusyBox-based bash image, which provides `/bin/sh` to run the
command. It runs the echo and exits — exactly the one-shot behavior wanted.
## Verify
```bash
# Pod ran to completion
kubectl get pod print-envars-greeting # STATUS: Completed
# The greeting output
kubectl logs print-envars-greeting
```
Expected — `print-envars-greeting` in `Completed` status, and the logs printing
`Welcome to DevOps Industries`.
> `Completed` (not `Running`) is correct here — the pod's job was to print once and exit. If it
> shows `CrashLoopBackOff`, `restartPolicy: Never` didn't take.

View File

@@ -0,0 +1,163 @@
# Assignment
There is a production deployment planned for next week. The Nautilus DevOps team wants to test the deployment update and rollback on Dev environment first so that they can identify the risks in advance. Below you can find more details about the plan they want to execute.
Create a namespace xfusion. Create a deployment called httpd-deploy under this new namespace, It should have one container called httpd, use httpd:2.4.27 image and 3 replicas. The deployment should use RollingUpdate strategy with maxSurge=1, and maxUnavailable=2. Also create a NodePort type service named httpd-service and expose the deployment on nodePort: 30008.
Now upgrade the deployment to version httpd:2.4.43 using a rolling update.
Finally, once all pods are updated undo the recent update and roll back to the previous/original version.
Note:
a. The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
b. Please make sure you only use the specified image(s) for this deployment and as per the sequence mentioned in the task description. If you mistakenly use a wrong image and fix it later, that will also distort the revision history which can eventually fail this task.
# Solution
# Deployment Update + Rollback — `httpd-deploy` in `devops`
Create the deployment at `httpd:2.4.27`, roll it forward to `httpd:2.4.43`, then roll it back —
keeping revision history clean throughout.
> **Sequencing matters:** apply each image one at a time and let each rollout finish before the
> next. Do **not** put `2.4.43` in the initial manifest, and don't "fix" a wrong image mid-stream
> — either distorts the revision history.
## Step 1 — Namespace + deployment (`2.4.27`) + service
```bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Namespace
metadata:
name: devops
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: httpd-deploy
namespace: devops
labels:
app: httpd
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 2
selector:
matchLabels:
app: httpd
template:
metadata:
labels:
app: httpd
spec:
containers:
- name: httpd
image: httpd:2.4.27
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: httpd-service
namespace: devops
spec:
type: NodePort
selector:
app: httpd
ports:
- port: 80
targetPort: 80
nodePort: 30008
EOF
# Let revision 1 fully settle BEFORE any update
kubectl rollout status deployment/httpd-deploy -n devops
```
## Step 2 — Rolling update to `httpd:2.4.43`
```bash
kubectl set image deployment/httpd-deploy httpd=httpd:2.4.43 -n devops
# Wait for the update to fully complete (revision 2)
kubectl rollout status deployment/httpd-deploy -n devops
```
## Step 3 — Roll back to the original version
```bash
kubectl rollout undo deployment/httpd-deploy -n devops
# Wait for the rollback to complete (revision 3, template of revision 1)
kubectl rollout status deployment/httpd-deploy -n devops
```
## How it works
### Sequencing keeps revision history clean
Each `kubectl` action that changes the pod template creates a new **revision**, each backed by a
ReplicaSet:
- **Revision 1** — the initial `2.4.27` deployment.
- **Revision 2** — the `set image` update to `2.4.43`.
- **Revision 3** — the `rollout undo`, which reuses revision 1's template (`2.4.27`).
`kubectl rollout status` between each step forces each rollout to **finish** before the next
begins, so the revisions are distinct and ordered. Firing the update before the create settles —
or applying a wrong image and correcting it — would inject extra revisions.
### The initial manifest
- **Namespace `devops`** created first so the deployment and service land in it.
- **Deployment** — `httpd-deploy`, container **`httpd`**, image **`httpd:2.4.27`**, `replicas: 2`.
- **`strategy.rollingUpdate`** — `maxSurge: 1` (at most 1 pod above the desired 2 during a
rollout) and `maxUnavailable: 2` (up to 2 pods down at once). With only 2 replicas and
`maxUnavailable: 2`, both pods can be replaced at once, so the rollout is fast.
- **`selector.matchLabels: app: httpd`** equals the template labels, and the **Service**'s
`selector: app: httpd` targets those same pods. `nodePort: 30008` is in the valid range
(`3000032767`).
### `set image` for the update, `rollout undo` for the rollback
- **`set image`** patches just the container image on the live deployment — the idiomatic way to
trigger a rolling update. Container name `httpd` is the left side of `httpd=httpd:2.4.43`.
- **`rollout undo`** with no `--to-revision` reverts to the **immediately previous** revision
(revision 1's `2.4.27` template) — exactly "the previous/original version" — and records it as a
new forward revision (3), preserving the audit trail.
## Verify
```bash
# Current image after rollback should be the ORIGINAL 2.4.27
kubectl get deployment httpd-deploy -n devops \
-o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
# Revision history: expect revisions 1, 2, 3 in order
kubectl rollout history deployment/httpd-deploy -n devops
# All pods ready, service exposed on 30008
kubectl get deployment httpd-deploy -n devops
kubectl get service httpd-service -n devops
kubectl get endpoints httpd-service -n devops
```
Expected — after Step 3 the image is back to `httpd:2.4.27`, rollout history shows three
revisions, deployment `READY 2/2`, and `httpd-service` NodePort `80:30008/TCP` with two
endpoints.
> Everything is in `devops` — keep `-n devops` on every command. If endpoints is empty, the
> service selector doesn't match the pod labels.

View File

@@ -0,0 +1,140 @@
# Assignment
The Nautilus DevOps team is planning to set up a Jenkins CI server to create/manage some deployment pipelines for some of the projects. They want to set up the Jenkins server on Kubernetes cluster. Below you can find more details about the task:
1) Create a namespace jenkins
2) Create a Service for jenkins deployment. Service name should be jenkins-service under jenkins namespace, type should be NodePort, nodePort should be 30008
3) Create a Jenkins Deployment under jenkins namespace, It should be name as jenkins-deployment , labels app should be jenkins , container name should be jenkins-container , use jenkins/jenkins image , containerPort should be 8080 and replicas count should be 1. It should also have the environment variable JAVA_OPTS with the value -Djenkins.install.runSetupWizard=false to skip the initial setup wizard.
Make sure to wait for the pods to be in running state and make sure you are able to access the Jenkins UI screen in the browser before hitting the Check button.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
# Solution
# Jenkins on Kubernetes — `jenkins-deployment` + `jenkins-service` in `jenkins`
A Jenkins Deployment exposed via a NodePort Service, with the setup wizard disabled. All objects
in one multi-document heredoc — no manifest file on disk.
## Apply (heredoc → kubectl)
```bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Namespace
metadata:
name: jenkins
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: jenkins-deployment
namespace: jenkins
labels:
app: jenkins
spec:
replicas: 1
selector:
matchLabels:
app: jenkins
template:
metadata:
labels:
app: jenkins
spec:
containers:
- name: jenkins-container
image: jenkins/jenkins
ports:
- containerPort: 8080
env:
- name: JAVA_OPTS
value: "-Djenkins.install.runSetupWizard=false"
---
apiVersion: v1
kind: Service
metadata:
name: jenkins-service
namespace: jenkins
spec:
type: NodePort
selector:
app: jenkins
ports:
- port: 8080
targetPort: 8080
nodePort: 30008
EOF
# Wait for Jenkins to come up (it takes a bit to initialize)
kubectl rollout status deployment/jenkins-deployment -n jenkins
```
## How it works
### The heredoc apply pattern
- **`kubectl apply -f -`** reads from **stdin**; `---` separates the three documents, applied in
order so the **Namespace** exists before the Deployment and Service land in it.
- **`<<'EOF'` (delimiter quoted)** keeps the manifest literal — important here so `JAVA_OPTS`'s
value (with its `-D...` flag) isn't mangled by the jump-host shell.
### The Deployment
- **`namespace: jenkins`, `name: jenkins-deployment`, `labels.app: jenkins`** — exactly as
required.
- **container `name: jenkins-container`, `image: jenkins/jenkins`** — the official Jenkins image.
- **`containerPort: 8080`** — Jenkins' web UI listens on 8080 inside the container.
- **`replicas: 1`** — a single Jenkins instance.
- **`env: JAVA_OPTS = -Djenkins.install.runSetupWizard=false`** — passed to Jenkins' JVM. This
system property **skips the initial setup wizard** (the "unlock Jenkins / install plugins"
screens), so the UI is immediately usable — which is what lets the Check succeed without manual
unlock steps.
- **`selector.matchLabels: app: jenkins`** equals the template labels — the Deployment↔pod link.
### The Service — port 8080 is the key detail
- **`type: NodePort`** exposes Jenkins outside the cluster.
- **`selector: app: jenkins`** targets the deployment's pod by its label.
- **Port mapping:**
- `port: 8080` — the Service's ClusterIP port.
- **`targetPort: 8080`** — the container port. This **must** be 8080 because that's where
Jenkins actually listens; pointing it elsewhere would give you a Service with endpoints but no
response.
- `nodePort: 30008` — the external port on each node (in the valid `3000032767` range).
Path: `<node-ip>:30008` → Service `:8080` → Jenkins pod `:8080`.
### Why waiting matters
Jenkins takes noticeably longer than a typical app to become ready — the JVM boots, unpacks
plugins, and initializes before the UI responds. `kubectl rollout status` blocks until the pod is
Ready, but the **UI** may need a few extra seconds after that. Confirm the login/dashboard
actually loads in the browser before hitting Check, as the task instructs.
## Verify
```bash
# Pod running
kubectl get pods -n jenkins
# Service exposed on 30008 with an endpoint
kubectl get service jenkins-service -n jenkins
kubectl get endpoints jenkins-service -n jenkins
# Jenkins responding inside the cluster (should return HTTP 200/403, not connection refused)
kubectl exec -n jenkins deploy/jenkins-deployment -- \
sh -c 'curl -sI http://localhost:8080 | head -n1' 2>/dev/null || true
```
Expected — the Jenkins pod `Running` (`READY 1/1`), `jenkins-service` NodePort `8080:30008/TCP`
with one endpoint, and the Jenkins dashboard loading at `<node-ip>:30008` in the browser (no setup
wizard, thanks to `JAVA_OPTS`).
> If the browser shows "unlock Jenkins," the `JAVA_OPTS` env var didn't take — confirm with
> `kubectl get deploy jenkins-deployment -n jenkins -o jsonpath='{.spec.template.spec.containers[0].env}'`.
> Give the pod a minute; Jenkins' first boot is slow.

View File

@@ -0,0 +1,169 @@
# Assignment
The Nautilus DevOps teams is planning to set up a Grafana tool to collect and analyze analytics from some applications. They are planning to deploy it on Kubernetes cluster. Below you can find more details.
1.) Create a deployment named grafana-deployment-xfusion using any grafana image for Grafana app. Set other parameters as per your choice.
2.) Create NodePort type service with nodePort 32000 to expose the app.
You do not need to make any configuration changes inside the Grafana app once deployed; just make sure you can access the Grafana login page.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
# Solution
# Grafana on Kubernetes — `grafana-deployment-xfusion` + NodePort Service
A Grafana Deployment exposed via a NodePort Service on `32000`. Both objects in one
multi-document heredoc — no manifest file on disk.
## Apply (heredoc → kubectl)
```bash
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: grafana-deployment-datacenter
labels:
app: grafana
spec:
replicas: 1
selector:
matchLabels:
app: grafana
template:
metadata:
labels:
app: grafana
spec:
containers:
- name: grafana-container
image: grafana/grafana:latest
ports:
- containerPort: 32000
---
apiVersion: v1
kind: Service
metadata:
name: grafana-service
labels:
app: grafana
spec:
type: NodePort
selector:
app: grafana
ports:
- port: 32000
targetPort: 3000
nodePort: 32000
EOF
kubectl rollout status deployment/kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: grafana-deployment-datacenter
labels:
app: grafana
spec:
replicas: 1
selector:
matchLabels:
app: grafana
template:
metadata:
labels:
app: grafana
spec:
containers:
- name: grafana-container
image: grafana/grafana:latest
ports:
- containerPort: 32000
---
apiVersion: v1
kind: Service
metadata:
name: grafana-service
labels:
app: grafana
spec:
type: NodePort
selector:
app: grafana
ports:
- port: 32000
targetPort: 3000
nodePort: 32000
EOF
kubectl rollout status deployment grafana-deployment-datacenter
```
## How it works
### The heredoc apply pattern
- **`kubectl apply -f -`** reads from **stdin**; `---` separates the two documents.
- **`<<'EOF'` (delimiter quoted)** keeps the manifest literal — the right default for k8s YAML.
### The Deployment
- **`name: grafana-deployment-xfusion`, `labels.app: grafana`** — the required name plus a label
used to tie the Service to the pods.
- **`image: grafana/grafana:latest`** — the official Grafana image. The task allows any Grafana
image; the official `grafana/grafana` is the standard pick.
- **`containerPort: 3000`** — Grafana's web UI listens on **3000** inside the container. This is
the detail that everything else keys off.
- **`replicas: 1`** and container name `grafana-container` — free-choice parameters, kept minimal.
- **`selector.matchLabels: app: grafana`** equals the template labels — the Deployment↔pod link.
### The Service — targetPort 3000 is the crux
- **`type: NodePort`** exposes Grafana outside the cluster.
- **`selector: app: grafana`** targets the deployment's pod by its label.
- **Port mapping:**
- `port: 3000` — the Service's ClusterIP port.
- **`targetPort: 3000`** — the container port. This **must** be 3000 because that's where
Grafana listens; pointing it elsewhere yields a Service with an endpoint but no response, and
the login page wouldn't load.
- `nodePort: 32000` — the external port on each node, exactly as required (in the valid
`3000032767` range).
Path: `<node-ip>:32000` → Service `:3000` → Grafana pod `:3000`.
### Why no in-app config is needed
The task only requires reaching the **login page** — Grafana serves its login UI out of the box
on port 3000 with no configuration. So the whole job is: run the container and route a NodePort to
3000. Default admin credentials are `admin` / `admin`, but you don't need to log in — just confirm
the login screen renders.
## Verify
```bash
# Pod running
kubectl get deployment grafana-deployment-xfusion
kubectl get pods -l app=grafana
# Service exposed on 32000 with an endpoint
kubectl get service grafana-service
kubectl get endpoints grafana-service
# Grafana responding inside the cluster
kubectl exec deploy/grafana-deployment-xfusion -- \
sh -c 'wget -qO- http://localhost:3000/login >/dev/null && echo ok' 2>/dev/null || true
```
Expected — deployment `READY 1/1`, `grafana-service` NodePort `3000:32000/TCP` with one endpoint,
and the Grafana **login page** loading at `<node-ip>:32000` in the browser.
> If the page doesn't load, check `targetPort` is `3000` and the pod is Running. Grafana starts
> reasonably fast, but give it a few seconds after `rollout status` reports ready.

View File

@@ -0,0 +1,142 @@
# Assignment
A new java-based application is ready to be deployed on a Kubernetes cluster. The development team had a meeting with the DevOps team to share the requirements and application scope. The team is ready to setup an application stack for it under their existing cluster. Below you can find the details for this:
Create a namespace named tomcat-namespace-datacenter.
Create a deployment for tomcat app which should be named as tomcat-deployment-datacenter under the same namespace you created. Replica count should be 1, the container should be named as tomcat-container-datacenter, its image should be kodekloud/centos-ssh-enabled:tomcat and its container port should be 8080.
Create a service for tomcat app which should be named as tomcat-service-datacenter under the same namespace you created. Service type should be NodePort and nodePort should be 32227.
Before clicking on Check button please make sure the application is up and running.
You can use any labels as per your choice.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
# Solution
# Tomcat on Kubernetes — namespace + deployment + service (`datacenter`)
A Tomcat Deployment and NodePort Service inside a dedicated namespace. All objects in one
multi-document heredoc — no manifest file on disk.
## Apply (heredoc → kubectl)
```bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Namespace
metadata:
name: tomcat-namespace-datacenter
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: tomcat-deployment-datacenter
namespace: tomcat-namespace-datacenter
labels:
app: tomcat
spec:
replicas: 1
selector:
matchLabels:
app: tomcat
template:
metadata:
labels:
app: tomcat
spec:
containers:
- name: tomcat-container-datacenter
image: kodekloud/centos-ssh-enabled:tomcat
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: tomcat-service-datacenter
namespace: tomcat-namespace-datacenter
labels:
app: tomcat
spec:
type: NodePort
selector:
app: tomcat
ports:
- port: 8080
targetPort: 8080
nodePort: 32227
EOF
kubectl rollout status deployment/tomcat-deployment-datacenter -n tomcat-namespace-datacenter
```
## How it works
### The heredoc apply pattern
- **`kubectl apply -f -`** reads from **stdin**; `---` separates the three documents, applied in
order so the **Namespace** exists before the Deployment and Service land in it.
- **`<<'EOF'` (delimiter quoted)** keeps the manifest literal — the right default for k8s YAML.
### Everything scoped to one namespace
All three objects carry `namespace: tomcat-namespace-datacenter` (the Namespace defines it, the
Deployment and Service reference it). Creating the namespace first in the same stream avoids a
`namespaces "..." not found` error. Because the Service and Deployment are in the same namespace,
the Service can select the deployment's pods by label directly.
### The Deployment
- **`name: tomcat-deployment-datacenter`**, container **`name: tomcat-container-datacenter`**,
**`image: kodekloud/centos-ssh-enabled:tomcat`**, **`replicas: 1`** — all exactly as required.
- **`containerPort: 8080`** — Tomcat serves on 8080 inside the container.
- **`app: tomcat`** label (free choice) on the template — ties the Deployment to its pods and the
Service to those pods.
- **`selector.matchLabels: app: tomcat`** equals the template labels — the Deployment↔pod link.
### The Service — targetPort 8080
- **`type: NodePort`** exposes Tomcat outside the cluster.
- **`selector: app: tomcat`** targets the deployment's pod by label.
- **Port mapping:**
- `port: 8080` — the Service's ClusterIP port.
- **`targetPort: 8080`** — the container port; must be 8080 to reach Tomcat.
- `nodePort: 32227` — the external port on each node, exactly as required (valid
`3000032767` range).
Path: `<node-ip>:32227` → Service `:8080` → Tomcat pod `:8080`.
### "Up and running" before Check
`kubectl rollout status` blocks until the pod is Ready. This image bundles Tomcat on a CentOS/SSH
base, so give it a moment to start the Tomcat service after the pod reports Ready, then confirm the
app responds at `<node-ip>:32227`.
## Verify
```bash
# Pod running in the namespace
kubectl get deployment tomcat-deployment-datacenter -n tomcat-namespace-datacenter
kubectl get pods -n tomcat-namespace-datacenter -l app=tomcat
# Service on 32227 with an endpoint
kubectl get service tomcat-service-datacenter -n tomcat-namespace-datacenter
kubectl get endpoints tomcat-service-datacenter -n tomcat-namespace-datacenter
# Tomcat responding
kubectl exec -n tomcat-namespace-datacenter deploy/tomcat-deployment-datacenter -- \
sh -c 'curl -sI http://localhost:8080 | head -n1' 2>/dev/null || true
```
Expected — deployment `READY 1/1`, `tomcat-service-datacenter` NodePort `8080:32227/TCP` with one
endpoint, and Tomcat reachable at `<node-ip>:32227`.
> Everything is in `tomcat-namespace-datacenter` — keep `-n tomcat-namespace-datacenter` on every
> command. If endpoints is empty, the service selector doesn't match the pod labels.

View File

@@ -0,0 +1,130 @@
# Assignment
The Nautilus development team has completed development of one of the node applications, which they are planning to deploy on a Kubernetes cluster. They recently had a meeting with the DevOps team to share their requirements. Based on that, the DevOps team has listed out the exact requirements to deploy the app. Find below more details:
Create a deployment using kodekloud/centos-ssh-enabled:node image, replica count must be 2.
Create a service to expose this app, the service type must be NodePort, targetPort must be 8080 and nodePort should be 30012.
Make sure all the pods are in Running state after the deployment.
You can check the application by clicking on NodeApp button on top bar.
You can use any labels as per your choice.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
# Solution
# Node App on Kubernetes — Deployment + NodePort Service
A 2-replica Node app Deployment exposed via a NodePort Service. Names/labels are free choice; the
required values are the image, replica count, and port mapping. One multi-document heredoc — no
manifest file on disk.
## Apply (heredoc → kubectl)
```bash
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: node-deployment
labels:
app: node-app
spec:
replicas: 2
selector:
matchLabels:
app: node-app
template:
metadata:
labels:
app: node-app
spec:
containers:
- name: node-container
image: kodekloud/centos-ssh-enabled:node
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: node-service
labels:
app: node-app
spec:
type: NodePort
selector:
app: node-app
ports:
- port: 8080
targetPort: 8080
nodePort: 30012
EOF
kubectl rollout status deployment/node-deployment
```
## How it works
### The heredoc apply pattern
- **`kubectl apply -f -`** reads from **stdin**; `---` separates the two documents.
- **`<<'EOF'` (delimiter quoted)** keeps the manifest literal — the right default for k8s YAML.
### The Deployment
- **`image: kodekloud/centos-ssh-enabled:node`** and **`replicas: 2`** — the two hard
requirements. Name (`node-deployment`), container name (`node-container`), and labels
(`app: node-app`) are free choices, kept simple.
- **`containerPort: 8080`** — the Node app listens on 8080 inside the container, matching the
service's `targetPort`.
- **`selector.matchLabels: app: node-app`** equals the template labels — the Deployment↔pod link,
and what the Service selects on.
### The Service — the port mapping is the graded part
- **`type: NodePort`** exposes the app outside the cluster.
- **`selector: app: node-app`** targets the deployment's two pods by label and load-balances
across them.
- **Port mapping:**
- `port: 8080` — the Service's ClusterIP port.
- **`targetPort: 8080`** — the container port, exactly as required; must match where the app
listens.
- `nodePort: 30012` — the external port on each node, exactly as required (valid
`3000032767` range).
Path: `<node-ip>:30012` → Service `:8080` → one of the two pods `:8080`.
### Ensuring all pods are Running (requirement 3)
`kubectl rollout status` blocks until **both** replicas are Ready. Only then is the deployment
fully available and the app reliably reachable through the Service — so wait for it to report
success before hitting the `NodeApp` button.
## Verify
```bash
# Both replicas running
kubectl get deployment node-deployment
kubectl get pods -l app=node-app
# Service on 30012 with two endpoints (one per pod)
kubectl get service node-service
kubectl get endpoints node-service
# App responding inside the cluster
kubectl exec deploy/node-deployment -- \
sh -c 'curl -sI http://localhost:8080 | head -n1' 2>/dev/null || true
```
Expected — deployment `READY 2/2`, both pods `Running`, `node-service` NodePort `8080:30012/TCP`
with **two** endpoints, and the app loading via the `NodeApp` button (`<node-ip>:30012`).
> If endpoints shows fewer than two IPs, a pod isn't Ready yet — give it a moment. Empty endpoints
> means the service selector doesn't match the pod labels.