Back to Modern DevOps & Platform Engineering Series

Part 3: Kubernetes Fundamentals

May 14, 2026 Wasil Zafar 35 min read

Master Kubernetes architecture, core resources, networking, and deploy a complete application to your local cluster.

Table of Contents

  1. Introduction
  2. Kubernetes Architecture
  3. Setting Up a Local Cluster
  4. kubectl Essentials
  5. Pods & Deployments
  6. Services & Networking
  7. Ingress Controllers
  8. ConfigMaps & Secrets
  9. Namespaces & Resource Management
  10. Deploying the Flask App
  11. Conclusion & What's Next

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.

Key Insight: Kubernetes doesn't just run containers — it provides a declarative API for managing the entire lifecycle of distributed applications, from scheduling to networking to storage.

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.

Kubernetes Cluster Architecture
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).
Important: Docker is still used for building images. Only Docker as a Kubernetes runtime was deprecated. Images built with Docker work perfectly on containerd and CRI-O.

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
Hands-On Lab 5 minutes
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.

Kind Local Development Multi-Node

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
Pro Tip: Set up shell aliases for frequently used commands: 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.

Pod Lifecycle
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
Rolling Update Strategy: With 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

Kubernetes 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 namespace
  • flask-app-service.my-namespace — resolves from any namespace
  • flask-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
Security Note: Kubernetes Secrets are Base64-encoded, not encrypted at rest by default. For production, enable encryption at rest (EncryptionConfiguration) and consider external secret managers like HashiCorp Vault, AWS Secrets Manager, or the External Secrets Operator.

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.

Complete Walkthrough 15 minutes
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.

Deployment Service Ingress ConfigMap
# 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
Production Checklist: Before going to production, ensure you have: Pod Disruption Budgets, Horizontal Pod Autoscaler, network policies, proper RBAC, external secrets management, monitoring (Prometheus/Grafana), and a proper CI/CD pipeline for deployments.

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.