OpenID Connect を用いて GitHub Actions から AWS にリソースをデプロイする方法

## 目的

AWS IAM User のアクセスキーの代わりに OpenID Connect を用いて GitHub Actions から AWS にリソースをデプロイする手順とサンプル実装を紹介します。

- [AWS での OpenID Connect の構成](https://docs.github.com/ja/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services)
- [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials)

## 全体の流れ

OpenID Connect (以下 OIDC) を用いて GitHub Actions から AWS のリソースをデプロイする全体の流れを図を示します。

![20240806_GitHub_AWS_OIDC-001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32736/c93c6c55-8529-fcce-7c38-1f6a87992a1f.png)

以下で (1) ~ (5) のポイントを紹介します。

### (1) デプロイの実行承認を得る

AWS の本番環境へのデプロイは、ブランチを制限したり、デプロイ前に実行承認を得るなどして慎重に行うべきです。GitHub には [デプロイに環境の使用](https://docs.github.com/ja/actions/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment) という仕組みが用意されているので、デプロイ保護ルールをプライベートリポジトリに設けました。なお、プライベートリポジトリにこの保護ルールを設けるためには [GitHub のプラン](https://docs.github.com/ja/get-started/learning-about-github/githubs-plans) のうち Enterprise プランにアップグレードする必要があります。

> Deployment protection rules with GitHub Actions for private or internal repositories

今回は次のように `deploy-aws` という指定した個人のレビューを必須とするデプロイ保護ルールを定義しました。

![20240806_GitHub_AWS_OIDC-002x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32736/327c0f8d-643d-a607-51a8-9627509d3c80.png)


上記のデプロイ保護ルールが設定されたワークフローが実行されるたびに [デプロイメントのレビュー](https://docs.github.com/ja/actions/managing-workflow-runs-and-deployments/managing-deployments/reviewing-deployments) で承認または拒否が得られるまでジョブは Pending 状態で待機となります。

### (2) OIDC トークンを取得する

[AWS での OpenID Connect の構成](https://docs.github.com/ja/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services) で紹介されている [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) アクションを使うことで GitHub の OIDC プロバイダから JWT (JSON Web トークン) が発行されます。以下に GitHub Actions のワークフローの実装例を紹介します。

``` yml
on:
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: deploy-aws
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::XXXXXXXXXXXX:role/gh-oidc-deploy-aws
          role-session-name: gh-oidc-${{ github.run_id }}-${{ github.run_attempt }}
          aws-region: ap-northeast-1

```

ポイントを抜粋します。

1. OIDC トークンを発行するために `permissions` に `id-token:write` を明示的に追加しています
2. 誰もが承認を得ることなく本番環境にデプロイできないように前述の `environment: deploy-aws` でレビュー必須としています
3. `role-to-assume` で後述のロールを指定して一時的なアクセスキーの発行をリクエストします

### (3) OIDC トークンで認証をする

前述の [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) アクション内で AWS の OIDC プロバイダに対して認証が行われます。このプロバイダは対象 AWS アカウントに対して 1 つ必要で [AWS への ID プロバイダーの追加](https://docs.github.com/ja/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services#adding-the-identity-provider-to-aws) を参考に利用者が作成します。このプロバイダを Terraform で設定する場合は次のように定義します。

``` tf
resource "aws_iam_openid_connect_provider" "github" {
  url = "https://token.actions.githubusercontent.com"
  client_id_list = [
    "sts.amazonaws.com",
  ]
  thumbprint_list = [
    "6938fd4d98bab03faadb97b34396831e3780aea1",
    "1c58a3a8518e8759bf075b76b750d4f2df264fcd"
  ]
}
```

この `thumbprint_list` は GitHub の OIDC プロバイダの CA 証明書のハッシュ値で [OpenID Connect ID プロバイダーのサムプリントを取得する](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html) で説明されています。具体的なサムプリントは [GitHub Blog](https://github.blog/changelog/2023-06-27-github-actions-update-on-oidc-integration-with-aws/) に紹介されていますが、[こちらのコメント](https://github.com/aws-actions/configure-aws-credentials/issues/357#issuecomment-1626357333) を読む限り 2023 年 7 月からは GitHub と AWS との認証に証明書のサムプリントは使われなくなったので、適当な値でも [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) は動いています。以下の記事が大変参考になります。

https://qiita.com/minamijoyo/items/eac99e4b1ca0926c4310



### (4) 一時的なアクセストークンを得る

前述の [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) アクション内で OIDC プロバイダへの認証が成功すると `role-to-assume` で指定したロールへの一時的なアクセストークンが発行され、実行環境の環境変数にセットされます。ここで重要なのは指定したロールの信頼ポリシーの定義です。

- [Configuring a role for GitHub OIDC identity provider](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html#idp_oidc_Create_GitHub)
- [About security hardening with OpenID Connect](https://docs.github.com/ja/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token)

> :warning: **Important**
> If you do not limit the condition key token.actions.githubusercontent.com:sub to a specific organization or repository, then GitHub Actions from organizations or repositories outside of your control are able to assume roles associated with the GitHub IAM IdP in your AWS account.

とあるように、信頼ポリシーの条件に必ず GitHub のリポジトリ名やその他の情報で制限をかける必要があります。条件については [こちら](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_policies_elements_condition.html) から記法を学べますが、この条件を誤って定義してしまうと自分たちが管理していない Organization の Actions からも AWS にアクセスされる恐れがあります。上記のリンクに幾つか制限の例がありますが、例えば `GitHubOrg/GitHubRepo` リポジトリの `main` ブランチを起点とした GitHub Action しか許容しない場合は次のように信頼ポリシーを定義します。

``` json
"Condition": {
  "StringEquals": {
    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
    "token.actions.githubusercontent.com:sub": "repo:GitHubOrg/GitHubRepo:ref:refs/heads/main"
  }
}
```

今回はブランチではなく前述の Environements `deploy-aws` で制限をかけたいので次のようにしました。

``` json
"Condition": {
  "StringEquals": {
    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
    "token.actions.githubusercontent.com:sub": "repo:GitHubOrg/GitHubRepo:environment:deploy-aws"
  }
}
```

このロールの定義はセキュリティリスクに直結する重要な設定なので OIDC プロバイダ同様、以下のように Terraform で IaC 管理し、継続的に意図しない変更がないことを自動チェックしています。

``` tf
resource "aws_iam_role" "gh-oidc-deploy-aws" {
  name = "gh-oidc-deploy-aws"
  path = "/"
  assume_role_policy = jsonencode(
    {
      Statement = [
        {
          Action = "sts:AssumeRoleWithWebIdentity"
          Condition = {
            StringEquals = {
              "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com",
              "token.actions.githubusercontent.com:sub" = "repo:${var.orgname}/${var.reponame}:environment:deploy-aws"
            }
          }
          Effect = "Allow"
          Principal = {
              Federated = aws_iam_openid_connect_provider.github.arn
          }
        }
      ],
      Version = "2012-10-17"
    }
  )
}
```

この (4) のステップが成功すると、ついに AWS への一時的なアクセスキーがワークフローの実行環境に環境変数として追加されます。

``` diff
+ AWS_ACCESS_KEY_ID=***
+ AWS_DEFAULT_REGION=ap-northeast-1
+ AWS_REGION=ap-northeast-1
+ AWS_SECRET_ACCESS_KEY=***
+ AWS_SESSION_TOKEN=***
```

[オプション](https://github.com/aws-actions/configure-aws-credentials?tab=readme-ov-file#options) によると、発行された一時的なアクセスキーの有効期限はデフォルト 1 時間と短命であることがわかります。

> The assumed role duration in seconds, if assuming a role. Defaults to 1 hour.

### (5) Terraform でリソースのデプロイを実行する

これまでのステップで AWS への一時的なアクセスキーが実行環境の環境変数に設定されました。あとは AWS CLI や Terraform で AWS のリソースを操作することができます。例えば [route53_record](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) リソースで CNAME レコードを管理することができます。
``` tf
resource "aws_route53_zone" "this" {
  name = local.hosted_zone
}

resource "aws_route53_record" "cname" {
  name    = aws_route53_zone.this.name
  zone_id = aws_route53_zone.this.zone_id
  type    = "CNAME"
  ttl     = 86400
  records = var.records
}
```
## まとめ

この仕組みを導入する以前は複数の AWS アカウントに対して権限別にマシンアカウントを作成し、それぞれのアクセスキーを GitHub Secrets で管理して、定期的にローテーションしていました。この作業は常にオペレーションミスのリスクが伴い、各 CI/CD ジョブに影響が出ないように慎重な作業が求められましたが [IAM でのセキュリティのベストプラクティス](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/best-practices.html) によると、アクセスキーなどの長期的な認証情報を作成すること自体がそもそも推奨されていませんでした。そこで今回の OIDC を用いた手法によって、長期的な認証情報の管理そのものを廃止し、管理工数削減と生成した短命なトークンを使うことによるセキュリティ面でのメリットが得られました。また、ロールの信頼ポリシーの定義はセキュリティ上重要な役割を持っているので、チームレビューをした後に、これもまた GitHub Actions からデプロイをし、継続的にドリフト検出を行うことで安心して利用できています。