🦋

GitHub Actions の OIDC 連携を AWS CDK で実現する方法

2024/11/07に公開

1. はじめに

GitHub Actions から AWS に OIDC (OpenID Connect) を使って安全にアクセスするために必要なリソースの定義を AWS CDK を利用して行ったので, その記録です.

今回は, 静的ファイルを S3 バケットにアップロードし, CloudFront で配信するユースケースを前提にして想定して定義を行いました.

※Github Actions の説明や AWS CDK の説明は本記事の対象外です

2. 作成したもの

2.1 ソースコード

作成したソースコードは以下で公開しています.
https://github.com/virtual-hippo/aws-oidc-provider-for-github

2.2 構成概略図

3. CDK で定義したリソースの解説

まずは, stack 定義全体から.

lib/stacks/aws-oidc-provider-for-github-stack.ts

import {
  aws_iam as iam,
  aws_s3 as s3,
  aws_cloudfront as cloudfront,
  aws_cloudfront_origins as cloudfront_origins,
  RemovalPolicy,
  Stack,
  StackProps,
} from "aws-cdk-lib";
import { Construct } from "constructs";

export interface AwsOIDCProviderForGithubStackProps extends StackProps {
  readonly githubAccountName: string;
  readonly repositryName: string;
}

export class AwsOIDCProviderForGithubStack extends Stack {
  constructor(
    scope: Construct,
    id: string,
    props: AwsOIDCProviderForGithubStackProps
  ) {
    super(scope, id, props);

    const accountId = Stack.of(this).account;
    const region = Stack.of(this).region;

    const bucket = new s3.Bucket(this, "Bucket", {
      bucketName: `${props.repositryName}-assets-${accountId}-${region}`,
      removalPolicy: RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });
    const s3BucketOrigin =
      cloudfront_origins.S3BucketOrigin.withOriginAccessControl(bucket);
    new cloudfront.Distribution(this, "Distribution", {
      defaultRootObject: "index.html",
      defaultBehavior: {
        origin: s3BucketOrigin,
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
      },
    });

    const gitHubIdProvider = new iam.OpenIdConnectProvider(
      this,
      "GitHubIdProvider",
      {
        url: "https://token.actions.githubusercontent.com",
        clientIds: ["sts.amazonaws.com"],
      }
    );

    const oidcDeployRole = new iam.Role(this, "GitHubOidcRole", {
      roleName: `github-${props.repositryName}-oidc-role`,
      assumedBy: new iam.FederatedPrincipal(
        gitHubIdProvider.openIdConnectProviderArn,
        {
          StringEquals: {
            "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
          },
          StringLike: {
            "token.actions.githubusercontent.com:sub": `repo:${props.githubAccountName}/${props.repositryName}:*`,
          },
        },
        "sts:AssumeRoleWithWebIdentity"
      ),
    });

    const deployPolicy = new iam.Policy(this, "DeployPolicy", {
      policyName: `github-${props.repositryName}-deploy-policy`,
      // 必要に応じてポリシーを追加する
      // 以下は Amazon S3 に静的コンテンツを配置するためのポリシー
      statements: [
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          actions: ["s3:ListBucket", "s3:DeleteObject", "s3:PutObject"],
          resources: [bucket.bucketArn, `${bucket.bucketArn}/*`],
        }),
      ],
    });

    oidcDeployRole.attachInlinePolicy(deployPolicy);

    new CfnOutput(this, "DeployRoleArn", { value: oidcDeployRole.roleArn });
    new CfnOutput(this, "BucketName", { value: bucket.bucketName });
  }
}

大きく以下 4 つのパートに分かれています

  • 静的ファイルの配置先の定義
  • IAM OIDC ID identity provider の定義
  • 3.3 GitHub Action が使う IAM Role, Policy の定義
  • GitHub Actions の設定に必要な値を出力

3.1 静的ファイルの配置先の定義

最初に静的ファイルの配置先の定義を行いました.

    // GitHub Actions から書き込む S3 バケット
    const bucket = new s3.Bucket(this, "Bucket", {
        // ...
    });

    // S3 バケットに配置した静的ファイルを配信する CloudFront Distribution (とオリジン設定)
    const s3BucketOrigin =
      cloudfront_origins.S3BucketOrigin.withOriginAccessControl(bucket);
    new cloudfront.Distribution(this, "Distribution", {
        // ...
      },
    });

ちなみに L2 コンストラクトで OAC を定義できるようになったのは最近です(今回初めて使いました).
aws-cdk v2.156.0 リリースノート

3.2 IAM OIDC ID identity provider の定義

    // GitHub 用の IAM エンティティを定義
    // OpenIdConnectProvider は OpenID Connect (OIDC) 標準をサポートする外部アイデンティティプロバイダー (IdP) サービスを IAM エンティティとして扱う際に利用する
    const gitHubIdProvider = new iam.OpenIdConnectProvider(
      this,
      "GitHubIdProvider",
      {
        url: "https://token.actions.githubusercontent.com",
        clientIds: ["sts.amazonaws.com"],
      }
    );

3.3 GitHub Action が使う IAM Role, Policy の定義

    const oidcDeployRole = new iam.Role(this, "GitHubOidcRole", {
      // ...
      // 信頼ポリシー の設定 (3.2 で作成した IAM OIDC ID  identity provider がこの role を利用できるようにする)
      assumedBy: new iam.FederatedPrincipal(
        // principal (IAM OIDC ID  identity provider) の指定
        gitHubIdProvider.openIdConnectProviderArn,
        // 条件の指定
        {
          // 指定したリポジトリの GitHub Actions のみこの IAM Role を引き受けることを許可する(AssumeRole を許可する).
          StringEquals: {
            // GitHub Actions から AWS にアクセスする際に 用いる Token の`aud` (audience) クレームが "sts.amazonaws.com" の場合のみこの IAM Role を引き受けることを許可する(AssumeRole を許可する).
            // 「AWS STS で認証し、IAM ロールを引き受けるためのトークンである」ことを確認する.
            // https://github.com/aws-actions/configure-aws-credentials#configure-aws-credentials-for-github-actions
            "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
          },
          // 指定したリポジトリの GitHub Actions のみこの IAM Role を引き受けることを許可する(AssumeRole を許可する).
          StringLike: {
            "token.actions.githubusercontent.com:sub": `repo:${props.githubAccountName}/${props.repositryName}:*`,
          },
        },
        // WebIdentity に対してのみこの IAM Role を引き受けることを許可する(AssumeRole を許可する).
        // https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html
        "sts:AssumeRoleWithWebIdentity"
      ),
    });


    // Amazon S3 に静的コンテンツを配置するためのポリシーを追加する
    const deployPolicy = new iam.Policy(this, "DeployPolicy", {
      policyName: `github-${props.repositryName}-deploy-policy`,
      statements: [
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          actions: ["s3:ListBucket", "s3:DeleteObject", "s3:PutObject"],
          resources: [bucket.bucketArn, `${bucket.bucketArn}/*`],
        }),
      ],
    });

    oidcDeployRole.attachInlinePolicy(deployPolicy);

3.4 GitHub Actions の設定に必要な値を出力

GitHub Actions に必要な値を出力します.

    new CfnOutput(this, "DeployRoleArn", { value: oidcDeployRole.roleArn });
    new CfnOutput(this, "BucketName", { value: bucket.bucketName });

cdk deploy が完了すると以下のようにターミナルに出力されます

...
Outputs:
AwsOIDCProviderForGithubStack.BucketName = bucketname-123456789012-region
AwsOIDCProviderForGithubStack.DeployRoleArn = arn:aws:iam::123456789012:role/rolename
...

これらの値を 3.5 で使います.

3.5 GitHub Actions の job 定義

3.4 で出力した値を IAM Role の ARN をsecrets に追加します.

  • AWS_OIDC_ROLE_ARN: {
    AwsOIDCProviderForGithubStack.DeployRoleArn の値}
  • BUCKET_NAME: {AwsOIDCProviderForGithubStack.BucketName の値}

Settings > Security > Actions > Secrets > New repositry secret

job の定義は以下のようにしました.
ビルドジョブで作成したファイルをダウンロードし, S3 バケットへコピーしています.

jobs:
  deploy:
    runs-on: ubuntu-latest
    needs: build
    env:
      AWS_OIDC_ROLE_ARN: ${{ secrets.AWS_OIDC_ROLE_ARN }}
      BUCKET_NAME: ${{ secrets.BUCKET_NAME }}
      AWS_REGION: ap-northeast-1
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
      - name: Assume Role
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ env.AWS_OIDC_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}
      - name: Deploy to AWS
        run: |
          aws s3 cp ./dist "s3://${{ env.BUCKET_NAME }}" --recursive
        timeout-minutes: 3

こちらのファイルは以下に配置しています.

GitHub virtual-hippo/walk-the-dog

4. 最後に

最後まで読んでくださりありがとうございました.
もし, 誤った記述を見つけた場合には指摘いただけると助かります.

参考

以下の Web ページを参考にしました

Discussion