Files
atelier/examples/bad/k8s-bare-pod-no-resources.md
T
Jon Chery d1aa5daf2b docs(milestone): complete v0.2 — infrastructure-as-code + kubernetes
---ci---
project: atelier
phase: 5
milestone: v0.2
status: complete
requirements:
  covered: [ATELIER-36, ATELIER-37, ATELIER-38, ATELIER-39, ATELIER-40, ATELIER-41, ATELIER-42, ATELIER-43, ATELIER-44, ATELIER-45, ATELIER-46, ATELIER-47, ATELIER-48, ATELIER-49, ATELIER-50, ATELIER-51, ATELIER-52, ATELIER-53, ATELIER-54, ATELIER-55, ATELIER-56, ATELIER-57, ATELIER-58, ATELIER-59]
  partial: []
---/ci---
2026-08-05 02:20:17 +00:00

5.1 KiB

Bad Example: Bare Pod, No Resources

A Kubernetes manifest that violates Atelier's Kubernetes principles. Each violation is cited.

The Code

apiVersion: v1
kind: Pod
metadata:
  name: api
  namespace: default
spec:
  containers:
    - name: api
      image: api:latest          # :latest, unversioned
      ports:
        - containerPort: 8080
      env:
        - name: DATABASE_URL
          value: "postgres://admin:hunter2@db:5432/app"   # secret in plaintext, in the manifest

The team applies it with kubectl apply -f api-pod.yaml. When the pod crashes, they kubectl delete pod api && kubectl apply -f api-pod.yaml to "restart" it. There are no probes, no resource requests, no RBAC, no NetworkPolicy.

What Makes It Bad

Bare Pod, No Controller (k8s P2 Pods are Mortal)

  • A kind: Pod with no controller. When the node dies, the pod does not come back. When the team needs three replicas, they copy the YAML twice and rename it.
  • The "restart" workflow (delete pod && apply) is manual recovery — exactly the manual-mutation anti-pattern from domains/devops/.
  • Fix: use a Deployment. The controller replaces dead pods, scales, and rolls back. See domains/kubernetes/workloads.md.

No Resource Requests (k8s P4 Requests and Limits are Contracts)

  • The container has no resources.requests or resources.limits. It is BestEffort — first evicted under node pressure. The scheduler has no signal to place it well; it lands wherever there is room, then gets killed when the node is full.
  • A workload without requests is an unbounded gamble on the scheduler.
  • Fix: set CPU and memory requests on every prod container; set a memory limit; consider a CPU limit. See domains/kubernetes/workloads.md.

No Probes (k8s P5 Probes Drive Health)

  • No readinessProbe — the Service routes traffic to the pod before it is ready. Users see 502s during startup.
  • No livenessProbe — a wedged container runs forever; no one notices until the outage.
  • The platform cannot heal what it cannot see. A pod without probes is invisible to the controller's reconciliation.
  • Fix: define readiness and liveness probes that check the workload's own health. See domains/kubernetes/workloads.md.

:latest Image Tag (k8s P1 + IaC P5 Version Everything)

  • image: api:latest is unversioned. Every kubectl apply pulls whatever is newest at that moment. Two pods "running the same manifest" run different images if latest moved between applies.
  • Rollback is impossible — there is no version to roll back to.
  • Fix: pin the image to a version or a digest: image: registry.example.com/api:v1.4.2 or image: registry.example.com/api@sha256:.... See domains/kubernetes/workloads.md and domains/infrastructure-as-code/terraform.md (P5 Version Everything).

Secret in Plaintext in the Manifest (k8s P9 Config and Secrets are Separate, IaC P10)

  • DATABASE_URL with the password is in the manifest in plaintext. If the manifest is committed (it is), the secret is in git.
  • Rotating the secret requires editing the manifest and re-applying — no separation of config from secret.
  • Fix: put the URL in a Secret (created out-of-band or via a secrets tool) and reference it with valueFrom.secretKeyRef. The manifest contains the reference, not the value. See domains/kubernetes/rbac.md and domains/security/secrets.md.

default Namespace (k8s P6 Namespaces Bound Blast Radius)

  • The pod runs in default. There is no namespace boundary for quota, RBAC, or NetworkPolicy. Every other workload in default can reach it; an outage in one affects the namespace all share.
  • Fix: give every prod workload a named namespace sized to its blast radius. default is for nothing in production. See domains/kubernetes/networking.md and domains/kubernetes/workloads.md.

No RBAC, No NetworkPolicy (k8s P7 RBAC by Intent, P6 Namespaces Bound Blast)

  • No serviceAccountName — the pod uses the default ServiceAccount, a shared identity.
  • No NetworkPolicy — every pod in the cluster can reach api. The network is flat by default.
  • Fix: a dedicated ServiceAccount with a least-privilege Role bound by intent. A default-deny NetworkPolicy with explicit allows. See domains/kubernetes/rbac.md and domains/kubernetes/networking.md.

The Cascade

The violations compound. A bare pod with no probes crashes silently and is not restarted. :latest means the "restart" pulls a different image than the one that crashed. The plaintext secret in the manifest is now in git, so rotating it means editing the manifest and re-applying — and the new manifest still has no controller, so the next crash starts the manual loop again. default namespace means the blast radius is the whole cluster.

  • domains/devops/P4 Rollback First — with :latest and no controller, there is no rollback path.
  • domains/security/authorization — the shared default ServiceAccount and flat network are the k8s expression of zero-trust violation.
  • domains/errors/P2 Fail Loudly — a pod with no probes fails silently; the platform has no signal to act on.