はじめに
本記事は、私自身の備忘録を兼ねてAWS CDKをこれから始める方の一助になればと思い、AWS CDKの使い方等をまとめたものです。
前回、AWS CDKでLambdaのCI/CD環境を作成しましたが、今回は、その構成を少し変更してLambda LayerのCI/CDにも対応するようにしたいと思います。
前回の記事は、こちら(【AWS CDK (Python)】 LambdaのCI/CD環境をIaCで作る)を参照してください。
なお、本記事は私自身の経験を基に記載していますが、間違いがあったらすみません。
環境
本記事は以下の環境を使用して記載しています。
- AWS Cloud9
- AWS CDK:2.80.0
- Python: 3.10.11
- Node.js: 16.20.0
また、以下の記事に基づいてAWS CDKの環境を作成しています。
LambdaのCI/CD環境をIaCで作る(Layer対応版)
Layerに対応しますが、構成としてはほぼ変わりありません。Lambdaと同様にCodeCommitにソースコードとbuildspecファイルを用意し、CDKでSAM Templateファイルを作成、パイプラインでBuild、Deployします。
Layer対応版のCDKのソースコードは以下のようになりました。
CDKコード全体
#!/usr/bin/env python3
import os
import aws_cdk as cdk
from cdk_app.cdk_lambda_cicd_stack import CdkAppStack
app = cdk.App()
CdkAppStack = CdkAppStack(app, "CdkAppStack",
)
app.synth()
from aws_cdk import (
Stack,
aws_s3 as s3,
RemovalPolicy,
aws_s3_deployment as s3deploy,
aws_iam as iam,
aws_codebuild as codebuild,
aws_codepipeline as codepipeline,
aws_events as events
)
from constructs import Construct
import json
import os
import zipfile
# S3バケットを作成する共通関数
def CreateBucket(scope, bucket_id):
cfn_bucket = s3.CfnBucket(
scope,
bucket_id,
versioning_configuration = s3.CfnBucket.VersioningConfigurationProperty(
status = "Enabled"
),
bucket_name = "cdk-test-{0}-c3hw2bm4".format(bucket_id)
)
return cfn_bucket
# CodeBuild用のIAMロールを作成
def CreateBuildIAMRole(scope, cfn_pkg_s3bucket, artifact_s3bucket):
assume_role_policy_document = {
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "codebuild.amazonaws.com"
}
}
]
}
policy_document = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::{0}/*".format(cfn_pkg_s3bucket.bucket_name),
"arn:aws:s3:::{0}".format(cfn_pkg_s3bucket.bucket_name),
"arn:aws:s3:::{0}/*".format(artifact_s3bucket.bucket_name),
"arn:aws:s3:::{0}".format(artifact_s3bucket.bucket_name)
],
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:GetObjectVersion",
"s3:GetBucketAcl",
"s3:GetBucketLocation"
]
},
{
"Effect": "Allow",
"Resource": "*",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
},
{
"Action": [
"iam:PassRole"
],
"Resource": "*",
"Effect": "Allow"
},
{
"Action": [
"cloudformation:*"
],
"Resource": "*",
"Effect": "Allow"
}
]
}
cfn_role = iam.CfnRole(
scope,
"codebuild-role",
assume_role_policy_document=assume_role_policy_document,
policies=[
iam.CfnRole.PolicyProperty(
policy_document=policy_document,
policy_name="codebuild-policy"
)
],
role_name="codebuild-role"
)
return cfn_role
# CodePipeline用のIAMロールを作成
def CreatePipelineIAMRole(scope, source_s3bucket, artifact_s3bucket):
assume_role_policy_document = {
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "codepipeline.amazonaws.com"
}
}
]
}
policy_document = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"codecommit:CancelUploadArchive",
"codecommit:GetBranch",
"codecommit:GetCommit",
"codecommit:GetUploadArchiveStatus",
"codecommit:UploadArchive"
],
"Resource": "*"
},
{
"Action": [
"codebuild:BatchGetBuilds",
"codebuild:StartBuild",
"codebuild:BatchGetBuildBatches",
"codebuild:StartBuildBatch"
],
"Resource": "*",
"Effect": "Allow"
},
{
"Action": [
"s3:Get*",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::{0}/*".format(source_s3bucket.bucket_name),
"arn:aws:s3:::{0}".format(source_s3bucket.bucket_name)
],
"Effect": "Allow"
},
{
"Action": [
"s3:Get*",
"s3:Put*",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::{0}/*".format(artifact_s3bucket.bucket_name),
"arn:aws:s3:::{0}".format(artifact_s3bucket.bucket_name)
],
"Effect": "Allow"
},
{
"Action": [
"cloudformation:DescribeStacks",
"cloudformation:DescribeChangeSet",
"cloudformation:CreateChangeSet",
"cloudformation:ExecuteChangeSet",
"cloudformation:DeleteChangeSet"
],
"Resource": [
"arn:aws:cloudformation:{0}:{1}:stack/lambda-stack-*".format("ap-northeast-1", os.environ["CDK_DEFAULT_ACCOUNT"])
],
"Effect": "Allow"
},
{
"Action": [
"cloudwatch:*"
],
"Resource": "*",
"Effect": "Allow"
},
{
"Action": [
"iam:PassRole"
],
"Resource": "*",
"Effect": "Allow"
}
]
}
cfn_role = iam.CfnRole(
scope,
"codepipeline-role",
assume_role_policy_document=assume_role_policy_document,
policies=[
iam.CfnRole.PolicyProperty(
policy_document=policy_document,
policy_name="codepipeline-policy"
)
],
role_name="codepipeline-role"
)
return cfn_role
# LambdaFunction用のIAMロールを作成
def CreateLambdaIAMRole(scope):
assume_role_policy_document = {
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
]
}
cfn_role = iam.CfnRole(
scope,
"lambda-role",
assume_role_policy_document=assume_role_policy_document,
managed_policy_arns=[
"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
],
role_name="lambda-role"
)
return cfn_role
# EvantBridge用のIAMロールを作成
def CreateExecPipelineIAMRole(scope, pipeline_arn):
assume_role_policy_document = {
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "events.amazonaws.com"
}
}
]
}
policy_document = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "codepipeline:StartPipelineExecution",
"Resource": pipeline_arn
}
]
}
cfn_role = iam.CfnRole(
scope,
"exec-codepipeline-role",
assume_role_policy_document=assume_role_policy_document,
policies=[
iam.CfnRole.PolicyProperty(
policy_document=policy_document,
policy_name="exec-codepipeline-policy"
)
],
role_name="exec-codepipeline-role"
)
return cfn_role
# ↓↓ Layer対応版 追加コード
# LambdaLayer作成用のSAM Templateファイルを作成
def EditSAMLayerTemplate(layer_id):
sam_template_data = {
"AWSTemplateFormatVersion": "2010-09-09",
"Transform": "AWS::Serverless-2016-10-31",
"Description": "An AWS Serverless Specification template describing your layer.",
"Resources": {
"TestLambdLayer": {
"Type": "AWS::Serverless::LayerVersion",
"Properties": {
"LayerName": "",
"ContentUri": "",
"CompatibleRuntimes": ["python3.10"]
}
},
"TestLayerArnParameter": {
"Type": "AWS::SSM::Parameter",
"Properties": {
"Name": "",
"Type": "String",
"Value": { "Ref" : "TestLambdLayer" }
}
}
}
}
sam_template_data["Resources"]["TestLambdLayer"]["Properties"]["ContentUri"] = layer_id
sam_template_data["Resources"]["TestLambdLayer"]["Properties"]["LayerName"] = layer_id
sam_template_data["Resources"]["TestLayerArnParameter"]["Properties"]["Name"] =\
"/test/layer_arn/{0}".format(layer_id)
return sam_template_data
# ↑↑ Layer対応版 追加コード
# LambdaFunction作成用のSAM Templateファイルを作成
# ↓↓ Layer対応版 変更コード
def EditSAMTemplate(lambda_id, lambda_role, layer_id):
# def EditSAMTemplate(lambda_id, lambda_role):
# ↑↑ Layer対応版 変更コード
sam_template_data = {
"AWSTemplateFormatVersion": "2010-09-09",
"Transform": "AWS::Serverless-2016-10-31",
"Resources": {
"TestLambdaFunction": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "lambda_function.lambda_handler",
"Runtime": "python3.10",
"CodeUri": "",
"Description": "",
"FunctionName": "",
"AutoPublishAlias": "",
# ↓↓ Layer対応版 追加コード
"AutoPublishAliasAllProperties": true,
# ↑↑ Layer対応版 追加コード
"MemorySize": 128,
"Timeout": 3,
"Role": "",
"Environment": {
"Variables": {
}
},
"DeploymentPreference": {
"Enabled": True,
"Type": "AllAtOnce"
}
}
}
}
}
sam_template_data["Resources"]["TestLambdaFunction"]["Properties"]["CodeUri"] = lambda_id
sam_template_data["Resources"]["TestLambdaFunction"]["Properties"]["FunctionName"] = lambda_id
sam_template_data["Resources"]["TestLambdaFunction"]["Properties"]["AutoPublishAlias"] = "Dev"
sam_template_data["Resources"]["TestLambdaFunction"]["Properties"]["Role"] =\
"arn:aws:iam::{0}:role/{1}".format(os.environ["CDK_DEFAULT_ACCOUNT"], lambda_role.role_name)
sam_template_data["Resources"]["TestLambdaFunction"]["Properties"]["Environment"]["Variables"] =\
{"test_variables": "test"}
# ↓↓ Layer対応版 追加コード
sam_template_data["Resources"]["TestLambdaFunction"]["Properties"]["Environment"]["Variables"]["layer"] =\
"{{resolve:ssm:" + "/test/layer_arn/{0}".format(layer_id) + "}}"
sam_template_data["Resources"]["TestLambdaFunction"]["Properties"]["Layers"] =\
["{{resolve:ssm:" + "/test/layer_arn/{0}".format(layer_id) + "}}"]
# ↑↑ Layer対応版 追加コード
return sam_template_data
# CodeCommitリポジトリの変更を検知するEventBridgeルールの作成
def CodecommitEvents(scope, rep_name, pipeline_arn, exec_pipeline_role):
event_pattern = {
"source": ["aws.codecommit"],
"detail-type": ["CodeCommit Repository State Change"],
"resources": [
"arn:aws:codecommit:{0}:{1}:{2}".format(
"ap-northeast-1", os.environ["CDK_DEFAULT_ACCOUNT"], rep_name)
],
"detail": {
"event": ["referenceCreated", "referenceUpdated"],
"referenceType": ["branch"],
"referenceName": ["main"]
}
}
cfn_rule = events.CfnRule(
scope,
"change-repository-events",
event_pattern=event_pattern,
name="change-repository-events",
state="ENABLED",
targets=[
events.CfnRule.TargetProperty(
arn=pipeline_arn,
id="change-repository-events",
role_arn=exec_pipeline_role.attr_arn,
)
]
)
class CdkAppStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
lambda_id = "TestFunc"
# ↓↓ Layer対応版 追加コード
layer_id = "TestLayer"
# ↑↑ Layer対応版 追加コード
repository_name = "test-lambda-rep"
# buildspec、SAM TemplateファイルをアップロードするS3バケットを作成
source_s3bucket = CreateBucket(self, "source-bucket")
source_s3bucket_l2 = s3.Bucket.from_bucket_arn(
self, "bucket_l1tol2-{0}".format(source_s3bucket.bucket_name), source_s3bucket.attr_arn)
# cloudformation packageのパッケージファイルをアップロードするS3バケットを作成
cfn_pkg_s3bucket = CreateBucket(self, "cfn-pkg")
# アーティファクトバケットを作成
artifact_s3bucket = CreateBucket(self, "artifact-bucket")
# CodeBuild用のIAMロールの作成
build_role = CreateBuildIAMRole(self, cfn_pkg_s3bucket, artifact_s3bucket)
# CodePipeline用のIAMロールの作成
pipeline_role = CreatePipelineIAMRole(self, source_s3bucket, artifact_s3bucket)
# LambdaFunction用のIAMロールの作成
lambda_role = CreateLambdaIAMRole(self)
zip_dir = "./cdk_app/work/"
zip_file = "template_{0}.zip".format(lambda_id)
zip_path = zip_dir + zip_file
template_file_name = "sam-template"
# SAM Templateファイルの編集
sam_template_data = EditSAMTemplate(lambda_id, lambda_role, layer_id)
# sam_template_data = EditSAMTemplate(lambda_id, lambda_role)
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_STORED) as z:
z.writestr(template_file_name + ".json", json.dumps(sam_template_data, indent=4))
# ↓↓ Layer対応版 追加コード
layer_zip_file = "template_{0}.zip".format(layer_id)
layer_zip_path = zip_dir + layer_zip_file
layer_template_file_name = "sam-layer-template"
# SAM Templateファイルの編集
sam_layer_template_data = EditSAMLayerTemplate(layer_id)
with zipfile.ZipFile(layer_zip_path, 'w', zipfile.ZIP_STORED) as z:
z.writestr(layer_template_file_name + ".json", json.dumps(sam_layer_template_data, indent=4))
# ↑↑ Layer対応版 追加コード
# 作成したSAM TemplateファイルをS3へアップロード
s3_deploy = s3deploy.BucketDeployment(
self,
"TemplateFileUploadS3",
sources=[s3deploy.Source.asset(zip_dir)],
destination_bucket=source_s3bucket_l2,
)
# Buildプロジェクトの作成
cfn_project = codebuild.CfnProject(
self,
"build-lambda-project",
artifacts=codebuild.CfnProject.ArtifactsProperty(
type="CODEPIPELINE",
),
environment=codebuild.CfnProject.EnvironmentProperty(
compute_type="BUILD_GENERAL1_SMALL",
image="aws/codebuild/standard:7.0",
type="LINUX_CONTAINER",
privileged_mode=False,
environment_variables=[
codebuild.CfnProject.EnvironmentVariableProperty(
name="TEMPLATE_FILE_NAME",
value=template_file_name,
),
codebuild.CfnProject.EnvironmentVariableProperty(
name="CFN_PKG_BUCKET",
value=cfn_pkg_s3bucket.bucket_name,
)
],
),
service_role=build_role.attr_arn,
source=codebuild.CfnProject.SourceProperty(
type="CODEPIPELINE",
build_spec="buildspec_{0}.yaml".format(lambda_id)
),
logs_config=codebuild.CfnProject.LogsConfigProperty(
cloud_watch_logs=codebuild.CfnProject.CloudWatchLogsConfigProperty(
status="ENABLED",
),
),
name="build_lambda_project",
)
# ↓↓ Layer対応版 追加コード
# Buildプロジェクト(Layer)の作成
cfn_layer_project = codebuild.CfnProject(
self,
"build-layer-project",
artifacts=codebuild.CfnProject.ArtifactsProperty(
type="CODEPIPELINE",
),
environment=codebuild.CfnProject.EnvironmentProperty(
compute_type="BUILD_GENERAL1_SMALL",
image="aws/codebuild/standard:7.0",
type="LINUX_CONTAINER",
privileged_mode=False,
environment_variables=[
codebuild.CfnProject.EnvironmentVariableProperty(
name="TEMPLATE_FILE_NAME",
value=layer_template_file_name,
),
codebuild.CfnProject.EnvironmentVariableProperty(
name="CFN_PKG_BUCKET",
value=cfn_pkg_s3bucket.bucket_name,
)
],
),
service_role=build_role.attr_arn,
source=codebuild.CfnProject.SourceProperty(
type="CODEPIPELINE",
build_spec="buildspec_{0}.yaml".format(layer_id)
),
logs_config=codebuild.CfnProject.LogsConfigProperty(
cloud_watch_logs=codebuild.CfnProject.CloudWatchLogsConfigProperty(
status="ENABLED",
),
),
name="build_layer_project",
)
# ↑↑ Layer対応版 追加コード
# CodePipelineの作成
source_actions = []
# Sourceアクション(CodeCommit(Lambdaコード、buildspec))の作成
source_actions.append(
codepipeline.CfnPipeline.ActionDeclarationProperty(
action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
category="Source",
owner="AWS",
provider="CodeCommit",
version="1"
),
name="Source_codecommit",
configuration={
"RepositoryName": repository_name,
"BranchName": "main",
"PollForSourceChanges": False
},
namespace="SourceVariables_codecommit",
output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
name="SourceArtifact_codecommit"
)],
region="ap-northeast-1",
run_order=1
)
)
# Sourceアクション(SAM Template)の作成
source_actions.append(
codepipeline.CfnPipeline.ActionDeclarationProperty(
action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
category="Source",
owner="AWS",
provider="S3",
version="1"
),
name="Source_template",
configuration={
"S3Bucket": source_s3bucket.bucket_name,
"S3ObjectKey": zip_file,
"PollForSourceChanges": False
},
namespace="SourceVariables_Template",
output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
name="SourceArtifact_template"
)],
region="ap-northeast-1",
run_order=1
)
)
# ↓↓ Layer対応版 追加コード
# Sourceアクション(LayerのSAM Template)の作成
source_actions.append(
codepipeline.CfnPipeline.ActionDeclarationProperty(
action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
category="Source",
owner="AWS",
provider="S3",
version="1"
),
name="Source_layer_template",
configuration={
"S3Bucket": source_s3bucket.bucket_name,
"S3ObjectKey": layer_zip_file,
"PollForSourceChanges": False
},
namespace="SourceVariables_LayerTemplate",
output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
name="SourceArtifact_layer_template"
)],
region="ap-northeast-1",
run_order=1
)
)
# ↑↑ Layer対応版 追加コード
# Buildアクションの作成
build_actions = []
build_actions.append(
codepipeline.CfnPipeline.ActionDeclarationProperty(
action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
category="Build",
owner="AWS",
provider="CodeBuild",
version="1"
),
name="Build",
configuration={
"ProjectName": cfn_project.name,
"PrimarySource": "SourceArtifact_codecommit"
},
input_artifacts=[
codepipeline.CfnPipeline.InputArtifactProperty(
name="SourceArtifact_codecommit"
),
codepipeline.CfnPipeline.InputArtifactProperty(
name="SourceArtifact_template"
)
],
namespace="BuildVariables",
output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
name="BuildArtifact"
)],
region="ap-northeast-1",
run_order=1
)
)
# ↓↓ Layer対応版 追加コード
# Buildアクション(Layer)の作成
build_actions.append(
codepipeline.CfnPipeline.ActionDeclarationProperty(
action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
category="Build",
owner="AWS",
provider="CodeBuild",
version="1"
),
name="Build_layer",
configuration={
"ProjectName": cfn_layer_project.name,
"PrimarySource": "SourceArtifact_codecommit"
},
input_artifacts=[
codepipeline.CfnPipeline.InputArtifactProperty(
name="SourceArtifact_codecommit"
),
codepipeline.CfnPipeline.InputArtifactProperty(
name="SourceArtifact_layer_template"
)
],
namespace="BuildLayerVariables",
output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
name="BuildLayerArtifact"
)],
region="ap-northeast-1",
run_order=1
)
)
# ↑↑ Layer対応版 追加コード
deploy_actions = []
# ↓↓ Layer対応版 追加コード
# Deployアクション(Layerの変更セットの作成)の作成
deploy_actions.append(
codepipeline.CfnPipeline.ActionDeclarationProperty(
action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
category="Deploy",
owner="AWS",
provider="CloudFormation",
version="1"
),
name="CreateLayerChangeSet",
configuration={
"ActionMode": "CHANGE_SET_REPLACE",
"RoleArn": "arn:aws:iam::{0}:role/cdk-hnb659fds-cfn-exec-role-{0}-{1}".\
format(os.environ["CDK_DEFAULT_ACCOUNT"], "ap-northeast-1"),
"StackName": "lambda-stack-{0}".format(layer_id),
"ChangeSetName": "lambda-stack-{0}-changeSet".format(layer_id),
"Capabilities": "CAPABILITY_NAMED_IAM",
"TemplatePath": "BuildLayerArtifact::{0}".format("out-{0}.yaml".format(layer_template_file_name))
},
input_artifacts=[
codepipeline.CfnPipeline.InputArtifactProperty(
name="BuildLayerArtifact"
)
],
namespace="DeployCreateLayerVariables",
region="ap-northeast-1",
run_order=1
)
)
# Deployアクション(Layerの変更セットの実行)の作成
deploy_actions.append(
codepipeline.CfnPipeline.ActionDeclarationProperty(
action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
category="Deploy",
owner="AWS",
provider="CloudFormation",
version="1"
),
name="ExecuteLayerChangeSet",
configuration={
"ActionMode": "CHANGE_SET_EXECUTE",
"ChangeSetName": "lambda-stack-{0}-changeSet".format(layer_id),
"StackName": "lambda-stack-{0}".format(layer_id)
},
input_artifacts=[
codepipeline.CfnPipeline.InputArtifactProperty(
name="BuildLayerArtifact"
)
],
namespace="DeployExecuteLayerVariables",
region="ap-northeast-1",
run_order=2
)
)
# ↑↑ Layer対応版
# Deployアクション(変更セットの作成)の作成
deploy_actions.append(
codepipeline.CfnPipeline.ActionDeclarationProperty(
action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
category="Deploy",
owner="AWS",
provider="CloudFormation",
version="1"
),
name="CreateChangeSet",
configuration={
"ActionMode": "CHANGE_SET_REPLACE",
"RoleArn": "arn:aws:iam::{0}:role/cdk-hnb659fds-cfn-exec-role-{0}-{1}".\
format(os.environ["CDK_DEFAULT_ACCOUNT"], "ap-northeast-1"),
"StackName": "lambda-stack-{0}".format(lambda_id),
"ChangeSetName": "lambda-stack-{0}-changeSet".format(lambda_id),
"Capabilities": "CAPABILITY_NAMED_IAM",
"TemplatePath": "BuildArtifact::{0}".format("out-{0}.yaml".format(template_file_name))
},
input_artifacts=[
codepipeline.CfnPipeline.InputArtifactProperty(
name="BuildArtifact"
)
],
namespace="DeployCreateVariables",
region="ap-northeast-1",
run_order=3
# run_order=1
)
)
# Deployアクション(変更セットの実行)の作成
deploy_actions.append(
codepipeline.CfnPipeline.ActionDeclarationProperty(
action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
category="Deploy",
owner="AWS",
provider="CloudFormation",
version="1"
),
name="ExecuteChangeSet",
configuration={
"ActionMode": "CHANGE_SET_EXECUTE",
"ChangeSetName": "lambda-stack-{0}-changeSet".format(lambda_id),
"StackName": "lambda-stack-{0}".format(lambda_id)
},
input_artifacts=[
codepipeline.CfnPipeline.InputArtifactProperty(
name="BuildArtifact"
)
],
namespace="DeployExecuteVariables",
region="ap-northeast-1",
run_order=4
# run_order=2
)
)
# Pipelineの作成
cfn_pipeline = codepipeline.CfnPipeline(
self,
"pipeline-lambda",
role_arn=pipeline_role.attr_arn,
stages=[
codepipeline.CfnPipeline.StageDeclarationProperty(
actions=source_actions,
name="Source",
),
codepipeline.CfnPipeline.StageDeclarationProperty(
actions=build_actions,
name="Build",
),
codepipeline.CfnPipeline.StageDeclarationProperty(
actions=deploy_actions,
name="Deploy",
)
],
artifact_store=codepipeline.CfnPipeline.ArtifactStoreProperty(
location=artifact_s3bucket.ref,
type="S3",
),
name="pipeline_lambda",
restart_execution_on_update=False
)
cfn_pipeline.node.add_dependency(s3_deploy)
pipeline_arn = "arn:aws:codepipeline:{0}:{1}:{2}".format(
"ap-northeast-1", os.environ["CDK_DEFAULT_ACCOUNT"], cfn_pipeline.name)
# EventBridge用のIAMロールの作成
exec_pipeline_role = CreateExecPipelineIAMRole(self, pipeline_arn)
# CodeCommitリポジトリの変更を検知するEventBridgeルールの作成
CodecommitEvents(self, repository_name, pipeline_arn, exec_pipeline_role)
以下は、CodeCommitリポジトリに用意するファイルです。
TestFunc/lambda_function.pyとbuildspec_TestFunc.yamlは、Layerの確認のため、少し前回と変更しています。
from test_util import test_message
def lambda_handler(event, context):
print(test_message())
version: 0.2
phases:
install:
commands:
- aws --version
- cp -pr ../0[1-9]/* .
- ls -lR ./
build:
commands:
- aws cloudformation package --template-file ${TEMPLATE_FILE_NAME}.json --s3-bucket ${CFN_PKG_BUCKET} --output-template-file out-${TEMPLATE_FILE_NAME}.yaml
artifacts:
files:
- out-${TEMPLATE_FILE_NAME}.yaml
def test_message():
return "test message!"
version: 0.2
phases:
install:
commands:
- aws --version
- cp -pr ../0[1-9]/* .
- ls -lR ./
build:
commands:
- aws cloudformation package --template-file ${TEMPLATE_FILE_NAME}.json --s3-bucket ${CFN_PKG_BUCKET} --output-template-file out-${TEMPLATE_FILE_NAME}.yaml
artifacts:
files:
- out-${TEMPLATE_FILE_NAME}.yaml
では、変更箇所を処理毎に順に説明します。
SAM Templateファイルの作成
Lambdaと同じように、Layerの設定を定義したSAM Templateファイルを作成します。
Layerの定義に加えて、LayerのARNをパラメータストアに格納するようにします。併せて、Lambdaの定義にLayerの設定を追加し、ARNをパラメータストアから読み込むようにします。
パラメータストアを使用せずにスタック参照(ExportValue&ImportValue)でもいけるかと思いましたが、Layerの更新の際にエラーになるため、パラメータストアを使用することにしました。
あと、環境変数にLayerのARNを設定するようにしています。Layerだけ更新された場合に、Lambdaのバージョンが作成されない(バグらしいですが。。)ため、Layerに伴って環境変数が変更されることで、Lambdaもバージョンが作成されるようにしています。
作成したSAM Templateファイルは、zip化した後、LambdaのSAM Templateファイルと一緒にaws_s3_deploymentでS3バケットへアップロードします。
(中略)
# ↓↓ Layer対応版 追加コード
# LambdaLayer作成用のSAM Templateファイルを作成
def EditSAMLayerTemplate(layer_id):
sam_template_data = {
"AWSTemplateFormatVersion": "2010-09-09",
"Transform": "AWS::Serverless-2016-10-31",
"Description": "An AWS Serverless Specification template describing your layer.",
"Resources": {
"TestLambdLayer": {
"Type": "AWS::Serverless::LayerVersion",
"Properties": {
"LayerName": "",
"ContentUri": "",
"CompatibleRuntimes": ["python3.10"]
}
},
"TestLayerArnParameter": {
"Type": "AWS::SSM::Parameter",
"Properties": {
"Name": "",
"Type": "String",
"Value": { "Ref" : "TestLambdLayer" }
}
}
}
}
sam_template_data["Resources"]["TestLambdLayer"]["Properties"]["ContentUri"] = layer_id
sam_template_data["Resources"]["TestLambdLayer"]["Properties"]["LayerName"] = layer_id
sam_template_data["Resources"]["TestLayerArnParameter"]["Properties"]["Name"] =\
"/test/layer_arn/{0}".format(layer_id)
return sam_template_data
# ↑↑ Layer対応版 追加コード
# LambdaFunction作成用のSAM Templateファイルを作成
# ↓↓ Layer対応版 変更コード
def EditSAMTemplate(lambda_id, lambda_role, layer_id):
# def EditSAMTemplate(lambda_id, lambda_role):
# ↑↑ Layer対応版 変更コード
sam_template_data = {
"AWSTemplateFormatVersion": "2010-09-09",
"Transform": "AWS::Serverless-2016-10-31",
"Resources": {
"TestLambdaFunction": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "lambda_function.lambda_handler",
"Runtime": "python3.10",
"CodeUri": "",
"Description": "",
"FunctionName": "",
"AutoPublishAlias": "",
# ↓↓ Layer対応版 追加コード
"AutoPublishAliasAllProperties": true,
# ↑↑ Layer対応版 追加コード
"MemorySize": 128,
"Timeout": 3,
"Role": "",
"Environment": {
"Variables": {
}
},
"DeploymentPreference": {
"Enabled": True,
"Type": "AllAtOnce"
}
}
}
}
}
sam_template_data["Resources"]["TestLambdaFunction"]["Properties"]["CodeUri"] = lambda_id
sam_template_data["Resources"]["TestLambdaFunction"]["Properties"]["FunctionName"] = lambda_id
sam_template_data["Resources"]["TestLambdaFunction"]["Properties"]["AutoPublishAlias"] = "Dev"
sam_template_data["Resources"]["TestLambdaFunction"]["Properties"]["Role"] =\
"arn:aws:iam::{0}:role/{1}".format(os.environ["CDK_DEFAULT_ACCOUNT"], lambda_role.role_name)
sam_template_data["Resources"]["TestLambdaFunction"]["Properties"]["Environment"]["Variables"] =\
{"test_variables": "test"}
# ↓↓ Layer対応版 追加コード
sam_template_data["Resources"]["TestLambdaFunction"]["Properties"]["Environment"]["Variables"]["layer"] =\
"{{resolve:ssm:" + "/test/layer_arn/{0}".format(layer_id) + "}}"
sam_template_data["Resources"]["TestLambdaFunction"]["Properties"]["Layers"] =\
["{{resolve:ssm:" + "/test/layer_arn/{0}".format(layer_id) + "}}"]
# ↑↑ Layer対応版 追加コード
return sam_template_data
(中略)
class CdkAppStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
lambda_id = "TestFunc"
# ↓↓ Layer対応版 追加コード
layer_id = "TestLayer"
# ↑↑ Layer対応版 追加コード
repository_name = "test-lambda-rep"
(中略)
zip_dir = "./cdk_app/work/"
zip_file = "template_{0}.zip".format(lambda_id)
zip_path = zip_dir + zip_file
template_file_name = "sam-template"
# SAM Templateファイルの編集
sam_template_data = EditSAMTemplate(lambda_id, lambda_role, layer_id)
# sam_template_data = EditSAMTemplate(lambda_id, lambda_role)
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_STORED) as z:
z.writestr(template_file_name + ".json", json.dumps(sam_template_data, indent=4))
# ↓↓ Layer対応版 追加コード
layer_zip_file = "template_{0}.zip".format(layer_id)
layer_zip_path = zip_dir + layer_zip_file
layer_template_file_name = "sam-layer-template"
# SAM Templateファイルの編集
sam_layer_template_data = EditSAMLayerTemplate(layer_id)
with zipfile.ZipFile(layer_zip_path, 'w', zipfile.ZIP_STORED) as z:
z.writestr(layer_template_file_name + ".json", json.dumps(sam_layer_template_data, indent=4))
# ↑↑ Layer対応版 追加コード
# 作成したSAM TemplateファイルをS3へアップロード
s3_deploy = s3deploy.BucketDeployment(
self,
"TemplateFileUploadS3",
sources=[s3deploy.Source.asset(zip_dir)],
destination_bucket=source_s3bucket_l2,
)
(中略)
Sourceアクションの作成
LayerのSAMテンプレートファイル分のSourceアクションを追加します。(今回、CodeCommitリポジトリはLambdaソースコードと同じにしているので、追加はしていません。)
(中略)
class CdkAppStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
lambda_id = "TestFunc"
# ↓↓ Layer対応版 追加コード
layer_id = "TestLayer"
# ↑↑ Layer対応版 追加コード
repository_name = "test-lambda-rep"
(中略)
source_actions = []
# Sourceアクション(CodeCommit(Lambdaコード、buildspec))の作成
source_actions.append(
codepipeline.CfnPipeline.ActionDeclarationProperty(
action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
category="Source",
owner="AWS",
provider="CodeCommit",
version="1"
),
name="Source_codecommit",
configuration={
"RepositoryName": repository_name,
"BranchName": "main",
"PollForSourceChanges": False
},
namespace="SourceVariables_codecommit",
output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
name="SourceArtifact_codecommit"
)],
region="ap-northeast-1",
run_order=1
)
)
# Sourceアクション(SAM Template)の作成
source_actions.append(
codepipeline.CfnPipeline.ActionDeclarationProperty(
action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
category="Source",
owner="AWS",
provider="S3",
version="1"
),
name="Source_template",
configuration={
"S3Bucket": source_s3bucket.bucket_name,
"S3ObjectKey": zip_file,
"PollForSourceChanges": False
},
namespace="SourceVariables_Template",
output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
name="SourceArtifact_template"
)],
region="ap-northeast-1",
run_order=1
)
)
# ↓↓ Layer対応版 追加コード
# Sourceアクション(LayerのSAM Template)の作成
source_actions.append(
codepipeline.CfnPipeline.ActionDeclarationProperty(
action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
category="Source",
owner="AWS",
provider="S3",
version="1"
),
name="Source_layer_template",
configuration={
"S3Bucket": source_s3bucket.bucket_name,
"S3ObjectKey": layer_zip_file,
"PollForSourceChanges": False
},
namespace="SourceVariables_LayerTemplate",
output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
name="SourceArtifact_layer_template"
)],
region="ap-northeast-1",
run_order=1
)
)
# ↑↑ Layer対応版 追加コード
(中略)
CodeBuildプロジェクト、Buildアクションの作成
Layer分のCodeBuildプロジェクトを作成し、そのCodeBuildプロジェクトを実行するパイプラインのBuildアクションを作成します。
内容としては、ほぼLambdaと同じです。(すみません。共通化をサボりました。。)
(中略)
class CdkAppStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
lambda_id = "TestFunc"
# ↓↓ Layer対応版 追加コード
layer_id = "TestLayer"
# ↑↑ Layer対応版 追加コード
repository_name = "test-lambda-rep"
(中略)
# Buildプロジェクトの作成
cfn_project = codebuild.CfnProject(
self,
"build-lambda-project",
artifacts=codebuild.CfnProject.ArtifactsProperty(
type="CODEPIPELINE",
),
environment=codebuild.CfnProject.EnvironmentProperty(
compute_type="BUILD_GENERAL1_SMALL",
image="aws/codebuild/standard:7.0",
type="LINUX_CONTAINER",
privileged_mode=False,
environment_variables=[
codebuild.CfnProject.EnvironmentVariableProperty(
name="TEMPLATE_FILE_NAME",
value=template_file_name,
),
codebuild.CfnProject.EnvironmentVariableProperty(
name="CFN_PKG_BUCKET",
value=cfn_pkg_s3bucket.bucket_name,
)
],
),
service_role=build_role.attr_arn,
source=codebuild.CfnProject.SourceProperty(
type="CODEPIPELINE",
build_spec="buildspec_{0}.yaml".format(lambda_id)
),
logs_config=codebuild.CfnProject.LogsConfigProperty(
cloud_watch_logs=codebuild.CfnProject.CloudWatchLogsConfigProperty(
status="ENABLED",
),
),
name="build_lambda_project",
)
# ↓↓ Layer対応版 追加コード
# Buildプロジェクト(Layer)の作成
cfn_layer_project = codebuild.CfnProject(
self,
"build-layer-project",
artifacts=codebuild.CfnProject.ArtifactsProperty(
type="CODEPIPELINE",
),
environment=codebuild.CfnProject.EnvironmentProperty(
compute_type="BUILD_GENERAL1_SMALL",
image="aws/codebuild/standard:7.0",
type="LINUX_CONTAINER",
privileged_mode=False,
environment_variables=[
codebuild.CfnProject.EnvironmentVariableProperty(
name="TEMPLATE_FILE_NAME",
value=layer_template_file_name,
),
codebuild.CfnProject.EnvironmentVariableProperty(
name="CFN_PKG_BUCKET",
value=cfn_pkg_s3bucket.bucket_name,
)
],
),
service_role=build_role.attr_arn,
source=codebuild.CfnProject.SourceProperty(
type="CODEPIPELINE",
build_spec="buildspec_{0}.yaml".format(layer_id)
),
logs_config=codebuild.CfnProject.LogsConfigProperty(
cloud_watch_logs=codebuild.CfnProject.CloudWatchLogsConfigProperty(
status="ENABLED",
),
),
name="build_layer_project",
)
# ↑↑ Layer対応版 追加コード
(中略)
# Buildアクションの作成
build_actions = []
build_actions.append(
codepipeline.CfnPipeline.ActionDeclarationProperty(
action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
category="Build",
owner="AWS",
provider="CodeBuild",
version="1"
),
name="Build",
configuration={
"ProjectName": cfn_project.name,
"PrimarySource": "SourceArtifact_codecommit"
},
input_artifacts=[
codepipeline.CfnPipeline.InputArtifactProperty(
name="SourceArtifact_codecommit"
),
codepipeline.CfnPipeline.InputArtifactProperty(
name="SourceArtifact_template"
)
],
namespace="BuildVariables",
output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
name="BuildArtifact"
)],
region="ap-northeast-1",
run_order=1
)
)
# ↓↓ Layer対応版 追加コード
# Buildアクション(Layer)の作成
build_actions.append(
codepipeline.CfnPipeline.ActionDeclarationProperty(
action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
category="Build",
owner="AWS",
provider="CodeBuild",
version="1"
),
name="Build_layer",
configuration={
"ProjectName": cfn_layer_project.name,
"PrimarySource": "SourceArtifact_codecommit"
},
input_artifacts=[
codepipeline.CfnPipeline.InputArtifactProperty(
name="SourceArtifact_codecommit"
),
codepipeline.CfnPipeline.InputArtifactProperty(
name="SourceArtifact_layer_template"
)
],
namespace="BuildLayerVariables",
output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
name="BuildLayerArtifact"
)],
region="ap-northeast-1",
run_order=1
)
)
# ↑↑ Layer対応版 追加コード
(中略)
version: 0.2
phases:
install:
commands:
- aws --version
- cp -pr ../0[1-9]/* .
- ls -lR ./
build:
commands:
- aws cloudformation package --template-file ${TEMPLATE_FILE_NAME}.json --s3-bucket ${CFN_PKG_BUCKET} --output-template-file out-${TEMPLATE_FILE_NAME}.yaml
artifacts:
files:
- out-${TEMPLATE_FILE_NAME}.yaml
version: 0.2
phases:
install:
commands:
- aws --version
- cp -pr ../0[1-9]/* .
- ls -lR ./
build:
commands:
- aws cloudformation package --template-file ${TEMPLATE_FILE_NAME}.json --s3-bucket ${CFN_PKG_BUCKET} --output-template-file out-${TEMPLATE_FILE_NAME}.yaml
artifacts:
files:
- out-${TEMPLATE_FILE_NAME}.yaml
Deployアクションの作成
パイプラインに組み込むDeployアクションを作成します。
ここもほほLambdaの時と同じですね。
LambdaよりLayerが先にデプロイされるようにrun_orderの値を設定します。
(中略)
class CdkAppStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
lambda_id = "TestFunc"
# ↓↓ Layer対応版 追加コード
layer_id = "TestLayer"
# ↑↑ Layer対応版 追加コード
repository_name = "test-lambda-rep"
(中略)
deploy_actions = []
# ↓↓ Layer対応版 追加コード
# Deployアクション(Layerの変更セットの作成)の作成
deploy_actions.append(
codepipeline.CfnPipeline.ActionDeclarationProperty(
action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
category="Deploy",
owner="AWS",
provider="CloudFormation",
version="1"
),
name="CreateLayerChangeSet",
configuration={
"ActionMode": "CHANGE_SET_REPLACE",
"RoleArn": "arn:aws:iam::{0}:role/cdk-hnb659fds-cfn-exec-role-{0}-{1}".\
format(os.environ["CDK_DEFAULT_ACCOUNT"], "ap-northeast-1"),
"StackName": "lambda-stack-{0}".format(layer_id),
"ChangeSetName": "lambda-stack-{0}-changeSet".format(layer_id),
"Capabilities": "CAPABILITY_NAMED_IAM",
"TemplatePath": "BuildLayerArtifact::{0}".format("out-{0}.yaml".format(layer_template_file_name))
},
input_artifacts=[
codepipeline.CfnPipeline.InputArtifactProperty(
name="BuildLayerArtifact"
)
],
namespace="DeployCreateLayerVariables",
region="ap-northeast-1",
run_order=1
)
)
# Deployアクション(Layerの変更セットの実行)の作成
deploy_actions.append(
codepipeline.CfnPipeline.ActionDeclarationProperty(
action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
category="Deploy",
owner="AWS",
provider="CloudFormation",
version="1"
),
name="ExecuteLayerChangeSet",
configuration={
"ActionMode": "CHANGE_SET_EXECUTE",
"ChangeSetName": "lambda-stack-{0}-changeSet".format(layer_id),
"StackName": "lambda-stack-{0}".format(layer_id)
},
input_artifacts=[
codepipeline.CfnPipeline.InputArtifactProperty(
name="BuildLayerArtifact"
)
],
namespace="DeployExecuteLayerVariables",
region="ap-northeast-1",
run_order=2
)
)
# ↑↑ Layer対応版
# Deployアクション(変更セットの作成)の作成
deploy_actions.append(
codepipeline.CfnPipeline.ActionDeclarationProperty(
action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
category="Deploy",
owner="AWS",
provider="CloudFormation",
version="1"
),
name="CreateChangeSet",
configuration={
"ActionMode": "CHANGE_SET_REPLACE",
"RoleArn": "arn:aws:iam::{0}:role/cdk-hnb659fds-cfn-exec-role-{0}-{1}".\
format(os.environ["CDK_DEFAULT_ACCOUNT"], "ap-northeast-1"),
"StackName": "lambda-stack-{0}".format(lambda_id),
"ChangeSetName": "lambda-stack-{0}-changeSet".format(lambda_id),
"Capabilities": "CAPABILITY_NAMED_IAM",
"TemplatePath": "BuildArtifact::{0}".format("out-{0}.yaml".format(template_file_name))
},
input_artifacts=[
codepipeline.CfnPipeline.InputArtifactProperty(
name="BuildArtifact"
)
],
namespace="DeployCreateVariables",
region="ap-northeast-1",
run_order=3
# run_order=1
)
)
# Deployアクション(変更セットの実行)の作成
deploy_actions.append(
codepipeline.CfnPipeline.ActionDeclarationProperty(
action_type_id=codepipeline.CfnPipeline.ActionTypeIdProperty(
category="Deploy",
owner="AWS",
provider="CloudFormation",
version="1"
),
name="ExecuteChangeSet",
configuration={
"ActionMode": "CHANGE_SET_EXECUTE",
"ChangeSetName": "lambda-stack-{0}-changeSet".format(lambda_id),
"StackName": "lambda-stack-{0}".format(lambda_id)
},
input_artifacts=[
codepipeline.CfnPipeline.InputArtifactProperty(
name="BuildArtifact"
)
],
namespace="DeployExecuteVariables",
region="ap-northeast-1",
run_order=4
# run_order=2
)
)
(中略)
CI/CD環境のデプロイ&実行確認
以上で準備が整ったので、CI/CD環境のデプロイを行います。デプロイすると自動でパイプラインも実行されます。
(.venv) user_name:~/environment/cdk-app (master) $ cdk deploy
Layerを確認すると、きちんと作成されていることが確認できました。
Lambdaを実行し、Layerが使用されていることも確認できました。
CodeCommitでLayerのソースコードを変更してみます。
パイプラインが完了すると、Layerが更新されていることが確認できます。
Lambdaを実行し、更新したLayerが使用されていることが確認できました!
まとめ
AWS CDKでLayerに対応したLambdaのCI/CD環境を作成しました。Lambda使うならLayerも使えた方がいいよなぁと思ってしましたが、ほぼLambdaと同じでしたね。
最後まで読んでいただいてありがとうございます。
少しでも参考になれば幸いです。