Build a Kubernetes Cluster

advanced | 180 min read | 2025.12.02

What You’ll Learn in This Tutorial

  • Build a local Kubernetes cluster with minikube
  • Basic concepts of Pod, Deployment, and Service
  • How to write YAML manifests
  • Application deployment and exposure
  • Scaling and rolling updates
  • Using ConfigMap, Secret, and Namespace
  • Best practices for production environments

Prerequisites: Docker Desktop must be installed. kubectl and minikube installation required.


History and Background of Kubernetes

Why Kubernetes Was Created

Kubernetes (also written as K8s) is a container orchestration platform that Google open-sourced in 2014. Its origins lie in the large-scale container management systems called “Borg” and “Omega” that Google had been operating internally for over 10 years.

“Kubernetes is based on a decade and a half of experience at Google running production workloads at scale, combined with best-of-breed ideas and practices from the community.”

Kubernetes Official Documentation

Google is said to launch billions of containers every week, and the knowledge gained from that operational experience is consolidated in Kubernetes.

Origin of the Kubernetes Name

“Kubernetes” means “helmsman” or “pilot” in Greek. It carries the meaning of steering the “ship” of containers, and the logo is designed around a ship’s helm. The abbreviation “K8s” comes from having 8 letters (ubernete) between K and s.

The Need for Container Orchestration

Container technology (Docker) has dramatically simplified application packaging and distribution. However, new challenges arise in production environments:

ChallengeDescription
ScalingDynamically adjust container count based on traffic
Failure RecoveryAutomatic restart when containers crash
Load BalancingTraffic distribution across multiple containers
Service DiscoveryManaging dynamically changing container IP addresses
Rolling UpdatesApplication updates without downtime
Secret ManagementSecure distribution of passwords and API keys

Kubernetes solves these challenges with declarative configuration (defining the desired state).

CNCF and Cloud Native Ecosystem

In 2015, Google donated Kubernetes to the CNCF (Cloud Native Computing Foundation). CNCF is an organization under the Linux Foundation that promotes the adoption and standardization of cloud native technologies.

Currently, CNCF includes important projects such as:

  • Kubernetes - Container orchestration
  • Prometheus - Monitoring and alerting
  • Envoy - Service proxy
  • Helm - Package manager
  • Argo - GitOps workflow

Reference: CNCF Cloud Native Interactive Landscape


Kubernetes Architecture Overview

A Kubernetes cluster consists of the Control Plane (master) and Worker Nodes.

Control Plane Components

flowchart TB
    subgraph ControlPlane["Control Plane"]
        API["API Server"]
        Scheduler["Scheduler"]
        Controller["Controller Manager"]
        etcd["etcd<br/>(Distributed Key-Value Store)"]
    end
ComponentRole
kube-apiserverEntry point for all API operations to the cluster
etcdDistributed database that stores cluster state
kube-schedulerDecides which node to place Pods on
kube-controller-managerRuns various controllers (ReplicaSet, Node, etc.)

Worker Node Components

flowchart TB
    subgraph WorkerNode["Worker Node"]
        kubelet["kubelet"]
        proxy["kube-proxy"]
        runtime["Container Runtime"]
        subgraph Pods["Pods"]
            Pod1["Pod"]
            Pod2["Pod"]
            Pod3["Pod"]
            Pod4["Pod"]
        end
    end
ComponentRole
kubeletAgent that manages Pods on the node
kube-proxyManages network rules to implement Service functionality
Container RuntimeRuns containers (containerd, CRI-O, etc.)

Step 1: Starting minikube

minikube is a tool that makes it easy to build a Kubernetes cluster in your local environment. It’s ideal for learning and development.

# Start minikube cluster
# You can specify the driver with --driver option (docker, virtualbox, hyperkit, etc.)
minikube start

# Start with specified memory and CPU (to simulate production-like environment)
minikube start --memory=4096 --cpus=2

# Check cluster status
minikube status

# Verify kubectl connection
kubectl cluster-info

# Check node information
kubectl get nodes -o wide

Other local environment tools: kind (Kubernetes in Docker) and k3s (lightweight Kubernetes) are also popular. Choose based on your needs.


Step 2: Kubernetes Basic Concepts

Let’s understand the main resources in Kubernetes. Detailed explanations are available in the official concepts documentation.

Relationship Between Main Resources

flowchart BT
    External["External Traffic"]
    Service["Service
(LB)"] External --> Service subgraph Deployment["Deployment
(Manages replica count, update strategy, rollback)"] subgraph ReplicaSet["ReplicaSet
(Maintains specified number of Pod replicas)"] Pod1["Pod
(Container)"] Pod2["Pod
(Container)"] Pod3["Pod
(Container)"] Pod4["Pod
(Container)"] end end Service --> Pod1 Service --> Pod2 Service --> Pod3 Service --> Pod4

Details of Each Resource

ResourceDescriptionUse Case
PodSmallest deployment unit. Contains one or more containersSingle application instance
ReplicaSetMaintains specified number of Pod replicasUsually managed automatically by Deployment
DeploymentPod replica management and rolling updatesStateless applications
StatefulSetOrdered Pod management and persistent storageDatabases, distributed systems
DaemonSetPlaces one Pod on each nodeLog collection, monitoring agents
ServiceAbstracts network access to PodsLoad balancing, service discovery
IngressHTTP/HTTPS routing and TLS terminationExternal exposure, path-based routing
ConfigMapStores configuration dataEnvironment variables, config files
SecretStores sensitive dataPasswords, API keys, certificates
NamespaceLogical separation of resourcesEnvironment isolation (dev/staging/prod)

Step 3: Deploy Your First Pod

Create Directly from Command Line (for development/debugging)

# Create nginx Pod
kubectl run my-nginx --image=nginx:latest

# Check Pod status
# Wait until STATUS becomes Running
kubectl get pods

# Monitor status in real-time
kubectl get pods -w

# Check Pod details (events, resource usage, etc.)
kubectl describe pod my-nginx

# Check Pod logs
kubectl logs my-nginx

# Follow logs in real-time
kubectl logs -f my-nginx

# Delete Pod
kubectl delete pod my-nginx

Tip: Pods created with kubectl run are not automatically recreated when deleted. Always use Deployment in production environments.

Debugging Inside Pods

# Start shell inside Pod
kubectl exec -it my-nginx -- /bin/bash

# Execute specific command
kubectl exec my-nginx -- cat /etc/nginx/nginx.conf

# Check network from inside Pod
kubectl exec my-nginx -- curl -s localhost

Step 4: Deploy with YAML Manifests

In production environments, follow the Infrastructure as Code principle and define resources in YAML files. This ensures version control, review, and reproducibility.

“Store the declarative configuration in source control. This allows you to roll back and roll forward configurations.”

Kubernetes Best Practices

Deployment Manifest

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  labels:
    app: my-app
    # Use labels to organize resources
    environment: development
    version: v1.0.0
spec:
  replicas: 3  # Maintain 3 Pod replicas
  selector:
    matchLabels:
      app: my-app
  # Update strategy configuration
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1        # Number of additional Pods that can be created during update
      maxUnavailable: 0  # Number of Pods that can be unavailable during update
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-app
        image: nginx:1.24
        ports:
        - containerPort: 80
        # Resource limits (essential best practice)
        resources:
          requests:  # Resources reserved during scheduling
            memory: "64Mi"
            cpu: "250m"    # 250 millicores = 0.25 CPU
          limits:    # Upper limit (OOMKilled or throttled if exceeded)
            memory: "128Mi"
            cpu: "500m"
        # Liveness Probe: Check if Pod is alive
        livenessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 10
          periodSeconds: 5
        # Readiness Probe: Check if ready to accept traffic
        readinessProbe:
          httpGet:
            path: /
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 3

Apply Deployment

# Apply manifest (--record to record rollout history)
kubectl apply -f deployment.yaml

# Check Deployment status
kubectl get deployments

# Also check ReplicaSet (automatically created by Deployment)
kubectl get replicasets

# Check Pod status (filter with label selector)
kubectl get pods -l app=my-app

# Output detailed info in YAML format
kubectl get deployment my-app -o yaml

Importance of Health Checks (Probes)

Kubernetes Probes are mechanisms to verify application health.

ProbePurposeAction on Failure
livenessProbeIs the Pod alive?Restart the container
readinessProbeCan it accept requests?Remove from Service endpoints
startupProbeHas startup completed?Disable liveness/readiness until startup completes

Warning: If you check DB connection in livenessProbe, a DB failure could cause infinite Pod restarts, leading to cascade failures.


Step 5: Expose Apps with Service

Service provides stable network access to Pods. While Pods are dynamically created/deleted with changing IP addresses, Service provides a fixed DNS name and IP.

Service Types

TypeUseAccess Scope
ClusterIPDefault. Internal cluster onlyInternal microservice communication
NodePortExternal exposure via node portDevelopment/test environments
LoadBalancerAuto-provision cloud LBProduction external exposure
ExternalNameAlias to external serviceExternal DB connection

Service Manifest

service.yaml

apiVersion: v1
kind: Service
metadata:
  name: my-app-service
  labels:
    app: my-app
spec:
  type: NodePort  # For development. Use LoadBalancer in production
  selector:
    app: my-app   # Forward traffic to Pods with this label
  ports:
  - name: http
    protocol: TCP
    port: 80         # Service port
    targetPort: 80   # Pod port
    nodePort: 30080  # Node port (30000-32767)

Apply and Verify Service

# Create Service
kubectl apply -f service.yaml

# Check Service
kubectl get services

# Service details (check Pod IPs in Endpoints)
kubectl describe service my-app-service

# Check Endpoints directly
kubectl get endpoints my-app-service

# Get access URL with minikube
minikube service my-app-service --url

# Open in browser
minikube service my-app-service

ClusterIP Service Example (for internal communication)

apiVersion: v1
kind: Service
metadata:
  name: backend-service
spec:
  type: ClusterIP  # Default
  selector:
    app: backend
  ports:
  - port: 8080
    targetPort: 8080

Accessible from within the cluster via backend-service.default.svc.cluster.local or simply backend-service.


Step 6: Scaling

One of Kubernetes’ powerful features is flexible scaling.

Manual Scaling

# Change replica count
kubectl scale deployment my-app --replicas=5

# Verify Pods increased
kubectl get pods -l app=my-app

# Check Deployment status
kubectl get deployment my-app

Horizontal Pod Autoscaler (HPA)

HPA automatically adjusts Pod count based on CPU usage or custom metrics.

# Set up CPU-based autoscaling
# Scale out when CPU usage exceeds 50%
kubectl autoscale deployment my-app --min=2 --max=10 --cpu-percent=50

# Check HPA status
kubectl get hpa

# Check details
kubectl describe hpa my-app

HPA YAML Manifest

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  # Fine-grained scaling behavior control
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300  # Scale down after 5 minutes of stability

Note: To use HPA, Metrics Server must be installed in the cluster. Enable with minikube addons enable metrics-server in minikube.


Step 7: Rolling Updates

Kubernetes rolling updates allow updating applications without downtime.

Executing Updates

# Update image
kubectl set image deployment/my-app my-app=nginx:1.25

# Or edit manifest and apply
kubectl apply -f deployment.yaml

# Monitor rollout status in real-time
kubectl rollout status deployment/my-app

# Check rollout history
kubectl rollout history deployment/my-app

# Check details of specific revision
kubectl rollout history deployment/my-app --revision=2

Rollback

# Rollback to previous version
kubectl rollout undo deployment/my-app

# Rollback to specific revision
kubectl rollout undo deployment/my-app --to-revision=1

# Pause rollout (for canary releases, etc.)
kubectl rollout pause deployment/my-app

# Resume rollout
kubectl rollout resume deployment/my-app

Update Strategy

Specify update strategy with the strategy field in Deployment:

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%        # Percentage of additional Pods during update
      maxUnavailable: 25%  # Percentage of Pods that can be stopped during update
SettingEffect
maxSurge: 1, maxUnavailable: 0Safety first (stop old Pod after new Pod starts)
maxSurge: 0, maxUnavailable: 1Resource saving (stop old Pod first)

Step 8: ConfigMap and Secret

By externalizing application settings and sensitive information, you can change configuration without rebuilding images. This follows The Twelve-Factor App principle of “Store config in environment variables”.

ConfigMap

Stores configuration data in key-value format.

configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  # Key-value format
  APP_ENV: "production"
  LOG_LEVEL: "info"
  MAX_CONNECTIONS: "100"
  # Embedded as file
  nginx.conf: |
    server {
      listen 80;
      location / {
        root /usr/share/nginx/html;
      }
    }

Secret

Stores sensitive data. Values are base64 encoded.

secret.yaml

apiVersion: v1
kind: Secret
metadata:
  name: app-secret
type: Opaque
data:
  # Base64 encoded values
  # echo -n "password123" | base64 → cGFzc3dvcmQxMjM=
  DB_PASSWORD: cGFzc3dvcmQxMjM=
  API_KEY: c2VjcmV0LWFwaS1rZXk=

Security Warning: Kubernetes Secrets are only base64 encoded by default, not encrypted. Consider the following for production:

Using ConfigMap/Secret in Pods

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    spec:
      containers:
      - name: my-app
        image: nginx:1.24
        # Inject as environment variables
        env:
        - name: APP_ENVIRONMENT
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: APP_ENV
        - name: DATABASE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: app-secret
              key: DB_PASSWORD
        # Inject all keys as environment variables
        envFrom:
        - configMapRef:
            name: app-config
        - secretRef:
            name: app-secret
        # Mount as file
        volumeMounts:
        - name: config-volume
          mountPath: /etc/nginx/conf.d
          readOnly: true
      volumes:
      - name: config-volume
        configMap:
          name: app-config
          items:
          - key: nginx.conf
            path: default.conf

Step 9: Resource Isolation with Namespace

Use Namespace to logically isolate resources.

Creating and Using Namespaces

# Create Namespaces
kubectl create namespace development
kubectl create namespace staging
kubectl create namespace production

# Check existing Namespaces
kubectl get namespaces

# Create resources in specific Namespace
kubectl apply -f deployment.yaml -n development

# Check resources specifying Namespace
kubectl get pods -n development

# Check resources in all Namespaces
kubectl get pods --all-namespaces
kubectl get pods -A  # Short form

Switching Default Namespace

# Check current context
kubectl config current-context

# Change default Namespace
kubectl config set-context --current --namespace=development

# Verify change
kubectl config view --minify | grep namespace

ResourceQuota per Namespace

Limit resource usage per team or environment.

apiVersion: v1
kind: ResourceQuota
metadata:
  name: development-quota
  namespace: development
spec:
  hard:
    requests.cpu: "4"
    requests.memory: "8Gi"
    limits.cpu: "8"
    limits.memory: "16Gi"
    pods: "20"
    services: "10"

Useful kubectl Commands

Checking Resources

# Show all resources
kubectl get all

# Wide output format
kubectl get pods -o wide

# Output in YAML/JSON format
kubectl get deployment my-app -o yaml
kubectl get deployment my-app -o json

# Custom columns output
kubectl get pods -o custom-columns=NAME:.metadata.name,STATUS:.status.phase

# Watch resources (real-time updates)
kubectl get pods -w

Debugging and Troubleshooting

# Execute command inside Pod
kubectl exec -it pod-name -- /bin/sh

# For multi-container Pods, specify container
kubectl exec -it pod-name -c container-name -- /bin/sh

# Port forward (access Pod directly from local)
kubectl port-forward pod/my-app-xxx 8080:80

# Port forward to Service
kubectl port-forward service/my-app-service 8080:80

# Check resource events
kubectl get events --sort-by='.lastTimestamp'

# Events for specific Pod
kubectl describe pod my-app-xxx | grep -A 10 Events

Deleting Resources

# Delete from manifest
kubectl delete -f deployment.yaml

# Bulk delete by label
kubectl delete pods -l app=my-app

# Delete entire Namespace (caution: deletes all resources inside)
kubectl delete namespace development

# Force delete (for stuck Pods, etc.)
kubectl delete pod my-app-xxx --force --grace-period=0

Production Environment Best Practices

1. Set Resource Limits

Set requests and limits for all containers.

resources:
  requests:
    memory: "64Mi"
    cpu: "250m"
  limits:
    memory: "128Mi"
    cpu: "500m"

Why is this important: Without resource limits, one Pod could consume all node resources, affecting other Pods.

2. Implement Health Checks (Probes)

Always set livenessProbe and readinessProbe.

3. Pod Disruption Budget (PDB)

Maintain minimum Pod count even during maintenance.

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: my-app-pdb
spec:
  minAvailable: 2  # or maxUnavailable: 1
  selector:
    matchLabels:
      app: my-app

4. Anti-Affinity Settings

Spread Pods across different nodes to improve availability.

spec:
  affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchLabels:
              app: my-app
          topologyKey: kubernetes.io/hostname

5. NetworkPolicy

Restrict Pod network traffic.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

6. Security Context

Minimize container privileges.

spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    fsGroup: 1000
  containers:
  - name: my-app
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop:
        - ALL

Common Mistakes and Anti-patterns

What to Avoid

Anti-patternProblemSolution
Using latest tagNo reproducibility, unexpected updatesUse explicit version tags
No resource limitsResource exhaustion, node downAlways set requests/limits
No ProbesDelayed failure detectionSet liveness/readinessProbe
Hardcoded SecretsSecurity riskUse ConfigMap/Secret
Running as rootSecurity riskSet runAsNonRoot: true
Single replicaSingle point of failureMaintain at least 2 replicas
# Good example
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: my-app
        image: my-app:1.2.3  # Explicit version
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "200m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
        securityContext:
          runAsNonRoot: true
          readOnlyRootFilesystem: true

Cleanup

# Delete resources
kubectl delete -f service.yaml
kubectl delete -f deployment.yaml
kubectl delete -f configmap.yaml
kubectl delete -f secret.yaml

# Or bulk delete
kubectl delete -f .

# Delete Namespace (resources inside are also deleted)
kubectl delete namespace development

# Stop minikube
minikube stop

# Completely delete minikube
minikube delete

# Delete all profiles
minikube delete --all

Next Steps

Based on the fundamentals learned in this tutorial, we recommend proceeding to these topics:

Advanced Topics


Official Documentation

Learning Resources

Best Practices

Tools

  • Lens - Kubernetes IDE
  • k9s - Terminal-based Kubernetes UI
  • kubectx/kubens - Context/Namespace switching tool
  • stern - Multi-Pod log viewer

Certifications

← Back to list