🐳

ECS × FastAPI でAPIサーバーを構築する - コンテナ化からALB公開まで

に公開

はじめに

この記事では、FastAPIをDockerコンテナ化し、Amazon ECS(Fargate)にデプロイしてALB経由で公開するまでの手順をまとめます。

「AWSコンソールで何となく動かしたことはある」という段階から、なぜこの構成にするのかという設計意図も含めて説明することを意識しています。

この記事で作るもの

インターネット

  ALB(パブリックサブネット)

ECS Fargate タスク(プライベートサブネット)

FastAPI コンテナ(ポート8080)

前提条件

  • AWS CLIが設定済みであること
  • Dockerがローカルで動作していること
  • VPCとサブネット(パブリック・プライベート)が作成済みであること

VPC構成については以下の記事を参照してください。

https://zenn.dev/ak_yoshimatsu/articles/aws-web-network-20260105

使用技術

  • 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