ECS × FastAPI でAPIサーバーを構築する - コンテナ化からALB公開まで
はじめに
この記事では、FastAPIをDockerコンテナ化し、Amazon ECS(Fargate)にデプロイしてALB経由で公開するまでの手順をまとめます。
「AWSコンソールで何となく動かしたことはある」という段階から、なぜこの構成にするのかという設計意図も含めて説明することを意識しています。
この記事で作るもの
インターネット
↓
ALB(パブリックサブネット)
↓
ECS Fargate タスク(プライベートサブネット)
↓
FastAPI コンテナ(ポート8080)
前提条件
- AWS CLIが設定済みであること
- Dockerがローカルで動作していること
- VPCとサブネット(パブリック・プライベート)が作成済みであること
VPC構成については以下の記事を参照してください。
使用技術
- FastAPI(Python)
- uv(パッケージマネージャー)
- Amazon ECR
- Amazon ECS(Fargate)
- Application Load Balancer(ALB)
アプリケーションの構成
今回使用するFastAPIアプリケーションの構成は以下のとおりです。
.
├── Dockerfile
├── .dockerignore
├── main.py
├── pyproject.toml
└── uv.lock
main.py はシンプルなエンドポイントを2つ持つ構成です。
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello from app!"}
@app.get("/hello/{name}")
async def hello(name: str):
return {"message": f"Hello {name}!"}
Dockerfile
FROM python:3.12-slim
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# uvのインストール
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
# 非rootユーザーの作成
RUN groupadd -g 1000 pythonista && \
useradd -u 1000 -g pythonista -m pythonista
# アプリケーションのコピーと依存関係のインストール
COPY . /app
WORKDIR /app
RUN uv sync --frozen --no-cache
# 非rootユーザーに切り替え
USER pythonista
# アプリケーションの起動(ポート8080)
CMD ["/app/.venv/bin/fastapi", "run", "./main.py", "--port", "8080", "--host", "0.0.0.0"]
ポイント:ポートは8080を使う
非rootユーザーでコンテナを実行する場合、ポート80(1024以下の特権ポート)はバインドできません。本記事では8080を使用します。詳細は後述の「詰まりポイント」で解説します。
手順
1. ECRリポジトリの作成
AWSコンソールでECRを開き、リポジトリを作成します。
- リポジトリ名:
fastapi-container - その他:デフォルトでOK
作成するとリポジトリURIが発行されます。
<AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/fastapi-container
2. DockerイメージのビルドとECRへのプッシュ
ターミナルで以下を順番に実行します。
# ECRへのログイン
aws ecr get-login-password --region ap-northeast-1 | \
docker login --username AWS --password-stdin \
<AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com
# イメージのビルド
docker build -t fastapi-container .
# タグの付与
docker tag fastapi-container:latest \
<AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/fastapi-container:latest
# ECRへのプッシュ
docker push \
<AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/fastapi-container:latest
ECRコンソールでイメージが表示されれば成功です。
3. ECSクラスターの作成
ECSコンソールから「クラスターの作成」を選択します。
- クラスター名:
FastAPICluster - インフラストラクチャ:AWS Fargate
- その他:デフォルトでOK
なぜFargateか
EC2タイプと比較したとき、Fargateはサーバーの管理が不要です。OSのパッチ適用やスケーリングの管理をAWSに委ねられるため、今回のようにアプリケーションの動作確認を目的とした構成では運用負荷を最小化できます。
4. タスク定義の作成
ECSコンソールの「タスク定義」から「新しいタスク定義の作成」を選択します。
基本設定
| 項目 | 値 |
|---|---|
| タスク定義ファミリー名 | fastapi-task |
| インフラストラクチャ | AWS Fargate |
| OS | Linux/X86_64 |
| CPU | 0.25 vCPU |
| メモリ | 0.5 GB |
コンテナの設定
| 項目 | 値 |
|---|---|
| コンテナ名 | fastapi-container |
| イメージURI | <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/fastapi-container:latest |
| コンテナポート | 8080 |
| プロトコル | TCP |
5. ALBとターゲットグループの作成
EC2コンソールの「ロードバランサー」から「Application Load Balancer」を作成します。
ALBの設定
| 項目 | 値 |
|---|---|
| 名前 | fastapi-alb |
| スキーム | インターネット向け |
| VPC | 使用するVPC |
| アベイラビリティゾーン | 2つ以上選択(マルチAZ) |
ALBのセキュリティグループ
新しいセキュリティグループを作成します。
- インバウンドルール:HTTP(80)、ソース:0.0.0.0/0
ターゲットグループの設定
ALB作成時に新しいターゲットグループを作成します。
| 項目 | 値 |
|---|---|
| 名前 | fastapi-tg |
| ターゲットタイプ | IP(Fargateの場合はIPを選択) |
| プロトコル | HTTP |
| ポート | 8080 |
| ヘルスチェックパス | / |
ターゲットタイプをIPにする理由
FargateはEC2インスタンスを持たないため、ターゲットタイプを「インスタンス」にするとターゲットを登録できません。Fargateでは必ず「IP」を選択します。
ターゲットの登録はスキップしてOKです。ECSサービス作成時に自動で登録されます。
6. ECSサービスの作成
ECSコンソールでFastAPIClusterを開き、「サービス」タブから「作成」を選択します。
基本設定
| 項目 | 値 |
|---|---|
| コンピューティングオプション | 起動タイプ |
| 起動タイプ | FARGATE |
| タスク定義 |
fastapi-task(最新リビジョン) |
| サービス名 | fastapi-service-cluster |
| タスクの数 | 1 |
ネットワーク設定
| 項目 | 値 |
|---|---|
| VPC | ALBと同じVPC |
| サブネット | プライベートサブネットを2つ選択 |
| パブリックIP | オフ |
セキュリティグループは新しく作成します。
- インバウンドルール:カスタムTCP、ポート8080、ソース:ALBのセキュリティグループ
ALBのセキュリティグループをソースにする理由
IPアドレス範囲ではなくセキュリティグループを参照することで、ALBを経由したリクエストのみを許可できます。ECSタスクはインターネットから直接アクセス不可になり、セキュリティが向上します。
ロードバランシング
| 項目 | 値 |
|---|---|
| ロードバランサーの種類 | Application Load Balancer |
| ロードバランサー | fastapi-alb |
| コンテナ | fastapi-container 8080:8080 |
| ターゲットグループ | fastapi-tg |
7. 動作確認
ECSコンソールでタスクのステータスがRUNNINGになったら、ALBのDNS名でアクセスします。
http://<ALBのDNS名>
以下のレスポンスが返れば成功です。
{ "message": "Hello from app!" }
詰まりポイントと解消方法
① 非rootユーザーと特権ポートの問題
症状
タスクが以下のエラーで停止する。
ERROR [Errno 13] Permission denied
Essential container in task exited
終了コード: 1
原因
Dockerfileで非rootユーザー(pythonista)に切り替えているのに、ポート80(特権ポート)を使おうとしているため権限エラーになります。Linuxではポート1024以下は root権限が必要です。また--reloadオプションを使っているとファイル監視のための権限も必要になり、同様のエラーが発生します。
解消方法
ポートを8080に変更し、--reloadオプションを外します。
# 変更前
CMD ["/app/.venv/bin/fastapi", "run", "./main.py", "--port", "80", "--host", "0.0.0.0", "--reload"]
# 変更後
CMD ["/app/.venv/bin/fastapi", "run", "./main.py", "--port", "8080", "--host", "0.0.0.0"]
タスク定義のコンテナポートも8080に変更します。
② セキュリティグループの設定漏れによる504エラー
症状
タスクはRUNNINGになっているのに、ALBのDNS名でアクセスすると504エラーが表示される。ターゲットグループのヘルスチェックがunhealthy(request timed out)になっている。
原因
ECSタスクのセキュリティグループで、ALBからポート8080へのアクセスを許可していないため、ALBがタスクに到達できません。
解消方法
ECSタスクのセキュリティグループに以下のインバウンドルールを追加します。
| 項目 | 値 |
|---|---|
| タイプ | カスタムTCP |
| ポート | 8080 |
| ソース | ALBのセキュリティグループ |
IPアドレス範囲(0.0.0.0/0)ではなく、ALBのセキュリティグループを指定することがポイントです。これによりALBを経由したリクエストのみを許可し、ECSタスクへの直接アクセスを防げます。
おわりに
今回の構成で意識した設計判断を整理すると次のとおりです。
- FargateでEC2管理を排除:サーバー管理の運用負荷を下げる
- タスクをプライベートサブネットに配置:インターネットから直接アクセスさせない
- ALB経由でのみアクセスを受け付ける:セキュリティグループでALB→タスクの経路のみを許可する
- ターゲットタイプをIPに設定:Fargate固有の要件に合わせる
Discussion