【AWS CDK】GitHub の Release をトリガーに CodePipeline を起動する(GitHub ソースアクション Version 2 版)

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

テントの中から失礼します、IoT 事業部のてんとタカハシです!

先日、GitHub の Release をトリガーにして、CodePipeline を起動する構成を CDK で構築する方法について記載しました。

ただ、上記で記載した構成では、GitHub のソースアクション「Version 1」を使用しています。GitHub のソースアクション「Version 1」は、既に非推奨となっているため、特別な理由が無い限りは「Version 2」を使用する方が好ましいです。

今回は、GitHub のソースアクション「Version 2」を使用して、同様な構成を CDK で構築します。

環境

% sw_vers
ProductName:	macOS
ProductVersion:	12.6
BuildVersion:	21G115

% aws --version
aws-cli/2.8.2 Python/3.9.11 Darwin/21.6.0 exe/x86_64 prompt/off

% cdk --version
2.46.0 (build 5a0595e)

GitHub コネクションを手動で作成する

GitHub のソースアクション Version 1 では、GitHub で生成したアクセストークンを使用することで、CodePipeline から GitHub への接続を可能としていました。一方 Version 2 では、アクセストークンの代わりに CodePipeline 側で「GitHub コネクション」というものを作成して、GitHub への接続を可能にします。

下記の記事に記載ある「GitHub コネクションを手動で作成」を参考にして GitHub コネクションを作成してください。

Webhook 用のシークレットトークンを準備する

GitHub から Webhook で送られてくるペイロードを受け付けるエンドポイントを CodePipeline 側に作成するのですが、その際にシークレットトークンを指定する必要があります。ここでは、ローカルで生成した UUID をシークレットトークンとして扱います。

また、シークレットトークンをソースコードに埋め込むわけにはいかないので、Secrets Manager に登録します。

% aws secretsmanager create-secret \
--name webhook-sample-secret-token \
--secret-string $(uuidgen)
{
    "ARN": "arn:aws:secretsmanager:ap-northeast-1:xxxxxxxxxxxx:secret:webhook-sample-secret-token-Ve5AE4",
    "Name": "webhook-sample-secret-token",
    "VersionId": "47639002-1651-47f6-b64c-fd994bd931f5"
}

実装

cdk.json

[GITHUB_OWNER_NAME][GITHUB_REPOSITORY_NAME]は各自の環境に合わせて変更してください。webhookSecretTokenNameには、前の手順で Secrets Manager に登録したシークレットの名前を入力しています。[CODESTAR_CONNECTION_ARN]は、前の手順で作成した GitHub コネクションの ARN に置き換えてください。

{
  "app": "npx ts-node --prefer-ts-exts bin/codepipeline-triggered-by-github-releases.ts",
  "watch": {
    ...
  },
  "context": {
    "projectName": "codepipeline-triggered-by-github-releases",
    "githubOwnerName": "[GITHUB_OWNER_NAME]",
    "githubRepositoryName": "[GITHUB_REPOSITORY_NAME]",
    "githubBranchName": "main",
    "webhookSecretTokenName": "webhook-sample-secret-token",
    "codestarConnectionArn": "[CODESTAR_CONNECTION_ARN]",
    ...
  }
}

bin

cdk.json に埋め込んだ各種情報をスタックに渡しています。

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CodepipelineTriggeredByGithubReleasesStack } from '../lib/codepipeline-triggered-by-github-releases-stack';

const app = new cdk.App();

const projectName = app.node.tryGetContext('projectName');
const githubOwnerName = app.node.tryGetContext('githubOwnerName');
const githubRepositoryName = app.node.tryGetContext('githubRepositoryName');
const githubBranchName = app.node.tryGetContext('githubBranchName');
const webhookSecretTokenName = app.node.tryGetContext('webhookSecretTokenName');
const codestarConnectionArn = app.node.tryGetContext('codestarConnectionArn');

new CodepipelineTriggeredByGithubReleasesStack(app, `${projectName}-cicd`, {
  projectName,
  githubOwnerName,
  githubRepositoryName,
  githubBranchName,
  webhookSecretTokenName,
  codestarConnectionArn,
});

CICD スタック

GitHub からソースコードをダウンロードする source アクション(GitHub ソースアクション Version 2)と、CodeBuild を実行する deploy アクションを持つパイプラインを作成します。シークレットトークンは Secrets Manager から取得しています。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as codeBuild from 'aws-cdk-lib/aws-codebuild';
import * as codePipeline from 'aws-cdk-lib/aws-codepipeline';
import * as codePipelineActions from 'aws-cdk-lib/aws-codepipeline-actions';

export interface CodepipelineTriggeredByGithubReleaseStackProps
  extends cdk.StackProps {
  projectName: string;
  githubOwnerName: string;
  githubRepositoryName: string;
  githubBranchName: string;
  webhookSecretTokenName: string;
  codestarConnectionArn: string;
}

export class CodepipelineTriggeredByGithubReleasesStack extends cdk.Stack {
  constructor(
    scope: Construct,
    id: string,
    props: CodepipelineTriggeredByGithubReleaseStackProps
  ) {
    super(scope, id, props);

    const {
      projectName,
      githubOwnerName,
      githubRepositoryName,
      githubBranchName,
      webhookSecretTokenName,
      codestarConnectionArn,
    } = props;

    const webhookSecretToken = cdk.SecretValue.secretsManager(
      webhookSecretTokenName
    ).unsafeUnwrap();

    const sourceArtifact = new codePipeline.Artifact();

    const codeBuildDeployProject = new codeBuild.PipelineProject(
      this,
      'CodeBuildDeployProject',
      {
        projectName: `${projectName}-deploy-project`,
        buildSpec: codeBuild.BuildSpec.fromSourceFilename('./buildspec.yml'),
      }
    );

    const sourceAction =
      new codePipelineActions.CodeStarConnectionsSourceAction({
        actionName: 'source',
        owner: githubOwnerName,
        repo: githubRepositoryName,
        branch: githubBranchName,
        connectionArn: codestarConnectionArn,
        output: sourceArtifact,
        // デフォルトのトリガーを無効にする
        triggerOnPush: false,
      });

    const deployAction = new codePipelineActions.CodeBuildAction({
      actionName: 'deploy',
      project: codeBuildDeployProject,
      input: sourceArtifact,
    });

    const deployPipeline = new codePipeline.Pipeline(this, 'DeployPipeline', {
      pipelineName: `${projectName}-deploy-pipeline`,
      stages: [
        {
          stageName: 'source',
          actions: [sourceAction],
        },
        {
          stageName: 'deploy',
          actions: [deployAction],
        },
      ],
    });

    const webhook = new codePipeline.CfnWebhook(this, 'WebhookResource', {
      authentication: 'GITHUB_HMAC',
      authenticationConfiguration: {
        secretToken: webhookSecretToken,
      },
      // GitHub の Release イベントで送られてくるペイロードを通すためのフィルター
      filters: [
        {
          jsonPath: '$.action',
          matchEquals: 'published',
        },
      ],
      targetAction: sourceAction.actionProperties.actionName,
      targetPipeline: deployPipeline.pipelineName,
      targetPipelineVersion: 1,
      // GitHub 側で Webhook を手動で作成する
      registerWithThirdParty: false,
    });

    // Webhook のエンドポイントを出力する
    new cdk.CfnOutput(this, 'WebhookUrl', {
      value: webhook.attrUrl,
    });
  }
}

実装ポイント

デフォルトでは、Push をトリガーにして送られてくるペイロードを受け付けるエンドポイントが作成されます。今回は、Release をトリガーにして送られてくるペイロードを受け付けたいので、デフォルトの設定は無効にします。

const sourceAction =
  new codePipelineActions.CodeStarConnectionsSourceAction({
    actionName: 'source',
    owner: githubOwnerName,
    repo: githubRepositoryName,
    branch: githubBranchName,
    connectionArn: codestarConnectionArn,
    output: sourceArtifact,
    // デフォルトのトリガーを無効にする
    triggerOnPush: false,
  });

デフォルトの代わりとなる Webhook のエンドポイントを作成します。こちらのエンドポイントには、Release をトリガーにして送られてくるペイロードを通すためのフィルターを設定します。

Release イベントで送られてくるペイロードの例はこちらに記載があります。

const webhook = new codePipeline.CfnWebhook(this, 'WebhookResource', {
  authentication: 'GITHUB_HMAC',
  authenticationConfiguration: {
    secretToken: webhookSecretToken,
  },
  // GitHub の Release イベントで送られてくるペイロードを通すためのフィルター
  filters: [
    {
      jsonPath: '$.action',
      matchEquals: 'published',
    },
  ],
  ...

GitHub 側の Webhook の設定を自動で作成してくれる registerWithThirdPartyは false にしています。

const webhook = new codePipeline.CfnWebhook(this, 'WebhookResource', {
  ...
  targetAction: sourceAction.actionProperties.actionName,
  targetPipeline: deployPipeline.pipelineName,
  targetPipelineVersion: 1,
  // GitHub 側で Webhook を手動で作成する
  registerWithThirdParty: false,
});

これはハマりポイントだったのですが、ここを true にして CDK をデプロイするとエラーが発生します。GitHub のソースアクション Version 1 を使用している場合は問題なく通るのですが、Version 2 で使用する GitHub コネクションでは、この設定を行うための権限が付与されていないのだろうと考えます。できないものは仕方ないので、後の手順にて手動で作成することにします。

最後に、新たに作成した Webhook のエンドポイントを出力します。こちらのエンドポイントを後の手順にて GitHub 側に登録します。

// Webhook のエンドポイントを出力する
new cdk.CfnOutput(this, 'WebhookUrl', {
  value: webhook.attrUrl,
});

buildspec.yml

CodeBuild が起動すれば良いだけですので、中身は特にありません。

version: 0.2

phases:
  install:
    commands:
      - echo "install ok"
  build:
    commands:
      - echo "build ok"

GitHub で Release をトリガーした Webhook の設定を行う

ここまでで実装した内容をデプロイすると、

% cdk deploy "*"

CloudFormation で Webhook のエンドポイントを確認できるので、これをコピーします。

また、Secrets Manager に登録したシークレットトークンの値もコピーします。

下記 URL にアクセスして、対象となるリポジトリの Webhook 一覧画面を表示した後、「Add webhook」をクリックします。

https://github.com/[USER_NAME]/[REPOSITORY_NAME]/settings/hooks

新しく追加する Webhook の各種設定を下記にします。

  • Payload URL:前の手順でコピーした Webhook のエンドポイント
  • Content Type:application/json
  • Secret:前の手順でコピーしたシークレットトークン

「Let me select individual events.」を選択すると、イベントの一覧が表示されるので、

Releasesにだけチェックを入れます。

下にスクロールして、「Add webhook」をクリックします。

これで準備完了です。

デモ

試しに空コミットを作成して main ブランチに Push してみます。

% git commit --allow-empty -m "cicd test"
% git push origin main

すると、Push ではパイプラインが起動しないことを確認できます。

続いて、GitHub で Release をしてみます。下記 URL にアクセスして、「Choose a tag」で適当な名前のタグを作成します。

https://github.com/[USER_NAME]/[REPOSITORY_NAME]/releases/new

「Publish release」をクリックして、Release します。

すると、パイプラインが起動することを確認できます。

おわりに

今回は Release をトリガーにした構成を作成しましたが、GitHub 側のトリガーと Webhook のエンドポイント側で設定するフィルターさえ変更すれば、他のイベントでも適用可能です。

少し設定するための手順が煩雑な気もしますが、ブランチの構成や様々な運用方法に合わせてパイプラインを起動することができますので、是非活用してみていただけると嬉しいです。

今回は以上になります。最後まで読んで頂きありがとうございました!