はじめに
私は業務で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:
#
# 以下の2つの処理を行う
# ・指定された環境変数に 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」
自作したコマンドです。以下の2つの処理を行います。
①「デフォルトの」プロファイルとしてAssumeRoleしたものを設定する。
② 指定した環境変数にAssumeRoleした際のアクセスキー、シークレットキーを設定する。
コマンド処理内容と説明
# 処理1
- 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
# 処理2
- 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
処理1では、AssumeRoleされるIAMユーザ(ほとんど権限が付与されていないIAMユーザ)のプロファイルを作成する。
この時、デフォルトでなく「base-profile」というデフォルト以外のプロファイルで作成する。
(この後、このプロファイルは利用しないので名前は何でもよい。)
処理2では、大きく3つの処理を行っている。
「CREDENTIALS="$(aws sts ~~」の行
→処理1で作成したプロファイルを利用し、AssumeRoleされたユーザのアクセスキーなどを取得する
「echo "export << parameters」の3行
→コマンドの引数で指定した環境変数にAssumeRoleされたユーザのアクセスキー、シークレットキー、セッショントークンを設定する。
「aws configure set」の3行
→AssumeRoleされたユーザのプロファイルを「デフォルトとして」設定する。
また、セッショントークンも設定する。
CircleCI 提供の orbs ではセッショントークンを設定できないようだった※ので、aws cliコマンドで実施している。
※実装当時の話
処理2のうち、「環境変数に設定する」の処理がまるっきり無駄なように見えるが、これは後続の処理のために必要となる。
ジョブ「ecr-build-and-push-image」
Dockerイメージを作成してECRにプッシュする処理。
ジョブ処理内容と説明
steps:
# 処理1
- 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
# 処理2
- 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 >>
処理1では自作した AssumeRole の設定を行う。
処理2では Docker イメージの作成、ECRへのデプロイを CircleCI 提供の orbs を利用して行う。
ここで呼び出すCircleCI の orbs 処理を見ると、以下の問題がある。
- 引数で指定したアクセスキーでプロファイルを作成し、そのプロファイルで処理を行う。
- orbs内で作成したプロファイルはセッショントークンを設定できない。
これを無理やり回避するため、(すでに登録しているが)再度デフォルトのプロファイルとして処理1で取得した AssumeRole 後のアクセスキー、シークレットキーを指定する。
内部処理としては、デフォルトのプロファイルのアクセスキー、シークレットキーを同じ値で上書きするだけなので(無駄な処理ではあるが)問題は起こさない。
※orbs をそのまま利用したいため、無理やり実現した結果がこれである。
ジョブ「ecs-deploy-service-update」
ECRに登録されているイメージでECSのサービスを更新する。
ジョブ処理内容と説明
steps:
# 処理1
- 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
# 処理2
- 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 >>
処理1では自作した AssumeRole の設定を行う。
処理2では ECR のDockerイメージを利用してECSのデプロイを行う。
ここで呼び出すCircleCI の orbs 処理を見ると、以下の問題がある。
- すでにプロファイル作成済として処理を行う。
- プロファイルを指定できるわけでなく、デフォルトプロファイルを利用する前提となっている。
しかし、処理1でデフォルトプロファイルで AssumeRole されたプロファイルを利用しているので問題なし。
環境変数の設定は完全に無駄になっていますが、放っておいてください。
まとめ
CircleCI が提供している orbs の処理に依存していますが、これにて AssumeRole を用いたECR、ECSのデプロイが実行できます。
割と無理やりな実装方法なので、あまり推奨された方法ではないかと思います。
今後 CircleCI が提供している orbs が AssumeRole に対応しましたら、もちろんそちらを利用しましょう。