A Kubernetes manifest is the source code of cloud infrastructure. Written in YAML, it describes the desired state of your applications, networks, storage, and security policies. When you submit a manifest, Kubernetes compares your declared intention against current reality and makes adjustments.
Manifests are not imperative instructions ("do this, then that"). They are declarative descriptions: "I want 3 replicas of this application running, with these resource limits, using this configuration, exposed via this service."
graph TB YAML["Manifest.yaml<br/>Desired State"] YAML -->|kubectl apply| API["Kubernetes<br/>API Server"] API --> Compare["Compare with<br/>Current State"] Compare --> Reconcile["Reconciliation<br/>Loop"] Reconcile -->|Create/Update| Resources["Resources<br/>Deployments<br/>Services<br/>ConfigMaps"] Resources -->|Monitor| Desired["Maintain<br/>Desired State"] Desired --> Status["Status Update<br/>feedback to manifest"]What Is a Manifest?
A Kubernetes manifest is a text file (YAML or JSON) that describes one or more Kubernetes resources.
Basic structure:
apiVersion: v1kind: Podmetadata: name: my-podspec: containers: - name: app image: myapp:latestThis describes a single resource: a Pod named "my-pod" running the "myapp:latest" container image.
apiVersion: The Kubernetes API version (v1, apps/v1, batch/v1, etc.) kind: The resource type (Pod, Deployment, Service, ConfigMap, etc.) metadata: Identifying information (name, namespace, labels) spec: The desired state (what to create, how to configure it)
A single manifest file can contain multiple resources separated by ---:
---apiVersion: v1kind: ConfigMapmetadata: name: app-configdata: setting1: value1---apiVersion: v1kind: Secretmetadata: name: app-secretstype: Opaquedata: api_key: c2tyLWFiYzEyMz==---apiVersion: v1kind: Servicemetadata: name: app-servicespec: selector: app: myapp ports: - port: 80 targetPort: 8000---apiVersion: apps/v1kind: Deploymentmetadata: name: myapp-deploymentspec: replicas: 3 selector: matchLabels: app: myapp template: metadata: labels: app: myapp spec: containers: - name: app image: myapp:latestThis single file defines 4 resources that work together: configuration, secrets, service, and deployment.
Core Resource Types
Kubernetes has dozens of resource types. These are the most common and important.
Pod
The smallest deployable unit in Kubernetes. A pod is one or more containers (usually one) running together on the same node.
apiVersion: v1kind: Podmetadata: name: app-pod namespace: production labels: app: myapp version: 1.0spec: containers: - name: app image: myapp:latest ports: - containerPort: 8000 - name: sidecar image: sidecar:latestPods are ephemeral: they're created, run, and deleted. You rarely create pods directly; instead, you create higher-level resources (Deployments) that manage pods for you.
Deployment
A Deployment manages multiple pod replicas, rolling updates, and rollbacks.
apiVersion: apps/v1kind: Deploymentmetadata: name: web-appspec: replicas: 3 # Keep 3 pods running at all times selector: matchLabels: app: web template: # Template for creating pods metadata: labels: app: web spec: containers: - name: web image: webapp:v2.1.0 ports: - containerPort: 8000 resources: requests: memory: "256Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m"When you create a Deployment, Kubernetes:
- Creates a ReplicaSet (which manages multiple pod copies)
- The ReplicaSet creates 3 Pods
- Watches continuously: if a pod dies, create a new one
- On image update: performs a rolling update (replace pods gradually)
Deployments are the standard way to run applications in Kubernetes.
Service
A Service exposes pods to the network. It provides a stable IP and DNS name even as pods are created and destroyed.
apiVersion: v1kind: Servicemetadata: name: web-servicespec: selector: app: web # Route traffic to pods with label app=web type: ClusterIP # Internal only (default) ports: - port: 80 # Incoming port targetPort: 8000 # Pod container port protocol: TCPTypes of Services exist in three main flavors. ClusterIP is the default, providing internal-only access where pods can reach each other. NodePort offers external access via the node IP and a port in the range 30000 and above. LoadBalancer provides an external IP from the cloud provider, though this option is slower and more expensive than the alternatives.
When you create this Service, Kubernetes:
- Assigns it a stable internal IP (e.g., 10.0.1.234)
- Creates DNS entry: web-service.default.svc.cluster.local
- Any pod can reach other pods via the service:
curl http://web-service - Traffic is load-balanced across all matching pods
ConfigMap
ConfigMaps store non-secret configuration data (strings, files, environment variables).
apiVersion: v1kind: ConfigMapmetadata: name: app-configdata: database_host: "postgres.default.svc.cluster.local" database_port: "5432" log_level: "debug" nginx.conf: | server { listen 8000; location / { ... } }Use in pods:
apiVersion: v1kind: Podmetadata: name: appspec: containers: - name: app image: myapp:latest env: - name: DB_HOST valueFrom: configMapKeyRef: name: app-config key: database_host volumeMounts: - name: config mountPath: /etc/nginx volumes: - name: config configMap: name: app-configThis injects config values as environment variables and mounts the nginx.conf as a file.
Secret
Secrets store sensitive data (passwords, API keys, tokens, certificates).
apiVersion: v1kind: Secretmetadata: name: db-credentialstype: Opaque # Generic key-value secretdata: username: dXNlcm5hbWU= # base64 encoded "username" password: cGFzc3dvcmQx # base64 encoded "password1"---apiVersion: v1kind: Secretmetadata: name: tls-certtype: kubernetes.io/tlsdata: tls.crt: LS0tLS1CRUdJTi... # Certificate (base64) tls.key: LS0tLS1CRUdJTi... # Private key (base64)Important: Secrets are NOT encrypted by default (only base64-encoded). For production, enable encryption at rest:
# In /etc/kubernetes/manifests/kube-apiserver.yaml--encryption-provider-config=/etc/kubernetes/encryption.yamlUse in pods:
spec: containers: - name: app env: - name: DB_PASSWORD valueFrom: secretKeyRef: name: db-credentials key: password volumeMounts: - name: tls mountPath: /etc/tls readOnly: true volumes: - name: tls secret: secretName: tls-certNamespace
Namespaces provide logical isolation. Resources in different namespaces can have the same name.
apiVersion: v1kind: Namespacemetadata: name: production---apiVersion: v1kind: Namespacemetadata: name: stagingThen create resources in a namespace:
kubectl apply -f manifest.yaml -n productionkubectl apply -f manifest.yaml -n stagingEach namespace can have its own quotas, network policies, and service accounts.
Ingress
An Ingress routes HTTP traffic to services based on hostname and path.
apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: web-ingressspec: rules: - host: api.example.com http: paths: - path: /v1 pathType: Prefix backend: service: name: api-service port: number: 8000 - path: /v2 pathType: Prefix backend: service: name: api-v2-service port: number: 8000 - host: admin.example.com http: paths: - path: / pathType: Prefix backend: service: name: admin-service port: number: 3000 tls: - hosts: - api.example.com - admin.example.com secretName: tls-certIngress requires an "ingress controller" (nginx, Traefik, etc.) to actually route traffic. Different cloud providers offer their own ingress implementations.
Deployment Anatomy: The Most Important Resource
Deployments are the workhorse of Kubernetes. Understanding their structure is critical.
apiVersion: apps/v1kind: Deploymentmetadata: name: web-app namespace: production labels: app: web tier: frontendspec: replicas: 3 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 # Max 1 extra pod during update maxUnavailable: 0 # Keep all pods available selector: matchLabels: app: web template: metadata: labels: app: web version: v2.1.0 spec: serviceAccountName: web-sa securityContext: runAsNonRoot: true runAsUser: 65532 containers: - name: web image: webapp:v2.1.0 imagePullPolicy: IfNotPresent ports: - containerPort: 8000 name: http env: - name: LOG_LEVEL value: "info" - name: DB_PASSWORD valueFrom: secretKeyRef: name: db-secret key: password resources: requests: memory: "256Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m" livenessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 30 periodSeconds: 10 failureThreshold: 3 readinessProbe: httpGet: path: /ready port: 8000 initialDelaySeconds: 5 periodSeconds: 5 volumeMounts: - name: config mountPath: /etc/config readOnly: true volumes: - name: config configMap: name: app-configKey sections:
replicas: Number of pod copies to maintain (3 copies of the application)
strategy: How to update. Rolling updates gradually replace old pods with new ones (zero downtime).
selector: Which pods this deployment manages (pods with label app: web)
template: Template for creating pods. This is a full Pod spec.
containers: The container(s) in each pod. Most pods have one container; some have multiple (sidecars).
env: Environment variables passed to the container
resources: CPU and memory requests (for scheduling) and limits (hard cap)
livenessProbe: Is the container still running? If probe fails 3 times in a row, restart the container.
readinessProbe: Is the container ready for traffic? If not, remove it from the load balancer temporarily (for live updates).
volumeMounts and volumes: Storage and config injection
Pod Spec Deep Dive
The pod spec (the spec: section inside template:) is where most configuration happens.
Init Containers
Init containers run before the main container, used for setup.
spec: initContainers: - name: wait-for-db image: busybox:1.35 command: ['sh', '-c', "until nc -z postgres:5432; do sleep 1; done"] containers: - name: app image: webapp:latestThe init container checks if PostgreSQL is listening on port 5432. Only after it succeeds does the main app container start.
Security Context
Security context defines privilege and access control settings.
spec: securityContext: runAsNonRoot: true runAsUser: 65532 fsGroup: 65532 containers: - name: app image: webapp:latest securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: - ALL add: - NET_BIND_SERVICEThe runAsNonRoot setting enforces that the container must run as a non-root user. The runAsUser setting specifies a particular UID to run as, with 65532 being the standard choice. The allowPrivilegeEscalation setting prevents sudo and similar privilege escalation techniques from working. The readOnlyRootFilesystem setting makes the root filesystem read-only, with only /tmp and /var/run remaining writable. The capabilities setting drops all Linux capabilities and allows you to add back only the specific ones your application needs.
Resources: Requests and Limits
Resources tell Kubernetes how to schedule pods and when to kill them.
spec: containers: - name: app resources: requests: memory: "256Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m"Requests: Minimum guaranteed resources. Kubernetes won't schedule the pod on a node unless that much is available. Used for scheduling decisions.
Limits: Maximum resources allowed. If the container exceeds limits, Kubernetes terminates it (OOMKilled for memory, throttled for CPU).
Units for memory are expressed in mebibytes (Mi) and gibibytes (Gi), while CPU is measured in millicores (m) where 1000m equals 1 full core.
Rule of thumb: Set requests = expected usage, limits = 2x requests (allow for spikes but kill if runaway).
Probes: Health Checks
Kubernetes uses probes to determine if containers are healthy.
Liveness probe: Is the container still running? If probe fails repeatedly, restart it.
livenessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 30 # Wait 30s before first check periodSeconds: 10 # Check every 10s failureThreshold: 3 # Restart after 3 failuresReadiness probe: Is the container ready to serve traffic? If probe fails, remove it from the load balancer.
readinessProbe: httpGet: path: /ready port: 8000 initialDelaySeconds: 5 periodSeconds: 5 failureThreshold: 1Startup probe: Is the container fully started (for slow-starting apps)?
startupProbe: httpGet: path: /startup port: 8000 failureThreshold: 30 periodSeconds: 10 # Total: 5 minutes to startProbe types include httpGet which calls an HTTP endpoint and expects a response code between 200-399, exec which runs a command in the container and expects an exit code of 0, and tcpSocket which opens a TCP connection and expects success. All three types are valid, and you should use whichever fits your application best.
Labels, Selectors, and Annotations
Labels are key-value pairs attached to resources. They're used for organization and selection.
apiVersion: v1kind: Podmetadata: name: web-pod labels: app: web # Label 1 version: v2 # Label 2 tier: frontend # Label 3 env: production # Label 4Selectors match labels:
# Service selector: route traffic to pods with app=webspec: selector: app: web # Or more complex:selector: matchLabels: app: web env: production matchExpressions: - key: version operator: In values: [v2, v3] # Match v2 OR v3Annotations are like labels but for humans and tools (not used by Kubernetes for selection).
metadata: annotations: description: "Production web server" contact: "team@example.com" image.repository: "gcr.io/myproject/webapp" security.scan: "passed"Namespace Isolation
Namespaces provide logical separation. Resources in different namespaces can have the same name.
apiVersion: v1kind: Namespacemetadata: name: production---apiVersion: v1kind: Namespacemetadata: name: stagingDeploy to a specific namespace:
kubectl apply -f deployment.yaml -n productionkubectl apply -f deployment.yaml -n stagingPods in production can reach each other: curl http://service.production.svc.cluster.local
Pods in production can reach pods in staging: curl http://service.staging.svc.cluster.local (but only if network policies allow)
Namespaces are useful for multiple purposes including separating environments such as prod, staging, and dev, isolating different teams so they can work independently, setting resource quotas per namespace to control resource consumption, and applying policies like network policies and RBAC at the namespace level.
Applying Manifests: From YAML to Running Containers
How does a manifest become running containers?
kubectl apply -f deployment.yamlKubernetes processes:
- Parse YAML: Convert to objects
- Validate: Check syntax and required fields
- Compare: Current state vs desired state
- Create/Update: Make changes as needed
- Watch: Monitor and maintain desired state
kubectl apply is declarative: it reconciles the desired state (from the manifest) with the current state (in the cluster). If the manifest changes, running kubectl apply again updates the cluster to match.
kubectl create is imperative: it fails if the resource already exists.
Always use kubectl apply for production. It's idempotent: applying the same manifest 10 times is the same as applying it once.
Common Manifest Patterns
Environment-Specific Configurations
Different configs for dev, staging, prod:
A typical Kustomize directory structure organizes files into a base directory containing the core manifests (deployment.yaml, service.yaml, and configmap.yaml), with separate overlay directories for each environment. The dev overlay contains its own kustomization.yaml and configmap.yaml overrides, the staging overlay has similar environment-specific kustomization and config, and the prod overlay applies production-specific configurations. This structure allows the base manifests to remain unchanged while environment-specific variations are applied via the overlays.
Deploy to different environments:
kubectl apply -k manifests/overlays/devkubectl apply -k manifests/overlays/stagingkubectl apply -k manifests/overlays/prod(This uses Kustomize, a tool for overlaying manifest changes.)
Helm Charts
Helm is a package manager for Kubernetes. Instead of writing raw YAML, you define values and Helm generates manifests.
# values.yamlreplicaCount: 3image: repository: webapp tag: v2.1.0resources: limits: memory: 512Mi cpu: 500mhelm install my-release ./webapp-chart -f values.yamlHelm generates manifests from templates and applies them.
DaemonSet (One Pod Per Node)
Run a pod on every node (logging, monitoring):
apiVersion: apps/v1kind: DaemonSetmetadata: name: filebeatspec: selector: matchLabels: app: filebeat template: metadata: labels: app: filebeat spec: containers: - name: filebeat image: filebeat:latestStatefulSet (Stateful Applications)
For applications requiring stable identities and persistent storage (databases, message queues):
apiVersion: apps/v1kind: StatefulSetmetadata: name: postgresspec: serviceName: postgres replicas: 1 selector: matchLabels: app: postgres template: metadata: labels: app: postgres spec: containers: - name: postgres image: postgres:15 volumeMounts: - name: data mountPath: /var/lib/postgresql volumeClaimTemplates: - metadata: name: data spec: accessModes: [ReadWriteOnce] resources: requests: storage: 10GiPods are numbered (postgres-0, postgres-1, etc.) and each gets its own persistent volume.
Common Mistakes in Manifests
Mistake 1: Using latest tag.
# BADimage: webapp:latest # Unpredictable, rebuilds pull different versions # GOODimage: webapp:v2.1.0 # Specific versionimage: webapp@sha256:abc123... # Specific digestMistake 2: No resource limits.
# BADcontainers:- name: app image: webapp:latest # No limits; app can consume all cluster resources # GOODcontainers:- name: app image: webapp:latest resources: requests: memory: 256Mi cpu: 100m limits: memory: 512Mi cpu: 500mMistake 3: No probes.
# BADcontainers:- name: app image: webapp:latest # No health checks; Kubernetes doesn't know if app is healthy # GOODcontainers:- name: app image: webapp:latest livenessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /ready port: 8000 initialDelaySeconds: 5 periodSeconds: 5Mistake 4: Hardcoded configuration and secrets.
# BADenv:- name: DB_PASSWORD value: "password123" # Secrets in manifest! # GOODenv:- name: DB_PASSWORD valueFrom: secretKeyRef: name: db-secret key: passwordMistake 5: Not setting requests (Kubernetes can't schedule properly).
# BADcontainers:- name: app image: webapp:latest # No requests; scheduler doesn't know memory/CPU needs # GOODresources: requests: memory: 256Mi cpu: 100mSummary: Manifests Are Code
Treat Kubernetes manifests like code by storing them in Git, code reviewing all pull requests before merging, using templating tools like Kustomize or Helm to reduce duplication across manifests, testing in staging environments before promoting to production, monitoring and alerting on deployment failures, and maintaining rollback procedures for when issues arise.
Manifests are the declarative specification of your infrastructure. They're as important as application code and should be treated with equal rigor.
The Kubernetes API has 60+ resource types and hundreds of configuration options. This guide covers the most important. For complete reference, consult the Kubernetes API documentation at kubernetes.io/docs.
