GitHub ActionsでDockerfile内からアクセスキーなしでAWS S3を操作する

こんにちは。 エキサイトの宮西です。

Advent Calendarの季節が今年もやってきました。
昨年同様、エキサイトホールディングス Advent Calendarで毎日投稿される予定です。お楽しみにー。

qiita.com

第一回目は、AWSのIAMロール(AssumeRole)のTipsを書いていこうと思います。

IAMユーザを使わずに外部からAWSリソースへアクセス

昨年からOIDCによる認証が使えるようになって、IAMユーザを作成せずとも外部からAWSリソースを操作できるようになりました。
クレデンシャルを発行してそれを保持しておく必要もないので、よりセキュアにもなったわけです。
私の所属するチームではCI/CDでGitHub Actionsを活用しており、その恩恵を存分に受けています。

そんなあるとき、GitHub Actionsのワークフロー内でDockerランタイムにクレデンシャルを渡す、という処理が必要になりまして。
実現するための解決策はいくつかあるのですが、今回はAssumeRoleを利用する例を紹介します。

IAMロールの準備

用意するIAMロールは以下の2つです。

  • arn:aws:iam::111111111111:role/github-oidc-role
    • ワークフロー内でAWSリソースを操作する(ECRへのPushなどデプロイ関連のアクション)ためのロール
  • arn:aws:iam::111111111111:role/upload-to-s3-role
    • 特定のS3バケットへのPutObjectを許可する、Dockerランタイムに渡したいロール

処理内容は、github-oidc-roleupload-to-s3-role をAssumeRoleする、です。
なるほど?

AssumeRoleといえば界隈では仮面や帽子などで例えられることで有名ですが、ざっくり言うと「指定したIAMロールを引き受けるアクション」です。
もう少し具体的に言えば、AssumeRoleするとそのIAMロールの一時的なクレデンシャルを返してくれる、といったところでしょうか。

イマイチよくわかりませんが、一時的なクレデンシャルを発行する・権限を引き受ける、ということはわかったので早速やってみようと思います。

upload-to-s3-roleの信頼されたエンティティ

upload-to-s3-role の許可ポリシーは単純なので省略(S3のPutObjectを許可するだけ)しますが、重要なのは 信頼されたエンティティ
以下のように、Principalを github-oidc-role にしてAssumeRoleできるように設定します。

{
    "Version": "2008-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::111111111111:role/github-oidc-role"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

github-oidc-roleの許可ポリシー

github-oidc-role 側は upload-to-s3-role をAssumeRoleできるように許可ポリシーを追加しておきます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "sts:AssumeRole"
            ],
            "Resource": [
                "arn:aws:iam::111111111111:role/upload-to-s3-role"
            ],
            "Effect": "Allow"
        }
    ]
}

これで、github-oidc-roleupload-to-s3-role をAssumeRoleして権限を引き受けることができるようになりました。

ワークフロー

処理の順序としては、

  1. OIDCで認証して、github-oidc-role をAssumeRoleする
  2. 1のクレデンシャルを使って、ECRにログインする
  3. 1のクレデンシャルを使って、upload-to-s3-role をAssumeRoleする
  4. 3のクレデンシャルをDockerランタイムに渡して、イメージをビルドする
  5. 1のクレデンシャルを使って、ビルドしたイメージをECRにプッシュする

といった感じです。実際のワークフローに書き起こすと↓のようになるかと思います。

- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v1
  with:
    role-to-assume: arn:aws:iam::111111111111:role/github-oidc-role
    aws-region: ap-northeast-1

- name: Login to Amazon ECR
  id: login-ecr
  uses: aws-actions/amazon-ecr-login@v1

- name: Get credentials
  run: |
    TEMP_ROLE=$(aws sts assume-role --role-arn arn:aws:iam::111111111111:role/upload-to-s3-role --role-session-name for-build)
    echo "${TEMP_ROLE}" | jq -r '.Credentials' > credentials.json

- name: Build, tag, and push image to Amazon ECR
  uses: docker/build-push-action@v3
  with:
    push: true
    tags: |
      ${{ steps.login-ecr.outputs.registry }}/image-name:latest
    secret-files: credentials=./credentials.json

なお、--build-arg でDockerランタイムにクレデンシャルを渡してしまうと docker history で内容が丸見えになってしまいます。
一時的なクレデンシャルと言えども、ベストプラクティス通りに --secret で渡すようにしましょう。
--secret で渡した場合、DockerfileではRUN命令文にオプションを付けてファイルをマウントするようにします。

FROM node:18-alpine

WORKDIR /app

COPY . .

RUN --mount=type=secret,id=credentials npm ci && node index.js

(以下略)

node index.js で実行されるコードは↓のような感じ。
デフォルトでは /run/secrets/ 配下にファイルがマウントされているので、それを読み込んで後続処理を進めていきます。

const data = fs.readFileSync('/run/secrets/credentials.json')
const credentials = JSON.parse(data)
const s3 = new AWS.S3({
    apiVersion: '2006-03-01',
    accessKeyId: credentials.AccessKeyId,
    secretAccessKey: credentials.SecretAccessKey,
    sessionToken: credentials.SessionToken,
    region: 'ap-northeast-1',
})

(以下略)

まとめ

「いやいや、そんな面倒臭いことしなくても、OIDC用のロール1つで全部処理しちゃえばいいじゃん」という声が聞こえてきそう。
要するに、ワークフローの aws-actions/configure-aws-credentials で発行されたクレデンシャルをDockerランタイムに渡す、ということです。
クレデンシャルは環境変数にセットされているので、それを使うことができるわけです。
そうなのですが、ユースケースによってはOIDC用のロールにポリシーを追加できない、ということもあるかと思います。

また、IAMのベストプラクティスには以下のような記述もあったりします。

IAM ポリシーでアクセス許可を設定するときは、タスクの実行に必要なアクセス許可のみを付与します。これを行うには、特定の条件下で特定のリソースに対して実行できるアクションを定義します。これは、最小特権アクセス許可とも呼ばれています。 https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/best-practices.html

目的が異なるものを一緒くたにしてしまうのは思わぬ事故を招いてしまいます。「最小権限の原則」を意識したIAM管理を心掛けていきたいです。
AssumeRoleを使いこなしてセキュアなシステムを構築していきましょう。

最後に、検証に付き合っていただいた師匠のN氏、ありがとうございましたー。