CDK/CircleCI/GitHubでAWSリソース管理リポジトリを作る

awstypescriptcirclecigithub

AWS CDKでリソースを記述し、PullRequestに対して自動でcdk diffで変更があるものを表示して、mergeしたときにcdk deployする。 全体のコードはGitHubにある。

AWS CDKでCloudFormationのテンプレートをTypeScriptから生成しデプロイする - sambaiz-net

追記 (2019-08-29): このフローで起こったいくつかの問題を解決するため新しいツールを作った。 PR上でCDKのレビューやデプロイを行うツールcdkbotを作った - sambaiz-net

CI Userの作成

まずcdkコマンドを実行するためのCI Userを作成する。これはCDK管理外のスタックで、AWSコンソール上から手動で上げる。

AWSのAssumeRole - sambaiz-net

AssumeRoleしかできないCIUserからCIAssumeRoleをassumeすることにした。

AWSTemplateFormatVersion: '2010-09-09'
Resources:
  CIAssumeRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: 'CIAssumeRole'
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/AdministratorAccess'
      AssumeRolePolicyDocument: 
        Version: '2012-10-17'
        Statement: 
          - Effect: 'Allow'
            Principal: 
              AWS: 
                - !Ref AWS::AccountId
            Action: 
              - 'sts:AssumeRole'
  CIGroup:
    Type: 'AWS::IAM::Group'
    Properties:
      GroupName: 'CI'
  CIPolicies:
    Type: 'AWS::IAM::Policy'
    Properties:
      PolicyName: 'CI'
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action: 'sts:AssumeRole'
            Resource: !GetAtt CIAssumeRole.Arn
      Groups:
        - !Ref CIGroup
  CIUser:
    Type: 'AWS::IAM::User'
    Properties:
      UserName: 'CI'
      Groups:
        - !Ref CIGroup
  CIUserKeys:
    Type: 'AWS::IAM::AccessKey'
    Properties:
      UserName: !Ref CIUser
Outputs:
  AccessKey:
    Value: !Ref CIUserKeys
  SecretKey:
    Value: !GetAtt CIUserKeys.SecretAccessKey

アップロードするとAccessKeyがOutputされるので、これに加え、SlackのWebhook URLとGitHubのTokenを作成しCircleCIのEnvironment Variablesに追加する。また、PR作成時にビルドが走るようにビルド設定のOnly build pull requestsをオンにする。

Environment Variables

CircleCIの設定

.circleci/config.ymlはこんな感じ。PRが立っていればdiffと、develop/masterにmergeするとstg/prd環境へのdeployを行う。 ただ、Only build pull requestsをオンにすると、PR以外ではdefault branchにしているmasterへのpushでしかjobが走らなくなってしまうので、 stg環境へのデプロイはdevelop->masterへのPRが存在していなければPRを作成したときに行われる。

CircleCI 2.1からのOrbでdocker buildしてECRにpushし、Slackに通知させる - sambaiz-net

version: 2.1
orbs:
  slack: circleci/[email protected]
executors:
  default:
    docker:
      - image: 'circleci/node:12.2.0'
    environment:
      AWS_REGION: 'ap-northeast-1'
jobs:
  build:
    executor: 'default'
    steps:
      - run:
          name: 'assume_role'
          command: |
            sudo apt-get install awscli
            unset AWS_SESSION_TOKEN
            echo $ASSUME_ROLE_ARN
            temp_role=$(aws sts assume-role --role-arn $ASSUME_ROLE_ARN --role-session-name $CIRCLE_PROJECT_REPONAME)
            echo "export AWS_ACCESS_KEY_ID=$(echo $temp_role | jq .Credentials.AccessKeyId | xargs)" >> $BASH_ENV
            echo "export AWS_SECRET_ACCESS_KEY=$(echo $temp_role | jq .Credentials.SecretAccessKey | xargs)" >> $BASH_ENV
            echo "export AWS_SESSION_TOKEN=$(echo $temp_role | jq .Credentials.SessionToken | xargs)" >> $BASH_ENV            
      - checkout
      - run:
          name: 'build'
          command: |
            npm install
            npm run build            
      - run:
          name: 'cdk_diff'
          command: |
            if [ -n "$CIRCLE_PULL_REQUEST" ]; then
              export ENV=stg
              if [ "${CIRCLE_BRANCH}" == "develop" ]; then
                export ENV=prd
              fi 
              pr_number=${CIRCLE_PULL_REQUEST##*/}
              block='```'
              diff=$(echo -e "cdk diff (env=${ENV})\n${block}\n$(npm run --silent ci_diff)\n${block}")
              data=$(jq -n --arg body "$diff" '{ body: $body }') # escape
              curl -X POST -H 'Content-Type:application/json' \
                -H 'Accept: application/vnd.github.v3+json' \
                -H "Authorization: token ${GITHUB_TOKEN}" \
                -d "$data" \
                "https://api.github.com/repos/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/issues/${pr_number}/comments"
            fi            
      - run:
          name: 'cdk_deploy'
          command: |
            if [ "${CIRCLE_BRANCH}" == "master" ]; then
              ENV=prd npm run ci_deploy
            elif [ "${CIRCLE_BRANCH}" == "develop" ]; then
              ENV=stg npm run ci_deploy
            fi             
      - slack/status:
          success_message: "cdk build/deploy (${CIRCLE_BRANCH}) has succeeded :tada:"
          failure_message: "cdk build/deploy (${CIRCLE_BRANCH}) has failed :crying_cat_face:"
          webhook: $SLACK_WEBHOOK
          only_for_branches: "develop,master"

Assumeしたkeyは$BASH_ENVでexportされるようにして以後のstepでも自動で読み込まれるようにする。 GitHubのコメント作成APIを呼ぶのにPR番号が必要で$CIRCLE_PR_NUMBERを使おうとしたが、folkされたPRでしか入らないので$CIRCLE_PULL_REQUESTのURLから/末尾を取り出して使っている。

cdk diffは差分があるとstderrに出力し終了ステータスを1で返してしまい、ビルドが中断してしまうのでstderrをstdoutに向けて||trueで強制的に成功させている。sedでは色を付けるためのescape sequenceを取り除いている。-cでenvの値をcontextとして渡して各環境のStackが作られるようにしていて、deploy時に--require-approval neverを付けているのはsgへのルール追加時などに確認が入ってCIが止まらないようにするため。

huskyでcommit時にprettierを走らせている。

$ cat package.json | jq ".scripts"
"scripts": {
  "build": "tsc",
  "watch": "tsc -w",
  "format": "prettier --write \"lib/**/*.ts\"",
  "ci_diff": "cdk diff -c env=${ENV:-stg} 2>&1 | sed -r 's/\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g' || true",
  "ci_deploy": "cdk deploy -c env=${ENV:-stg} --require-approval never"
}

$ cat .huskyrc.json 
{
  "hooks": {
    "pre-commit": "npm run format && git add ."
  }
}

PRを作成するとcdk diffの結果が表示される。

PRに投げられたcdk diffの結果

deployが完了するとSlackに通知が飛ぶ。

Slackの通知