この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
CX事業本部の岩田です。
CDKすごく便利そうですよね??便利そうなので、この際インフラの管理は全てCDKに移行して...と言いたいところですが、既にCloudFormationやSAMで諸々の環境が構築済みで移行コストやリスクを懸念してCDKの利用に踏み切れない といったケースは多いと思います。そういったケースに対応するため、比較的移行コストとリスクを下げつつSAMとCDKを共存させる方法について調べてみました。
環境等
- OS : macOS Mojave 10.14.6
- Node.js : v10.15.1
- AWS CDK : 1.4.0 (build 175471f)
やること
元々SAMでリソースを管理しているサーバーレスアプリケーションがあります。 現在SAMで管理しているリソースの中で記述が冗長になりがちな部分をCDKに切り出します。 現状デプロイはにSAMを利用しているので、CDK導入後もデプロイは継続してSAMを利用することとします。 同様にSAM CLIを使ってローカルテストが実施できることも必須要件です。
現行の構成
こんなディレクトリ構成です。docs/swagger.yml
でAPIの仕様を定義しており、SAMテンプレートからも参照しています。
├── docs
│ └── swagger.yml
├── sam.yml
└── src
├── create_pet.py
├── get_pet.py
└── list_pets.py
現行のSAMテンプレート
現行のSAMテンプレートです。
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: 'CDK With SAM'
Globals:
Function:
Runtime: python3.7
Resources:
ListPets:
Type: 'AWS::Serverless::Function'
Properties:
CodeUri: src
Handler: list_pets.handler
Role: !GetAtt LambdaExecuteRole.Arn
GetPet:
Type: 'AWS::Serverless::Function'
Properties:
CodeUri: src
Handler: get_pet.handler
Role: !GetAtt LambdaExecuteRole.Arn
CreatePet:
Type: 'AWS::Serverless::Function'
Properties:
CodeUri: src
Handler: create_pet.handler
Role: !GetAtt LambdaExecuteRole.Arn
PetStoreApi:
Type: AWS::Serverless::Api
Properties:
StageName: prd
DefinitionBody:
Fn::Transform:
Name: AWS::Include
Parameters:
Location: docs/swagger.yml
LambdaExecuteRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- 'lambda.amazonaws.com'
Action: sts:AssumeRole
Policies:
-
PolicyName: 'lambda-permissions'
PolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Action:
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Resource:
- "*"
LambdaPermissionListPets:
Type: "AWS::Lambda::Permission"
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref ListPets
Principal: apigateway.amazonaws.com
LambdaPermissionGetPet:
Type: "AWS::Lambda::Permission"
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref GetPet
Principal: apigateway.amazonaws.com
LambdaPermissionCreatePet:
Type: "AWS::Lambda::Permission"
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref CreatePet
Principal: apigateway.amazonaws.com
ListPetsLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/lambda/${ListPets}
RetentionInDays: 731
GetPetLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/lambda/${GetPet}
RetentionInDays: 731
CreatePetLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/lambda/${CreatePet}
RetentionInDays: 731
Lambdaを定義する部分はまあ良いのですが、Cloudwatch Logsのロググループの定義とAPI GwからLambdaを呼び出すためのパーミッションの定義をLambdaの数だけ繰り返しており(ハイライト箇所)非常に冗長です。こういったループ処理はプログラムにやらせたいですね。 とりあえず検証用にデプロイしておきます。
sam package --template-file sam.yml --s3-bucket <適当なS3バケット> --output-template-file output.yml
sam deploy --template-file output.yml --stack-name sam-with-cdk --capabilities CAPABILITY_IAM
CDKと共存させてみる
早速CDKと共存させてみましょう。cdk
というディレクトリを作って、ここにCDK関連の諸々を突っ込んでいきます。
mkdir cdk
cd cdk
cdk init app --language=typescript
ディレクトリ構成はこんな感じに変わります
├── cdk
│ ├── README.md
│ ├── bin
│ │ └── cdk.ts
│ ├── cdk.json
│ ├── cdk.out
│ │ ├── CdkStack.template.json
│ │ ├── cdk.out
│ │ └── manifest.json
│ ├── lib
│ │ └── cdk-stack.ts
│ ├── node_modules
│ ├── package-lock.json
│ ├── package.json
│ └── tsconfig.json
├── docs
│ └── swagger.yml
├── output.yml
├── sam.yml
├── output.yml # sam packageでパッケージ後のテンプレート
└── src
├── create_pet.py
├── get_pet.py
└── list_pets.py
CDKのコードを書く!!
準備ができたのでCDKのコードを書いていきます。 今回は既存のSAMテンプレートから
AWS::Lambda::Permission
のリソースAWS::Logs::LogGroup
のリソース
の定義を削除し、CDKで生成することを目標とします。
まずはCDKから既存のSAMテンプレートを読み込みます。CDKにはCfnInclude
というクラスが用意されているので、このクラスのコンストラクタにSAMテンプレートを渡してインスタンスを生成します。なお、ここで指定するテンプレートはsam package
でパッケージ後のテンプレートです。
const sam_output_path = path.resolve(__dirname, '../../output.yml');
const cfn_template = <any> new cdk.CfnInclude(this, "PackagedSamTemplate", {
template: yaml.parse(fs.readFileSync(sam_output_path).toString())
});
続いて読み込んだテンプレートからAWS::Serverless::Function
のリソースを抽出します
const resources = cfn_template.template.Resources;
const funcs = Object.keys(resources).filter(key => {
return resources[key].Type === 'AWS::Serverless::Function';
});
これでfuncs
の中にリソースのキー一覧が入ります。
今回のテンプレートだと、funcs
の中身は['ListPets', 'GetPet', 'CreatePet']
になります。
リソースのキーが全て取得できたので、ループしながらロググループの作成とパーミッションの設定を行います。
logGroupName
やfunctionName
はSAMテンプレートで定義されたLambdaのFunctionNameを動的に利用するため、Fn
クラスのスタティックメソッドsub
を利用します。このメソッドがCloudFormationのFn::Sub
相当の動きになります。
funcs.forEach(func_key => {
new LogGroup(this, `${func_key}LogGroup`, {
retention: RetentionDays.TWO_YEARS,
logGroupName: Fn.sub(`/aws/lambda/\${${func_key}}`)
});
new CfnPermission(this, `LamberPermission${func_key}`, {
principal: 'apigateway.amazonaws.com',
action: 'lambda:InvokeFunction',
functionName: Fn.sub(`\${${func_key}}`)
});
});
最終形
最終的なCDKのコードです。
cdk/lib/cdk-stack.ts
import * as cdk from '@aws-cdk/core';
import {LogGroup, RetentionDays} from '@aws-cdk/aws-logs';
import * as fs from 'fs';
import * as yaml from 'yaml';
import * as path from 'path';
import { Fn } from '@aws-cdk/core';
import { CfnPermission} from '@aws-cdk/aws-lambda';
export class CdkStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// The code that defines your stack goes here
// Package後のSAMテンプレートを読み込み
const sam_output_path = path.resolve(__dirname, '../../output.yml');
const cfn_template = <any> new cdk.CfnInclude(this, "PackagedSamTemplate", {
template: yaml.parse(fs.readFileSync(sam_output_path).toString())
});
// SAMテンプレートからTypeがAWS::Serverless::Functionのリソースを抽出
const resources = cfn_template.template.Resources;
const funcs = Object.keys(resources).filter(key => {
return resources[key].Type === 'AWS::Serverless::Function';
});
funcs.forEach(func_key => {
// LogGroupを作成する
new LogGroup(this, `${func_key}LogGroup`, {
retention: RetentionDays.TWO_YEARS,
logGroupName: Fn.sub(`/aws/lambda/\${${func_key}}`)
});
// ApiGWからLambdaをInvokeするためのパーミッションを設定する
new CfnPermission(this, `LamberPermission${func_key}`, {
principal: 'apigateway.amazonaws.com',
action: 'lambda:InvokeFunction',
functionName: Fn.sub(`\${${func_key}}`)
});
});
}
}
CDKに移植できたのでSAMテンプレートからは
AWS::Lambda::Permission
のリソースAWS::Logs::LogGroup
のリソース
の記述を削除します。
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: 'CDK With SAM'
Globals:
Function:
Runtime: python3.7
Resources:
ListPets:
Type: 'AWS::Serverless::Function'
Properties:
CodeUri: src
Handler: list_pets.handler
Role: !GetAtt LambdaExecuteRole.Arn
GetPet:
Type: 'AWS::Serverless::Function'
Properties:
CodeUri: src
Handler: get_pet.handler
Role: !GetAtt LambdaExecuteRole.Arn
CreatePet:
Type: 'AWS::Serverless::Function'
Properties:
CodeUri: src
Handler: create_pet.handler
Role: !GetAtt LambdaExecuteRole.Arn
PetStoreApi:
Type: AWS::Serverless::Api
Properties:
StageName: prd
DefinitionBody:
Fn::Transform:
Name: AWS::Include
Parameters:
Location: docs/swagger.yml
LambdaExecuteRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- 'lambda.amazonaws.com'
Action: sts:AssumeRole
Policies:
-
PolicyName: 'lambda-permissions'
PolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Action:
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Resource:
- "*"
SAM & CDKでデプロイしてみる!
準備ができたのでSAMとCDKを組み合わせてデプロイしてみます。 一点注意したいのですが、一気にSAM & CDKでのデプロイに移行すると、存在するロググループを作成しにいってエラーになってしまいます。SAM & CDKの構成に移行するため、一旦現在のSAMテンプレートでデプロイを行いロググループを削除します。 もし稼働中のシステムをSAM & CDKの構成に移行する場合は要注意です!!
sam package --template-file sam.yml --s3-bucket <適当なS3バケット> --output-template-file output.yml
sam deploy --template-file output.yml --stack-name sam-with-cdk --capabilities CAPABILITY_IAM
これで準備ができたので改めてSAM & CDKでデプロイを行います。cdk synth
というコマンドを実行することでCDKからCloudFormationのテンプレートが出力できるので
- sam package
- cdk synth
- sam deploy
と順番に実行し、デプロイを行います。
sam package --template-file sam.yml --s3-bucket <適当なS3バケット> --output-template-file output.yml
cd cdk
cdk synth > cdk_output.yml
sam deploy --template-file cdk_output.yml --stack-name sam-with-cdk --capabilities CAPABILITY_IAM
マネコンから確認してみましょう
CDKで追加したリソースもバッチリ作成できていますね!
ちなみにcdk synth
の出力はこんな感じでした
Description: CDK With SAM
Transform: AWS::Serverless-2016-10-31
AWSTemplateFormatVersion: "2010-09-09"
Globals:
Function:
Runtime: python3.7
Resources:
ListPets:
Type: AWS::Serverless::Function
Properties:
CodeUri: s3://some-s3bucket/21a80cbe04a4a456dc36ec1643dfcb20
Handler: list_pets.handler
Role:
Fn::GetAtt:
- LambdaExecuteRole
- Arn
GetPet:
Type: AWS::Serverless::Function
Properties:
CodeUri: s3://some-s3bucket/21a80cbe04a4a456dc36ec1643dfcb20
Handler: get_pet.handler
Role:
Fn::GetAtt:
- LambdaExecuteRole
- Arn
CreatePet:
Type: AWS::Serverless::Function
Properties:
CodeUri: s3://some-s3bucket/21a80cbe04a4a456dc36ec1643dfcb20
Handler: create_pet.handler
Role:
Fn::GetAtt:
- LambdaExecuteRole
- Arn
PetStoreApi:
Type: AWS::Serverless::Api
Properties:
StageName: prd
DefinitionBody:
Fn::Transform:
Name: AWS::Include
Parameters:
Location: s3://some-s3bucket/fa270f10518978fcd487a80e0c9c0f0d
LambdaExecuteRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: lambda-permissions
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource:
- "*"
ListPetsLogGroup297C159C:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName:
Fn::Sub: /aws/lambda/${ListPets}
RetentionInDays: 731
UpdateReplacePolicy: Retain
DeletionPolicy: Retain
Metadata:
aws:cdk:path: CdkStack/ListPetsLogGroup/Resource
LamberPermissionListPets:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName:
Fn::Sub: ${ListPets}
Principal: apigateway.amazonaws.com
Metadata:
aws:cdk:path: CdkStack/LamberPermissionListPets
GetPetLogGroupD6EA9D77:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName:
Fn::Sub: /aws/lambda/${GetPet}
RetentionInDays: 731
UpdateReplacePolicy: Retain
DeletionPolicy: Retain
Metadata:
aws:cdk:path: CdkStack/GetPetLogGroup/Resource
LamberPermissionGetPet:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName:
Fn::Sub: ${GetPet}
Principal: apigateway.amazonaws.com
Metadata:
aws:cdk:path: CdkStack/LamberPermissionGetPet
CreatePetLogGroup292AC3D6:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName:
Fn::Sub: /aws/lambda/${CreatePet}
RetentionInDays: 731
UpdateReplacePolicy: Retain
DeletionPolicy: Retain
Metadata:
aws:cdk:path: CdkStack/CreatePetLogGroup/Resource
LamberPermissionCreatePet:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName:
Fn::Sub: ${CreatePet}
Principal: apigateway.amazonaws.com
Metadata:
aws:cdk:path: CdkStack/LamberPermissionCreatePet
CDKMetadata:
Type: AWS::CDK::Metadata
Properties:
Modules: aws-cdk=1.4.0,@aws-cdk/assets=1.4.0,@aws-cdk/aws-cloudwatch=1.4.0,@aws-cdk/aws-ec2=1.4.0,@aws-cdk/aws-events=1.4.0,@aws-cdk/aws-iam=1.4.0,@aws-cdk/aws-kms=1.4.0,@aws-cdk/aws-lambda=1.4.0,@aws-cdk/aws-logs=1.4.0,@aws-cdk/aws-s3=1.4.0,@aws-cdk/aws-s3-assets=1.4.0,@aws-cdk/aws-sqs=1.4.0,@aws-cdk/aws-ssm=1.4.0,@aws-cdk/core=1.4.0,@aws-cdk/cx-api=1.4.0,@aws-cdk/region-info=1.4.0,jsii-runtime=node.js/v10.15.1
読み込んだSAMテンプレートに色々追加されているのが分かると思います
まとめ
今回紹介したような内容であればCloudFormationのマクロを作ってしまうのもありだと思いますが、マクロに比べてCDKの方が環境準備が簡単そうな感触でした。
既存資産のことを考えると、ドラスティックにCDKへ移行するというのは難しいケースもあるかと思いますが、こういったテクニックを組み合わせつつ改善していきたいですね。
CfnInclude
で読み込んだテンプレートをCDK側で更新したい!!というissueも上がっているので、こちらの動向にも注目です。