Now that you understand K8s’s architecture, it’s time to get to know the most core API objects in K8s: Pod, Deployment and Service, plus ConfigMap and Secret, which manage configuration and secrets. These five are the concepts you’ll use most often in your day-to-day dealings with K8s; once you’ve got them clear, you’ve officially got your foot in the K8s door.
1. Pod — the Smallest Scheduling Unit in K8s
If you ask someone who has just learned Docker, “what’s the basic unit of a container in K8s?”, their most likely answer is “a container”. But that answer is wrong.
A Pod is the smallest unit of creation and management in K8s; a Pod can contain one or more containers.
Why Do Pods Exist? Why Not Just Schedule Containers Directly?
This is the key question for understanding K8s’s design. Docker can already run containers — why does K8s wrap another layer, the Pod, on top of containers?
The answer lies in this: some containers are born as “conjoined twins” that need to share resources and work in close coordination.
For example: a web application container needs a log collection agent to push logs to Elasticsearch. The two containers need to share a filesystem (the web container writes logs, the agent reads them) and share the network namespace (communicating over localhost). If they were independent containers, that sharing would be very hard to achieve.
The Pod solves this problem: all containers within the same Pod share:
- Network namespace: they share IP and port space, and containers talk to each other over
localhost. - Storage volume (Volume): files can be shared between containers.
- Lifecycle: they live and die together, scheduled onto the same Node.
This pattern is called the Sidecar pattern, and the log collection agent is the web container’s sidecar — they share the Pod’s “body” and collaborate to accomplish one task.
The YAML Definition of a Pod
apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
labels:
app: myapp
version: v1
spec:
containers:
- name: myapp
image: harbor.yourcompany.com/backend/myapp:v1.0
ports:
- containerPort: 8080
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
Let me explain line by line:
apiVersion: the API version. K8s APIs are grouped, and Pod belongs to thev1core group.kind: the resource type, herePod.metadata.name: the Pod’s name, which must be unique within the same Namespace.metadata.labels: labels — one of K8s’s most powerful organisational mechanisms (covered later).spec.containers: the list of containers. A Pod can have multiple containers.resources.requests: the baseline resources the container requests; the Scheduler uses this to pick a Node.resources.limits: the resource ceiling for the container. Exceeding the limit gets CPU throttled and memory OOM Killed.
Key point: requests determines scheduling, limits determines the runtime cap. If memory requests is 256Mi but limits is 512Mi, K8s finds an adequate Node based on 256Mi, but at runtime the container is allowed to use up to 512Mi.
Common Pod States
| State | Meaning |
|---|---|
Pending |
The Pod has been created but the containers haven’t started yet (it may be pulling images or waiting to be scheduled) |
Running |
The Pod is bound to a Node and at least one container is running |
Succeeded |
All containers exited normally (exit code 0) |
Failed |
At least one container exited abnormally |
CrashLoopBackOff |
The container repeatedly crashes and restarts |
CrashLoopBackOff is the state beginners run into most — the container starts, then crashes, K8s restarts it, it crashes again, restarts again, and the interval between restarts keeps growing. When you see this state, it means the container’s start command has a problem or the image itself won’t run at all.
2. Deployment — the Pod’s Housekeeper
Creating Pods directly is rarely used in production — a Pod has no self-healing ability of its own, and if the Node goes down the Pod is gone.
A Deployment is the Pod’s manager: it ensures that a specified number of Pod replicas are always running. You tell it “I want 3 replicas” and it maintains those 3 for you — create more when there are too few, delete them when there are too many, and rebuild on another Node if one goes down.
The YAML Definition of a Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-deployment
labels:
app: myapp
spec:
replicas: 3 # desired number of replicas
selector:
matchLabels:
app: myapp # selector: manage Pods with the app=myapp label
template: # Pod template
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: harbor.yourcompany.com/backend/myapp:v1.0
ports:
- containerPort: 8080
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
Note that selector.matchLabels and template.metadata.labels must match. The Deployment uses labels to find “which Pods are mine”. If the labels don’t match, the Deployment will keep creating new Pods (thinking the old ones don’t exist), causing a resource leak.
Everyday Deployment Operations
# Create
kubectl apply -f deployment.yaml
# Inspect
kubectl get deployments
kubectl describe deployment myapp-deployment
# Scale up / scale down
kubectl scale deployment myapp-deployment --replicas=5
# Update the image
kubectl set image deployment/myapp-deployment myapp=harbor.yourcompany.com/backend/myapp:v2.0
# Check the rollout status
kubectl rollout status deployment/myapp-deployment
# Roll back
kubectl rollout undo deployment/myapp-deployment
Labels — the Glue of K8s
Labels are an extremely important organisational mechanism in K8s. They’re simply key-value pairs stuck onto any K8s object. Objects are then filtered and linked through a Label Selector.
For example, one Deployment managing two groups of Pods at different versions:
# the Pod's labels
labels:
app: myapp
version: v1
env: staging
# the Selector can pick
selector:
matchLabels:
app: myapp
# or something more complex:
matchExpressions:
- {key: version, operator: In, values: [v1, v2]}
Labels take K8s’s decoupling to the extreme — the Deployment doesn’t care what the Pods are named or where they’re created, only about “are there 3 Pods with the app=myapp label”.
3. Service — a Stable Access Entry Point
With a Deployment managing the Pods, you’d think everything is taken care of. But a Pod’s IP is temporary — the IP changes after the Pod is rebuilt. If other services access the Pod IP directly, they lose contact the moment the Pod is rebuilt.
A Service is what solves this: it provides a stable access entry point for the group of Pods behind it.
Service Modes
A Service has several types; the most common is ClusterIP:
apiVersion: v1
kind: Service
metadata:
name: myapp-service
spec:
type: ClusterIP # only reachable inside the cluster
selector:
app: myapp # select the backend Pods
ports:
- port: 80 # the Service's port
targetPort: 8080 # the backend Pod's port
This Service gets a fixed virtual IP (ClusterIP) within the cluster, and any traffic from a Pod to myapp-service:80 is forwarded by kube-proxy to port 8080 of one of the backend Pods labelled app=myapp.
Comparing the Three Service Types
| Type | Access scope | Use case |
|---|---|---|
| ClusterIP | Inside the cluster | Service-to-service calls; the default type |
| NodePort | NodeIP:NodePort |
Development and debugging; simple external exposure |
| LoadBalancer | External LB → Node | External exposure in production (requires cloud provider support) |
DNS Service Discovery
K8s has a built-in DNS service (CoreDNS). When a Service is created, a DNS record is automatically registered for it:
<service-name>.<namespace>.svc.cluster.local
So from any container inside the cluster, you can access it directly by the Service name:
# without needing to know what the ClusterIP is
curl http://myapp-service:80/health
This solves the most central problem of service discovery: you don’t need to know where the other party is, only what it’s called.
4. ConfigMap and Secret — Configuration and Secret Management
By now you’ve got hold of K8s’s “compute trio”: Pod runs the containers, Deployment manages the replicas, Service manages access. But one problem remains unsolved: how do we manage configuration and secrets?
Hard-coding sensitive information like database addresses, API keys and certificates into the image? Absolutely not. Not only does it violate the 12-Factor principles (configuration should be separated from code), it also creates a risk of secret leakage.
K8s provides two dedicated API objects to solve this: ConfigMap (for ordinary configuration) and Secret (for sensitive information).
4.1 ConfigMap — Centralised Management of Non-Sensitive Configuration
A ConfigMap stores configuration data in the form of key-value pairs, such as environment variables and configuration file contents.
Several ways to create a ConfigMap:
# Way one: create directly from key-value pairs
kubectl create configmap app-config \
--from-literal=app.env=production \
--from-literal=app.log.level=INFO
# Way two: create from a file (the file contents become the value)
kubectl create configmap nginx-config \
--from-file=nginx.conf=./nginx.conf
# Way three: create in bulk from a directory (each file name becomes a key)
kubectl create configmap app-config-dir \
--from-file=./config-dir/
You can also define it in a YAML file:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
# simple key-value pairs
app.env: "production"
app.log.level: "INFO"
# a multi-line config file (note that | is the YAML syntax that preserves newlines)
nginx.conf: |
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
}
}
Using a ConfigMap in a Pod:
A ConfigMap’s data can be injected into a container in two ways: environment variables and file volume mounts.
apiVersion: v1
kind: Pod
metadata:
name: configmap-demo
spec:
containers:
- name: app
image: myapp:v1
# Way one: as environment variables
env:
- name: APP_ENV
valueFrom:
configMapKeyRef:
name: app-config
key: app.env
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: app.log.level
# inject all key-value pairs at once (the key names become the environment variable names)
envFrom:
- configMapRef:
name: app-config
# Way two: mounted as files
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
name: app-config
After mounting, files named after the ConfigMap’s keys are created in the /etc/config/ directory, with the file contents being the corresponding values.
4.2 Secret — Secure Management of Sensitive Information
The design purpose of a Secret is similar to a ConfigMap, but it’s dedicated to storing sensitive data such as passwords, tokens and certificates. K8s gives Secrets special treatment:
- Secret data is base64-encoded when stored in etcd (note: encoded, not encrypted! K8s 1.7+ supports etcd encryption at rest, but it isn’t enabled by default)
- Only Pods that need to access the Secret are mounted onto the node by the kubelet
- Inside the container’s memory, the Secret exists as tmpfs (an in-memory filesystem) and is never written to the node’s disk
Creating a Secret:
# Way one: create from key-value pairs (the value is automatically base64-encoded)
kubectl create secret generic db-secret \
--from-literal=username=admin \
--from-literal=password='S3cur3P@ss!'
# Way two: create from files (for example a TLS certificate)
kubectl create secret tls tls-secret \
--cert=./tls.crt \
--key=./tls.key
# Way three: create in bulk from files
kubectl create secret generic app-secret \
--from-file=./secrets/
Defining it in YAML (note that the value has to be base64-encoded by hand):
apiVersion: v1
kind: Secret
metadata:
name: db-secret
type: Opaque # generic type; could also be kubernetes.io/tls, etc.
data:
# note: the values here must be base64-encoded
username: YWRtaW4= # admin
password: UDNzc3cwcmQ= # don't use a weak password in real production :)
stringData: # the stringData field is automatically base64-encoded, which is more convenient
database: myapp-db
Security note:
stringDatais a convenience field introduced in K8s 1.14+; you write plaintext and K8s automatically encodes it to base64 before storing it in thedatafield. But either way, what’s stored in etcd is base64 encoding (reversible!). In production, be sure to enable etcd encryption at rest and consider using an external secret management system (such as HashiCorp Vault or AWS Secrets Manager).
Using a Secret in a Pod:
The way you use it is almost identical to a ConfigMap:
apiVersion: v1
kind: Pod
metadata:
name: secret-demo
spec:
containers:
- name: app
image: myapp:v1
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-secret
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
volumeMounts:
- name: secret-volume
mountPath: /etc/secrets
readOnly: true
volumes:
- name: secret-volume
secret:
secretName: db-secret
4.3 Usage Advice and Best Practices
| Scenario | Recommended approach | Reason |
|---|---|---|
| Small configuration, environment variables | ConfigMap → environment variables | Simple and direct; the application needs no code changes |
| Configuration files (such as nginx.conf) | ConfigMap → file mount | Supports hot reload (requires application support) |
| Database passwords, API keys | Secret → environment variables or file mount | Avoids storing sensitive information in plaintext |
| TLS certificates | Secret (type: kubernetes.io/tls) → file mount | The standard approach for Ingress Controllers |
| Large configuration files (>1MB) | Consider an external configuration centre (such as Consul or Apollo) | ConfigMaps/Secrets have a size limit (1MB) |
4.4 Echoing the 12-Factor Principles
Recall the 12-Factor principles mentioned in the first article; Factor III is Config:
Configuration should be stored in environment variables, strictly separated from code.
ConfigMap and Secret are precisely this principle’s concrete implementation in K8s. With them you can:
- Use the same image across development, testing and production, injecting different configuration only through different ConfigMaps/Secrets
- Keep sensitive information out of the image and out of the code repository
- Change configuration without rebuilding the image
# same image, different environments, different configuration
# development
kubectl apply -f configmap-dev.yaml -f deployment.yaml
# production
kubectl apply -f configmap-prod.yaml -f deployment.yaml
5. Putting the Core Concepts Together: a Complete Example
Below is a complete Nginx deployment example, including a Deployment and a Service:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.19-alpine
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "200m"
---
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app: nginx
ports:
- port: 80
targetPort: 80
Deploy and verify:
# Create
kubectl apply -f deployment.yaml
# Check the Pod distribution
kubectl get pods -o wide
# Create the Service
kubectl apply -f service.yaml
# Test: access the Service with a temporary Pod
kubectl run test --rm -it --image=busybox -- wget -qO- http://nginx-service
You’ll see the requests distributed randomly across the 3 Nginx Pods.
Summary
Pod, Deployment, Service, ConfigMap and Secret are the most core concepts in everyday K8s use: Pod is the wrapper in which containers run, Deployment manages the Pod lifecycle and replica count, Service provides a stable service entry point, and ConfigMap and Secret manage configuration and secrets. Once you understand how these objects relate to and work with one another, the door to K8s is fully open.
With Pods and Services in hand, the question of how they communicate naturally arises. Next time we’ll go deep into K8s’s network model — CNI, how kube-proxy works, and how communication between Pods is implemented.
A small step forward every day is a new height!