😊

CI/CD 用に assume-role するコマンド arar を作りました

2023/10/01に公開

arar の概要

「assume-roleしてコマンドを実行する」というだけのコマンド arar: Assume Role And Run を作りました。

例えば、以下のように Github Actions で利用する想定です:

name: ci
on:
  push:
jobs:
  check:
    name: terraform plan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: install arar
        run: |
          curl -sL -o arar https://github.com/ikedam/arar/releases/download/v0.0.1/arar_0.0.1_linux_amd64
          chmod 755 arar
      - uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: 1.5.7
      - name: terraform plan
        run: |
          terraform init
          ./arar -u -- terraform plan
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_ROLE_ARN: arn:aws:iam::xxxxxxxxxxxx:role/terraform-role
          AWS_DEFAULT_REGION: ap-northeast-1

というのも…

背景: AWSCLI/AWS SDK では環境変数で認証して assume-role を行えない

もっと正確には、「AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY 環境変数を使用して、 さらにプロファイルの指定を AWS_PROFILE 環境変数で行って assume-role を行った認証情報を利用することができない」です。

以下のようなシチュエーションを考えます:

  • CI/CD で、 aws のリソースにアクセスする。例えば terraform を実行するとか。
  • 該当の操作のために、IAM ロール iam-example-role を作成してある。
  • CI/CD では CI/CD 用の IAM ユーザー iam-example-user を作成してあり、そのユーザーから assume-role をして処理を行う。
  • CI/CD では認証のための秘密情報を環境変数 AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY として引き渡す。

これはこんな感じに簡単に実現できる気がします:

  1. aws_config ファイルを作成する (参考: AWS CLI で IAM ロールを使用する - AWS Command Line Interface):

    cat >./aws_config <<EOF
    [profile cicd]
    role_arn = arn:aws:iam::xxxxxxxxxxxx:role/iam-example-role
    credential_source = Environment
    EOF
    
  2. CI/CD でコマンドを実行する:

    AWS_CONFIG_FILE=./aws_config AWS_PROFILE=cicd terraform plan
    

ところがこれは期待通りに動作しません。このとき terraform は iam-example-role の権限では実行されず、 AWS_ACCESS_KEY_ID の持ち主である iam-example-user の権限で実行されます。

これは terraform に限らない、 awscli、 AWS SDK 共通の仕様です。

なお assume-role をした認証情報を取得するためには、プロファイルの指定が必須です。
ところが AWS_ACCESS_KEY_ID と AWS_PROFILE を指定してコマンドを実行すると、 assume-role した認証情報ではなく、 AWS_ACCESS_KEY_ID の認証情報でコマンドが実行されてしまいます。
CI/CD では認証関連の秘密情報を環境変数として引き渡すことを考えると、これはかなり困った仕様です。

この動作を、もう少し順を追って見ていきます。

おさらい: assume-role して awscli を実行する

以降では、AWS で認証を行うコマンドの代表として awscli に絞って記載していきます。
一般的に awscli と AWS SDK との動作は微妙に異なる部分がありますが、今回の assume-role の挙動については動作は同じです。

iam-example-user から iam-example-role に assume role をして awscli を実行する場合の手順は以下の通りになります:

  1. ~/.aws/credentials に、 iam-example-user のアクセスキー、アクセスシークレットを記載する。

    [iam-example-user]
    aws_access_key_id = xxxxxxxxx
    aws_secret_access_key = xxxxxxxxxx
    
  2. iam-example-user で認証できることを確認。

    $ aws sts get-caller-identity
    {
        "UserId": "xxxxxxxxxxxxxxxxxxxxx",
        "Account": "xxxxxxxxxxxx",
        "Arn": "arn:aws:iam::xxxxxxxxxxxx:user/iam-example-user"
    }
    
  3. ~/.aws/config に以下のようにプロファイルを指定する。

    [profile iam-example-role]
    role_arn = arn:aws:iam::xxxxxxxxxxxx:role/iam-example-role
    source_profile = iam-example-user
    
  4. iam-example-role で認証できることを確認。

    $ AWS_PROFILE=iam-example-role aws sts get-caller-identity
    {
        "UserId": "xxxxxxxxxxxxxxxxxxxxx:botocore-session-1696125397",
        "Account": "xxxxxxxxxxxx",
        "Arn": "arn:aws:sts::xxxxxxxxxxxx:assumed-role/iam-example-role/botocore-session-1696125397"
    }
    

設定ファイルを書くのが面倒ではありますが、特に気になる点もなく、簡単明快です。

認証情報を環境変数で与えて assume-role する

前項では、 ~/.aws/credentials にアクセスキーIDとシークレットアクセスキーを書きましたが、 CI/CD では多くの場合、秘密情報は環境変数で渡ってくるのが一般的です。

awscli も ~/.aws/credentials の代わりに AWS_ACCESS_KEY_ID , AWS_SECRET_ACCESS_KEY で認証を行うことができるので、前項の assume-role の設定をちょっと変えて、環境変数で認証するようにしてみましょう。

前項では assume-role するプロファイルで使用する認証情報を source_profile で指定して ~/.aws/credentials 内のセクションを参照していましたが、今回は ~/.aws/credentials を使わない前提ですので、ここの書き換えが必要そうです。

AWS CLI で IAM ロールを使用する - AWS Command Line Interface によると、 source_profile の代わりに credential_source という項目があること、環境変数からであれば Environment を指定すれば良いとあります。

したがって ~/.aws/config は以下の通りで良さそうです:

[profile iam-example-role]
role_arn = arn:aws:iam::xxxxxxxxxxxx:role/iam-example-role
credential_source = Environment

環境変数で認証情報を与え、 get-caller-identity を実行してみると…

$ AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxx \
> AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
> AWS_PROFILE=iam-example-role aws sts get-caller-identity
{
    "UserId": "xxxxxxxxxxxxx",
    "Account": "xxxxxxxxxxxx",
    "Arn": "arn:aws:iam::xxxxxxxxxxxx:user/iam-example-user"
}

上記の通り、 iam-example-role ではなく、iam-example-user の権限になっていることがわかります。
つまり、 assume-role ができていません。

なぜ環境変数を使うと assume-role できないのか?

前項には多少フェアでない部分があり、以下のように実行するときちんと assume-role を行えます。

$ AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxx \
> AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
> aws --profile=iam-example-role sts get-caller-identity
{
    "UserId": "xxxxxxxxxxxxxxxxxxxxx:botocore-session-1696125397",
    "Account": "xxxxxxxxxxxx",
    "Arn": "arn:aws:sts::xxxxxxxxxxxx:assumed-role/iam-example-role/botocore-session-1696125397"
}

相違がわかりにくいですが、プロファイル名を AWS_PROFILE 環境変数ではなく、 --profile コマンドラインオプションで引き渡しています。

実はこれが環境変数では assume-role できないことの説明にもなっています。

認証とアクセス認証情報 - AWS Command Line Interface#設定と認証情報の優先順位 で、認証情報の参照方法について以下のように記載されています:

  1. コマンドラインオプション
  2. 環境変数

実際にはこれが完全な説明になっているわけでもないですが、おそらく awscli / AWS SDK での設定の参照の優先順位として、結果として以下のようになっているものと思われます。

  1. --profile コマンドラインオプション
  2. AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY による認証情報
  3. AWS_PROFILE によるプロファイル指定

このため、 AWS_PROFILE 環境変数でプロファイルを指定しても、先に AWS_ACCESS_KEY_ID による認証情報が利用され、ユーザーとして認証されてしまうのです。

回避方法の案

たたかいのきろく

AWS_PROFILE 環境変数を使わない道

じゃあ、 AWS_PROFILE 環境変数を使わずに --profile オプションを使えばいいんじゃない?となると、そうもいきません。

例えば terraform には --profile に相当するコマンドラインオプションがありません。
代わりに設定ファイルに profile を書くことができるのですが、 私はプロファイルはユーザー設定の一部だと考えており、コードにハードコードするべきものではないと考えており、適切ではない方法と思っています。

ソースコード内に /home/ikedam/... といった個人環境依存のものを書くのと同じくらいの暴挙と考えています。

デフォルトプロファイルで assume-role の指定を行えばいいのでは

試したのですが、どうもプロファイルはなんであっても明示的に指定する必要があるような様子です。

$ cat ~/.aws/config
[default]
role_arn = arn:aws:iam::xxxxxxxxxxxx:role/iam-example-role
credential_source = Environment

[profile default]
role_arn = arn:aws:iam::xxxxxxxxxxxx:role/iam-example-role
credential_source = Environment

$ AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxx \
> AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
> aws sts get-caller-identity
{
    "UserId": "xxxxxxxxxxxxx",
    "Account": "xxxxxxxxxxxx",
    "Arn": "arn:aws:iam::xxxxxxxxxxxx:user/iam-example-user"
}

--profile=default を指定すると assume-role します。

$ AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxx \
> AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
> aws --profile=default sts get-caller-identity
{
    "UserId": "xxxxxxxxxxxxxxxxxxxxx:botocore-session-1696125397",
    "Account": "xxxxxxxxxxxx",
    "Arn": "arn:aws:sts::xxxxxxxxxxxx:assumed-role/iam-example-role/botocore-session-1696125397"
}

アプリケーション側で assume-role の指定をすればよいのでは

例えば Terraform であれば、テンプレート内で assume_role ブロックを使って assume-role するロールを指定することができます。
Docs overview | hashicorp/aws | Terraform | Terraform Registry#assume_role Configuration Block

ところが、私の環境ではセッション名で問題がおきました。
セキュリティ・運用的な都合で、「assume-role するときのセッション名を IAM ユーザー名にすることを強制する」という運用はしばしば行われます (参考: [アップデート] IAMロールセッション名にユーザー名を強制できる条件 sts:RoleSessionName が使えるようになりました | DevelopersIO)。

assume_role ブロックでは、セッション名は SDK が自動生成するものか、もしくは固定値のどちらかにすることしかできず、複数のユーザーでの運用に支障があり、見送りとなりました。

あと、provider と backend の両方でロールの指定が必要なのもつらい点です。

正しい解決方法

いかにも awscli / AWS SDK に仕様的な不備があるといった内容になっていますが、AWS 的な正しいソリューションは以下になると思われます:

  • Web Identity (OpenID Connect) を使用する。
  • EC2 インスタンスなどにアタッチしたロール情報を使用する。
    • そこから更に assume-role してもよい。

特に、最近 AWS はアクセスキーID、シークレットアクセスキーを使わないようにする設計・運用を推奨しており、アクセスキーID、シークレットアクセスキーを使わないといけない状況自体を疑ってみるべきです。
そうはいっても使わざるを得ない状況、上記のソリューションが採用できない環境はいくらでもあるものです。

arar のおまけの機能

ロール名・セッション名を環境変数で指定する

そもそも、 assume-role するのにいちいち ~/.aws/config を書かないといけない、というのが面倒くさいのです。
CI/CD 環境だとホームディレクトリのファイルを管理するのは避けたいものです。
AWS_CONFIG_FILE 環境変数でファイルの場所を指定できますが、それにしても面倒くさい。

role_arn などの代わりになりそうな AWS_ROLE_ARN 環境変数AWS_ROLE_SESSION_NAME 環境変数 が存在するようなのですが、どうやら Web Identity 専用の環境変数で、通常の assume-role には使えません。

arar ではこれらの環境変数を使えるようにしています:

$ AWS_ACCESS_KEY_ID=xxx \
> AWS_SECRET_ACCESS_KEY=xxx \
> AWS_REGION=us-east-1 \
> AWS_ROLE_ARN=arn:aws:iam::xxxxxxxxxx:role/iam-example-role \
> AWS_ROLE_SESSION_NAME=iam-example-user \
> arar -- aws sts get-caller-identity
{
    "UserId": "xxxxxxxxxxxxxxxxxxxxx:iam-example-user",
    "Account": "xxxxxxxxxxxx",
    "Arn": "arn:aws:sts::xxxxxxxxxxxx:assumed-role/iam-example-role/iam-example-user"
}

セッション名を自動的にIAMユーザー名にする

セキュリティ・運用的な都合で、「assume-role するときのセッション名を IAM ユーザー名にすることを強制する」という運用はしばしば行われます (参考: [アップデート] IAMロールセッション名にユーザー名を強制できる条件 sts:RoleSessionName が使えるようになりました | DevelopersIO)。

~/.aws/configrole_session_name を指定するのですが、こんなの自動的に設定してほしいよなと思う機会がしばしばあります。

arar では -u/--username-session でサポートしています:

$ AWS_ACCESS_KEY_ID=xxx \
> AWS_SECRET_ACCESS_KEY=xxx \
> AWS_REGION=us-east-1 \
> AWS_ROLE_ARN=arn:aws:iam::xxxxxxxxxx:role/iam-example-role \
> arar -u -- aws sts get-caller-identity
{
    "UserId": "xxxxxxxxxxxxxxxxxxxxx:iam-example-user",
    "Account": "xxxxxxxxxxxx",
    "Arn": "arn:aws:sts::xxxxxxxxxxxx:assumed-role/iam-example-role/iam-example-user"
}

Discussion