# Kubernetes

> This skill MUST be loaded when ANY Kubernetes manifest file (.yml/.yaml) under a k8s/ directory is being read, reviewed, edited, or created — including Deployments, Services, ConfigMaps, Secrets, PVCs, HPAs, Namespaces, StatefulSets, Ingresses, or any resource manifest. Also use when the user asks to "create a Kubernetes deployment", "write K8s manifests", "add health probes", "configure liveness and readiness probes", "create a PersistentVolumeClaim", "set up a ConfigMap or Secret", "add an HPA", "create a K8s namespace", "write a StatefulSet", "expose a service with NodePort or ClusterIP", "set resource limits and requests", "add init containers", "configure envFrom with ConfigMap and Secret", "create a horizontal pod autoscaler", "debug pod startup issues", "fix CrashLoopBackOff", "troubleshoot ImagePullBackOff", "set up persistent storage for databases", "configure Redis persistence in K8s", "deploy a PostgreSQL StatefulSet", "add rolling update strategy", or mentions Kubernetes, K8s, kubectl, pods, deplo

- Skill: `jawwad-ali/kubernetes` (Agent Skill, multi-file: 9 files)
- Install (CLI): `npx skillmds@latest add jawwad-ali/kubernetes`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jawwad-ali/kubernetes/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: jawwad-ali (https://skillmd.com/u/jawwad-ali)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/jawwad-ali/kubernetes

---


# Kubernetes Manifest Guide

Write production-grade Kubernetes manifests that are secure, observable, and
resilient by default. Every manifest should be deployable with a single
`kubectl apply -f k8s/` and produce a working system.

## Core Principles

1. **Declarative over imperative** — define desired state in YAML; let the
   controller loop converge. Never rely on manual `kubectl` sequences for
   production state.
2. **Namespace isolation** — every project gets its own namespace. All resources
   reference it explicitly so `kubectl apply` is idempotent and scoped.
3. **Least privilege** — containers run as non-root, Secrets hold credentials,
   ConfigMaps hold non-sensitive config. Never embed secrets in manifests
   committed to source control (use placeholders + `kubectl create secret`).
4. **Resource budgeting** — every container declares `requests` (scheduling
   guarantee) and `limits` (hard ceiling). This prevents noisy-neighbor
   starvation and enables HPA.
5. **Health-first deployments** — every long-running container has a liveness
   probe (restart on hang) and a readiness probe (stop routing on dependency
   failure). Probes must be lightweight and side-effect-free.
6. **Persistent state is explicit** — databases and caches use PVCs with named
   claims. Stateless services never mount persistent volumes.

---

## Manifest Fundamentals

### API Version & Kind

Always use the stable API version for each resource:

```yaml
# Workloads
apiVersion: apps/v1          # Deployment, StatefulSet, DaemonSet
# Core resources
apiVersion: v1               # Service, ConfigMap, Secret, Namespace, PVC
# Scaling
apiVersion: autoscaling/v2   # HorizontalPodAutoscaler
# Ingress
apiVersion: networking.k8s.io/v1  # Ingress
```

### Metadata Convention

Every resource needs consistent metadata for filtering and management:

```yaml
metadata:
  name: <resource-name>
  namespace: <project-namespace>    # Always explicit — never rely on context
  labels:
    app: <service-name>             # Selector target
    component: <role>               # backend, frontend, database, cache
    part-of: <project-name>         # Groups all project resources
```

### Label Selectors

Deployments and Services find pods via label selectors. The golden rule:
**`spec.selector.matchLabels` must be a subset of `spec.template.metadata.labels`**.

```yaml
# Deployment
spec:
  selector:
    matchLabels:
      app: my-api                # ← Must match pod template labels
  template:
    metadata:
      labels:
        app: my-api              # ← Matched by selector
        component: backend       # ← Extra labels are fine

# Service targeting the same pods
spec:
  selector:
    app: my-api                  # ← Same label selects the same pods
```

---

## Namespace

Isolate project resources. Create the namespace first — everything else
references it.

```yaml
apiVersion: v1
kind: Namespace
metadata:
  name: my-project
  labels:
    app: my-project
```

Apply order: namespace → config → storage → workloads → scaling.

---

## ConfigMap & Secret

### ConfigMap — Non-sensitive configuration

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: my-project
data:
  LOG_LEVEL: "info"
  DATABASE_HOST: "postgres"
  CACHE_URL: "redis://redis:6379"
```

### Secret — Sensitive credentials

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
  namespace: my-project
type: Opaque
data:
  DATABASE_PASSWORD: ""       # Base64: echo -n 'value' | base64
  API_KEY: ""                 # Never commit real values
```

**Mounting in pods** — use `envFrom` to inject all keys as environment variables:

```yaml
spec:
  containers:
    - name: app
      envFrom:
        - configMapRef:
            name: app-config
        - secretRef:
            name: app-secrets
```

For composite values (like a DATABASE_URL that embeds a password), use `env`
with variable substitution:

```yaml
env:
  - name: DATABASE_PASSWORD
    valueFrom:
      secretKeyRef:
        name: app-secrets
        key: DATABASE_PASSWORD
  - name: DATABASE_URL
    value: "postgresql://user:$(DATABASE_PASSWORD)@postgres:5432/mydb"
```

K8s resolves `$(VAR_NAME)` from previously defined env vars in the same
container spec. Order matters — the referenced variable must appear first.

---

## PersistentVolumeClaim

PVCs decouple storage from pod lifecycle. Data survives pod restarts, image
pulls, and rescheduling.

```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: db-pvc
  namespace: my-project
spec:
  accessModes:
    - ReadWriteOnce           # Single-node write (standard for databases)
  resources:
    requests:
      storage: 1Gi
```

**Access modes:**

| Mode | Use Case |
|------|----------|
| `ReadWriteOnce` | Single pod writes (databases, caches) |
| `ReadOnlyMany` | Multiple pods read (shared config, assets) |
| `ReadWriteMany` | Multiple pods write (shared files — needs NFS/Ceph) |

Mount in a deployment:

```yaml
spec:
  containers:
    - volumeMounts:
        - name: db-storage
          mountPath: /var/lib/postgresql/data
  volumes:
    - name: db-storage
      persistentVolumeClaim:
        claimName: db-pvc
```

---

## Deployments

### Resource Requests & Limits

Every container must declare resources. Without them, HPA cannot calculate
utilization and the scheduler cannot make informed placement decisions.

```yaml
resources:
  requests:              # Guaranteed minimum — scheduler reserves this
    cpu: 100m            # 100 millicores = 0.1 CPU core
    memory: 256Mi        # 256 MiB RAM
  limits:                # Hard ceiling — OOMKill if memory exceeded
    cpu: 500m
    memory: 512Mi
```

**Sizing guidelines:**

| Service Type | CPU Request | CPU Limit | Memory Request | Memory Limit |
|-------------|-------------|-----------|----------------|--------------|
| API / backend | 100m–250m | 500m–1000m | 256Mi–512Mi | 512Mi–1Gi |
| Web frontend | 50m–100m | 200m–500m | 128Mi–256Mi | 256Mi–512Mi |
| Database | 100m–500m | 500m–2000m | 256Mi–1Gi | 512Mi–2Gi |
| Cache (Redis) | 50m–100m | 200m–500m | 128Mi–256Mi | 256Mi–512Mi |

These are starting points. For production, profile actual usage and set
requests to P50, limits to P99.

### Health Probes

Probes serve fundamentally different purposes — choosing the wrong one causes
cascading failures.

**Liveness probe** — "Is this process alive?"
Restart the pod if it fails. Use for detecting deadlocks, infinite loops, or
zombie processes. Keep it cheap — no dependency checks.

```yaml
livenessProbe:
  httpGet:
    path: /healthz             # Or /health/live — lightweight, no deps
    port: 8000
  initialDelaySeconds: 10      # Wait for app startup
  periodSeconds: 10            # Check every 10s
  timeoutSeconds: 5            # Fail if no response in 5s
  failureThreshold: 3          # Restart after 3 consecutive failures
```

**Readiness probe** — "Can this instance serve traffic?"
Remove from Service endpoints if it fails. Use for dependency checks (database,
cache). The pod stays running — it just stops receiving traffic until recovery.

```yaml
readinessProbe:
  httpGet:
    path: /ready               # Or /health/ready — checks dependencies
    port: 8000
  initialDelaySeconds: 5
  periodSeconds: 5             # Check frequently — fast recovery
  timeoutSeconds: 5
  failureThreshold: 2          # Remove quickly on failure
```

**Startup probe** — "Has the app finished starting?"
Disables liveness/readiness until the app is ready. Use for slow-starting
applications (JVM warmup, large model loading).

```yaml
startupProbe:
  httpGet:
    path: /healthz
    port: 8000
  periodSeconds: 5
  failureThreshold: 30         # 30 × 5s = 150s max startup time
```

**Critical mistakes to avoid:**
- Never put dependency checks in the liveness probe — if the database goes
  down, you don't want every API pod restarting (cascade failure)
- Always set `initialDelaySeconds` — probing before the app starts causes
  unnecessary restarts
- Keep probe endpoints fast (< 200ms) — slow probes cause false positives

### Init Containers

Run setup tasks before the main container starts. Common uses: database
migrations, config generation, waiting for dependencies.

```yaml
spec:
  initContainers:
    - name: run-migrations
      image: my-app:latest           # Reuse the app image
      command: ["bash", "scripts/migrate.sh"]
      envFrom:
        - configMapRef:
            name: app-config
        - secretRef:
            name: app-secrets
  containers:
    - name: app
      # ... main container starts after init completes
```

**Init container rules:**
- Runs to completion before main container starts
- If it fails, the pod restarts (respects `restartPolicy`)
- Has access to the same volumes, ConfigMaps, and Secrets as main container
- Scripts must be idempotent — they run on every pod start

### Rolling Update Strategy

Control how deployments roll out new versions:

```yaml
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1              # Create 1 extra pod during update
      maxUnavailable: 0        # Never reduce below desired replicas
```

`maxUnavailable: 0` ensures zero-downtime deploys — new pods must pass
readiness before old pods are terminated.

For single-instance databases, use `strategy: Recreate` to prevent dual-write.

---

## Services

Services provide stable DNS names and load balancing for pods.

### ClusterIP (Internal)

Default type. Accessible within the cluster via DNS:
`<service-name>.<namespace>.svc.cluster.local` (or just `<service-name>`
within the same namespace).

```yaml
apiVersion: v1
kind: Service
metadata:
  name: my-api
  namespace: my-project
spec:
  type: ClusterIP
  selector:
    app: my-api
  ports:
    - name: http
      port: 8000
      targetPort: 8000
      protocol: TCP
```

### NodePort (External Access for Dev)

Exposes on each node's IP at a static port (30000–32767). Use for local dev
when you need external access without an Ingress controller.

```yaml
apiVersion: v1
kind: Service
metadata:
  name: my-web
  namespace: my-project
spec:
  type: NodePort
  selector:
    app: my-web
  ports:
    - name: http
      port: 3000
      targetPort: 3000
      nodePort: 30000         # Optional: omit for auto-assignment
```

### When to use which

| Type | Use Case | Access |
|------|----------|--------|
| ClusterIP | Internal services (DB, cache, API-to-API) | `svc-name:port` within cluster |
| NodePort | Dev/local external access | `localhost:nodePort` |
| LoadBalancer | Cloud production external access | Cloud-provisioned external IP |
| Ingress | HTTP routing, TLS, path/host-based | Domain-based routing |

---

## HorizontalPodAutoscaler

Auto-scale workloads based on observed metrics. Requires resource requests
on target pods (HPA calculates utilization as `current / request`).

```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-api-hpa
  namespace: my-project
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-api
  minReplicas: 1
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70    # Scale up when avg CPU > 70%
```

**Behavior tuning** (prevent flapping):

```yaml
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
        - type: Pods
          value: 2
          periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300   # 5 minutes cooldown
      policies:
        - type: Pods
          value: 1
          periodSeconds: 60
```

Scale up aggressively to handle spikes; scale down conservatively to avoid
oscillation.

---

## Best Practices

1. **Always set a namespace explicitly** — never depend on kubectl context.
   Manifests should be self-contained and apply correctly regardless of the
   user's current context.

2. **Never commit real secrets** — use empty placeholders in manifests and
   create real secrets with `kubectl create secret generic`. Add secret files
   to `.gitignore` if they ever contain real values.

3. **Match probe type to failure mode** — liveness for process health (no deps),
   readiness for dependency health (DB, cache). Mixing them causes cascading
   restarts.

4. **Set resource requests AND limits on every container** — requests enable
   scheduling; limits prevent runaway consumption. Without requests, HPA
   cannot function.

5. **Use `envFrom` over individual `env` entries** — cleaner manifests, easier
   to add new config without touching deployments.

6. **Make init containers idempotent** — they run on every pod start, not just
   the first time. Use `IF NOT EXISTS` in SQL, check-then-create patterns.

7. **PVCs for stateful services only** — databases and caches get PVCs. API
   servers and web frontends should be stateless and disposable.

8. **Label everything consistently** — `app`, `component`, and `part-of` labels
   enable `kubectl get pods -l app=my-api` and service discovery.

9. **Order manifests for dependency** — namespace first, then config/secrets,
   then storage, then workloads, then scaling. Use file naming
   (00-namespace.yml, 10-config.yml) or kustomize for ordering.

10. **Test with `kubectl apply --dry-run=client`** — catches syntax errors and
    missing fields before hitting the cluster. Follow with `kubectl diff` to
    preview changes against the live state.

---

## When to Read Reference Files

| Need | File |
|------|------|
| Deployment, StatefulSet, init containers, probes, rolling updates, pod security | `references/workload-resources.md` |
| Services, Ingress, PVCs, ConfigMaps, Secrets, NetworkPolicy, storage classes | `references/networking-and-storage.md` |
| HPA, VPA, resource tuning, pod disruption budgets, chaos testing, monitoring | `references/scaling-and-reliability.md` |

## Example Templates

| Need | File |
|------|------|
| Namespace + ConfigMap + Secret (project bootstrap) | `examples/namespace-config.yml` |
| Database Deployment + PVC + Service (PostgreSQL pattern) | `examples/stateful-database.yml` |
| Cache Deployment + PVC + Service (Redis with AOF) | `examples/stateful-cache.yml` |
| API Deployment with init container, probes, HPA + Service | `examples/api-deployment.yml` |
| Web Deployment + NodePort Service (frontend pattern) | `examples/web-deployment.yml` |

