🐡

CDKで構築したリソースのプレビュー環境をGithub Actionで構築してみる

2024/10/20に公開

こんにちは、会社ではインフラ関連で色々やっているNHatanakaです。
最近プルリクエストのマージ前にAWSの環境上で確認できないか考え、なんとなく形になったと思うのでまとめたいと思います。(実務で運用はしてません)

構想

プレビュー環境を用意したい時にGithub Actionsを手動で実行しブランチ名をCloudFormationのスタック名に含ませることでプレビュー環境を構築したいと思います。
今回プレビュー環境としてデプロイする内容は下記で紹介されているStepFunctionsのチュートリアルです。
https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/tutorial-lambda-state-machine-cdk.html

ディレクトリ構成

├── .github/
│   └── workflows/
│       ├── create-preview.yml
│       └── delete-preview.yml
│
├── lib/
│   ├── iam-stack.ts
│   └── step-stack.ts
│
以下略

CDKのセットアップ

まずCDKのセットアップを行います。

mkdir cdk-preview && cd $_
cdk init --language typescript

GitHubActions上で使用するロールを作成

GitHubActions上でcdk deployを実行するためのポリシーを持ったロールを構築します。

libディレクトリ直下にiam-stack.tsを作成します。
<GitHubユーザー名>と<GitHubリポジトリ名>はご自身の環境に合わせ修正してください。

iam-stack.ts
import * as cdk from 'aws-cdk-lib';
import { FederatedPrincipal, PolicyStatement, Role } from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';

export class IamStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const deployRole = new Role(this, 'DeployRole', {
      assumedBy: new FederatedPrincipal(
        `arn:aws:iam::${this.account}:oidc-provider/token.actions.githubusercontent.com`,
        {
          StringLike: {
            'token.actions.githubusercontent.com:sub': `repo:<GitHubユーザー名>/<GitHubリポジトリ名>:*`
          }
        },
        'sts:AssumeRoleWithWebIdentity'
      ),
      roleName: `DeployRole`,
      maxSessionDuration: cdk.Duration.seconds(3600)
    });
    deployRole.addToPolicy(
      new PolicyStatement({
        resources: [
          `arn:aws:iam::${this.account}:role/cdk-*-${this.account}-${this.region}`
        ],
        actions: [
          'sts:AssumeRole'
        ],
      })
    );
  }
}

cdk-preview.tsでIamStackをインポートします。

import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { IamStack } from '../lib/iam-stack';

const app = new cdk.App();

new IamStack(app, 'IamStack');

最後にcdk deploy IamStackを実行します。

StepFunctionsのスタックファイル作成

libディレクトリ直下にstep-stack.tsを作成します。

step-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda';
import { StateMachine, Succeed } from 'aws-cdk-lib/aws-stepfunctions';
import { LambdaInvoke } from 'aws-cdk-lib/aws-stepfunctions-tasks';
import { Construct } from 'constructs';

export class StepStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const helloFunction = new Function(this, 'MyLambdaFunction', {
      code: Code.fromInline(`
            exports.handler = (event, context, callback) => {
                callback(null, "Hello World!");
            };
        `),
      runtime: Runtime.NODEJS_18_X,
      handler: "index.handler",
      timeout: cdk.Duration.seconds(3)
    });
    new StateMachine(this, 'MyStateMachine', {
      definition: new LambdaInvoke(this, "MyLambdaTask", {
        lambdaFunction: helloFunction
      }).next(new Succeed(this, "GreetedWorld"))
    });
  }
}

cdk-preview.tsでStepStackをインポートします。
この時スタック名にブランチ名を含ませるためにenvをコンテキストとして渡せるようにします。

import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { IamStack } from '../lib/iam-stack';

const app = new cdk.App();

const env = app.node.tryGetContext('env');

new IamStack(app, 'IamStack');
new StepStack(app, `${env}-StepStack`);

Githubのシークレットに必要な情報を登録

最初にIamStackで構築したロールを使用するためにはaws-actions/configure-aws-credentialsを使用します。
この時にアカウントIDとリージョンが必要なのでGithubのシークレットに登録します。

プレビュー用ワークフローの構築

プレビュー環境用ワークフローを構築しますが、注意点としてデフォルトブランチにマージしてないワークフローを手動で実行するためには、一度ワークフローのトリガーにpushを追加し実際にpushしてワークフローを認識させる必要があります。
下記のように一覧にワークフローが出現すればトリガーのpushは削除してください。

ただこの方法で認識させたワークフローはコマンドでしか起動できないため、ブラウザ上で起動したい場合、先に下記のワークフローをデフォルトブランチにマージした方が楽かもしれません。

まずプレビュー環境を構築するワークフローを構築します。

create-preview.yml
name: Create Preview

on:
  workflow_dispatch:

permissions:
  id-token: write
  contents: read

jobs:
  create-preview:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v3-node20
        with:
          role-to-assume: "arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/DeployRole"
          aws-region: ${{ secrets.AWS_REGION }}
          role-duration-seconds: 900
          role-session-name: GitHubActionsSession
      - name: Npm CI
        run: npm ci
      - name: CDK Deploy
        run: npx cdk deploy ${{ github.ref_name }}-StepStack -c env=${{ github.ref_name }} --require-approval never

ワークフロー内でgithub.ref_nameを使うことによりブランチ名を取得しcdkのコンテキストに渡すことでスタック名にブランチ名を持たせます。

- name: CDK Deploy
 run: npx cdk deploy ${{ github.ref_name }}-StepStack -c env=${{ github.ref_name }} --require-approval never

次にプレビュー環境を削除するワークフローを構築します。

delete-preview.yml
name: Delete Preview

on:
  workflow_dispatch:

permissions:
  id-token: write
  contents: read

jobs:
  delete-preview:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v3-node20
        with:
          role-to-assume: "arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/DeployRole"
          aws-region: ${{ secrets.AWS_REGION }}
          role-duration-seconds: 900
          role-session-name: GitHubActionsSession
      - name: Npm CI
        run: npm ci
      - name: CDK Destroy
        run: npx cdk destroy ${{ github.ref_name }}-StepStack -c env=${{ github.ref_name }} --force

検証

下記のコマンドを実行し実際にプレビュー環境構築用ワークフローを起動します。
(ブランチ名はpreview作成しました)

gh workflow run "Create Preview" --ref preview

すると下記のように起動すると思います。

完了すればコンソールにログインしCloudFormationとStepFunctions確認してください。

次は削除するので下記コマンドを実行します。

gh workflow run "Delete Preview" --ref preview

CloudFormationがリソースを削除していきます。

余談ですがデフォルトブランチにマージすればブラウザで実行できます。
この時指定したブランチ名がスタック名に含まれます。

まとめ

トリガーを手動起動にするかプルリクエストにするかは迷いましたが、状況によって不要な場合もあるかと思ったので今回は手動にしました。
また個人的にはメンバーがローカルで実際にデプロイする運用等もあるかと思いますが、メンバーがCDKの知見があまりなかったり、環境変数等で色々ややこしい場合選択肢になりうるかと思っています。

O-KUN Tech Blog

Discussion