GitHub ActionsとAWSのOIDC連携に使用するリソースをAWS CDKで作る

2022.04.23

こんにちは、CX事業本部 IoT事業部の若槻です。

GitHub ActionsではOpenID Connect(OIDC)がサポートされたため、AWSなどと安全にキーのやり取りが可能となっています。

GitHub ActionsをAWSとOIDC連携する場合は、「ID Provider」と「IAM Role」をAWS上に作成する必要があるのですが、これらリソースをAWS CDKで作ってコードで管理するようにしてみました。

やってみた

以前作ったID Providerの確認、削除

以前にコンソールから手動で作成した同じくGitHubとのOIDC連携用のID Providerを取得します。

$ OPEN_ID_CONNECT_PROVIDER_ARN=<OPEN_ID_CONNECT_PROVIDER_ARN>
$ aws iam get-open-id-connect-provider \
  --open-id-connect-provider-arn ${OPEN_ID_CONNECT_PROVIDER_ARN}
{
    "Url": "token.actions.githubusercontent.com",
    "ClientIDList": [
        "sts.amazonaws.com"
    ],
    "ThumbprintList": [
        "6938fxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    ],
    "CreateDate": "2022-02-28T14:20:17.398000+00:00",
    "Tags": []
}

URLおよびClientIDListをCDKで作るときにも指定して上げれば良さそうです。

ちなみに同じURLのID Providerは複数作成できないので、ここで削除しておきます。

$ aws iam delete-open-id-connect-provider \
  --open-id-connect-provider-arn ${OPEN_ID_CONNECT_PROVIDER_ARN}

CDKコード

CDK Stackで次の2つのConstructを作成します。

  • ID Provider
  • IAM Role(およびInline Policy)

lib/aws-cdk-v2-project-stack.ts

import { Construct } from 'constructs';
import { Stack, StackProps, aws_iam } from 'aws-cdk-lib';

export interface AwsCdkV2ProjectStackProps extends StackProps {
  principalFederatedSub: string;
}

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

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

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

    const oidcDeployRole = new aws_iam.Role(this, 'GitHubOidcRole', {
      roleName: 'github-oidc-role',
      assumedBy: new aws_iam.FederatedPrincipal(
        gitHubIdProvider.openIdConnectProviderArn,
        {
          StringLike: {
            'token.actions.githubusercontent.com:sub':
              props.principalFederatedSub,
          },
        },
        'sts:AssumeRoleWithWebIdentity' //これを忘れるとStatementのActionが'sts:AssumeRole'となりOIDCでのAssumeRoleで使えなくなる。
      ),
    });

    const deployPolicy = new aws_iam.Policy(this, 'deployPolicy', {
      policyName: 'deployPolicy',
      statements: [
        new aws_iam.PolicyStatement({
          effect: aws_iam.Effect.ALLOW,
          actions: [
            's3:getBucketLocation',
            's3:List*',
            'cloudformation:CreateStack',
            'cloudformation:CreateChangeSet',
            'cloudformation:DeleteChangeSet',
            'cloudformation:DescribeChangeSet',
            'cloudformation:DescribeStacks',
            'cloudformation:DescribeStackEvents',
            'cloudformation:ExecuteChangeSet',
            'cloudformation:GetTemplate',
          ],
          resources: [
            'arn:aws:s3:::*',
            `arn:aws:cloudformation:${region}:${accountId}:stack/CDKToolkit/*`,
            `arn:aws:cloudformation:${region}:${accountId}:stack/*/*`,
          ],
        }),
        new aws_iam.PolicyStatement({
          effect: aws_iam.Effect.ALLOW,
          actions: ['s3:PutObject', 's3:GetObject'],
          resources: [`arn:aws:s3:::cdk-*-assets-${accountId}-${region}/*`],
        }),
        new aws_iam.PolicyStatement({
          effect: aws_iam.Effect.ALLOW,
          actions: ['ssm:GetParameter'],
          resources: [
            `arn:aws:ssm:${region}:${accountId}:parameter/cdk-bootstrap/*/version`,
          ],
        }),
        new aws_iam.PolicyStatement({
          effect: aws_iam.Effect.ALLOW,
          actions: ['iam:PassRole'],
          resources: [
            `arn:aws:iam::${accountId}:role/cdk-*-cfn-exec-role-${accountId}-${region}`,
          ],
        }),
      ],
    });

    oidcDeployRole.attachInlinePolicy(deployPolicy);
  }
}

CDK AppでprincipalFederatedSubをConstructに注入します。ここではAccountはcm-rwakatsuki、Repositoryはaws-cdk-v2-projectとし、どのブランチからも使用可能(*)としています。必要に応じて書き換えてください。

bin/aws-cdk-v2-project.ts

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { AwsCdkV2ProjectStack } from '../lib/aws-cdk-v2-project-stack';

const app = new cdk.App();
new AwsCdkV2ProjectStack(app, 'AwsCdkV2ProjectStack', {
  env: { region: 'ap-northeast-1' },
  principalFederatedSub:
    'repo:cm-rwakatsuki/aws-cdk-v2-project:ref:refs/heads/*',
});

cdk deployでCDK Stackをデプロイします。

動作確認

GitHub側でActions SecretとしてIAM RoleのArnを登録します。

次のWorkflowで試してみます。OIDCにより取得したJWTを使用してAssumeRoleを行い、取得した一時クレデンシャルでCDK Deployを行っています。

.github/workflows/cicd.yaml

on: 
  push:
    paths-ignore:
      - '**/*.md'

jobs:
  integration:
    runs-on: ubuntu-latest
    steps: 
      - name: Checkout
        uses: actions/checkout@v3

      - name: Cache CDK Dependency
        uses: actions/cache@v3
        id: cache_cdk_dependency_id
        env:
          cache-name: cache-cdk-dependency
        with:
          path: node_modules
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('package-lock.json') }}
          restore-keys: ${{ runner.os }}-build-${{ env.cache-name }}-

      - name: Install CDK Dependency
        if: ${{ steps.cache_cdk_dependency_id.outputs.cache-hit != 'true' }}
        run: npm ci

      - name: CDK Test
        run: npm run test

  deploy:
    runs-on: ubuntu-latest
    if: ${{ github.ref_name == 'main' }}
    needs: integration
    env:
      AWS_OIDC_ROLE_ARN: ${{ secrets.AWS_OIDC_ROLE_ARN }}
      AWS_REGION: ap-northeast-1
    permissions:
      id-token: write
      contents: read
    steps:
      - name: debug
        run: |
          echo AWS_OIDC_ROLE_ARN: ${AWS_OIDC_ROLE_ARN/::*:/::XXXXXXXXXXXX:}

      - name: Checkout
        uses: actions/checkout@v3

      - name: Assume Role
        uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: ${{ env.AWS_OIDC_ROLE_ARN }}
          aws-region: ${{env.AWS_REGION}}

      - name: Cache CDK Dependency
        uses: actions/cache@v3
        id: cache_cdk_dependency_id
        env:
          cache-name: cache-cdk-dependency
        with:
          path: node_modules
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('package-lock.json') }}

      - name: Deploy
        run: npm run deploy

Workflowを実行すると、OIDCによるAssumeRoleが行われ、CDK Deployを行うことができました!

注意点

ID ProviderのConstructクラスは2種類ある

今回ID Providerの作成に使用したのはOpenIdConnectProviderというOIDC対応のID Providerを作成するConstructクラスです。

これと似ているものとしてSampProviderというSAML2.0対応のID Providerを作成するものもあるので、取り違えないように注意。

同じURLのID Providerが既に作成されていないか注意する

デプロイしようとするID Providerと同じURLのものがすでに作成済みの場合は次のようなエラーとなります。よって冒頭で前回作成分の削除を行う必要がありました。

11:19:36 PM | CREATE_FAILED        | Custom::AWSCDKOpenIdConnectProvider | IdProvider/Resource/Default
Received response status [FAILED] from custom resource. Message returned: EntityAlreadyExists: Provider with url https://token.
actions.githubusercontent.com already exists.

参考

以上