First exposure to Kubernetes usually means staring at 50 YAML files and not understanding which one talks to which. Helm chart renders make it worse. This guide explains the four fields every manifest shares (apiVersion / kind / metadata / spec), the resource kinds you'll actually meet, how a Service finds its Pods, and the gotchas that bite people new to K8s.
The shape every manifest shares
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: production
labels:
app: web
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.27Four top-level fields:
- apiVersion — which API group/version owns this resource. Examples:
v1(core),apps/v1(Deployment etc.),networking.k8s.io/v1(Ingress). - kind — the resource type:
Deployment,Service,ConfigMap, etc. - metadata — identifying info:
name,namespace,labels,annotations. - spec — desired state. The actual configuration. Structure varies wildly per kind.
A fifth field, status, is populated by the cluster and not part of authored manifests.
Why YAML?
The Kubernetes API itself speaks JSON. kubectl apply -f converts YAML to JSON before sending. YAML is preferred because:
- It supports comments — runtime notes alongside the spec.
- Multi-line literals (
|,>) make embedding certs and scripts easy. - Less visual noise than JSON — fewer quotes.
But indentation is significant, tabs vs spaces bite, and anchors (&name / *name) misfire when you don't intend them. Round-trip with YAML ↔ JSON to see exactly what JSON structure your YAML produces.
Core resources — 14 in 4 groups
Workloads — run containers
- Deployment — stateless services. The default for most apps. Replica count + pod template + rolling updates.
- StatefulSet — stateful (databases, queues). Stable pod names (
web-0,web-1) and persistent PVCs. - DaemonSet — one pod per node (logging agents, network plugins).
- Job — run-once-and-finish (migrations, batch work).
- CronJob — scheduled Job. Linux cron syntax.
- Pod — the smallest unit, one or more containers. Rarely created directly; the workloads above create them.
Networking
- Service — a stable virtual IP + DNS for a set of pods. Three types:
ClusterIP(internal),NodePort(node port),LoadBalancer(cloud LB). - Ingress — HTTP/HTTPS routing. Host + path → Service.
- NetworkPolicy — pod-to-pod firewall rules.
Configuration, secrets, storage
- ConfigMap — key/value configuration (env vars, files).
- Secret — secrets (base64-encoded, not encrypted by default — needs etcd encryption).
- PersistentVolumeClaim (PVC) — request for persistent disk. A StorageClass provisions the actual PV.
Scaling and access control
- HorizontalPodAutoscaler (HPA) — auto-scales replicas on CPU/memory/custom metrics.
- ServiceAccount — identity for a pod when it calls the K8s API.
- Role / RoleBinding — RBAC. Defines what a ServiceAccount can do.
- Namespace — logical isolation (dev/staging/prod, per team).
To see all of these in one view, drop your manifests into Kubernetes YAML Visualizer for an instant resource-and-relationship graph.
The heart of K8s — how resources connect
The most confusing part of K8s is "which pods does this Service target?" and "which ConfigMap does this Deployment use?" The answer is rarely a direct name reference — it's almost always a label selector.
Service → Pod: label selector
# Pod template labels
apiVersion: apps/v1
kind: Deployment
metadata: {name: web}
spec:
selector:
matchLabels: {app: web, tier: frontend}
template:
metadata:
labels: {app: web, tier: frontend} # ← these labels
spec: {containers: [...]}
---
# Service selector matches the labels above
apiVersion: v1
kind: Service
metadata: {name: web}
spec:
selector: {app: web, tier: frontend} # ← every key=value must match
ports: [{port: 80, targetPort: 8080}]Important: Service's selector and Deployment's spec.selector.matchLabels are different things.
- Deployment.spec.selector.matchLabels — which pods this Deployment claims as its own.
- Deployment.spec.template.metadata.labels — the labels actually applied to created pods.
- Service.spec.selector — which pods this Service routes traffic to.
Deployment's two selectors must match exactly (otherwise apply rejects). The Service's selector matches if it's a subset of the pod labels.
Ingress → Service: direct name reference
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: {name: web}
spec:
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web # ← direct Service name
port: {number: 80}Deployment → ConfigMap / Secret: three ways
How pods consume ConfigMaps and Secrets:
spec:
containers:
- name: web
# Way 1: envFrom — all keys as env variables
envFrom:
- configMapRef: {name: web-config}
- secretRef: {name: web-secrets}
# Way 2: env.valueFrom — pick specific keys
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: web-secrets
key: database-url
# Way 3: volumeMounts — mount as files
volumeMounts:
- name: config
mountPath: /etc/web
volumes:
- name: config
configMap: {name: web-config}Deployment → PVC: volumes
spec:
containers:
- name: db
volumeMounts:
- {name: data, mountPath: /var/lib/db}
volumes:
- name: data
persistentVolumeClaim: {claimName: db-data}HPA → Deployment: scaleTargetRef
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: {name: web-hpa}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web
minReplicas: 3
maxReplicas: 10Multi-doc YAML — what --- means
Multiple resources in one file are separated by --- — the K8s convention.
apiVersion: apps/v1
kind: Deployment
metadata: {name: web}
spec: {...}
---
apiVersion: v1
kind: Service
metadata: {name: web}
spec: {...}
---
apiVersion: v1
kind: ConfigMap
metadata: {name: web-config}
data: {...}kubectl apply -f all.yaml applies all three. Helm and Kustomize renders are usually multi-doc YAML too.
Namespaces
Resources sharing a metadata.namespace auto-connect. Calling a Service in another namespace needs the FQDN: web.production.svc.cluster.local.
Omitting namespace defaults to default. Production environments should split explicitly — production, staging, per-team, etc.
Common pitfalls
1. selector ↔ labels mismatch
One character difference in a Service's selector versus the pod's labels means zero traffic. The "why am I getting 503s?" classic. Kubernetes YAML Visualizer validates the match — if there's no arrow between them, you have a problem.
2. Deployment's two selectors don't match
spec.selector.matchLabels and spec.template.metadata.labels must match. kubectl apply rejects mismatches outright — confusing at first sight.
3. ConfigMap / Secret updates don't restart pods
Updating a ConfigMap or Secret doesn't auto-restart pods that consume it via envFrom. Use kubectl rollout restart or embed a checksum annotation in the manifest to force restart on change.
4. Secret base64 ≠ encryption
data: values being base64 makes them feel encrypted. They're not — just encoded. Real secret protection requires separate measures (etcd encryption at rest, external secrets, Sealed Secrets, etc.).
5. Forgetting namespace
A production manifest applied without a namespace lands in default. Always set it in metadata, or pass -n prod to kubectl.
6. Wrong apiVersion
Deprecated versions like extensions/v1beta1 eventually vanish in cluster upgrades. Use stable groups like networking.k8s.io/v1.
7. Duplicate resources
Same kind + namespace + name defined multiple times — the last one wins. Unless you intend Kustomize-style patching, this is a bug.
8. Missing resource limits
Without resources.limits, a pod can consume the entire node's memory. Set both requests and limits as a default.
The practical workflow
- Write manifests or render a Helm chart (
helm template). - Paste into Kubernetes YAML Visualizer to verify the wiring is what you intended.
kubectl apply --dry-run=server -ffor schema validation.kubectl apply -fto apply.kubectl get all -n <ns>to verify.
Helm vs Kustomize — the big picture
- Helm — chart (template + values) → render → manifests. Versioning and release management. The most common deployment tool.
- Kustomize — base manifests + overlays (patches). Models per-environment differences clearly. Built into
kubectl apply -k. - Raw YAML — small projects, learning. Per-env divergence is hard to manage.
All three eventually produce manifest YAML. Understanding the result is the foundation.
Summary
- Every manifest = apiVersion + kind + metadata + spec.
- The key relationships use label selectors — Service ↔ pod labels. Direct name references are for Ingress→Service, HPA→Workload, volume claims, etc.
- ConfigMap / Secret inject via three patterns: envFrom, env.valueFrom, or volumes.
- Multi-doc files use
---. Namespaces isolate. - Secret base64 is not encryption. Layer etcd encryption or external secrets on top.
- When handed a stack of manifests, paste into Kubernetes YAML Visualizer for a five-minute map of the system.