DevOps Best Practices
This skill encodes opinionated, production-grade DevOps defaults. Apply them whenever generating or reviewing infrastructure code. When the user's request conflicts with a default below, surface the conflict and explain the tradeoff — don't silently override.
These are opinionated. Other valid approaches exist. The opinions here are chosen because they prevent the failure modes that hurt teams most often in real production environments.
When to use this skill
Trigger whenever the task involves any of:
- Terraform files (
*.tf, *.tfvars), Terragrunt, Pulumi, CDK
- Kubernetes manifests (Deployment, Service, Ingress, StatefulSet, etc.), Helm charts, Kustomize overlays
- Dockerfiles,
docker-compose.yml
- CI/CD config (
.github/workflows/*.yml, .gitlab-ci.yml, Jenkinsfile, CircleCI, Buildkite)
- Cloud provider SDKs or CLIs (AWS, GCP, Azure)
- IAM policies, security groups, network ACLs
- Observability config (Prometheus, Grafana, OpenTelemetry, Datadog, CloudWatch)
- Shell scripts deployed to servers (
/etc/init.d, systemd units, deploy scripts)
- DNS, TLS, CDN configuration
If unsure, default to applying the safety and security sections (they almost never hurt).
Foundational principles (apply to everything)
- Default to safety over convenience. A slightly harder UX that prevents production incidents wins.
- Default to least privilege. Start with zero permissions and add only what the workload demonstrably needs.
- Make failure modes loud. Silent failures destroy trust. Logs > swallowed errors. Alerts > "we'll notice eventually."
- Cost is a non-functional requirement. Generated infra that costs 10x what it should is a bug.
- Reproducibility beats cleverness. If a teammate can't recreate this environment from the repo in 30 minutes, the design is wrong.
- Document tradeoffs inline. If a non-obvious choice was made, leave a comment explaining why so future-you (or future-Claude) doesn't undo it.
Terraform
Module structure
- One module per logical resource grouping. Modules in
modules/<name>/. Roots in environments/<env>/.
- Always pin module versions and provider versions. Floating versions break reproducibility.
- Use
terraform-aws-modules/* community modules where available — they're battle-tested and avoid common mistakes.
- Never put state in local files. Use S3 backend with DynamoDB locking (AWS) or GCS backend (GCP). Always.
- One state file per environment. Never share state across dev/staging/prod.
# Good
terraform {
required_version = "~> 1.7"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.30"
}
}
backend "s3" {
bucket = "company-tfstate-prod"
key = "vpc/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "tfstate-lock"
encrypt = true
}
}
Variable hygiene
- Every variable has a
description and a type. No exceptions.
- Sensitive variables marked with
sensitive = true.
- Defaults only for genuinely safe values. Don't default an environment name to
"prod".
- Use
validation blocks for inputs with constraints (e.g., instance type must be in approved list).
Resource naming
- Consistent naming pattern:
{project}-{env}-{purpose} (e.g., acme-prod-api-eks).
- Tag every resource:
Environment, Project, Owner, CostCenter, ManagedBy=terraform.
State management
- Run
terraform plan in CI on every PR. Block merge if plan fails.
terraform apply runs only from CI on merge to main, never from a developer's laptop.
- Use Atlantis, Spacelift, or Terraform Cloud for plan/apply automation.
- Never use
-auto-approve outside of automated CI/CD with strict guardrails.
What NOT to do
- ❌ Hardcoded credentials, ARNs, or account IDs in
.tf files — use variables or data sources
- ❌
local-exec provisioners — they break reproducibility; use proper providers
- ❌ One giant
main.tf — split by resource type or concern
- ❌ Manual changes in the cloud console that aren't reflected in code — drift kills you
Kubernetes
Workload defaults
Every Deployment / StatefulSet must have:
spec:
replicas: 3 # never 1 in production; minimum 2 for HA, 3 for quorum-based systems
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 65534 # nobody
fsGroup: 65534
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: registry.example.com/app:sha-abc123 # always a SHA tag, never latest
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi # set memory limit; do NOT set CPU limit (causes throttling)
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /readyz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 10
Required accompanying resources
For every Deployment, also create:
- Service — even if internal-only
- PodDisruptionBudget —
minAvailable: 1 or maxUnavailable: 1 based on replicas; required for safe rolling updates and node draining
- NetworkPolicy — default-deny ingress/egress, then allow what's needed
- HorizontalPodAutoscaler — even with conservative limits, prevents single-replica outages under load
- ServiceMonitor (if using Prometheus Operator) — observability is not optional
Helm vs Kustomize vs raw YAML
Use Helm. For everything non-trivial. Reasons:
- Versioned releases with rollback (
helm rollback)
- Templating handles environment differences cleanly
- De-facto standard — every operator and tool publishes a Helm chart
- Helm 3 has no Tiller (the security objection from Helm 2 is dead)
Use Kustomize only for simple overlays on top of upstream YAML you don't control.
Never use raw YAML for production deployments — no rollback story, no diff, no upgrade safety.
Namespaces
- One namespace per application or tightly-coupled group
- Apply ResourceQuotas and LimitRanges per namespace
- Never deploy to
default namespace in production
Cluster-level
- Use managed K8s (EKS, GKE, AKS) — self-managed K8s is not worth the operational burden in 2026
- Enable cluster-level features: audit logging, encryption at rest, private endpoints
- Use Karpenter (AWS) or Cluster Autoscaler for node management. Karpenter is the default choice in 2026.
- Install: cert-manager (TLS), external-dns (DNS automation), metrics-server (HPA), Prometheus stack, OPA Gatekeeper or Kyverno (policy)
What NOT to do
- ❌
latest image tags in production — always immutable SHAs or semver-pinned versions
- ❌ CPU limits — they cause throttling that's worse than the original problem; set requests, not limits
- ❌ Single replicas in production — minimum 2 for stateless, 3 for stateful
- ❌ Missing readiness probes — without them, traffic flows to unready pods during deploys
- ❌
hostNetwork: true or hostPID: true — only for system components, never apps
- ❌ Privileged containers — almost never needed; if you think you need it, you probably don't
- ❌ Sharing service accounts across workloads — one SA per workload, least privilege
CI/CD
GitHub Actions defaults
Every workflow:
- Pin actions to SHA, not version tag:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- Set explicit
permissions: block at the top — default to read-only, grant write only where needed
- Use OIDC for cloud auth (
aws-actions/configure-aws-credentials with role-to-assume), never long-lived access keys
- Pin runner versions:
runs-on: ubuntu-22.04, not ubuntu-latest
- Cache dependencies:
actions/setup-node@... with cache: 'npm', etc.
name: CI
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-22.04
permissions:
id-token: write # only this job, only this scope
contents: read
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm test
Secret handling
- Never commit secrets. Use the platform's secret store: GitHub Secrets, AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault
- For CI access to cloud: OIDC > short-lived assumed roles > long-lived access keys
- Rotate any leaked secret immediately, even if "the repo is private"
- Scan PRs for secrets with
gitleaks or trufflehog in CI
Pipeline structure
- Fast feedback first: lint → unit tests → build → integration tests → deploy
- Parallel where possible: matrix builds, parallel test shards
- Cache aggressively: node_modules, .gradle, Docker layers, Go modules — these are 90% of CI time
- One pipeline per repo: monorepo pipelines should detect what changed and run only relevant jobs
Deploy gates
- Production deploys require: passing tests, security scan, manual approval (for high-stakes changes), and a rollback plan
- Use deployment strategies: blue/green, canary, or progressive (Argo Rollouts, Flagger)
- Always include a rollback step in the pipeline — not just hope-based
What NOT to do
- ❌
actions/checkout@v4 (floating tag) — pin to SHA to avoid supply-chain attacks
- ❌
${{ github.event.pull_request.title }} interpolation in run: blocks — RCE via PR title attack
- ❌ Long-lived AWS access keys in CI — use OIDC
- ❌ Deploying directly from a developer laptop — always through CI
- ❌ Running CI on
pull_request_target without strict guardrails — exposes secrets to forked PRs
Docker
Dockerfile defaults
# Multi-stage builds: build artifacts in one stage, copy to slim runtime
FROM golang:1.22-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/app ./cmd/app
# Runtime: minimal base
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
Rules:
- Multi-stage builds for compiled languages (Go, Rust, Java)
- Distroless or Alpine base images, not full OS
- Non-root user always (
USER nonroot or numeric UID)
- Specific version tags for base images (
alpine:3.19, not alpine:latest)
- Use
.dockerignore aggressively — secrets, .git, node_modules, test fixtures
- Cache layers: order Dockerfile from least-frequently-changed to most-frequently-changed
- Sign and scan images:
cosign for signing, trivy or grype for scanning
What NOT to do
- ❌ Running as root in container — use
USER nonroot
- ❌
apt-get update && apt-get install -y without cleanup — bloats image; combine with && rm -rf /var/lib/apt/lists/*
- ❌ Storing secrets in image layers (env vars in
ENV, files in COPY) — they persist in image history forever
- ❌
COPY . . early in the Dockerfile — invalidates cache on every code change
Cloud security
IAM (universally applies across AWS, GCP, Azure)
- Least privilege. Start with zero permissions, add only what's demonstrably needed.
- No wildcards in production policies.
"Action": "*" and "Resource": "*" are red flags.
- Use roles, not users. Humans assume roles via SSO. Workloads use IAM Roles for Service Accounts (AWS IRSA) or Workload Identity (GCP).
- Rotate any long-lived credentials. Long-lived access keys are an anti-pattern. Prefer short-lived assumed roles.
- MFA required for any human access to production accounts.
Networking
- Private subnets by default. Public subnets only for resources that must accept inbound from the internet (ALB, NAT gateway).
- Security groups: least-permissive ingress. No
0.0.0.0/0:22 ever. SSH via SSM Session Manager or Tailscale, not public SSH.
- VPC endpoints for AWS service access — avoid sending traffic through the public internet to reach AWS services within the same region.
- TLS everywhere. Internal traffic too. Use service mesh (Istio, Linkerd) or sidecar TLS.
Secrets and data
- Encryption at rest by default for storage (S3, EBS, RDS, etc.). It's free in 2026; there's no excuse.
- Encryption in transit — TLS 1.3 where supported, TLS 1.2 minimum.
- Secrets in a secret manager (AWS Secrets Manager, GCP Secret Manager, Vault). Never in env vars committed to source.
- KMS-managed keys for sensitive workloads. Audit key access via CloudTrail equivalents.
What NOT to do
- ❌ S3 buckets with public read by default — explicit opt-in only, audit regularly
- ❌ IAM user with admin access for daily use — assume roles instead
- ❌ Storing terraform state unencrypted in S3 —
encrypt = true is one line
- ❌ Allowing 0.0.0.0/0 in security groups for non-internet-facing resources
- ❌ Disabling CloudTrail/CloudWatch to "save money" — you need the audit trail when things go wrong
Observability
The three pillars
Every production workload must emit:
- Logs — structured JSON, single line per log event, sent to a central log aggregator
- Metrics — Prometheus format, exposed on
/metrics, scraped by Prometheus or compatible collector
- Traces — OpenTelemetry SDK, traces sent to a backend (Jaeger, Tempo, Honeycomb, Datadog)
Logging
- Structured logs only (JSON). No printf-style unstructured logs in production.
- Required fields:
timestamp, level, service, trace_id, span_id, message
- Log levels: ERROR (real problems), WARN (degraded but working), INFO (significant events), DEBUG (off in production)
- Never log: passwords, tokens, PII, full credit card numbers, full request/response bodies of sensitive endpoints
- Use OpenTelemetry's logging SDK to correlate logs with traces automatically
Metrics
- Use Prometheus naming conventions:
metric_name_unit{labels}
- Histograms for latency, counters for events, gauges for instantaneous values
- Cardinality discipline: avoid high-cardinality labels (user IDs, request IDs) — they explode metric storage
- The Four Golden Signals: latency, traffic, errors, saturation — every service must expose these
Alerting
- Alert on symptoms, not causes (alert on "users seeing errors", not "CPU at 80%")
- Every alert links to a runbook (
docs/runbooks/<alert-name>.md)
- Alert fatigue is real: if an alert fires more than once a week and isn't actioned, fix it or delete it
- Use SLO-based alerting (burn-rate alerts) for user-facing services
What NOT to do
- ❌ Unstructured text logs that can't be queried
- ❌ Logging at INFO level for every request — flood your aggregator and your bill
- ❌ Alerts without runbooks — on-call wakes up at 3am with no idea what to do
- ❌ Sampling all traces — sample, but keep error traces and slow traces at 100%
Cost optimization
Compute
- Right-size first. Most workloads are over-provisioned by 2-5x. Use Vertical Pod Autoscaler in recommendation mode for K8s; check CloudWatch metrics for EC2.
- Spot instances for fault-tolerant workloads. Batch jobs, dev/staging clusters, stateless web tiers with multiple replicas. Use Karpenter to manage spot pools safely.
- Reserved Instances or Savings Plans for steady-state baseline load. Commit to 1-year if usage is predictable; 3-year only if very confident.
- Auto-scale aggressively in non-prod. Scale dev clusters to zero overnight and on weekends —
kube-green, Karpenter consolidation, scheduled scaling.
Storage
- Lifecycle policies for object storage. S3 → Glacier after N days, delete logs after retention period. Cost compounds.
- Right-size EBS volumes. Default to gp3 (cheaper and faster than gp2 in 2026). Resize down or delete unused.
- Compress logs and metrics at rest. Default compression in modern log aggregators.
Network
- VPC endpoints to avoid NAT gateway costs. NAT charges per GB; endpoints are free for in-region S3/DynamoDB.
- CloudFront / CDN for egress-heavy workloads. Edge cache reduces origin egress.
- Same-AZ where possible for chatty services. Cross-AZ data transfer charges add up.
Visibility
- Cost allocation tags on every resource. Without tags, you can't attribute spend.
- Daily cost anomaly detection (AWS Cost Anomaly Detection, GCP Recommender). Catches surprise spend.
- Per-team chargeback with Kubecost or OpenCost in Kubernetes environments.
What NOT to do
- ❌ Running 24/7 dev clusters that nobody uses on weekends
- ❌ EBS volumes attached to terminated instances ("orphaned volumes") — script regular cleanup
- ❌ Old EBS snapshots accumulating forever — lifecycle policy
- ❌ NAT gateway routing for traffic that could use a VPC endpoint
- ❌ Ignoring Cost Explorer / Billing alerts — set a monthly budget alert at $X, $2X, $5X
Disaster recovery
Backups
- Define RPO and RTO for every system. Without targets, "we have backups" is meaningless.
- Test restores quarterly. Untested backups don't exist.
- 3-2-1 rule: 3 copies, 2 different media, 1 off-site. Modern translation: production data, snapshot in same region, replicated copy in different region.
- Application-consistent backups for databases (not just disk snapshots).
Runbooks
- Every critical system has a runbook in
docs/runbooks/<system>.md
- Runbook structure: detection → diagnosis → mitigation → resolution → postmortem prompt
- Runbooks are tested, not aspirational — game day exercises catch the gaps
Chaos engineering
- For high-availability systems, regular chaos drills: kill pods, drain nodes, simulate AZ failure
- Tools: Chaos Mesh, Litmus (K8s), AWS Fault Injection Simulator
- Start with non-production. Move to production only when team is mature.
Common antipatterns (refuse to generate without surfacing the issue)
When the user asks for any of these, explain the risk before generating:
- Public S3 bucket — confirm intent, suggest pre-signed URLs or CloudFront instead
- Security group with 0.0.0.0/0 on SSH/RDP — suggest SSM Session Manager or VPN
- Long-lived AWS access keys in CI — suggest OIDC
- Kubernetes Deployment with
replicas: 1 for production — flag the SPOF risk
latest image tag — flag the rollback impossibility
- Terraform
local-exec for cloud resource creation — suggest proper providers
- Disabling audit logs to "reduce noise" — flag the incident-response cost
- Custom encryption implementation — never; use platform KMS
chmod 777 in scripts — explain why and suggest specific permissions
When generating infra code
- Apply the relevant defaults from above
- Add inline comments where a non-obvious choice was made
- Include a brief "production checklist" comment at the top of generated files listing what the user still needs to verify
- If the user's environment is unclear (dev vs prod, scale, compliance requirements), ask before generating
Example top-of-file comment for a generated production manifest:
# Production checklist:
# [ ] Verify resource requests/limits match actual workload profile
# [ ] Confirm liveness/readiness probe endpoints exist in the app
# [ ] Apply NetworkPolicy (separate file)
# [ ] Configure HorizontalPodAutoscaler (separate file)
# [ ] Set up ServiceMonitor for Prometheus scraping
# [ ] Document runbook for this service
What this skill is NOT
- Not a tutorial — assumes basic familiarity with the tools
- Not exhaustive — covers the failure modes that hurt teams most, not every possible best practice
- Not opinionated about everything — silent on choices where the tradeoff is genuinely subjective (e.g., Go vs Rust for tooling)
- Not a replacement for thinking — defaults are starting points, not unchangeable rules
When a user has a specific reason to deviate, support them. The defaults exist to prevent the common mistakes, not to suppress valid context-specific decisions.
1---2name: devops-best-practices3description: Opinionated production-grade DevOps defaults for Terraform, Kubernetes, CI/CD, Docker, cloud security, observability, cost, and disaster recovery. ALWAYS use when generating, reviewing, or modifying any infrastructure code, Kubernetes manifests (Deployment, Service, StatefulSet, Helm, Kustomize), Terraform (.tf, modules, state), Dockerfiles, docker-compose, CI/CD pipelines (.github/workflows, .gitlab-ci.yml, Jenkinsfile), cloud resources (AWS/GCP/Azure), IAM policies, security groups, observability setup (Prometheus, Grafana, OpenTelemetry), or DNS/TLS/CDN config — even if the user does not explicitly ask for best practices. Prevents the failure modes that hurt production teams most often: missing PDBs, single replicas in prod, latest image tags, public S3 buckets, long-lived credentials, missing observability, and CI/CD supply-chain risks. Apply opinionated defaults by default; surface tradeoffs when the user has reason to deviate.4---56# DevOps Best Practices78This skill encodes opinionated, production-grade DevOps defaults. Apply them whenever generating or reviewing infrastructure code. When the user's request conflicts with a default below, surface the conflict and explain the tradeoff — don't silently override.910These are **opinionated**. Other valid approaches exist. The opinions here are chosen because they prevent the failure modes that hurt teams most often in real production environments.1112---1314## When to use this skill1516Trigger whenever the task involves any of:1718- Terraform files (`*.tf`, `*.tfvars`), Terragrunt, Pulumi, CDK19- Kubernetes manifests (Deployment, Service, Ingress, StatefulSet, etc.), Helm charts, Kustomize overlays20- Dockerfiles, `docker-compose.yml`21- CI/CD config (`.github/workflows/*.yml`, `.gitlab-ci.yml`, Jenkinsfile, CircleCI, Buildkite)22- Cloud provider SDKs or CLIs (AWS, GCP, Azure)23- IAM policies, security groups, network ACLs24- Observability config (Prometheus, Grafana, OpenTelemetry, Datadog, CloudWatch)25- Shell scripts deployed to servers (`/etc/init.d`, systemd units, deploy scripts)26- DNS, TLS, CDN configuration2728If unsure, default to applying the safety and security sections (they almost never hurt).2930---3132## Foundational principles (apply to everything)33341. **Default to safety over convenience.** A slightly harder UX that prevents production incidents wins.352. **Default to least privilege.** Start with zero permissions and add only what the workload demonstrably needs.363. **Make failure modes loud.** Silent failures destroy trust. Logs > swallowed errors. Alerts > "we'll notice eventually."374. **Cost is a non-functional requirement.** Generated infra that costs 10x what it should is a bug.385. **Reproducibility beats cleverness.** If a teammate can't recreate this environment from the repo in 30 minutes, the design is wrong.396. **Document tradeoffs inline.** If a non-obvious choice was made, leave a comment explaining why so future-you (or future-Claude) doesn't undo it.4041---4243## Terraform4445### Module structure4647- One module per logical resource grouping. Modules in `modules/<name>/`. Roots in `environments/<env>/`.48- Always pin module versions and provider versions. Floating versions break reproducibility.49- Use `terraform-aws-modules/*` community modules where available — they're battle-tested and avoid common mistakes.50- Never put state in local files. Use S3 backend with DynamoDB locking (AWS) or GCS backend (GCP). Always.51- One state file per environment. Never share state across dev/staging/prod.5253```hcl54# Good55terraform {56 required_version = "~> 1.7"57 required_providers {58 aws = {59 source = "hashicorp/aws"60 version = "~> 5.30"61 }62 }63 backend "s3" {64 bucket = "company-tfstate-prod"65 key = "vpc/terraform.tfstate"66 region = "us-east-1"67 dynamodb_table = "tfstate-lock"68 encrypt = true69 }70}71```7273### Variable hygiene7475- Every variable has a `description` and a `type`. No exceptions.76- Sensitive variables marked with `sensitive = true`.77- Defaults only for genuinely safe values. Don't default an environment name to `"prod"`.78- Use `validation` blocks for inputs with constraints (e.g., instance type must be in approved list).7980### Resource naming8182- Consistent naming pattern: `{project}-{env}-{purpose}` (e.g., `acme-prod-api-eks`).83- Tag every resource: `Environment`, `Project`, `Owner`, `CostCenter`, `ManagedBy=terraform`.8485### State management8687- Run `terraform plan` in CI on every PR. Block merge if plan fails.88- `terraform apply` runs only from CI on merge to main, never from a developer's laptop.89- Use Atlantis, Spacelift, or Terraform Cloud for plan/apply automation.90- Never use `-auto-approve` outside of automated CI/CD with strict guardrails.9192### What NOT to do9394- ❌ Hardcoded credentials, ARNs, or account IDs in `.tf` files — use variables or data sources95- ❌ `local-exec` provisioners — they break reproducibility; use proper providers96- ❌ One giant `main.tf` — split by resource type or concern97- ❌ Manual changes in the cloud console that aren't reflected in code — drift kills you9899---100101## Kubernetes102103### Workload defaults104105Every Deployment / StatefulSet must have:106107```yaml108spec:109 replicas: 3 # never 1 in production; minimum 2 for HA, 3 for quorum-based systems110 template:111 spec:112 securityContext:113 runAsNonRoot: true114 runAsUser: 65534 # nobody115 fsGroup: 65534116 seccompProfile:117 type: RuntimeDefault118 containers:119 - name: app120 image: registry.example.com/app:sha-abc123 # always a SHA tag, never latest121 imagePullPolicy: IfNotPresent122 securityContext:123 allowPrivilegeEscalation: false124 readOnlyRootFilesystem: true125 capabilities:126 drop: ["ALL"]127 resources:128 requests:129 cpu: 100m130 memory: 128Mi131 limits:132 memory: 256Mi # set memory limit; do NOT set CPU limit (causes throttling)133 livenessProbe:134 httpGet:135 path: /healthz136 port: 8080137 initialDelaySeconds: 30138 periodSeconds: 10139 failureThreshold: 3140 readinessProbe:141 httpGet:142 path: /readyz143 port: 8080144 initialDelaySeconds: 5145 periodSeconds: 5146 startupProbe:147 httpGet:148 path: /healthz149 port: 8080150 failureThreshold: 30151 periodSeconds: 10152```153154### Required accompanying resources155156For every Deployment, also create:1571581. **Service** — even if internal-only1592. **PodDisruptionBudget** — `minAvailable: 1` or `maxUnavailable: 1` based on replicas; required for safe rolling updates and node draining1603. **NetworkPolicy** — default-deny ingress/egress, then allow what's needed1614. **HorizontalPodAutoscaler** — even with conservative limits, prevents single-replica outages under load1625. **ServiceMonitor** (if using Prometheus Operator) — observability is not optional163164### Helm vs Kustomize vs raw YAML165166**Use Helm.** For everything non-trivial. Reasons:167168- Versioned releases with rollback (`helm rollback`)169- Templating handles environment differences cleanly170- De-facto standard — every operator and tool publishes a Helm chart171- Helm 3 has no Tiller (the security objection from Helm 2 is dead)172173Use Kustomize only for simple overlays on top of upstream YAML you don't control.174175Never use raw YAML for production deployments — no rollback story, no diff, no upgrade safety.176177### Namespaces178179- One namespace per application or tightly-coupled group180- Apply ResourceQuotas and LimitRanges per namespace181- Never deploy to `default` namespace in production182183### Cluster-level184185- Use **managed K8s** (EKS, GKE, AKS) — self-managed K8s is not worth the operational burden in 2026186- Enable cluster-level features: audit logging, encryption at rest, private endpoints187- Use **Karpenter** (AWS) or **Cluster Autoscaler** for node management. Karpenter is the default choice in 2026.188- Install: cert-manager (TLS), external-dns (DNS automation), metrics-server (HPA), Prometheus stack, OPA Gatekeeper or Kyverno (policy)189190### What NOT to do191192- ❌ `latest` image tags in production — always immutable SHAs or semver-pinned versions193- ❌ CPU limits — they cause throttling that's worse than the original problem; set requests, not limits194- ❌ Single replicas in production — minimum 2 for stateless, 3 for stateful195- ❌ Missing readiness probes — without them, traffic flows to unready pods during deploys196- ❌ `hostNetwork: true` or `hostPID: true` — only for system components, never apps197- ❌ Privileged containers — almost never needed; if you think you need it, you probably don't198- ❌ Sharing service accounts across workloads — one SA per workload, least privilege199200---201202## CI/CD203204### GitHub Actions defaults205206Every workflow:207208- Pin actions to SHA, not version tag: `uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11`209- Set explicit `permissions:` block at the top — default to read-only, grant write only where needed210- Use OIDC for cloud auth (`aws-actions/configure-aws-credentials` with `role-to-assume`), never long-lived access keys211- Pin runner versions: `runs-on: ubuntu-22.04`, not `ubuntu-latest`212- Cache dependencies: `actions/setup-node@...` with `cache: 'npm'`, etc.213214```yaml215name: CI216on:217 pull_request:218 push:219 branches: [main]220221permissions:222 contents: read223224jobs:225 test:226 runs-on: ubuntu-22.04227 permissions:228 id-token: write # only this job, only this scope229 contents: read230 steps:231 - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11232 - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b233 with:234 node-version: '20'235 cache: 'npm'236 - run: npm ci237 - run: npm test238```239240### Secret handling241242- Never commit secrets. Use the platform's secret store: GitHub Secrets, AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault243- For CI access to cloud: OIDC > short-lived assumed roles > long-lived access keys244- Rotate any leaked secret immediately, even if "the repo is private"245- Scan PRs for secrets with `gitleaks` or `trufflehog` in CI246247### Pipeline structure248249- **Fast feedback first**: lint → unit tests → build → integration tests → deploy250- **Parallel where possible**: matrix builds, parallel test shards251- **Cache aggressively**: node_modules, .gradle, Docker layers, Go modules — these are 90% of CI time252- **One pipeline per repo**: monorepo pipelines should detect what changed and run only relevant jobs253254### Deploy gates255256- Production deploys require: passing tests, security scan, manual approval (for high-stakes changes), and a rollback plan257- Use deployment strategies: blue/green, canary, or progressive (Argo Rollouts, Flagger)258- Always include a rollback step in the pipeline — not just hope-based259260### What NOT to do261262- ❌ `actions/checkout@v4` (floating tag) — pin to SHA to avoid supply-chain attacks263- ❌ `${{ github.event.pull_request.title }}` interpolation in `run:` blocks — RCE via PR title attack264- ❌ Long-lived AWS access keys in CI — use OIDC265- ❌ Deploying directly from a developer laptop — always through CI266- ❌ Running CI on `pull_request_target` without strict guardrails — exposes secrets to forked PRs267268---269270## Docker271272### Dockerfile defaults273274```dockerfile275# Multi-stage builds: build artifacts in one stage, copy to slim runtime276FROM golang:1.22-alpine AS build277WORKDIR /src278COPY go.mod go.sum ./279RUN go mod download280COPY . .281RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/app ./cmd/app282283# Runtime: minimal base284FROM gcr.io/distroless/static-debian12:nonroot285COPY --from=build /out/app /app286USER nonroot:nonroot287ENTRYPOINT ["/app"]288```289290Rules:291292- Multi-stage builds for compiled languages (Go, Rust, Java)293- Distroless or Alpine base images, not full OS294- Non-root user always (`USER nonroot` or numeric UID)295- Specific version tags for base images (`alpine:3.19`, not `alpine:latest`)296- Use `.dockerignore` aggressively — secrets, `.git`, `node_modules`, test fixtures297- Cache layers: order Dockerfile from least-frequently-changed to most-frequently-changed298- Sign and scan images: `cosign` for signing, `trivy` or `grype` for scanning299300### What NOT to do301302- ❌ Running as root in container — use `USER nonroot`303- ❌ `apt-get update && apt-get install -y` without cleanup — bloats image; combine with `&& rm -rf /var/lib/apt/lists/*`304- ❌ Storing secrets in image layers (env vars in `ENV`, files in `COPY`) — they persist in image history forever305- ❌ `COPY . .` early in the Dockerfile — invalidates cache on every code change306307---308309## Cloud security310311### IAM (universally applies across AWS, GCP, Azure)312313- **Least privilege.** Start with zero permissions, add only what's demonstrably needed.314- **No wildcards in production policies.** `"Action": "*"` and `"Resource": "*"` are red flags.315- **Use roles, not users.** Humans assume roles via SSO. Workloads use IAM Roles for Service Accounts (AWS IRSA) or Workload Identity (GCP).316- **Rotate any long-lived credentials.** Long-lived access keys are an anti-pattern. Prefer short-lived assumed roles.317- **MFA required** for any human access to production accounts.318319### Networking320321- **Private subnets by default.** Public subnets only for resources that must accept inbound from the internet (ALB, NAT gateway).322- **Security groups: least-permissive ingress.** No `0.0.0.0/0:22` ever. SSH via SSM Session Manager or Tailscale, not public SSH.323- **VPC endpoints for AWS service access** — avoid sending traffic through the public internet to reach AWS services within the same region.324- **TLS everywhere.** Internal traffic too. Use service mesh (Istio, Linkerd) or sidecar TLS.325326### Secrets and data327328- **Encryption at rest by default** for storage (S3, EBS, RDS, etc.). It's free in 2026; there's no excuse.329- **Encryption in transit** — TLS 1.3 where supported, TLS 1.2 minimum.330- **Secrets in a secret manager** (AWS Secrets Manager, GCP Secret Manager, Vault). Never in env vars committed to source.331- **KMS-managed keys** for sensitive workloads. Audit key access via CloudTrail equivalents.332333### What NOT to do334335- ❌ S3 buckets with public read by default — explicit opt-in only, audit regularly336- ❌ IAM user with admin access for daily use — assume roles instead337- ❌ Storing terraform state unencrypted in S3 — `encrypt = true` is one line338- ❌ Allowing 0.0.0.0/0 in security groups for non-internet-facing resources339- ❌ Disabling CloudTrail/CloudWatch to "save money" — you need the audit trail when things go wrong340341---342343## Observability344345### The three pillars346347Every production workload must emit:3483491. **Logs** — structured JSON, single line per log event, sent to a central log aggregator3502. **Metrics** — Prometheus format, exposed on `/metrics`, scraped by Prometheus or compatible collector3513. **Traces** — OpenTelemetry SDK, traces sent to a backend (Jaeger, Tempo, Honeycomb, Datadog)352353### Logging354355- Structured logs only (JSON). No printf-style unstructured logs in production.356- Required fields: `timestamp`, `level`, `service`, `trace_id`, `span_id`, `message`357- Log levels: ERROR (real problems), WARN (degraded but working), INFO (significant events), DEBUG (off in production)358- Never log: passwords, tokens, PII, full credit card numbers, full request/response bodies of sensitive endpoints359- Use OpenTelemetry's logging SDK to correlate logs with traces automatically360361### Metrics362363- Use Prometheus naming conventions: `metric_name_unit{labels}`364- Histograms for latency, counters for events, gauges for instantaneous values365- Cardinality discipline: avoid high-cardinality labels (user IDs, request IDs) — they explode metric storage366- The Four Golden Signals: latency, traffic, errors, saturation — every service must expose these367368### Alerting369370- Alert on symptoms, not causes (alert on "users seeing errors", not "CPU at 80%")371- Every alert links to a runbook (`docs/runbooks/<alert-name>.md`)372- Alert fatigue is real: if an alert fires more than once a week and isn't actioned, fix it or delete it373- Use SLO-based alerting (burn-rate alerts) for user-facing services374375### What NOT to do376377- ❌ Unstructured text logs that can't be queried378- ❌ Logging at INFO level for every request — flood your aggregator and your bill379- ❌ Alerts without runbooks — on-call wakes up at 3am with no idea what to do380- ❌ Sampling all traces — sample, but keep error traces and slow traces at 100%381382---383384## Cost optimization385386### Compute387388- **Right-size first.** Most workloads are over-provisioned by 2-5x. Use Vertical Pod Autoscaler in recommendation mode for K8s; check CloudWatch metrics for EC2.389- **Spot instances for fault-tolerant workloads.** Batch jobs, dev/staging clusters, stateless web tiers with multiple replicas. Use Karpenter to manage spot pools safely.390- **Reserved Instances or Savings Plans** for steady-state baseline load. Commit to 1-year if usage is predictable; 3-year only if very confident.391- **Auto-scale aggressively in non-prod.** Scale dev clusters to zero overnight and on weekends — `kube-green`, Karpenter consolidation, scheduled scaling.392393### Storage394395- **Lifecycle policies for object storage.** S3 → Glacier after N days, delete logs after retention period. Cost compounds.396- **Right-size EBS volumes.** Default to gp3 (cheaper and faster than gp2 in 2026). Resize down or delete unused.397- **Compress logs and metrics at rest.** Default compression in modern log aggregators.398399### Network400401- **VPC endpoints to avoid NAT gateway costs.** NAT charges per GB; endpoints are free for in-region S3/DynamoDB.402- **CloudFront / CDN for egress-heavy workloads.** Edge cache reduces origin egress.403- **Same-AZ where possible** for chatty services. Cross-AZ data transfer charges add up.404405### Visibility406407- **Cost allocation tags** on every resource. Without tags, you can't attribute spend.408- **Daily cost anomaly detection** (AWS Cost Anomaly Detection, GCP Recommender). Catches surprise spend.409- **Per-team chargeback** with Kubecost or OpenCost in Kubernetes environments.410411### What NOT to do412413- ❌ Running 24/7 dev clusters that nobody uses on weekends414- ❌ EBS volumes attached to terminated instances ("orphaned volumes") — script regular cleanup415- ❌ Old EBS snapshots accumulating forever — lifecycle policy416- ❌ NAT gateway routing for traffic that could use a VPC endpoint417- ❌ Ignoring Cost Explorer / Billing alerts — set a monthly budget alert at $X, $2X, $5X418419---420421## Disaster recovery422423### Backups424425- **Define RPO and RTO** for every system. Without targets, "we have backups" is meaningless.426- **Test restores quarterly.** Untested backups don't exist.427- **3-2-1 rule**: 3 copies, 2 different media, 1 off-site. Modern translation: production data, snapshot in same region, replicated copy in different region.428- **Application-consistent backups** for databases (not just disk snapshots).429430### Runbooks431432- Every critical system has a runbook in `docs/runbooks/<system>.md`433- Runbook structure: detection → diagnosis → mitigation → resolution → postmortem prompt434- Runbooks are tested, not aspirational — game day exercises catch the gaps435436### Chaos engineering437438- For high-availability systems, regular chaos drills: kill pods, drain nodes, simulate AZ failure439- Tools: Chaos Mesh, Litmus (K8s), AWS Fault Injection Simulator440- Start with non-production. Move to production only when team is mature.441442---443444## Common antipatterns (refuse to generate without surfacing the issue)445446When the user asks for any of these, explain the risk before generating:447448- Public S3 bucket — confirm intent, suggest pre-signed URLs or CloudFront instead449- Security group with 0.0.0.0/0 on SSH/RDP — suggest SSM Session Manager or VPN450- Long-lived AWS access keys in CI — suggest OIDC451- Kubernetes Deployment with `replicas: 1` for production — flag the SPOF risk452- `latest` image tag — flag the rollback impossibility453- Terraform `local-exec` for cloud resource creation — suggest proper providers454- Disabling audit logs to "reduce noise" — flag the incident-response cost455- Custom encryption implementation — never; use platform KMS456- `chmod 777` in scripts — explain why and suggest specific permissions457458---459460## When generating infra code4614621. Apply the relevant defaults from above4632. Add inline comments where a non-obvious choice was made4643. Include a brief "production checklist" comment at the top of generated files listing what the user still needs to verify4654. If the user's environment is unclear (dev vs prod, scale, compliance requirements), ask before generating466467Example top-of-file comment for a generated production manifest:468469```yaml470# Production checklist:471# [ ] Verify resource requests/limits match actual workload profile472# [ ] Confirm liveness/readiness probe endpoints exist in the app473# [ ] Apply NetworkPolicy (separate file)474# [ ] Configure HorizontalPodAutoscaler (separate file)475# [ ] Set up ServiceMonitor for Prometheus scraping476# [ ] Document runbook for this service477```478479---480481## What this skill is NOT482483- Not a tutorial — assumes basic familiarity with the tools484- Not exhaustive — covers the failure modes that hurt teams most, not every possible best practice485- Not opinionated about everything — silent on choices where the tradeoff is genuinely subjective (e.g., Go vs Rust for tooling)486- Not a replacement for thinking — defaults are starting points, not unchangeable rules487488When a user has a specific reason to deviate, support them. The defaults exist to prevent the common mistakes, not to suppress valid context-specific decisions.