Goal: Learn Kubernetes fundamentals through hands-on practice with persistent storage, perfect for SRE/DBRE roles.
Prerequisites: Basic Linux knowledge, Docker installed, kubectl CLI, and minikube or kind.
Watch the complete video walkthrough of this tutorial:
🎥 Watch on YouTube: Kubernetes Foundations Tutorial
# Install kubectl (if not already installed)
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv kubectl /usr/local/bin/
# Install minikube
curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
chmod +x minikube
sudo mv minikube /usr/local/bin/
# Or install kind (alternative)
curl -Lo kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64
chmod +x kind
sudo mv kind /usr/local/bin/
# Option 1: Using minikube
minikube start --memory=4096 --cpus=2
kubectl cluster-info
# Option 2: Using kind
kind create cluster --name k8s-lab
kubectl cluster-info
# Verify your cluster is running
kubectl get nodes
Expected Output:
NAME STATUS ROLES AGE VERSION
minikube Ready control-plane 1m v1.28.0
A Pod is the smallest deployable unit in Kubernetes. It can contain one or more containers that share:
# Create a simple nginx pod
kubectl run hello-nginx --image=nginx --port=80
# Check if the pod is running
kubectl get pods
Expected Output:
NAME READY STATUS RESTARTS AGE
hello-nginx 1/1 Running 0 30s
# Get detailed information about the pod
kubectl describe pod hello-nginx
# Check the pod logs
kubectl logs hello-nginx
# Get pod information in YAML format
kubectl get pod hello-nginx -o yaml
# Execute commands inside the pod
kubectl exec -it hello-nginx -- /bin/bash
# Inside the pod, test nginx
curl localhost
# Exit the pod
exit
# Delete the pod
kubectl delete pod hello-nginx
# Verify it's deleted
kubectl get pods
run, get, describe, logs, exec, deleteA Deployment manages ReplicaSets, which ensure a specified number of pod replicas are running. Deployments provide:
# Create a deployment with 3 replicas
kubectl create deployment web-app --image=nginx --replicas=3
# Check the deployment
kubectl get deployments
kubectl get replicasets
kubectl get pods
Expected Output:
NAME READY UP-TO-DATE AVAILABLE AGE
web-app 3/3 3 3 30s
NAME DESIRED CURRENT READY AGE
web-app-7d4f8b9c6 3 3 3 30s
NAME READY STATUS RESTARTS AGE
web-app-7d4f8b9c6-abc123 1/1 Running 0 30s
web-app-7d4f8b9c6-def456 1/1 Running 0 30s
web-app-7d4f8b9c6-ghi789 1/1 Running 0 30s
# Scale up to 5 replicas
kubectl scale deployment web-app --replicas=5
# Check the scaling
kubectl get pods -l app=web-app
# Scale down to 2 replicas
kubectl scale deployment web-app --replicas=2
# Update the image to a different version
kubectl set image deployment/web-app nginx=nginx:1.21
# Watch the rolling update
kubectl rollout status deployment/web-app
# Check rollout history
kubectl rollout history deployment/web-app
# Rollback to previous version
kubectl rollout undo deployment/web-app
# Check the status
kubectl rollout status deployment/web-app
A Service provides a stable network endpoint for pods. Even when pods die and restart, the service IP remains constant.
# Create a service for our web-app
kubectl expose deployment web-app --port=80 --target-port=80 --type=ClusterIP
# Check the service
kubectl get services
kubectl describe service web-app
Expected Output:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web-app ClusterIP 10.96.123.45 <none> 80/TCP 30s
# Get the service IP
kubectl get service web-app
# Test from within the cluster
kubectl run test-pod --image=busybox --rm -it --restart=Never -- wget -qO- http://web-app
# Delete the ClusterIP service
kubectl delete service web-app
# Create a NodePort service
kubectl expose deployment web-app --port=80 --target-port=80 --type=NodePort
# Get the NodePort
kubectl get service web-app
Expected Output:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web-app NodePort 10.96.123.45 <none> 80:30080/TCP 30s
# Get minikube IP
minikube ip
# Access the service (replace with your minikube IP)
curl http://$(minikube ip):30080
# Or use minikube service command
minikube service web-app
PersistentVolumes (PV) and PersistentVolumeClaims (PVC) provide persistent storage that survives pod restarts and deletions.
Create mariadb-pvc.yaml:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mariadb-pvc
labels:
app: mariadb
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
storageClassName: standard
# Apply the PVC
kubectl apply -f mariadb-pvc.yaml
# Check the PVC
kubectl get pvc
kubectl describe pvc mariadb-pvc
Expected Output:
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
mariadb-pvc Bound pvc-12345678-1234-1234-1234-123456789abc 2Gi RWO standard 30s
Create mariadb-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: mariadb
labels:
app: mariadb
spec:
replicas: 1
selector:
matchLabels:
app: mariadb
template:
metadata:
labels:
app: mariadb
spec:
containers:
- name: mariadb
image: mariadb:10.11
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: "rootpassword"
- name: MYSQL_DATABASE
value: "testdb"
- name: MYSQL_USER
value: "testuser"
- name: MYSQL_PASSWORD
value: "testpassword"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
volumeMounts:
- name: mariadb-storage
mountPath: /var/lib/mysql
livenessProbe:
exec:
command:
- mysqladmin
- ping
- -h
- localhost
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- mysql
- -h
- localhost
- -u
- root
- -prootpassword
- -e
- "SELECT 1"
initialDelaySeconds: 5
periodSeconds: 2
volumes:
- name: mariadb-storage
persistentVolumeClaim:
claimName: mariadb-pvc
# Apply the deployment
kubectl apply -f mariadb-deployment.yaml
# Check the deployment
kubectl get pods -l app=mariadb
kubectl get deployments
Create mariadb-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: mariadb
labels:
app: mariadb
spec:
type: ClusterIP
ports:
- port: 3306
targetPort: 3306
protocol: TCP
name: mysql
selector:
app: mariadb
# Apply the service
kubectl apply -f mariadb-service.yaml
# Check the service
kubectl get services
# Wait for MariaDB to be ready
kubectl wait --for=condition=ready pod -l app=mariadb --timeout=60s
# Connect to MariaDB and create some data
kubectl exec -it $(kubectl get pods -l app=mariadb -o jsonpath='{.items[0].metadata.name}') -- mysql -u root -prootpassword -e "
CREATE DATABASE persistence_test;
USE persistence_test;
CREATE TABLE test_table (id INT PRIMARY KEY, message VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
INSERT INTO test_table (id, message) VALUES (1, 'Hello Kubernetes Persistence!');
INSERT INTO test_table (id, message) VALUES (2, 'Data should survive pod restarts');
SELECT * FROM test_table;
"
Expected Output:
+----+----------------------------------+---------------------+
| id | message | created_at |
+----+----------------------------------+---------------------+
| 1 | Hello Kubernetes Persistence! | 2024-01-15 10:30:00 |
| 2 | Data should survive pod restarts | 2024-01-15 10:30:01 |
+----+----------------------------------+---------------------+
# Delete the pod to test persistence
kubectl delete pod $(kubectl get pods -l app=mariadb -o jsonpath='{.items[0].metadata.name}')
# Wait for new pod to be ready
kubectl wait --for=condition=ready pod -l app=mariadb --timeout=60s
# Check if data persisted
kubectl exec -it $(kubectl get pods -l app=mariadb -o jsonpath='{.items[0].metadata.name}') -- mysql -u root -prootpassword -e "
USE persistence_test;
SELECT * FROM test_table;
"
Expected Output:
+----+----------------------------------+---------------------+
| id | message | created_at |
+----+----------------------------------+---------------------+
| 1 | Hello Kubernetes Persistence! | 2024-01-15 10:30:00 |
| 2 | Data should survive pod restarts | 2024-01-15 10:30:01 |
+----+----------------------------------+---------------------+
StatefulSets are ideal for stateful applications that need:
Create mariadb-statefulset.yaml:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mariadb-statefulset
labels:
app: mariadb-statefulset
spec:
serviceName: mariadb-statefulset
replicas: 1
selector:
matchLabels:
app: mariadb-statefulset
template:
metadata:
labels:
app: mariadb-statefulset
spec:
containers:
- name: mariadb
image: mariadb:10.11
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: "rootpassword"
- name: MYSQL_DATABASE
value: "testdb"
- name: MYSQL_USER
value: "testuser"
- name: MYSQL_PASSWORD
value: "testpassword"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
volumeMounts:
- name: mariadb-data
mountPath: /var/lib/mysql
livenessProbe:
exec:
command:
- mysqladmin
- ping
- -h
- localhost
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- mysql
- -h
- localhost
- -u
- root
- -prootpassword
- -e
- "SELECT 1"
initialDelaySeconds: 5
periodSeconds: 2
volumeClaimTemplates:
- metadata:
name: mariadb-data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 2Gi
Create mariadb-statefulset-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: mariadb-statefulset
labels:
app: mariadb-statefulset
spec:
clusterIP: None
ports:
- port: 3306
targetPort: 3306
protocol: TCP
name: mysql
selector:
app: mariadb-statefulset
# Apply the StatefulSet and Service
kubectl apply -f mariadb-statefulset.yaml
kubectl apply -f mariadb-statefulset-service.yaml
# Check the StatefulSet
kubectl get statefulsets
kubectl get pods -l app=mariadb-statefulset
kubectl get pvc
Expected Output:
NAME READY AGE
mariadb-statefulset 1/1 30s
NAME READY STATUS RESTARTS AGE
mariadb-statefulset-0 1/1 Running 0 30s
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
mariadb-data-mariadb-statefulset-0 Bound pvc-12345678-1234-1234-1234-123456789def 2Gi RWO standard 30s
# Check pod names
kubectl get pods -l app=mariadb
kubectl get pods -l app=mariadb-statefulset
# Check services
kubectl get services
# Check persistent volumes
kubectl get pv
kubectl get pvc
| Feature | Deployment | StatefulSet |
|---|---|---|
| Pod Naming | Random (web-app-abc123) | Ordered (mariadb-statefulset-0) |
| Storage | Shared PVC | Individual PVC per pod |
| Scaling | Parallel | Ordered (0, 1, 2…) |
| Network | Service IP | Stable network identity |
| Use Case | Stateless apps | Stateful apps (databases) |
# Create ConfigMap from literal values
kubectl create configmap app-config \
--from-literal=LOG_LEVEL=DEBUG \
--from-literal=DATABASE_NAME=testdb \
--from-literal=MAX_CONNECTIONS=100
# Check the ConfigMap
kubectl get configmaps
kubectl describe configmap app-config
kubectl get configmap app-config -o yaml
# Create Secret from literal values
kubectl create secret generic db-secret \
--from-literal=username=admin \
--from-literal=password=secretpassword123
# Check the Secret
kubectl get secrets
kubectl describe secret db-secret
kubectl get secret db-secret -o yaml
Create mariadb-with-config.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: mariadb-with-config
labels:
app: mariadb-with-config
spec:
replicas: 1
selector:
matchLabels:
app: mariadb-with-config
template:
metadata:
labels:
app: mariadb-with-config
spec:
containers:
- name: mariadb
image: mariadb:10.11
ports:
- containerPort: 3306
env:
# Environment variables from ConfigMap
- name: MYSQL_DATABASE
valueFrom:
configMapKeyRef:
name: app-config
key: DATABASE_NAME
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: LOG_LEVEL
# Environment variables from Secret
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
- name: MYSQL_USER
valueFrom:
secretKeyRef:
name: db-secret
key: username
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
volumeMounts:
- name: mariadb-storage
mountPath: /var/lib/mysql
volumes:
- name: mariadb-storage
persistentVolumeClaim:
claimName: mariadb-pvc
# Apply the deployment
kubectl apply -f mariadb-with-config.yaml
# Check if it's using the ConfigMap and Secret
kubectl describe pod $(kubectl get pods -l app=mariadb-with-config -o jsonpath='{.items[0].metadata.name}')
# Check pod status
kubectl get pods -A
# Get detailed pod information
kubectl describe pod <pod-name>
# Check pod logs
kubectl logs <pod-name>
kubectl logs <pod-name> --previous # Previous container logs
# Execute commands in pod
kubectl exec -it <pod-name> -- /bin/bash
# Check events
kubectl get events --sort-by=.metadata.creationTimestamp
# Check resource usage
kubectl top pods
kubectl top nodes
# Create a deployment with wrong image
kubectl create deployment broken-app --image=nginx:invalid-tag
# Check the status
kubectl get pods -l app=broken-app
kubectl describe pod $(kubectl get pods -l app=broken-app -o jsonpath='{.items[0].metadata.name}')
# Check events for errors
kubectl get events --sort-by=.metadata.creationTimestamp | grep broken-app
# Check pod status
kubectl get pods -l app=broken-app -o wide
# Get detailed pod information
kubectl describe pod $(kubectl get pods -l app=broken-app -o jsonpath='{.items[0].metadata.name}')
# Update the image to a valid one
kubectl set image deployment/broken-app nginx=nginx:latest
# Check if it's fixed
kubectl get pods -l app=broken-app
kubectl rollout status deployment/broken-app
# Check resource usage
kubectl top pods
kubectl top nodes
# Check pod resource limits
kubectl describe pod <pod-name> | grep -A 5 -B 5 "Limits\|Requests"
# Check cluster information
kubectl cluster-info
kubectl get nodes -o wide
# Check all resources
kubectl get all
# Check why pod is pending
kubectl describe pod <pod-name>
# Common causes:
# - Insufficient resources
# - No available nodes
# - PVC not bound
# Check pod logs
kubectl logs <pod-name>
kubectl logs <pod-name> --previous
# Check events
kubectl get events --sort-by=.metadata.creationTimestamp
# Common causes:
# - Application errors
# - Resource limits exceeded
# - Configuration issues
# Check service endpoints
kubectl get endpoints <service-name>
# Check service selector
kubectl get pods -l <selector>
# Test service connectivity
kubectl run test-pod --image=busybox --rm -it --restart=Never -- wget -qO- http://<service-name>
# Check PVC status
kubectl get pvc
kubectl describe pvc <pvc-name>
# Check PV status
kubectl get pv
kubectl describe pv <pv-name>
# Check storage class
kubectl get storageclass
#!/bin/bash
# Quick cluster health check
echo "=== Cluster Status ==="
kubectl get nodes
echo ""
echo "=== Pod Status ==="
kubectl get pods -A
echo ""
echo "=== Service Status ==="
kubectl get services
echo ""
echo "=== Storage Status ==="
kubectl get pv,pvc
echo ""
echo "=== Recent Events ==="
kubectl get events --sort-by=.metadata.creationTimestamp | tail -10
# Basic operations
kubectl get <resource>
kubectl describe <resource> <name>
kubectl logs <pod-name>
kubectl exec -it <pod-name> -- <command>
# Deployments
kubectl create deployment <name> --image=<image>
kubectl scale deployment <name> --replicas=<number>
kubectl rollout status deployment <name>
# Services
kubectl expose deployment <name> --port=<port> --target-port=<port>
# Storage
kubectl get pv,pvc
kubectl describe pvc <name>
# Debugging
kubectl get events --sort-by=.metadata.creationTimestamp
kubectl top pods
kubectl top nodes