Github ActionsでFargateをデプロイする
Github Actions
これまではCodePipeline上に構築することが多かったんですが、もう少しサクッと作れないかなーと思い試してみました。
環境
- Ruby製のアプリケーション 。
- コンテナは、pumaを動かすアプリケーションとNginxの2台構成。
- ECS Serviceとして常駐させる。
- 起動タイプはFargate。
ECSの設定、解説は特にしません。
VPC等々のリソースも作成済みとします。
そのあたり気になる方は、こちらの記事でCloudFormationで一発で作るテンプレートを公開してるのでどうぞ。
ワークフローを作っていく
Actionsにはいくつかテンプレートが用意されていて、対話形式でWeb上で作っていくことができるのですが、今回は先にdevelopブランチにpushしたかったので、ymlをコピーしてきて手元でカスタマイズしました。
Deploy to Amazon ECSをベースにします。
on: push: branches: - develop name: Deploy to staging ECS jobs: deploy: name: Deploy runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Configure AWS credentials 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 id: login-ecr uses: aws-actions/amazon-ecr-login@v1 - name: Build, tag, and push app image to Amazon ECR id: build-app-image env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} ECR_REPOSITORY: app-staging IMAGE_TAG: ${{ github.sha }} run: | docker build --build-arg DEPLOYMENT=true -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" - name: Build, tag, and push nginx image to Amazon ECR id: build-nginx-image env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} ECR_REPOSITORY: app-staging IMAGE_TAG: ${{ github.sha }} run: | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:nginx_$IMAGE_TAG ./docker/nginx/ docker push $ECR_REGISTRY/$ECR_REPOSITORY:nginx_$IMAGE_TAG echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:nginx_$IMAGE_TAG" - name: Fill in the new app image ID in the Amazon ECS task definition id: task-def1 uses: aws-actions/amazon-ecs-render-task-definition@v1 with: task-definition: staging-task-definition.json container-name: app image: ${{ steps.build-app-image.outputs.image }} - name: Fill in the new nginx image ID in the Amazon ECS task definition id: task-def2 uses: aws-actions/amazon-ecs-render-task-definition@v1 with: task-definition: ${{ steps.task-def1.outputs.task-definition }} container-name: nginx image: ${{ steps.build-nginx-image.outputs.image }} - name: Deploy Amazon ECS task definition uses: aws-actions/amazon-ecs-deploy-task-definition@v1 with: task-definition: ${{ steps.task-def2.outputs.task-definition }} service: app-staging cluster: app-staging wait-for-service-stability: true - name: Slack notification env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} uses: Ilshidur/action-slack@master with: args: '(staging) app deployed to ECS'
一つずつ見ていきます。
トリガー
on: push: branches: - develop
develop
ブランチへのプッシュにしています。
ここは様々な条件を付けることができて、releaseやtag、特定のファイルが変更されたら、という条件も可能なよう。
また、cron式によるスケジューラや、Github外部のイベントも設定できるしい。
ワークフロー名
name: Deploy to staging ECS
ここに表示される名前です。
ジョブ
jobs: deploy: name: Deploy runs-on: ubuntu-latest steps:
今回はdeploy
という一つのジョブにまとめました。このジョブという単位は、並列で実行することができる(依存関係も定義できる)ので、効率的に実行するためには適切に分割する設計が求められます。
RunnerはUbuntuの最新バージョンです。
Github Actionsではワークフローを実行するマシンをRunner
と呼びます。
ステップ
steps
で各処理を記述します。
ほぼほぼテンプレートのまま流用しているのですが、イメージのビルドを2つやる必要があるところと、最後にSlackに通知を飛ばすところを変更・追加しています。
チェックアウト
- name: Checkout uses: actions/checkout@v2
特に解説はいらないですね。
uses
は、他で定義されているアクションを利用するという意味です。
ライブラリみたいなものと考えておけばいいと思います。
AWS認証
- name: Configure AWS credentials 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
今回必要となるIAMポリシーはこんな感じです。 ECRへのプッシュ、タスク定義の更新、サービスへのデプロイなどです。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "0", "Effect": "Allow", "Action": [ "ecs:RegisterTaskDefinition", "ecr:GetAuthorizationToken" ], "Resource": "*" }, { "Sid": "1", "Effect": "Allow", "Action": [ "ecs:UpdateService", "iam:PassRole", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "ecr:CompleteLayerUpload", "ecs:DescribeServices", "ecr:UploadLayerPart", "ecr:InitiateLayerUpload", "ecr:BatchCheckLayerAvailability", "ecr:PutImage" ], "Resource": [ "arn:aws:ecr:*:*:repository/app-staging", "arn:aws:ecs:*:*:service/app-staging/app-staging", "arn:aws:iam::*:role/app-staging-ecs-task", ] } ] }
これをアタッチしたユーザーを作成して、アクセスキーとシークレットキーをSettings
のSecrets
に保存します。
ECRログイン
- name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v1
特に解説いらないですね。 いつもやってるやつです。
Dockerビルド
- name: Build, tag, and push app image to Amazon ECR id: build-app-image env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} ECR_REPOSITORY: app-staging IMAGE_TAG: ${{ github.sha }} run: | docker build --build-arg DEPLOYMENT=true -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
ECR_REPOSITORY
にリポジトリ名を記述します。
イメージタグにはgitのコミットハッシュをつけるようになっているのでそのまま。
run
に実行するコマンドを書いていきます。
--build-arg
とかプロジェクト固有のオプションがある場合は変更します。
最後のset-output
で、ビルドされたイメージ名を後のステップで参照できるようにしています。
テンプレートは1つだけでしたが、Nginxの方のビルドを追加します。
ECRリポジトリは共通のものを使い、タグのプレフィックスにnginx_
をつけることとします。
- name: Build, tag, and push nginx image to Amazon ECR id: build-nginx-image env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} ECR_REPOSITORY: app-staging IMAGE_TAG: ${{ github.sha }} run: | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:nginx_$IMAGE_TAG ./docker/nginx/ docker push $ECR_REGISTRY/$ECR_REPOSITORY:nginx_$IMAGE_TAG echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:nginx_$IMAGE_TAG"
タスク定義の更新
- name: Fill in the new app image ID in the Amazon ECS task definition id: task-def1 uses: aws-actions/amazon-ecs-render-task-definition@v1 with: task-definition: staging-task-definition.json container-name: app image: ${{ steps.build-app-image.outputs.image }}
新しくビルドされたイメージを使うように、タスク定義の新しいリビジョンを作成します。
タスク定義が記述された staging-task-definition.json
というファイルを作成してプロジェクトのルートに置きます。
既存のものから雛形を作るのが楽なので以下のコマンドを実行します。
aws ecs describe-task-definition --task-definition app-staging:1 --query taskDefinition > staging-task-definition.json
必要ない属性が記述されているとこんな感じのWarningが出るので、適宜削除します。
{ "containerDefinitions": [ { "name": "app", "image": "", "cpu": 0, "memoryReservation": 512, "essential": true, "command": [ "bundle", "exec", "pumactl", "start" ], "environment": [ { "name": "APP_ENV", "value": "production" } ], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-create-group": "true", "awslogs-group": "app-staging", "awslogs-region": "ap-northeast-1", "awslogs-stream-prefix": "app-app" } }, "healthCheck": { "command": [ "CMD-SHELL", "ps aux | grep puma | grep -v grep", "|| exit 1" ], "interval": 30, "timeout": 5, "retries": 3, "startPeriod": 15 } }, { "name": "nginx", "image": "", "cpu": 0, "memoryReservation": 512, "portMappings": [ { "containerPort": 80, "hostPort": 80, "protocol": "tcp" } ], "essential": true, "volumesFrom": [ { "sourceContainer": "app", "readOnly": true } ], "dependsOn": [ { "containerName": "app", "condition": "HEALTHY" } ], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-create-group": "true", "awslogs-group": "app-staging", "awslogs-region": "ap-northeast-1", "awslogs-stream-prefix": "app-nginx" } }, "healthCheck": { "command": [ "CMD-SHELL", "curl -f http://localhost/", "|| exit 1" ], "interval": 30, "timeout": 5, "retries": 3, "startPeriod": 30 } } ], "family": "app-staging", "taskRoleArn": "arn:aws:iam::{AWS_ID}:role/app-staging-ecs-task", "executionRoleArn": "arn:aws:iam::{AWS_ID}:role/app-staging-ecs-task", "networkMode": "awsvpc", "requiresCompatibilities": [ "FARGATE" ], "cpu": "256", "memory": "1024" }
このタスク定義に、先ほどビルドしたイメージIDを突っ込んでくれるのがこのアクションの本質です。
そして今回はコンテナが2台あるので続けてこのアクションを実行します。
ポイントは、タスク定義ファイルに、前のステップで出力されたタスク定義を指定しているところです。
こうしないと、appコンテナのイメージIDが注入される前のタスク定義に戻ってしまいます。
複数コンテナに一括で注入できるような仕様は今の所ないらしいです。
- name: Fill in the new nginx image ID in the Amazon ECS task definition id: task-def2 uses: aws-actions/amazon-ecs-render-task-definition@v1 with: task-definition: ${{ steps.task-def1.outputs.task-definition }} container-name: nginx image: ${{ steps.build-nginx-image.outputs.image }}
サービスの更新
- name: Deploy Amazon ECS task definition uses: aws-actions/amazon-ecs-deploy-task-definition@v1 with: task-definition: ${{ steps.task-def2.outputs.task-definition }} service: app-staging cluster: app-staging wait-for-service-stability: true
できあがったタスク定義のリビジョンを使ってECSサービスを更新します。
wait-for-service-stability
は、サービスが安定するまでステップの成否判定を待つオプションです。
Slack通知
- name: Slack notification env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} uses: Ilshidur/action-slack@master with: args: '(staging) app deployed to ECS'
コミュニティのアクションがいろいろあるんですが、Webフックが使いたかったので、それに対応していてスターが多いものを選びました。
WebフックをSecrets
に保存します。
args
にはメッセージを記述します。
チャンネル、ユーザー名、アバター画像はデフォルトのままです。
まとめ
Github Actionsの作法が分かればCodePipelineよりサクッと組めると思います。
CloudFormationのテンプレートとか持ってなくて一から作る場合は、圧倒的にこちらをおすすめします。初学者にも分かりやすい。
Lambdaとか手動の承認プロセスとか凝ったことをやりたい場合はAWSの方に軍配があがるかなと。AWSの機能だから当たり前なんですけど。
Githubに結びついてる分、デプロイファーストへの敷居下がりますね〜