CodeCommit × CodeBuildでプルリクエスト発行時に自動でCIが走るようにする
はじめに
仕事でCode3兄弟(CodeCommit、CodeBuild、CodePipeline)を使用している方もいるかと思いますが、他のCIツールと比べてかゆいところに手が届かない感じがしませんか?
私の場合、プルリクエストを作成したブランチに対して自動でユニットテストやLinterが走るようになればレビューアが確認する手間が省けていいなーと思ってやり方を考えていたのですが、CodeBuildやCodePipelineでCIを作ろうとするとあらかじめブランチ名を指定して作らなければならず、作業ブランチを作るたびにいちいちCodePipelineを作る羽目になりなかなかつらいです。
今回はAWS CodeCommitとCodeBuildでプルリクエスト発行時に自動でCIが走る仕組みを作りましたので、簡単にまとめてみました。
アーキテクチャ
大きく3つのパートに分かれています。
1つ目はCodeCommitリポジトリの作業ブランチでプルリクエストが発行されたイベントをCloudWatch Eventsで検知し、CodeBuildにビルドプロジェクトを作るパートです。
2つ目は作業ブランチがアップデートされたことをCloudWatch Eventsで検知し、作成したビルドプロジェクトを実行するパートです。
最後の3つ目は、プルリクエストがクローズされたことをCloudWatch Eventsで検知し、作成したビルドプロジェクトを削除するパートです。
使用する言語・デプロイツール
- 言語
- TypeScript
- デプロイツール
- Serverless Framework
IAMロール
CloudWatchから起動するLambdaに付与するIAMロールと、LambdaからCodeBuildへ引き渡すロールの2種類を定義していきます。
Lambdaに付与するIAMロールは以下の通りです。CodeBuildへロールを引き渡すためには"iam:GetRole"
と"iam:PassRole"
が必要なことに注意です。
Resources:
AutoCreateandDeleteCodeBuildLambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: "AutoCreateandDeleteCodeBuildLambdaRole-${self:provider.stage}"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Service: "lambda.amazonaws.com"
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Path: "/"
Policies:
- PolicyName: "AutoCreateandDeleteCodeBuildLambdaPolicy-${self:provider.stage}"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Resource: "*"
Effect: Allow
Action:
- "iam:GetRole"
- "iam:PassRole"
- Resource: "*"
Effect: Allow
Action:
- "codebuild:*"
- "codecommit:*"
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
- "logs:GetLogEvents"
LambdaからCodeBuildへ引き渡すロールは以下の通りです。
Resources:
CodeBuildServiceRole:
Type: AWS::IAM::Role
Properties:
RoleName: "CodeBuildServiceRole-${self:provider.stage}"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Action: sts:AssumeRole
Path: "/"
Policies:
- PolicyName: codecommit-pullrequest-codebuild-execute-role
PolicyDocument:
Version: "2012-10-17"
Statement:
- Resource: "*"
Effect: Allow
Action:
- codecommit:*
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- Resource: "*"
Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:GetObjectVersion
Lambda関数
Lambda関数の実装方法は人それぞれだと思いますので、ここでは私の実装を簡単に解説します。
まずはCodeBuildの操作を行う関数をまとめてクラス化します。
import { CodeBuild } from 'aws-sdk';
class Codebuild {
client: CodeBuild;
constructor(serviceClient: CodeBuild) {
this.client = serviceClient;
}
// ビルドプロジェクトを作成する
createBuildProject(cloneUrlHttps: string, sourceVersion: string, buildProjectName: string) {
const params = {
artifacts: {
type: 'NO_ARTIFACTS',
},
badgeEnabled: true,
description:
'This build project is triggered on create or update pull request in AWS CodeCommit.',
environment: {
computeType: 'BUILD_GENERAL1_SMALL',
image: 'aws/codebuild/amazonlinux2-x86_64-standard:3.0-20.09.14',
type: 'LINUX_CONTAINER',
},
name: buildProjectName,
serviceRole: process.env.CODE_BUILD_ROLE_ARN,
source: {
type: 'CODECOMMIT',
location: cloneUrlHttps,
},
sourceVersion,
};
return new Promise((resolve, reject) => {
this.client.createProject(params, (err, data) => {
if (err) {
console.log('Create Build Project Failure!!');
reject(err);
} else {
console.log('Create Build Project Success!!');
resolve(data);
}
});
});
}
// 作成したビルドプロジェクトを使ってビルドする
startBuild(buildProjectName: string) {
const params = {
projectName: buildProjectName,
};
return new Promise((resolve, reject) => {
this.client.startBuild(params, (err, data) => {
if (err) {
console.log('Start Build Failure!!');
reject(err);
} else {
console.log('Start Build Success!!');
resolve(data);
}
});
});
}
// 作成したビルドプロジェクトを削除する
deleteBuildProject(buildProjectName: string) {
const params = {
name: buildProjectName,
};
return new Promise((resolve, reject) => {
this.client.deleteProject(params, (err, data) => {
if (err) {
console.log('[Failure autoDeleteCodeBuild!!]');
reject(err);
} else {
console.log('[Success autoDeleteCodeBuild!!]');
resolve(data);
}
});
});
}
}
export default Codebuild;
上記クラスを使って、CloudWatch Eventsから呼び出されるLambda関数を実装していきます。
まずはビルドプロジェクトを作成し、ビルドを実施するLambda関数です。
import * as AWS from 'aws-sdk';
import { Handler } from 'aws-lambda';
import Codebuild from '../../aws/codeBuild';
AWS.config.apiVersions = {
codebuild: '2016-10-06',
};
const codebuild = new AWS.CodeBuild();
export const handler: Handler = async (event, context, callback) => {
console.log('[Start autoCreateCodeBuild]');
console.log(event);
const repositoryName = event.detail.repositoryNames[0];
const region = process.env.REGION;
const cloneUrlHttps = `https://git-codecommit.${region}.amazonaws.com/v1/repos/${repositoryName}`;
const sourceVersion = event.detail.sourceReference;
const branchName = sourceVersion.split('/');
const buildProjectName = `${repositoryName}-${branchName.slice(-1)[0]}`;
const codeBuild = new Codebuild(codebuild);
try {
await codeBuild.createBuildProject(cloneUrlHttps, sourceVersion, buildProjectName);
// 作成完了後ビルドを実行する
await codeBuild.startBuild(buildProjectName);
return callback(null, {
statusCode: 200,
});
} catch (err) {
return callback(err);
}
};
次にビルドプロジェクトを実行するLambda関数です。
import * as AWS from 'aws-sdk';
import { Handler } from 'aws-lambda';
import Codebuild from '../../aws/codeBuild';
AWS.config.apiVersions = {
codebuild: '2016-10-06',
};
const codebuild = new AWS.CodeBuild();
export const handler: Handler = async (event, context, callback) => {
console.log('[Start autoStartCodeBuild]');
console.log(event);
const repositoryName = event.detail.repositoryNames[0];
const sourceVersion = event.detail.sourceReference;
const branchName = sourceVersion.split('/');
const buildProjectName = `${repositoryName}-${branchName.slice(-1)[0]}`;
const codeBuild = new Codebuild(codebuild);
try {
await codeBuild.startBuild(buildProjectName);
return callback(null, {
statusCode: 200,
});
} catch (err) {
return callback(err);
}
};
最後に、ビルドプロジェクトを削除するLambda関数です。
import * as AWS from 'aws-sdk';
import { Handler } from 'aws-lambda';
import Codebuild from '../../aws/codeBuild';
AWS.config.apiVersions = {
codebuild: '2016-10-06',
};
const codebuild = new AWS.CodeBuild();
export const handler: Handler = async (event, context, callback) => {
console.log('[Start autoDeleteCodeBuild]');
console.log(event);
const repositoryName = event.detail.repositoryNames[0];
const sourceVersion = event.detail.sourceReference;
const branchName = sourceVersion.split('/');
const buildProjectName = `${repositoryName}-${branchName.slice(-1)[0]}`;
const codeBuild = new Codebuild(codebuild);
try {
await codeBuild.deleteBuildProject(buildProjectName);
return callback(null, {
statusCode: 200,
});
} catch (err) {
return callback(err);
}
};
CloudWatch Events
CloudWatch Eventsの定義ファイルは以下の通りです。アーキテクチャの箇所でも説明した内容に沿って3つに分かれています。
まずはプルリクエスト発行を検知するための定義(AutoCreateCodeBuild
)です。
Resources:
AutoCreateCodeBuild:
Type: AWS::Events::Rule
Properties:
Description: Auto create codeBuild
EventPattern:
source:
- "aws.codecommit"
detail-type:
- "CodeCommit Pull Request State Change"
detail:
event:
- "pullRequestCreated"
pullRequestStatus:
- "Open"
Name: AutoCreateCodeBuild
State: "ENABLED"
Targets:#先ほど作成したLambda関数を指定する
- Arn: arn:aws:lambda:ap-northeast-1:${self:provider.environment.ACCOUNT}:function:autoCreateCodeBuild-dev
Id: autoCreateCodeBuild
次にブランチの変更を検知するための定義(AutoStartCodeBuild
)です。
AutoStartCodeBuild:
Type: AWS::Events::Rule
Properties:
Description: Auto start codeBuild
EventPattern:
source:
- "aws.codecommit"
detail-type:
- "CodeCommit Pull Request State Change"
detail:
event:
- "pullRequestSourceBranchUpdated"
pullRequestStatus:
- "Open"
Name: AutoStartCodeBuild
State: "ENABLED"
Targets:#先ほど作成したLambda関数を指定する
- Arn: arn:aws:lambda:ap-northeast-1:${self:provider.environment.ACCOUNT}:function:autoStartCodeBuild-dev
Id: autoStartCodeBuild
最後にプルリクエストのクローズを検知する定義(AutoDeleteCodeBuild
)です。
AutoDeleteCodeBuild:
Type: AWS::Events::Rule
Properties:
Description: Auto delete codeBuild
EventPattern:
source:
- "aws.codecommit"
detail-type:
- "CodeCommit Pull Request State Change"
detail:
event:
- "pullRequestStatusChanged"
- "pullRequestMergeStatusUpdated"
pullRequestStatus:
- "Closed"
Name: AutoDeleteCodeBuild
State: "ENABLED"
Targets:#先ほど作成したLambda関数を指定する
- Arn: arn:aws:lambda:ap-northeast-1:${self:provider.environment.ACCOUNT}:function:autoDeleteCodeBuild-dev
Id: autoDeleteCodeBuild
これら定義に加えて、Lambdaを起動するPermissionを忘れないように注意してください。
上記定義の後ろに付け加えればOKです。
AutoCreateCodeBuildLambdaInvokePermission:
Type: "AWS::Lambda::Permission"
Properties:
FunctionName: "autoCreateCodeBuild-${self:provider.stage}"# serverless.yml(後述)で定義する関数名を記載する
Action: "lambda:InvokeFunction"
Principal: "events.amazonaws.com"
SourceArn:
"Fn::GetAtt": [AutoCreateCodeBuild, Arn]# CloudWatch Events定義のリソース名を指定
DependsOn: AutoCreateCodeBuild
AutoStartCodeBuildLambdaInvokePermission:
Type: "AWS::Lambda::Permission"
Properties:
FunctionName: "autoStartCodeBuild-${self:provider.stage}"# serverless.yml(後述)で定義する関数名を記載する
Action: "lambda:InvokeFunction"
Principal: "events.amazonaws.com"
SourceArn:
"Fn::GetAtt": [AutoStartCodeBuild, Arn]# CloudWatch Events定義のリソース名を指定
DependsOn: AutoStartCodeBuild
AutoDeleteCodeBuildLambdaInvokePermission:
Type: "AWS::Lambda::Permission"
Properties:
FunctionName: "autoDeleteCodeBuild-${self:provider.stage}"# serverless.yml(後述)で定義する関数名を記載する
Action: "lambda:InvokeFunction"
Principal: "events.amazonaws.com"
SourceArn:
"Fn::GetAtt": [AutoDeleteCodeBuild, Arn]# CloudWatch Events定義のリソース名を指定
DependsOn: AutoDeleteCodeBuild
serverless.yml
デプロイスクリプトであるserverless.ymlは以下の通りです。
service: automationPipeline
provider:
name: aws
runtime: nodejs12.x
# you can overwrite defaults here
stage: dev
region: ap-northeast-1
environment:
ACCOUNT: "{Please replace with your IAM Account ID here.}"
# you can add packaging information here
package:
exclude:
- templates/**
individually: true
custom:
webpack:
webpackConfig: ./webpack.config.js
includeModules:
packagePath: ./../package.json
forceExclude:
- aws-sdk
plugins:
- serverless-webpack
- serverless-pseudo-parameters
functions:
autoCreateCodeBuild:
handler: src/lambda/autoCreateCodeBuild/index.handler
name: "autoCreateCodeBuild-${self:provider.stage}"
role: "arn:aws:iam::#{AWS::AccountId}:role/AutoCreateandDeleteCodeBuildLambdaRole-${self:provider.stage}"
description: "This function is test app."
environment:
CODE_BUILD_ROLE_ARN: "arn:aws:iam::#{AWS::AccountId}:role/CodeBuildServiceRole-${self:provider.stage}"
REGION: "${self:provider.region}"
autoStartCodeBuild:
handler: src/lambda/autoStartCodeBuild/index.handler
name: "autoStartCodeBuild-${self:provider.stage}"
role: "arn:aws:iam::#{AWS::AccountId}:role/AutoCreateandDeleteCodeBuildLambdaRole-${self:provider.stage}"
description: "This function is test app."
autoDeleteCodeBuild:
handler: src/lambda/autoDeleteCodeBuild/index.handler
name: "autoDeleteCodeBuild-${self:provider.stage}"
role: "arn:aws:iam::#{AWS::AccountId}:role/AutoCreateandDeleteCodeBuildLambdaRole-${self:provider.stage}"
description: "This function is test app."
# you can add CloudFormation resource templates here
resources:
- ${file(./templates/iam.yml)}
- ${file(./templates/cloudwatchevent.yml)}
buildspec.yml
今回の趣旨とは少し離れますが、参考にbuildspec.ymlも載せておきます。
version: 0.2
env:
variables:
NODE_ENV: "development"
phases:
install:
runtime-versions:
nodejs: 12
pre_build:
commands:
- npm install
build:
commands:
- npm run lint
- npm test
注意点
今回作成したコードではリポジトリの区別まではしていないので、同じAWSアカウントを複数のチームで共有している場合は他のリポジトリに対しても実行されてしまいます。必要であれば、CloudWatch Eventsから渡される情報からリポジトリ名はわかりますので、それでうまくハンドリングしてください。
おわりに
今回ご紹介したシステムにより、以下が可能となります。
- プルリクエストが発行されたときにビルドプロジェクトが自動で作成・実行される
- プルリクエストが発行されたブランチに変更が入ると、自動でビルドされる
- プルリクエストがマージなどによりクローズされると、ビルドプロジェクトが自動削除される
皆様の参考になれば幸いです。
Discussion