Kubernetes YAML for Beginners: Pods, Deployments, Services
Learn how to write Kubernetes YAML manifests for Pods, Deployments, and Services. Covers labels, selectors, common patterns, and practical examples.
Kubernetes runs your containers, but you talk to it through YAML. Every pod, deployment, service, and config map is described by a YAML manifest that tells Kubernetes what you want. If you are new to Kubernetes, these manifests can look intimidating — but the structure is actually consistent and predictable once you understand the pattern.
This guide walks through the core resource types you need to know: Pods, Deployments, and Services. You will learn the YAML structure, what each field does, and common patterns for production use. If YAML syntax itself is new to you, our YAML vs JSON vs TOML guide is a good primer before diving in.
The Universal Structure
Every Kubernetes resource follows the same top-level structure:
apiVersion: v1 # API group and version
kind: Pod # Type of resource
metadata: # Name, namespace, labels
name: my-app
labels:
app: my-app
spec: # The desired state
# Resource-specific configuration
- apiVersion: Tells Kubernetes which API to use. Core resources use
v1. Deployments useapps/v1. - kind: The resource type. Kubernetes uses this with apiVersion to determine how to process the manifest.
- metadata: Identification information — name, namespace, labels, and annotations.
- spec: The actual configuration. This is where the resource-specific details go.
Pods: The Smallest Unit
A Pod is the smallest deployable unit in Kubernetes. It wraps one or more containers that share the same network namespace and storage volumes.
apiVersion: v1
kind: Pod
metadata:
name: web-server
labels:
app: web-server
environment: development
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "250m"
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
Key Pod Fields
| Field | Purpose |
|---|---|
containers[].name |
Identifier for the container within the pod |
containers[].image |
Docker image and tag to run |
containers[].ports |
Ports the container exposes |
containers[].resources |
CPU and memory requests/limits |
containers[].readinessProbe |
Check if the container is ready to serve traffic |
containers[].livenessProbe |
Check if the container is still alive |
containers[].env |
Environment variables |
containers[].volumeMounts |
Where to mount volumes inside the container |
Why You Rarely Create Pods Directly
In practice, you almost never create bare Pods. If a Pod crashes, nothing restarts it. If the node it runs on fails, the Pod is gone. Instead, you use higher-level controllers like Deployments that manage Pods for you.
Deployments: Managing Pod Replicas
A Deployment manages a set of identical Pods. It handles scaling, rolling updates, and self-healing. When you tell a Deployment you want three replicas, it makes sure three Pods are always running.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
labels:
app: web-app
spec:
replicas: 3
selector:
matchLabels:
app: web-app
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: web-app
spec:
containers:
- name: web
image: my-registry/web-app:1.2.0
ports:
- containerPort: 8080
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
- name: LOG_LEVEL
value: "info"
resources:
requests:
memory: "128Mi"
cpu: "200m"
limits:
memory: "256Mi"
cpu: "500m"
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 15
Understanding the Deployment Structure
The Deployment has three nested layers:
- Deployment spec (
spec.replicas,spec.selector,spec.strategy) — controls how many Pods and how to update them - Pod template (
spec.template) — the blueprint for each Pod the Deployment creates - Container spec (
spec.template.spec.containers) — the actual container configuration
Labels and Selectors
Labels are key-value pairs attached to resources. Selectors use labels to identify which resources to manage.
spec:
selector:
matchLabels:
app: web-app # "I manage Pods with this label"
template:
metadata:
labels:
app: web-app # "Give new Pods this label"
The selector must match the template labels. This is how the Deployment knows which Pods belong to it. If they do not match, Kubernetes rejects the manifest.
Update Strategies
The strategy field controls how Deployments roll out changes:
- RollingUpdate (default): Gradually replaces old Pods with new ones.
maxSurgecontrols how many extra Pods can exist during the update.maxUnavailablecontrols how many Pods can be down. - Recreate: Kills all existing Pods before creating new ones. Use this when you cannot have two versions running simultaneously (e.g., database migrations).
You can generate Kubernetes YAML quickly using our Kubernetes YAML generator instead of writing it from scratch every time.
Services: Exposing Your Pods
Pods get IP addresses, but those addresses change every time a Pod is recreated. A Service provides a stable network endpoint that routes traffic to the right Pods using label selectors.
ClusterIP Service (Default)
Exposes the service on an internal cluster IP. Only reachable from within the cluster.
apiVersion: v1
kind: Service
metadata:
name: web-app-service
spec:
type: ClusterIP
selector:
app: web-app # Routes to Pods with this label
ports:
- protocol: TCP
port: 80 # Port the service listens on
targetPort: 8080 # Port on the container
Other Pods in the cluster can reach this service at web-app-service:80 or web-app-service.default.svc.cluster.local:80.
NodePort Service
Exposes the service on a static port on every node. Reachable from outside the cluster via <NodeIP>:<NodePort>.
apiVersion: v1
kind: Service
metadata:
name: web-app-nodeport
spec:
type: NodePort
selector:
app: web-app
ports:
- protocol: TCP
port: 80
targetPort: 8080
nodePort: 30080 # External port (30000-32767)
LoadBalancer Service
Creates an external load balancer (on cloud providers) that routes traffic to your Pods.
apiVersion: v1
kind: Service
metadata:
name: web-app-lb
spec:
type: LoadBalancer
selector:
app: web-app
ports:
- protocol: TCP
port: 80
targetPort: 8080
Choosing a Service Type
| Type | Use Case |
|---|---|
| ClusterIP | Internal services, microservice communication |
| NodePort | Development, on-premise deployments |
| LoadBalancer | Production, cloud environments |
| ExternalName | Alias for an external DNS name |
Environment Variables and ConfigMaps
Hard-coding configuration in your Deployment is a bad idea. Use ConfigMaps for non-sensitive configuration and Secrets for sensitive data.
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
LOG_LEVEL: "info"
MAX_CONNECTIONS: "100"
FEATURE_FLAGS: |
{
"new_ui": true,
"beta_api": false
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
# ...
template:
spec:
containers:
- name: web
image: my-registry/web-app:1.2.0
envFrom:
- configMapRef:
name: app-config
The --- separator lets you define multiple resources in one file.
Common Patterns
Multi-Container Pod (Sidecar)
spec:
containers:
- name: app
image: my-app:1.0
ports:
- containerPort: 8080
- name: log-shipper
image: fluentd:latest
volumeMounts:
- name: logs
mountPath: /var/log/app
volumes:
- name: logs
emptyDir: {}
Init Containers
Run setup tasks before the main container starts:
spec:
initContainers:
- name: wait-for-db
image: busybox:1.36
command: ['sh', '-c', 'until nc -z postgres-service 5432; do sleep 2; done']
containers:
- name: app
image: my-app:1.0
Resource Quotas
Always set resource requests and limits. Without them, a single Pod can consume all the resources on a node.
resources:
requests: # Guaranteed minimum
memory: "128Mi"
cpu: "200m" # 200 millicores = 0.2 CPU cores
limits: # Maximum allowed
memory: "256Mi"
cpu: "500m"
Validation and Debugging
Before applying manifests, validate them:
# Dry run to check for errors
kubectl apply -f deployment.yaml --dry-run=client
# Validate against the cluster's API
kubectl apply -f deployment.yaml --dry-run=server
# Check the status after applying
kubectl get pods -l app=web-app
kubectl describe pod <pod-name>
kubectl logs <pod-name>
When converting configuration between formats during your Kubernetes work, the YAML to JSON converter is handy for transforming manifests or piping them through tools that expect JSON input.
Essential kubectl Commands
| Command | Purpose |
|---|---|
kubectl apply -f file.yaml |
Create or update resources |
kubectl get pods |
List pods |
kubectl describe pod <name> |
Detailed pod info |
kubectl logs <pod> -f |
Stream pod logs |
kubectl exec -it <pod> -- /bin/sh |
Shell into a pod |
kubectl delete -f file.yaml |
Delete resources |
kubectl rollout status deployment/<name> |
Watch rollout progress |
kubectl rollout undo deployment/<name> |
Roll back to previous version |
Summary
Kubernetes YAML follows a consistent structure across all resource types. Pods define what containers to run, Deployments manage replicas and updates, and Services provide stable network endpoints. Labels connect everything together through selectors. Start with these three resource types, add ConfigMaps and Secrets for configuration, and you have the foundation for running production workloads on Kubernetes.
Comments