CodeCommitのPull Request関連のイベントをSlackに通知してみた
Pull Requestのターゲットブランチ毎に通知するチャンネルを変更したい
こんにちは、のんピ(@non____97)です。
CodeCommitでPull Requestしたらすぐレビューしてもらうために通知して欲しいですよね? 私もそう思います。
CodeCommitとAWS Chatbotを連携させればPull Requestに限らず色々なイベントをSlackに通知できます。
しかし、Codeシリーズの通知機能である「Notifications」だと、Pull Requestのターゲットブランチ毎に通知するチャンネルを変更することといったことはできません。
ここで、「複雑な条件を設定したいなら、EventBridgeルールとAWS Chatbotを連携させれば良いんじゃない?」と思う方もいらっしゃるかもしれませんが、以下記事の中で検証されている通り、このような構成ではSlackに通知することができません。
AWS公式ドキュメントでも「CloudWatch Alarms、CodeBuild、CodeCommit、CodeDeploy、CodePipelineからのイベント通知は、現在EventBridgeルールを介してサポートされていません。」と記載されています。
Event notifications from: CloudWatch Alarms, CodeBuild, CodeCommit, CodeDeploy, and CodePipeline are not currently supported via EventBridge rules. If you want to receive notifications for one of these services, you can go to its console, and configure Amazon SNS notifications that you can then map to your Slack channel or Amazon Chime webhook configuration in AWS Chatbot. For more information, see Amazon CloudWatch alarms or Notifications for AWS developer tools.
Using AWS Chatbot with other AWS services - Amazon EventBridge
これではターゲットブランチ毎にレビュアーが異なるときに、ちょっと面倒です。
そこで、以下記事を参考に、EventBridgeルールとLambda関数を使ってCodeCommitのPull Request関連のイベントをSlackに通知してみます。
いきなりまとめ
- 2022年1月現在、CloudWatchアラームとCodeシリーズのイベントをEventBridgeルールを介してAWS Chatbotで通知することはできない
- AWS Chatbot経由ではなくLambda関数からSlackに通知することで、通知内容をカスタマイズできる
検証の構成
今回の検証の構成は以下の通りです。
develop
ブランチへのPull Request関連のイベントはアプリチームのSlackチャンネル、main
ブランチへのPull Request関連のイベントはアプリチームとインフラチームのSlackチャンネルに通知するように設定します。
Incoming Webhooksアプリの作成
SlackにWebhookでメッセージ通知をするためにIncoming Webhooksアプリを作成します。
まず、Slack APIのYour Appsにアクセスします。
新規にIncoming Webhooksアプリを作成するのでCreate New App
をクリックします。
From scratch
をクリックします。
Incoming Webhooksアプリの名前とSlackワークスペースを入力・選択し、Create App
をクリックします。
Incoming Webhooks
からActivate Incoming Webhooks
をOn
にします。その後、Add New Webhook to Workspace
をクリックします。
通知先のチャンネル(アプリチームのチャンネル)を選択して、許可する
をクリックします。
Webhook URLが追加されました。
同様の手順でインフラチームのチャンネルへの通知用Webhook URLも追加します。
以上でSlack側の下準備は完了です。
CodeCommitのPull Request関連のイベントをSlackに通知するLambda関数
今回の対象のイベントは以下の7つです。
- commentOnPullRequestCreated Event
- commentOnPullRequestUpdated Event
- pullRequestCreated Event
- pullRequestSourceBranchUpdated Event
- pullRequestStatusChanged Event
- pullRequestMergeStatusUpdated Event
- pullRequestApprovalStateChanged Event
以下AWS公式ドキュメントを参考にして各イベントの構造を確認します。
確認したイベントの構造を元に、イベント発火時にLambda関数に渡されるオブジェクトを定義します。Lambda関数に渡されるオブジェクトには発生したイベントの情報であるoriginalEvent
と通知先情報であるnoticeTargets
を定義しました。こちらのオブジェクトの整形はEventBridgeルールのInput Transformerで行います。
interface CommentOnPullRequestCreatedDetailEvent { afterCommitId: string; beforeCommitId: string; callerUserArn: string; commentId: string; displayName: string; emailAddress: string; event: string; inReplyTo: string; notificationBody: string; pullRequestId: string; repositoryId: string; repositoryName: string; } interface CommentOnPullRequestUpdatedDetailEvent { afterCommitId: string; beforeCommitId: string; callerUserArn: string; commentId: string; event: string; inReplyTo: string; notificationBody: string; pullRequestId: string; repositoryId: string; repositoryName: string; } interface PullRequestDetailEvent { author: string; callerUserArn: string; creationDate: string; description: string; destinationCommit: string; destinationReference: string; event: string; isMerged: string; lastModifiedDate: string; notificationBody: string; pullRequestId: string; pullRequestStatus: string; repositoryNames: string[]; revisionId: string; sourceCommit: string; sourceReference: string; title: string; } interface PullRequestMergeStatusUpdatedDetailEvent { author: string; callerUserArn: string; creationDate: string; description: string; destinationCommit: string; destinationReference: string; event: string; isMerged: string; lastModifiedDate: string; mergeOption: string; notificationBody: string; pullRequestId: string; pullRequestStatus: string; repositoryNames: string[]; revisionId: string; sourceCommit: string; sourceReference: string; title: string; } interface PullRequestApprovalStateChangedDetailEvent { approvalStatus: string; author: string; callerUserArn: string; creationDate: string; description: string; destinationCommit: string; destinationReference: string; event: string; isMerged: string; lastModifiedDate: string; notificationBody: string; pullRequestId: string; pullRequestStatus: string; repositoryNames: string[]; revisionId: string; sourceCommit: string; sourceReference: string; title: string; } interface CodeCommitEvent { originalEvent: { version: string; id: string; "detail-type": string; source: string; account: string; time: string; region: string; resources: string[]; detail: | CommentOnPullRequestCreatedDetailEvent | CommentOnPullRequestUpdatedDetailEvent | PullRequestDetailEvent | PullRequestMergeStatusUpdatedDetailEvent | PullRequestApprovalStateChangedDetailEvent; }; noticeTargets: { [key: string]: string[] }[]; }
対象イベントのPull Request IDから詳細な情報を取得し、Slackに通知するメッセージを構成していきます。
Slackに通知するメッセージの構成はSlackの公式ドキュメントを参考に、タイトルや段組み、対象Pull Requestへのリンクを記載するようにしました。
全体のコードは以下の通りです。
import { CodeCommitClient, GetPullRequestCommand, GetCommentsForPullRequestCommand, } from "@aws-sdk/client-codecommit"; import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from "axios"; // Ref: https://docs.aws.amazon.com/ja_jp/codecommit/latest/userguide/monitoring-events.html#pullRequestCreated // - commentOnPullRequestCreated Event // - commentOnPullRequestUpdated Event // - pullRequestCreated Event // - pullRequestSourceBranchUpdated Event // - pullRequestStatusChanged Event // - pullRequestMergeStatusUpdated Event // - pullRequestApprovalStateChanged Event interface CommentOnPullRequestCreatedDetailEvent { afterCommitId: string; beforeCommitId: string; callerUserArn: string; commentId: string; displayName: string; emailAddress: string; event: string; inReplyTo: string; notificationBody: string; pullRequestId: string; repositoryId: string; repositoryName: string; } interface CommentOnPullRequestUpdatedDetailEvent { afterCommitId: string; beforeCommitId: string; callerUserArn: string; commentId: string; event: string; inReplyTo: string; notificationBody: string; pullRequestId: string; repositoryId: string; repositoryName: string; } interface PullRequestDetailEvent { author: string; callerUserArn: string; creationDate: string; description: string; destinationCommit: string; destinationReference: string; event: string; isMerged: string; lastModifiedDate: string; notificationBody: string; pullRequestId: string; pullRequestStatus: string; repositoryNames: string[]; revisionId: string; sourceCommit: string; sourceReference: string; title: string; } interface PullRequestMergeStatusUpdatedDetailEvent { author: string; callerUserArn: string; creationDate: string; description: string; destinationCommit: string; destinationReference: string; event: string; isMerged: string; lastModifiedDate: string; mergeOption: string; notificationBody: string; pullRequestId: string; pullRequestStatus: string; repositoryNames: string[]; revisionId: string; sourceCommit: string; sourceReference: string; title: string; } interface PullRequestApprovalStateChangedDetailEvent { approvalStatus: string; author: string; callerUserArn: string; creationDate: string; description: string; destinationCommit: string; destinationReference: string; event: string; isMerged: string; lastModifiedDate: string; notificationBody: string; pullRequestId: string; pullRequestStatus: string; repositoryNames: string[]; revisionId: string; sourceCommit: string; sourceReference: string; title: string; } interface CodeCommitEvent { originalEvent: { version: string; id: string; "detail-type": string; source: string; account: string; time: string; region: string; resources: string[]; detail: | CommentOnPullRequestCreatedDetailEvent | CommentOnPullRequestUpdatedDetailEvent | PullRequestDetailEvent | PullRequestMergeStatusUpdatedDetailEvent | PullRequestApprovalStateChangedDetailEvent; }; noticeTargets: { [key: string]: string[] }[]; } interface SlackMessgage { blocks: { type: string; block_id?: string; text?: { type: string; text: string }; fields?: { type: string; text: string }[]; }[]; } // Number of characters limit for slack // Ref: https://api.slack.com/reference/block-kit/blocks#section_fields const characterLimit = 2000; const requestSlack = async ( slackWebhookUrl: string, slackMessage: SlackMessgage ) => { return new Promise<AxiosResponse | AxiosError>((resolve, reject) => { // Request parameters const options: AxiosRequestConfig = { url: slackWebhookUrl, method: "POST", headers: { "Content-type": "application/json", }, data: slackMessage, }; // Request Slack axios(options) .then((response) => { console.log( `response data : ${JSON.stringify(response.data, null, 2)}` ); resolve(response); }) .catch((error) => { console.error(`response error : ${JSON.stringify(error, null, 2)}`); reject(error); }); }); }; export const handler = async ( event: CodeCommitEvent ): Promise<AxiosResponse | AxiosError | void> => { if (!process.env["REGION"]) { console.log( `The region name environment variable (REGION_NAME) is not specified. e.g. us-east-1` ); return; } console.log(`event : ${JSON.stringify(event, null, 2)}`); const region: string = process.env["REGION"]; const codeCommitclient = new CodeCommitClient({ region: region }); const getPullRequestCommandOutput = await codeCommitclient.send( new GetPullRequestCommand({ pullRequestId: event.originalEvent.detail.pullRequestId, }) ); const slackMessage: SlackMessgage = { blocks: [ { type: "header", block_id: "header", text: { type: "plain_text", text: "", }, }, { type: "divider", }, { type: "section", block_id: "fieldsSection", fields: new Array(), }, { type: "section", block_id: "textSection", text: { type: "mrkdwn", text: "", }, }, ], }; const headerIndex = slackMessage.blocks.findIndex( (block) => block.block_id === "header" ); const fieldsSectionIndex = slackMessage.blocks.findIndex( (block) => block.block_id === "fieldsSection" ); const textSectionIndex = slackMessage.blocks.findIndex( (block) => block.block_id === "textSection" ); const repositoryName = "repositoryNames" in event.originalEvent.detail ? event.originalEvent.detail.repositoryNames.toString() : event.originalEvent.detail.repositoryName ? event.originalEvent.detail.repositoryName : "undifined"; const notificationBody = event.originalEvent.detail.notificationBody; const callerUserArn = event.originalEvent.detail.callerUserArn; const destinationReference = getPullRequestCommandOutput.pullRequest?.pullRequestTargets?.find( (pullRequestTarget) => pullRequestTarget.destinationReference )?.destinationReference; const consoleUrl = notificationBody.substring( notificationBody.indexOf("https://") ); slackMessage.blocks[fieldsSectionIndex].fields?.push({ type: "mrkdwn", text: `*AWS Management Console URL:*\n${consoleUrl}`, }); slackMessage.blocks[fieldsSectionIndex].fields?.push({ type: "mrkdwn", text: `*Caller User ARN:*\n${callerUserArn}`, }); slackMessage.blocks[ textSectionIndex ].text!.text = `\`\`\`${notificationBody.substring( 0, characterLimit - 1 )}\`\`\``; slackMessage.blocks[fieldsSectionIndex].fields?.push({ type: "mrkdwn", text: `*Pull Request ID:*\n${getPullRequestCommandOutput.pullRequest?.pullRequestId}`, }); slackMessage.blocks[fieldsSectionIndex].fields?.push({ type: "mrkdwn", text: `*Pull Request Title:*\n${getPullRequestCommandOutput.pullRequest?.title?.substring( 0, characterLimit - 1 )}`, }); slackMessage.blocks[fieldsSectionIndex].fields?.push({ type: "mrkdwn", text: `*Pull Request Status:*\n${getPullRequestCommandOutput.pullRequest?.pullRequestStatus}`, }); slackMessage.blocks[fieldsSectionIndex].fields?.push({ type: "mrkdwn", text: `*isMerged Status:*\n${ getPullRequestCommandOutput.pullRequest?.pullRequestTargets?.find( (pullRequestTarget) => pullRequestTarget.mergeMetadata )?.mergeMetadata?.isMerged }`, }); slackMessage.blocks[fieldsSectionIndex].fields?.push({ type: "mrkdwn", text: `*Destination Reference:*\n${destinationReference?.substring( 0, characterLimit - 1 )}`, }); slackMessage.blocks[fieldsSectionIndex].fields?.push({ type: "mrkdwn", text: `*Source Reference:*\n${getPullRequestCommandOutput.pullRequest?.pullRequestTargets ?.find((pullRequestTarget) => pullRequestTarget.sourceReference) ?.sourceReference?.substring(0, characterLimit - 1)}`, }); if ("commentId" in event.originalEvent.detail) { const getCommentsForPullRequestCommandOutput = await codeCommitclient.send( new GetCommentsForPullRequestCommand({ pullRequestId: event.originalEvent.detail.pullRequestId, }) ); slackMessage.blocks[fieldsSectionIndex].fields?.push({ type: "mrkdwn", text: `*File:*\n${getCommentsForPullRequestCommandOutput.commentsForPullRequestData ?.find((pullRequest) => pullRequest.comments?.find( (comment) => "commentId" in event.originalEvent.detail && comment.commentId === event.originalEvent.detail.commentId ) ) ?.location?.filePath?.substring(0, characterLimit - 1)}`, }); slackMessage.blocks[fieldsSectionIndex].fields?.push({ type: "mrkdwn", text: `*Comment:*\n${getCommentsForPullRequestCommandOutput.commentsForPullRequestData ?.find((pullRequest) => pullRequest.comments?.find( (comment) => "commentId" in event.originalEvent.detail && comment.commentId === event.originalEvent.detail.commentId ) ) ?.comments?.find( (comment) => "commentId" in event.originalEvent.detail && comment.commentId === event.originalEvent.detail.commentId ) ?.content?.substring(0, characterLimit - 1)}`, }); } if ("approvalStatus" in event.originalEvent.detail) { slackMessage.blocks[fieldsSectionIndex].fields?.push({ type: "mrkdwn", text: `*Approval Status:*\n${event.originalEvent.detail.approvalStatus}`, }); } switch (event.originalEvent.detail.event) { case "commentOnPullRequestCreated": slackMessage.blocks[ headerIndex ].text!.text = `The pull request in the ${repositoryName} repository has been commented`; break; case "commentOnPullRequestUpdated": slackMessage.blocks[ headerIndex ].text!.text = `The ${repositoryName} repository pull request comments has been updated`; break; case "pullRequestCreated": slackMessage.blocks[ headerIndex ].text!.text = `The pull request has been created in the ${repositoryName} repository`; break; case "pullRequestSourceBranchUpdated": slackMessage.blocks[ headerIndex ].text!.text = `The source branch of the pull request in the ${repositoryName} repository has been updated`; break; case "pullRequestStatusChanged": slackMessage.blocks[ headerIndex ].text!.text = `The status of the ${repositoryName} repository pull request has changed`; break; case "pullRequestMergeStatusUpdated": slackMessage.blocks[ headerIndex ].text!.text = `The merge status of the ${repositoryName} repository pull request has changed`; break; case "pullRequestApprovalStateChanged": slackMessage.blocks[ headerIndex ].text!.text = `The approval status of the ${repositoryName} repository pull request has changed`; break; } console.log(`slackMessage : ${JSON.stringify(slackMessage, null, 2)}`); for (const [key, slackWebhookUrls] of Object.entries( event.noticeTargets.find( (noticeTarget) => destinationReference! in noticeTarget )! )) { for (const [index, slackWebhookUrl] of slackWebhookUrls.entries()) { await requestSlack(slackWebhookUrl, slackMessage); } } };
AWS CDKの構成
AWS CDKのディレクトリ構成とpackage.json
AWS CDKのディレクトリ構成は以下の通りです。
> tree . ├── .gitignore ├── .npmignore ├── README.md ├── bin │ └── notice-pull-request-events.ts ├── cdk.json ├── jest.config.js ├── lib │ ├── notice-pull-request-events-function-stack.ts │ ├── notice-pull-request-events-stack.ts │ └── role-and-approve-rule-template-stack.ts ├── package-lock.json ├── package.json ├── src │ └── lambda │ └── functions │ └── notice-pull-request-events.ts ├── test │ └── notice-pull-request-events.test.ts └── tsconfig.json 6 directories, 14 files
package.json
は以下の通りです。AWS CDKのバージョンは2.5.0
です。
{ "name": "notice-pull-request-events", "version": "0.1.0", "bin": { "notice-pull-request-events": "bin/notice-pull-request-events.js" }, "scripts": { "build": "tsc", "watch": "tsc -w", "test": "jest", "cdk": "cdk" }, "devDependencies": { "@types/jest": "^26.0.10", "@types/node": "10.17.27", "aws-cdk": "2.5.0", "jest": "^26.4.2", "ts-jest": "^26.2.0", "ts-node": "^9.0.0", "typescript": "~3.9.7" }, "dependencies": { "@aws-sdk/client-codecommit": "^3.46.0", "aws-cdk-lib": "2.5.0", "axios": "^0.24.0", "constructs": "^10.0.0", "source-map-support": "^0.5.16" } }
IAMロールと承認ルールテンプレートのスタック
Pull Requestの承認時の動作確認を行いたいので、IAMロールと承認ルールテンプレートを作成します。こちらのスタックは以前作成した以下記事で紹介しているものとほぼ同じです。(違いは承認ルールテンプレートでdevelopブランチも承認対象になっている程度です)
実際のコードは以下の通りです。
import { Construct } from "constructs"; import { Stack, StackProps, aws_iam as iam, aws_logs as logs, custom_resources as cr, } from "aws-cdk-lib"; export class RoleAndApproveRuleTemplateStack extends Stack { public readonly approvalRuleTemplate: cr.AwsCustomResource; constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); const jumpAccountId: string = this.node.tryGetContext("jumpAccountId"); // Create Infra Team IAM role const iamRole = new iam.Role(this, "IamRole", { assumedBy: new iam.AccountPrincipal(jumpAccountId).withConditions({ Bool: { "aws:MultiFactorAuthPresent": "true", }, }), managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName("AdministratorAccess"), ], }); this.approvalRuleTemplate = new cr.AwsCustomResource( this, "ApprovalRuleTemplate", { logRetention: logs.RetentionDays.ONE_WEEK, onCreate: { action: "createApprovalRuleTemplate", parameters: { approvalRuleTemplateContent: JSON.stringify({ Version: "2018-11-08", DestinationReferences: ["refs/heads/main", "refs/heads/develop"], Statements: [ { Type: "Approvers", NumberOfApprovalsNeeded: 1, ApprovalPoolMembers: [ `arn:aws:sts::${this.account}:assumed-role/${iamRole.roleName}/*`, ], }, ], }), approvalRuleTemplateDescription: "Approval rule template for the main branch", approvalRuleTemplateName: "approvalRuleTemplate", }, physicalResourceId: cr.PhysicalResourceId.fromResponse( "approvalRuleTemplate.approvalRuleTemplateId" ), service: "CodeCommit", }, onUpdate: { action: "updateApprovalRuleTemplateContent", parameters: { newRuleContent: JSON.stringify({ Version: "2018-11-08", DestinationReferences: ["refs/heads/main", "refs/heads/develop"], Statements: [ { Type: "Approvers", NumberOfApprovalsNeeded: 1, ApprovalPoolMembers: [ `arn:aws:sts::${this.account}:assumed-role/${iamRole.roleName}/*`, ], }, ], }), approvalRuleTemplateName: "approvalRuleTemplate", }, physicalResourceId: cr.PhysicalResourceId.fromResponse( "approvalRuleTemplate.approvalRuleTemplateId" ), service: "CodeCommit", }, onDelete: { action: "deleteApprovalRuleTemplate", parameters: { approvalRuleTemplateName: "approvalRuleTemplate", }, service: "CodeCommit", }, policy: cr.AwsCustomResourcePolicy.fromStatements([ new iam.PolicyStatement({ actions: [ "codecommit:CreateApprovalRuleTemplate", "codecommit:UpdateApprovalRuleTemplateContent", "codecommit:DeleteApprovalRuleTemplate", ], resources: ["*"], }), ]), } ); } }
CodeCommitのPull Request関連のイベントをSlackに通知するLambda関数のスタック
CodeCommitのPull Request関連のイベントをSlackに通知するLambda関数をデプロイするためのスタックも準備します。Lambda関数で使用するAPIの権限許可を忘れがちなので忘れないようにします。(私は2回忘れました)
実際のコードは以下の通りです。
import { Construct } from "constructs"; import { Stack, StackProps, aws_iam as iam, aws_lambda as lambda, aws_lambda_nodejs as nodejs, } from "aws-cdk-lib"; export class NoticePullRequestEventsFunctionStack extends Stack { public readonly noticePullRequestEventsFunction: nodejs.NodejsFunction; constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); const noticePullRequestEventsFunctionIamPolicy = new iam.ManagedPolicy( this, "NoticePullRequestEventsFunctionIamPolicy", { statements: [ new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ "codecommit:GetPullRequest", "codecommit:GetCommentsForPullRequest", ], resources: [`arn:aws:codecommit:${this.region}:${this.account}:*`], }), ], } ); // Create an IAM role for Lambda functions. const noticePullRequestEventsFunctionIamRole = new iam.Role( this, "NoticePullRequestEventsFunctionIamRole", { assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName( "service-role/AWSLambdaBasicExecutionRole" ), noticePullRequestEventsFunctionIamPolicy, ], } ); // Lambda function this.noticePullRequestEventsFunction = new nodejs.NodejsFunction( this, "NoticePullRequestEventsFunction", { entry: "src/lambda/functions/notice-pull-request-events.ts", runtime: lambda.Runtime.NODEJS_14_X, bundling: { minify: true, }, environment: { REGION: this.region, }, role: noticePullRequestEventsFunctionIamRole, } ); } }
CodeCommitリポジトリとEventBridgeルールのスタック
CodeCommitリポジトリと、Pull Request関連のイベントを検知するためのEventBridgeルールを作成します。
EventBridgeルール作成部分以外は以下記事で紹介しているものと同じです。
EventBridgeルールでは検知対象のイベントを指定してあげます。また、Input TransformerでLambda関数に渡すオブジェクトを以下のように構成します。
{ "originalEvent": { // 発生したイベントのオブジェクト }, "noticeTargets": [ { "refs/heads/develop": [ // アプリチームのSlackチャンネルへのWebhook URL ] }, { "refs/heads/main": [ // アプリチームのSlackチャンネルへのWebhook URL // インフラチームのSlackチャンネルへのWebhook URL ] } ] }
実際のコードは以下の通りです。
import { Construct } from "constructs"; import { Stack, StackProps, aws_logs as logs, aws_iam as iam, aws_codecommit as codecommit, custom_resources as cr, aws_events as events, aws_events_targets as targets, aws_lambda_nodejs as nodejs, } from "aws-cdk-lib"; interface NoticePullRequestEventsStackProps extends StackProps { approvalRuleTemplate: cr.AwsCustomResource; noticePullRequestEventsFunction: nodejs.NodejsFunction; appTeamWebhookUrl: string; infraTeamWebhookUrl: string; } export class NoticePullRequestEventsStack extends Stack { constructor( scope: Construct, id: string, props: NoticePullRequestEventsStackProps ) { super(scope, id, props); const repository = new codecommit.Repository(this, "Repository", { repositoryName: "CodeCommitTestRepository", }); new cr.AwsCustomResource( this, "AssociateApprovalRuleTemplateWithRepository", { logRetention: logs.RetentionDays.ONE_WEEK, onCreate: { action: "associateApprovalRuleTemplateWithRepository", parameters: { approvalRuleTemplateName: props.approvalRuleTemplate.getResponseFieldReference( "approvalRuleTemplate.approvalRuleTemplateName" ), repositoryName: repository.repositoryName, }, physicalResourceId: cr.PhysicalResourceId.of( `${props.approvalRuleTemplate.getResponseFieldReference( "approvalRuleTemplate.approvalRuleTemplateName" )}-${repository.repositoryName}` ), service: "CodeCommit", }, onDelete: { action: "disassociateApprovalRuleTemplateFromRepository", parameters: { approvalRuleTemplateName: props.approvalRuleTemplate.getResponseFieldReference( "approvalRuleTemplate.approvalRuleTemplateName" ), repositoryName: repository.repositoryName, }, service: "CodeCommit", }, policy: cr.AwsCustomResourcePolicy.fromStatements([ new iam.PolicyStatement({ actions: [ "codecommit:AssociateApprovalRuleTemplateWithRepository", "codecommit:DisassociateApprovalRuleTemplateFromRepository", ], resources: ["*"], }), ]), } ); new events.Rule(this, "PullRequestEventBriedgeRule", { eventPattern: { source: ["aws.codecommit"], detail: { event: [ "commentOnPullRequestCreated", "commentOnPullRequestUpdated", "pullRequestCreated", "pullRequestSourceBranchUpdated", "pullRequestStatusChanged", "pullRequestMergeStatusUpdated", "pullRequestApprovalStateChanged", ], }, resources: [repository.repositoryArn], }, targets: [ new targets.LambdaFunction(props.noticePullRequestEventsFunction, { event: events.RuleTargetInput.fromObject({ originalEvent: events.EventField.fromPath("$"), noticeTargets: [ { "refs/heads/develop": [props.appTeamWebhookUrl], }, { "refs/heads/main": [ props.appTeamWebhookUrl, props.infraTeamWebhookUrl, ], }, ], }), }), ], }); } }
スタック間の連携
各スタックで作成されたリソースを別スタックで参照できるように定義してあげます。また、アプリチーム/インフラチームのWebhook URLもここで渡してあげます。
実際のコードは以下の通りです。
#!/usr/bin/env node import "source-map-support/register"; import * as cdk from "aws-cdk-lib"; import { RoleAndApproveRuleTemplateStack } from "../lib/role-and-approve-rule-template-stack"; import { NoticePullRequestEventsFunctionStack } from "../lib/notice-pull-request-events-function-stack"; import { NoticePullRequestEventsStack } from "../lib/notice-pull-request-events-stack"; const app = new cdk.App(); const roleAndApproveRuleTemplateStack = new RoleAndApproveRuleTemplateStack( app, "RoleAndApproveRuleTemplateStack" ); const noticePullRequestEventsFunctionStack = new NoticePullRequestEventsFunctionStack( app, "NoticePullRequestEventsFunctionStack" ); new NoticePullRequestEventsStack(app, "NoticePullRequestEventsStack", { appTeamWebhookUrl: "https://hooks.slack.com/services/xxxxxxxxxxxxxxxxxxxxxx", infraTeamWebhookUrl: "https://hooks.slack.com/services/yyyyyyyyyyyyyyyyyyyyyy", approvalRuleTemplate: roleAndApproveRuleTemplateStack.approvalRuleTemplate, noticePullRequestEventsFunction: noticePullRequestEventsFunctionStack.noticePullRequestEventsFunction, });
準備ができたらnpx cdk deploy --all
で全てのスタックをデプロイします。
テスト用のファイルとブランチ作成
Pull Requestを行う前に、テスト用のファイルとブランチを作成します。
CodeCommitのコンソールでAWS CDKで作成されたリポジトリを選択します。ファイルの追加
- ファイルの作成
をクリックします。
ファイルの中身やファイル名、作成者名などを入力して、変更のコミット
をクリックします。
mainブランチにファイルが追加されたことを確認します。
続いてブランチを作成します。
ブランチの作成
をクリックします。
ブランチ名をdevelop
、ブランチ元をmain
と入力・選択し、ブランチの作成
をクリックして、developブランチを作成します。
同様の手順でfeatureブランチを作成すると、以下のようになります。
下準備は以上で完了です。
developブランチへのPull Request作成
それでは、developブランチへのPull Requestを作成します。
featureブランチのREADME.mdに適当な文字列を追加して、変更のコミット
をクリックします。
featureブランチのREADME.mdを確認して編集した結果を確認して、プルリクエストの作成
をクリックします。
ターゲットをdevelop
、ソースをfeature
のPull Requestを作成します。タイトルや説明、差分を確認してプルリクエストの作成
をクリックします。
アプリチームのチャンネルを確認すると、developブランチへのPull Requestが作成されたことを知らせるメッセージが届いていました。
こちらのメッセージにはAWSマネージメントコンソールへのURLが記載されているので、そちらをクリックします。クリックすると以下のように対象のPull Requestが表示されます。
developブランチへのPull Requestの承認/承認取り消し
Pull Requestの承認
次にPull Request承認時の動作確認を行います。
承認可能なIAMロールにスイッチロールをすると、承認
ボタンが表示されるので、そちらをクリックします。
アプリチームのチャンネルを確認すると、developブランチへのPull Requestが承認されたことを知らせるメッセージが届いていました。
Pull Requestの承認取り消し
Pull Requestの承認が取り消された場合の動作確認も行います。
承認をすると、承認を取り消し
ボタンが表示されるので、そちらをクリックします。
アプリチームのチャンネルを確認すると、developブランチへのPull Requestの承認が取り消されたことを知らせるメッセージが届いていました。
developブランチへのPull Requestへのコメント
ファイルに関するコメント追加
ファイルに関するコメントを追加した場合の動作確認を行います。
変更
タブからファイルに関するコメント
をクリックすると、テキストエリアが表示されます。テキストエリアに適当にコメントを入力して、保存
をクリックします。
アプリチームのチャンネルを確認すると、README.mdへのコメント追加を知らせるメッセージが届いていました。
AWS Management Console URL
のURLをクリックすると、対象Pull Requestのアクティビティ履歴を表示してくれます。
ファイルに関するコメント編集
ファイルに関するコメントを編集した場合の動作確認も行います。
先程のコメントの編集
ボタンをクリックすると、テキストエリアが表示されます。テキストエリアに適当にコメントを入力して、保存
をクリックします。
アプリチームのチャンネルを確認すると、README.mdへのコメントが編集されたことを知らせるメッセージが届いていました。
AWS Management Console URL
のURLをクリックすると、対象Pull Requestのアクティビティ履歴を表示してくれます。Markdown形式なので、更新部分が1行で表示されていますね。
変更に関するコメント追加
次に変更に関するコメントを追加した場合の動作確認を行います。
変更
タブの変更に関するコメント
のテキストエリアに適当にコメントを入力して、保存
をクリックします。
アプリチームのチャンネルを確認すると、変更に関するコメントが追加されたことを知らせるメッセージが届いていました。
変更に関するコメント編集
変更に関するコメントを編集した場合の動作確認も行います。
先程のコメントの編集
ボタンをクリックすると、テキストエリアが表示されます。テキストエリアに適当にコメントを入力して、保存
をクリックします。
アプリチームのチャンネルを確認すると、変更に関するコメントが編集されたことを知らせるメッセージが届いていました。
AWS Management Console URL
のURLをクリックすると、対象Pull Requestのアクティビティ履歴を表示してくれます。Markdownを解釈して表示されていることが分かります。
developブランチへのPull Requestのマージ
マージした場合の動作確認も行います。
Pull Requestを承認すると、マージ
ボタンが表示されるので、そちらをクリックします。
マージ方法や作成者名、メールアドレスを選択・入力して、プルリクエストのマージ
をクリックします。
Pull Requestがマージされたことを確認します。
アプリチームのチャンネルを確認すると、Pull Requestがマージされたことを知らせるメッセージが届いていました。
mainブランチへのPull Request作成
最後にmainブランチにPull Requestを作成して、インフラチームのチャンネルにも通知されるかどうかを確認します。
ターゲットをmain
、ソースをdevelop
のPull Requestを作成します。タイトルや説明、差分を確認してプルリクエストの作成
をクリックします。
すると、インフラチームとアプリチームのチャンネルにメッセージが届いていることが確認できました。
メッセージを確認すると、どちらもmainブランチへのPull Requestが作成されたことを知らせるメッセージでした。
対応していなければLambda関数にやらせればいいじゃないか
EventBridgeルールとLambda関数を使ってCodeCommitのPull Request関連のイベントをSlackに通知してみました。
AWS Catbotの動作が未対応だったので、Lambda関数にやってもらいました。今回はSlackチャンネルに通知しましたが、同様の仕組みでTeamsのチャンネルにも通知できると考えています。
この記事が誰かの助けになれば幸いです。
以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!