style(no-slop): remove every em-dash + banned words across all modules + capstone
Apply the no-ai-slop standard (now binding in AGENTS.md): the em-dash character is banned outright (restructured, not blind-replaced), plus the banned word/phrase list (delve, leverage, robust, seamless, truly, unlock, etc.). 0 em-dashes remain in modules + capstone; the only "robust" left is the planted M10 ai-change.patch trap. Module H1 titles use a colon separator. All deliberate teaching devices preserved; labs compile/parse (py/sh/yaml/json); no junk. AGENTS.md updated with the hard no-slop rules. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TfzV5QvtPDz8LJS3Pu5VLT
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Module 18 — Continuous Delivery and Deployment
|
||||
# Module 18: Continuous Delivery and Deployment
|
||||
|
||||
> **Merged isn't running.** This module closes the last gap in the pipeline: getting approved code
|
||||
> from `main` to something actually serving traffic, automatically, with a way back when it's wrong.
|
||||
@@ -7,18 +7,18 @@
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Module 10 — Reviewing Code You Didn't Write.** The PR review gate. Auto-deploy is only safe
|
||||
- **Module 10: Reviewing Code You Didn't Write.** The PR review gate. Auto-deploy is only safe
|
||||
because a human (or an agent under supervision) signed off on the diff first.
|
||||
- **Module 14 — Continuous Integration.** You already have a pipeline that lints, builds, and tests
|
||||
on every push. CD is not a new system — it's **more stages on that same pipeline**, after the
|
||||
- **Module 14: Continuous Integration.** You already have a pipeline that lints, builds, and tests
|
||||
on every push. CD is not a new system; it's **more stages on that same pipeline**, after the
|
||||
checks pass.
|
||||
- **Module 15 — Security Scanning.** Dependency, secret, and static-analysis gates on the same
|
||||
- **Module 15: Security Scanning.** Dependency, secret, and static-analysis gates on the same
|
||||
pushes. These are part of what makes shipping without a human in the loop survivable.
|
||||
- **Module 16 — Containers and Reproducible Environments.** The container image is *what you ship*.
|
||||
- **Module 16: Containers and Reproducible Environments.** The container image is *what you ship*.
|
||||
CD takes that image and runs it somewhere. This module assumes you can already build and tag an
|
||||
image of the `tasks-app`.
|
||||
- **Module 17 — Secrets, Config, and Environments.** A running service needs configuration and
|
||||
secrets at runtime — *what it needs to run*. CD wires those into the deploy step instead of baking
|
||||
- **Module 17: Secrets, Config, and Environments.** A running service needs configuration and
|
||||
secrets at runtime, *what it needs to run*. CD wires those into the deploy step instead of baking
|
||||
them into the image.
|
||||
|
||||
If you've done 14–17, you have all the parts. This module is the assembly.
|
||||
@@ -34,7 +34,7 @@ By the end of this module you can:
|
||||
2. Extend your CI pipeline with build-and-publish stages that turn a merge into a versioned,
|
||||
deployable artifact.
|
||||
3. Wire a deploy step that takes that artifact, injects runtime config/secrets, and brings up the
|
||||
new version — provider-neutrally.
|
||||
new version, provider-neutrally.
|
||||
4. Add a health check and an automatic **rollback** so a bad deploy reverts itself instead of
|
||||
staying down.
|
||||
5. Reason about the deploy gate the way this audience already reasons about change windows: what's
|
||||
@@ -66,12 +66,12 @@ step.
|
||||
These two terms get used interchangeably and they are not the same thing. The difference is exactly
|
||||
one decision: **who pushes the button to prod.**
|
||||
|
||||
- **Continuous Delivery** — every merge to `main` automatically produces a **deployable artifact**
|
||||
- **Continuous Delivery:** every merge to `main` automatically produces a **deployable artifact**
|
||||
(a built, tagged, tested container image, sitting in a registry) and deploys it as far as a
|
||||
staging/pre-prod environment. Production deploy is **one click by a human**. The pipeline
|
||||
guarantees the artifact is *ready to ship at any moment*; a person decides *when*.
|
||||
|
||||
- **Continuous Deployment** — same pipeline, but there's **no button**. If it passes every gate, it
|
||||
- **Continuous Deployment:** same pipeline, but there's **no button**. If it passes every gate, it
|
||||
goes all the way to production automatically. Merge is the last human action.
|
||||
|
||||
```
|
||||
@@ -91,11 +91,11 @@ one decision: **who pushes the button to prod.**
|
||||
deploy to prod done
|
||||
```
|
||||
|
||||
Both are "CD." When someone says "we do CD," ask which one — the operational risk is completely
|
||||
Both are "CD." When someone says "we do CD," ask which one; the operational risk is completely
|
||||
different. Continuous deployment is not the more advanced/better option you graduate to; it's a
|
||||
different risk posture that's appropriate for some systems and reckless for others. A blog,
|
||||
internal dashboard, or stateless web service with good tests is a fine candidate. A billing engine,
|
||||
a database migration, or anything with a regulatory change-control requirement usually is not — and
|
||||
a database migration, or anything with a regulatory change-control requirement usually is not, and
|
||||
"a human clicks deploy" is a perfectly mature answer there, not a failure to automate.
|
||||
|
||||
The honest default for most teams adopting this: **start with continuous *delivery*.** Get the
|
||||
@@ -105,37 +105,37 @@ remove that button only once you trust the gates more than you trust the click.
|
||||
### The artifact is the unit of deploy
|
||||
|
||||
Here's the discipline that makes CD reliable, and it comes straight from Module 16: **you deploy a
|
||||
built image, not a Git ref.** "Deploy `main`" is ambiguous — it means "go to the prod box, pull,
|
||||
built image, not a Git ref.** "Deploy `main`" is ambiguous; it means "go to the prod box, pull,
|
||||
and rebuild," and that rebuild can pull a different base image or dependency version than CI tested.
|
||||
"Deploy `tasks-app:9f3a2c1`" is not ambiguous. It's the exact bytes CI built and tested.
|
||||
|
||||
So the build-and-publish stage does this once, centrally:
|
||||
|
||||
1. Build the image from the merged code.
|
||||
2. Tag it with something **immutable and traceable** — the Git commit SHA is the standard choice
|
||||
2. Tag it with something **immutable and traceable**: the Git commit SHA is the standard choice
|
||||
(`tasks-app:9f3a2c1`). Optionally also a moving tag like `:latest` or `:staging` for convenience,
|
||||
but the SHA tag is the one you trust.
|
||||
3. Push it to a container registry — the durable, shared home for images, the same way a Git remote
|
||||
3. Push it to a container registry, the durable home for images the same way a Git remote
|
||||
(Module 8) is the durable home for commits.
|
||||
|
||||
Every later deploy — to staging, to prod, a rollback — just says "run *this* tag." Build once, run
|
||||
Every later deploy (to staging, to prod, a rollback) just says "run *this* tag." Build once, run
|
||||
the identical artifact everywhere. That single property is what kills "works on my machine" at the
|
||||
deploy layer.
|
||||
|
||||
### The deploy step, provider-neutrally
|
||||
|
||||
The shape of a deploy is the same everywhere, whatever the target — a cloud platform, a Kubernetes
|
||||
cluster, a single VM, a PaaS:
|
||||
The shape of a deploy is the same everywhere, whatever the target (a cloud platform, a Kubernetes
|
||||
cluster, a single VM, a PaaS):
|
||||
|
||||
1. **Pull** the specific image tag onto the target.
|
||||
2. **Inject runtime config and secrets** (Module 17) — environment variables, mounted secret files,
|
||||
2. **Inject runtime config and secrets** (Module 17): environment variables, mounted secret files,
|
||||
a secrets-manager lookup. Never baked into the image; supplied at run time so the *same* image
|
||||
runs in staging and prod with different config.
|
||||
3. **Start the new version** alongside or in place of the old one.
|
||||
4. **Health-check** it before sending real traffic.
|
||||
5. **Cut over** if healthy; **roll back** if not.
|
||||
|
||||
This module is deliberately provider-agnostic on *where* — the same way Module 8 stayed neutral on
|
||||
This module is deliberately provider-agnostic on *where*, the same way Module 8 stayed neutral on
|
||||
hosts. The mechanics differ (a `kubectl` apply, a platform CLI, a `docker run`, a `compose up`), but
|
||||
the five steps don't. The lab does the simplest possible real version: a local container run. The
|
||||
logic is identical at scale.
|
||||
@@ -159,7 +159,7 @@ blue-green (run old and new side by side, flip a switch) and canary (send 5% of
|
||||
watch, ramp). They're all variations on "keep the old one ready until the new one proves itself."
|
||||
|
||||
> **Reframe for the ops reader:** you already know this instinct. It's the deployment equivalent of
|
||||
> a maintenance window with a back-out plan — except the back-out plan is automated, tested on every
|
||||
> a maintenance window with a back-out plan, except the back-out plan is automated, tested on every
|
||||
> single deploy, and takes seconds instead of a panicked hour. CD doesn't remove the discipline you
|
||||
> already have; it encodes it so it runs every time instead of only when someone remembers.
|
||||
|
||||
@@ -171,7 +171,7 @@ CI existed long before AI, and so did CD. What changed is the **rate**, and rate
|
||||
the merged-to-prod gate.
|
||||
|
||||
AI writes and ships changes dramatically faster. More PRs open, more merge, and they merge sooner.
|
||||
That's the upside — and it means the volume of code flowing toward production goes *up*, while the
|
||||
That's the upside, and it means the volume of code flowing toward production goes *up*, while the
|
||||
human attention available to babysit each deploy stays flat. The gap between "merged" and "in prod"
|
||||
stops being a quiet formality and becomes the place where that speed either pays off or hurts you.
|
||||
|
||||
@@ -189,7 +189,7 @@ Two consequences follow, and they pull in opposite directions:
|
||||
mistakes to production at full speed.
|
||||
|
||||
So the AI-era posture is specific: **strengthen the early gates, then automate the late ones.** The
|
||||
more you trust review + CI + scanning, the further right you can safely push automation — up to and
|
||||
more you trust review + CI + scanning, the further right you can safely push automation, up to and
|
||||
including no human on the prod button. The strength of the gates is the dial that decides whether
|
||||
continuous *deployment* is responsible or reckless for a given repo. And when an agent itself is the
|
||||
one merging (Unit 5), this stops being theoretical: the deploy gate is the last thing standing
|
||||
@@ -201,16 +201,16 @@ between an autonomous contributor and your users.
|
||||
|
||||
**Lab language:** shell, driving the container tooling from Module 16. You'll extend the `tasks-app`
|
||||
into a tiny running service, then build a deploy script that ships it locally with a health check and
|
||||
automatic rollback — the whole CD motion, simulated on your own machine.
|
||||
automatic rollback, the whole CD motion simulated on your own machine.
|
||||
|
||||
This lab simulates deployment with a **local container run** so it works on any machine with no cloud
|
||||
account. The five deploy steps are real; only the *target* is your laptop instead of a server.
|
||||
|
||||
**You'll need:**
|
||||
|
||||
- A container runtime from Module 16 — Docker or Podman. (Commands below use `docker`; if you run
|
||||
- A container runtime from Module 16: Docker or Podman. (Commands below use `docker`; if you run
|
||||
Podman, `alias docker=podman` or substitute.) As in Module 16, the engine must be **running**
|
||||
before you build or deploy — on macOS/Windows start Docker Desktop (or `podman machine start`);
|
||||
before you build or deploy. On macOS/Windows start Docker Desktop (or `podman machine start`);
|
||||
`docker --version` succeeds even when the engine is stopped, so confirm it's live with
|
||||
`docker info` first, or `deploy.sh`'s build step fails with "Cannot connect to the Docker daemon."
|
||||
- The `tasks-app` from Modules 1–2, now a Git repo.
|
||||
@@ -221,20 +221,20 @@ account. The five deploy steps are real; only the *target* is your laptop instea
|
||||
|
||||
Starter files are in this module's `lab/` folder:
|
||||
|
||||
- `serve.py` — turns the `tasks-app` into a minimal HTTP service with a `/health` endpoint, using
|
||||
- `serve.py`: turns the `tasks-app` into a minimal HTTP service with a `/health` endpoint, using
|
||||
only the Python standard library (no dependencies). This is the long-running thing CD deploys.
|
||||
- `Dockerfile` — the Module 16 container image, adjusted to run the service.
|
||||
- `deploy.sh` — the deploy step: build, tag, run, health-check, cut over or roll back.
|
||||
- `cd-starter.yml` — the CD pipeline stages, written as GitHub Actions and extending the Module 14
|
||||
- `Dockerfile`: the Module 16 container image, adjusted to run the service.
|
||||
- `deploy.sh`: the deploy step: build, tag, run, health-check, cut over or roll back.
|
||||
- `cd-starter.yml`: the CD pipeline stages, written as GitHub Actions and extending the Module 14
|
||||
CI file. GitLab/other-forge notes are in the comments.
|
||||
|
||||
### Part A — Make something worth deploying
|
||||
### Part A: Make something worth deploying
|
||||
|
||||
A CLI that exits immediately is awkward to "deploy." Give the app a long-running face.
|
||||
|
||||
1. Direct Claude Code to bring the starter files into your `tasks-app` folder next to `tasks.py` and
|
||||
`cli.py`: *"Copy `serve.py`, `Dockerfile`, and `deploy.sh` from this module's `lab/` into the
|
||||
tasks-app folder."* Then **read `serve.py` yourself** — it's ~40 lines wrapping the `TaskList` you
|
||||
tasks-app folder."* Then **read `serve.py` yourself**; it's ~40 lines wrapping the `TaskList` you
|
||||
already have in a stdlib HTTP server with two routes, `/health` and `/tasks`. Verify the three
|
||||
files landed next to `tasks.py`/`cli.py`.
|
||||
|
||||
@@ -252,11 +252,11 @@ A CLI that exits immediately is awkward to "deploy." Give the app a long-running
|
||||
```
|
||||
|
||||
Stop it with Ctrl-C. Now have Claude Code commit the new files: *"Stage and commit the HTTP
|
||||
service and Dockerfile with a clear message."* **Verify** the commit before moving on — read the
|
||||
service and Dockerfile with a clear message."* **Verify** the commit before moving on: read the
|
||||
diff it staged and confirm no secret, state file, or junk got swept in (it should be just
|
||||
`serve.py`, `Dockerfile`, and `deploy.sh`).
|
||||
|
||||
### Part B — Build and tag the artifact
|
||||
### Part B: Build and tag the artifact
|
||||
|
||||
3. Have Claude Code build the image and tag it with the current commit SHA, the immutable, traceable
|
||||
tag: *"Build the container image and tag it with the short commit SHA and also `:latest`."*
|
||||
@@ -268,7 +268,7 @@ A CLI that exits immediately is awkward to "deploy." Give the app a long-running
|
||||
|
||||
That `:<sha>` tag is the unit of deploy. Everything downstream refers to *this exact image*.
|
||||
|
||||
### Part C — Deploy it (with a net)
|
||||
### Part C: Deploy it (with a net)
|
||||
|
||||
4. **Read `lab/deploy.sh` yourself** before running it. It does the five steps: stops any running
|
||||
`tasks-app` container, starts the new image with runtime config injected as env vars (Module 17,
|
||||
@@ -287,7 +287,7 @@ A CLI that exits immediately is awkward to "deploy." Give the app a long-running
|
||||
rollback target. You now have continuous *delivery* in miniature: one command turns a commit into
|
||||
a running, version-tagged service.
|
||||
|
||||
### Part D — Break a deploy and watch it roll back
|
||||
### Part D: Break a deploy and watch it roll back
|
||||
|
||||
5. Now prove the net works. The service honors a `BREAK=1` env var that makes `/health` return
|
||||
`500`, a stand-in for "this build starts but is actually broken." First have the agent deploy a
|
||||
@@ -303,27 +303,27 @@ A CLI that exits immediately is awkward to "deploy." Give the app a long-running
|
||||
broken instance and brings the previous good one back up.** Confirm you're still serving:
|
||||
|
||||
```bash
|
||||
curl localhost:8000/health # ok — the bad deploy reverted itself
|
||||
curl localhost:8000/health # ok, the bad deploy reverted itself
|
||||
```
|
||||
|
||||
That automatic reversal, not the build and not the run, is the part that makes auto-deploy
|
||||
something you can sleep through.
|
||||
|
||||
### Part E — Wire it into the pipeline (read + reason)
|
||||
### Part E: Wire it into the pipeline (read + reason)
|
||||
|
||||
6. Open `lab/cd-starter.yml` and compare it to the Module 14 `ci-starter.yml`. It's the **same
|
||||
pipeline with stages appended**: the lint/test/scan gates run first (unchanged), and only `on:
|
||||
push` to `main` (a merge) do the build-publish-deploy stages run. Trace the `needs:`/dependency
|
||||
chain that makes deploy run *only after* the checks pass.
|
||||
|
||||
7. Find the one line that is the delivery-vs-deployment switch — the deploy-to-prod step gated behind
|
||||
7. Find the one line that is the delivery-vs-deployment switch: the deploy-to-prod step gated behind
|
||||
a manual approval (`environment:` with a required reviewer, commented in the file). Decide, for
|
||||
the `tasks-app`, which side you'd choose and why, and ask Claude Code to make the case for the
|
||||
*other* choice. The goal isn't a "right" answer; it's being able to articulate the risk posture
|
||||
either way.
|
||||
|
||||
> **A note on running the full pipeline:** actually executing `cd-starter.yml` end to end needs a
|
||||
> forge with a container registry and a deploy target wired up — that's environment-specific and
|
||||
> forge with a container registry and a deploy target wired up; that's environment-specific and
|
||||
> partly Module 19's territory (the runners and compute underneath). Parts A–D give you the deploy
|
||||
> *logic* runnable today on your own machine; the YAML shows how it slots into the automated
|
||||
> pipeline you already started in Module 14.
|
||||
@@ -332,7 +332,7 @@ A CLI that exits immediately is awkward to "deploy." Give the app a long-running
|
||||
|
||||
## Where it breaks
|
||||
|
||||
Be honest about the edges — this is where teams get burned.
|
||||
Be honest about the edges: this is where teams get burned.
|
||||
|
||||
- **The deploy is only as safe as the gates in front of it.** Continuous deployment with weak tests
|
||||
and no review isn't "moving fast," it's an automated mistake-shipping machine. If you haven't done
|
||||
@@ -341,17 +341,17 @@ Be honest about the edges — this is where teams get burned.
|
||||
- **Health checks lie.** A `200` from `/health` means "the process started," not "the feature
|
||||
works." A shallow health check passes while the app returns garbage to users. Make the check
|
||||
meaningful (does it reach its database? can it serve a real request?) and lean on canary/gradual
|
||||
rollout for anything important — but know that no health check replaces real tests and real
|
||||
rollout for anything important, but know that no health check replaces real tests and real
|
||||
monitoring.
|
||||
- **Rollback isn't free, and some things don't roll back.** Reverting the *running image* is cheap.
|
||||
Reverting a **database migration**, a sent email, a charged credit card, or a published message is
|
||||
not — those are forward-only. The cleaner the separation between code deploys and irreversible
|
||||
not. Those are forward-only. The cleaner the separation between code deploys and irreversible
|
||||
state changes, the more rollback actually saves you. Don't assume "we can always roll back" covers
|
||||
data.
|
||||
- **This lab simulates the target.** A local `docker run` is the deploy logic, not the deploy
|
||||
reality. Real targets add networking, DNS cutover, load balancers, zero-downtime orchestration,
|
||||
and multiple instances. The five steps hold; the operational surface around them is larger. The
|
||||
*compute* that runs all of this — and why you might run your own — is Module 19.
|
||||
*compute* that runs all of this (and why you might run your own) is Module 19.
|
||||
- **"Build once" only holds if you actually do.** The instant someone rebuilds on the prod box "just
|
||||
to be sure," you've lost the guarantee that prod runs what CI tested. Deploy the artifact CI built.
|
||||
No rebuilds downstream.
|
||||
@@ -363,7 +363,7 @@ Be honest about the edges — this is where teams get burned.
|
||||
**You're done when:**
|
||||
|
||||
- You can state the difference between continuous delivery and continuous deployment in one sentence
|
||||
— *who clicks the prod button* — and say which one `tasks-app` should use and why.
|
||||
(*who clicks the prod button*) and say which one `tasks-app` should use and why.
|
||||
- `./deploy.sh` builds, tags by commit SHA, runs the container, and reports a healthy deploy you can
|
||||
`curl`.
|
||||
- You have **watched a bad deploy roll itself back** to the previous good version, and the service
|
||||
@@ -373,7 +373,7 @@ Be honest about the edges — this is where teams get burned.
|
||||
|
||||
When a deploy is one command, a bad one reverts itself, and you can argue the delivery-vs-deployment
|
||||
call for a given repo, you've closed the merged-to-running gap. Module 19 goes underneath all of
|
||||
this — the runners and compute actually executing your CI/CD, and why you'd own them.
|
||||
this: the runners and compute actually executing your CI/CD, and why you'd own them.
|
||||
|
||||
---
|
||||
|
||||
@@ -382,12 +382,12 @@ this — the runners and compute actually executing your CI/CD, and why you'd ow
|
||||
This is expansion-zone material (Module 15+); some specifics drift. Re-check at build/publish time:
|
||||
|
||||
- [ ] **Action/runner versions** in `cd-starter.yml` (`actions/checkout`, `actions/setup-python`,
|
||||
any build/login/push actions) — pin to current major versions and confirm they still exist.
|
||||
- [ ] **Registry login + push syntax** — the standard build-and-push action names and auth flow
|
||||
any build/login/push actions); pin to current major versions and confirm they still exist.
|
||||
- [ ] **Registry login + push syntax:** the standard build-and-push action names and auth flow
|
||||
change; verify against current forge docs rather than the comments here.
|
||||
- [ ] **Manual-approval mechanism** — the way a forge gates a job behind human approval
|
||||
- [ ] **Manual-approval mechanism:** the way a forge gates a job behind human approval
|
||||
(GitHub `environment` protection rules, GitLab `when: manual`, others) shifts in naming/UI.
|
||||
Confirm the delivery-vs-deployment switch still maps to the current feature.
|
||||
- [ ] **Container runtime commands** — confirm `docker`/`podman` flags used in `deploy.sh`
|
||||
- [ ] **Container runtime commands:** confirm `docker`/`podman` flags used in `deploy.sh`
|
||||
(`run`, `--health-*`, `inspect`) match current CLI behavior.
|
||||
- [ ] **Cross-references** to Modules 16, 17, and 19 still match those modules' final content.
|
||||
|
||||
Reference in New Issue
Block a user