docs: add Kubernetes CKS study notes
This commit is contained in:
141
kubernetes/level 2/task-1.md
Normal file
141
kubernetes/level 2/task-1.md
Normal 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}'`.
|
||||
120
kubernetes/level 2/task-10.md
Normal file
120
kubernetes/level 2/task-10.md
Normal 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.
|
||||
143
kubernetes/level 2/task-11.md
Normal file
143
kubernetes/level 2/task-11.md
Normal 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.
|
||||
120
kubernetes/level 2/task-2.md
Normal file
120
kubernetes/level 2/task-2.md
Normal 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.
|
||||
121
kubernetes/level 2/task-3.md
Normal file
121
kubernetes/level 2/task-3.md
Normal 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 (`30000–32767`), 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`).
|
||||
123
kubernetes/level 2/task-4.md
Normal file
123
kubernetes/level 2/task-4.md
Normal 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.
|
||||
163
kubernetes/level 2/task-5.md
Normal file
163
kubernetes/level 2/task-5.md
Normal 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
|
||||
(`30000–32767`).
|
||||
|
||||
### `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.
|
||||
140
kubernetes/level 2/task-6.md
Normal file
140
kubernetes/level 2/task-6.md
Normal 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 `30000–32767` 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.
|
||||
169
kubernetes/level 2/task-7.md
Normal file
169
kubernetes/level 2/task-7.md
Normal 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
|
||||
`30000–32767` 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.
|
||||
142
kubernetes/level 2/task-8.md
Normal file
142
kubernetes/level 2/task-8.md
Normal 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
|
||||
`30000–32767` 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.
|
||||
130
kubernetes/level 2/task-9.md
Normal file
130
kubernetes/level 2/task-9.md
Normal 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
|
||||
`30000–32767` 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.
|
||||
Reference in New Issue
Block a user