はじめに
こちらはFastAPI の API を AWS の ECS (Fargate) に載せ、GitLab の main に push すると自動でデプロイされる状態まで、AWS CLI で作る手順です。
学習を目的とし、「とりあえず動くものをミニマムにやってみる」をコンセプトとしておりますので、趣旨と異なる場合はブラウザバックし別の技術ブログをご覧になることを推奨します。
前提
- 課金が発生するサービス(ALB,Fargate)を利用しています。確認後に必要に応じて各自でリソース削除などは行ってください。
-
<プレフィックス>のように<と>で囲んだ部分は、自分の値に置き換えます。
デプロイするアプリを用意する
ECS に載せる対象として、FastAPI で書いた最小の API サーバーを用意します。/health にアクセスすると {"status":"ok"} を返すだけのアプリです。この /health は、後の手順で ALB が「コンテナが動いているか」を確認するためにも使います。
アプリは Docker イメージにして ECS に渡すため、Dockerfile も一緒に用意します。次の 3 ファイルを、リポジトリの直下に置いてください。
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
def health():
return {"status": "ok"}
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY app ./app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
fastapi
uvicorn
ファイルの配置は次のとおりです。
.
├── app/main.py
├── Dockerfile
└── requirements.txt
AWS CLI の設定と、変数の準備
以降の手順では、AWS CLI のコマンドを複数回実行します。同じ値(リソース名の先頭、アカウント ID、ネットワークの ID)を何度も打たなくて済むように、先に CMD の変数に入れておきます。
実務では、Terraform や AWS CDK のように、リソースの構成をコードで書き、作成と削除をまとめて行う方法(IaC)が一般的に使われます。この記事は、どのリソースが必要で、どうつながるかを 1 つずつ確認するため、AWS CLI で順に作る方法にしています。
AWS CLI の認証を設定する
IAM ユーザーのアクセスキーを使って、AWS CLI がアカウントに接続できるようにします。プロファイル名は、自分で決めた名前にします。リージョンは ap-northeast-1、出力形式は json を入力します。この設定は 1 回だけ行います。
aws configure --profile <プロファイル名>
変数を入れる
次のコマンドで、以降の手順で使う値を変数に入れます。
-
AWS_PROFILE: 以降のawsコマンドが使うプロファイル -
AWS_DEFAULT_REGION: 作成先のリージョン(東京) -
P: リソース名の先頭に付ける<プレフィックス> -
ACCOUNT_ID: 接続している AWS アカウントの ID -
VPC_ID: ネットワーク(VPC)の ID。今回は新しく作らず、アカウントのデフォルト VPC を使います。デフォルト VPC を削除しているアカウントでは、この手順はそのままでは使えません -
ECR_URI: イメージの置き場(ECR)の住所
set AWS_PROFILE=<プロファイル名>
set AWS_DEFAULT_REGION=ap-northeast-1
set P=<プレフィックス>
for /f %i in ('aws sts get-caller-identity --query Account --output text') do set ACCOUNT_ID=%i
for /f %i in ('aws ec2 describe-vpcs --filters Name^=isDefault^,Values^=true --query Vpcs[0].VpcId --output text') do set VPC_ID=%i
set ECR_URI=%ACCOUNT_ID%.dkr.ecr.ap-northeast-1.amazonaws.com/%P%
for /f の行は、コマンドの結果を変数に入れる CMD の書き方です。
ALB は、2 つ以上のアベイラビリティゾーン(データセンターの区画)のサブネットを指定します。ここでは 2 つを使います。デフォルト VPC のサブネット(区画ごとのネットワーク)を調べ、ap-northeast-1a と ap-northeast-1c のサブネット ID を変数に入れます。
aws ec2 describe-subnets --filters Name=default-for-az,Values=true --query Subnets[].[SubnetId,AvailabilityZone] --output text
set SUBNET_1=<1a のサブネット ID>
set SUBNET_2=<1c のサブネット ID>
CMD を閉じたときは
set で入れた変数は、そのウィンドウの中だけで有効です。CMD のウィンドウを閉じると消えます。開き直したときは、aws configure 以外の set と for /f の行を、もう一度実行してください。
AWS リソースの作成
ECS でアプリを動かすために必要な AWS のリソースを、AWS CLI で作ります。作るものと、使う AWS のサービスは次のとおりです。
| 作るもの | 使う AWS のサービス | 役割 |
|---|---|---|
| イメージの置き場 | ECR | ビルドした Docker イメージを保管する |
| ログの出力先 | CloudWatch Logs | コンテナのログを集める |
| タスク実行ロール | IAM | ECS が ECR からイメージを取得し、ログを書くための権限 |
| セキュリティグループ 2 つ | EC2 | 通信の許可ルール(ALB 用と ECS 用) |
| ALB、ターゲットグループ、リスナー | ELB | インターネットからの入口。受けた通信をコンテナに転送する |
| クラスター | ECS | コンテナを動かす単位 |
コンテナの実行そのものは、サーバーを管理しなくて済む Fargate で行います。
イメージの置き場、ログ、権限を作る
ECR、CloudWatch Logs のロググループ、タスク実行ロールを作ります。ロールには、「ECS のタスクがこのロールを使ってよい」という信頼ポリシーが必要です。次の内容を trust-ecs-tasks.json として保存します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "ecs-tasks.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}
ロールの作成後に、AWS が用意している AmazonECSTaskExecutionRolePolicy を付けて、ECR からの取得とログの書き込みを許可します。
aws ecr create-repository --repository-name %P%
aws logs create-log-group --log-group-name /ecs/%P%
aws iam create-role --role-name %P%-task-exec-role --assume-role-policy-document file://trust-ecs-tasks.json
aws iam attach-role-policy --role-name %P%-task-exec-role --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
セキュリティグループを作る
ALB 用と ECS 用の 2 つを作ります。ALB には、インターネットから 80 番を許可します。ECS には、ALB からの 8000 番だけを許可します。こうすると、インターネットからコンテナに直接接続できません。
for /f %i in ('aws ec2 create-security-group --group-name %P%-alb-sg --description %P%-alb-sg --vpc-id %VPC_ID% --query GroupId --output text') do set ALB_SG=%i
aws ec2 authorize-security-group-ingress --group-id %ALB_SG% --protocol tcp --port 80 --cidr 0.0.0.0/0
for /f %i in ('aws ec2 create-security-group --group-name %P%-ecs-sg --description %P%-ecs-sg --vpc-id %VPC_ID% --query GroupId --output text') do set ECS_SG=%i
aws ec2 authorize-security-group-ingress --group-id %ECS_SG% --protocol tcp --port 8000 --source-group %ALB_SG%
ALB を作る
ALB は、インターネットからの入口です。次の 3 つを作ります。
- ALB: 80 番でリクエストを受ける
- ターゲットグループ: ALB の転送先。ヘルスチェックのパスに
/healthを指定する - リスナー: 「80 番に来たリクエストを、このターゲットグループに転送する」というルール
ALB を作った時点から、課金が始まります。
for /f %i in ('aws elbv2 create-load-balancer --name %P%-alb --subnets %SUBNET_1% %SUBNET_2% --security-groups %ALB_SG% --scheme internet-facing --type application --query LoadBalancers[0].LoadBalancerArn --output text') do set ALB_ARN=%i
for /f %i in ('aws elbv2 create-target-group --name %P%-tg --protocol HTTP --port 8000 --vpc-id %VPC_ID% --target-type ip --health-check-path /health --matcher HttpCode^=200 --query TargetGroups[0].TargetGroupArn --output text') do set TG_ARN=%i
aws elbv2 create-listener --load-balancer-arn %ALB_ARN% --protocol HTTP --port 80 --default-actions Type=forward,TargetGroupArn=%TG_ARN%
ECS クラスターを作る
aws ecs create-cluster --cluster-name %P%-cluster
イメージの push とサービスの作成
ここでは、アプリのイメージを ECR に送り、ECS にコンテナを起動させます。
イメージを ECR に push する
Docker Desktop を起動してから実行します。ECR にログインし、イメージをビルドして送ります。--platform linux/amd64 は、Fargate が動く CPU の種類に合わせる指定です。
aws ecr get-login-password | docker login --username AWS --password-stdin %ACCOUNT_ID%.dkr.ecr.ap-northeast-1.amazonaws.com
docker build --platform linux/amd64 -t %ECR_URI%:latest .
docker push %ECR_URI%:latest
タスク定義を登録して、サービスを作る
タスク定義は、コンテナの設計図です。使うイメージ、CPU とメモリ、ポート、ログの出力先を書きます。<アカウントID> を自分のアカウント ID に、<プレフィックス> を自分の値に置き換えて、taskdef.json として保存します。
{
"family": "<プレフィックス>-task",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512",
"executionRoleArn": "arn:aws:iam::<アカウントID>:role/<プレフィックス>-task-exec-role",
"containerDefinitions": [
{
"name": "<プレフィックス>",
"image": "<アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/<プレフィックス>:latest",
"essential": true,
"portMappings": [{ "containerPort": 8000, "protocol": "tcp" }],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/<プレフィックス>",
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "ecs"
}
}
}
]
}
サービスは、「このタスク定義のコンテナを常に 1 個動かし、ALB につなぐ」という指示です。最後の wait は、サービスの状態が安定するまで待ちます。
aws ecs register-task-definition --cli-input-json file://taskdef.json
aws ecs create-service --cluster %P%-cluster --service-name %P%-service --task-definition %P%-task --desired-count 1 --launch-type FARGATE --network-configuration "awsvpcConfiguration={subnets=[%SUBNET_1%,%SUBNET_2%],securityGroups=[%ECS_SG%],assignPublicIp=ENABLED}" --load-balancers targetGroupArn=%TG_ARN%,containerName=%P%,containerPort=8000 --health-check-grace-period-seconds 30
aws ecs wait services-stable --cluster %P%-cluster --services %P%-service
動作を確認する
ALB の DNS 名を調べ、ブラウザで開きます。
aws elbv2 describe-load-balancers --names %P%-alb --query LoadBalancers[0].DNSName --output text
表示された DNS 名で http://<DNS 名>/health を開き、{"status":"ok"} が返れば成功です。
GitLab CI
ここでは、main に push したときに、イメージのビルド、ECR への push、ECS の更新を自動で行う仕組みを作ります。用意するものは次の 4 つです。
| 用意するもの | 役割 |
|---|---|
| CI 用の IAM ユーザー | CI が AWS を操作するための権限とキー |
| GitLab の CI/CD 変数 | そのキーを CI のジョブに渡す |
| GitLab Runner | ジョブを実行するプロセス。今回は自分の PC の Docker 上で動かす |
.gitlab-ci.yml |
ジョブの内容(ビルド、push、ECS の更新) |
CI 用の IAM ユーザーを作る
CI が使う AWS のユーザーを、自分のユーザーとは別に作ります。CI に自分の権限を渡さないためです。権限は、ECR への push と、対象サービスの更新だけにします。次の内容を ci-policy.json として保存します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "ecr:GetAuthorizationToken",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ecr:BatchCheckLayerAvailability",
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:PutImage"
],
"Resource": "arn:aws:ecr:ap-northeast-1:<アカウントID>:repository/<プレフィックス>"
},
{
"Effect": "Allow",
"Action": ["ecs:UpdateService", "ecs:DescribeServices"],
"Resource": "arn:aws:ecs:ap-northeast-1:<アカウントID>:service/<プレフィックス>-cluster/<プレフィックス>-service"
}
]
}
ユーザーを作り、権限を付けて、アクセスキーを発行します。
aws iam create-user --user-name %P%-ci
aws iam put-user-policy --user-name %P%-ci --policy-name %P%-ci-policy --policy-document file://ci-policy.json
aws iam create-access-key --user-name %P%-ci --query AccessKey.[AccessKeyId,SecretAccessKey] --output text
キーを GitLab の変数に登録する
発行したキーは、1 回しか表示されません。
GitLab の「設定 → CI/CD → 変数」で、AWS_ACCESS_KEY_ID に 1 行目、AWS_SECRET_ACCESS_KEY に 2 行目を登録します。値を画面に表示しないように、マスクの設定にします。
Runner を作る
Runner は、CI のジョブを実行するプロセスです。ここでは、共有の Runner を使わず、自分の PC の Docker 上に、このプロジェクト専用の Runner を作ります。
GitLab の「設定 → CI/CD → Runner」で「プロジェクト Runner を作成」を選び、タグに <Runnerのタグ> を指定して作成します。作成後に認証トークン(glrt- で始まる文字列)が表示されます。画面を閉じるとトークンは再表示されないため、先に控えてください。
次のコマンドで、Runner のコンテナを起動します。ジョブの中で docker build を実行するために、PC の Docker をジョブから使えるようにしています。続けて、控えたトークンで GitLab に登録します。
docker run -d --name <Runnerのコンテナ名> -v gitlab-runner-config:/etc/gitlab-runner -v /var/run/docker.sock:/var/run/docker.sock gitlab/gitlab-runner:latest
docker exec <Runnerのコンテナ名> gitlab-runner register --non-interactive --url <GitLab の URL> --token <トークン> --executor docker --docker-image docker:27 --docker-volumes /var/run/docker.sock:/var/run/docker.sock
Runner registered successfully. と表示され、GitLab の画面で Runner が緑の丸(オンライン)になれば成功です。
.gitlab-ci.yml を置く
リポジトリの直下に .gitlab-ci.yml を置きます。main に push されたときだけ動く、1 つのジョブを書いています。ジョブは次の順に処理します。
- AWS CLI をインストールし、ECR にログインする
- イメージをビルドして、ECR に push する
- ECS のサービスに、新しいイメージでの再デプロイを指示する(
--force-new-deployment) - 再デプロイが終わるまで待つ
tags には、先ほど Runner に付けたタグを指定します。このタグを持つ Runner だけが、ジョブを実行します。
variables:
AWS_DEFAULT_REGION: ap-northeast-1
ECR_REPO: <プレフィックス>
ECS_CLUSTER: <プレフィックス>-cluster
ECS_SERVICE: <プレフィックス>-service
build-and-deploy:
stage: deploy
image: docker:27
tags:
- <Runnerのタグ>
rules:
- if: $CI_COMMIT_BRANCH == "main"
before_script:
- apk add --no-cache aws-cli
- export ECR_HOST="$(aws sts get-caller-identity --query Account --output text).dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com"
- aws ecr get-login-password | docker login --username AWS --password-stdin "$ECR_HOST"
script:
- docker build --platform linux/amd64 -t "$ECR_HOST/$ECR_REPO:latest" .
- docker push "$ECR_HOST/$ECR_REPO:latest"
- aws ecs update-service --cluster "$ECS_CLUSTER" --service "$ECS_SERVICE" --force-new-deployment
- aws ecs wait services-stable --cluster "$ECS_CLUSTER" --services "$ECS_SERVICE"
動作を確認する
main に push します。GitLab の「ビルド → パイプライン」でジョブが成功すれば、自動デプロイの仕組みは完成です。続けて app/main.py のレスポンスを変えて push すると、数分後に ALB の URL で変更が反映されます。
リソースの削除
動作を確認したあとに、課金を止めるため作ったリソースを削除する場合の手順です。依存関係があるため、次の順に実行します。
ECS のサービスとクラスターを削除します。
aws ecs update-service --cluster %P%-cluster --service %P%-service --desired-count 0
aws ecs delete-service --cluster %P%-cluster --service %P%-service --force
aws ecs wait services-inactive --cluster %P%-cluster --services %P%-service
aws ecs delete-cluster --cluster %P%-cluster
ALB を削除し、削除が完了してから、ターゲットグループを削除します。完了の直後はリスナーが残っていて「使用中」のエラーになることがあります。その場合は、数十秒待ってから再度実行してください。
aws elbv2 delete-load-balancer --load-balancer-arn %ALB_ARN%
aws elbv2 wait load-balancers-deleted --load-balancer-arns %ALB_ARN%
aws elbv2 delete-target-group --target-group-arn %TG_ARN%
セキュリティグループ、IAM ロール、ロググループ、ECR のリポジトリを削除します。セキュリティグループは、ALB と ECS が使っていたネットワークインターフェースの解放を待つため、1 分ほど置いてから実行します。
aws ec2 delete-security-group --group-id %ECS_SG%
aws ec2 delete-security-group --group-id %ALB_SG%
aws iam detach-role-policy --role-name %P%-task-exec-role --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
aws iam delete-role --role-name %P%-task-exec-role
aws logs delete-log-group --log-group-name /ecs/%P%
aws ecr delete-repository --repository-name %P% --force
CI 用の IAM ユーザーを、アクセスキーとポリシーを先に削除してから削除します。
aws iam list-access-keys --user-name %P%-ci --query AccessKeyMetadata[].AccessKeyId --output text
aws iam delete-access-key --user-name %P%-ci --access-key-id <アクセスキーID>
aws iam delete-user-policy --user-name %P%-ci --policy-name %P%-ci-policy
aws iam delete-user --user-name %P%-ci
最後に、自分の PC の Runner を削除します。GitLab の「設定 → CI/CD」で、Runner と CI/CD 変数(AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY)も削除してください。
docker rm -f <Runnerのコンテナ名>
docker volume rm gitlab-runner-config
CMD を開き直して変数が消えている場合は、%ALB_ARN% などに入れる ARN や ID を、aws elbv2 describe-load-balancers や aws ec2 describe-security-groups で調べて入れ直してください。
まとめ
この記事では、次のことを行いました。
- FastAPI の最小の API を、Docker イメージにした
- ECR、ALB、セキュリティグループ、IAM ロール、ECS クラスターを AWS CLI で作り、Fargate でコンテナを起動した
- ALB の URL で、
/healthの応答を確認した - CI 用の IAM ユーザーと、自分の PC の GitLab Runner を用意した
-
.gitlab-ci.ymlを置き、main への push で、ビルド、ECR への push、ECS の更新が自動で動くようにした
付録
この構成をベースに学習を続ける場合の例です。
- HTTPS 化: ACM で証明書を取得し、ALB に 443 番のリスナーを追加する
- IaC 化: 今回 CLI で作ったリソースを、Terraform や AWS CDK で書き直す
- デプロイの安全性: イメージにコミットのハッシュのタグを付け、問題があったときに前のイメージへ戻せるようにする
- CI の拡張: ビルドの前にテストのジョブを追加する
- 構成の見直し: タスクを private subnet に置き、NAT Gateway 経由で通信する