正在学习
8.5 Example: Automating a Full Deployment Workflow
Stage 2: runtime
FROM python:3.11-alpine
RUN addgroup -S app && adduser -S app -G app
USER app
WORKDIR /app
COPY --from=builder /install /usr/local
COPY app.py .
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
Makefile
```makefile
APP=taskflow
REGISTRY?=ghcr.io/your-org
IMAGE=(APP)
TAG?=$(shell git rev-parse --short HEAD 2>/dev/null || echo dev)
.PHONY: help
help:
@echo "Targets: venv, test, build, run, push, deploy, rollback, smoke"
venv:
python -m venv .venv && . .venv/bin/activate && pip install -r requirements.txt pytest
test: venv
. .venv/bin/activate && pytest -q || (echo "Tests failed"; exit 1)
build:
docker build -t (TAG) -t $(IMAGE):latest .
run:
docker run --rm -p 8000:8000 --name (IMAGE):$(TAG)
push:
docker push (TAG) && docker push $(IMAGE):latest
deploy:
./scripts/deploy.sh "(TAG)"
rollback:
./scripts/rollback.sh $(APP)
smoke:
./scripts/smoke.sh
tests/test_app.py
from fastapi.testclient import TestClient
from app import app
client = TestClient(app)
def test_health():
r = client.get("/health")
assert r.status_code == 200
assert r.json()["status"] == "ok"
def test_create_and_get_task():
r = client.post("/tasks", json={"title": "Ship"})
assert r.status_code == 201
tid = r.json()["id"]
r2 = client.get(f"/tasks/{tid}")
assert r2.status_code == 200
assert r2.json()["title"] == "Ship"
scripts/deploy.sh
#!/usr/bin/env bash
deploy.sh — idempotent remote-or-local deploy with health check
Usage: ./scripts/deploy.sh
set -euo pipefail
IMAGE="${1:-}"
APP_NAME="taskflow"
PORT="${PORT:-8000}"
if [[ -z "$IMAGE" ]]; then
echo "Usage: $0
exit 1
fi
echo "[deploy] pulling $IMAGE"
docker pull "$IMAGE" >/dev/null 2>&1 || true
echo "[deploy] stopping old container (if any)"
docker rm -f "$APP_NAME" >/dev/null 2>&1 || true
echo "[deploy] starting new container"
docker run -d --name "IMAGE"
echo "[deploy] health check"
for i in {1..20}; do
if curl -fsS "http://127.0.0.1:${PORT}/health" >/dev/null; then
echo "[deploy] healthy"
exit 0
fi
sleep 1
done
echo "[deploy] health check failed"
docker logs "$APP_NAME" || true
exit 1
scripts/rollback.sh
#!/usr/bin/env bash
rollback.sh — revert to previous local image tag
Usage: ./scripts/rollback.sh
set -euo pipefail
APP="${1:-taskflow}"
PREV=$(docker images --format '{{.Repository}}:{{.Tag}} {{.CreatedAt}}' \
| grep "$APP" \
| sort -rk2 \
| awk 'NR==2{print $1}')
if [[ -z "$PREV" ]]; then
echo "[rollback] no previous image found"
exit 1
fi
echo "[rollback] switching to $PREV"
docker rm -f "$APP" >/dev/null 2>&1 || true
docker run -d --name "PREV"
echo "[rollback] done"
scripts/smoke.sh
#!/usr/bin/env bash
smoke.sh — tiny post-deploy checks
set -euo pipefail
URL="${URL:-http://127.0.0.1:8000}"
echo "[smoke] GET ${URL}/health"
curl -fsS "${URL}/health" | jq .
echo "[smoke] creating a task"
curl -fsS -X POST "${URL}/tasks" -H "Content-Type: application/json" -d '{"title":"Check"}' | jq .
echo "[smoke] list tasks"
curl -fsS "${URL}/tasks" | jq .
.github/workflows/ci.yml
name: TaskFlow Full Workflow
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
workflow_dispatch: {}
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}/taskflow
jobs:
test:
runs-on: ubuntu-latest
steps:
uses: actions/checkout@v4
uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: |
python -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt pytest
pytest -q
build_and_push:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
uses: actions/checkout@v4
name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Short SHA
id: vars
run: echo "SHORT=GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
{{ env.IMAGE_NAME }}:${{ steps.vars.outputs.SHORT }}
{{ env.IMAGE_NAME }}:latest
deploy:
needs: build_and_push
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy over SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_KEY }}
script: |
set -e
cd ${{ secrets.DEPLOY_PATH }}
./scripts/deploy.sh ghcr.io/{GITHUB_SHA::7}
sleep 3
./scripts/smoke.sh
rollback_on_failure:
needs: deploy
if: failure()
runs-on: ubuntu-latest
steps:
- name: Roll back over SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_KEY }}
script: |
set -e
cd ${{ secrets.DEPLOY_PATH }}
./scripts/rollback.sh taskflow
Local usage
make venv
make test
make build TAG=$(git rev-parse --short HEAD || echo dev)
make run
In a second terminal:
make smoke
CI usage
- Add repository secrets: DEPLOY_HOST, DEPLOY_USER, DEPLOY_KEY, DEPLOY_PATH.
- Push to main. The workflow builds, pushes, deploys, smokes, and rolls back automatically on failure.
Clarification Table
| Stage | Command or File | What It Ensures | Failure Behavior |
|---|---|---|---|
| Test | make test, tests/test_app.py | Contract correctness at HTTP layer | Pipeline stops; Claude can tighten assertions |
| Package | Dockerfile | Reproducible runtime, non-root user | Build fails; logs point at missing deps |
| Tag & Push | make push or CI build_and_push | Deterministic image tags (SHA, latest) | No deploy if push fails |
| Deploy | scripts/deploy.sh | Idempotent container restart and health check | Prints logs and exits non-zero |
| Smoke | scripts/smoke.sh | Post-deploy verification of core endpoints | Triggers rollback job in CI |
| Rollback | scripts/rollback.sh | One-command reversion to previous image | Restores service quickly |
You now have a compact, end-to-end deployment workflow that is test-gated, containerized, tag-driven, health-checked, and reversible. Claude Code’s value is in keeping this system coherent: it can adjust the Dockerfile for smaller images, tune Makefile targets, harden health checks, or explain CI failures with specific remediation steps. With this foundation, you can scale the pattern to multiple services, environments, and teams while preserving the same clarity and operational safety.
Chapter 9 – Project 3: Building a Claude Code Assistant for Teams
练习题
In the Dockerfile runtime stage, what is the purpose of the USER app instruction?
Which command in the Makefile is responsible for running the FastAPI application in a Docker container?
What are the purposes of the deploy.sh script? (Select all that apply)
The rollback.sh script can revert to any previous local image tag available on the system.
In the smoke.sh script, the default URL used for making requests is ___.
Explain the purpose of the EXPOSE 8000 instruction in the Dockerfile.
What is the role of the CMD instruction in the Dockerfile?
Which of the following are valid targets in the Makefile? (Select all that apply)
The test_app.py file contains test cases that verify the health check endpoint and the ability to create and retrieve tasks.
What is the significance of the set -euo pipefail command at the beginning of the deploy.sh, rollback.sh, and smoke.sh scripts?
What is the purpose of the EXPOSE 8000 instruction in the Dockerfile runtime stage?
Which of the following are required for a successful deployment according to the TaskFlow CI workflow? (Select all that apply)
test job must pass before proceeding to build_and_push.deploy job runs regardless of the branch.deploy job only runs if the branch is 'main'.rollback_on_failure job runs if the deploy job fails.build_and_push job requires manual triggering.The rollback.sh script can revert to any previous image tag found in the local Docker image list.
The CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"] instruction in the Dockerfile specifies the ___ to run when the container starts.
What is the purpose of the health check loop in the deploy.sh script?
登录后解锁笔记、知识点解析、AI 问答
立即登录