正在学习

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

  1. Add repository secrets: DEPLOY_HOST, DEPLOY_USER, DEPLOY_KEY, DEPLOY_PATH.
  2. 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?

A. To create a new user named 'app'
B. To set the user context for subsequent instructions to 'app'
C. To add the user 'app' to the sudoers group
D. To specify the default user for running the application

Which command in the Makefile is responsible for running the FastAPI application in a Docker container?

A. make venv
B. make test
C. make run
D. make deploy

What are the purposes of the deploy.sh script? (Select all that apply)

A. Pulling the Docker image
B. Stopping the old container if it exists
C. Starting a new container with the latest image
D. Running health checks on the deployed application
E. Building the Docker image

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?

A. To specify the default command to run when starting the container
B. To copy files into the container
C. To set environment variables
D. To expose ports

Which of the following are valid targets in the Makefile? (Select all that apply)

A. venv
B. test
C. build
D. deploy
E. push
F. smoke

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?

A. It makes the container listen on port 8000 at runtime.
B. It maps port 8000 from the host to the container.
C. It tells Docker that the container listens on port 8000 at runtime, but does not actually publish the port.
D. It installs a dependency on port 8000.

Which of the following are required for a successful deployment according to the TaskFlow CI workflow? (Select all that apply)

A. The test job must pass before proceeding to build_and_push.
B. The deploy job runs regardless of the branch.
C. The deploy job only runs if the branch is 'main'.
D. The rollback_on_failure job runs if the deploy job fails.
E. The 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 问答

立即登录