1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS ECS + FastAPIをデプロイした — ECRとFargateの構成

1
Posted at

はじめに

FastAPIのアプリをAWSで本番運用するにあたってECSを使った構成を作った。

Lambdaも試したが(前の記事参照)、常時トラフィックがあるAPIにはECSのほうが向いている。ECSの設定項目が多くて最初は何がなんだかわからなかったので、ECRへのイメージプッシュからFargateでのタスク起動まで一連の流れを整理した。


全体構成

[GitHub]
    ↓ git push
[GitHub Actions]
    ↓ docker build
[ECR(コンテナレジストリ)]
    ↓ イメージをデプロイ
[ECS(Fargate)]
    ├── タスク定義(コンテナの設定)
    └── サービス(タスクの起動管理)
          ↑
         ALB(ロードバランサー)
          ↑
        Internet

使うAWSサービス

ECR   — Dockerイメージの保存場所
ECS   — コンテナの実行環境
Fargate — ECSの起動タイプ(サーバーレスコンテナ)
ALB   — HTTPSの終端とロードバランシング
VPC   — ネットワーク(プライベートサブネットにECSを置く)
IAM   — ECSタスクの権限管理
CloudWatch — ログ収集
Secrets Manager — 環境変数の機密情報管理

FastAPIアプリのDockerfile

FROM python:3.12-slim AS builder

WORKDIR /app

RUN pip install --no-cache-dir uv

COPY requirements.txt .
RUN uv pip install --no-cache --system -r requirements.txt


FROM python:3.12-slim AS production

WORKDIR /app

COPY --from=builder /usr/local/lib/python3.12/site-packages \
                    /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY src/ ./src/

ENV PYTHONPATH=/app
ENV PYTHONUNBUFFERED=1

RUN useradd --create-home --no-log-init appuser
USER appuser

EXPOSE 8000

CMD ["uvicorn", "src.main:app",
     "--host", "0.0.0.0",
     "--port", "8000",
     "--workers", "2",
     "--log-level", "info"]

ECR — Dockerイメージの保存

リポジトリの作成

# ECRリポジトリを作成
aws ecr create-repository \
    --repository-name fastapi-app \
    --region ap-northeast-1 \
    --image-scanning-configuration scanOnPush=true \
    --encryption-configuration encryptionType=AES256

scanOnPush=trueでプッシュ時に脆弱性スキャンが自動で走る。セキュリティリスクを早期発見できる。

イメージのビルドとプッシュ

# ECRにログイン
aws ecr get-login-password --region ap-northeast-1 | \
    docker login --username AWS --password-stdin \
    123456789012.dkr.ecr.ap-northeast-1.amazonaws.com

# ビルド
docker build -t fastapi-app .

# タグ付け
docker tag fastapi-app:latest \
    123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/fastapi-app:latest

# プッシュ
docker push \
    123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/fastapi-app:latest

IAMロールの作成

ECSタスクに必要なIAMロールを作成する。

タスク実行ロール(ECSがECRからイメージを取得するために必要)

// trust-policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect":    "Allow",
            "Principal": {"Service": "ecs-tasks.amazonaws.com"},
            "Action":    "sts:AssumeRole"
        }
    ]
}
# タスク実行ロールを作成
aws iam create-role \
    --role-name ecsTaskExecutionRole \
    --assume-role-policy-document file://trust-policy.json

# AWS管理ポリシーをアタッチ
aws iam attach-role-policy \
    --role-name ecsTaskExecutionRole \
    --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

# Secrets Managerへのアクセスを追加
aws iam attach-role-policy \
    --role-name ecsTaskExecutionRole \
    --policy-arn arn:aws:iam::aws:policy/SecretsManagerReadWrite

タスクロール(コンテナ内のアプリが使う権限)

// task-policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::my-app-bucket/*"
        },
        {
            "Effect":   "Allow",
            "Action":   ["secretsmanager:GetSecretValue"],
            "Resource": "arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:*"
        }
    ]
}
aws iam create-role \
    --role-name ecsTaskRole \
    --assume-role-policy-document file://trust-policy.json

aws iam put-role-policy \
    --role-name ecsTaskRole \
    --policy-name ecsTaskPolicy \
    --policy-document file://task-policy.json

Secrets Managerにシークレットを保存

# データベース接続情報を保存
aws secretsmanager create-secret \
    --name production/database \
    --region ap-northeast-1 \
    --secret-string '{
        "DATABASE_URL": "postgresql://user:pass@rds-endpoint:5432/mydb"
    }'

# APIキーを保存
aws secretsmanager create-secret \
    --name production/api-keys \
    --region ap-northeast-1 \
    --secret-string '{
        "SECRET_KEY": "your-secret-key",
        "OPENAI_API_KEY": "sk-..."
    }'

タスク定義

// task-definition.json
{
    "family":                  "fastapi-app",
    "networkMode":             "awsvpc",
    "requiresCompatibilities": ["FARGATE"],
    "cpu":                     "512",
    "memory":                  "1024",
    "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
    "taskRoleArn":      "arn:aws:iam::123456789012:role/ecsTaskRole",
    "containerDefinitions": [
        {
            "name":  "fastapi",
            "image": "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/fastapi-app:latest",
            "portMappings": [
                {
                    "containerPort": 8000,
                    "protocol":      "tcp"
                }
            ],
            "environment": [
                {"name": "APP_ENV",    "value": "production"},
                {"name": "LOG_LEVEL",  "value": "info"}
            ],
            "secrets": [
                {
                    "name":      "DATABASE_URL",
                    "valueFrom": "arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:production/database:DATABASE_URL::"
                },
                {
                    "name":      "SECRET_KEY",
                    "valueFrom": "arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:production/api-keys:SECRET_KEY::"
                }
            ],
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group":         "/ecs/fastapi-app",
                    "awslogs-region":        "ap-northeast-1",
                    "awslogs-stream-prefix": "ecs",
                    "awslogs-create-group":  "true"
                }
            },
            "healthCheck": {
                "command":     ["CMD-SHELL", "curl -f http://localhost:8000/health || exit 1"],
                "interval":    30,
                "timeout":     5,
                "retries":     3,
                "startPeriod": 60
            },
            "essential": true
        }
    ]
}
# タスク定義を登録
aws ecs register-task-definition \
    --cli-input-json file://task-definition.json

ECSクラスターとサービスの作成

# クラスターを作成
aws ecs create-cluster \
    --cluster-name production \
    --capacity-providers FARGATE FARGATE_SPOT \
    --default-capacity-provider-strategy \
        capacityProvider=FARGATE,weight=1,base=1
// service.json
{
    "cluster":        "production",
    "serviceName":    "fastapi-service",
    "taskDefinition": "fastapi-app:1",
    "desiredCount":   2,
    "launchType":     "FARGATE",
    "networkConfiguration": {
        "awsvpcConfiguration": {
            "subnets": [
                "subnet-private-1a",
                "subnet-private-1c"
            ],
            "securityGroups": ["sg-ecs-fastapi"],
            "assignPublicIp": "DISABLED"
        }
    },
    "loadBalancers": [
        {
            "targetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:targetgroup/fastapi-tg/xxx",
            "containerName":  "fastapi",
            "containerPort":  8000
        }
    ],
    "deploymentConfiguration": {
        "maximumPercent":        200,
        "minimumHealthyPercent": 100,
        "deploymentCircuitBreaker": {
            "enable":   true,
            "rollback": true
        }
    },
    "enableExecuteCommand": true
}
# サービスを作成
aws ecs create-service \
    --cli-input-json file://service.json

deploymentCircuitBreakerを有効にすると、デプロイが失敗したときに自動でロールバックする。

enableExecuteCommand: trueでコンテナにexecコマンドでSSH的に接続できる。デバッグに便利。


ALBの設定

# ターゲットグループの作成
aws elbv2 create-target-group \
    --name fastapi-tg \
    --protocol HTTP \
    --port 8000 \
    --vpc-id vpc-xxx \
    --target-type ip \
    --health-check-path /health \
    --health-check-interval-seconds 30 \
    --health-check-timeout-seconds 5 \
    --healthy-threshold-count 2 \
    --unhealthy-threshold-count 3

# ALBの作成
aws elbv2 create-load-balancer \
    --name fastapi-alb \
    --subnets subnet-public-1a subnet-public-1c \
    --security-groups sg-alb \
    --scheme internet-facing \
    --type application

# HTTPSリスナーの追加
aws elbv2 create-listener \
    --load-balancer-arn arn:aws:elasticloadbalancing:...:loadbalancer/app/fastapi-alb/xxx \
    --protocol HTTPS \
    --port 443 \
    --certificates CertificateArn=arn:aws:acm:ap-northeast-1:123456789012:certificate/xxx \
    --default-actions Type=forward,TargetGroupArn=arn:aws:...:targetgroup/fastapi-tg/xxx

# HTTPをHTTPSにリダイレクト
aws elbv2 create-listener \
    --load-balancer-arn arn:aws:elasticloadbalancing:...:loadbalancer/app/fastapi-alb/xxx \
    --protocol HTTP \
    --port 80 \
    --default-actions \
        Type=redirect,RedirectConfig='{Protocol=HTTPS,Port=443,StatusCode=HTTP_301}'

FastAPIのヘルスチェックエンドポイント

# src/main.py
from fastapi             import FastAPI
from sqlalchemy          import text
from database            import engine

app = FastAPI()

@app.get("/health")
async def health_check():
    """ALBのヘルスチェック用エンドポイント"""
    try:
        with engine.connect() as conn:
            conn.execute(text("SELECT 1"))
        db_status = "ok"
    except Exception as e:
        db_status = f"error: {str(e)}"

    status = "ok" if db_status == "ok" else "degraded"
    return {
        "status":  status,
        "db":      db_status,
        "version": "1.0.0",
    }

オートスケーリングの設定

# オートスケーリングターゲットの登録
aws application-autoscaling register-scalable-target \
    --service-namespace ecs \
    --resource-id service/production/fastapi-service \
    --scalable-dimension ecs:service:DesiredCount \
    --min-capacity 2 \
    --max-capacity 10

# CPU使用率に基づくスケーリングポリシー
aws application-autoscaling put-scaling-policy \
    --service-namespace ecs \
    --resource-id service/production/fastapi-service \
    --scalable-dimension ecs:service:DesiredCount \
    --policy-name cpu-scaling \
    --policy-type TargetTrackingScaling \
    --target-tracking-scaling-policy-configuration '{
        "TargetValue": 70.0,
        "PredefinedMetricSpecification": {
            "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
        },
        "ScaleInCooldown":  60,
        "ScaleOutCooldown": 30
    }'

CPU使用率が70%を超えるとタスク数を増やし、下がると減らす。


GitHub ActionsでCI/CDを構築

# .github/workflows/deploy.yml
name: Deploy to ECS

on:
  push:
    branches: [main]

env:
  AWS_REGION:       ap-northeast-1
  ECR_REPOSITORY:   fastapi-app
  ECS_CLUSTER:      production
  ECS_SERVICE:      fastapi-service
  CONTAINER_NAME:   fastapi

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: |
          pip install uv
          uv pip install --system -r requirements.txt
          uv pip install --system pytest pytest-cov

      - name: Run tests
        run: pytest --cov=src tests/

  deploy:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      id-token: write   # OIDC認証に必要
      contents: read

    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials(OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-role
          aws-region:     ${{ env.AWS_REGION }}

      - name: Login to ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and push image
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG:    ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT

      - name: Download task definition
        run: |
          aws ecs describe-task-definition \
              --task-definition fastapi-app \
              --query taskDefinition \
              > task-definition.json

      - name: Update image in task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name:  ${{ env.CONTAINER_NAME }}
          image:           ${{ steps.build-image.outputs.image }}

      - name: Deploy to ECS
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service:         ${{ env.ECS_SERVICE }}
          cluster:         ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: true

      - name: Notify success
        if: success()
        run: |
          echo "デプロイ完了: ${{ github.sha }}"

wait-for-service-stability: trueでデプロイが完了するまでActions が待つ。デプロイ失敗時もGitHub上で確認できる。

OIDC認証(アクセスキーを使わない方法)

// IAMロールの信頼ポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
                },
                "StringLike": {
                    "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:*"
                }
            }
        }
    ]
}

GitHub ActionsにAWSのアクセスキーを渡さずにOIDCでロールを引き受ける方法。シークレットにアクセスキーを保存しなくていいのでセキュリティが向上する。


デバッグ — コンテナへの接続

# ECS Execでコンテナに接続(SSM Agent経由)
aws ecs execute-command \
    --cluster production \
    --task タスクID \
    --container fastapi \
    --command "/bin/bash" \
    --interactive

# ログをリアルタイムで確認
aws logs tail /ecs/fastapi-app --follow

# 特定の時間帯のログを確認
aws logs get-log-events \
    --log-group-name /ecs/fastapi-app \
    --log-stream-name ecs/fastapi/タスクID \
    --start-time $(date -d "1 hour ago" +%s000)

よくあるトラブルシューティング

# サービスの状態を確認
aws ecs describe-services \
    --cluster production \
    --services fastapi-service \
    --query 'services[0].{Status:status,Running:runningCount,Desired:desiredCount,Events:events[0:3]}'

# タスクが起動しない場合 — タスクのログを確認
aws ecs list-tasks \
    --cluster production \
    --service-name fastapi-service \
    --desired-status STOPPED

aws ecs describe-tasks \
    --cluster production \
    --tasks タスクID \
    --query 'tasks[0].stoppedReason'

# デプロイの状態を確認
aws ecs describe-services \
    --cluster production \
    --services fastapi-service \
    --query 'services[0].deployments'

まとめ

  • ECRにDockerイメージを保存してECSから参照する
  • FargateはサーバーレスコンテナでEC2の管理不要
  • タスク実行ロール(ECRからのイメージ取得)とタスクロール(アプリの権限)は分ける
  • Secrets ManagerのシークレットをタスクDefinitionで環境変数に注入する
  • deploymentCircuitBreakerを有効にして失敗時に自動ロールバック
  • GitHub ActionsのOIDC認証でアクセスキーを排除できる
  • ECS ExecとCloudWatch Logsでデバッグする

最初はAWS CLIのコマンドの多さに圧倒されたが、一度構築してしまえばあとはGitHubにプッシュするだけでデプロイが走る。構築のコストが高い分、運用が楽になる。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?