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

はじめに

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

GitHub と CodePipeline を連携した際、デフォルトでは Push をトリガーにして、パイプラインを起動する設定になりますが、ブランチの運用手段によっては Release など他のイベントをトリガーにしたいケースがあります。

今回は、GitHub リポジトリ上での Release をトリガーにして、パイプラインを起動する構成を CDK で構築します。

尚、GitHub のソースアクションは「Version 1」と「Version 2」の2つが存在しますが、本記事では既に非推奨となっている「Version 1」を使用する点についてご注意ください。「Version 2」を使用した場合の構築については別記事にて記載します。

環境

% 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」を使用して、CodePipeline から GitHub のリポジトリにアクセスするためには、GitHub のアクセストークンが必要です。

https://github.com/settings/tokens にアクセスして「Generate new token (classic)」をクリックします。

アクセストークンの名前を入力した後、repo

admin:repo_hookを許可するためにチェックを入れます。

「Generate token」をクリックして、アクセストークンを生成します。

アクセストークンが表示されますので、コピーしてください。

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

% aws secretsmanager create-secret \
--name github-sample-token \
--secret-string <GITHUB_ACCESS_TOKEN>

{
    "ARN": "arn:aws:secretsmanager:ap-northeast-1:xxxxxxxxxxxx:secret:github-sample-token-jvRSBB",
    "Name": "github-sample-token",
    "VersionId": "95c08eaa-a640-41b2-a2c7-f5673f65f6a3"
}

Push をトリガーにする実装

まずはデフォルトの Push をトリガーにして、パイプラインを起動できるように実装します。

cdk.json

githubOwnerNamegithubRepositoryNameは各自の環境に合わせて変更してください。ここでは、main ブランチに Push したタイミングでパイプラインを起動するようにします。githubTokenNameには、前の手順で Secrets Manager に登録したシークレットの名前を入力しています。

cdk.json

{
  "app": "npx ts-node --prefer-ts-exts bin/codepipeline-triggered-by-github-releases.ts",
  "watch": {
    ...
  },
  "context": {
    "projectName": "codepipeline-triggered-by-github-releases",
    "githubOwnerName": "iam326",
    "githubRepositoryName": "codepipeline-triggered-by-github-releases",
    "githubBranchName": "main",
    "githubTokenName": "github-sample-token",
    ...
  }
}

bin

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

bin/codepipeline-triggered-by-github-releases.ts

#!/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 githubTokenName = app.node.tryGetContext('githubTokenName');

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

CICD スタック

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

lib/codepipeline-triggered-by-github-releases-stack.ts

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;
  githubTokenName: string;
}

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

    const {
      projectName,
      githubOwnerName,
      githubRepositoryName,
      githubBranchName,
      githubTokenName,
    } = props;

    const githubToken =
      cdk.SecretValue.secretsManager(githubTokenName).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.GitHubSourceAction({
      actionName: 'source',
      owner: githubOwnerName,
      repo: githubRepositoryName,
      branch: githubBranchName,
      oauthToken: new cdk.SecretValue(githubToken),
      output: sourceArtifact,
    });

    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],
        },
      ],
    });
  }
}

buildspec.yml

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

buildspec.yml

version: 0.2

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

デモ

下記で CICD スタックをデプロイします。

% cdk deploy "*"

続いて、空コミットを作成してから、main ブランチに Push します。

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

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

Release をトリガーにする実装

前の手順で作成したリソースを基に、パイプラインを起動するトリガーを Release に変更します。

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

GitHub の Release をトリガーにして Webhook で送られてくるペイロードを受付可能なエンドポイントを CodePipeline 側に作成するのですが、その際にシークレットトークンを指定する必要があります。ここでは、ローカルで生成した UUID をシークレットトークンとして扱います。GitHub のアクセストークンと同様に 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

webhookSecretTokenNameを追加します。前の手順で Secrets Manager に登録したシークレットの名前を入力しています。

cdk.json

  ...
  "context": {
    "projectName": "codepipeline-triggered-by-github-releases",
    "githubOwnerName": "iam326",
    "githubRepositoryName": "codepipeline-triggered-by-github-releases",
    "githubBranchName": "main",
    "githubTokenName": "github-sample-token",
    "webhookSecretTokenName": "webhook-sample-secret-token",
   ...

bin

cdk.json に追加したwebhookSecretTokenNameをスタックに渡すようにします。

bin/codepipeline-triggered-by-github-releases.ts

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

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

CICD スタック

デフォルトのトリガーを無効にしつつ、Webhook 用のシークレットトークンを使用して新たな Webhook のエンドポイントを作成します。このエンドポイントでは、GitHub の Release イベントをトリガーとして送られてくるペイロードを通すためのフィルターを設定します。

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

lib/codepipeline-triggered-by-github-releases-stack.ts

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;
  githubTokenName: string;
  webhookSecretTokenName: string;
}

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

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

    const githubToken =
      cdk.SecretValue.secretsManager(githubTokenName).unsafeUnwrap();
    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.GitHubSourceAction({
      actionName: 'source',
      owner: githubOwnerName,
      repo: githubRepositoryName,
      branch: githubBranchName,
      oauthToken: new cdk.SecretValue(githubToken),
      output: sourceArtifact,
      // デフォルトのトリガーを無効にする
      trigger: codePipelineActions.GitHubTrigger.NONE,
    });

    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],
        },
      ],
    });

    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,
      registerWithThirdParty: true,
    });
  }
}

GitHub で Release をトリガーに Webhook を投げる

上記の実装により、CodePipeline 側が GitHub の Release イベントで送られてくるペイロードを受け付けることができるようになりました。続いて、GitHub 側が Release をトリガーに Webhook を投げるための設定を行います。

先に上記のスタックを更新しておきます。

% cdk deploy "*"

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

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

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

Pushesのチェックを外して、代わりにReleasesにチェックを入れます。

「Update webhook」をクリックします。

Webhook 一覧画面に戻り、対象の Webhook に (release) と表示されていれば OK です。これで準備完了です。

デモ

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

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

すると、Push ではパイプラインが起動しなくなったことを確認できます(下記画像では source アクションの成功が44分前になっている)。

続いて、GitHub で Release をしてみます。下記 URL にアクセスして、対象となるリポジトリの Release 一覧を表示した後、「Create a new release」をクリックします。

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

「Choose a tag」で適当な名前のタグを作成します。

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

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

おわりに

GitHub の Release をトリガーにして、パイプラインを起動するためには少し煩雑な手順が必要でした。しかし、この手順を活用すれば、Release 以外のイベントについてもトリガーにすることが可能ですので、覚えておいて損は無いと思います。

近いうちに、GitHub のソースアクション「Version 2」を使用した場合の構成についても記事にしようと思うので、そちらも確認いただけると幸いです。

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