LambdaのCI/CDをAWS CDKで構築する
はじめに
Lambda、使ってますか? Lambda、デプロイしてますか? デプロイ、自動化してますか?
LambdaはSAM CLIでもさくっとデプロイできますが、ブランチをPushしたらそのままデプロイが素敵ですよね。
今回はCodeBuildとCodePipelineを使用した、DockerイメージのAWS LambdaをデプロイするCI/CDパイプラインを、AWS CDKで実装したいと思います。
対象読者
- AWS CDKを使ったことがある方
- AWS CDK Toolkitがインストール済みの方
- CloudFormationを使ったことがある方
- LambdaのCI/CD構築に興味がある方
技術スタック
- AWS CDK(TypeScript)
- AWS CodeBuild
- AWS CodePipeline
- AWS Lambda(Python)
- Amazon ECR
- Amazon S3
- Docker
概要
CodePipelineとCodeBuildを利用したことがない方向けに簡単に説明を記載します。
詳しい説明は本家のリンクを記載してますのでそちらでご確認いただけたら幸いです。
CodePipelineについて
CodePipelineはソースコード取得、ビルド、デプロイまでの一連の流れを自動化してくれるサービスです。
上記の一連の流れをそれぞれステージという単位に区切ってつなげているイメージです。
ソースコードの取得はAWS CodeCommitやGithub、Gitlabなど様々な外部サービスと連携できます。
CodeBuildについて
CodeBuildは、CodePipelineで説明したビルド部分を担当する部分で、LinuxやWindowsなど豊富なDockerイメージからビルドするサーバー環境を簡単に作成することができます。
この仮想サーバー上でコマンドを駆使して柔軟なビルドを実装することができます。
CI/CD全体の流れ
- Githubにmainブランチをpush
- CodeBuildでLambdaプロジェクトに配置した
buildspec.yml
をもとにビルド- 単体テスト実行
- DockerビルドしてイメージをECRにpush
- Lambdaプロジェクトに配置した
template.yml
をビルド
- CloudformationでLambdaをデプロイ
事前準備
Githubでトークンの生成
今回ソースコードの取得はGithubから行います。
CodePipelineとの連携のためにGithub APIを利用するための認証トークンの生成しましょう。
Personal access tokens(classic)の右上にあるGenerate new tokenをクリックして、以下のアクセス権にチェックして、保存をしてください。
生成したトークンを控えておきましょう。
コード解説
今回使用するAWS CDKのコードはGithubに置いてるので動かしたい方はCloneしてみてください。
また、デプロイするサンプルのLambdaは手前味噌で恐縮ですが、こちらの記事で紹介しているAWS Lambdaのプロジェクトを使用します。
こちらのコードもGithubに置いてありますので動かしたい方はCloneしてみてください。
AWS CDK
今回は複数のStackに分けています。
-
ArtifactBucketStack
- CodePipelineの成果物を格納するためのS3バケットを作成
- シンプルかつCodePipelineは自動で作成してもくれるので解説はスキップ
-
SecretsManagerStack
- AWS Secrets Managerのシークレットを作成
- Githubで取得した認証トークンを保存しておくために使用
-
LambdaCodePipelineStack
- 以下を使用したCI/CDを作成
- CodeBuildの作成
- CodePipelineの作成
- 以下を使用したCI/CDを作成
SecretsMangerStack
Githubで生成したトークンは機密情報なので、安全に取り扱う必要があります。
そのため、今回はSecretsMangerに登録しておいて、取得はSecretsManagerを通して行います。
// 下記2つはパイプライン作成時に参照するためexportしておく
export const SECRET_NAME = "AwsCicdRecipe";
export const GITHUB_OAUTH_TOKEN_KEY = "GITHUB_OAUTH_TOKEN";
export class SecretsManagerStack extends cdk.Stack {
public readonly secret: secretsmanager.Secret;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const githubAuthToken = this.node.tryGetContext(
"GITHUB_OAUTH_TOKEN"
) as string;
new secretsmanager.Secret(this, `${SECRET_NAME}Id`, {
secretName: SECRET_NAME,
secretObjectValue: {
[GITHUB_OAUTH_TOKEN_KEY]:
cdk.SecretValue.unsafePlainText(githubAuthToken),
},
});
}
}
GITHUB_AUTH_TOKEN
という名前で生成したトークンをコンテキストから取得し、シークレット作成と同時にトークンを登録しています。
exportしている変数はこの後のLambdaCodePipelineStack
で使用します。
LambdaCodePipelineStack
今回のメインです。
Githubのソースやブランチの設定は今回ハードコーディングしていますが、実際の運用ではコンテキストから取得したほうが汎用性が高いと思います。
const STACK_NAME = "HtmlToPdfLambdaStack";
export class LambdaCodePipelineStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const artifactBucket = aws_s3.Bucket.fromBucketName(
this,
"LambdaCodePipelineArtifactBucket",
ARTIFACT_BUCKET_NAME
);
const codePipeline = this.createCodePipeline(artifactBucket);
const project = this.createCodeBuild();
const sourceOutput = new aws_codepipeline.Artifact();
const buildOutput = new aws_codepipeline.Artifact();
// Githubソースコード連携のステージ追加
codePipeline.addStage({
stageName: "Source",
actions: [
new aws_codepipeline_actions.GitHubSourceAction({
actionName: "Source",
owner: "Tomoaki-Moriya",
repo: "html-to-pdf-lambda",
branch: "main",
// GitHubの認証トークンをSecretsManagerから取得し、設定
oauthToken: cdk.SecretValue.secretsManager(SECRET_NAME, {
jsonField: GITHUB_OAUTH_TOKEN_KEY,
}),
output: sourceOutput, // ソースコードを成果物としてビルドステージへ渡す
runOrder: 1,
}),
],
});
// ビルドステージを追加し、作成したCodeBuildを指定
codePipeline.addStage({
stageName: "Build",
actions: [
new aws_codepipeline_actions.CodeBuildAction({
actionName: "Build",
project,
input: sourceOutput,
// build.ymlとparam.jsonを成果物としてデプロイステージへ渡す
outputs: [buildOutput],
runOrder: 2,
}),
],
});
// デプロイステージを追加
codePipeline.addStage({
stageName: "Deploy",
actions: [
// CloudFormationでLambda作成スタックを作成
new aws_codepipeline_actions.CloudFormationCreateReplaceChangeSetAction(
{
actionName: "CreateChangeSet",
stackName: STACK_NAME,
changeSetName: `${STACK_NAME}ChangeSet`,
runOrder: 3,
// ビルドステージでtemplate.ymlから作成したbuild.ymlを参照
templatePath: buildOutput.atPath("build.yml"),
// ビルドステージで作成したテンプレートパラメータファイルを参照
templateConfiguration: buildOutput.atPath("param.json"),
// trueに設定することでCloudFormationに対する全権限が付与されたIamが作成される
// 最小限にしたい場合はfalseに設定して任意のIamを作成してアタッチする
// ただしその場合CodePipelineのIamにAssumeRoleできる権限が必要なので注意
adminPermissions: true,
}
),
// 作成したスタックを適用するためのアクションを設定
// このアクションを設定しないとスタックが実行されない
new aws_codepipeline_actions.CloudFormationExecuteChangeSetAction({
actionName: "ExecuteChangeSet",
stackName: STACK_NAME,
changeSetName: `${STACK_NAME}ChangeSet`,
runOrder: 4,
}),
],
});
}
private createCodeBuild() {
// CodeBuildのIAMロールを作成
// ECRへのアクセス権限を付与
const codeBuildRole = new aws_iam.Role(
this,
"LambdaCodePipelineCodeBuildRoleId",
{
assumedBy: new aws_iam.ServicePrincipal("codebuild.amazonaws.com"),
inlinePolicies: {
CodeBuildRolePolicy: new aws_iam.PolicyDocument({
statements: [
new aws_iam.PolicyStatement({
actions: [
"ecr:DescribeRepositories",
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:GetRepositoryPolicy",
"ecr:ListImages",
"ecr:BatchGetImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:PutImage",
],
resources: ["*"],
}),
],
}),
},
}
);
// Iam RoleをアタッチしてCodeBuildを作成
// buildspecで参照する環境変数を設定
return new aws_codebuild.PipelineProject(this, "Project", {
projectName: "LambdaProject",
environment: {
privileged: true, // CodeBuildでDockerを使うために必要(Docker in Docker)
buildImage: aws_codebuild.LinuxBuildImage.AMAZON_LINUX_2_5,
},
environmentVariables: {
AWS_ACCOUNT_ID: {
value: this.account,
type: aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
},
ARTIFACT_S3_BUCKET_NAME: {
value: ARTIFACT_BUCKET_NAME,
type: aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
},
},
role: codeBuildRole,
});
}
private createCodePipeline(artifactBucket: IBucket) {
// CodePipelineのIAMロールを作成しています
// 以下の権限を付与
// - S3(ArtifactBucket)への成果物の取り出しができるように
// - CloudFormationでLambdaを作成する際に必要な権限
// - Lambda関数の作成できるように
// - Lambda関数にIamRoleを作成してアタッチできるように
const codePipelineRole = new aws_iam.Role(
this,
"LambdaCodePipelineRoleId",
{
assumedBy: new aws_iam.ServicePrincipal("codepipeline.amazonaws.com"),
inlinePolicies: {
LambdaCodePipelinePolicy: new aws_iam.PolicyDocument({
statements: [
new aws_iam.PolicyStatement({
actions: ["s3:GetBucket*", "s3:GetObject*", "s3:List*"],
resources: [
`arn:aws:s3:::${ARTIFACT_BUCKET_NAME}/*`,
`arn:aws:s3:::${ARTIFACT_BUCKET_NAME}`,
],
effect: aws_iam.Effect.ALLOW,
}),
new aws_iam.PolicyStatement({
effect: aws_iam.Effect.ALLOW,
actions: [
"iam:GetRole",
"iam:CreateRole",
"iam:TagRole",
"iam:AttachRolePolicy",
"iam:PassRole",
],
resources: ["*"],
}),
new aws_iam.PolicyStatement({
effect: aws_iam.Effect.ALLOW,
actions: [
"lambda:GetFunction",
"lambda:GetFunctionUrlConfig",
"lambda:CreateFunction",
"lambda:CreateFunctionUrlConfig",
"lambda:AddPermission",
"lambda:TagResource",
],
resources: ["*"],
}),
],
}),
},
}
);
// Iam RoleをアタッチしてCodePipelineを作成
return new aws_codepipeline.Pipeline(this, "LambdaCodePipelineId", {
pipelineName: "LambdaCodePipeline",
role: codePipelineRole,
artifactBucket, // 成果物を保存するS3バケット。指定しなければ作成される
});
}
}
Lambda
以下の解説を行います。
- template.yml
- Lambda作成するCloudFormationテンプレート
- buildspec.yml
- CodeBuildで行う処理をコマンドで実装
- プロジェクトルートに配置することでCodeBuildが自動的に実行する
CloudFormation
ECRに登録してあるイメージからAWS Lambdaを作成するだけのシンプルなテンプレートです。
簡易的なAPIとして使用するので関数URLも同時に作成しています。
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Parameters:
ImageTag: # ECRに登録したイメージタグの指定。ビルドステージから受け取る。
Type: String
Default: "latest"
Resources:
Function:
Type: AWS::Serverless::Function
Properties:
PackageType: Image
ImageUri: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/html-to-pdf-lambda:${ImageTag}"
MemorySize: 512
Timeout: 30
Policies:
- AWSLambdaBasicExecutionRole
Metadata:
Dockerfile: Dockerfile
DockerTag: html-to-pdf-lambda
DockerContext: ./
FunctionLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /aws/lambda/Function
Permission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref Function
FunctionUrlAuthType: "NONE"
Action: lambda:InvokeFunctionUrl
Principal: "*"
FunctionUrl:
Type: AWS::Lambda::Url
Properties:
AuthType: NONE
TargetFunctionArn: !GetAtt Function.Arn
Outputs:
FunctionUrl:
Value: !GetAtt FunctionUrl.FunctionUrl
Export:
Name: FunctionUrl
buildspec
単体テストやDockerビルドしています。
またイメージの登録も行うため、ECRに登録するためのリポジトリを検索してなければ作成する処理を入れています。
version: 0.2
env:
variables:
IMAGE_REPOSITORY_NAME: "html-to-pdf-lambda"
phases:
install:
on-failure: ABORT
runtime-versions:
python: 3.11
commands:
- python -m venv venv
- source venv/bin/activate
pre_build:
on-failure: ABORT
commands:
- pip install -r src/requirements.txt
- pip install -r tests/requirements.txt
# ECRがなければ作成する
- aws ecr describe-repositories --repository-names ${IMAGE_REPOSITORY_NAME} > /dev/null 2>&1 || aws ecr create-repository --repository-name ${IMAGE_REPOSITORY_NAME}
build:
on-failure: ABORT
commands:
- PYTHONPATH=src pytest
- docker build -t ${IMAGE_REPOSITORY_NAME} .
# コミットハッシュの頭7桁をイメージタグとして設定
- IMAGE_TAG=$(echo "$CODEBUILD_RESOLVED_SOURCE_VERSION" | head -c 7)
- IMAGE_URI="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_REPOSITORY_NAME}:${IMAGE_TAG}"
- docker tag ${IMAGE_REPOSITORY_NAME} ${IMAGE_URI}
- aws ecr get-login-password --region ${AWS_DEFAULT_REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com
# ビルドしたイメージをECRにpush
- docker push ${IMAGE_URI}
# イメージURIを指定してCloudFormationテンプレートをビルド
- sam package
- --template-file template.yml
- --output-template-file build.yml
- --s3-bucket "${ARTIFACT_S3_BUCKET_NAME}"
- --image-repository "${IMAGE_URI}"
# templateにパラメータを流し込むためのjsonを作成
- echo '{"Parameters":{"ImageTag":"'"${IMAGE_TAG}"'"}}' > param.json
# デプロイステージへビルドしたテンプレートとパラメータ設定を渡す
artifacts:
files:
- "build.yml"
- "param.json"
デプロイしてみる
さっそくCDKをデプロイしてCI/CDを動かしてみます。
成果物格納用のS3作成
cdk deploy ArtifactBucketStack
Githubの認証トークン管理用のSecrets Mangager作成
cdk deploy SecretsManagerStack -c GITHUB_OAUTH_TOKEN=取得した認証トークン
CI/CD作成
cdk deploy LambdaCodePipelineStack
動作確認
デプロイすると初回は自動的にCI/CDが走ります。
コンソールで確認してみましょう。
CodePipeline
問題なく全てのステージが成功してますね。
Lambda
Lambdaもちゃんと作成されてます!
まとめ
いかがだったでしょうか。
今回はDockerイメージのAWS LambdaのCI/CDを作成しましたが、ECRの処理をbuildspec.yml
から除けば普通のAWS Lambdaも同じくデプロイされるはずです。
チームならなおさらですが、CI/CDがあると開発効率がとてもいいです。
CodePipeline、CodeBuild共に永久無料枠が少しあるので、個人利用でも作っているプロダクトがある場合はおすすめです!
素敵な自動デプロイ生活を送りましょう。
Discussion