Introduction
In Part 2, we containerized our Flask application with Docker. A single container running on a single machine works for development, but production demands more: high availability, horizontal scaling, self-healing, rolling updates, and service discovery. This is where container orchestration enters the picture.
Why Kubernetes Won
In the early days of container orchestration, several platforms competed: Docker Swarm, Apache Mesos, Nomad, and Kubernetes. Kubernetes, originally developed by Google based on their internal Borg system, won because of:
- Declarative configuration — You describe desired state; K8s reconciles actual state
- Extensibility — Custom Resource Definitions (CRDs) and operators enable infinite extensibility
- Community — The largest open-source community (CNCF ecosystem) with thousands of integrations
- Cloud provider support — Every major cloud offers managed Kubernetes (EKS, AKS, GKE)
- Self-healing — Automatic restart, rescheduling, and replacement of failed containers
Kubernetes Architecture
A Kubernetes cluster consists of a control plane (the brain) and one or more worker nodes (the muscles). Understanding this architecture is essential for debugging, tuning, and operating production clusters.
flowchart TB
subgraph CP["Control Plane"]
API["API Server
(kube-apiserver)"]
ETCD["etcd
(Cluster Store)"]
SCHED["Scheduler
(kube-scheduler)"]
CM["Controller Manager
(kube-controller-manager)"]
end
subgraph W1["Worker Node 1"]
KL1["kubelet"]
KP1["kube-proxy"]
CR1["Container Runtime"]
P1["Pod A"]
P2["Pod B"]
end
subgraph W2["Worker Node 2"]
KL2["kubelet"]
KP2["kube-proxy"]
CR2["Container Runtime"]
P3["Pod C"]
P4["Pod D"]
end
API --> ETCD
API --> SCHED
API --> CM
KL1 --> API
KL2 --> API
KP1 --> API
KP2 --> API
KL1 --> CR1
KL2 --> CR2
CR1 --> P1
CR1 --> P2
CR2 --> P3
CR2 --> P4
Control Plane Components
The control plane makes global decisions about the cluster and detects and responds to cluster events:
- API Server (kube-apiserver) — The front door to the cluster. All communication goes through the API server, including kubectl commands, node communication, and controller queries. It validates and processes RESTful requests.
- etcd — A distributed key-value store that holds all cluster state. Every resource definition, configuration, and secret is persisted here. It's the single source of truth.
- Scheduler (kube-scheduler) — Watches for newly created Pods with no assigned node and selects an optimal node based on resource requirements, affinity/anti-affinity, taints/tolerations, and topology constraints.
- Controller Manager — Runs controller loops that watch cluster state and make changes to move current state toward desired state. Includes the Deployment controller, ReplicaSet controller, Node controller, and Job controller.
Worker Node Components
- kubelet — An agent on every node that ensures containers described in PodSpecs are running and healthy. Reports node status back to the API server.
- kube-proxy — Maintains network rules on nodes, implementing Service abstraction by managing iptables/IPVS rules for routing traffic to the correct Pod endpoints.
- Container Runtime — The software responsible for running containers. Kubernetes supports containerd, CRI-O, and any CRI-compliant runtime (Docker was deprecated as a runtime in K8s 1.24).
Setting Up a Local Cluster
For local development, you have several options for running Kubernetes:
| Tool | Best For | Multi-Node | Resource Usage |
|---|---|---|---|
| Kind | CI/CD, testing, multi-node | Yes | Low |
| Minikube | Learning, add-ons ecosystem | Yes (experimental) | Medium |
| Docker Desktop | Quick single-node | No | High |
| k3d | Lightweight, fast startup | Yes | Low |
Kind (Kubernetes in Docker) Setup
Kind runs Kubernetes nodes as Docker containers — it's fast, lightweight, and perfect for development and CI pipelines.
# Install Kind (macOS/Linux)
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
# Verify installation
kind --version
# Create a multi-node cluster with a config file
cat > kind-config.yaml << 'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings:
- containerPort: 80
hostPort: 80
protocol: TCP
- containerPort: 443
hostPort: 443
protocol: TCP
- role: worker
- role: worker
EOF
# Create the cluster
kind create cluster --name devops-lab --config kind-config.yaml
# Verify cluster is running
kubectl cluster-info --context kind-devops-lab
kubectl get nodes
Create Your First Cluster
Install Kind, create a 3-node cluster using the config above, and verify all nodes are in Ready state. Run kubectl get nodes -o wide to see node IPs and container runtime versions. Try docker ps to see the Kind containers that represent your Kubernetes nodes.
Minikube Alternative
# Install Minikube
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
# Start a cluster with specific resources
minikube start --cpus=4 --memory=8192 --driver=docker
# Enable useful add-ons
minikube addons enable ingress
minikube addons enable metrics-server
minikube addons enable dashboard
# Open the Kubernetes dashboard
minikube dashboard
kubectl Essentials
kubectl is the command-line tool for interacting with Kubernetes clusters. It communicates with the API server and is your primary interface for managing resources.
Configuration & Contexts
kubectl uses a kubeconfig file (default: ~/.kube/config) to store cluster connection details. Contexts allow you to switch between multiple clusters.
# View current configuration
kubectl config view
# List all contexts
kubectl config get-contexts
# Switch to a specific context
kubectl config use-context kind-devops-lab
# Set a default namespace for the current context
kubectl config set-context --current --namespace=my-app
# View which cluster you're connected to
kubectl cluster-info
Essential Commands
# Get resources (pods, services, deployments, etc.)
kubectl get pods # List pods in current namespace
kubectl get pods -A # List pods in ALL namespaces
kubectl get pods -o wide # Extended output with node info
kubectl get pods -o yaml # Full YAML output
kubectl get all # Get all resource types
# Describe a resource (detailed info + events)
kubectl describe pod my-pod
kubectl describe node worker-1
# Create resources from YAML
kubectl apply -f deployment.yaml # Create or update
kubectl create -f deployment.yaml # Create only (fails if exists)
# Delete resources
kubectl delete -f deployment.yaml # Delete from file
kubectl delete pod my-pod # Delete by name
kubectl delete pods --all -n my-namespace # Delete all pods in namespace
# Logs and debugging
kubectl logs my-pod # View pod logs
kubectl logs my-pod -c my-container # Specific container in multi-container pod
kubectl logs -f my-pod # Follow/stream logs
kubectl exec -it my-pod -- /bin/bash # Interactive shell in pod
# Output formats
kubectl get pods -o json # JSON output
kubectl get pods -o jsonpath='{.items[*].metadata.name}' # JSONPath
kubectl get pods -o custom-columns=NAME:.metadata.name,STATUS:.status.phase
alias k=kubectl, alias kgp="kubectl get pods", alias kaf="kubectl apply -f". Also install kubectx and kubens for fast context and namespace switching.
Pods & Deployments
Pod Specification
A Pod is the smallest deployable unit in Kubernetes — a group of one or more containers that share storage, network, and a specification for how to run. In practice, you rarely create Pods directly; you use higher-level controllers like Deployments.
stateDiagram-v2
[*] --> Pending: Pod Created
Pending --> Running: Containers Started
Running --> Succeeded: All Containers Exit 0
Running --> Failed: Container Exits Non-Zero
Running --> Unknown: Node Communication Lost
Pending --> Failed: Image Pull Error
Succeeded --> [*]
Failed --> [*]
# pod-example.yaml - A standalone Pod (for learning purposes)
apiVersion: v1
kind: Pod
metadata:
name: flask-app-pod
labels:
app: flask-app
environment: development
spec:
containers:
- name: flask
image: flask-app:1.0
ports:
- containerPort: 5000
protocol: TCP
env:
- name: FLASK_ENV
value: "development"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: connection-string
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /ready
port: 5000
initialDelaySeconds: 5
periodSeconds: 10
restartPolicy: Always
Deployments & Rolling Updates
A Deployment manages a ReplicaSet, which in turn manages Pods. This hierarchy enables declarative updates, rollbacks, and scaling:
# deployment.yaml - Production-ready Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: flask-app
labels:
app: flask-app
spec:
replicas: 3
selector:
matchLabels:
app: flask-app
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # Max pods above desired count during update
maxUnavailable: 0 # Zero downtime - never go below desired count
template:
metadata:
labels:
app: flask-app
version: "1.0"
spec:
containers:
- name: flask
image: flask-app:1.0
ports:
- containerPort: 5000
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /ready
port: 5000
initialDelaySeconds: 5
periodSeconds: 10
# Apply the deployment
kubectl apply -f deployment.yaml
# Watch the rollout progress
kubectl rollout status deployment/flask-app
# Scale the deployment
kubectl scale deployment/flask-app --replicas=5
# Update the image (triggers rolling update)
kubectl set image deployment/flask-app flask=flask-app:2.0
# View rollout history
kubectl rollout history deployment/flask-app
# Rollback to previous version
kubectl rollout undo deployment/flask-app
# Rollback to a specific revision
kubectl rollout undo deployment/flask-app --to-revision=2
maxSurge: 1 and maxUnavailable: 0, Kubernetes creates one new Pod, waits for it to pass readiness checks, then terminates one old Pod. This ensures zero-downtime deployments but takes longer to complete.
Services & Networking
Pods are ephemeral — they get IP addresses dynamically and can be rescheduled to different nodes. A Service provides a stable network endpoint that routes traffic to the correct set of Pods, regardless of their actual IP addresses.
Service Types
flowchart LR
subgraph External["External Traffic"]
CLIENT["Client"]
end
subgraph Cluster["Kubernetes Cluster"]
LB["LoadBalancer
:80"]
NP["NodePort
:30080"]
CIP["ClusterIP
:5000"]
P1["Pod 1"]
P2["Pod 2"]
P3["Pod 3"]
end
CLIENT --> LB
CLIENT --> NP
LB --> CIP
NP --> CIP
CIP --> P1
CIP --> P2
CIP --> P3
# service-clusterip.yaml - Internal-only access
apiVersion: v1
kind: Service
metadata:
name: flask-app-service
labels:
app: flask-app
spec:
type: ClusterIP
selector:
app: flask-app
ports:
- protocol: TCP
port: 80 # Service port (what other pods connect to)
targetPort: 5000 # Container port (where the app listens)
name: http
---
# service-nodeport.yaml - Accessible on each node's IP
apiVersion: v1
kind: Service
metadata:
name: flask-app-nodeport
spec:
type: NodePort
selector:
app: flask-app
ports:
- protocol: TCP
port: 80
targetPort: 5000
nodePort: 30080 # Accessible on :30080
---
# service-loadbalancer.yaml - Cloud load balancer provisioned
apiVersion: v1
kind: Service
metadata:
name: flask-app-lb
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
spec:
type: LoadBalancer
selector:
app: flask-app
ports:
- protocol: TCP
port: 80
targetPort: 5000
Service Discovery & DNS
Kubernetes includes a built-in DNS server (CoreDNS) that automatically creates DNS records for Services:
flask-app-service— resolves within the same namespaceflask-app-service.my-namespace— resolves from any namespaceflask-app-service.my-namespace.svc.cluster.local— fully qualified domain name (FQDN)
# Test DNS resolution from inside a pod
kubectl run dns-test --image=busybox:1.36 --rm -it --restart=Never -- nslookup flask-app-service
# Output:
# Server: 10.96.0.10
# Address: 10.96.0.10:53
# Name: flask-app-service.default.svc.cluster.local
# Address: 10.96.45.123
# Verify service endpoints
kubectl get endpoints flask-app-service
Ingress Controllers
While Services expose applications internally (ClusterIP) or on raw ports (NodePort/LoadBalancer), Ingress provides HTTP/HTTPS routing with features like path-based routing, virtual hosting, and TLS termination — similar to a reverse proxy like NGINX.
NGINX Ingress Controller Setup
# Install NGINX Ingress Controller on Kind
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
# Wait for the controller to be ready
kubectl wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=120s
# Verify installation
kubectl get pods -n ingress-nginx
kubectl get svc -n ingress-nginx
# ingress.yaml - HTTP routing rules
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: flask-app-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
nginx.ingress.kubernetes.io/rate-limit: "10"
nginx.ingress.kubernetes.io/rate-limit-window: "1m"
spec:
ingressClassName: nginx
rules:
- host: flask-app.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: flask-app-service
port:
number: 80
- path: /api
pathType: Prefix
backend:
service:
name: flask-api-service
port:
number: 80
TLS Configuration
# ingress-tls.yaml - HTTPS with TLS termination
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: flask-app-ingress-tls
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
tls:
- hosts:
- flask-app.example.com
secretName: flask-app-tls
rules:
- host: flask-app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: flask-app-service
port:
number: 80
ConfigMaps & Secrets
Kubernetes separates configuration from container images using ConfigMaps (non-sensitive data) and Secrets (sensitive data like passwords, tokens, keys).
# configmap.yaml - Application configuration
apiVersion: v1
kind: ConfigMap
metadata:
name: flask-app-config
data:
FLASK_ENV: "production"
LOG_LEVEL: "info"
MAX_CONNECTIONS: "100"
APP_CONFIG: |
[database]
pool_size = 10
timeout = 30
[cache]
backend = redis
ttl = 300
---
# secret.yaml - Sensitive configuration
apiVersion: v1
kind: Secret
metadata:
name: flask-app-secrets
type: Opaque
stringData:
DATABASE_URL: "postgresql://user:password@db-host:5432/myapp"
REDIS_URL: "redis://:secret@redis-host:6379/0"
JWT_SECRET_KEY: "super-secret-jwt-key-change-in-production"
---
# Using ConfigMap and Secret in a Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: flask-app
spec:
replicas: 3
selector:
matchLabels:
app: flask-app
template:
metadata:
labels:
app: flask-app
spec:
containers:
- name: flask
image: flask-app:1.0
ports:
- containerPort: 5000
envFrom:
- configMapRef:
name: flask-app-config
- secretRef:
name: flask-app-secrets
volumeMounts:
- name: config-volume
mountPath: /etc/app/config
readOnly: true
volumes:
- name: config-volume
configMap:
name: flask-app-config
items:
- key: APP_CONFIG
path: app.ini
Namespaces & Resource Management
Namespaces provide logical isolation within a cluster — useful for separating environments (dev/staging/prod), teams, or applications. Resource quotas and limits prevent any single workload from monopolizing cluster resources.
# namespace.yaml - Namespace with resource constraints
apiVersion: v1
kind: Namespace
metadata:
name: flask-production
labels:
environment: production
team: platform
---
# Resource quota for the namespace
apiVersion: v1
kind: ResourceQuota
metadata:
name: production-quota
namespace: flask-production
spec:
hard:
requests.cpu: "4"
requests.memory: "8Gi"
limits.cpu: "8"
limits.memory: "16Gi"
pods: "20"
services: "10"
persistentvolumeclaims: "5"
---
# Default limits for pods without explicit resource specs
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: flask-production
spec:
limits:
- default:
memory: "256Mi"
cpu: "500m"
defaultRequest:
memory: "128Mi"
cpu: "100m"
type: Container
# Apply namespace and quotas
kubectl apply -f namespace.yaml
# Deploy resources into the namespace
kubectl apply -f deployment.yaml -n flask-production
# View resource usage
kubectl top pods -n flask-production
kubectl describe quota production-quota -n flask-production
# View limit ranges
kubectl describe limitrange default-limits -n flask-production
Deploying the Flask App
Let's bring everything together and deploy the Flask application from Part 2 to our local Kubernetes cluster with a complete production-like setup.
End-to-End Deployment
We'll create a namespace, ConfigMap, Secret, Deployment, Service, and Ingress — the full stack needed to run an application in Kubernetes. Each manifest is independently applicable and follows production best practices.
# flask-app-complete.yaml - Complete application manifests
# Step 1: Namespace
apiVersion: v1
kind: Namespace
metadata:
name: flask-app
labels:
app: flask-app
---
# Step 2: ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: flask-config
namespace: flask-app
data:
FLASK_ENV: "production"
FLASK_APP: "app.py"
LOG_LEVEL: "info"
WORKERS: "4"
---
# Step 3: Secret
apiVersion: v1
kind: Secret
metadata:
name: flask-secrets
namespace: flask-app
type: Opaque
stringData:
SECRET_KEY: "change-this-in-production-use-sealed-secrets"
DATABASE_URL: "sqlite:///app.db"
---
# Step 4: Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: flask-app
namespace: flask-app
labels:
app: flask-app
spec:
replicas: 3
selector:
matchLabels:
app: flask-app
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: flask-app
version: "1.0"
spec:
containers:
- name: flask
image: flask-app:1.0
ports:
- containerPort: 5000
name: http
envFrom:
- configMapRef:
name: flask-config
- secretRef:
name: flask-secrets
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 15
periodSeconds: 30
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
startupProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 12
---
# Step 5: Service
apiVersion: v1
kind: Service
metadata:
name: flask-app-service
namespace: flask-app
spec:
type: ClusterIP
selector:
app: flask-app
ports:
- protocol: TCP
port: 80
targetPort: 5000
name: http
---
# Step 6: Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: flask-app-ingress
namespace: flask-app
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "5m"
spec:
ingressClassName: nginx
rules:
- host: flask-app.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: flask-app-service
port:
number: 80
# Load the Docker image into Kind (since Kind can't pull from local Docker daemon)
kind load docker-image flask-app:1.0 --name devops-lab
# Apply all manifests
kubectl apply -f flask-app-complete.yaml
# Watch pods come up
kubectl get pods -n flask-app -w
# Check deployment status
kubectl rollout status deployment/flask-app -n flask-app
# Verify the service
kubectl get svc -n flask-app
# Add local DNS entry (Linux/Mac)
echo "127.0.0.1 flask-app.local" | sudo tee -a /etc/hosts
# Test the application
curl http://flask-app.local/
# View application logs
kubectl logs -l app=flask-app -n flask-app --tail=50
# Scale up for a load test
kubectl scale deployment/flask-app -n flask-app --replicas=5
# Clean up
kubectl delete namespace flask-app
Conclusion & What's Next
In this article, we covered the core building blocks of Kubernetes:
- Architecture — Control plane and worker node components
- Local development — Kind for fast, multi-node local clusters
- kubectl — Essential commands and output formats
- Core resources — Pods, Deployments, Services, Ingress
- Configuration — ConfigMaps and Secrets for externalized config
- Resource management — Namespaces, quotas, and limits
You now have a working Kubernetes deployment of the Flask application with networking, configuration management, and ingress routing.
Next in the Series
In Part 4: Helm & Package Management, we'll template our Kubernetes manifests into reusable Helm charts, manage values files for different environments, and explore the Helm ecosystem for installing complex applications like Prometheus and Grafana with a single command.