Quick Start: Hello Kubernetes
Before diving into object types, let's run through a complete exercise on the cluster you built in Part 6. This demonstrates every core Kubernetes capability in 7 commands — we'll then spend the rest of this article understanding what each object is and why it works.
1. Deploy & Expose
# Create a Deployment (3 replicas of nginx) and expose it:
kubectl create deployment hello-k8s --image=nginx:1.25 --replicas=3
kubectl expose deployment hello-k8s --type=NodePort --port=80
# Watch pods schedule across worker nodes:
kubectl get pods -o wide
# NAME READY STATUS NODE
# hello-k8s-59bb5f8cdc-abc12 1/1 Running worker-1
# hello-k8s-59bb5f8cdc-def34 1/1 Running worker-2
# hello-k8s-59bb5f8cdc-ghi56 1/1 Running worker-1
# Access via NodePort (extract the assigned port dynamically):
NODE_PORT=$(kubectl get svc hello-k8s -o jsonpath='{.spec.ports[0].nodePort}')
curl http://localhost:$NODE_PORT
# <!DOCTYPE html>...<h1>Welcome to nginx!</h1>...
kubectl create deployment created a Deployment → which created a ReplicaSet → which created 3 Pods. kubectl expose created a Service. Four object types, two commands. The rest of this article explains each one.
2. Scale & Self-Heal
# Scale to 6 replicas:
kubectl scale deployment hello-k8s --replicas=6
kubectl get pods -o wide
# NAME READY STATUS IP NODE
# hello-k8s-59bb5f8cdc-7gv6r 1/1 Running 10.244.254.202 worker-2
# hello-k8s-59bb5f8cdc-cnfpj 1/1 Running 10.244.126.10 worker-1
# hello-k8s-59bb5f8cdc-tq662 1/1 Running 10.244.126.9 worker-1
# hello-k8s-59bb5f8cdc-vgwwv 1/1 Running 10.244.254.203 worker-2
# hello-k8s-59bb5f8cdc-xfgks 1/1 Running 10.244.126.11 worker-1
# hello-k8s-59bb5f8cdc-z9x4z 1/1 Running 10.244.254.201 worker-2
# 6 pods spread across both worker nodes (scheduler balances load)
# Scale down to 2:
kubectl scale deployment hello-k8s --replicas=2
kubectl get pods -o wide
# hello-k8s-59bb5f8cdc-cnfpj 1/1 Running 10.244.126.10 worker-1
# hello-k8s-59bb5f8cdc-z9x4z 1/1 Running 10.244.254.201 worker-2
# Kubernetes terminated 4 pods, keeping 1 per node for balance
# Self-healing — delete a pod and watch it recover:
# NOTE: -o name returns "pod/name", so use `kubectl delete` without `pod`:
kubectl delete $(kubectl get pods -l app=hello-k8s -o name | head -1)
# pod "hello-k8s-59bb5f8cdc-cnfpj" deleted
kubectl get pods -o wide
# hello-k8s-59bb5f8cdc-vrmfl 1/1 Running 10.244.126.12 worker-1 (new!)
# hello-k8s-59bb5f8cdc-z9x4z 1/1 Running 10.244.254.201 worker-2
# The ReplicaSet detected desired(2) != actual(1) and created a replacement.
3. Rolling Update & DNS
# Zero-downtime update (nginx 1.25 → 1.26):
kubectl set image deployment/hello-k8s nginx=nginx:1.26
kubectl rollout status deployment/hello-k8s
# deployment "hello-k8s" successfully rolled out
# Rollback:
kubectl rollout undo deployment/hello-k8s
# Service discovery via DNS (from inside the cluster):
kubectl run test-dns --image=busybox:1.36 --rm -it --restart=Never -- sh
/ # nslookup hello-k8s
# Name: hello-k8s.default.svc.cluster.local
# Address: 10.107.50.16
/ # wget -qO- http://hello-k8s | head -5
# <!DOCTYPE html>...<h1>Welcome to nginx!</h1>...
/ # exit
# Cleanup:
kubectl delete deployment hello-k8s
kubectl delete svc hello-k8s
kubectl taint nodes --all node-role.kubernetes.io/control-plane-. All exercises work identically — pods just all schedule on the same node.
In 7 commands we used Deployments, ReplicaSets, Pods, and Services — plus labels, selectors, and DNS discovery under the hood. Now let's understand each object type in depth.
Anatomy of a Kubernetes Object
YAML Structure
Every Kubernetes object follows the same four-section structure. Understanding this structure means you can read and write any Kubernetes manifest:
# Every Kubernetes object has these four top-level fields:
apiVersion: apps/v1 # API group + version
kind: Deployment # Object type
metadata: # Identity and organisation
name: payment-service # Unique within namespace
namespace: production # Logical grouping (default: "default")
labels: # Key-value pairs for selection/organisation
app: payment
tier: backend
version: v2.1
annotations: # Non-identifying metadata
deployment.kubernetes.io/revision: "3"
description: "Handles payment processing"
spec: # DESIRED STATE — what you want
replicas: 3
selector:
matchLabels:
app: payment
template:
metadata:
labels:
app: payment
spec:
containers:
- name: payment
image: payment:v2.1
# status: # ACTUAL STATE — set by Kubernetes (read-only)
# availableReplicas: 3
# readyReplicas: 3
# conditions:
# - type: Available
# status: "True"
apiVersion + kind tell Kubernetes what type of object this is. metadata identifies it. spec declares what you want (desired state). status reports what actually exists (set by controllers, never by you).
Labels & Selectors
Labels are the glue that connects Kubernetes objects. A Service finds its pods through labels. A Deployment manages pods through labels. Without labels, objects would be isolated and unmanageable.
# Labels: key-value pairs attached to objects
metadata:
labels:
app: payment # Application name
tier: backend # Architecture tier
environment: prod # Deployment environment
version: v2.1 # Current version
team: platform # Owning team
# Selectors: how objects find each other
# Equality-based:
selector:
matchLabels:
app: payment
tier: backend
# Set-based (more powerful):
selector:
matchExpressions:
- key: environment
operator: In
values: ["prod", "staging"]
- key: tier
operator: NotIn
values: ["frontend"]
- key: version
operator: Exists
# Label operations with kubectl:
# Add labels to existing objects:
kubectl label pods payment-pod-abc12 version=v2.2 --overwrite
# Select pods by label:
kubectl get pods -l app=payment # Exact match
kubectl get pods -l 'tier in (backend, middleware)' # Set-based
kubectl get pods -l app=payment,tier=backend # Multiple conditions
# Show labels as columns:
kubectl get pods --show-labels
kubectl get pods -L app,tier,version # Show specific labels as columns
# Remove a label:
kubectl label pods payment-pod-abc12 version-
Annotations
Annotations store non-identifying metadata — information that tools, libraries, or humans need but that Kubernetes doesn't use for selection:
# Annotations: arbitrary metadata (not used for selection)
metadata:
annotations:
# Deployment history
kubernetes.io/change-cause: "Updated image to v2.1 for security patch"
# Ingress configuration
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "true"
# Monitoring
prometheus.io/scrape: "true"
prometheus.io/port: "9090"
# Team/ownership
owner: "platform-team@company.com"
oncall: "platform-pager"
# Build info
git-commit: "a1b2c3d4e5f6"
build-date: "2026-05-14T10:30:00Z"
# Create a pod with labels:
kubectl run label-demo --image=nginx:1.25 --labels="app=demo,tier=frontend,env=dev"
# Query by label:
kubectl get pods -l app=demo # Exact match
kubectl get pods -l 'env in (dev,staging)' # Set-based
kubectl get pods --show-labels # See all labels
# Add/change/remove labels on existing pods:
kubectl label pod label-demo version=v1.0
kubectl label pod label-demo env=staging --overwrite
kubectl label pod label-demo version- # Remove label
# Annotate:
kubectl annotate pod label-demo owner="platform-team"
kubectl describe pod label-demo | grep Annotations
# Cleanup:
kubectl delete pod label-demo
Pods
A Pod is the smallest deployable unit in Kubernetes — one or more containers that share networking (same IP) and storage (shared volumes). You rarely create pods directly; controllers manage them for you.
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 fails
Failed --> [*]
Succeeded --> [*]
# A complete Pod specification:
apiVersion: v1
kind: Pod
metadata:
name: web-app
labels:
app: web
spec:
# Restart policy: Always (default), OnFailure, Never
restartPolicy: Always
# Resource requests and limits
containers:
- name: web
image: nginx:1.25-alpine
ports:
- containerPort: 80
name: http
resources:
requests: # Minimum guaranteed resources (scheduling)
memory: "128Mi"
cpu: "100m" # 100 millicores = 0.1 CPU core
limits: # Maximum allowed (enforcement)
memory: "256Mi"
cpu: "500m"
# Environment variables
env:
- name: APP_ENV
value: "production"
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: database.host
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: db-password
# Health probes
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 10
periodSeconds: 15
readinessProbe:
httpGet:
path: /ready
port: 80
initialDelaySeconds: 5
periodSeconds: 10
# Volume mounts
volumeMounts:
- name: app-data
mountPath: /data
- name: config-volume
mountPath: /etc/config
readOnly: true
# Volumes
volumes:
- name: app-data
emptyDir: {}
- name: config-volume
configMap:
name: app-config
Multi-Container Patterns
Pods can contain multiple containers that share the same network namespace (localhost communication) and storage volumes. Three common patterns:
| Pattern | Purpose | Example |
|---|---|---|
| Sidecar | Extend/enhance main container | Log shipper, Istio proxy, monitoring agent |
| Ambassador | Proxy external connections | Database connection pooler, API gateway |
| Adapter | Transform output for consumption | Log format converter, metrics exporter |
# Sidecar pattern: main app + log collector
apiVersion: v1
kind: Pod
metadata:
name: app-with-sidecar
spec:
containers:
# Main application container
- name: app
image: my-app:v1.0
ports:
- containerPort: 8080
volumeMounts:
- name: shared-logs
mountPath: /var/log/app
# Sidecar: ships logs to central logging
- name: log-shipper
image: fluentd:v1.16
volumeMounts:
- name: shared-logs
mountPath: /var/log/app
readOnly: true
- name: fluentd-config
mountPath: /fluentd/etc
volumes:
- name: shared-logs
emptyDir: {}
- name: fluentd-config
configMap:
name: fluentd-config
Init Containers
Init containers run sequentially before the main containers start. They're used for setup tasks: waiting for dependencies, populating shared volumes, or running database migrations.
# Init containers: run before main containers
apiVersion: v1
kind: Pod
metadata:
name: app-with-init
spec:
initContainers:
# Wait for database to be ready
- name: wait-for-db
image: busybox:1.36
command: ['sh', '-c', 'until nc -z postgres-svc 5432; do echo waiting for db; sleep 2; done']
# Run database migrations
- name: run-migrations
image: my-app:v1.0
command: ['./migrate', '--direction=up']
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
containers:
- name: app
image: my-app:v1.0
ports:
- containerPort: 8080
# Run a standalone pod:
kubectl run my-pod --image=nginx:1.25
# Inspect it:
kubectl get pod my-pod -o wide
kubectl describe pod my-pod
# Exec into the running container:
kubectl exec -it my-pod -- bash
root@my-pod:/# curl localhost
root@my-pod:/# exit
# View logs:
kubectl logs my-pod
# Watch lifecycle states (in another terminal: kubectl delete pod my-pod):
kubectl get pod my-pod -w
# my-pod 1/1 Running 0 2m
# my-pod 1/1 Terminating 0 2m
# Multi-container pod (sidecar pattern):
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: sidecar-demo
spec:
containers:
- name: app
image: nginx:1.25
volumeMounts:
- name: logs
mountPath: /var/log/nginx
- name: log-reader
image: busybox:1.36
command: ["sh", "-c", "tail -f /var/log/nginx/access.log"]
volumeMounts:
- name: logs
mountPath: /var/log/nginx
readOnly: true
volumes:
- name: logs
emptyDir: {}
EOF
# Both containers share the volume:
kubectl logs sidecar-demo -c log-reader
# Cleanup:
kubectl delete pod my-pod sidecar-demo
Workload Controllers
ReplicaSets
A ReplicaSet ensures a specified number of pod replicas are running at all times. If pods die, it creates new ones. If too many exist, it removes extras. You rarely create ReplicaSets directly — Deployments manage them for you.
# ReplicaSet reconciliation in action:
kubectl get rs
# NAME DESIRED CURRENT READY AGE
# payment-6f7g8h9j 3 3 3 2d
# Kill a pod — ReplicaSet immediately creates a replacement:
kubectl delete pod payment-6f7g8h9j-abc12
kubectl get pods -w
# payment-6f7g8h9j-abc12 1/1 Terminating 0 2d
# payment-6f7g8h9j-xyz99 0/1 Pending 0 0s
# payment-6f7g8h9j-xyz99 0/1 ContainerCreating 0 1s
# payment-6f7g8h9j-xyz99 1/1 Running 0 3s
# Create a Deployment (which creates a ReplicaSet):
kubectl create deployment rs-demo --image=nginx:1.25 --replicas=3
# See the ReplicaSet it created:
kubectl get rs
# NAME DESIRED CURRENT READY AGE
# rs-demo-59bb5f8cdc 3 3 3 10s
# Delete a pod — watch the RS recreate it:
# NOTE: -o name returns "pod/name", so use `kubectl delete` without `pod`:
kubectl delete $(kubectl get pods -l app=rs-demo -o name | head -1)
kubectl get pods -l app=rs-demo
# Still 3 pods — the ReplicaSet replaced the deleted one instantly
# Cleanup:
kubectl delete deployment rs-demo
Deployments
Deployments are the primary workload controller for stateless applications. They manage ReplicaSets and provide declarative updates with rollout strategies:
# Deployment with rolling update strategy
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
labels:
app: payment
spec:
replicas: 5
# Rolling update: replace pods gradually
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 2 # Allow 2 extra pods during update
maxUnavailable: 1 # At most 1 pod unavailable during update
selector:
matchLabels:
app: payment
template:
metadata:
labels:
app: payment
version: v2.1
spec:
containers:
- name: payment
image: payment:v2.1
ports:
- containerPort: 8080
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
# Deployment operations:
# Update image (triggers rolling update):
kubectl set image deployment/payment-service payment=payment:v2.2
# Watch rollout progress:
kubectl rollout status deployment/payment-service
# Waiting for deployment "payment-service" rollout to finish:
# 3 out of 5 new replicas have been updated...
# 4 out of 5 new replicas have been updated...
# deployment "payment-service" successfully rolled out
# View rollout history:
kubectl rollout history deployment/payment-service
# REVISION CHANGE-CAUSE
# 1 Initial deployment
# 2 kubectl set image deployment/payment-service payment=payment:v2.1
# 3 kubectl set image deployment/payment-service payment=payment:v2.2
# Rollback to previous version:
kubectl rollout undo deployment/payment-service
# Rollback to specific revision:
kubectl rollout undo deployment/payment-service --to-revision=1
# Pause/resume rollout (for canary-style testing):
kubectl rollout pause deployment/payment-service
# ... test the partial rollout ...
kubectl rollout resume deployment/payment-service
# Create a deployment:
kubectl create deployment deploy-demo --image=nginx:1.25 --replicas=3
# Rolling update (1.25 → 1.26):
kubectl set image deployment/deploy-demo nginx=nginx:1.26
kubectl rollout status deployment/deploy-demo
# deployment "deploy-demo" successfully rolled out
# View history:
kubectl rollout history deployment/deploy-demo
# REVISION CHANGE-CAUSE
# 1
# 2
# Rollback:
kubectl rollout undo deployment/deploy-demo
kubectl describe deployment deploy-demo | grep Image
# Image: nginx:1.25
# See both ReplicaSets (old and new):
kubectl get rs -l app=deploy-demo
# One has 3 replicas (active), one has 0 (previous version)
# Cleanup:
kubectl delete deployment deploy-demo
StatefulSets
StatefulSets are for applications that need stable identity and persistent storage — databases, message queues, and distributed systems that require ordered startup and unique network names.
# StatefulSet for a PostgreSQL replica set
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres-headless # Required: headless Service for DNS
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
# Each pod gets its own PVC (persistent storage)
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 50Gi
# Result: pods get stable names and storage:
# postgres-0 → PVC data-postgres-0 (50Gi)
# postgres-1 → PVC data-postgres-1 (50Gi)
# postgres-2 → PVC data-postgres-2 (50Gi)
# DNS: postgres-0.postgres-headless.default.svc.cluster.local
| Property | Deployment | StatefulSet |
|---|---|---|
| Pod names | Random suffix (pod-7f8g9h-abc12) | Ordered (postgres-0, postgres-1, postgres-2) |
| Storage | Shared or ephemeral | Per-pod persistent volumes |
| Startup order | All pods start simultaneously | Sequential (0 before 1 before 2) |
| Network identity | Via Service (random routing) | Stable DNS per pod |
| Use case | Stateless web apps, APIs | Databases, Kafka, Elasticsearch |
# Create a headless Service (required by StatefulSets):
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Service
metadata:
name: sts-demo-headless
spec:
clusterIP: None
selector:
app: sts-demo
ports:
- port: 80
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: sts-demo
spec:
serviceName: sts-demo-headless
replicas: 3
selector:
matchLabels:
app: sts-demo
template:
metadata:
labels:
app: sts-demo
spec:
containers:
- name: nginx
image: nginx:1.25
EOF
# Observe ORDERED creation (0, then 1, then 2):
kubectl get pods -w -l app=sts-demo
# sts-demo-0 0/1 Pending 0 0s
# sts-demo-0 1/1 Running 0 3s
# sts-demo-1 0/1 Pending 0 0s
# sts-demo-1 1/1 Running 0 3s
# sts-demo-2 0/1 Pending 0 0s
# sts-demo-2 1/1 Running 0 3s
# Stable DNS names:
kubectl run dns-check --image=busybox:1.36 --rm -it --restart=Never -- \
nslookup sts-demo-0.sts-demo-headless
# Address: 10.244.x.x (specific pod IP)
# Delete pod-1 — it comes back as sts-demo-1 (same name!):
kubectl delete pod sts-demo-1
kubectl get pods -l app=sts-demo
# sts-demo-0, sts-demo-1 (recreated), sts-demo-2
# Cleanup:
kubectl delete statefulset sts-demo
kubectl delete svc sts-demo-headless
DaemonSets
A DaemonSet ensures one pod runs on every node (or a subset of nodes). Used for node-level agents like log collectors, monitoring, and network plugins:
# DaemonSet: run a monitoring agent on every node
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-exporter
namespace: monitoring
spec:
selector:
matchLabels:
app: node-exporter
template:
metadata:
labels:
app: node-exporter
spec:
tolerations:
# Run on ALL nodes including masters
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
containers:
- name: node-exporter
image: prom/node-exporter:v1.7.0
ports:
- containerPort: 9100
hostPort: 9100
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
# Create a DaemonSet:
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: ds-demo
spec:
selector:
matchLabels:
app: ds-demo
template:
metadata:
labels:
app: ds-demo
spec:
tolerations:
- effect: NoSchedule
operator: Exists
containers:
- name: agent
image: busybox:1.36
command: ["sh", "-c", "while true; do echo $(hostname); sleep 60; done"]
EOF
# Verify: one pod per node (including master with toleration):
kubectl get pods -l app=ds-demo -o wide
# ds-demo-abc12 1/1 Running master-1
# ds-demo-def34 1/1 Running worker-1
# ds-demo-ghi56 1/1 Running worker-2
# Cleanup:
kubectl delete daemonset ds-demo
Jobs & CronJobs
# Job: run a task to completion
apiVersion: batch/v1
kind: Job
metadata:
name: database-backup
spec:
backoffLimit: 3 # Retry 3 times on failure
activeDeadlineSeconds: 300 # Timeout after 5 minutes
template:
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: postgres:16
command: ["pg_dump", "-h", "postgres-0.postgres-headless", "-U", "admin", "-d", "appdb"]
---
# CronJob: scheduled recurring task
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-backup
spec:
schedule: "0 2 * * *" # Every day at 2 AM
concurrencyPolicy: Forbid # Don't start new if previous still running
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: postgres:16
command: ["pg_dump", "-h", "postgres-0.postgres-headless", "-U", "admin", "-d", "appdb"]
# Run a one-shot Job:
kubectl create job pi-calc --image=perl:5.34 \
-- perl -Mbignum=bpi -wle 'print bpi(500)'
# Watch it complete:
kubectl get jobs -w
# NAME COMPLETIONS DURATION AGE
# pi-calc 0/1 5s 5s
# pi-calc 1/1 12s 12s
# View the result:
kubectl logs job/pi-calc
# 3.14159265358979323846...
# The pod stays in Completed state (not deleted):
kubectl get pods -l job-name=pi-calc
# pi-calc-abc12 0/1 Completed 0 30s
# Cleanup:
kubectl delete job pi-calc
Configuration Objects
ConfigMaps
ConfigMaps separate configuration from container images, allowing the same image to run in different environments with different settings:
# Create ConfigMap from literal values:
kubectl create configmap app-config \
--from-literal=database.host=postgres-svc \
--from-literal=database.port=5432 \
--from-literal=log.level=info
# Create ConfigMap from a file:
kubectl create configmap nginx-config --from-file=nginx.conf
# View ConfigMap:
kubectl get configmap app-config -o yaml
# ConfigMap definition:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
database.host: "postgres-svc"
database.port: "5432"
log.level: "info"
app.properties: |
server.port=8080
spring.datasource.url=jdbc:postgresql://postgres-svc:5432/appdb
spring.redis.host=redis-svc
---
# Using ConfigMap in a Pod:
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: my-app:v1.0
# As environment variables:
env:
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: database.host
# As volume mount (file):
volumeMounts:
- name: config
mountPath: /etc/app/config
readOnly: true
volumes:
- name: config
configMap:
name: app-config
# Create a ConfigMap from literals:
kubectl create configmap demo-config \
--from-literal=database.host=postgres-svc \
--from-literal=log.level=info
# Inspect it:
kubectl get configmap demo-config -o yaml
# Mount in a pod and verify:
kubectl run cm-demo --image=busybox:1.36 --restart=Never \
--overrides='{
"spec": {
"containers": [{
"name": "cm-demo",
"image": "busybox:1.36",
"command": ["sh", "-c", "cat /etc/config/database.host; sleep 3600"],
"volumeMounts": [{"name": "config", "mountPath": "/etc/config"}]
}],
"volumes": [{"name": "config", "configMap": {"name": "demo-config"}}]
}
}'
kubectl logs cm-demo
# postgres-svc
# Cleanup:
kubectl delete pod cm-demo
kubectl delete configmap demo-config
Secrets
Secrets store sensitive data (passwords, tokens, certificates). They're similar to ConfigMaps but with additional protections: base64-encoded at rest, can be encrypted in etcd, and access can be restricted via RBAC.
# Create Secret:
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password='S3cur3P@ssw0rd!'
# Create TLS Secret:
kubectl create secret tls app-tls \
--cert=tls.crt \
--key=tls.key
# View Secret (values are base64-encoded):
kubectl get secret db-credentials -o yaml
# data:
# username: YWRtaW4= (base64 of "admin")
# password: UzNjdXIzUEBzc3cwcmQh
# Decode a secret value:
kubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 -d
# Create a Secret:
kubectl create secret generic demo-secret \
--from-literal=username=admin \
--from-literal=password='K8s-S3cret!'
# Values are base64-encoded (not encrypted!):
kubectl get secret demo-secret -o yaml
# data:
# password: SzhzLVMzY3JldCE=
# username: YWRtaW4=
# Decode:
kubectl get secret demo-secret -o jsonpath='{.data.password}' | base64 -d
# K8s-S3cret!
# Cleanup:
kubectl delete secret demo-secret
Services & Discovery
Service Types
A Service provides a stable network endpoint for a set of pods. Pods come and go, but the Service IP and DNS name remain constant:
# ClusterIP Service (default): internal-only access
apiVersion: v1
kind: Service
metadata:
name: payment-svc
spec:
type: ClusterIP
selector:
app: payment # Routes to all pods with label app=payment
ports:
- port: 80 # Service port (what clients connect to)
targetPort: 8080 # Pod port (where containers listen)
protocol: TCP
---
# NodePort Service: accessible from outside on every node's IP
apiVersion: v1
kind: Service
metadata:
name: web-nodeport
spec:
type: NodePort
selector:
app: web
ports:
- port: 80
targetPort: 8080
nodePort: 30080 # External port (30000-32767)
---
# LoadBalancer Service: provisions cloud load balancer
apiVersion: v1
kind: Service
metadata:
name: web-public
spec:
type: LoadBalancer
selector:
app: web
ports:
- port: 443
targetPort: 8080
flowchart TD
subgraph ClusterIP
C1[Internal DNS] --> CS[Service 10.96.x.x]
CS --> P1[Pod 10.244.1.5]
CS --> P2[Pod 10.244.2.8]
end
subgraph NodePort
EXT1[External:30080] --> N1[Node1:30080]
EXT1 --> N2[Node2:30080]
N1 --> NS[Service]
N2 --> NS
NS --> P3[Pod]
NS --> P4[Pod]
end
subgraph LoadBalancer
LB[Cloud LB
Public IP] --> N3[Node1]
LB --> N4[Node2]
N3 --> LS[Service]
N4 --> LS
LS --> P5[Pod]
LS --> P6[Pod]
end
DNS Resolution
Kubernetes automatically creates DNS records for Services. Pods can reach any Service by name without knowing IP addresses:
# DNS naming convention:
# <service-name>.<namespace>.svc.cluster.local
# From a pod in the "default" namespace:
curl http://payment-svc # Same namespace (short name)
curl http://payment-svc.default # Explicit namespace
curl http://payment-svc.default.svc.cluster.local # Fully qualified
# From a pod in a DIFFERENT namespace:
curl http://payment-svc.production # Cross-namespace
curl http://payment-svc.production.svc.cluster.local
# Headless Service (no ClusterIP, returns pod IPs directly):
# Useful for StatefulSets where clients need individual pod addresses
apiVersion: v1
kind: Service
metadata:
name: postgres-headless
spec:
clusterIP: None # Makes it headless
selector:
app: postgres
ports:
- port: 5432
# DNS returns individual pod IPs:
# nslookup postgres-headless → 10.244.1.5, 10.244.2.8, 10.244.3.12
# nslookup postgres-0.postgres-headless → 10.244.1.5 (specific pod)
# Create a deployment + ClusterIP service:
kubectl create deployment svc-demo --image=nginx:1.25 --replicas=2
kubectl expose deployment svc-demo --port=80
# Check the ClusterIP:
kubectl get svc svc-demo
# NAME TYPE CLUSTER-IP PORT(S) AGE
# svc-demo ClusterIP 10.96.x.x 80/TCP 5s
# Test DNS from inside the cluster:
kubectl run dns-test --image=busybox:1.36 --rm -it --restart=Never -- sh
/ # nslookup svc-demo
# Name: svc-demo.default.svc.cluster.local
# Address: 10.96.x.x
/ # wget -qO- http://svc-demo | head -3
# <!DOCTYPE html>...
/ # exit
# Compare: create a NodePort service:
kubectl expose deployment svc-demo --name=svc-demo-np --type=NodePort --port=80
NODE_PORT=$(kubectl get svc svc-demo-np -o jsonpath='{.spec.ports[0].nodePort}')
curl http://localhost:$NODE_PORT
# Cleanup:
kubectl delete deployment svc-demo
kubectl delete svc svc-demo svc-demo-np
Exercises
maxSurge: 1 and maxUnavailable: 0. Watch the rollout with kubectl rollout status. Then roll back. Explain what happens to the ReplicaSets during each operation.
kubectl exec.
Conclusion
The Kubernetes object model gives you a universal vocabulary for declaring infrastructure. Key takeaways:
- Everything is an API object with apiVersion, kind, metadata, spec, and status
- Labels are the connective tissue — Services find pods, controllers manage pods, all through label selectors
- Controllers maintain desired state — Deployments, ReplicaSets, StatefulSets, DaemonSets, Jobs
- Configuration is separated from images — ConfigMaps for settings, Secrets for sensitive data
- Services provide stable endpoints — DNS-based discovery regardless of pod IP changes
In Part 8, we'll dive deep into Kubernetes networking — the flat networking model, CNI plugins, pod-to-pod communication, and how network policies control traffic flow between services.