CDKを使ってGitHub Actions OpenID Connect (OIDC) で多段 AssumeRoleする

この記事は ハンズラボ [AdventCalendar2021](https://qiita.com/advent-calendar/2021/handslab)　3日目の記事です。

#はじめに
今回は、こちらの記事のCDK化(Python)を行っていきます。
構成やcfnテンプレートなど詳細はこちらをご参照ください。

https://qiita.com/daisukeArk/items/a985bf6d2f03e9bd0306

# 前提条件
- [GitHub Actions OpenID Connect (OIDC) で多段 AssumeRole する](https://qiita.com/daisukeArk/items/a985bf6d2f03e9bd0306)を読み構成を理解していること
- CDKがインストールされ実行環境が準備されていること

# 実装
ではさっそくやっていきます。

#### 初期設定
ディレクトリを作成し、`cdk init`を実行してプロジェクトを立ち上げます。

```terminal
$ mkdir github_oicd
$ cd github_oicd

$ cdk init app --language python
```

これで準備完了ですので、早速コーディングしていきます。

#### Provider作成
まずはProviderを作成していきます。
ここはドキュメント通りに値をセットするだけですので、簡単に実装できました。
ドキュメントは[こちら](https://docs.aws.amazon.com/cdk/api/latest/python/aws_cdk.aws_iam/OpenIdConnectProviderProps.html)

```python:github_oicd_stack.py
github_oidc_provider = iam.OpenIdConnectProvider(self,
    client_ids=["sts.amazonaws.com"],
    thumbprints=["a031c46782e6e6c662c2c87c76da9aa62ccabd8e"],
    id="GithubOicdProvider",
    url="https://token.actions.githubusercontent.com",
)

```

#### bastion_role（踏み台ロール)作成
ここから少し難易度が上がった印象です。
Principalに先ほど作成したProviderを設定する箇所でかなり苦戦しましたが、最後はドキュメントが助けてくれました。
（最初からドキュメントちゃんと読めという話ですが、、）

助けてくれたドキュメント達 ( [Role](https://docs.aws.amazon.com/cdk/api/latest/python/aws_cdk.aws_iam/Role.html),  [FederatedPrincipal](https://docs.aws.amazon.com/cdk/api/latest/python/aws_cdk.aws_iam/FederatedPrincipal.html), [PolicyDocument](https://docs.aws.amazon.com/cdk/api/latest/python/aws_cdk.aws_iam/PolicyDocument.html), [PolicyStatement](https://docs.aws.amazon.com/cdk/api/latest/python/aws_cdk.aws_iam/PolicyStatement.html#aws_cdk.aws_iam.PolicyStatement) )

```python:github_oicd_stack.py
GitHubOrgName = "example"
GitHubRepoName = "example"

conditions = {
    "StringEquals" : {
        "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
    },
    "StringLike" : {
        "token.actions.githubusercontent.com:sub" : f"repo:{GitHubOrgName}/{GitHubRepoName}:*"
    }
}

bastion_role = iam.Role(self,
    assumed_by=iam.FederatedPrincipal(
        github_oidc_provider.open_id_connect_provider_arn,
        conditions=conditions,
        assume_role_action="sts:AssumeRoleWithWebIdentity"
    ),
    path="/",
    inline_policies={
        "AssumeRolePolicy": iam.PolicyDocument(
            statements=[
                iam.PolicyStatement(
                    effect=iam.Effect.ALLOW,
                    actions=["sts:GetCallerIdentity"],
                    resources=['*']
                 ),
                 iam.PolicyStatement(
                     effect=iam.Effect.ALLOW,
                     actions=[
                         "sts:AssumeRole",
                         "sts:TagSession"
                      ],
                      resources=["*"]
                 )
             ]
        )
    },
    id="BastionRole",
    role_name="BastionRole",
)
```

#### deploy_role(デプロイ用ロール)作成
最後のロール作成です。
ここが一番難しかったです。
一度に複数のStatementを設定したかったのですが、想定している設定がなかなかできなかったので、最初に1つ設定して後付けしています。

新たに助けてくれたドキュメント達 ( [ArnPrincipal](https://docs.aws.amazon.com/cdk/api/latest/python/aws_cdk.aws_iam/ArnPrincipal.html), [PrincipalWithConditions](https://docs.aws.amazon.com/cdk/api/latest/python/aws_cdk.aws_iam/PrincipalWithConditions.html) )

```python:github_oicd_stack.py
ExternalId = "example1234"

aws_principal = iam.ArnPrincipal(
    arn=bastion_role.role_arn,
)

# add condition
aws_principal_with_condition = iam.PrincipalWithConditions(aws_principal, {
    "StringEquals": {
        "sts:ExternalId": ExternalId
    }
})

github_actions_deploy_role = iam.Role(self,
    assumed_by=aws_principal_with_condition,
    id="GitHubActionsDeployRole",
    path="/",
    role_name="GitHubActionsDeployRole",
)

# add policy
github_actions_deploy_role.assume_role_policy.add_statements(iam.PolicyStatement(
    actions=["sts:TagSession"],
    principals=[
        aws_principal.grant_principal,
    ],
))

```

この様に書くと以下の様なポリシーになります。
完璧です。

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::{AccoutID}:role/BastionRole"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "example1234"
        }
      }
    },
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::{AccoutID}:role/BastionRole"
      },
      "Action": "sts:TagSession"
    }
  ]
}
```


##完成系
```python:github_oicd_stack.py
from aws_cdk import (
    aws_iam as iam,
    core,
)


class AwsCdkOicdStack(core.Stack):

    def __init__(self, scope: core.Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        ExternalId = "example1234"
        GitHubOrgName = "example"
        GitHubRepoName = "example"

        github_oidc_provider = iam.OpenIdConnectProvider(self,
            client_ids=["sts.amazonaws.com"],
            thumbprints=["a031c46782e6e6c662c2c87c76da9aa62ccabd8e"],
            id="GithubOicdProvider",
            url="https://token.actions.githubusercontent.com",
        )

        conditions = {
            "StringEquals" : {
                "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
            },
            "StringLike" : {
                "token.actions.githubusercontent.com:sub" : f"repo:{GitHubOrgName}/{GitHubRepoName}:*"
            }
        }

        bastion_role = iam.Role(self,
            assumed_by=iam.FederatedPrincipal(
                github_oidc_provider.open_id_connect_provider_arn,
                conditions=conditions,
                assume_role_action="sts:AssumeRoleWithWebIdentity"
            ),
            path="/",
            inline_policies={
                "AssumeRolePolicy": iam.PolicyDocument(
                    statements=[
                        iam.PolicyStatement(
                            effect=iam.Effect.ALLOW,
                            actions=["sts:GetCallerIdentity"],
                            resources=['*']
                        ),
                        iam.PolicyStatement(
                            effect=iam.Effect.ALLOW,
                            actions=[
                                "sts:AssumeRole",
                                "sts:TagSession"
                            ],
                            resources=["*"]
                        )
                    ]
                )
            },
            id="BastionRole",
            role_name="BastionRole",
        )

        aws_principal = iam.ArnPrincipal(
            arn=bastion_role.role_arn,
        )

        # add condition
        aws_principal_with_condition = iam.PrincipalWithConditions(aws_principal, {
            "StringEquals": {
                "sts:ExternalId": ExternalId
            }
        })

        github_actions_deploy_role = iam.Role(self,
            assumed_by=aws_principal_with_condition,
            id="GitHubActionsDeployRole",
            path="/",
            role_name="GitHubActionsDeployRole",
        )

        # add policy
        github_actions_deploy_role.assume_role_policy.add_statements(iam.PolicyStatement(
            actions=["sts:TagSession"],
            principals=[
                aws_principal.grant_principal,
            ],
        ))

```

# デプロイ　& 動作確認
では、実際にデプロイして動作確認してみます。
Github Actions Workflowは[こちら](https://qiita.com/daisukeArk/items/a985bf6d2f03e9bd0306#github-actions-workflow-%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB)を参照してください。

```terminal
$ cdk deploy
```

無事、それぞれのroleでAWSアカウント情報を取得することができました。
![スクリーンショット_2021-12-02_12_48_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481706/d1e47df5-e164-b6cd-4c25-773eba8ac6a1.png)


# 所感
個人的にはcfnよりもコーディングが楽しかったです。
今回初めてCDKを使って構築してみましたが、普段使用している言語だったためか、かなりスムーズに構築できたと思います。
cfnを初めて触った時はひどいもんでした。

みなさんも気分転換に一度CDKで構築してみてください。楽しいはずです！
以上！