diff --git a/.ciagent/CHECKPOINT.json b/.ciagent/CHECKPOINT.json index a036394..6fd29f1 100644 --- a/.ciagent/CHECKPOINT.json +++ b/.ciagent/CHECKPOINT.json @@ -1,12 +1,10 @@ { - "phase": 3, - "stage": "complete", + "phase": 4, + "stage": "execute", "milestone": "v0.2", "phase_role": "execution", "project": "atelier", "attempts": 0, - "updated_at": "2026-08-05T02:25:00Z", - "milestone_complete": false, - "phase_tag": "v0.1.3", - "release_id": 465 + "updated_at": "2026-08-05T02:30:00Z", + "milestone_complete": false } \ No newline at end of file diff --git a/domains/kubernetes/kustomize.md b/domains/kubernetes/kustomize.md index 4fc2b0a..3c26dc5 100644 --- a/domains/kubernetes/kustomize.md +++ b/domains/kubernetes/kustomize.md @@ -49,6 +49,7 @@ - Use Kustomize when you patch existing manifests or keep env deltas in one repo. Use Helm when you distribute a reusable app or consume third-party charts. - Mixing both is fine and common: Kustomize for the internal apps, Helm for the packaged parts. The decision is per-workload, not per-cluster. +- `commonLabels` is the kustomize-native enforcement of P3 (Labels Select); see `domains/devops/first-principles.md` P6 (Configuration as Code) for the upstream principle that the rendered manifest — not a console click — is the source of truth. ## What Violates Kustomize Discipline diff --git a/examples/bad/k8s-bare-pod-no-resources.md b/examples/bad/k8s-bare-pod-no-resources.md new file mode 100644 index 0000000..3d642d2 --- /dev/null +++ b/examples/bad/k8s-bare-pod-no-resources.md @@ -0,0 +1,71 @@ +# Bad Example: Bare Pod, No Resources + +> A Kubernetes manifest that violates Atelier's Kubernetes principles. Each violation is cited. + +## The Code + +```yaml +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. + +## Cross-Domain Links + +- `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. \ No newline at end of file diff --git a/examples/bad/terraform-unlocked-state.md b/examples/bad/terraform-unlocked-state.md new file mode 100644 index 0000000..3d3b63e --- /dev/null +++ b/examples/bad/terraform-unlocked-state.md @@ -0,0 +1,74 @@ +# Bad Example: Unlocked Local State + +> An IaC configuration that violates Atelier's Infrastructure as Code principles. Each violation is cited. + +## The Code + +```hcl +# main.tf — committed to the repo +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + # no version pin + } + } +} + +provider "aws" { + region = "us-east-1" + access_key = "AKIAIOSFODNN7EXAMPLE" # committed + secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" # committed +} + +resource "aws_s3_bucket" "logs" { + bucket = "app-logs-prod" +} + +resource "aws_db_instance" "primary" { + identifier = "app-prod" + username = "admin" + password = "hunter2" # committed, in plaintext, in state + allocated_storage = 20 + engine = "postgres" + instance_class = "db.t3.micro" + skip_final_snapshot = true +} +``` + +The team runs `terraform apply` from their laptops. The state file `terraform.tfstate` is committed alongside `main.tf` "so everyone is on the same page." + +## What Makes It Bad + +### Local State in a Shared Environment (IaC P8 Remote State with Locking) +- State is `terraform.tfstate` on each laptop. Two team members run `terraform apply` simultaneously; the second to finish silently overwrites the first's changes. There is no lock. +- The state file is committed to the repo. It contains the DB password in plaintext. It is a secret-bearing artifact in version control. +- **Fix:** use a remote backend with locking (S3 + DynamoDB, GCS, etc.). Never commit state. See `domains/infrastructure-as-code/state.md`. + +### Hardcoded Secrets (IaC P10 Secrets Never in Code) +- `access_key` and `secret_key` are committed in `main.tf`. The DB `password` is committed and also written to state in plaintext. +- The secrets are now in the git history. Rotating them is not optional; the history must be scrubbed or the credentials rotated and the old ones revoked. +- **Fix:** credentials from environment, a secrets manager, or a `data` source (`aws_secretsmanager_secret_version`). Mark sensitive attributes `sensitive = true`. See `domains/security/secrets.md`. + +### Unpinned Provider (IaC P5 Version Everything) +- The `aws` provider has no `version`. The next `terraform init` pulls whatever is latest — a different provider version can change resource behavior with no review. +- **Fix:** pin `version = "~> 5.0"`. Commit the lock file (`.terraform.lock.hcl`). See `domains/infrastructure-as-code/terraform.md`. + +### Manual Drift, No Plan Review (IaC P4 Plan Before Apply, P9 Drift is Recoverable) +- The team applies from laptops with no `plan` review. When the DB password is wrong, someone SSHes in and changes it manually — drift that `plan` will later report as a surprise. +- Manual changes to managed resources are an incident, not a shortcut. Each one is a future `plan` diff that no one can explain. +- **Fix:** run `terraform plan` in CI; review the diff; `apply` from CI on merge. Treat every drift report as an incident to investigate. See `domains/infrastructure-as-code/state.md` (Drift and Reconciliation). + +### No Module Composition (IaC P6 Modules Compose) +- The S3 bucket and DB instance are inline. When the team needs a second bucket, they copy-paste the block and rename it. The two copies drift over time. +- **Fix:** a versioned module for each reusable pattern. The difference is a variable, not a copy. See `domains/infrastructure-as-code/modules.md` (the module-vs-copy boundary). + +## The Cascade + +The violations compound. Unlocked local state lets two `apply` runs race. Committed secrets mean the race loser's changes — and the secrets — are in the repo. Manual drift hides the corruption until a `plan` surfaces a diff no one can explain. The unpinned provider means that diff might be the provider's fault, not the team's, and no one can tell which. + +## Cross-Domain Links + +- `domains/security/secrets.md` — secret hygiene is non-tradeable; this example violates it in three places. +- `domains/security/supply-chain.md` — committed credentials in git are a supply-chain incident. +- `domains/devops/P6 Configuration as Code` — config in the repo is correct; committed *state and secrets* is the violation. \ No newline at end of file diff --git a/examples/good/k8s-deployment.md b/examples/good/k8s-deployment.md new file mode 100644 index 0000000..ac60fdf --- /dev/null +++ b/examples/good/k8s-deployment.md @@ -0,0 +1,157 @@ +# Good Example: Kubernetes Deployment + +> A Kubernetes Deployment that follows Atelier's Kubernetes principles. Each aspect cites the principle it satisfies. + +## The Deployment + +A stateless web service deployed as a Deployment with probes, resource contracts, RBAC, and a rolling update strategy — the canonical "production workload" pattern. + +### Manifest + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: api + namespace: api-prod + labels: + app: api + tier: web +spec: + replicas: 3 + selector: + matchLabels: + app: api + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + template: + metadata: + labels: + app: api + tier: web + spec: + serviceAccountName: api + automountServiceAccountToken: false + containers: + - name: api + image: registry.example.com/api:v1.4.2 # pinned, not :latest + ports: + - containerPort: 8080 + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + memory: 512Mi + readinessProbe: + httpGet: + path: /healthz/ready + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /healthz/live + port: 8080 + initialDelaySeconds: 15 + periodSeconds: 10 + env: + - name: LOG_LEVEL + valueFrom: + configMapKeyRef: + name: api-config + key: log_level + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: api-secrets + key: database_url + volumeMounts: + - name: config + mountPath: /etc/api + readOnly: true + volumes: + - name: config + configMap: + name: api-config +--- +apiVersion: v1 +kind: Service +metadata: + name: api + namespace: api-prod +spec: + selector: + app: api + ports: + - port: 80 + targetPort: 8080 +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: api-deny-ingress + namespace: api-prod +spec: + podSelector: + matchLabels: + app: api + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + tier: edge +``` + +## What Makes It Good + +### Controller, Not Bare Pod (k8s P2 Pods are Mortal) +- A `Deployment` manages the pods. If one dies, the controller replaces it. A bare pod has no recovery. +- See `domains/kubernetes/workloads.md`. + +### Resource Contracts (k8s P4 Requests and Limits are Contracts) +- Every container has CPU and memory requests and a memory limit. The workload is `Burstable`, not `BestEffort` (first evicted under pressure). +- See `domains/kubernetes/workloads.md` for QoS classes. + +### Probes (k8s P5 Probes Drive Health) +- `readinessProbe` gates traffic: a pod that is not ready is removed from the Service's endpoints. +- `livenessProbe` restarts a wedged container. +- The probes check the workload's own health (`/healthz/ready`, `/healthz/live`), not a dependency. A liveness probe that calls the database would cascade-restart on a DB blip. +- See `domains/kubernetes/workloads.md`. + +### Image Pinning (k8s P1 + IaC P5 Version Everything) +- `image: registry.example.com/api:v1.4.2` — pinned to a version, not `:latest`. A pod restart pulls the same image it was built with. +- See `domains/infrastructure-as-code/terraform.md` and `domains/devops/P7 Immutability` for the immutability angle. + +### RBAC (k8s P7 RBAC by Intent, Not Identity) +- `serviceAccountName: api` — the workload runs as a dedicated ServiceAccount, not the `default` shared identity. +- `automountServiceAccountToken: false` — the workload does not call the API, so it gets no token. See `domains/kubernetes/rbac.md`. +- A matching `Role` + `RoleBinding` (not shown) would grant `get, list, watch` on `configmaps` in this namespace — least privilege, scoped by intent. + +### Config and Secrets Separate (k8s P9 Config and Secrets are Separate) +- `LOG_LEVEL` from a ConfigMap (non-sensitive). `DATABASE_URL` from a Secret (sensitive). Both injected at runtime; neither baked into the image. +- A configuration change does not require a rebuild. A secret rotation does not require an image redeploy. +- See `domains/kubernetes/rbac.md` and `domains/security/secrets.md`. + +### Namespaces Bound Blast Radius (k8s P6 Namespaces Bound Blast Radius) +- The workload lives in `api-prod`, not `default`. The namespace is the unit of quota, RBAC, and NetworkPolicy. A problem in `api-prod` does not leak to other workloads. +- See `domains/kubernetes/networking.md`. + +### NetworkPolicy Default-Deny (k8s P6, P7) +- The `NetworkPolicy` allows ingress only from the `edge` namespace. Without it, every pod in the cluster could reach `api`. Default-deny is the baseline; allows are the exceptions. +- See `domains/kubernetes/networking.md`. + +### Roll Forward, Roll Back (k8s P10 Roll Forward Roll Back) +- `strategy: RollingUpdate` with `maxSurge: 1, maxUnavailable: 0` — the rollout adds a new pod before removing an old one. Availability is maintained. +- `kubectl rollout undo deployment/api` reverts to the previous ReplicaSet. The rollback is tested before it is needed. +- See `domains/kubernetes/workloads.md` and `domains/devops/P5 Progressive Delivery`. + +### Cross-Domain Links +- `domains/devops/P4 Rollback First` — the rollout strategy makes the deploy reversible. +- `domains/security/authorization` — the ServiceAccount + Role model is the k8s expression of least-privilege authorization. +- `domains/observability/metrics` — the probes are the platform's observability into the workload's health; the workload's own metrics complete the picture. \ No newline at end of file diff --git a/examples/good/terraform-module.md b/examples/good/terraform-module.md new file mode 100644 index 0000000..1693606 --- /dev/null +++ b/examples/good/terraform-module.md @@ -0,0 +1,125 @@ +# Good Example: Terraform Module + +> A reusable Terraform module that follows Atelier's Infrastructure as Code principles. Each aspect cites the principle it satisfies. + +## The Module + +A versioned module that provisions an S3 bucket with logging, versioning, and encryption — the canonical "secure bucket" pattern, composed rather than copy-pasted. + +### Consumer Call + +```hcl +module "logs_bucket" { + source = "registry.example.com/infra/secure-bucket/aws" + version = "1.2.0" + + name = "app-logs" + region = "us-east-1" + force_destroy = false + retention_days = 90 +} +``` + +### Module Structure + +``` +secure-bucket/ +├── main.tf # the resource +├── variables.tf # typed inputs +├── outputs.tf # the interface to consumers +├── versions.tf # provider pin +└── README.md # the module contract +``` + +### `versions.tf` (P5 Version Everything) + +```hcl +terraform { + required_version = ">= 1.5.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} +``` + +### `variables.tf` (P1 Declarative Intent, C2 Clarity) + +```hcl +variable "name" { + type = string + description = "Globally unique bucket name." + validation { + condition = can(regex("^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$", var.name)) + error_message = "Bucket name must be lowercase, 3-63 chars, DNS-compatible." + } +} + +variable "retention_days" { + type = number + default = 30 + description = "S3 lifecycle transition age in days." +} +``` + +### `main.tf` (P1, P3 State is Truth, P10 Secrets Never in Code) + +```hcl +resource "aws_s3_bucket" "this" { + bucket = var.name +} + +resource "aws_s3_bucket_versioning" "this" { + bucket = aws_s3_bucket.this.id + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "this" { + bucket = aws_s3_bucket.this.id + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +resource "aws_s3_bucket_lifecycle_configuration" "this" { + bucket = aws_s3_bucket.this.id + rule { + id = "retention" + status = "Enabled" + filter { prefix = "" } + expiration { days = var.retention_days } + } +} +``` + +## What Makes It Good + +### Composition (IaC P6 Modules Compose, C6 Composability) +- The bucket pattern is one module, versioned once, consumed many times. A new consumer does not copy 40 lines of HCL — they call the module with a `name` and a `retention_days`. +- See `domains/infrastructure-as-code/modules.md` for the module-vs-copy boundary. + +### Pinning (IaC P5 Version Everything) +- The consumer pins `version = "1.2.0"`. The module pins its provider (`version = "~> 5.0"`) and the required Terraform version. A commit is a complete, reproducible world. +- No `latest` anywhere. See `domains/infrastructure-as-code/terraform.md`. + +### State Discipline (IaC P3 State is Truth, P8 Remote State with Locking) +- The consumer's root configuration declares a remote backend with locking (S3 + DynamoDB, GCS, etc.). The module itself does not declare a backend — the consumer owns state. +- See `domains/infrastructure-as-code/state.md` for backend selection and locking. + +### Secrets Hygiene (IaC P10 Secrets Never in Code) +- The bucket is encrypted at rest (SSE-S3 AES256). No secret is hardcoded; encryption is a provider-managed default. If KMS were used, the key would come from a `data` source or a dedicated KMS module — never a literal. +- See `domains/security/secrets.md` for the general secrets principles. + +### Plan Before Apply (IaC P4 Plan Before Apply) +- The consumer runs `terraform plan` before `apply`. The plan shows the new bucket, versioning, encryption, and lifecycle. Every line is reviewed. The plan is the contract review; `apply` is the signature. + +### Cross-Domain Links +- `domains/devops/P1 Reproducibility` — the module makes the bucket reproducible from source. +- `domains/devops/P6 Configuration as Code` — the bucket is config, not a console click. +- `domains/security/supply-chain` — a versioned, signed module from a trusted registry is a supply-chain control. \ No newline at end of file