GitHub ActionでDockerのビルドキャッシュを有効にしてAmazonECSへデプロイする
やること
GitHub Actionを用いてDockerイメージをビルドし、Amazon ECRに保存し、Amazon ECSへデプロイします。
- ポイント
- 本番運用を想定し、ブランチにリリースを作成した場合にGitHub Actionが動作するようにする。
- Dockerのビルドを高速化するために、ビルドキャッシュを有効にする
準備
- 保存先のECRを作成
- デプロイ先のECRを作成
- GitHub Actionで使用するためのAWS IAMユーザーを作成(ECRとECSの権限が必要です)
これらは作成済みとして進めます。
作成するワークフロー
下記のようなワークフローを作成し、レポジトリの.github/workflows/
へdeploy-to-ecs.yml
のような適当な名前で保存します。
mainブランチにtagをpushすることにより、このワークフローが動作し、ECRへDockerイメージがpushされ、ECSへデプロイします。
Githubのリリースを用いることにより運用することを想定しています。
.github/workflows/deploy-to-ecs.yml
name: Deploy to ECS
on:
push:
tags:
- v*
env:
ECR_REPOSITORY: your-repository-name
ECS_SERVICE: your-service-name
ECS_CLUSTER: your-cluster-name
jobs:
deploy:
name: Deploy to ECS
if: github.event.base_ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Configure AWS Credentials # AWSアクセス権限設定
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
- name: Login to Amazon ECR # ECRログイン処理
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Set Docker Tag Env # Docker Imageのバージョンをタグに合わせる
run: echo "IMAGE_TAG=$(echo ${{ github.ref }} | sed -e "s#refs/tags/##g")" >> $GITHUB_ENV
- name: Build, tag, and push image to Amazon ECR # Docker イメージ Build&Push
env:
DOCKER_BUILDKIT: 1
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
run: |
docker build --cache-from=$ECR_REGISTRY/$ECR_REPOSITORY:latest --build-arg BUILDKIT_INLINE_CACHE=1 -f Dockerfile -t $ECR_REPOSITORY .
docker tag $ECR_REPOSITORY:latest $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker tag $ECR_REPOSITORY:latest $ECR_REGISTRY/$ECR_REPOSITORY:latest
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
- name: Render Amazon ECS task definition for app container # appコンテナのECSタスク定義ファイルレンダリング
id: render-app-container
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: .aws/ecs/task-definition.json
container-name: app
image: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}
- name: Deploy to Amazon ECS service # ECSサービスデプロイ
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.render-app-container.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: false
解説
ワークフローの動作について上から順に具体的に説明します。
1. ワークフロー名
name: Deploy to ECS
ワークフローの名前を決めています。Githubのレポジトリ、Actionタブからワークフローを一覧で確認する際などにこの名前が表示されます。検証環境や本番環境で複数のワークフローを作成する場合は、区別できる分かりやすい名前に変更する事を推奨します。
2. ワークフローの動作条件
on:
push:
tags:
- v*
v1.0.0
のようなv
で始まるタグがpushされた場合に動作するようにしており、リリースの作成によりこのワークフローが動作することを想定しています。
また、今回は他のブランチにタグがpushされてしまった場合に動作をしないようにjobs内に
if: github.event.base_ref == 'refs/heads/main'
のようにmainブランチではない場合には動作をしないようにしています。
特定のブランチにpushされた場合に動作するようにするにはif
の記述を無くし
on:
push:
branches:
- target-branch
のように記述してください。(target-branchはデプロイ対象とするブランチに変更して下さい。)
補足
on: push:
の記述方法で、特定のブランチにタグがpushされた場合動作するという事を実現するために
on:
push:
tags:
- v*
branches:
- target-branch
のように記述すると、and条件ではなくor条件(タグかブランチのどちらかがpushされた場合)になってしまう為、on: push:
で対象のタグを指定し、if
で対象のブランチを指定しています。
3. 環境変数への代入
env:
ECR_REPOSITORY: your-repository-name
ECS_SERVICE: your-service-name
ECS_CLUSTER: your-cluster-name
jobで何度も使う値の記述を減らすために環境変数へ代入しています。
your-〇〇〇-name
は各自の環境のものへ変更して下さい
4. Jobsの実行開始
jobs:
deploy:
name: Deploy to ECS
if: github.event.base_ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
ubuntu-latest
の環境にて対象ブランチにcheckoutし、jobの実行を始めます。
(先程にも記述しましたが、タグがpushされた場合のmainブランチでのみ実行するようにif
行を記述しています。動作条件をブランチのpushに変更した場合は不要です。)
5. AWSへのログイン
- name: Configure AWS Credentials # AWSアクセス権限設定
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
pushを行うECR、デプロイ先のECSを作成してあるAWS アカウントへログインをしています。
Githubのシークレットを用いてデプロイを行うIAMのアクセスキーとシークレットキーをAWS_ACCESS_KEY_ID
とAWS_SECRET_ACCESS_KEY
へ登録してください。
6. ECRログイン
- name: Login to Amazon ECR # ECRログイン処理
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
5.でログインしたAWSアカウントのECRへのログインを行います。
7. Dockerのイメージタグの設定
- name: Set Docker Tag Env # Docker Imageのバージョンをタグに合わせる
run: echo "IMAGE_TAG=$(echo ${{ github.ref }} | sed -e "s#refs/tags/##g")" >> $GITHUB_ENV
Dockerのイメージタグがgitのtagと同じ値になるようにpushされたtagをv1.0.0
のような形式で取り出し、環境変数へ代入しています。
ワークフローの動作条件にタグを用いない場合は、
run: echo "IMAGE_TAG=${{ github.sha }}" >> $GITHUB_ENV
のようにコミットハッシュを用いてイメージタグが一意になるようにすると良いと思います。
8. Dockerイメージのビルド&ECRへのPush
- name: Build, tag, and push image to Amazon ECR # Docker イメージ Build&Push
env:
DOCKER_BUILDKIT: 1
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
run: |
docker build --cache-from=$ECR_REGISTRY/$ECR_REPOSITORY:latest --build-arg BUILDKIT_INLINE_CACHE=1 -f Dockerfile -t $ECR_REPOSITORY .
docker tag $ECR_REPOSITORY:latest $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker tag $ECR_REPOSITORY:latest $ECR_REGISTRY/$ECR_REPOSITORY:latest
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
ローカルでDockerイメージをビルドする場合、前回にビルドしたイメージを用いてビルドが高速化されますが、GitHub Actionでは前回のビルド結果を保持していないため、毎回ビルドに時間が掛かってしまいます。
そこで、ここではDockerのdocker build --cache-from
を用いてビルドの高速化を行っています。
行っている内容としては、dockerイメージをビルドし、環境変数IMAGE_TAGに保存されているタグをpushしつつ、その値はワークフローの動作毎に動的に変化してしまい、前回ビルドした際の値を取得することが難しいためlatestタグもpushし、latestタグをキャシュに用いるようにしています。このようにすることにより、リリースタグと整合性を取りつつ、キャッシュを用いれます。
(DOCKER_BUILDKIT
, BUILDKIT_INLINE_CACHE
は--cache-from
を用いるために必要なため記述しています。)
注意: Dockerのマルチステージビルドを用いている場合は、Dockerのイメージ中にビルドプロセスが全て含まれていないためこの方法ではキャッシュできません。その場合は、このような記事を参考にすると良いと思います。
9. ECSタスク定義の作成
- name: Render Amazon ECS task definition for app container # appコンテナのECSタスク定義ファイルレンダリング
id: render-app-container
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: .aws/ecs/task-definition.json
container-name: app
image: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}
ECSの実行にはタスク定義が必要になり、そこに使用するDockerイメージのレポジトリやタグの情報を記述する必要があります。
従って、DockerImageを更新した際はタスク定義の更新も必要になり、その処理を行っています。
今回はレポジトリへタスク定義ファイル.aws/ecs/task-definition.json
を作成してあり、そのファイルを呼び出し、Dockerイメージの情報を更新しています。
task-definition.json
の参考例)
{
"containerDefinitions": [
"portMappings": [
{
"hostPort": 80,
"protocol": "tcp",
"containerPort": 80
}
],
"image": "your-image-name",
"name": "app"
}
],
"cpu": "256",
"executionRoleArn": "your-role-name",
"family": "your-family",
"memory": "512",
"requiresCompatibilities": [
"FARGATE"
],
"networkMode": "awsvpc"
}
s3から読み出す事も可能のようなので各々お好きなタスク定義の管理をして下さい。
10. ECSへデプロイ
- name: Deploy to Amazon ECS service # ECSサービスデプロイ
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.render-app-container.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: false
9で作成したタスク定義をもとにECSへデプロイしています。
wait-for-service-stability
をtrue
にすることにより、デプロイが完了するまで実行完了を待機させ、デプロイ完了を通知すること等が可能ですが、Github Actionsの実行時間が増加してしまい、実行枠を越えてしまう等の可能性が存在します。
AWS Lambdaを用いてデプロイの完了をフックし、通知する事も可能なのでデプロイの多い環境ではそうする事を推奨します。
11. おわり
上記のフローが正しく動作すればデプロイ完了です。
おまけ. task単体の実行
- name: Run Migrate # Migrationを実行
env:
CLUSTER_ARN: your_cluster_arn
ECS_SUBNER_FIRST: your_subnet_first
ECS_SUBNER_SECOND: your_subner_second
ECS_SECURITY_GROUP: your_security_group
run: |
aws ecs run-task --launch-type FARGATE --cluster $ECS_CLUSTER --task-definition ${{ steps.put-migrate-task.outputs.render-app-container }} --network-configuration "awsvpcConfiguration={subnets=[$ECS_SUBNER_FIRST, $ECS_SUBNER_SECOND],securityGroups=[$ECS_SECURITY_GROUP],assignPublicIp=ENABLED}" > run-task.log
TASK_ARN=$(jq -r '.tasks[0].taskArn' run-task.log)
aws ecs wait tasks-stopped --cluster $CLUSTER_ARN --tasks $TASK_ARN
今回デプロイ前にDBのmigrationを行いたく、このようなjobを手順10のECSへデプロイ前に追加しtaskを単体で実行しました。
このようにすることにより、Github Actions内でtask単体を実行することも可能です。
まとめ
ECSへのデプロイですとAWS CodeBuild、AWS CodePipelineを組み合わせるといった選択肢もあると思いますが、GitHub Actionを用いても簡単にデプロイすることが可能です。
参考記事
下記の記事を大変参考にさせて頂きました。
Discussion