CI/CD でのキーレスな Cloud サービスデプロイを実現する仕組みとは？

# 導入
CI/CD において Cloud サービスのリソースを作成・変更する際に、 サービスアカウントキー/IAM アクセスキー/アカウントキー等のキーを使わない仕組みにすることが当たり前になってきたように思う。
例えば Github でいうと下記のような GitHub Actions がそれぞれの Cloud サービスにおいて提供されており、キーレスなデプロイが可能になっている。

| Cloud サービス | Github Actions | 関連する機能 |
|:-:|:-:|:-:|
| AWS  | [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials)  | [AssumeRole](https://docs.aws.amazon.com/sdkref/latest/guide/access-assume-role-web.html)  |
| Google Cloud  | [oogle-github-actions/auth](https://github.com/google-github-actions/auth)  |  [Google Cloud Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation?hl=ja) |
| Azure  | [azure/login](https://github.com/Azure/login)  | [Entra Workload Identity Federation](https://learn.microsoft.com/ja-jp/entra/workload-id/workload-identity-federation)  |

こらは全て Oauth 2.0 の拡張仕様をベースとした機能であり、仕様と各社の実装の理解を深め使いこなせるようにしたい。

# キーレスを実現する仕様
## Oauth 2.0 Token Exchange 
キーレスの仕組みは Oauth 2.0 Token Exchange と呼ばれ、[RFC 8693](https://www.rfc-editor.org/rfc/rfc8693.html) に定義されている。
2020年1月に Proposed Standard として公開された新しめの仕様であり、ユーザに代わってシステムが別のバックエンドサービスを呼び出すことを許可するユースケースを想定したものになっている。

主にセキュリティトークンサービス(STS)、すなわち提供されたセキュリティトークンを検証して、それに応じて新しいセキュリティトークンを発行するサービスのインターフェース定義を対象にしている。
他の Oauth 2.0 仕様と同様にクライアントは HTTP・ JSON パーサーのみを要求されるかなりシンプルなもので、面白いポイントとしてはOAuth クライアントに限定されない旨が記載されていることである。
つまり、仕様としては何らかのセキュリティトークン(SAML トークンなども含む)を扱うクライアントであれば何であってもトークンの交換が可能なように定められている。

逆にそれ以外は定義されておらず、例えばセキュリティトークン提供者との信頼関係の結び方、STSでのトークン検証方法、新しいトークンを発行する元のアカウント定義方法などは実装者が決める必要があるということだと思われる。

仕様にはセキュリティトークンを交換する上でキーとなる考え方が定義されており、それが「委任と偽装」である。

## 委譲と偽装
委譲と偽装は複数のバックエンドサービス/リソースサーバーがある場合にユーザを介さずにアクセスを実現する２つの手段である。

委任は別のバックエンドサービスにアクセスする際にアクセス元のサービスが持つIDに委任して、別のバックエンドサービスへのアクセス時の主体(subject)はアクセス元のサービスになる。
![delegation.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269590/9b982142-6db0-abd0-5744-426256b9a4b1.png)

偽装は別のバックエンドサービスにアクセスする際にアクセス元のサービスがトークン交換を行うが、別のバックエンドサービスへのアクセス時の主体(subject)はユーザのままである。
![impersonation.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269590/bb0cc167-8c37-9aeb-a0aa-a7ee40695f07.png)

# 各 Cloud サービスごとの設定方法

GitHub と各 Cloud の連携においては偽装を使って実現している。
GitHub で CI/CD 実行する際の具体的な利用方法について、各 Cloud サービスごとに設定手順・内容を紹介する。

## AWS

AWS 側では AssumeRole の仕組みを利用し、 連携用の設定を追加した IAM Role を設定する。
まず、OIDC Provider の設定を追加する。
```bash
aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com
```

その上で、作成した OIDC Provider に対する下記のような IAM Role を作成する。
```bash
ACCOUNT_ID=<AWS アカウントID>
ROLE_NAME=<IAM ロール名>
GITHUB_ORG=<GitHub組織名>
GITHUB_REPO=<GitHubリポジトリ名>

POLICY="{
    \"Version\": \"2012-10-17\",
    \"Statement\": [
        {
            \"Effect\": \"Allow\",
            \"Principal\": {
                \"Federated\": \"arn:aws:iam::${Account_ID}:oidc-provider/token.actions.githubusercontent.com\"
            },
            \"Action\": \"sts:AssumeRoleWithWebIdentity\",
            \"Condition\": {
                \"StringLike\": {
                    \"token.actions.githubusercontent.com:sub\": \"repo:${GITHUB_ORG}/${GITHUB_REPO}:*\"
                },
                \"StringEquals\": {
                    \"token.actions.githubusercontent.com:aud\": \"sts.amazonaws.com\"
                }
            }
        }
    ]
}"
aws iam create-role \
  --role-name ${ROLE_NAME} \
  --assume-role-policy-document "${POLICY}"
```
偽装先の権限管理は作った IAM Role に対してポリシーをアタッチすることで実現する。

### Github Actions 設定

```yaml
jobs:
  deploy:
    steps:
      - name: configure aws credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}
```

## Google Cloud

Google Cloud 側では workload identity pool を作り GitHub を provider に設定する。

```bash
WI_POOL=<pool名>
WI_PROVIDER=<provider名>
GITHUB_ORG=<GitHub組織名>
gcloud iam workload-identity-pools create ${WI_POOL} --location="global"

gcloud iam workload-identity-pools providers create-oidc ${WI_PROVIDER} \
  --location="global" \
  --workload-identity-pool="${WI_POOL}" \
  --issuer-uri="https://token.actions.githubusercontent.com" \
  --attribute-mapping=""google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner"" \
  --attribute-condition="assertion.repository_owner == '${GITHUB_ORG}'"
```

接続先のサービスアカウントを作成し、workload identity pool に紐づける。
```bash
SERVICE_ACCOUNT_NAME=<サービスアカウント名>
PROJECT_NAME=<プロジェクト名>
PROJECT_NUMBER=<プロジェクト番号>
MAPPING_ATTRIBUTE=<サービスアカウント紐づけ対象の属性名>
MAPPING_ATTRIBUTE_VALUE=<サービスアカウント紐づけ対象の属性値>

gcloud iam service-accounts create $SERVICE_ACCOUNT_NAME
gcloud iam service-accounts add-iam-policy-binding $SERVICE_ACCOUNT_NAME@$PROJECT_NAME.iam.gserviceaccount.com \
    --role=roles/iam.workloadIdentityUser \
    --member="principalSet://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$WI_POOL/attribute.$MAPPING_ATTRIBUTE/$MAPPING_ATTRIBUTE_VALUE"
```

偽装先の権限管理は紐づけたサービスアカウントに対して IAM をアタッチすることで実現する。

### Github Actions 設定
`WORKLOAD_IDENTITY_PROVIDER` は `projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${WI_POOL}/providers/${${WI_PROVIDER}` を設定する。 `PROJECT_NUMBER` は Google Cloud のプロジェクト番号である。

```yaml
jobs:
  deploy:
    steps:
      - id: 'auth'
        name: 'Authenticate to Google Cloud'
        uses: 'google-github-actions/auth@v2'
        with:
          workload_identity_provider: '${{ env.WORKLOAD_IDENTITY_PROVIDER }}'
```

これにより `GOOGLE_APPLICATION_CREDENTIALS` が設定され、後続の step で Google Cloud にアクセスできるようになる。

## Azure

Azure 側では Microsoft Entra アプリケーションか、マネージド ID を使ってフェデレーション ID を設定できる。以下では Microsoft Entra アプリケーションによるフェデレーションIDの設定方法を紹介する。

まずフェデレーション設定するアプリケーションを作成する。
```bash
APPLICATION_NAME=<アプリ名>
APP_CLIENT_ID=$(az ad app create --display-name $appregname --query appId --output tsv)
APP_OBJECT_ID=$(az ad app show --id $clientid --query Id --output tsv)
```

そのアプリケーションに対して、GitHub とのフェデレーション設定を追加する。
```bash
AZURE_CLIENT_NAME=<クライアント名>

CREDENTIAL="
{
    \"name\": \"${AZURE_CLIENT_NAME}\",
    \"issuer\": \"https://token.actions.githubusercontent.com\",
    \"subject\": \"repo:${GITHUB_ORG}/${GITHUB_REPO}:environment:Production\",
    \"audiences\": [
        \"api://AzureADTokenExchange\"
    ]
}
"
az ad app federated-credential create --id ${APP_OBJECT_ID} --parameters ${CREDENTIAL}
```

偽装先の権限管理は上記で登録したアプリケーションに対して行うことで管理される。

### Github Actions 設定

下記設定を GiHub 側に設定し、Azure ログインが可能になる。
`AZURE_SUBSCRIPTION_ID` は Azure のサブスクリプションID、 `AZURE_CLIENT_ID`, `AZURE_TENANT_ID` は上記で登録したアプリのクライアント・テナントIDである。

```yaml
jobs:
  deploy:
    steps:
      - name: 'Az CLI login'
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
```


## ほか
基本的には応用して、 OIDC Provider を Cloud サービス側で指定し、 CI/CD 側で生成した token をもとに Cloud サービス側の token エンドポイントを叩く流れを実現すれば良い。

CI/CD 実行側に関しても同様で、 OIDC Provider を置き、その issuer URI を Cloud サービス側に指定すれば OIDC Provider で作成したトークンからトークン交換を行える。

# さいごに

上記にて紹介したコードサンプルは[こちら](https://github.com/hiroyoshii/token-exchange-samples)にまとめた。