AWS実践 #1 インフラの骨格 — FastAPI/Django を ECS Fargate にデプロイ
基礎 7 編 でアカウント / リージョン / IAM / コスト / CLI / セキュリティ / ログを、中級 7 編 で EC2 / VPC / S3 / RDS / DNS / ALB / CloudFront を、上級 7 編 で ECS / Lambda / メッセージング / Secrets / Step Functions を、それぞれ一つずつ押さえてきました。21 編の道具箱が集まったところで — いよいよ 本物のバックエンドを 1 つのプロジェクトとして 乗せます。
このシリーズが扱うのは、FastAPI 実戦 または Django DRF シリーズで作った ブログ API (Post + Comment + User) をドメインに据え、6 編をかけて運用可能な姿まで引き上げる流れです。
全体像 #
今回作るインフラ:
Internet
│
▼
┌──────────────┐
│ Route 53 │ blog.example.com
└──────┬───────┘
│
▼
┌────────────────┐
│ ALB │ :443 → :8000
│ (HTTPS, ACM) │
└────────┬───────┘
│
┌──────────┴──────────┐
▼ ▼
┌───────────┐ ┌───────────┐
│ AZ-a │ │ AZ-c │
│ Fargate │ │ Fargate │
│ Task #1 │ │ Task #2 │
│ (Blog) │ │ (Blog) │
└─────┬─────┘ └─────┬─────┘
│ │
└──────────┬──────────┘
▼
┌──────────────┐
│ RDS Postgres│ (Multi-AZ, Private)
└──────────────┘構成要素ごとに整理すると:
| 構成要素 | 役割 | 出どころ |
|---|---|---|
| Route 53 | ドメイン → ALB | 中級 #5 |
| ALB | TLS 終端、ルーティング、ヘルスチェック | 中級 #6 |
| ACM | TLS 証明書の発行 / 更新 | 中級 #6 |
| ECR | イメージの保管 | 上級 #2 |
| ECS Fargate | コンテナの実行 (サーバーレス) | 上級 #1 |
| RDS | DB | 中級 #4、#2 |
| VPC + Subnet | ネットワーク分離 | 中級 #1 |
| Secrets Manager | DB パスワード | 上級 #6、#2 |
今回は DB を除くすべて を一気にセットアップします。RDS は #2 で別途。
ドメイン — ブログ API コンテナの一行サマリ #
このシリーズが想定するコンテナは FastAPI 実戦 #6 または DRF #6 の成果物 — 次のような形です。
FROM python:3.14-slim AS base
WORKDIR /app
ENV PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 curl && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY app /app/app
EXPOSE 8000
HEALTHCHECK --interval=10s --timeout=3s --retries=3 \
CMD curl -fsS http://127.0.0.1:8000/health || exit 1
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]中核となる約束は 3 つ:
- ポート 8000 で listen する
/healthが 200 を返す (DB 依存のない軽いチェック)/readyが DB 接続 OK なら 200、ダメなら 503 — ALB / ECS がトラフィックルーティングを判断する材料
Django なら gunicorn -w 4 myproject.wsgi で同じ約束を満たせます。
1) VPC とサブネット — ネットワークの骨格 #
ECS / RDS / ALB はすべて VPC の中 に住みます。VPC がなければ 1 行も走らせられません。幸い新しいアカウントには default VPC がリージョンごとに用意されているので、立ち上げを急ぐときはそれを使っても OK。本番は自前 VPC が推奨。
推奨レイアウト #
Public Subnet (10.0.0.0/24, AZ-a) ← ALB, NAT GW
Public Subnet (10.0.1.0/24, AZ-c) ← ALB, NAT GW
Private Subnet (10.0.10.0/24, AZ-a) ← Fargate Task
Private Subnet (10.0.11.0/24, AZ-c) ← Fargate Task
DB Subnet (10.0.20.0/24, AZ-a) ← RDS
DB Subnet (10.0.21.0/24, AZ-c) ← RDS3 つのサブネット:
| Subnet | トラフィック方向 | 誰が住むか |
|---|---|---|
| Public | インターネット ↔ | ALB, NAT Gateway |
| Private | インターネット X (NAT 経由で outbound のみ) | Fargate, EC2 |
| DB | インターネット X、Fargate のみアクセス | RDS |
この記事では default VPC の public subnet だけ を使って素早く立ち上げます (Fargate task に public IP を付与)。本番の形は #4 Terraform でコード化。
Security Group 2 つ #
sg-alb 80, 443 ← 0.0.0.0/0
(インターネットが ALB へ)
sg-fargate 8000 ← sg-alb
(ALB のみ Fargate へ)重要なパターン: SG は 別の SG を source として 受け取れます。IP レンジではなく「この SG が付いているリソースだけ」という意味です。ALB の IP が変わっても規則が自動的に追従してくれます。
2) ECR にイメージを上げる #
上級 #2 ECR ですでに扱いましたが、復習がてら手早く。
aws ecr create-repository \
--repository-name blog-api \
--image-scanning-configuration scanOnPush=true \
--region ap-northeast-2scanOnPush=true — イメージ push 時に自動で脆弱性スキャン (#5 モニタリング で結果を確認)。
ビルドと push #
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=ap-northeast-2
REPO=$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/blog-api
# 1) ログイン
aws ecr get-login-password --region $REGION | \
docker login --username AWS --password-stdin $REPO
# 2) ビルド (linux/amd64 — Fargate の標準アーキ)
docker build --platform=linux/amd64 -t blog-api:v1 .
# 3) タグ
docker tag blog-api:v1 $REPO:v1
docker tag blog-api:v1 $REPO:latest
# 4) push
docker push $REPO:v1
docker push $REPO:latestApple Silicon (M1/M2/M3) の Mac でそのまま
docker buildすると arm64 イメージができてしまい、Fargate (x86_64 標準) では起動しません。--platform=linux/amd64を必ず明示。Fargate は ARM もサポートしますが別途設定が必要。
イメージタグ戦略 #
| タグ | 意味 |
|---|---|
latest | 最新 — 本番では 使わない (ロールバック不能) |
v1, v2, ... | 人が読めるバージョン |
<git-sha> | 追跡用 — CI が自動発行 (#3) |
<git-sha>-prod | 環境別エイリアス |
latest は開発者の便宜のため。本番の Task Definition は常に git SHA か semver で固定 します — そうすれば「いまどのコードが動いているか」が一切の疑問なく確認できます。
3) Task Definition — コンテナの「住民票」 #
ECS で最も重要な部分。イメージ + CPU/メモリ + 環境変数 + ポート + ログ設定 が 1 つの JSON にまとまります。
{
"family": "blog-api",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::123456789012:role/blog-api-task-role",
"containerDefinitions": [
{
"name": "api",
"image": "123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/blog-api:v1",
"portMappings": [
{ "containerPort": 8000, "protocol": "tcp" }
],
"essential": true,
"environment": [
{ "name": "ENVIRONMENT", "value": "production" },
{ "name": "LOG_LEVEL", "value": "info" }
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/blog-api",
"awslogs-region": "ap-northeast-2",
"awslogs-stream-prefix": "api",
"awslogs-create-group": "true"
}
},
"healthCheck": {
"command": ["CMD-SHELL", "curl -fsS http://127.0.0.1:8000/health || exit 1"],
"interval": 10,
"timeout": 3,
"retries": 3,
"startPeriod": 30
}
}
]
}中核となる項目:
| キー | 意味 |
|---|---|
cpu / memory | Fargate は決まった組み合わせのみ許可 (例: 256/512、512/1024、1024/2048) |
executionRoleArn | ECS agent が ECR pull / Logs / Secrets アクセスに使うロール |
taskRoleArn | コンテナ内のコード が使う IAM ロール — boto3 がこれで sign |
awslogs | ログが自動で CloudWatch へ (#5) |
healthCheck | コンテナ自身のヘルスチェック (Dockerfile のものとは別) |
IAM ロール 2 つの違いがよく混乱される #
executionRoleArn | taskRoleArn | |
|---|---|---|
| 誰が使うか | ECS agent (起動段階) | コンテナ内のコード (実行中) |
| 権限 | ECR pull、CloudWatch 書き込み、Secrets 読み | S3 アクセス、RDS、SQS など — アプリのロジック |
executionRoleArn 抜け → イメージ pull 失敗。taskRoleArn 抜け → boto3 が NoCredentialsError。
登録 #
aws ecs register-task-definition \
--cli-input-json file://task-definition.json \
--region ap-northeast-2登録のたびに revision 番号 (blog-api:1、blog-api:2、…) が上がります。ロールバックは前の revision 番号を指定して #3 で。
4) ALB + Target Group — トラフィックを受ける入り口 #
中級 #6 で作った ALB パターンそのまま。要点は:
ALB:443 (HTTPS, ACM 証明書)
│
▼
Listener: 443 → forward → tg-blog-api
│
▼
Target Group: tg-blog-api
- Protocol: HTTP / 8000
- Target type: ip ← Fargate は必ず ip
- Health check: GET /health
- Healthy threshold: 2
- Interval: 15sTarget type は必ず ip — Fargate task は毎回 IP が変わるので instance モードは効きません。
aws elbv2 create-target-group \
--name tg-blog-api \
--protocol HTTP --port 8000 \
--vpc-id $VPC_ID \
--target-type ip \
--health-check-path /health \
--healthy-threshold-count 2 \
--health-check-interval-seconds 15ALB Listener ルールは 中級 #6 を参照。HTTPS 443 → forward → tg-blog-api、HTTP 80 → 443 リダイレクト。
5) ECS Service — コンテナの「会社」 #
Task Definition が従業員の職務記述書なら、Service は会社 です — 常に desired count 分の task を維持し、タスクがダウンすれば自動で立て直し、デプロイ時には少しずつ入れ替えます。
aws ecs create-cluster --cluster-name blog-clusteraws ecs create-service \
--cluster blog-cluster \
--service-name blog-api \
--task-definition blog-api:1 \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={
subnets=[subnet-aaa, subnet-bbb],
securityGroups=[sg-fargate],
assignPublicIp=ENABLED
}" \
--load-balancers "targetGroupArn=$TG_ARN,containerName=api,containerPort=8000" \
--health-check-grace-period-seconds 60 \
--deployment-configuration "deploymentCircuitBreaker={enable=true,rollback=true},maximumPercent=200,minimumHealthyPercent=100"中核オプションの整理:
| オプション | 意味 |
|---|---|
desired-count 2 | 最低 2 個 — Multi-AZ 配置で AZ 1 つの障害に耐える |
assignPublicIp=ENABLED | private subnet + NAT がないとき (簡易セット)。本番は NAT 推奨 |
health-check-grace-period | Service が task を起動した直後 ALB ヘルスチェックの待ち時間 (アプリのブート時間) |
deploymentCircuitBreaker | 新デプロイが N 回連続で失敗したら自動ロールバック (#3 で詳細) |
maximumPercent=200 | デプロイ中の最大 task 数 (200% = 旧 + 新が同時に存在) |
minimumHealthyPercent=100 | デプロイ中の最低 healthy 比率 (100% = ダウンタイム 0) |
この 2 つの % が ローリングアップデート の形を決めます。
オートスケーリング #
Service を立てただけでは自動スケールは入りません。別途:
aws application-autoscaling register-scalable-target \
--service-namespace ecs \
--resource-id service/blog-cluster/blog-api \
--scalable-dimension ecs:service:DesiredCount \
--min-capacity 2 --max-capacity 10aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--resource-id service/blog-cluster/blog-api \
--scalable-dimension ecs:service:DesiredCount \
--policy-name cpu-target \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 60.0,
"PredefinedMetricSpecification": {"PredefinedMetricType": "ECSServiceAverageCPUUtilization"},
"ScaleOutCooldown": 30,
"ScaleInCooldown": 120
}'平均 CPU 60% を基準に自動 scale out / in。本番では最初は控えめ (40~60%) にして、トラフィックパターンを見ながら調整。
6) 初回デプロイの検証 #
Service が安定状態に入るまで待つ #
aws ecs wait services-stable \
--cluster blog-cluster \
--services blog-apiヘルスチェックを直接確認 #
ALB_DNS=$(aws elbv2 describe-load-balancers \
--names blog-alb \
--query 'LoadBalancers[0].DNSName' --output text)
curl -i https://$ALB_DNS/health
# HTTP/2 200
# {"status": "ok"}ログの確認 #
aws logs tail /ecs/blog-api --follow --since 5mリクエストを 1 本投げてログに access log が出てくれば、このシリーズの最初の到達点 に立ったことになります 🎉。
落とし穴 — 初回デプロイで上がらない 5 つの原因 #
1) STOPPED 状態で永遠に再起動
#
ECS コンソールの Tasks タブで STOPPED 行をクリック → “Stopped reason” を確認。よくある原因:
| メッセージ | 原因 |
|---|---|
CannotPullContainerError | ECR 権限抜け → executionRole |
ResourceInitializationError: ... secret manager | Secrets ARN のタイポ / 権限 |
Essential container ... exited | コンテナ自体が死んでいる → CloudWatch logs |
Task failed ELB health checks | ALB が healthy 判定できない → 次項目 |
2) ALB ヘルスチェック失敗 #
最頻出。点検ポイント:
- コンテナポート (
8000) と Target Group ポート (8000) が一致しているか /healthエンドポイントが本当に 200 を返しているか (DB 依存 X)health-check-grace-periodがアプリのブート時間 (FastAPI 5s、Django 20~40s) より長いか- Fargate Security Group inbound が ALB SG だけを許可しているか
- ALB がその task のサブネットまでルーティングできるか (同一 VPC)
3) awsvpc networkMode の ENI 上限
#
Fargate task は ENI (Elastic Network Interface) を 1 個ずつ消費します。AZ / サブネットの IP が枯れると新しい task を立てられません。CIDR を狭くしすぎないこと (上の例 /24 = 256 IP)。
4) Public IP なしで ECR pull 失敗 #
Private subnet に task を立てているのに NAT Gateway も VPC Endpoint もないと、ECR / Secrets Manager / CloudWatch へのトラフィックが詰まって起動が失敗します。
解決 3 通り:
- NAT Gateway を追加 (時間 ~$0.045 + データ転送)
- ECR / Logs / Secrets の Interface VPC Endpoint を追加 (NAT より安い)
- Public subnet +
assignPublicIp=ENABLED(学習用)
5) デプロイ停滞 — 新 task が healthy にならない #
deploymentCircuitBreaker が有効なら N 分後に自動ロールバック。無効なら service が永遠に IN_PROGRESS。aws ecs describe-services で deployments 配列を確認。
まとめ #
今回押さえたこと:
- 全体像 — Route 53 → ALB → Fargate (× 2 AZ) → RDS、Multi-AZ 運用の標準形
- VPC 骨格 — public / private / db subnet の役割、SG 2 つで ALB ↔ Fargate
- ECR — イメージビルド時に
--platform=linux/amd64、タグは git SHA または semver、latestは本番禁止 - Task Definition — Fargate の CPU/メモリ組み合わせ、executionRole vs taskRole の分離、awslogs で自動ロギング
- ALB Target Group — Fargate は
target-type ip、ヘルスチェックは/health - ECS Service — desired count、deployment circuit breaker、maximum/minimum % がローリングの形を決める
- Auto Scaling —
application-autoscalingで CPU / リクエスト数ベースの target tracking - 検証 —
services-stablewait、ALB DNS curl、CloudWatch Logs tail - 落とし穴 — STOPPED 原因の分析 / ALB ヘルスチェック失敗 5 か所 / ENI IP 不足 / NAT/Endpoint 抜け / デプロイ停滞
次回 — RDS #
ALB の後ろにトラフィックは入り始めましたが、API はまだ DB がなく メモリの中だけで生きています。
#2 RDS 連携とマイグレーションの運用 では VPC 内に RDS Postgres Multi-AZ を立て、Secrets Manager でパスワードを注入し、Alembic / Django migrations の運用上のポイント — そして本番トラフィックを 殺さない blue/green マイグレーションパターンまで整理します。