AnsibleでAssumeRoleを行いbotoによるMFA入力を回避する

渡辺です。

複数のAWSアカウントに対し、IAMロールを管理しようとして色々ハマりました・・・。 管理方法として、Ansible、Terraform、CloudFormationと検討しましたが、最終的にAnsibleに落ち着きました。

前提として次のような制約があります。

  • メインアカウントのみMFAを有効にしたIAMユーザを作成し、アクセスキー/シークレットキーは作成する
  • 管理対象のAWSアカウントのアクセスキー/シークレットキーは作成しない
  • 設定ファイルなどで構成管理を行う

なお、構成管理を行うには、Ansible以外に、CloudFormationやTerraformなどが選択肢としてあります。

iam_role モジュールとwith_items

Ansibleでは、AWS関連モジュールも多く提供されています。 はじめに、 iam_roleモジュールと、繰り返し処理を行う with_items を利用してPlaybookを作成してみました。

    - name: "IAM Roles"
      iam_role:
        name: "{{ item.name }}"
        assume_role_policy_document: 
          Version: '2012-10-17'
          Statement:
            - Action: sts:AssumeRole
              Effect: Allow
              Principal:
                AWS: "arn:aws:iam::{{item.aws_account_id}}:root"
              Condition:
                Bool:
                  "aws:MultiFactorAuthPresent": "true"
        managed_policy:
          - "AdministratorAccess"
        profile: "xxx"
      with_items: "{{ iam_roles }}"
iam_roles:
  - name: "role-1"
    aws_account_id: "XXXXXXXXXXXX"
  - name: "role-2"
    aws_account_id: "XXXXXXXXXXXX"

iam_roleモジュールでは、profileにクレデンシャル情報に定義したプロファイル名を指定できます。

Ansibleの実装で利用しているboto3はprofileやAssumeRoleに対応しています。 次のように適切な設定が行われていれば、上手く動きます。

[default]
aws_access_key_id = アクセスキーID
aws_secret_access_key = シークレットアクセスキー
[profile xxxx]
role_arn = arn:aws:iam::【対象AWSアカウントID】:role/yyyy
mfa_serial = arn:aws:iam::【主AWSアカウントID】:mfa/zzzz
source_profile = default

しかし、実行してみるとループ毎にMFAトークンの入力を求められてしまいます。 おそらく、モジュール毎に新しいプロセスが起動するため、AssumeRoleを行った情報が引き継がれないのでしょう。 これでは、使い物になりません・・・。

sts_assume_roleでAssuemRoleを行う

iam_roleモジュールで、profileを指定した場合、クレデンシャル情報を元にPythonのbotoでAssumeRoleが行われます。 通常はこの仕組みを利用すれば良いでしょう。 しかし、今回のようにMFAを有効化している場合は、sts_assume_roleモジュールでAssumeRoleを行うと上手くいきます。

    - name: "Assume Role"
      sts_assume_role:
        role_arn: "{{ role_arn }}"
        role_session_name: "ansible-session"
        mfa_serial_number: "{{ mfa_serial }}"
        mfa_token: "{{ mfa_token }}"
        region: "ap-northeast-1"
      register: assumed_role
      changed_when: False
    - debug:
        var: assumed_role
        verbosity: 1

sts_assume_roleモジュールでは、role_arnを指定し、AssumeRoleを行います。 変数は、group_varsなどに設定しておきましょう。

MFAを有効にしている場合は、mfa_serial_numbermfa_tokenが必要です。 mfa_tokenは実行時のトークンを指定するため、コマンドライン引数で指定します。

$ ansible-playbook iam-role.yml --extra-vars="mfa_token=000000"

AssumeRoleを行った時の情報は、変数assumed_roleに登録されます。

TASK [debug] ***************************************************************************************
ok: [localhost] => {
    "assumed_role": {
        "changed": false, 
        "failed": false, 
        "sts_creds": {
            "access_key": "XXXXXXXXXXXXXXXXXXXXXX", 
            "expiration": "2018-02-07T08:02:10Z", 
            "parent": null, 
            "request_id": null, 
            "secret_key": "zzzzzzzzzzzzzzzzzzzzzzzzz", 
            "session_token": "xxxxxxxxxxxxxxxxxxxx=="
        }, 
        "sts_user": {
            "arn": "arn:aws:sts::XXXXXXXXXXXX:assumed-role/zzzzzzzzzzz/ansible-session", 
            "assume_role_id": "HHHHHHHHHHHHHHHH:ansible-session"
        }
    }
}

AssuemRoleした情報をiam_roleモジュールに設定する

AssumeRoleを行うと、一時的なクレデンシャル情報が発行されます。 すなわち、access_keysecret_keysession_tokenです。 これらをiam_roleモジュールに設定してください。

    - name: "IAM Roles"
      iam_role:
        name: "{{ item.name }}"
        assume_role_policy_document: 
          Version: '2012-10-17'
          Statement:
            - Action: sts:AssumeRole
              Effect: Allow
              Principal:
                AWS: "arn:aws:iam::{{item.aws_account_id}}:root"
              Condition:
                Bool:
                  "aws:MultiFactorAuthPresent": "true"
        managed_policy:
          - "AdministratorAccess"
        aws_access_key: "{{ assumed_role.sts_creds.access_key }}"
        aws_secret_key: "{{ assumed_role.sts_creds.secret_key }}"
        security_token: "{{ assumed_role.sts_creds.session_token }}"
      with_items: "{{ iam_roles }}"

これでコマンド実行時に1回だけMFAトークンを指定すれば、一時的なクレデンシャル情報を再利用してモジュールを実行可能です。

まとめ

AssumeRoleやスイッチロールは、複数のAWSアカウントを運用する上で便利な機能です。 ツールでは、AssumeRoleを意識せずに利用できることが多いと思います。 しかし、MFAが有効な場合、ツールなどが利用する時に、ハマりやすいポイントとなります。

そのような場合は、AssumeRoleの仕組みを思い出してください。 一時的なクレデンシャル情報を取得するのがAssumeRoleです。 単発でAssumeRoleを行い、access_keysecret_keysession_tokenを取得できれば、ツールで上手く利用できるかもしれません。

最後にリファクタリングしたIAM Roleを管理するPlaybookを置いておきます。 MFAのシリアルはiam_mfa_device_factsモジュールで取得できます。

---
- hosts: localhost
  connection: local
  gather_facts: False
  become: False
  tasks:
    - iam_mfa_device_facts:
      register: mfa_devices
      changed_when: False
    - debug:
        var: mfa_devices
        verbosity: 1
    - name: "Assume Role"
      sts_assume_role:
        role_arn: "arn:aws:iam::{{ aws_account_id }}:role/{{ user_name }}"
        role_session_name: "ansible-session"
        mfa_serial_number: "{{ mfa_devices.mfa_devices[0].serial_number }}"
        mfa_token: "{{ mfa_token }}"
        region: "ap-northeast-1"
      register: assumed_role
      changed_when: False
    - debug:
        var: assumed_role
        verbosity: 1
    - name: "IAM Roles"
      iam_role:
        name: "{{ item.name }}"
        state: "{{ item.state }}"
        assume_role_policy_document: 
          Version: '2012-10-17'
          Statement:
            - Action: sts:AssumeRole
              Effect: Allow
              Principal:
                AWS: "arn:aws:iam::{{item.aws_account_id}}:root"
              Condition:
                Bool:
                  "aws:MultiFactorAuthPresent": "true"
        managed_policy:
          - "AdministratorAccess"
        aws_access_key: "{{ assumed_role.sts_creds.access_key }}"
        aws_secret_key: "{{ assumed_role.sts_creds.secret_key }}"
        security_token: "{{ assumed_role.sts_creds.session_token }}"
      with_items: "{{ iam_roles }}"
---
aws_account_id: "XXXXXXXXXXXX"
user_name: "role-name-when-assume-role"
iam_roles:
  - name: "role-1"
    aws_account_id: "XXXXXXXXXXXX"
    state: present
  - name: "role-2"
    aws_account_id: "XXXXXXXXXXXX"
    state: present
  - name: "role-3"
    aws_account_id: "XXXXXXXXXXXX"
    state: absent