CircleCIにてAssumeRoleを用いたECR、ECSのデプロイ方法

## はじめに

私は業務でAWS上にAPIサーバを作成しています。
そして、そのAPIサーバのソースコードはGuthubで管理しており、CircleCIと連携してGithubにプッシュすると自動的にAWS上のECSへデプロイされます。
昨今ではよくみる構成かもしれません。特段珍しいものでもないでしょう。

その中でセキュリティ向上のために AWS の Assume Role 機能を使うようになりました。

AssumeRole の対応方法を調べましたが、ネット上では情報が足りない（日本語の中では「Assume Role 機能を試してみました！」で止まっているような情報が多い。今回の要件を満たすような記事は私には見つけられなかった）ので、備忘録として残しておきます。

## 注意事項
この記事は以下の点に注意して参照してください。

* 記事の内容は2022年1月に作成したものです。その後に CircleCI 提供の orbs にアップデートが入り、このような回りくどいことしなくても大丈夫になっている可能性もあります。
* CircleCI 提供の orbs の処理内容に依存しています。下記バージョン以外を利用されると不整合が起きる可能性もありますのでご注意ください。（後述する処理内容さえクリアできれば、これより新しいバージョンでも動作すると思います）

  circleci/aws-ecr@7.0.0
  circleci/aws-ecs@2.1.0
  circleci/aws-cli@2.0.0

## ゴールとその要件、前提条件

### ゴール
* AssumeRole を用いてGithubからECSへデプロイする。

### 要件
* 可能な限り CircleCI の.circleci/config.yml に処理を書かない。（orbsを利用する）
* CircleCI の orbs は、公式（CircleCI）が提供しているものだけ利用可能とする。

### 前提条件
* AssumeRole を利用しない方法でのデプロイはすでに理解しているものとする。

## 結論

結論からいえば、以下のような .circleci/config.yml を記載します。
コメントも記載していますので、これだけでなんとなく理解できるかもしれません。

```
version: 2.1
orbs:
  aws-ecr: circleci/aws-ecr@7.0.0
  aws-ecs: circleci/aws-ecs@2.1.0
  aws-cli: circleci/aws-cli@2.0.0


# ---------------------------------------------------------------------------------------------------
#
# commands
#
# ---------------------------------------------------------------------------------------------------
commands:

  #
  # 以下の２つの処理を行う
  # ・指定された環境変数に Assume Role 用のアクセスキー、シークレットキーを設定する
  # ・デフォルトのプロファイルに「セッショントークンも含めて」Assume Role ユーザを「デフォルトのプロファイルで」設定する
  #
  setup-assume-role-and-set-env-var:
    parameters:
      aws-access-key-id:
        default: AWS_ACCESS_KEY_ID
        type: env_var_name
      aws-secret-access-key:
        default: AWS_SECRET_ACCESS_KEY
        type: env_var_name
      aws-region:
        default: AWS_REGION
        type: env_var_name
      role-arn:
        type: string
      aws-access-key-env-name:
        default: AWS_ACCESS_KEY_ID
        type: string
      aws-secret-access-env-name:
        default: AWS_SECRET_ACCESS_KEY
        type: string
      aws-session-token-env-name:
        default: AWS_SESSION_TOKEN
        type: string
    steps:
      # 権限を委譲されるユーザ（つまり権限をほぼ持っていないユーザ）のプロファイルを作成
      # 権限を委譲されるユーザはここでしか利用しないので、プロファイル名を指定してデフォルト以外を利用する
      - aws-cli/setup:
          aws-access-key-id: << parameters.aws-access-key-id >>
          aws-secret-access-key: << parameters.aws-secret-access-key >>
          aws-region: <<parameters.aws-region>>
          profile-name: base-profile
      # Assume Role された（上記のユーザで権限を委譲された状態）でアクセスするためのアクセスキーなど取得
      # 前半では指定された環境変数にアクセスキーとシークレットキーを設定
      # （後に利用する CircleCI の orbs でaws-cli/setup しても処理を通すようにするため）
      # 後半では後に利用するため、「セッショントークンも付けて」権限を委譲された「デフォルトの」プロファイルを作成する
      # （CircleCI の orbs ではデフォルトのプロファイル利用前提のものがあるため）
      - run:
          name: Assume role and export environment variables
          command: >
            CREDENTIALS="$(aws sts assume-role --role-arn << parameters.role-arn >> --role-session-name S-<< parameters.aws-access-key-env-name >> --profile base-profile | jq '.Credentials')"

            echo "export << parameters.aws-access-key-env-name >>=$(echo $CREDENTIALS | jq -r '.AccessKeyId')" >> $BASH_ENV

            echo "export << parameters.aws-secret-access-env-name >>=$(echo $CREDENTIALS | jq -r '.SecretAccessKey')" >> $BASH_ENV

            echo "export << parameters.aws-session-token-env-name >>=$(echo $CREDENTIALS | jq -r '.SessionToken')" >> $BASH_ENV

            aws configure set aws_access_key_id "$(echo $CREDENTIALS | jq -r '.AccessKeyId')" --profile default

            aws configure set aws_secret_access_key "$(echo $CREDENTIALS | jq -r '.SecretAccessKey')" --profile default

            aws configure set aws_session_token "$(echo $CREDENTIALS | jq -r '.SessionToken')" --profile default

# ---------------------------------------------------------------------------------------------------
#
# jobs
#
# --------------------------------------------------------------------------------------------------- 
jobs:

  #
  # AssumeRoleを用いてECRをデプロイする
  #
  ecr-build-and-push-image:
    executor: aws-ecr/default
    parameters:
      account-url:
        default: AWS_ECR_ACCOUNT_URL
        type: env_var_name
      aws-access-key-id:
        default: AWS_ACCESS_KEY_ID
        type: env_var_name
      aws-secret-access-key:
        default: AWS_SECRET_ACCESS_KEY
        type: env_var_name
      dockerfile:
        default: Dockerfile
        type: string
      extra-build-args:
        default: ''
        type: string
      region:
        default: AWS_REGION
        type: env_var_name
      repo:
          type: string
      tag:
        default: latest
        type: string
      role-arn:
        type: string
    steps:
      # 権限を委譲されたデフォルトのプロファイルを作成する
      - setup-assume-role-and-set-env-var:
          aws-access-key-id: << parameters.aws-access-key-id >>
          aws-secret-access-key: << parameters.aws-secret-access-key >>
          aws-region: << parameters.region >>
          role-arn:  << parameters.role-arn >>
          aws-access-key-env-name: ASSUME_ACCESS_ECR
          aws-secret-access-env-name: ASSUME_SECRET_ECR
          aws-session-token-env-name: ASSUME_TOKEN_ECR
      # Dockerイメージを作成してECRにプッシュ
      # 　orbs のコマンド内でプロファイルを設定するので、権限を委譲されたプロファイルを作成するように前処理で指定した環境変数を指定する。
      # 　同じ権限を二重で登録することになるが、認証キーは同じものを利用しているので問題ない。（同じ値を上書きするだけなので）
      # 　orbs をそのまま利用したいため、このような回りくどいことを行っている
      - aws-ecr/build-and-push-image:
          account-url: << parameters.account-url >>
          aws-access-key-id: ASSUME_ACCESS_ECR
          aws-secret-access-key: ASSUME_SECRET_ECR
          region: << parameters.region >>
          dockerfile: << parameters.dockerfile >>
          extra-build-args: << parameters.extra-build-args >>
          repo: << parameters.repo >>
          tag: << parameters.tag >>

  #
  # ECSクラスタ内のスケジュールされたタスクの更新
  #
  ecs-deploy-service-update:
    docker:
      - image: 'circleci/python:3.7.1'
    parameters:
      aws-access-key-id:
        default: AWS_ACCESS_KEY_ID
        type: env_var_name
      aws-region:
        default: AWS_REGION
        type: env_var_name
      aws-secret-access-key:
        default: AWS_SECRET_ACCESS_KEY
        type: env_var_name
      cluster-name:
        type: string
      container-env-var-updates:
        default: ''
        type: string
      container-image-name-updates:
        default: ''
        type: string
      family:
        type: string
      service-name:
        default: ''
        type: string
      role-arn:
        type: string
    steps:
      # 権限を委譲されたデフォルトのプロファイルを作成する
      - setup-assume-role-and-set-env-var:
          aws-access-key-id: << parameters.aws-access-key-id >>
          aws-secret-access-key: << parameters.aws-secret-access-key >>
          aws-region: << parameters.aws-region >>
          role-arn:  << parameters.role-arn >>
          aws-access-key-env-name: ASSUME_ACCESS_ECS
          aws-secret-access-env-name: ASSUME_SECRET_ECS
          aws-session-token-env-name: ASSUME_TOKEN_ECS
      # ECSのサービス更新
      # 　この orbs のコマンドはデフォルトのプロファイルを利用する前提
      - aws-ecs/update-service:
          family: << parameters.family >>
          cluster-name: << parameters.cluster-name >>
          service-name: << parameters.service-name >>
          container-image-name-updates: << parameters.container-image-name-updates >>
          container-env-var-updates: << parameters.container-env-var-updates >>

# ---------------------------------------------------------------------------------------------------
#
# workflows
#
# ---------------------------------------------------------------------------------------------------

workflows:
  deploy-to-ecs:

    jobs:

      # ECR デプロイ
      - ecr-build-and-push-image:
          name: build-ecr
          account-url: AWS_ECR_ACCOUNT_URL
          aws-access-key-id: ASSUME_ACCESS_KEY_ID
          aws-secret-access-key: ASSUME_SECRET_ACCESS_KEY
          dockerfile: 'Dockerfile-base'
          repo: api-ecr
          tag: myapi
          role-arn: ${DEPLOY_ROLE_ARN}
          filters:
            branches:
              only:
                - branch-a

      # ECS デプロイ
      - ecs-deploy-service-update:
          name: deploy-ecs
          aws-access-key-id: ASSUME_ACCESS_KEY_ID
          aws-secret-access-key: ASSUME_SECRET_ACCESS_KEY
          aws-region: AWS_REGION
          cluster-name: 'api-ecs'
          family: 'api-ecs'
          service-name: 'api-ecs-service'
          role-arn: ${DEPLOY_ROLE_ARN}
          requires:
            - build-ecr
          filters:
            branches:
              only:
                - branch-a
```

## 各コマンド、ジョブの説明

### コマンド「setup-assume-role-and-set-env-var」

自作したコマンドです。以下の２つの処理を行います。
①「デフォルトの」プロファイルとしてAssumeRoleしたものを設定する。
② 指定した環境変数にAssumeRoleした際のアクセスキー、シークレットキーを設定する。

#### コマンド処理内容と説明

```
      # 処理１
      - aws-cli/setup:
          aws-access-key-id: << parameters.aws-access-key-id >>
          aws-secret-access-key: << parameters.aws-secret-access-key >>
          aws-region: <<parameters.aws-region>>
          profile-name: base-profile
      # 処理２
      - run:
          name: Assume role and export environment variables
          command: >
            CREDENTIALS="$(aws sts assume-role --role-arn << parameters.role-arn >> --role-session-name S-<< parameters.aws-access-key-env-name >> --profile base-profile | jq '.Credentials')"

            echo "export << parameters.aws-access-key-env-name >>=$(echo $CREDENTIALS | jq -r '.AccessKeyId')" >> $BASH_ENV

            echo "export << parameters.aws-secret-access-env-name >>=$(echo $CREDENTIALS | jq -r '.SecretAccessKey')" >> $BASH_ENV

            echo "export << parameters.aws-session-token-env-name >>=$(echo $CREDENTIALS | jq -r '.SessionToken')" >> $BASH_ENV

            aws configure set aws_access_key_id "$(echo $CREDENTIALS | jq -r '.AccessKeyId')" --profile default

            aws configure set aws_secret_access_key "$(echo $CREDENTIALS | jq -r '.SecretAccessKey')" --profile default

            aws configure set aws_session_token "$(echo $CREDENTIALS | jq -r '.SessionToken')" --profile default

```


処理１では、AssumeRoleされるIAMユーザ（ほとんど権限が付与されていないIAMユーザ）のプロファイルを作成する。
この時、デフォルトでなく「base-profile」というデフォルト以外のプロファイルで作成する。
（この後、このプロファイルは利用しないので名前は何でもよい。）

処理２では、大きく３つの処理を行っている。
「CREDENTIALS="$(aws sts ～～」の行
→処理１で作成したプロファイルを利用し、AssumeRoleされたユーザのアクセスキーなどを取得する

「echo "export << parameters」の３行
→コマンドの引数で指定した環境変数にAssumeRoleされたユーザのアクセスキー、シークレットキー、セッショントークンを設定する。

「aws configure set」の３行
→AssumeRoleされたユーザのプロファイルを「デフォルトとして」設定する。
　また、セッショントークンも設定する。
　CircleCI 提供の orbs ではセッショントークンを設定できないようだった※ので、aws cliコマンドで実施している。
　※実装当時の話

処理２のうち、「環境変数に設定する」の処理がまるっきり無駄なように見えるが、これは後続の処理のために必要となる。


### ジョブ「ecr-build-and-push-image」

Dockerイメージを作成してECRにプッシュする処理。

#### ジョブ処理内容と説明

```
    steps:
      # 処理１
      - setup-assume-role-and-set-env-var:
          aws-access-key-id: << parameters.aws-access-key-id >>
          aws-secret-access-key: << parameters.aws-secret-access-key >>
          aws-region: << parameters.region >>
          role-arn:  << parameters.role-arn >>
          aws-access-key-env-name: ASSUME_ACCESS_ECR
          aws-secret-access-env-name: ASSUME_SECRET_ECR
          aws-session-token-env-name: ASSUME_TOKEN_ECR
      # 処理２
      - aws-ecr/build-and-push-image:
          account-url: << parameters.account-url >>
          aws-access-key-id: ASSUME_ACCESS_ECR
          aws-secret-access-key: ASSUME_SECRET_ECR
          region: << parameters.region >>
          dockerfile: << parameters.dockerfile >>
          extra-build-args: << parameters.extra-build-args >>
          repo: << parameters.repo >>
          tag: << parameters.tag >>
```

処理１では自作した AssumeRole の設定を行う。

処理２では Docker イメージの作成、ECRへのデプロイを CircleCI 提供の orbs を利用して行う。
ここで呼び出すCircleCI の orbs 処理を見ると、以下の問題がある。

* 引数で指定したアクセスキーでプロファイルを作成し、そのプロファイルで処理を行う。
* orbs内で作成したプロファイルはセッショントークンを設定できない。

これを無理やり回避するため、（すでに登録しているが）再度デフォルトのプロファイルとして処理１で取得した AssumeRole 後のアクセスキー、シークレットキーを指定する。
内部処理としては、デフォルトのプロファイルのアクセスキー、シークレットキーを同じ値で上書きするだけなので（無駄な処理ではあるが）問題は起こさない。
※orbs をそのまま利用したいため、無理やり実現した結果がこれである。


### ジョブ「ecs-deploy-service-update」

ECRに登録されているイメージでECSのサービスを更新する。

#### ジョブ処理内容と説明

```
    steps:
      # 処理１
      - setup-assume-role-and-set-env-var:
          aws-access-key-id: << parameters.aws-access-key-id >>
          aws-secret-access-key: << parameters.aws-secret-access-key >>
          aws-region: << parameters.aws-region >>
          role-arn:  << parameters.role-arn >>
          aws-access-key-env-name: ASSUME_ACCESS_ECS
          aws-secret-access-env-name: ASSUME_SECRET_ECS
          aws-session-token-env-name: ASSUME_TOKEN_ECS
      # 処理２
      - aws-ecs/update-service:
          family: << parameters.family >>
          cluster-name: << parameters.cluster-name >>
          service-name: << parameters.service-name >>
          container-image-name-updates: << parameters.container-image-name-updates >>
          container-env-var-updates: << parameters.container-env-var-updates >>
```

処理１では自作した AssumeRole の設定を行う。

処理２では ECR のDockerイメージを利用してECSのデプロイを行う。
ここで呼び出すCircleCI の orbs 処理を見ると、以下の問題がある。

* すでにプロファイル作成済として処理を行う。
* プロファイルを指定できるわけでなく、デフォルトプロファイルを利用する前提となっている。

しかし、処理１でデフォルトプロファイルで AssumeRole されたプロファイルを利用しているので問題なし。
環境変数の設定は完全に無駄になっていますが、放っておいてください。


## まとめ

CircleCI が提供している orbs の処理に依存していますが、これにて AssumeRole を用いたECR、ECSのデプロイが実行できます。
割と無理やりな実装方法なので、あまり推奨された方法ではないかと思います。

今後 CircleCI が提供している orbs が AssumeRole に対応しましたら、もちろんそちらを利用しましょう。