はじめに
本記事は、私自身の備忘録を兼ねてAWS CDKをこれから始める方の一助になればと思い、AWS CDKの使い方等をまとめたものです。
今回は、AWS CDKでLambdaのCI/CD環境を作成します。
なお、本記事は私自身の経験を基に記載していますが、間違いがあったらすみません。
LambdaのソースコードはCodeCommitで管理したい。けどLambdaの設定を含むインフラはIaC(CDK)で管理したい!
LambdaのCI/CD環境を作成するんですが、前提条件として、
- LambdaのソースコードはCodeCommitで管理する
- VPCやDBなどリソースはIaC(CDK)で作成、管理する(←Lambdaの設定(メモリサイズやVPC設定など)もここに含む)
です。
LambdaのCI/CDのベースは、以下のような構成になります。
概要としては、
- CodeCommitのリポジトリからLambdaのデプロイに必要なLambdaのソースコード、buildspecファイル、SAMテンプレートファイルを取得する
- CodeBuildで
aws cloudformation package
を実行し、CloudFormationでデプロイするためのアーティファクト作成しS3にアップロードする - アーティファクト、SAMテンプレートに基づいてCloudFormationで変更セットを作成する
- CloudFormationで変更セットを実行する
になりますが、問題は2.でaws cloudformation package
に必要になるLambda関数を作成するSAMテンプレートファイル(もしくはCloudFormationテンプレートファイル)です。この構成だと、CodeCommitリポジトリにLambdaのソースコードと一緒に含まなければなりませんが、前提条件の2つ目のように、Lambdaの設定はCDKで管理したいため、CodeCommitリポジトリには含めたくありません。
このため、LambdaのソースコードはCodeCommitで管理しつつ、Lambdaの設定はCDKで管理する構成を考えました。
環境
本記事は以下の環境を使用して記載しています。
- AWS Cloud9
- AWS CDK:2.80.0
- Python: 3.10.11
- Node.js: 16.20.0
また、以下の記事に基づいてAWS CDKの環境を作成しています。
LambdaのCI/CD環境をIaCで作る
では、実際作成したLambdaのCI/CD環境ですが、以下のような構成にしました。
- CDKでSAMテンプレートファイルを作成し、S3バケットにアップロードする
- CodeCommitのリポジトリからLambdaのデプロイに必要なLambdaのソースコード、buildspecファイル、
SAMテンプレートファイルを取得する - S3バケットからSAMテンプレートファイルを取得する
- CodeBuildで
aws cloudformation package
を実行し、CloudFormationでデプロイするためのアーティファクト作成しS3にアップロードする - アーティファクト、SAMテンプレートに基づいてCloudFormationで変更セットを作成する
- CloudFormationで変更セットを実行する
ベースとなった構成との変更点は赤枠の範囲ですね。
SAMテンプレートファイルをインフラ環境と一緒にCDKで作成し、CodeCommitリポジトリに含めずにS3へアップロードします。SAMテンプレートファイルをCodeCommitと別にCodePipelineのSourceとし、CodeBuildの入力になるように設定しました。こうすることで、LambdaのVPCやメモリサイズなどの設定をCDKで管理でき、パイプラインを実行することで、変更の反映が可能です。
最終的な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
# LambdaFunction作成用のSAM Templateファイルを作成
def EditSAMTemplate(lambda_id, lambda_role):
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": "",
"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"}
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"
repository_name = "test-lambda-rep"
# 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)
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_STORED) as z:
z.writestr(template_file_name + ".json", json.dumps(sam_template_data, indent=4))
# 作成したSAM TemplateファイルをS3へアップロード
s3_deploy = s3deploy.BucketDeployment(
self,
"BuildspecFileUploadS3",
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",
)
# 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_buildspec",
output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
name="SourceArtifact_template"
)],
region="ap-northeast-1",
run_order=1
)
)
# 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
)
)
deploy_actions = []
# 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=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=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リポジトリに用意するファイルです。
※今回の例では、CodeCommitリポジトリはCDKで作成していませんが、CDKに含めても問題ありません。
def lambda_handler(event, context):
print("hello! World!!")
version: 0.2
phases:
install:
commands:
- aws --version
- cp -pr ../01/* .
- 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
では、処理毎に順に説明します。
- S3バケットの作成
- IAMロールの作成
- SAM Templateファイルの作成
- Sourceアクションの作成
- CodeBuildプロジェクト、Buildアクションの作成
- Deployアクションの作成
- パイプラインの作成
- パイプラインの自動起動設定
S3バケットの作成
CI/CDに必要なS3バケットを作成します。(必要に応じてバケットポリシーなどの設定を追加してください。)
必要なバケットは以下のとおりです。
- CDKで作成したSMAテンプレートをアップロードするバケット
-
aws cloudformation package
の実行で作成されるアーティファクトをアップロードするバケット - パイプラインのアーティファクト用バケット
なお、今回S3バケットの作成はL1コンストラクタを使用していますが、SAMテンプレートをアップロードするバケットは、aws_s3_deploymentを使用する都合上、バケット作成後にs3.Bucket.from_bucket_arn
を使用することでL2コンストラクタに変換しています。
(中略)
# 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
(中略)
class CdkAppStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
lambda_id = "TestFunc"
repository_name = "test-lambda-rep"
# 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")
(中略)
IAMロールの作成
CI/CDに必要なIAMロールを作成します。
必要なIAMロールは以下のとおりです。
- CodePipelineが使用するロール
- CodeBuildが使用するロール
- CloudFormationが使用するロール
- EventBridge(CodeCommitリポジトリの更新時にパイプラインを実行)が使用するロール
- CI/CDで作成するLambda関数が使用するロール
うち、CloudFormationが使用するロールは、cdkがデプロイに使用するロール(cdk bootstrap
で作成されるロール)を使用するようにしたため、作成はしていません。
権限は、できるだけ必要なものだけにしているつもりですが、必要に応じて見直しをお願いします。
(中略)
# 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
(中略)
class CdkAppStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
(中略)
# 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)
(中略)
# EventBridge用のIAMロールの作成
exec_pipeline_role = CreateExecPipelineIAMRole(self, pipeline_arn)
(中略)
SAM Templateファイルの作成
Lambdaを作成するためのLambdaの各種設定を定義したSAM Templateファイルを作成します。
今回は必要最低限な定義のみにしていますが、Lambdaの処理に合わせてVPCの設定やLayer、トリガーの設定(AWS::Lambda::Permission)など必要な情報を追加してください。
作成したSAM Templateファイルは、zip化した後、aws_s3_deploymentでS3バケットへアップロードします。
aws_s3_deploymentの使い方は、以下を参照してください。
(中略)
# LambdaFunction作成用のSAM Templateファイルを作成
def EditSAMTemplate(lambda_id, lambda_role):
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": "",
"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"}
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"
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)
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_STORED) as z:
z.writestr(template_file_name + ".json", json.dumps(sam_template_data, indent=4))
# 作成したSAM TemplateファイルをS3へアップロード
s3_deploy = s3deploy.BucketDeployment(
self,
"BuildspecFileUploadS3",
sources=[s3deploy.Source.asset(zip_dir)],
destination_bucket=source_s3bucket_l2,
)
(中略)
Sourceアクションの作成
パイプラインに組み込むSourceアクションを作成します。
Sourceは、LambdaのソースコードとbuildspecがあるCodeCommitリポジトリとSAMテンプレートファイルがあるS3バケットの2つがあるので、アクションも2つ作成します。
(中略)
class CdkAppStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
lambda_id = "TestFunc"
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_buildspec",
output_artifacts=[codepipeline.CfnPipeline.OutputArtifactProperty(
name="SourceArtifact_template"
)],
region="ap-northeast-1",
run_order=1
)
)
(中略)
CodeBuildプロジェクト、Buildアクションの作成
CodeBuildプロジェクトを作成し、そのCodeBuildプロジェクトを実行するパイプラインのBuildアクションを作成します。
CodeBuildプロジェクトについて、特筆することは特にありませんが、buildspecファイル内で環境変数を使っているで、CodeBuildプロジェクトの設定で環境変数を設定します。(SAMテンプレートファイル名やS3バケット名)
Buildアクションでは、2つ作成したSourceアクションのOutputアーティファクトを両方Inputにするようにします。 Inputが複数ある場合は、PrimarySourceを設定する必要があるので、CodeCommitのSourceアクションのOutputアーティファクトをPrimarySourceに設定します。(buildspecがこちらにあるため)
(中略)
class CdkAppStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
lambda_id = "TestFunc"
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",
)
(中略)
# 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
)
)
(中略)
version: 0.2
phases:
install:
commands:
- aws --version
- cp -pr ../01/* .
- 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アクションを作成します。
Deployアクションは、CloudFormation変更セットの作成と変更セットの実行の2段になるので、それぞれ作成します。
特に特筆すべき内容はありませんが、前述のとおり実行ロールは、cdkがデプロイに使用するロール(cdk bootstrapで作成されるロール)を使用するようにしています。
(中略)
class CdkAppStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
lambda_id = "TestFunc"
repository_name = "test-lambda-rep"
(中略)
deploy_actions = []
# 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=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=2
)
)
(中略)
パイプラインの作成
上記までで作成した、Sourceアクション、Buildアクション、Deployアクションを使用して、パイプラインを作成します。
(中略)
class CdkAppStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
lambda_id = "TestFunc"
repository_name = "test-lambda-rep"
(中略)
# 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)
(中略)
パイプラインの自動起動設定
CodeCommitリポジトリが更新されたらパイプラインが自動起動するようにEventBridgeルールを作成します。
(中略)
# 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)
(中略)
# CodeCommitリポジトリの変更を検知するEventBridgeルールの作成
CodecommitEvents(self, repository_name, pipeline_arn, exec_pipeline_role)
CI/CD環境のデプロイ&実行確認
以上で準備が整ったので、CI/CD環境のデプロイを行います。デプロイすると自動でパイプラインも実行されます。
(.venv) user_name:~/environment/cdk-app (master) $ cdk deploy
Lambda関数を確認すると、こちらも作成されていることが確認できました。
CodeCommitでLambdaのソースコードを変更してみます。
パイプラインが完了すると、Lambdaのソースコードが反映されていることが確認できます。
また、CodeDeployのコンソールでもデプロイの確認ができます。(今回はAllAtOnceにしたので、あまり面白くないですが。。)
まとめ
AWS CDKでLambdaのCI/CD環境を作成しました。インフラをCDKで管理しつつ、LambdaのソースコードはCodeCommitで管理するのはよく想定される構成と思いますが、実際しようとすると結構複雑ですね。。
最後まで読んでいただいてありがとうございます。
少しでも参考になれば幸いです。