『入る学科間違えた高専生』の日記

プログラミングのコードを書いたりする予定です。あとは日記等。あといつまで高専生やねん

AWS BatchへのデプロイをGitHub Actionsで実装した

はじめに

また3ヶ月以上もブログを書いていなかったらしい。(生存ブログに関しては11月20日から書いていない)

今回は、仕事で AWS 環境化においての Batch 、 ECR 及び CloudWatch Events の更新を手動でやるのはダルいなという判断(脳内会議)になったので最近熱いと噂の GitHub Actions を使って CD 環境を作ろうとなって実装したのでその記事

CircleCI 等の環境でも同じようなことはできますが、僕が CircleCI を別のリポジトリで使いすぎて週間クレジットをリアルに毎週使い切るということになりそうなのでそれはそっちで使うということで使わないという判断です。

AWS のサービスの CodeBuild を使うという手もあるんですが、今の仕事では CloudFormation (以下、見出し等以外 CFn ) を使って一貫管理してないところもある *1 のですが95%以上は管理されているのでそのためにユーザ (Role) 増やして CodeBuild のログを見られるように修正パッチを毎回デプロイするとかかが面倒なので GitHub Actions での対応を判断しました。

github.co.jp

circleci.com

aws.amazon.com

環境

  • AWS
    • VPC
    • CloudFormation
    • ECR
    • Batch
    • CloudWatch Events
    • IAM
  • GitHub Actions

CloudFormation

Batch 内で RDS との通信等がありますが今回はスルーします。
またサブネットは外部との通信ができるように Public IPv4 を起動に設定するようにしてます。 *2

再度、書き起こして作ったものなのでミスがありそうですがごめんなさい。

Batch 関連

Parameters

  KeyName:
    Type: AWS::EC2::KeyPair::KeyName
    Description: 接続するためのキー名

IAM

Batch で使用する権限の作成

  BatchServiceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action:
              - sts:AssumeRole
            Effect: Allow
            Principal:
              Service:
                - batch.amazonaws.com
                - events.amazonaws.com
                - ecs-tasks.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSBatchServiceRole
        - arn:aws:iam::aws:policy/service-role/AWSBatchServiceEventTargetRole
      Path: /service-role/
      RoleName: BatchServiceRole

  ECSInstanceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action:
              - sts:AssumeRole
            Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonECS_FullAccess
        - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
        - arn:aws:iam::aws:policy/AmazonEC2FullAccess
        - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
        - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role
      Path: /
      RoleName: ECSInstanceRole

  ECSInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: ECSInstanceProfile
      Rules:
        - !Ref 'ECSInstanceRole'

ECR

言わずもがなリポジトリ

  Repository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: Repository
      RepositoryPolicyText:
        Statement:
          - Action:
              - ecr:GetDownloadUrlForLayer
              - ecr:BatchGetImage
              - ecr:BatchCheckLayerAvailability
            Effect: Allow
            Principal:
              AWS: !GetAtt 'ECSInstanceProfile.Arn'

Batch

Batch の定義等の作成

  ComputeEnvironment:
    Type: AWS::Batch::ComputeEnvironment
    Properties:
      ComputeResources:
        DesiredvCpus: 1
        Ec2KeyPair: !Ref 'KeyName'
        InstanceRole: !GetAtt 'ECSInstanceProfile.Arn'
        InstanceTypes:
          - optimal
        MaxvCpus: 64
        MinvCpus: 0
        SecurityGroupIds:
          - !Ref 'BatchSecurityGroup'
        Subnets:
          - !Ref 'Subnet'
        Type: EC2
      ServiceRole: !GetAttr 'BatchServiceRole.Arn'
      State: ENABLED

  JobDefinition:
    Type: AWS::Batch::JobDefinition
    Properties:
      ContainerProperties:
        Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${Repository}:latest'
        Memory: 2048
        Vcpus: 2
      JobDefinitionName: JobDefinition
      Type: container

  JobQueue:
    Type: AWS::Batch:JobQueue
    Properties:
      ComputeEnvironmentOrder:
        - ComputeEnvironment: !Ref 'ComputeEnvironment'
          Order: 1
        JobQueueName: JobQueue
        Priority: 1
        State: ENABLED

CloudWatch Events

毎日、日本時間0時0分にトリガーをするルールを作成

  Rule:
    Type: AWS::Events::Rule
    Properties:
      Description: Batchのトリガー 毎日0時 JST
      Name: Rule
      ScheduleExpression: cron(0 15 * * ? *)
      State: ENABLED
      Targets:
        - Arn: !Ref 'JobQueue'
          BatchParameters:
            JobDefinition: !Ref 'JobDefinition'
            JobName: every-batch-job
          Id: Every-Rule-Id
          RoleArn: !GetAtt 'BatchServiceRole.Arn'

GitHub Actions に使用するユーザ関連

ここでのテンプレートは上で作ったテンプレートとは別で作成しています。
またパラメータ等は時間の都合上、簡易的なものにしています。

Parameters:

  JobDefinition:
    Type: String
    Default: JobDefinition
    Description: AWS::Batch::JobDefinitionのリビジョンを含まないArn
    MinLength: 1
    MaxLength: 100

  Rule:
    Type: String
    Description: AWS::Events:RuleのArn
    MinLength: 1
    MaxLength: 100

  BatchServiceRole:
    Type: String
    Description: AWS::IAM:RoleのArn
    MinLength: 1
    MaxLength: 100

Resources:

  GitHubActionsUser:
    Type: AWS::IAM::User
    Properties:
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser
        - arn:aws:iam::aws:policy/AWSCloudFormationReadOnlyAccess
      UserName: github_actions

  GitHubActionsUserPolicy:
    Type: AWS::IAM::Policy
    Properties:
      Statement:
        - Action:
            - batch:DescribeJobDefinitions
          Effect: Allow
          Resource:
            - '*'
        - Action:
            - batch:RegisterJobDefinition
          Effect: Allow
          Resource:
            - !Ref 'JobDefinition'
        - Action:
            - batch:DeregisterJobDefinition
          Effect: Allow
          Resource:
            - !Sub '${JobDefinition}:*'
        - Action:
            - events:PutTargets
            - events:RemoveTargets
          Effect: Allow
          Resource:
            - !Ref 'Rule'
        - Action:
            - iam:PassRole
          Condition:
            StringEquals:
              iam:PassedToService:
                - batch.amazonaws.com
                - ec2.amazonaws.com
          Effect: Allow
          Resource:
            - !Ref 'BatchServiceRole'
      PolicyName: GitHubActionsUserPolicy
      Users:
        - !Ref 'GitHubActionsUser'

CFn 適用後、アクセスキー及びシークレットアクセスキーを作成して保存しておく。

GitHub Actions の設定

タグがプッシュされたときトリガーをするアクションを作成しています。

また、リポジトリSettings -> Secrets より以下の環境変数を追加しています

  • AWS_ACCOUNT_ID
  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • AWS_ECR_REPO_NAME
  • AWS_BATCH_SERVICE_ROLE
  • AWS_BATCH_JOB_QUEUE_ARN
# .github/workflows/main.yml
name: CD

on:
  push:
    tags:
      - v*

jobs:
  build:
    runs-on: ubuntu-18.04
    timeout-minutes: 600

    steps:
    - users: actions/checkout@2

    - 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: Get version number from tag
      id: extract-version
      shell: bash
      run: echo "##[set-output name=version;]$(echo ${GITHUB_REF#refs/tags/v})"

    - name: Build tag & push image to Amazon ECR
      id: push-ecr
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        ECR_REPOSITORY: ${{ secrets.AWS_ECR_REPO_NAME }}
        IMAGE_TAG: ${{ steps.extract-version.outputs.version }}
      shell: bash
      run: |
        docker build -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: Get current JobDefinition revision
     id: get-current-job-definition
     shell: bash
     run: |
       if [[ ! $(which aws) > /dev/null ]]; then
         apt-get update && apt-get install -y awscli
       fi

       REVISION=$(aws batch describe-job-definitions \
         --job-definition-name JobDefinition \  # CFn で作成したジョブ定義の実際の名前
         --status ACTIVE \
         --query "jobDefinitions[0].revision")

       echo "##[set-output name=revision;]$REVISION"
       echo "##[set-output name=job-definition;]JobDefinition"

    - name: Register new JobDefinition
      id: register-new-job-definition
      env:
        ROLE_ARN: ${{ secrets.AWS_BATCH_SERVICE_ROLE }}
        ECR_IMAGE_NAME: ${{ steps.push-ecr.outputs.image }}
        JOB_DEFINITION_NAME: ${{ steps.get-current-definition.outputs.job-definition }}
      shell: bash
      run: |
        NEW_REVISION=$(aws batch register-job-definition \
          --job-definition-name $JOB_DEFINITION_NAME
          --type container \
          --container-properties "{
            \"image\": \"$ECR_IMAGE_NAME\",
            \"vcpus\": 2,
            \"memory\": 2048,
            \"jobRoleArn\": \"$ROLE_ARN\"
          }" \
          --query "revision")

        echo "##[set-output name=revision;]$NEW_REVISION"

    - name: Update CloudWatch Events Rule
      shell: bash
      env:
        CURRENT_REVISION: ${{ steps.get-current-job-definition.outputs.revision }}
        NEW_REVISION: ${{ steps.register-new-job-definition.outputs.revision }}
        JOB_DEFINITION_NAME: ${{ steps.get-current-definition.outputs.job-definition }}
        JOB_QUEUE_ARN: ${{ secrets.JOB_QUEUE_ARN }}
        ROLE_ARN: ${{ secrets.AWS_BATCH_SERVICE_ROLE }}
      run: |
        RULE_NAME=Rule
        TARGET_ID=Every-Rule-Id
        JOB_NAME=every-batch-job
        JOB_DEFINITION_ARN=arn:aws:batch:ap-northeast-1:${{ secrets.AWS_ACCOUNT_ID }}:job-definition/$JOB_DEFINITION_NAME
        NEW_JOB_DEFINITION=$JOB_DEFINITION_ARN:$NEW_REVISION
        OLD_JOB_DEFINITION=$JOB_DEFINITION_ARN:$CURRENT_REVISION

        # 古いターゲットを削除する
        aws events remove-targets --rule $RULE_NAME --ids $TARGET_ID

        # CloudWatch Eventsのターゲットを更新する
        aws events put-targets $RULE_NAME \
          --targets "[
            {
              \"Id\": \"$TARGET_ID\",
              \"Arn\": \"$JOB_QUEUE_ARN\",
              \"RoleArn\": \"$ROLE_ARN\",
              \"BatchParameters\": {
                \"JobDefinition\": \"$NEW_JOB_DEFINITION\",
                \"JobName\": \"$JOB_NAME\"
              }
            }
          ]"

        # 古いリビジョンを解除する
        aws batch deregister-job-definition --job-definition $OLD_JOB_DEFINITION

という感じで...
やってることは

  1. AWS CLI をインストール後にする aws configure 作業
  2. ECR にログインをする
  3. プッシュされたタグ (v*) からバージョンを取得する
  4. docker builddocker push
  5. CLI から最新のジョブ定義のリビジョンを取得する
  6. CLI から置き換えるジョブ定義を作成する
  7. CLI で CloudWatch Events のルールから現在のターゲットを削除→新しいターゲットを作成→古いジョブ定義を終了
  8. put-targets 全部置き換えかなって思ってたけど CREATE の動作しかしかなった
  9. 既存のターゲットを更新するような API がなかった *3 ので削除→作成のような面倒なことになった

最後に

今回は、 GitHub Actions を使って、 AWS の ECR とか Batch とか CloudWatch Events を更新する継続的デプロイメントな環境を実装した。
こういう CI/CD 環境を作るのってお金になるって小耳にしたんだけどマジ!?
最初は、手動でもいいやって思ってたけど普通に僕がいなくなったあと誰がこれメンテすんの?事案が(脳内)会議で勃発したので GitHub Actions の勉強がてら実装をしてみた。使用制限も結構ミニマムなプロジェクトだったらガバガバ(悪いことはしてないし悪い意味でもない)なのでなので制限食らうことないなぁっていう感じで他の CI サービス食いに言ってるなぁという感覚がすごい。
iOS アプリとかの CI/CD 環境にも利用されることがあるとかないとかサービスリリース当初に Twitter で見かけたけど macOS 環境下の制限が結構厳しいし Xcode のバージョンがどのぐらい早く更新されるかっていうの結構重要だと思うので今のところは Bitrise 一択だなと個人的には思っています。

詳しく比較検証はしてないんですが、 CircleCI の machine ビルドと GitHub Actions のコールドスタートは後者優勢かなぁっていう感覚がありました。 CircleCI では machine を設定をしてビルド書けると30~60秒かかるらしい。 *4
あと、これは今は知らないけど CircleCI の AWS 設定をしたときに1ステップしか CLI で作業ができないような謎仕様があるのでこれをしなくても aws-actions/configure-aws-credentials を使うことでフローの最後までクレデンシャルが通るのは熱い!!

AWS の ポリシーは 2048 bytes の上限があるのであんまり1つのユーザで実装を楽にしたい場合とかはユーザを増やしてみたいになるかなとは思います。自身は前に書いた CFn の記事 *5 のやつと ECR , Batch , Events 更新用の権限を今回作ったユーザに割り当ててあげてます。

Lambda での実装でもよかったんですが *6RDS Proxy が現状、 Aurora for MySQLRDS for MySQL にしか対応されていない *7 ので DB アクセスを伴うバッチ処理は、 Batch かなと思って使うことにしています。(2020年3月29日(日) 12:08現在)

最近溜まってきたブログネタ帳からようやく1つ解消することができた。(ネタ帳に山程ネタが溜まっているという話ではない。)

早く Aurora for PostgreSQLRDS Proxy 使いたい...

アホだからこの記事4時間も書いてたらしいwwwww

参考

dev.classmethod.jp

help.github.com

*1:外部ライブラリに依存した Labmda 関数のデプロイパッケージの配置された S3 バケット

*2:サブネットの MapPublicIpOnLaunch に 'true' を設定

*3:events — AWS CLI 1.22.63 Command Reference

*4:Choosing an Executor Type - CircleCI

*5:troposphereで作ったAWS CloudFormationのテンプレートをCircleCIのジョブで検証する - 『入る学科間違えた高専生』の日記

*6:別のシステムではこんな感じの CD 環境の実装経験あり

*7:PostgreSQL を使っています