Cloud Modernization Patterns Module 2 · Where Should It Run?

Critique These Manifests

Last reviewed · content updated

Advanced

What you'll learn

~20 min
  • Adversarially review Kubernetes manifests instead of trusting generated output
  • Identify the ten highest-frequency manifest defects: QoS, probes, secrets, policy, rollout, and drift
  • Turn the findings into a reusable review checklist for any manifest set

The setup

A contractor “helpfully” ran a conversion tool over an old compose file and committed the resulting manifests for a review environment: AssetTrack’s API plus a scratch Oracle instance. The tooling was mechanical, nobody hand-tuned the output, and it works — pods start, requests flow. Which is exactly what makes it dangerous: manifests that work in the demo and fail under contact are the default output of every generator, AI or otherwise.

Your job — and the skill this lesson trains — is the adversarial read. Here is the set — abridged (the API’s Service and the PVC wiring are omitted for space) but valid, deployable YAML:

review-env.yaml — find the flaws before reading on
apiVersion: apps/v1
kind: Deployment
metadata:
name: assettrack-api
labels: { generated-by: convert-tool-v1 }
spec:
replicas: 1
strategy: { type: Recreate }
selector: { matchLabels: { app: assettrack-api } }
template:
metadata: { labels: { app: assettrack-api } }
spec:
containers:
- name: api
image: acrmeridian.azurecr.io/assettrack-api:latest
ports: [{ containerPort: 8080 }]
livenessProbe:
tcpSocket: { port: 8080 }
readinessProbe:
tcpSocket: { port: 8080 }
resources: {}
env:
- name: DB_PASSWORD
valueFrom:
configMapKeyRef: { name: app-config, key: db-password }
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: oracle
spec:
replicas: 1
selector: { matchLabels: { app: oracle } }
template:
metadata: { labels: { app: oracle } }
spec:
containers:
- name: oracle
image: registry.meridian.internal/oracle-db:12.1.0
ports: [{ containerPort: 1521 }]
resources: {}
# no probes of any kind
---
apiVersion: v1
kind: Service
metadata:
name: oracle
spec:
selector: { app: oracle }
ports: [{ port: 1521, targetPort: 1521 }]
---
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
db-password: "Or4cle_R3view!"
ORACLE_HOST: oracle.default.svc.cluster.local

The README next to it says “deploy into the assettrack-review namespace.” Sit with the YAML for two minutes before scrolling. Then run the same exercise with your AI CLI:

Adversarially review these Kubernetes manifests. Assume they "work" in a
demo. Find every defect that would surface under production conditions:
scheduling pressure, node failure, rollouts, restarts, security review.
Rank by severity. Cite the exact line for each finding.

The ten findings

  1. resources: {} everywhere → BestEffort QoS. No requests, no limits means both pods are in the first eviction class under node memory pressure. The database gets OOM-killed before anything with so much as a request set. Fix: requests+limits on everything; requests == limits (Guaranteed QoS) for the database.

  2. TCP probes on an HTTP service. The API’s probes check “port 8080 accepts connections.” A deadlocked app with a live listener passes forever — hung-but-listening reads as healthy. Fix: HTTP probes against /healthz (Lesson 2.2 built the endpoint for exactly this).

  3. No probes at all on the slow-boot database. Oracle takes minutes to open. With no readiness probe, the Service routes connections to it the moment the container starts; the API’s first minutes are a mystery-failure spray. Fix: the SQL-based readiness probe from Lesson 2.4, with honest timing.

  4. A credential in a ConfigMap. db-password sits in plaintext config, readable by anyone with ConfigMap read access — a much broader audience than Secret readers, and it will land in any config export or debug dump. Fix: a Secret at minimum; a vault-backed secret provider by preference (Lesson 4.1).

  5. No NetworkPolicy anywhere + a credential-guarded DB. Every pod in the cluster can reach Oracle on 1521. Combined with finding 4, the blast radius of any compromised pod includes the database. Fix: default-deny in the namespace; allow only api→oracle:1521.

  6. replicas: 1 with strategy: Recreate. Every deploy is an outage: the old pod dies before the new one starts. Fine for a scratch DB, wrong for the API even in review — and this file is one copy-paste from being “the prod manifests.” Fix: RollingUpdate with at least 2 replicas for anything serving traffic.

  7. image: ...:latest on the API. Deploys become non-deterministic: what runs depends on the last push, rollbacks have nothing to roll back to, and two nodes can cache different “latest.” Fix: pinned tags, updated deliberately (Lesson 2.3’s :1.4.2).

  8. Version drift on Oracle: 12.1.0. Meridian’s real database is 19c; the converter faithfully copied an EOL image from a years-old compose file. A review environment on the wrong major version validates nothing — worse, it passes tests that production will fail. Fix: parity with production, enforced by review.

  9. Namespace mismatch: manifests deploy to default, docs say assettrack-review. Nothing in the YAML sets a namespace, so everything silently lands in default — while the ConfigMap’s ORACLE_HOST hardcodes oracle.default.svc..., meaning the docs’ instructions break the app if followed. Silent-landing-in-default plus hardcoded FQDNs is a classic generated-manifest bug pair. Fix: explicit namespace in metadata (or kustomize), and service names without hardcoded namespaces.

  10. The ladder question outranks the YAML. Before a single line of manifest review: AssetTrack’s API is a rung-3 workload — Lesson 2.3 already runs it on Container Apps with zero cluster ops. The converter put a PaaS-destined workload on the cluster because the compose file happened to contain it. The first finding of any manifest review is “should this workload be here at all?” — everything below assumes the answer was yes.

  11. Generator residue as documentation. generated-by: convert-tool-v1 labels and mechanically-copied structure signal that no human has reviewed this file. Residue is not itself a defect — it is a flag that every other defect on this list is probably present. Treat it as the “wet paint” sign.

⚠The pattern behind the pattern

Notice what the ten have in common: none of them stop the demo from working. Every one of them is invisible until scheduling pressure, a node failure, a deploy, a security review, or an unlucky restart makes it visible — in production, at night. This is why “it runs” is the beginning of manifest review, and why the adversarial prompt tells the AI to assume production conditions.

Your reusable checklist

And the ten are not exhaustive by design: a thorough adversarial pass will surface more (the scratch database running as a Deployment with no persistent volume — its data dies with the pod — where Lesson 2.4 put production Oracle in a StatefulSet for exactly that reason; the credential’s value being a real-looking password committed to git). When your AI review returns findings beyond an answer key, that is the exercise working, not failing.

Distilled from the findings — run it against any manifest set, generated or handwritten:

□ Every container: resources set; Guaranteed QoS for stateful workloads
□ Probes test the service, not the socket; startup timing honest
□ No credential outside a Secret (or vault-backed provider)
□ NetworkPolicy: default-deny, explicit allows
□ Rollout: RollingUpdate + replicas >= 2 for anything serving traffic
□ Images pinned; versions match the environment they claim to model
□ Namespace explicit; no hardcoded cross-namespace FQDNs
□ Generator residue = full-review flag
KNOWLEDGE CHECK

Under node memory pressure, the scratch Oracle pod is OOM-killed while other teams' pods survive. The manifests 'worked fine' for weeks. Which finding explains it?

Key takeaway

Generated manifests that demo clean and fail under contact are the default, not the exception. The adversarial review — yours plus your AI’s, against the eight-line checklist — is what turns “it runs” into “it survives.” This closes Module 2; next, the data itself starts moving.

Search lessons