はじめに
初めまして。現在、新卒1年目でグーホームチームに所属しているプロトソリューションの川崎です。
今回は業務でAPIサーバーのCI/CDを構築する業務があり、その中で得た知見を書いていこうと思います。
環境
【筆者ローカル環境】
・Windows
・WSL(コントリビューション:Ubuntu-20.04)
・Docker 20.10.23
【使用した主なAWSサービス】
・CodeCommit
・CodeBuild
・CodePipeline
・Elastic Container Registry
・Elastic Container Service(ECS on Fargate)
この記事で分かること
CodePipelineを使用した、ECS on Fargateにおける開発環境・本番環境における各環境変数の設定方法が分かります。
今回紹介する方法以外にも環境変数の設定の仕方は様々あるかと思いますが、個人的にはフォルダ直下に環境変数がある方が見やすいので今回はそのような構成をお伝えします。
また、CI/CDを主軸にしているので、基本的なGitの使用・運用方法、Docker、VPC、IAM、FastAPIの説明は省略しているのでご容赦ください。
手順や本論
フォルダ構成とファイルは以下になります。
appspec.ymlとtaskdef.jsonは途中で書くため、最初には記載しません。
tech_press/
├ backend/
│ ├ app/
│ │ ├ main.py
│ │ └ requirements.txt
│ └ Dockerfile
├ pipeline/
│ ├ dev/
│ │ ├ appspec.yml
│ │ └ taskdef.json
│ └ product/
│ ├ appspec.yml
│ └ taskdef.json
├ .env.dev
├ .env.local
├ .env.product
├ buildspec.yml
└ docker-compose.yaml
import os
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/check")
async def root():
container_env = os.environ['CONTAINER_ENV']
return {"Check": container_env + " is healthy"}
fastapi==0.75.0
uvicorn==0.16.0
FROM tiangolo/uvicorn-gunicorn:python3.7
COPY ./app /app
RUN pip install -U pip &&\
pip install --no-cache-dir -r /app/requirements.txt
ARG CONTAINER_ENV
ENV CONTAINER_ENV=${CONTAINER_ENV}
# COMMON
TIMEZONE=Asia/Tokyo
# API
API_HOST=0.0.0.0
API_PORT=8080
CONTAINER_ENV=Devel
# COMMON
TIMEZONE=Asia/Tokyo
# API
API_HOST=0.0.0.0
API_PORT=8080
CONTAINER_ENV=Local
# COMMON
TIMEZONE=Asia/Tokyo
# API
API_HOST=0.0.0.0
API_PORT=8080
CONTAINER_ENV=Product
version: '3.8'
services:
fastapi:
container_name: tech-press-api
image: tech-press-api
build:
context: ./backend
dockerfile: Dockerfile
args:
- CONTAINER_ENV
volumes:
- ./backend/app:/app
environment:
- TZ=${TIMEZONE}
command: uvicorn main:app --reload --host ${API_HOST} --port ${API_PORT}
ports:
- ${API_PORT}:${API_PORT}
1.まずはローカルで実行
まずはローカル環境下でFastAPIを立ち上げます。
(初回実行時)
docker compose --env-file .env.local up -d --build
(2回目以降)
docker compose --env-file .env.local up -d
下記画像のように
http://localhost:8080/check に
「{"Check":"Local is healthy"}」のJSONレスポンスが返ってきたらOKです
2.CodeCommit リポジトリ作成
CodeCommit -> リポジトリを作成 でリポジトリを作成します
今回は「tech-press」というリポジトリを作成しました
3.開発環境パイプラインを構築
ここからは、まず開発環境を構築していきます。
3-1.開発ECRのリポジトリ作成
Elastic Container Registry -> 「リポジトリを作成」でリポジトリを作成します。
他の設定はデフォルトのまま、「リポジトリを作成」を押下します。
次にCodeBuildからECRリポジトリにアクセス許可を与える設定をする必要があります。
Elastic Container Registry -> 作成したリポジトリ(今回でいうと「dev-tech-press-api」)の横にあるラジオボタンを選択 -> アクション -> 許可 -> ポリシーJSONの編集 を選択し、以下を追加します。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CodeBuildAccess",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<AWSアカウントID>:root",
"Service": "codebuild.amazonaws.com"
},
"Action": [
"ecr:BatchCheckLayerAvailability",
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer"
],
"Condition": {
"StringEquals": {
"aws:SourceArn": "arn:aws:codebuild:ap-northeast-1:<AWSアカウントID>:project/dev-tech-press",
"aws:SourceAccount": "<AWSアカウントID>"
}
}
}
]
}
Conditionのaws:SourceArnのプロジェクト名はまだ作成していないので、
ここで付けた名前をCodeBuildを作成するときのプロジェクト名にしてください(今回でいうと「dev-tech-press」)
3-2.buildspec.ymlの作成
まずはビルドが成功しているかどうかを見たいので、artifacts以下はコメントアウトしております。
デプロイフェーズで再度コメントアウトを外します
version: 0.2
env:
variables:
DOCKERHUB_USER: "<Docker Hubのユーザー名>"
DOCKERHUB_PASS: "<Docker Hubのパスワード>"
phases:
install:
runtime-versions:
docker: 20
pre_build:
commands:
# ECR へのログイン
- echo Logging in to Amazon ECR...
- $(aws ecr get-login --no-include-email --region ap-northeast-1)
# DockerHub へのログイン(pull回数制限回避のため)
- echo Logging in to Docker Hub...
# ID/PWはAWS Systems Manager > パラメータストアを参照
- echo ${DOCKERHUB_PASS} | docker login -u ${DOCKERHUB_USER} --password-stdin
build:
commands:
- echo Build started on `date`
- echo Building the Docker image...
- TAG_HOST=<AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com
- TAG_NAME=dev-tech-press-api
- LOCAL_IMAGE_NAME=tech-press-api
- TAG_VERSION=$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION} | head -c 8)
- docker-compose --env-file .env.dev up -d
- docker tag ${LOCAL_IMAGE_NAME}:latest ${TAG_HOST}/${TAG_NAME}:${TAG_VERSION}
- docker tag ${LOCAL_IMAGE_NAME}:latest ${TAG_HOST}/${TAG_NAME}:latest
post_build:
commands:
- echo Build completed on `date`
- echo Pushing the Docker image...
- docker images
- docker push ${TAG_HOST}/${TAG_NAME}:${TAG_VERSION}
- docker push ${TAG_HOST}/${TAG_NAME}:latest
- printf '{"ImageURI":"%s"}' ${TAG_HOST}/$TAG_NAME:latest > imageDetail.json
# artifacts:
# files:
# - ${TASK_DEF_PATH}
# - ${APPSPEC_PATH}
3-3.CodeBuildの構築
次にBuild環境の構築をしていきます。
CodeBuild -> 「ビルドプロジェクトを作成する」でビルドプロジェクトを作成します。
プロジェクト名は「dev-tech-press」です。
※先ほど作成したECRリポジトリのポリシーJSONのプロジェクト名と一致させてください
ソースは先ほど作成した「tech-perss」リポジトリを選択します。
ブランチは現時点では「master」ブランチのままにしておき、後ほど変更します。
環境はマネージド型イメージを使用します。
他、細かい部分は画像にある通りです。ロール名はデフォルトのまま変更していません。
Buildspecでは、「buildspecファイルを使用する」を選択し、Buildspec名は「buildspec.yml」と記載します。
その他の設定はデフォルトのままで、「ビルドプロジェクトを作成する」を押下します。
ビルドプロジェクトが作成されたので、一度「ビルドを開始」を押下して、ビルドを開始しましょう。
すると、以下の画像のようにビルドが失敗します。
COMMAND_EXECUTION_ERROR: Error while executing command: $(aws ecr get-login --no-include-email --region ap-northeast-1). Reason: exit status 255
理由は「環境」で新しく作成したロールにECRのポリシーが設定されていないためです。
先ほど作成したロール(codebuild-dev-tech-press-service-role)に「AmazonEC2ContainerRegistryPowerUser」ポリシーを追加します。
この状態になっていればOKです。
再度、ビルドを開始します。
成功していればビルドフェーズはOKです。
3-4.ECS on Fargate + CodeDeployの構築
3-4-1. クラスターの作成
まずはクラスターを作成します。
Elastic Container Service -> クラスター -> クラスターの作成 でクラスター作成画面に遷移します。
「ネットワーキングのみ」を選択して「次のステップ」を押下します
クラスター名は「dev-tech-press」とし、その他はデフォルトのままとします。
3-4-2. タスク定義の作成
次に新しいタスク定義を作成します。
Elastic Container Service -> タスク定義 -> 新しいタスク定義の作成 で新しいタスク定義を作成します。
Fargateを選択し、「次のステップ」を押下します。
タスク定義名は「dev-tech-press」とし、オペレーティングシステムファミリーは「Linux」を選択します。
タスクサイズはタスクメモリを「0.5GB」・タスクCPUを「0.25vCPU」とします。
※必要に応じてスペックを調整します
コンテナ名はdocker-compose.yamlで定義しているコンテナ名を使用し、イメージは「+:latest」とします。
ポートマッピングは80番に設定します。
そのほかの設定はデフォルトのままで「追加」を押下します。
その他のタスク定義もデフォルトのままで「作成」を押下します。
タスク定義が完成したら、タスク定義 -> <作成したタスク定義名> -> <作成したタスク定義名>:1 -> JSON を押下します
背景がグレーになっている部分がタスク定義のJSONなので、pipeline/dev 直下にtaskdef.jsonを作成し、背景がグレーになっている部分をコピーし、codecommitにプッシュします。
3-4-3. サービス・CodeDeployの作成
次にサービスを作成し、その過程でCodeDeployも同時に作成します。
まずは、サービスにアタッチするロールにCodeDeployの権限を付与する必要があります。
IAM -> ロール -> ロールの作成 -> でロールの作成画面に遷移します。そこから、
AWSのサービス -> ユースケースを表示するサービスを選択する -> 「CodeDeploy」と検索 -> 「CodeDeploy - ECS」をチェック -> 次へ -> 次へ とします。
ロール名は「TechPressCodeDeployRoleForECS」とします。
「ロール名を作成」でロールを作成します。
次に、サービスを作成します。
Elastic Container Service -> 先ほど作成したクラスター(今回でいうと「dev-tech-press」) -> 「サービス」タブを選択した状態で「作成」を押下 でサービスの作成画面に遷移します。
サービスの設定は
起動タイプ:Fargate
タスク定義:dev-tech-press
サービス名:dev-tech-press-service
タスク数:1
と、そのほかはデフォルトで設定します。
デプロイメントは
・Blue/Greenデプロイメントを選択
・CodeDeployのサービスロールは先ほど作成した「TechPressCodeDeployRoleForECS」を選択します。
VPCとセキュリティグループはこの記事で作成していないので、割愛します。
パブリック IP の自動割り当ては「ENABLED」を選択します。
ロードバランシングはALBを選択します。
ここでロードバランサを作成する場合は、ロードバランサのリスナーの設定をなしにしましょう。(サービス作成時に自動的に作成されるため)
また、ロードバランシングの上にあるヘルスチェックもここで変更します。
次にロードバランス用のコンテナで「ロードバランサーに追加」を押下し、
ロードバランス用のコンテナのプロダクションリスナーポートを「80」に設定します。
Additional configurationは
ターゲットグループ 1 の名前:dev-tech-press-01
ターゲットグループ 2 の名前:dev-tech-press-02
にし、そのほかはデフォルトのまま設定し、「次のステップ」を押下します
Auto Scalingは特に設定しません。次のステップに行きます
内容を確認し、「サービスの作成」を押下します。
パブリックIPが見れます。
このIPアドレスを開くと
http://<パブリックIP>/check
開発環境の環境変数である「Devel」が反映されていればOKです。
3-4-4. CodePipelineの構築
開発環境の最後にCodePipelineの構築をします。
CodePipeline -> パイプラインを作成する でパイプラインの構築画面に遷移します。
パイプラインの名前を「dev-tech-press」とします。
ソースステージは
・ソースプロバイダー:AWS CodeCommit
・リポジトリ名:tech-press
・ブランチ名:master
とします。
※ブランチは後ほど開発用を作成します。
ビルドステージは
・プロバイダー:AWS CodeBuild
・プロジェクト名:dev-tech-press
・環境変数を
TECH_PRESS_ECR_NAME:dev-tech-press-api
ENV_FILE:./.env.dev
TASK_DEF_PATH:pipeline/dev/taskdef.json
APPSPEC_PATH:pipeline/dev/appspec.yml
と設定します。
デプロイステージは
・デプロイプロバイダー:Amazon ECS(ブルー/グリーン)
・アプリケーション名:サービス作成時に構築されたもの
・デプロイグループ:サービス作成時に構築されたもの
Amazon ECS タスク定義は「BuildArtifact」を選択し、「pipeline/dev/taskdef.json」
AWS CodeDeploy AppSpec ファイルは「BuildArtifact」を選択し、「pipeline/dev/appspec.yml」とします。
最後にレビューを確認し、パイプラインを作成します。
作成した後、パイプラインが自動で起動しますが、この段階ではDeploy時にエラーが発生します。理由はbuildspec.ymlがビルド段階までのソースになっているからです。
buildspec.ymlを変更します
version: 0.2
env:
variables:
DOCKERHUB_USER: "<Docker Hubのユーザー名>"
DOCKERHUB_PASS: "<Docker Hubのパスワード>"
phases:
install:
runtime-versions:
docker: 20
pre_build:
commands:
# ECR へのログイン
- echo Logging in to Amazon ECR...
- $(aws ecr get-login --no-include-email --region ap-northeast-1)
# DockerHub へのログイン(pull回数制限回避のため)
- echo Logging in to Docker Hub...
# ID/PWはAWS Systems Manager > パラメータストアを参照
- echo ${DOCKERHUB_PASS} | docker login -u ${DOCKERHUB_USER} --password-stdin
build:
commands:
- echo Build started on `date`
- echo Building the Docker image...
- TAG_HOST=<AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com
- TAG_NAME=${TECH_PRESS_ECR_NAME}
- LOCAL_IMAGE_NAME=tech-press-api
- TAG_VERSION=$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION} | head -c 8)
- docker-compose --env-file ${ENV_FILE} up -d
- docker tag ${LOCAL_IMAGE_NAME}:latest ${TAG_HOST}/${TAG_NAME}:${TAG_VERSION}
- docker tag ${LOCAL_IMAGE_NAME}:latest ${TAG_HOST}/${TAG_NAME}:latest
post_build:
commands:
- echo Build completed on `date`
- echo Pushing the Docker image...
- docker images
- docker push ${TAG_HOST}/${TAG_NAME}:${TAG_VERSION}
- docker push ${TAG_HOST}/${TAG_NAME}:latest
- printf '{"ImageURI":"%s"}' ${TAG_HOST}/$TAG_NAME:latest > imageDetail.json
artifacts:
files:
- ${TASK_DEF_PATH}
- ${APPSPEC_PATH}
buildspec.ymlを変更し、プッシュし、再度「dev-tech-press」パイプラインを走らせます。
この段階でパイプラインが成功していれば、一旦開発パイプラインの構築は完了です。
4.本番環境パイプラインを構築
開発環境と構築方法は同じなので、変更している名前等を書き記します。
4-1.ECRのリポジトリ作成
リポジトリ名:product-tech-press-api
4-2.buildspec.ymlの作成
ここで作成するbuildspec.ymlは3-4-4.で作成したものでOKなので何もしなくて大丈夫です
4-3.CodeBuildの構築
プロジェクト名:product-tech-press
他の設定は3-3と同様です
また、この段階で「ビルドを開始」は不要です
4-4.ECS on Fargate + CodeDeployの構築
4-4-1. クラスターの作成
クラスター名:product-tech-press
4-4-2. タスク定義の作成
タスク定義名:product-tech-press
イメージ:「<本番ECRリポジトリのURI>+:latest」
4-4-3. サービス・CodeDeployの作成
ロールは3-4-3で作成した「「TechPressCodeDeployRoleForECS」でOKです
タスク定義:product-tech-press
サービス名:product-tech-press-service
ロードバランサ名:product-tech-press
ターゲットグループ 1 の名前:product-tech-press-01
ターゲットグループ 2 の名前:product-tech-press-02
この段階でサービスは立ち上がりますが、タスクは起動→停止を繰り返します。
パイプラインを構築することで、解消されます。
4-4-4. CodePipelineの構築
ビルドステージ
プロジェクト名:product-tech-press
環境変数を
TECH_PRESS_ECR_NAME:product-tech-press-api
ENV_FILE:./.env.product
TASK_DEF_PATH:pipeline/product/taskdef.json
APPSPEC_PATH:pipeline/product/appspec.yml
デプロイステージ
AWS CodeDeploy アプリケーション名:AppECS-product-tech-press-product-tech-press-service
AWS CodeDeploy デプロイグループ:DgpECS-product-tech-press-product-tech-press-service
3-4と同様に構築ができれば最終的にパイプラインが成功するかと思います。
Elastic Container Service -> 本番用クラスター -> サービス名 -> タスク で、タスクが正常に動いていたらOKです
前回のステータスが「RUNNING」が作動していたらタスクが正常に動いています。
本番用のパブリックIPで本番用の環境変数が適用されているか確認します。
問題なく動いているので、本番用のパイプラインの構築も完了です
5.パイプラインのトリガーをブランチ毎に変更
最後に、今の状態だと両パイプラインがmasterブランチが変更するとパイプラインが起動する設定になっています。
「staging」ブランチと「production」ブランチを作成し、「staging」ブランチに変更がかかると開発パイプラインが、「production」ブランチに変更がかかると本番パイプラインが起動する設定に変更します。
まずは、stagingブランチとproductionブランチを作ります
次に開発のパイプラインの設定をします。
CodePipeline -> 開発用パイプライン -> 編集 -> Sourceの「ステージを編集する」 -> 編集のアイコンをクリック -> ブランチ名を「staging」に変更 -> 完了 -> 保存
これで「staging」ブランチに変更がかかると開発パイプラインが、「production」ブランチに変更がかかると本番パイプラインが起動する設定に変更されました。
最後に実際に変更がかかるのか、プルリクエストをして確認します。
まずは開発機の変更を確認するためのブランチ、「develop/開発パイプライン確認ブランチ」を作成します。
「develop/開発パイプライン確認ブランチ」のブランチには以下の変更を加えます
# COMMON
TIMEZONE=Asia/Tokyo
# API
API_HOST=0.0.0.0
API_PORT=8080
- CONTAINER_ENV=Devel
+ CONTAINER_ENV=開発
プッシュして、プルリクエストを「staging」ブランチに投げ、マージします
パイプラインが終了すると、環境変数が変更されているのが分かります。
同様に「develop/本番パイプライン確認ブランチ」を作成し、.env.productを変更し、「production」にマージします
# COMMON
TIMEZONE=Asia/Tokyo
# API
API_HOST=0.0.0.0
API_PORT=8080
- CONTAINER_ENV=Product
+ CONTAINER_ENV=本番
本番も反映されていますね。めでたしめでたし。
おわりに
気が付いたら、中々のボリュームになっていましたw
今回は省きましたが、所属しているチームではパイプラインにテストを入れていたり、承認を入れたりして各フェーズでのチェックを行っています。
CI/CDは構築されたらその後は基本的に触らないため、今回の執筆でも思い出す部分が多かったです。
まだまだ1年目で浅くしか経験していないので、知識をより深めていきたいです!