Github ActionsでECSにデプロイする構成自体はかなり多くの記事が出回っているので苦労はしないと思うのですが、IAMのキーの運用についてや、migrateタスクを自動で更新してコンテナを立ち上げて動かすのに少し時間がかかったので、網羅的にまとめておこうと思い、記事を書きます。
この記事の内容
- IAMの作成(Assume Role)
- GitHubActionsの設定ファイルの書き方
GitHub Actions参考ドキュメント
https://github.com/aws-actions
https://github.com/aws-actions/amazon-ecr-login
https://github.com/aws-actions/configure-aws-credentials
https://github.com/aws-actions/amazon-ecs-deploy-task-definition
https://docs.github.com/ja/actions/using-workflows/workflow-syntax-for-github-actions
https://docs.github.com/ja/actions/using-workflows/reusing-workflows
事前準備
-
まずこちらの記事を読むことをお勧め(わかりやすくECSデプロイの基本をつかめる非常に良記事)
-
デプロイ用の環境変数を設定
リポジトリの「Settings」→「secrets」→「Actions」から設定できる
GithubActions向けにIAMまわりのリソースを作成
GitHub Actionsで使うIAMユーザーをAssumeRoleを使ってセキュアに作ってみる。
参考:
https://github.com/aws-actions/configure-aws-credentials
https://aws.amazon.com/jp/blogs/news/create-a-ci-cd-pipeline-for-amazon-ecs-with-github-actions-and-aws-codebuild-tests/
-
AssumeRole は、IAM ロールを引き受けるためのアクション
-
sts:AssumeRoleを使うと、IAM ロールが持っている権限を引き受けてその権限を使用できる。
-
作成するリソースは以下
- GithubActionsで実際に使用する認証情報を持ったIAMユーザー
- AssumeRole(特定の指定したRoleのみを引き受けることができる)をおこなうためのIAMポリシーをインラインポリシーに作成(sts:AssumeRole)
- AssumeRoleの対象となるIAMロール
- 実際にECR/ECSへの権限を付与したIAMポリシー
- 信頼ポリシーにsts:AssumeRole,sts:TagSessionアクションを引き受ける対象のリソース(最初に作ったGitHubActions用のIAMユーザー)を定義
- GithubActionsで実際に使用する認証情報を持ったIAMユーザー
AWSTemplateFormatVersion: "2010-09-09"
Description:
Create IAM User and Role for Github Actions
Parameters:
ENV:
Type: String
AllowedValues:
- prod
- stg
PREFIX:
Type: String
Resources:
# GiHubActionsで認証情報として使うIAMユーザ
DeployUserForServer:
Type: AWS::IAM::User
Properties:
UserName: !Sub "${PREFIX}-${ENV}-deploy-user-for-server"
# GiHubActionsで認証情報として使うIAMユーザに付与するIAMポリシー(AssumeRole実施権限)
DeployPoricyForServer:
Type: AWS::IAM::Policy
Properties:
PolicyName: !Sub "${PREFIX}-${ENV}-deploy-policy-for-server"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action: "sts:AssumeRole"
Resource: !GetAtt DeployRoleForServer.Arn
Users:
- !Ref DeployUserForServer
# GiHubActionsで認証情報として使うIAMユーザがAssumeRole(引き受け)するIAMロール
DeployRoleForServer:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "${PREFIX}-${ENV}-deploy-role-for-server"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
# 信頼ポリシーで引き受け先とアクションを指定する
- Effect: "Allow"
Action: "sts:AssumeRole"
Principal:
AWS:
- !GetAtt DeployUserForServer.Arn
# 信頼ポリシーで引き受け先とアクションを指定する
# GitHubActionsの処理でAssumeRoleする際にセッションタグを渡すので必要になる
- Effect: "Allow"
Action: "sts:TagSession"
Principal:
AWS:
- !GetAtt DeployUserForServer.Arn
MaxSessionDuration: 3600
# IAMロールに付与するIAMポリシー(ECR/ECSに対する権限)
DeployPoricyForAssumeRole:
Type: AWS::IAM::Policy
Properties:
PolicyName: !Sub "${PREFIX}-${ENV}-deploy-policy-for-assume-role"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "ecr:CompleteLayerUpload"
- "ecr:UploadLayerPart"
- "ecr:InitiateLayerUpload"
- "ecr:BatchCheckLayerAvailability"
- "ecr:PutImage"
Resource: !Sub "arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/${PREFIX}-${ENV}*"
- Effect: "Allow"
Action:
- "ecs:RegisterTaskDefinition"
- "ecs:DescribeTaskDefinition"
- "ecs:RunTask"
- "ecs:ListTaskDefinitions"
- "ecs:DescribeTasks"
- "ecr:GetAuthorizationToken"
Resource: "*"
# タスク定義更新時にタスクロール/タスク実行ロールをタスク定義にPassRoleできる権限を与えておく
- Effect: "Allow"
Action:
- "iam:PassRole"
Resource:
- !Sub "arn:aws:iam::303975652040:role/${PREFIX}-${ENV}-ECSTaskRole"
- !Sub "arn:aws:iam::303975652040:role/${PREFIX}-${ENV}-ECSTaskExecution_Role"
Roles:
- !Ref DeployRoleForServer
Outputs:
DeployUserForServer:
Description: "IAM User for Server"
Value: !GetAtt DeployUserForServer.Arn
DeployRoleForServer:
Description: "IAM Role (AssumeRole) for Server"
Value: !GetAtt DeployRoleForServer.Arn
aws cloudformation create-stack \
--template-body file://iam_for_github_actions.yml \
--stack-name stg-iam-for-github-actions \
--parameters ParameterKey=ENV,ParameterValue=stg ParameterKey=PREFIX,ParameterValue=hoge \
--capabilities CAPABILITY_NAMED_IAM
CloudFormationで作成したスタックを確認
- IAMロールのARNを確認すること
aws cloudformation describe-stacks \
--stack-name stg-iam-for-github-actions \
--query 'Stacks[].Outputs'
IAMユーザーの認証情報を作成
- ACCESS_KEY_IDとSECRET_ACCESS_KEYを発行する
aws iam create-access-key \
--user-name <GiHubActionsで認証情報として使うIAMユーザ名>
{
"AccessKey": {
"UserName": <GiHubActionsで認証情報として使うIAMユーザ名>,
"AccessKeyId": "***",
"Status": "Active",
"SecretAccessKey": "***",
"CreateDate": "2021-06-30T07:40:56+00:00"
}
}
発行した認証情報をGitHubActionsのSettingsからセットする。
Assume Role実施箇所
GitHubActions上での実施箇所
# 2. AWS認証情報を設定(Assume Roleで一時的な権限付与をしている)
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.DEV_AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
# 以下の設定箇所で実際にAssume RoleしてIAMに一時的な権限の付与をおこなっている
role-to-assume: ${{ secrets.DEV_AWS_ASSUME_ROLE_ARN }}
role-duration-seconds: 1200 # 権限がくっついている期間は1200seconds
GitHub Actionsでのデプロイ処理フロー
IAMができたので、GitHubActionsの処理を書いていく。
- CI実行
- AWS認証情報をGitHub Actions上の実行環境にセット
- AWS CLIのインストール
- Amazon ECRにログイン
- NginxとRailsのDockerイメージをビルド
- タスク定義の自動更新
- タスク定義の実行
- 実行結果をslackに通知
CIの設定
- CIの実行はデプロイより前に必ず実行され、CIが通らない限りはデプロイされないように制御
- テストは並列実行用のライブラリを入れている( https://qiita.com/Dai_Kentaro/items/5741ed87c3b1fe68d5df )
name: CI
on:
push:
# 以下のデプロイに紐づくブランチではciファイルは実行せずにcdファイル内で呼び出して実行するようにする
branches-ignore:
- develop
- staging
- production
# https://docs.github.com/ja/actions/using-workflows/reusing-workflows
# CDファイルから呼び出されるときにCDファイル内から環境変数を渡しこちらで受け取るようにしないと設定した環境変数が使えない
workflow_call:
secrets:
RAILS_MASTER_KEY:
required: true
CC_TEST_REPORTER_ID:
required: true
jobs:
rspec:
runs-on: ubuntu-latest
# コミットメッセージに特定の文字列を入れるとCIをスキップできる
if: ${{ !contains(github.event.commits.*.message, '[skip ci]') }}
env:
RAILS_ENV: test
DB_HOST: 127.0.0.1
DB_PORT: 33060
DB_USERNAME: root
DB_PASSWORD: ''
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
AWS_ACCESS_KEY_ID: AWS_ACCESS_KEY_ID # AWSアクセスキーを使ったアクセスが必要なテストがあれば適当な文字列でテスト環境内のみアクセスできるようにしておく
AWS_SECRET_ACCESS_KEY: AWS_SECRET_ACCESS_KEY # AWSアクセスキーを使ったアクセスが必要なテストがあれば適当な文字列でテスト環境内のみアクセスできるようにしておく
AWS_DEFAULT_REGION: ap-northeast-1
AWS_DEFAULT_OUTPUT: json
services:
mysql:
image: mysql:8.0
ports:
- 33060:3306
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes # mysqlのパスワードを使わない場合はyes
BIND-ADDRESS: 0.0.0.0
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 # mysqlの疎通確認
steps:
# pumaのimageをbuild
- uses: actions/checkout@v2
- name: Check ./docker/puma/prod/Dockerfile
uses: hadolint/hadolint-action@v1.5.0
with:
dockerfile: ./docker/puma/prod/Dockerfile
# nginxのimageをbuild
- name: Check ./docker/nginx/prod/Dockerfile
uses: hadolint/hadolint-action@v1.5.0
with:
dockerfile: ./docker/nginx/prod/Dockerfile
- name: Set up Ruby 2.7
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7.3
bundler-cache: true
- name: Setup DB
run: |
sudo service mysql start
bundle exec rails db:prepare
# S3のモックを使いたいためminioを導入
- name: Setup minio
env:
AWS_ACCESS_KEY_ID: minio_access_key
AWS_SECRET_ACCESS_KEY: minio_secret_key
AWS_EC2_METADATA_DISABLED: true
run: |
docker run -d -p 9000:9000 --name minio \
-e "MINIO_ACCESS_KEY=minio_access_key" \
-e "MINIO_SECRET_KEY=minio_secret_key" \
-v /tmp/data:/data \
-v /tmp/config:/root/.minio \
minio/minio server /data
aws --endpoint-url http://localhost:9000/ s3 mb s3://<bucket名>
# rubocopを回す
- name: Run RuboCop
run: bundle exec rubocop
# Code Climateにデータを送っている(入れてなければ必要なし)
- name: Setup Code Climate test-reporter
run: |
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
chmod +x ./cc-test-reporter
./cc-test-reporter before-build
# テストを回す(並列実行用のgem(parallel_tests)を入れている。入れてなければ普通にbundle exec rspecコマンドのみで良い)
- name: Run RSpec
env:
AWS_S3_ENDPOINT: http://127.0.0.1:9000
run: |
./cc-test-reporter before-build
bundle exec rake parallel:create
bundle exec rake parallel:prepare
bundle exec rake parallel:spec
./cc-test-reporter after-build
CDの設定
GithubAcitonsで用意してくれているECS関連のプラグイン的なものを使う
- configure-aws-credentials
- AWSへアクセスするときのAWS認証情報を設定
- amazon-ecr-login
- ECRにログインする
- amazon-ecs-render-task-definition
- タスク定義にイメージURIを挿入する
- amazon-ecs-deploy-task-definition
- 挿入した最新イメージURIでタスク定義を更新してデプロイ(処理順的にはamazon-ecs-render-task-definitionの後に来るようにする)
その他
-
ワークフローの再利用して呼び出しCIを先行実行
-
migrate用のタスク定義にmigrateコマンドを上書きして設定したものを予め作成しておき、自動で最新のimageでタスク定義を更新してタスク実行するところで少し苦戦した。
- migrate用のタスク定義の中身を新しいimageに更新する処理を、
name: Render Amazon ECS migrate task definition with app container
のjobで行ってくれていると思っていたが、あくまで、imageをセットしているだけで、実際のタスク定義を最新のimageで更新する処理を行ってくれない模様 -
name: Revision to Amazon ECS migrate task
のjob内で、新しいimageを紐づけたタスク定義を新リビジョンとして更新するjobを明示的に実行すると新しいimageで更新されたタスク定義が作成されることを確認できた - その後、
name: Deploy to Amazon ECS migrate service
で最新Revisionのタスク定義を取得して、タスク定義を実行するとmigrate処理が完了する
- migrate用のタスク定義の中身を新しいimageに更新する処理を、
-
指定したid内のjob内で、
echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
みたいな感じにしておくと、別のstepで、${{ steps.build-image-app.outputs.image }}
とすれば変数として活用することが可能 -
GitHub上に登録した環境変数は
${{ secrets.DEV_AWS_ACCESS_KEY_ID }}
みたいな要領で使える -
それ以外のところは通常のECSデプロイのフローの通り
設定ファイル
name: CD
on:
push:
branches:
- develop
jobs:
# CIを先行実行(別のワークフロー(CI)をここで呼び出す)
rspec:
uses: ./.github/workflows/ci.yml
# ここで別のワークフローに変数を渡している
secrets:
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
# テスト完了後にデプロイ開始
deploy:
needs: [rspec] # rspec完了後にデプロイが実行されるようにしておく
name: Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
# 1. デプロイ用の環境変数を設定(デプロイの実行を環境ごとに分けるため)
- name: Set env values for dev
run: echo "TARGET=dev" >> $GITHUB_ENV
# 2. AWS認証情報を設定(Assume Roleで一時的な権限付与をしている)
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.DEV_AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
role-to-assume: ${{ secrets.DEV_AWS_ASSUME_ROLE_ARN }}
role-duration-seconds: 1200 # Assume roleしてroleを引き受けている秒数
# 3. AWS CLIのセット
- name: Setup Python 3.7 for awscli
uses: actions/setup-python@v1
with:
version: '3.7'
architecture: 'x64'
# 4. Amazon ECRにログイン
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
# 5. NginxとRailsのDockerイメージをビルド
- name: Build, tag, and push Nginx image to Amazon ECR
id: build-image-web
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: <プロジェクト名>-${{ env.TARGET }}-web
IMAGE_TAG: ${{ github.sha }} # ランダムなタグ名をつける
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f ./docker/nginx/dev_server/Dockerfile ./docker/nginx/dev_server
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
- name: Build, tag, and push Rails image to Amazon ECR
id: build-image-app
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: <プロジェクト名>-${{ env.TARGET }}-app
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f ./docker/puma/dev_server/Dockerfile .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
# 6. タスク定義の自動更新
- name: Download task definition
run: |
aws ecs describe-task-definition --task-definition <プロジェクト名>-${{ env.TARGET }} --query taskDefinition > app-task-definition.json
aws ecs describe-task-definition --task-definition <プロジェクト名>-${{ env.TARGET }}-worker --query taskDefinition > worker-task-definition.json
aws ecs describe-task-definition --task-definition <プロジェクト名>-${{ env.TARGET }}-migrate --query taskDefinition > migrate-task-definition.json
- name: Render Amazon ECS app task definition for web container
id: render-web-container
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: app-task-definition.json
container-name: <プロジェクト名>-${{ env.TARGET }}-web
image: ${{ steps.build-image-web.outputs.image }}
- name: Render Amazon ECS app task definition with app container
id: render-app-container
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
container-name: <プロジェクト名>-${{ env.TARGET }}-app
image: ${{ steps.build-image-app.outputs.image }}
- name: Render Amazon ECS worker task definition with app container
id: render-worker-container
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: worker-task-definition.json
container-name: <プロジェクト名>-${{ env.TARGET }}-worker
image: ${{ steps.build-image-app.outputs.image }}
- name: Render Amazon ECS migrate task definition with app container
id: render-migrate-container
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: migrate-task-definition.json
container-name: <プロジェクト名>-${{ env.TARGET }}-app-migrate
image: ${{ steps.build-image-app.outputs.image }}
# 7. タスク定義の実行
# 新しいimageを紐付けたmigrate用のタスク定義の新しいRevisionを明示的に作成する必要がある
- name: Revision to Amazon ECS migrate task
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.render-migrate-container.outputs.task-definition }}
cluster: <プロジェクト名>-${{ env.TARGET }}
# 最新Revisionのmigrate用のタスク定義でmigrateを実行する
- name: Deploy to Amazon ECS migrate service
env:
CLUSTER_ARN: ${{ secrets.DEV_CLUSTER_ARN }}
ECS_SUBNET_ID: ${{ secrets.DEV_ECS_SUBNET_ID }}
ECS_SECURITY_GROUP: ${{ secrets.DEV_ECS_SECURITY_GROUP }}
run: |
LATEST_REVISION=$(aws ecs list-task-definitions | jq -r '.taskDefinitionArns[]' | grep <プロジェクト名>-${{ env.TARGET }}-migrate: | tail -n1)
aws ecs run-task --launch-type FARGATE --cluster $CLUSTER_ARN --task-definition $LATEST_REVISION --network-configuration "awsvpcConfiguration={subnets=[$ECS_SUBNET_ID],securityGroups=[$ECS_SECURITY_GROUP],assignPublicIp=DISABLED}" > run-task.log
TASK_ARN=$(jq -r '.tasks[0].taskArn' run-task.log)
aws ecs wait tasks-stopped --cluster $CLUSTER_ARN --tasks $TASK_ARN
TASK_DEFINITION_ARN=$(aws ecs describe-tasks --cluster <プロジェクト名>-${{ env.TARGET }} --tasks $TASK_ARN --query "tasks[].taskDefinitionArn" --output text)
echo ${TASK_DEFINITION_ARN}
# 最新Revisionのapp用のタスク定義でappコンテナをデプロイする
- name: Deploy to Amazon ECS app service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.render-app-container.outputs.task-definition }}
service: <プロジェクト名>-${{ env.TARGET }}
cluster: <プロジェクト名>-${{ env.TARGET }}
# 最新Revisionのworker用のタスク定義でworkerコンテナをデプロイする
- name: Deploy to Amazon ECS worker service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.render-worker-container.outputs.task-definition }}
service: <プロジェクト名>-${{ env.TARGET }}-worker
cluster: <プロジェクト名>-${{ env.TARGET }}
# 8. 実行結果をslackに通知
- name: Slack Notification on Success
if: success()
uses: rtCamp/action-slack-notify@v2.0.2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_USERNAME: GitHUb Actions
SLACK_TITLE: Workflow Succeeded
SLACK_ICON: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
SLACK_MESSAGE: 'dev環境へのデプロイに成功しました。Run number : #${{ github.run_number }}'
SLACK_COLOR: good
- name: Slack Notification on Failure
uses: rtCamp/action-slack-notify@v2.0.2
if: failure()
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_USERNAME: GitHUb Actions
SLACK_TITLE: Workflow failed
SLACK_ICON: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
SLACK_MESSAGE: 'dev環境へのデプロイに失敗しました。Run number : #${{ github.run_number }}'
SLACK_COLOR: danger
いかがでしょうか??migrateのところとかは色々なやり方あると思うのでご意見いただきたいです。よろしくお願いいたします。