Kubernetes

🧪 Lab — Working With Kubernetes & Minikube

Duration: ~1 Goal: Learn how to start a local Kubernetes cluster with Minikube, deploy workloads using YAML, interact with Pods/Jobs/CronJobs, and understand how storage shares data between workloads.


📘 Learning Objectives

By the end of this lab, you will be able to:

  • Install kubectl and minikube (Windows / macOS / Ubuntu)
  • Start a local Kubernetes cluster and verify it’s healthy
  • Deploy a Pod from a YAML manifest and interact with it
  • Use kubectl port-forward, logs, exec, and cp
  • Run a Job and confirm it completed successfully
  • Use a CronJob to refresh content on a schedule
  • Use a PersistentVolumeClaim (PVC) to share data between workloads
  • Use a Deployment with an initContainer to enforce startup ordering

1️⃣ Install Minikube + kubectl

You need kubectl (Kubernetes CLI) and minikube (local single-node Kubernetes).
Minikube also needs a “driver” (Docker is easiest on all OSes).


🪟 Windows 10/11

1) Install kubectl

Use the official kubectl guide for Windows (recommended method is downloading the binary or using a package manager).

Quick option (Winget):

winget install Kubernetes.kubectl

Verify:

kubectl version --client

2) Install Minikube

Option A (Winget):

winget install Kubernetes.minikube

Option B (Chocolatey):

choco install minikube

Verify:

minikube version

Install Docker Desktop first (WSL2 recommended). Then:

minikube start --driver=docker
kubectl get nodes

🍎 macOS

1) Install kubectl

If you use Homebrew:

brew install kubectl
kubectl version --client

2) Install Minikube

Using Homebrew:

brew install minikube
minikube version

3) Start a local cluster

If you have Docker installed:

minikube start --driver=docker
kubectl get nodes

🐧 Ubuntu (22.04/24.04)

1) Install kubectl

Option A (snap):

sudo snap install kubectl --classic
kubectl version --client

2) Install Minikube

Binary install (amd64):

curl -LO https://github.com/kubernetes/minikube/releases/latest/download/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
minikube version

3) Start a local cluster

If Docker is installed:

minikube start --driver=docker
kubectl get nodes

✅ Quick Cluster Health Check (all OS)

kubectl get nodes
kubectl get pods -A
minikube status

🧩 Part A — Nginx Pod & Basic Kubectl Commands

Provide the YAML that runs a Pod with Nginx 1.27.4-alpine as well as the kubectl commands needed to:

YAML manifest: Nginx 1.27.4-alpine Pod

Create a file named nginx-pod.yaml:

apiVersion: v1
kind: Pod
metadata:
  name: nginx-alpine
  labels:
    app: nginx-alpine
spec:
  containers:
    - name: nginx
      image: nginx:1.27.4-alpine
      ports:
        - containerPort: 80

1. Start the Pod

kubectl apply -f nginx-pod.yaml
kubectl get pods
kubectl describe pod nginx-alpine

Wait until it’s Running:


2. Forward port 80 locally and test in browser/curl

Forward local port 8000 → Pod port 80:

kubectl port-forward pod/nginx-alpine 8000:80

In a second terminal:

curl http://localhost:8000/

Expected answer: the default Nginx welcome page HTML (it typically contains “Welcome to nginx!”).

Stop port-forward with Ctrl+C when done.


3. See logs of the running container

kubectl logs nginx-alpine

4. Open a shell session inside the container and edit the default page

Open a shell in the container (Alpine uses sh, not bash):

kubectl exec -it nginx-alpine -- sh

Edit the first sentence in /usr/share/nginx/html/index.html:

The container is minimal (no nano). Use sed:

cd /usr/share/nginx/html

# Replace the first occurrence of the “Welcome to nginx!” text
sed -i 's/Welcome to nginx!/Welcome to MY nginx!/g' index.html

exit

Validate:

kubectl port-forward pod/nginx-alpine 8000:80
curl http://localhost:8000/ | head

5. From your computer terminal: download page locally, upload a new one

Download the page locally:

kubectl port-forward pod/nginx-alpine 8000:80
curl http://localhost:8000/index.html -o index.html

Create a replacement page (example):

cat > index.html <<'EOF'
<!DOCTYPE html>
<html>
  <body>
    <h1>Replaced from my computer!</h1>
    <p>Uploaded using kubectl cp.</p>
  </body>
</html>
EOF

Upload it into the Pod:

kubectl cp ./index.html nginx-alpine:/usr/share/nginx/html/index.html

Validate:

kubectl port-forward pod/nginx-alpine 8000:80
curl http://localhost:8000/

6. Stop the Pod and remove it from Kubernetes

A Pod created directly is removed by deleting it (or deleting the YAML):

kubectl delete -f nginx-pod.yaml
# or:
kubectl delete pod nginx-alpine

Confirm it’s gone:

kubectl get pods

🧩 Part B: Job + ConfigMap Script + CronJob + Shared Volume + Deployment + initContainer

Note: PVC –> PersistentVolumeClaim

In this part, you’ll download a website using: wget -E -k -p <url> You will share the downloaded content with Nginx using a PVC. Minikube is usually single-node, so ReadWriteOnce PVCs work well for sharing between Pods scheduled on the same node.


1. YAML: Job (Ubuntu 24.04) that downloads ceid.upatras.gr using a ConfigMap script

Create site-downloader-job.yaml:

apiVersion: v1
kind: ConfigMap
metadata:
  name: download-script
data:
  download.sh: |
    #!/usr/bin/env bash

    # -e  : exit immediately if a command fails
    # -u  : treat unset variables as an error
    # -o pipefail : fail if any command in a pipeline fails
    set -euo pipefail

    # Install tools needed to download HTTPS content
    apt-get update
    apt-get install -y --no-install-recommends wget ca-certificates
    rm -rf /var/lib/apt/lists/*

    # /data is the PVC mount. Anything written here persists and is shared with Nginx.
    mkdir -p /data

    # Download the website snapshot to /data_tmp
    # -p: download prerequisites (css/js/images)
    # -k: convert links for offline browsing
    # -E: add .html extension when needed
    # -nH: do NOT create a host folder like /data_tmp/ceid.upatras.gr/
    wget -E -k -p -nH https://ceid.upatras.gr/ -P /data

    echo "Download finished."    
---
apiVersion: batch/v1
kind: Job
metadata:
  name: download-ceid-upatras
spec:
  backoffLimit: 1
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: ubuntu
          image: ubuntu:24.04
          command: ["/bin/bash", "/scripts/download.sh"]
          volumeMounts:
            - name: script
              mountPath: /scripts
      volumes:
        - name: script
          configMap:
            name: download-script
            defaultMode: 0755

Apply:

kubectl apply -f site-downloader-job.yaml
kubectl get jobs
kubectl get pods

2. Which command confirms the Job completed successfully?

kubectl get job download-ceid-upatras

(You can also check logs from the Job Pod)

kubectl logs job/download-ceid-upatras

3. Download Ceid Upatras page and serve it through a container

  • PVC for persistent shared storage (site-pvc)
  • ConfigMap that stores a download script (download-script)
  • Job to do an initial download (download-ceid-upatras)
  • Nginx Pod that serves the downloaded content (nginx-site)
  • CronJob that refreshes the content nightly (refresh-ceid-upatras-nightly)

✅ Big Picture: What’s the goal?

You want Kubernetes to:

  1. Download the web site https://ceid.upatras.gr/ into a persistent volume.
  2. Serve the downloaded static files through Nginx.
  3. Refresh (re-download) the content automatically every night at 02:15.

This is achieved by having a downloader workload (Job/CronJob) and the Nginx workload share the same persistent volume.


🔁 Data Flow (the key idea)

Shared storage path mapping

  • Downloader containers mount the PVC at: /data
  • Nginx mounts the same PVC at: /usr/share/nginx/html

So the directory layout is:

  • Downloader writes: /data/index.html, /data/somefile.css, …
  • Nginx reads and serves: /usr/share/nginx/html/index.html, /usr/share/nginx/html/somefile.css, …

These are the same underlying files, because both are backed by the same PVC.


🧱 Why you need a PVC

Pods and containers are ephemeral:

  • If the Pod restarts or is recreated, its container filesystem returns to the image default.

A PersistentVolumeClaim (PVC) gives you:

  • data that persists beyond Pod lifetime
  • a shared filesystem that multiple Pods can mount (within access mode limits)

In Minikube, PVCs typically work well for this kind of local lab.


🧩 Component-by-component logic

1) PersistentVolumeClaim — site-pvc

Purpose: allocate storage that will hold the mirrored website.

  • ReadWriteOnce means: the volume can be mounted read/write by Pods on a single node.
  • In Minikube (single node), that’s fine.

2) ConfigMap — download-script

Purpose: store the download script as data in Kubernetes.

Why a ConfigMap?

  • It keeps the script versioned in YAML
  • You don’t need to bake a custom Docker image just to run a script
  • Any Pod can mount it and run it

The script is mounted inside Pods at /scripts/download.sh.


3) The download script logic

This is what the script does, in order:

  1. Install dependencies
    It installs wget and ca-certificates inside the Ubuntu container so HTTPS downloads work.

    apt-get update
    apt-get install -y --no-install-recommends wget ca-certificates
    
  2. Download the website into /data

    wget -E -k -p -nH https://ceid.upatras.gr/ -P /data
    

    Flags meaning:

    • -p : download all page prerequisites (images/css/js)
    • -k : convert links for local browsing
    • -E : add .html extension when appropriate
    • -nH : do not create a host folder like /data_tmp/ceid.upatras.gr/
    • -P : destination folder

4) Job — download-ceid-upatras

Purpose: run once (manually / on apply) to populate the PVC immediately.

This is useful because:

  • CronJob runs only at 02:15.
  • You want the site available right away for testing.

The Job:

  • mounts the script ConfigMap
  • mounts the PVC at /data
  • runs /scripts/download.sh

You can confirm completion with:

kubectl get job download-ceid-upatras
kubectl wait --for=condition=complete job/download-ceid-upatras --timeout=10m

5) Nginx Pod — nginx-site

Purpose: serve the downloaded content.

Nginx serves static files from:

  • /usr/share/nginx/html

You mount the PVC there:

  • mountPath: /usr/share/nginx/html

So Nginx serves whatever the Job/CronJob downloaded.

Because it’s readOnly: true, Nginx cannot accidentally modify the site files.

Test it with:

kubectl port-forward pod/nginx-site 8080:80
curl http://localhost:8080/

6) CronJob — refresh-ceid-upatras-nightly

Purpose: refresh the website every night automatically.

Schedule:

schedule: "15 2 * * *"

This means:

  • every day
  • at 02:15 (cluster time)

CronJob creates a Job each run, which:

  • mounts the same PVC
  • runs the same script
  • updates the content served by Nginx

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: site-pvc
spec:
  # In Minikube this usually maps to a local storage provisioner.
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: download-script
data:
  download.sh: |
    #!/usr/bin/env bash

    # -e  : exit immediately if a command fails
    # -u  : treat unset variables as an error
    # -o pipefail : fail if any command in a pipeline fails
    set -euo pipefail

    # Install tools needed to download HTTPS content
    apt-get update
    apt-get install -y --no-install-recommends wget ca-certificates
    rm -rf /var/lib/apt/lists/*

    # /data is the PVC mount. Anything written here persists and is shared with Nginx.
    mkdir -p /data

    # Download the website snapshot to /data_tmp
    # -p: download prerequisites (css/js/images)
    # -k: convert links for offline browsing
    # -E: add .html extension when needed
    # -nH: do NOT create a host folder like /data_tmp/ceid.upatras.gr/
    wget -E -k -p -nH https://ceid.upatras.gr/ -P /data

    echo "Download finished."    
---
apiVersion: batch/v1
kind: Job
metadata:
  name: download-ceid-upatras
spec:
  backoffLimit: 1
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: ubuntu
          image: ubuntu:24.04
          # Run the script that is stored in the ConfigMap and mounted at /scripts/download.sh
          command: ["/bin/bash", "/scripts/download.sh"]
          volumeMounts:
            # Mount the ConfigMap containing download.sh
            - name: script
              mountPath: /scripts
            # Mount the PVC at /data so downloaded files persist and can be shared with Nginx
            - name: site
              mountPath: /data
      volumes:
        - name: script
          configMap:
            name: download-script
            # Make the script executable (0755 decimal is 493)
            defaultMode: 0755
        - name: site
          persistentVolumeClaim:
            claimName: site-pvc
---
apiVersion: v1
kind: Pod
metadata:
  name: nginx-site
  labels:
    app: nginx-site
spec:
  containers:
    - name: nginx
      image: nginx:1.27.4-alpine
      ports:
        - containerPort: 80
      volumeMounts:
        # Serve the PVC content instead of the default nginx html folder content.
        - name: site
          mountPath: /usr/share/nginx/html
          readOnly: true
  volumes:
    - name: site
      persistentVolumeClaim:
        claimName: site-pvc
---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: refresh-ceid-upatras-nightly
spec:
  # Run every day at 02:15
  schedule: "15 2 * * *"
  successfulJobsHistoryLimit: 1
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      backoffLimit: 1
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: ubuntu
              image: ubuntu:24.04
              # Reuse the exact same script to refresh content nightly
              command: ["/bin/bash", "/scripts/download.sh"]
              volumeMounts:
                - name: script
                  mountPath: /scripts
                - name: site
                  mountPath: /data
          volumes:
            - name: script
              configMap:
                name: download-script
                defaultMode: 0755
            - name: site
              persistentVolumeClaim:
                claimName: site-pvc

Previous