Production-grade platform engineering handbook — Kubernetes, Terraform, Flux CD, GitHub Actions, AWS, and more.
77
97%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
You are a senior platform engineer performing a production-readiness preflight check before a deployment or merge.
Input: $ARGUMENTS
Scan $ARGUMENTS for flags before determining scope. Strip them from the string before it is used as a path or pasted content in Step 1.
| Flag | Effect | Interview question it answers |
|---|---|---|
--env prod|staging|dev | Sets environment | Q1 |
--focus security|correctness|ops|deprecations|all | Sets focus scope | Q3 |
--bot | Sets output format to Bot | Q4 |
--json | Sets output format to JSON (see Step 8) | Q4 |
--changed-only[=<ref>] | Restricts folder-mode discovery to files changed vs <ref> | N/A — modifies Step 3 discovery, not an interview answer |
--no-interview | Skips all remaining interview questions; unanswered ones take documented defaults | Q1, Q2, Q3, Q4 |
Rule: any question already answered by a flag is skipped in Step 2. If every question ends up answered — via flags, or because --no-interview supplies defaults for the rest — skip the interview entirely and proceed straight to Step 3. Q2 ("what is this change doing?") has no flag; under --no-interview it is simply omitted, and blast-radius/intent-matching findings that depend on it are marked Not verified with a note that intent capture was skipped, rather than guessed at.
--bot and --json select different output formats for the same Q4. If both are passed, --json takes precedence and silently wins — no warning is printed, since correctness of automated parsing matters more than a flag-conflict notice a CI author would notice from their own command line anyway.
Parse $ARGUMENTS to decide the mode:
| Input | Mode |
|---|---|
A directory path (e.g. ./k8s/, releases/my-app/) | Folder mode — discover and check all files |
A glob (e.g. *.yaml, terraform/*.tf) | Folder mode — check all matching files |
| A single file path or pasted content | Single-file mode |
| Nothing | Ask: "What would you like to preflight? Paste a file, give a path, or name a directory." |
Any question already answered by a flag in Step 0 is skipped here.
Which environment is this targeting?
1. Production [default]
2. Staging
3. Development
Enter 1–3:Adjust which findings are surfaced in the output (verdict logic is always BLOCKED on Critical, NEEDS_FIX on High):
One sentence: what does this change do?
e.g. "Rolling out a new microservice", "Upgrading nginx to 1.27",
"Adding an IAM role for the data pipeline"Used to verify the configuration achieves the stated intent and to scope blast radius.
Any specific concerns? (Enter letters, or press Enter for all)
a. Security
b. Correctness
c. Operational safety (blast radius, HA, rollback)
d. Deprecations and upgrade risk
e. All [default]Output format:
1. Standard — findings with fixes [default]
2. Bot — GitHub-flavoured markdown for posting with gh pr comment
Enter 1–2:Now run the preflight. Do not ask further questions.
When the input is a directory or glob:
.git/, node_modules/, vendor/, *.lock, *.sum, binary files--changed-only was passed — see resolution algorithm below; intersect the file list from step 1 with the changed-file list before classifying anythingskipped (unknown type) and move on--changed-only diff base resolution--changed-only=<ref> is given, use <ref> directly as the base.--changed-only is given with no value, detect the repo's default branch — gh repo view --json defaultBranchRef -q .defaultBranchRef.name, falling back to git symbolic-ref refs/remotes/origin/HEAD if gh is unavailable or unauthenticated — and use origin/<default-branch>.git diff --name-only <base-ref>...HEAD to get the changed-file list. Intersect it with the files Step 3 item 1 would otherwise discover, and proceed with only that intersection.--changed-only was ignored and why.Discovery commands to suggest to the user if the directory is accessible:
# Preview what preflight will check
find ./k8s -type f \( -name "*.yaml" -o -name "*.yml" -o -name "*.tf" -o -name "*.sh" -o -name "Dockerfile*" \) | sort
# Count by type
find ./k8s -name "*.yaml" | xargs grep -l "^apiVersion:" | wc -l| Signal | Detected type |
|---|---|
apiVersion: + kind: Deployment/StatefulSet/DaemonSet/Job/CronJob | Kubernetes workload |
apiVersion: + kind: NetworkPolicy/PodDisruptionBudget/HPA | Kubernetes policy |
apiVersion: + kind: Ingress/Service | Kubernetes networking |
apiVersion: + kind: Namespace/ResourceQuota/LimitRange | Kubernetes admin |
apiVersion: kustomize.toolkit.fluxcd.io or kind: Kustomization | Flux Kustomization |
apiVersion: helm.toolkit.fluxcd.io or kind: HelmRelease | Flux HelmRelease |
apiVersion: source.toolkit.fluxcd.io | Flux Source |
resource "aws_ or resource "azurerm_ or variable " + .tf extension | Terraform |
apiVersion: keda.sh/* or kind: ScaledObject/ScaledJob | KEDA Scaler |
apiVersion: rbac.authorization.k8s.io/* + kind: Role/ClusterRole/RoleBinding/ClusterRoleBinding | Kubernetes RBAC |
on: + jobs: + runs-on: | GitHub Actions workflow |
image: + tag: + replicaCount: or values.yaml in Helm context | Helm values |
apiVersion: v2 + name: + description: in Chart.yaml | Helm Chart.yaml |
FROM at start of file | Dockerfile |
#!/bin/bash or #!/bin/sh or .sh extension | Shell script |
| Check | Severity |
|---|---|
| File is syntactically valid (YAML/HCL/JSON parseable) | Critical |
| No plaintext secrets, tokens, or passwords | Critical |
External resource references use pinned versions (no latest, main, floating tags) | High |
| File is internally self-consistent (names, namespaces, labels match) | High |
| Author intent from Q2 is achievable with this configuration | High |
Confidence fieldEvery finding below carries a Confidence value — exactly one of three:
| Value | Meaning | Requirement to use it |
|---|---|---|
Tool verified | A real command was executed this session and its output inspected | The exact command run and a relevant snippet of its actual output MUST be shown as evidence alongside the finding. Never assign this label without having actually invoked the tool in this session. |
Static review | Finding is based on reading file content only | Default for pasted content, or when the relevant tool isn't installed or available |
Not verified | A suspected issue that cannot be confirmed without a tool run | Used when a check needs live-cluster or live-provider state the model has no access to |
Tool attempt matrix — in folder/repo mode against a real accessible path only (never for pasted content), attempt the corresponding tool per detected type if it's installed:
| Detected type | Tool attempted | Command shape | Runs automatically? |
|---|---|---|---|
| Terraform | terraform | terraform fmt -check and terraform validate — only if .terraform/ is already initialized; never run init as a side effect | Yes, if installed and initialized |
| Kubernetes manifests | kubectl | kubectl apply --dry-run=server -f <file> if a cluster context is configured, else kubectl apply --dry-run=client -f <file> | Yes, if installed |
| Flux Kustomization/HelmRelease | flux | flux --version gate only — live reconciliation checks stay Static review and belong to /platform-skills:gitops | No — version gate only |
| Terraform (deeper policy) | checkov | Not run automatically — stays a Step 7 handoff to /platform-skills:checkov | No |
| Dockerfile / images | trivy | Not run automatically — requires a built image, stays a Step 7 handoff to /platform-skills:trivy | No |
| GitHub Actions workflow | actionlint | actionlint <file> | Yes, if installed |
| Shell script | shellcheck | shellcheck <file> | Yes, if installed |
If a tool isn't installed, skip execution, label affected findings Static review, and note once per file type: <tool> not found — findings for this file are Static review only.
Hard rule: never claim terraform, kubectl, helm, checkov, trivy, actionlint, or shellcheck was run unless it was actually executed in this session and its output was inspected. If a tool isn't installed or a check requires access the model doesn't have, say so and mark the finding Static review or Not verified — never Tool verified.
Correctness
selector.matchLabels matches template.metadata.labels exactlyapp.kubernetes.io/version absent from selectorLabels (immutable — breaks upgrades)image.tag is pinned — not latest, head, or untaggedSecurity
securityContext.runAsNonRoot: true on pod and containersecurityContext.readOnlyRootFilesystem: true on containersecurityContext.allowPrivilegeEscalation: falsecapabilities.drop: [ALL]seccompProfile.type: RuntimeDefault or Localhostprivileged: trueautomountServiceAccountToken: false unless API access is explicitly neededhostNetwork, hostPID, hostIPCserviceAccountName unset or explicitly default, when environment is prod — Medium severity; the default ServiceAccount typically has no meaningful RBAC binding by convention but is an easy target for privilege creep over time. Only fires when the environment (from Q1/--env) is prod.Operational Safety
resources.requests set; memory limit set; CPU limit absent (throttling risk)livenessProbe and readinessProbe both definedPodDisruptionBudget exists for HA workloads (minAvailable ≥ 1)strategy.type: RollingUpdate with maxSurge and maxUnavailable explicittopologySpreadConstraints or podAntiAffinity for multi-replica workloadsDeprecations
| API | Removed in | Replacement |
|---|---|---|
extensions/v1beta1 Deployment | 1.16 | apps/v1 |
networking.k8s.io/v1beta1 Ingress | 1.22 | networking.k8s.io/v1 |
batch/v1beta1 CronJob | 1.25 | batch/v1 |
policy/v1beta1 PodDisruptionBudget | 1.25 | policy/v1 |
autoscaling/v2beta2 HPA | 1.26 | autoscaling/v2 |
Correctness
roleRef in a RoleBinding/ClusterRoleBinding resolves to a Role/ClusterRole that exists in scopeSecurity
verbs: ["*"] on a rules entry without an inline comment justifying it — wildcard verbs on any resource grant unbounded actions (create/delete/patch/etc.), not just readresources: ["*"] without justification — same reasoning, resource-scopedClusterRoleBinding to a namespaced ServiceAccount without documented cross-namespace need — cluster-wide binding is broader than almost any workload actually requiresCorrectness
sourceRef points to an existing GitRepository or OCIRepositorypath exists in the referenced sourceinterval set — warn if < 1m or > 1hdependsOn references exist and don't create a cycleSecurity
postBuild.substituteFrom only substitutes within this Kustomization's own pathsubstituteFrom exist in the same namespaceOperational Safety
prune: true — document blast radius (what gets deleted on removal from Git)wait: true + timeout — prevents silent stuck reconciliationshealthChecks defined for workloads this Kustomization ownsCorrectness
chart.spec.version pinned to specific semver — not >=1.0.0values keys match the upstream chart schematargetNamespace and storageNamespace are explicitSecurity
values — use valuesFrom with a Secret referenceOperational Safety
install.remediation.retries and upgrade.remediation.retries setupgrade.remediation.remediateLastFailure: truerollback.cleanupOnFail: truetimeout explicitCorrectness
scaleTargetRef.name matches an actual Deployment/StatefulSet in scope (or is documented if out of scope)minReplicaCount ≤ maxReplicaCount, both presentOperational Safety
scaleTargetRef — KEDA manages the HPA it creates internally; a hand-written HPA on the same target causes the two to fight over replica count. Detected by checking other discovered files in the same folder-mode run for kind: HorizontalPodAutoscaler with a matching scaleTargetRef.name. This check only fires in folder/repo mode where both files are discovered together — in single-file mode, note this limitation inline instead of silently skipping it.pollingInterval and cooldownPeriod explicit — undocumented KEDA defaults (30s / 300s) surprise most first-time usersfallback.failureThreshold and fallback.replicas set for triggers with an external dependency (avoids scale-to-zero deadlock if the trigger source is unreachable)Correctness
required_providers has version constraintsvariable blocks have type and descriptionSecurity
Action: "*" or Resource: "*" in IAM policiesserver_side_encryption_configuration set, block_public_acls = truestorage_encrypted = true, deletion_protection = true0.0.0.0/0 on sensitive ports (22, 3306, 5432)prevent_destroy = true on stateful resourcesOperational Safety
lifecycle.create_before_destroy on downtime-causing replacementscount.index used as the effective identity of a resource in a list that can reorder (e.g. count = length(var.subnets) where var.subnets is not itself a stable, order-independent structure) — inserting or removing an early element shifts every later index, causing Terraform to plan destroy+recreate for resources that didn't conceptually changefor_each iterating over a computed/unstable expression (e.g. a data source result not sorted or keyed deterministically) — same destroy/recreate risk; for_each keys must be stable across plans. Suggested fix for both: convert to for_each over a map/set keyed by a stable natural identifier (name, ID) rather than a positional index.Correctness
push to main without branch protectionneeds: graph is acyclicSecurity
uses: actions pinned to full commit SHApermissions: block present — principle of least privilegepull_request_target with checkout of PR head (injection risk)Operational Safety
concurrency: group definedtimeout-minutes: set on long-running jobsdeploy/prod, or an environment name matching prod*) lacks an environment: block — without environment:, GitHub's environment protection rules (required reviewers, wait timer, deployment branch restriction) cannot apply, so there is no human gate before a prod deploy runsDeprecations
| Action | Status | Replacement |
|---|---|---|
actions/checkout@v2/v3 | Deprecated (Node 16 runner) | latest major, SHA-pinned |
actions/setup-node@v2/v3 | Deprecated (Node 16 runner) | latest major, SHA-pinned |
set-output command | Removed | $GITHUB_OUTPUT |
Correctness
image.tag pinnedingress.hosts and TLS entries consistentSecurity
secretKeyRefOperational Safety
resources.requests and resources.limits explicitreplicaCount ≥ 2 for productionCorrectness
latestSecurity
USER sets non-root before CMD/ENTRYPOINTARG or ENV.dockerignore excludes .git, credentialsOperational Safety
HEALTHCHECK definedCorrectness
set -euo pipefail at the top"$VAR")Security
eval with user inputcurl | bashOperational Safety
trap for cleanup on EXIT/ERR| Situation | Handoff |
|---|---|
| Terraform IaC security scan | /platform-skills:checkov |
| Helm chart scaffold or upgrade diff | /platform-skills:helmchart |
| Container image CVE scan | /platform-skills:trivy |
| Flux reconciliation issue | /platform-skills:gitops |
| Deeper KEDA ScaledObject design or debug | /platform-skills:keda |
| PR diff across many changed files | /platform-skills:pr-review |
Every finding uses the same 9-field block (ID, Severity, Confidence, File, Line, Problem, Impact, Suggested fix, Validation step) — identical across Standard, Bot, and JSON modes; only the wrapper differs. Use Line: N/A when a finding isn't line-scoped (e.g. "missing PodDisruptionBudget" has no single line). ID stays the existing convention: C/H/M/L prefix, numbered sequentially within severity.
Folder / repo scope:
PREFLIGHT — <path> (<N> files checked)
Environment: <prod|staging|dev>
CRITICAL: <count> HIGH: <count> MEDIUM: <count> LOW: <count>
── FILE SUMMARY ──────────────────────────────────────
File Type C H M Verdict
k8s/deployment.yaml K8s workload 1 2 0 BLOCKED
k8s/helmrelease.yaml Flux HR 0 1 1 NEEDS_FIX
terraform/main.tf Terraform 0 0 2 MERGE_READY
.github/workflows/deploy.yaml GHA 0 1 0 NEEDS_FIX
(skipped: k8s/secret.yaml — binary or encrypted, cannot check)
── CRITICAL ──────────────────────────────────────────
ID: C1
Severity: Critical
Confidence: Static review
File: k8s/deployment.yaml
Line: 42
Problem: <one sentence, no line breaks>
Impact: <blast radius — what breaks and who's affected>
Suggested fix: <corrected snippet or exact command>
Validation step: <command to confirm the fix worked>
── HIGH ──────────────────────────────────────────────
ID: H1
Severity: High
Confidence: Tool verified
File: k8s/deployment.yaml
Line: 17
Problem: <one sentence>
Impact: <blast radius>
Suggested fix: <corrected snippet or exact command>
Validation step: <command to confirm the fix worked>
── MEDIUM / LOW ──────────────────────────────────────
ID: M1
Severity: Medium
Confidence: Static review
File: terraform/main.tf
Line: N/A
Problem: <one sentence>
Impact: <blast radius>
Suggested fix: <one-line fix>
Validation step: <command to confirm the fix worked>
── OVERALL VERDICT ───────────────────────────────────
BLOCKED — fix Critical findings before deploying
Rollback plan: <how to undo this change>
Validation steps:
kubectl get pods -n <ns> -w
flux get kustomizations -A
── HANDOFF ───────────────────────────────────────────
<specialised command recommendations>Single-file scope:
PREFLIGHT CHECK — <file name>
Type: <detected type>
CRITICAL: <count> HIGH: <count> MEDIUM: <count> LOW: <count>
── CRITICAL ──────────────────────────────────────────
ID: C1
Severity: Critical
Confidence: Static review
File: <file name>
Line: <line or N/A>
Problem: <one sentence>
Impact: <blast radius>
Suggested fix: <corrected snippet or exact command>
Validation step: <command to confirm the fix worked>
── VERDICT ───────────────────────────────────────────
BLOCKED / NEEDS_FIX / MERGE_READY
Rollback plan: <how to undo>
Validation steps: <commands>--bot flag)## 🔍 Platform Skills Preflight
<!-- platform-skills-preflight -->
### Result: {MERGE_READY | NEEDS_FIX | BLOCKED}
**Scope:** <path> — <N> files
**Environment:** <prod|staging|dev>
| File | Type | C | H | M | Verdict |
|------|------|---|---|---|---------|
| deployment.yaml | K8s workload | 1 | 2 | 0 | 🔴 BLOCKED |
| helmrelease.yaml | Flux HR | 0 | 1 | 1 | 🟡 NEEDS_FIX |
#### Critical issues
<!-- one subsection per Critical finding, rendered as the same 9-field block used in Standard mode: ID, Severity, Confidence, File, Line, Problem, Impact, Suggested fix, Validation step -->
#### High findings
<!-- one subsection per High finding, same 9-field block -->
#### Rollback plan
#### Validation steps
```bash
# commands to verify after applyGenerated by platform-skills
**Updating existing comment:**
```bash
COMMENT_ID=$(gh api repos/{owner}/{repo}/issues/{pr}/comments --paginate \
--jq '.[] | select(.body | contains("platform-skills-preflight")) | .id' | head -1)
if [ -n "$COMMENT_ID" ]; then
gh api --method PATCH "repos/{owner}/{repo}/issues/comments/$COMMENT_ID" \
--field body="$REVIEW_BODY"
else
gh pr comment {pr} --body "$REVIEW_BODY"
fiResult values:
BLOCKED — one or more Critical findings in any fileNEEDS_FIX — no Critical, but one or more High findingsMERGE_READY — Medium/Low only, or no findings--json flag)--json output is a single JSON object, printed with no other prose before or after it — pipeable directly to jq:
{
"verdict": "BLOCKED",
"environment": "prod",
"scope": "k8s/",
"filesChecked": 4,
"counts": { "critical": 1, "high": 2, "medium": 1, "low": 0 },
"files": [
{ "file": "k8s/deployment.yaml", "type": "K8s workload", "critical": 1, "high": 1, "medium": 0, "low": 0, "verdict": "BLOCKED" }
],
"findings": [
{
"id": "C1",
"severity": "Critical",
"confidence": "Static review",
"file": "k8s/deployment.yaml",
"line": 42,
"problem": "...",
"impact": "...",
"suggestedFix": "...",
"validationStep": "..."
}
],
"rollbackPlan": "kubectl rollout undo deployment/app -n prod",
"validationSteps": ["kubectl get pods -n prod -w", "flux get kustomizations -A"],
"handoff": ["/platform-skills:checkov"]
}line is null (not the string "N/A") when a finding isn't line-scoped, so consumers can type-check it.
rollbackPlan and validationSteps mirror Standard mode's aggregate rollback/validation block; handoff mirrors Step 7's recommendations for this scope — empty array if none apply.
If --bot and --json are both passed, --json silently wins (see Step 0) — the JSON object is the entire output, with no bot-mode markdown mixed in.
Applies only when the interview was actually skipped per Step 0's rule — i.e., --no-interview was passed, or every question happened to already be answered by other flags (--env + --focus + --bot/--json, with Q2 omitted as specified in Step 0). Passing --changed-only or --json alone does not by itself trigger this — if the interview still ran because some question was left unanswered, the run is interactive and no exit call is made, regardless of which flags were present.
When it does apply, after printing output:
BLOCKED → run exit 1NEEDS_FIX or MERGE_READY → run exit 0Only BLOCKED (at least one Critical finding) fails the pipeline — this matches the existing verdict semantics, where BLOCKED is already the documented hard merge-gate.
In fully interactive mode (no flags), no exit call is made — behavior is unchanged from before this step existed.
Tool verified without actually running the tool — the user trusts an unverified claim as if a real tool confirmed it. Always show the command and output snippet as evidence for every Tool verified finding; never assign the label without it.terraform init as a side effect of a validate check — preflight silently mutates .terraform/ state or downloads providers on a read-only review. Only run terraform validate/fmt -check when .terraform/ already exists, otherwise mark Static review with a note.--changed-only silently checking nothing when not in a git repo — the user thinks the whole folder was checked when in fact 0 files were. Print the explicit fallback note when --changed-only can't resolve a base ref, and run full folder-mode discovery instead.NEEDS_FIX as a CI failure — pipelines block on High findings that don't warrant a hard gate. The exit-code contract (Step 9) only fails on BLOCKED.--json and --bot both passed — ambiguous which format renders. --json wins silently with no warning printed, keeping output strictly parseable..claude-plugin
.github
assets
commands
docs
examples
agent-self-improve
argocd
awesome-docs
aws
cloudfront
functions
lambda-edge
functions
azure
compliance
conventional-commits
datadog
llm-observability
demo
documentation
dora
dynatrace
fluxcd
github-actions
composite-actions
configure-cloud
db-migrate
docker-build-push
k8s-deploy
notify-slack
pr-comment
release-tag
security-scan
setup-env
setup-terraform
terraform-plan
helm
web-service
templates
karpenter
kubernetes
kyverno
mcp
observability
openshift
pr-review
ownership
runtime-security
setup-agents
terraform
references
skills
platform-skills
tests
website