1
2

More than 1 year has passed since last update.

【AWS CDK (Python)】 LambdaのCI/CD環境をIaCで作る

Last updated at Posted at 2023-06-26

はじめに

本記事は、私自身の備忘録を兼ねて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のベースは、以下のような構成になります。
image.png

概要としては、

  1. CodeCommitのリポジトリからLambdaのデプロイに必要なLambdaのソースコード、buildspecファイル、SAMテンプレートファイルを取得する
  2. CodeBuildでaws cloudformation packageを実行し、CloudFormationでデプロイするためのアーティファクト作成しS3にアップロードする
  3. アーティファクト、SAMテンプレートに基づいてCloudFormationで変更セットを作成する
  4. 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環境ですが、以下のような構成にしました。
image.png

  1. CDKでSAMテンプレートファイルを作成し、S3バケットにアップロードする
  2. CodeCommitのリポジトリからLambdaのデプロイに必要なLambdaのソースコード、buildspecファイル、SAMテンプレートファイルを取得する
  3. S3バケットからSAMテンプレートファイルを取得する
  4. CodeBuildでaws cloudformation packageを実行し、CloudFormationでデプロイするためのアーティファクト作成しS3にアップロードする
  5. アーティファクト、SAMテンプレートに基づいてCloudFormationで変更セットを作成する
  6. CloudFormationで変更セットを実行する

ベースとなった構成との変更点は赤枠の範囲ですね。
SAMテンプレートファイルをインフラ環境と一緒にCDKで作成し、CodeCommitリポジトリに含めずにS3へアップロードします。SAMテンプレートファイルをCodeCommitと別にCodePipelineのSourceとし、CodeBuildの入力になるように設定しました。こうすることで、LambdaのVPCやメモリサイズなどの設定をCDKで管理でき、パイプラインを実行することで、変更の反映が可能です。
最終的なCDKのソースコードは以下のようになりました。

CDKコード全体
app.py
#!/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()
cdk_app/cdk_lambda_cicd_stack.py
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に含めても問題ありません。

test-lambda-rep/TestFunc/lambda_function.py
def lambda_handler(event, context):
    print("hello! World!!")
test-lambda-rep/buildspec_TestFunc.yaml
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バケットの作成

CI/CDに必要なS3バケットを作成します。(必要に応じてバケットポリシーなどの設定を追加してください。)
必要なバケットは以下のとおりです。

  • CDKで作成したSMAテンプレートをアップロードするバケット
  • aws cloudformation packageの実行で作成されるアーティファクトをアップロードするバケット
  • パイプラインのアーティファクト用バケット

なお、今回S3バケットの作成はL1コンストラクタを使用していますが、SAMテンプレートをアップロードするバケットは、aws_s3_deploymentを使用する都合上、バケット作成後にs3.Bucket.from_bucket_arnを使用することでL2コンストラクタに変換しています。

cdk_app/cdk_lambda_cicd_stack.py

(中略)

# 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で作成されるロール)を使用するようにしたため、作成はしていません。
権限は、できるだけ必要なものだけにしているつもりですが、必要に応じて見直しをお願いします。

cdk_app/cdk_lambda_cicd_stack.py

(中略)

# 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の使い方は、以下を参照してください。

cdk_app/cdk_lambda_cicd_stack.py

(中略)

# 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つ作成します。

cdk_app/cdk_lambda_cicd_stack.py

(中略)

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がこちらにあるため)

cdk_app/cdk_lambda_cicd_stack.py

(中略)

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
            )
        )

(中略)
test-lambda-rep/buildspec_TestFunc.yaml
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で作成されるロール)を使用するようにしています。

cdk_app/cdk_lambda_cicd_stack.py

(中略)

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アクションを使用して、パイプラインを作成します。

cdk_app/cdk_lambda_cicd_stack.py

(中略)

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ルールを作成します。

cdk_app/cdk_lambda_cicd_stack.py

(中略)
# 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

image.png

Lambda関数を確認すると、こちらも作成されていることが確認できました。
image.png

CodeCommitでLambdaのソースコードを変更してみます。
image.png

パイプラインが自動で起動しました。
image.png

パイプラインが完了すると、Lambdaのソースコードが反映されていることが確認できます。
image.png

また、CodeDeployのコンソールでもデプロイの確認ができます。(今回はAllAtOnceにしたので、あまり面白くないですが。。)
image.png

まとめ

AWS CDKでLambdaのCI/CD環境を作成しました。インフラをCDKで管理しつつ、LambdaのソースコードはCodeCommitで管理するのはよく想定される構成と思いますが、実際しようとすると結構複雑ですね。。
最後まで読んでいただいてありがとうございます。
少しでも参考になれば幸いです。

参考文献

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2