Dependabot と GitHub Actions でAWS CDKのスナップショットテストを自動化してみた

Dependabotで作成されたプルリクの中で『AWS CDK』のライブラリアップデートの時のみ、CDKのスナップショットテストを実行するCI環境をGitHub Actionsで構築してみました。
2023.05.29

はじめに

こんにちは、CX事業本部 Delivery部の塚本です。

今回は、DependabotとAWS CDKのスナップショットテストを使い、自動でCDKのバージョンアップによるデグレを検知できる構成を作ってみました。

今回やりたいことは以下です。

『Dependabotで aws-cdk-lib のアップデートのプルリクが作成された際に、CDKのスナップショットテストを実行する』

※初めてDependabotを触ったので、基本的な内容も多めの記事になってます

Dependabot とは

Dependabot は GitHub のツールで、ライブラリのバージョンを更新するプルリクを自動で生成してくれる機能を持っています。

OSSのリポジトリで上ような状態を目にしたことがあるかと思いますが、こちらがDependabotによるプルリクです。

以下の3つの機能があります。

  • Dependabot Alert
  • Dependabot security updates
  • Dependabot version updates

今回は Dependabot version updates 機能が記事の対象です。

CDKのスナップショットテスト とは

CDKで生成される CloudFormation のテンプレートに「変更がないこと」を確認するテストです。

スナップショットテストは主に以下の確認で利用します。

  • コードリファクタ時にデグレが起きないこと
  • CDKフレームワークのバージョンアップ時にデグレが起きないこと

今回は CDKフレームワークのバージョンアップ時 が記事の対象です。

やりたいこと

今回は Dependabot による aws-cdk-lib のアップデートを検知し、GitHub Actions上でスナップショットテストを行うところまでやっていきます。 フロー図を示します。

結果

先に結果から紹介して、後でコードの全体を紹介します。

以下の package.json を利用します。 現在(2023/5/24)時点では aws-cdk-lib の最新は2.80.0なので、Dependabotの更新対象になるようにわざとバージョンを 2.60.0 に指定します。

比較のために @types/node のバージョンも下げておきます。

package.json

{
  "name": "sample-dependabot",
  "version": "0.1.0",
  "bin": {
    "sample": "bin/sample.js"
  },
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "test": "jest",
    "cdk": "cdk"
  },
  "devDependencies": {
    "@types/jest": "^29.5.1",
    "@types/node": "20.1.7",
    "jest": "^29.5.0",
    "ts-jest": "^29.1.0",
    "aws-cdk": "2.60.0",
    "ts-node": "^10.9.1",
    "typescript": "~5.0.4"
  },
  "dependencies": {
    "aws-cdk-lib": "2.60.0",
    "constructs": "^10.0.0"
  }
}

ソースをプッシュした時点で以下のようにDependabotによるプルリクエストが作成されました。

GitHub Actionsにて、 aws-cdk-lib の更新時のみスナップショットテストが走るように出来ているか確認します。

下画像で確認できる通り、aws-cdk-lib の更新時にスナップショットテストの実行ができています。

また、他のライブラリ更新時にはスナップショットテストは実行されません。

やりたいことが実現できていることを確認できました。

ソース紹介

ここからはソースの紹介です。 今回作ったソース全体はこちらのリポジトリに載せています。

実際にDependabotによって作成されたプルリクも見ることができます。

Dependabot version updates の設定

Dependabot version updatesはリポジトリ内の .github 配下に dependabot.yml を作成するだけで有効化できます。

.github/dependabot.yml

version: 2
updates:
  - package-ecosystem: "npm" # See documentation for possible values
    directory: "/" # Location of package manifests
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 5

各設定値について少しだけ触れておきます。

schedule

.github/dependabot.yml

schedule:
      interval: "weekly"

毎週1回バージョンチェックを実行します。デフォルトで月曜日に設定されており、変更には schedule.day という追加のオプションを利用します。

参考:

open-pull-requests-limit

.github/dependabot.yml

open-pull-requests-limit: 5

Dependabotがバージョン更新でオープンできるプルリクエストの上限値です。

5に設定しているので、5つすでにプルリクが作成されている場合は新たなプルリクを作成しなくなります。

参考:

GitHub Actions の設定

GitHub Actionsは .github/workflows ディレクトリに YAML ファイルを配置することで有効化できます。

参考:

全体は以下のようになりました。

.github/workflows/dependabot-cdk-version-update-job.yml

name: Dependabot CDK version update workflow
on:
  pull_request:
    branches: [main]

permissions:
  pull-requests: write
  contents: read

jobs:
  output-target-library-name:
    runs-on: ubuntu-latest
    if: ${{ github.actor == 'dependabot[bot]' }}
    outputs:
      LIBRARY_NAME: ${{ steps.output-library-name.outputs.LIBRARY_NAME }}
    steps:
      - name: Dependabot metadata
        id: metadata
        uses: dependabot/fetch-metadata@v1.5.0
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"
      - name: Output library name
        id: output-library-name
        run: echo "LIBRARY_NAME=$DEPENDENCY_NAME" >> "$GITHUB_OUTPUT" # 変数のアウトプット(参照: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter)
        env:
          DEPENDENCY_NAME: ${{steps.metadata.outputs.dependency-names}} # 対象ライブラリ名の取得(参照: https://github.com/dependabot/fetch-metadata#:~:text=Subsequent%20actions%20will%20have%20access%20to%20the%20following%20outputs%3A)
  snapshot-test:
    runs-on: ubuntu-latest
    needs: output-target-library-name
    if: ${{ needs.output-target-library-name.outputs.LIBRARY_NAME == 'aws-cdk-lib' }}
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Use Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18
      - name: npm ci
        run: npm ci
      - name: CDK snapshot test
        run: npm test

構成について、少しだけ紹介します。

jobs

.github/workflows/dependabot-cdk-version-update-job.yml

jobs:
  output-target-library-name:
   ~~~~~
   ~~~~~
  snapshot-test:
   if: ${{ needs.output-target-library-name.outputs.LIBRARY_NAME == 'aws-cdk-lib' }}
   ~~~~~
   ~~~~~

ジョブは2つに分割しました。

1つ目 output-target-library-name はDependabot で作成されたプルリクが何のライブラリを対象としているか出力するようにしています。

2つ目 snapshot-test は 1つ目のジョブの出力を受け取り、CDKのバージョンアップであればジョブ自体を実行するようにしています。

job間の値の受け渡し

抜粋:

.github/workflows/dependabot-cdk-version-update-job.yml

jobs:
  output-target-library-name:
    outputs:
      LIBRARY_NAME: ${{ steps.output-library-name.outputs.LIBRARY_NAME }}
    steps:
      - name: Output library name
        id: output-library-name
        run: echo "LIBRARY_NAME=$DEPENDENCY_NAME" == "$GITHUB_OUTPUT"
        env:
          DEPENDENCY_NAME: ${{steps.metadata.outputs.dependency-names}}
  snapshot-test:
    needs: output-target-library-name
    if: ${{ needs.output-target-library-name.outputs.LIBRARY_NAME == 'aws-cdk-lib' }}

.github/workflows/dependabot-cdk-version-update-job.yml

run: echo "LIBRARY_NAME=$DEPENDENCY_NAME" == "$GITHUB_OUTPUT"

上記のコードでステップのアウトプットのパラメータを設定します。

参考: workflow-commands-for-github-actions

.github/workflows/dependabot-cdk-version-update-job.yml

    outputs:
      LIBRARY_NAME: ${{ steps.output-library-name.outputs.LIBRARY_NAME }}

上記のコードでジョブのアウトプットのパラメータを設定します。次のジョブで値を使うためです。

.github/workflows/dependabot-cdk-version-update-job.yml

    needs: output-target-library-name
    if: ${{ needs.output-target-library-name.outputs.LIBRARY_NAME == 'aws-cdk-lib' }}

上記のコードでは前に実行されたジョブのアウトプットを取得して判定をしています。

needs で特定のジョブを指定することで、前のジョブが失敗した場合には次のジョブを実行しない、という動きができます。

needs.[job-id].outputs. とすることで、依存しているジョブのアウトプットを取得することができます。

参考:

アップデート対象ライブラリ名の取得

.github/workflows/dependabot-cdk-version-update-job.yml

    steps:
      - name: Dependabot metadata
        id: metadata
        uses: dependabot/fetch-metadata@v1.5.0
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"
      - name: Output library name
        id: output-library-name
        run: echo "LIBRARY_NAME=$DEPENDENCY_NAME" >> "$GITHUB_OUTPUT" # 変数のアウトプット(参照: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter)
        env:
          DEPENDENCY_NAME: ${{steps.metadata.outputs.dependency-names}} # 対象ライブラリ名の取得(参照: https://github.com/dependabot/fetch-metadata#:~:text=Subsequent%20actions%20will%20have%20access%20to%20the%20following%20outputs%3A)

Dependabotのfetch-metadata action を利用して作成されたプルリクの情報を取得します。

fetch-metadata によって自動的にそのステップの steps.[step-id].outputs に値が埋め込まれます。

参考:

steps.[step-id].outputs.dependency-names を以降のステップで参照することで、対象のライブラリ名を取得することができます。

Dependabot のプルリクを判別

.github/workflows/dependabot-cdk-version-update-job.yml

if: ${{ github.actor == 'dependabot[bot]' }}

上のコードでDependabotからのプルリクであることを判定しています。

AWS CDKのソース

今回CDKのコードは簡単なものになっているので、詳細な説明は省いて簡単に紹介します。

Stackの定義です。簡単にDynamoDBテーブルを作るだけのものです。

lib/sample-stack.ts

import { Stack, StackProps, RemovalPolicy } from "aws-cdk-lib";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import { Construct } from "constructs";

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

    const sampleTable = new dynamodb.Table(this, "sampleTable", {
      partitionKey: { name: "id", type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: RemovalPolicy.DESTROY,
    });
  }
}

スナップショットテストの定義です。toMatchSnapshotを使ってシンプルに記述できます。

import * as cdk from "aws-cdk-lib";
import { Template } from "aws-cdk-lib/assertions";
import { SampleStack } from "../lib/sample-stack";

test("snapshot test", () => {
  const app = new cdk.App();
  const stack = new SampleStack(app, "MyTestStack");
  // スタックからテンプレート(JSON)を生成
  const template = Template.fromStack(stack).toJSON();

  // 生成したテンプレートとスナップショットが同じか検証
  expect(template).toMatchSnapshot();
});

今後の展望

今回はDependabotとAWS CDKのスナップショットテストを組み合わせてみました。

まず、初めて触るDependabotの使い方やGitHub Actionsとの連携方法が分かったので勉強になりました。

実プロジェクトで運用していないので考慮事項は色々ありますが、とりあえず実現可能だと分かって良かったです。

さらに実プロジェクトでの運用のために、以下のような発展系を試してみたいと思いました。

  • スナップショットテストが成功した際の自動マージを実行する
  • dev, stg, prdのように環境を想定し、それぞれの環境への自動デプロイも考慮してみる
  • DependabotのプルリクをSlackで通知する

また読んでください。

参考文献

Dependabot関連:

AWS CDKスナップショットテスト関連:

GitHub Actions関連: