この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
1 はじめに
CX事業本部の平内(SIN)です。
今回は、AWS CDKで、CodePipeline、CodeCommit、CodeBuildを使用した開発環境を作ってみました。デプロイされるのは、Lambdaファンクションのみです。
CodeCommitのリポジトリで、developブランチをコミットすると、dev環境のLambdaが更新され、masterブランチでprd環境が更新されるようになってます。
2 リポジトリ作成
最初に、CodeCommitでリポジトリを作成します。
スタックの中で作成することも可能ですが、その場合、スタックの削除でリポジトリも消えてしまうので、ちょっと運用上まずいかと思います。
$ aws codecommit create-repository --repository-name SampleRepo --repository-description "My sample repository"
3 AWS CDK
AWS CDKによる作業手順は、以下のとおりです。
(1) プロジェクト作成
作業用ディレクトリを作成して、その中で、initコマンドを実行します。
$ mkdir my-cicd;cd my-cicd
$ cdk init app --language=typescript
(2) モジュールインストール
使用するモジュールをインストールします。
$ npm install @aws-cdk/aws-lambda
$ npm install @aws-cdk/aws-codepipeline
$ npm install @aws-cdk/aws-codepipeline-actions
$ npm install @aws-cdk/aws-codebuild
$ npm install @aws-cdk/aws-iam
$ npm install @aws-cdk/aws-codecommit
(3) DMYプロジェクト
これは、スタックの中でLambda関数を新しく生成するのですが、そのCodeアセットのためのダミーです。(AWS CDKには、直接関係ないです)
$ mkdir dmy
$ echo "SAMPLE" > dmy/README.md
(4) コード作成
スタックのコードは、lib/my-cicd-stack.tsを編集します。(細部は、後述)
(5) ビルド・デプロイ
tscでビルドします。
$ npm run build
エラーがあれば、synthコマンドでもある程度のエラーは確認できます。
$ cdk synth
deployコマンドでデプロイします。
$ cdk deploy
4 コード
スタック作成のためのコードは、以下のとおりです。作業単位をメソッドとして定義できるので、非常に見通しが良いように思います。
devとprdは、ステージ変数(stage)にして、forEachで回しています。
createId()は、スタック間でも一意である必要のあるID生成で、tagをその識別子としています。一方、cerateName()は、リソースの名前生成で、AWSコンソール上での視認性を高めるため、自動生成(スタック名+XXX)では無く、tag + stageで積極的に指定してみました。
lib/my-cicd-stack.ts
// 対象Lambda
// (develop => targetFunctionName_dev)
// (master => targetFunctionName_prd)
const targetFunctionName = "sample-function";
// リポジトリ名 repositoryName
const repositoryName = "SampleRepo"
// 識別するためのタグ
const tag = "my-cicd";
import cdk = require('@aws-cdk/core');
import * as lambda from '@aws-cdk/aws-lambda';
import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions';
import * as codebuild from '@aws-cdk/aws-codebuild';
import * as iam from '@aws-cdk/aws-iam';
import * as codecommit from '@aws-cdk/aws-codecommit';
export class MyCiCdStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// リポジトリの取得
const repo = this.getRepository(repositoryName);
// 2つのステージ(prd/dev)分のリソースを生成する
["prd","dev"].forEach( stage => {
// デプロイ先のLambdaを仮に作成する
// buildspec.ymlからのデプロイは、updateのみ許可する
const targetFunction = this.createFunction(stage);
// プロジェクトの生成
const project = this.createProject(targetFunction, stage);
// パイプラインの生成
const sourceOutput = new codepipeline.Artifact();
// 対象ブランチ(prd:master dev:developとなる)
const branch = (stage=='dev')?'develop':'master';
new codepipeline.Pipeline(this, this.createId('Pipeline',stage), {
pipelineName: this.createName(stage),
stages: [{
stageName: 'Source',
actions: [
this.createSourceAction(repo, branch, sourceOutput)
],
},
{
stageName: 'Build',
actions: [
this.createBuildAction(project, sourceOutput)
],
},
],
});
})
}
//**************************************************** */
// idの生成(tag + name + stage)
//**************************************************** */
private createId(name:string, stage: string): string {
return tag + '-' + name + '-' + stage
}
//**************************************************** */
// 名前の生成(tag + stage)
//**************************************************** */
private createName(stage: string): string {
return tag + '_' + stage
}
//**************************************************** */
// Lambdaファンクションの生成
//**************************************************** */
private createFunction(stage: string):lambda.Function {
return new lambda.Function(this, this.createId('Target', stage), {
functionName: targetFunctionName + '_' + stage,
code: lambda.Code.asset('dmy'), // コードは仮のもの
handler: 'index.handler',
runtime: lambda.Runtime.NODEJS_10_X,
});
}
//**************************************************** */
// プロジェクトの生成
//**************************************************** */
private createProject(targetFunction:lambda.Function, stage: string):codebuild.PipelineProject {
const project = new codebuild.PipelineProject(this, this.createId('Project',stage), {
projectName: this.createName(stage),
environment: {
// 環境変数(関数名及び、ステージ)をbuildspec.ymlに送る
environmentVariables: {
FUNCTION_NAME: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: targetFunction.functionName,
},
STAGE: {
type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
value: stage,
}
},
},
});
// buildspc.ymlからLambdaをupdateするため、パーミッションを追加
project.addToRolePolicy(new iam.PolicyStatement({
resources: [targetFunction.functionArn],
actions: ['lambda:UpdateFunctionCode',
'lambda:UpdateFunctionConfiguration',] }
));
return project;
}
//**************************************************** */
// リポジトリの取得
//**************************************************** */
private getRepository(repositoryName:string):codecommit.Repository {
return codecommit.Repository.fromRepositoryName(this, this.createId("repo",""), repositoryName) as codecommit.Repository;
// 新規にリポジトリを作成する場合(注:スタックの削除でリポジトリも削除される)
// return new codecommit.Repository(this, this.createId('Repository',"") ,{
// repositoryName: repositoryName,
// });
}
//**************************************************** */
// CodePipelineのソースアクション(CodeCommit)の生成
//**************************************************** */
private createSourceAction(repo:codecommit.Repository, branch: string, sourceOutput: codepipeline.Artifact): codepipeline_actions.CodeCommitSourceAction {
return new codepipeline_actions.CodeCommitSourceAction ({
actionName: 'CodeCommit',
repository: repo,
branch: branch,
output: sourceOutput,
});
}
//**************************************************** */
// CodePipelineのビルドアクション(CodeBuild) の生成
//**************************************************** */
private createBuildAction(project: codebuild.IProject, sourceOutput: codepipeline.Artifact) {
return new codepipeline_actions.CodeBuildAction({
actionName: 'CodeBuild',
project,
input: sourceOutput,
outputs: [new codepipeline.Artifact()]
});
}
}
5 リソース
作成されているリソースを少し確認しておきます。
CodePipeline
CodeBuild
Lambda
ビルド実行の様子です。
6 buildspec.yml
こちらは、コミットされるコードのトップに置かれたbuildspec.ymlです。CodeBuildで実行され、LambdaをUpdateしています。
完全に個人的な趣味ですが、ここに、Lambdaの環境変数などのパラメータを集約することで、コード側で管理できるようにしています。
version: 0.2
env:
variables:
DESCRIPTION: sample function
RUN_TIME: nodejs10.x
MEMORY: 128
TIMEOUT: 5
HANDLER: index.handler
ENV: TZ=Asia/Tokyo,NODE_ENV= # NODE_ENV=$STAGE
ZIP_FILE: /tmp/upload.zip
phases:
pre_build:
commands:
- yarn global add typescript
- yarn install
-
build:
commands:
- tsc
- npm test
- zip -r -q ${ZIP_FILE} *
- aws lambda update-function-code --function-name $FUNCTION_NAME --zip-file fileb://$ZIP_FILE --publish
- aws lambda update-function-configuration --function-name $FUNCTION_NAME --environment Variables={$ENV$STAGE} --memory-size $MEMORY --runtime $RUN_TIME --description "$DESCRIPTION" --timeout $TIMEOUT --handler $HANDLER
post_build:
commands:
- echo Deploy completed
7 最後に
すっかり、AWS CDKが気に入ってしまいました。恥ずかしながら、もともと、CloudFormationのテンプレートが得意なわけでは決して無いので、AWS CDKの生成するテンプレートはどうなの?と言われると正直返事に詰まります。
とりあえず、ベストプラクティスによる自動生成に頼りながら、順次、勉強したいと思います。