docs(milestone): complete v0.2 — infrastructure-as-code + kubernetes
---ci--- project: atelier phase: 5 milestone: v0.2 status: complete requirements: covered: [ATELIER-36, ATELIER-37, ATELIER-38, ATELIER-39, ATELIER-40, ATELIER-41, ATELIER-42, ATELIER-43, ATELIER-44, ATELIER-45, ATELIER-46, ATELIER-47, ATELIER-48, ATELIER-49, ATELIER-50, ATELIER-51, ATELIER-52, ATELIER-53, ATELIER-54, ATELIER-55, ATELIER-56, ATELIER-57, ATELIER-58, ATELIER-59] partial: [] ---/ci---
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user