🐿

AWS CDK で YAML を書かずに AWS SAM を利用! ~良いとこどりな IaC の実現を目指す~

2022/12/25に公開

🗓 AWS CDK Advent Calendar 2022 の 25 日目 🗓

🔰 はじめに

対象

AWS Cloud Development Kit (CDK)AWS Serverless Application Model (SAM) を知っている方

やること

CDK の aws-cdk-lib.aws_sam で SAM テンプレートの定義・出力ができることを利用して

  • 全ての AWS リソース定義は CDK で統一する
    • CDK ではテンプレートファイルの出力までを任せる
  • Lambda 関数のパッケージング・デバッグ、そして AWS リソースのデプロイは SAM で実施する

という手順で、コードによるリソース定義ができる CDK と Lambda 関数のパッケージングが便利な SAM のそれぞれ得意な部分を活かしながら AWS リソースを管理をします!

※ こんなこともできるよという使い方で、動作を保証するものではないです

背景

AWS でサーバーレスアプリケーションを構築する場合、SAM を使用することで Lambda 関数のビルド・デプロイ・デバッグや対応サービスのリソース定義が簡潔にできて便利ですが、こんなことを考えて SAM を使用せず CDK を採用することもあるかなと思います!

  • SAM に対応してないリソースも定義したい場合は AWS CloudFormation の定義を使うことになるので、IDE のコード補完や L2 コンストラクタが使えて、たくさんの YAML を書かなくても良い CDK が良さそう
  • すでに Amazon VPC や Application Load Balancer (ALB) など他のリソース定義に CDK を使用しているので、リソース定義は CDK で統一したい

しかし同時に、現時点では CDK で定義した Lambda 関数の依存関係のパッケージングが必要な場合はこの辺りも気になると思います。

そのため、この部分に関しては CDK より考えることや作業量が少なくなる sam build & sam deploy コマンドが使える SAM もやっぱり魅力的です!

そこで今回は CDK と SAM のそれぞれの良いとこどりができるかも?な併用方法を考えて試してみました。

環境

### AWS CDK
cdk --version
2.56.1 (build 9329a73)

### AWS SAM
sam --version
SAM CLI, version 1.67.0

🔖 要約

サンプルコード

https://github.com/sugikeitter/sam-written-by-cdk

手順

  1. aws-cdk-lib.aws_sam を使用して CDK でコードを書く
  2. CDK が使用するメタデータなどをテンプレートに出力しないように、./cdk.json の編集と cdk synth 実行時にオプションを追加する
  3. SAM 固有の Glabals セクション を使用できるように ./sam-globals.yaml ファイルの作成と実行時オプションの --build='cat sam-globals.yaml' を組み合わせる
    • aws-cdk-lib.aws_sam は現時点で Glabals セクションに対応していないため
  4. cdk synth --no-staging --no-version-reporting --no-path-metadata --build='cat sam-globals.yaml' > template.yaml で SAM テンプレートを出力
    • さらに package.json を修正して npm run cdk2sam で実施できるようにしておく
  5. sam build & sam deploy でリソースをデプロイする
    • もちろん sam local でのテストや sam sync によるデプロイも可能

メリット

デメリット

  • CDK と SAM 両方の使い方を知っている必要がある
  • デプロイまでの手順は CDK と SAM 両方のコマンドを使用するため一手間増える
    • 本番環境へのデプロイは CI/CD で自動化する仕組みがあればそんなに問題はないかも
  • (個人的に) 差分の出力が見やすい cdk diff は使えない
    • sam deploy --no-execute-changeset で変更セットの確認はできる

🧑‍🏫 工夫した点と解説

サンプルコード内の主要なファイルはこちら

.
├── bin
│   └── sam-written-by-cdk.ts       # CDK の App クラス
├── cdk.json                        # cdk synth の出力制御などオプション設定
├── functions                       # Lambda 関数のコードを配置するディレクトリ
│   └── index                       # 関数ごとに区切ったディレクトリ
│       ├── __init__.py
│       ├── app.py
│       └── requirements.txt
├── lib
│   └── sam-written-by-cdk-stack.ts # SAM リソースやその他 AWS リソース定義
├── package-lock.json
├── package.json
├── sam-globals.yaml                # Globals セクションの定義
└── template.yaml                   # cdk synth で出力した SAM テンプレート

🌟 aws-cdk-lib.aws_sam を使用した CDK のコード

いくつか aws-cdk-lib.aws_sam 使用のポイントを挙げていきます。

import 文の定義

  • import * as sam from 'aws-cdk-lib/aws-sam' としておけば、sam.XXX でモジュールが使用できるようになる
sam-written-by-cdk-stack.ts
 import { Stack, StackProps } from 'aws-cdk-lib';
 import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
+import * as sam from 'aws-cdk-lib/aws-sam';
 import { Construct } from 'constructs';
 :

SAM リソースの定義

  • 現在は Type: AWS::Serverless::XXX を定義する L1 コンストラクタのみが用意されており、new sam.CfnXXXprops 引数に SAM のリソースごとの Properties を設定する
  • 例えば AWS::Serverless::Api を定義したい場合は CfnApi コンストラクタがあり、propsこちら に記載されている内容が CfnApiProps として型定義されています!
sam-written-by-cdk-stack.ts
const restApi = new sam.CfnApi(this, 'RestApi', {
  stageName: 'dev',
  name: 'cdk2sam-api',
});

Lambda 関数の定義

  • Lambda 関数の定義も SAM と同様に codeUri でローカルのパスを指定する
  • Amazon API Gateway の ID や、Amazon DynamoDB のテーブル名も変数のプロパティで取得でき、CDK の機能でテンプレート出力時に Ref などを自動で変換してくれる
  • SAM の ポリシーテンプレート も使用できるので、Lambda 関数の許可の範囲をアプリケーションが使用するリソースに絞り込みが楽!
sam-written-by-cdk-stack.ts
new sam.CfnFunction(this, 'IndexFunction', {
  functionName: 'cdk2sam-index',
  codeUri: 'functions/index/',
  handler: 'app.lambda_handler',
  runtime: 'python3.9',
  events: {
    api: {
      type: 'Api',
      properties: {
        restApiId: restApi.ref,
        method: 'GET',
        path: '/',
      }
    }
  },
  policies: [
    {
      dynamoDbWritePolicy: {
        tableName: ddb.tableName,
      }
    }
  ]
});
:

コード補完

CDK なのでコード補完も利用可能なので快適!

VS Code を利用

テンプレートの出力

cdk synth を実行すると、Transform: AWS::Serverless-2016-10-31Type セクションが AWS::Serverless::XXX のリソース定義を含んだ SAM テンプレートが出力されます。

cdk synth の出力結果
Transform:
  - AWS::Serverless-2016-10-31
Resources:
  RestApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: dev
      Name: cdk2sam-api
:

🌟 cdk synth で CDK が使用するメタデータ出力の制御

cdk synth では YAML 形式のテンプレートを出力できますが、そのままだとテンプレートに CDK が利用するメタデータを出力したり、cdk.out/ ディレクトリへ JSON 形式の CloudFormation テンプレートとして <Stack Name>.template.json を書き出したりなど、sam deploy 実行時には不要な処理もあるのでこれらを抑制してみます。

ちなみにテンプレートに出力される CDK のメタデータを確認するため ↓ のようなコードを用意して、CDK のデフォルト設定のまま cdk synth すると…

const ddb = new dynamodb.Table(this, 'BookTable', {
  partitionKey: {
    name: 'id',
    type: dynamodb.AttributeType.STRING
  }
});

↓ の YAML ファイルが出力されますが、+ で示した CDK が使用するメタデータがたくさんあります。

template.yaml
 Resources:
   BookTable:
     Type: AWS::DynamoDB::Table
     Properties:
       KeySchema:
         - AttributeName: id
           KeyType: HASH
 :
+    Metadata:
+      aws:cdk:path: xxxx/BookTable/Resource

+  CDKMetadata:
+    Type: AWS::CDK::Metadata
+    Properties:
+      Analytics: v2:deflate64:xxxxxx
+    Metadata:
+      aws:cdk:path: xxxx/CDKMetadata/Default
+    Condition: CDKMetadataAvailable
+Conditions:
+  CDKMetadataAvailable:
+    Fn::Or:
+      - Fn::Or:
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - af-south-1
+          - Fn::Equals:
+              - Ref: AWS::Region
+              - ap-east-1
+          - Fn::Equals:
 :
+Parameters:
+  BootstrapVersion:
+    Type: AWS::SSM::Parameter::Value<String>
+    Default: /cdk-bootstrap/xxxxxx/version
+    +escription: Version of the CDK Bootstrap resources in this  environment, automatically retrieved from SSM Parameter Store.  [cdk:skip]
+Rules:
+  CheckBootstrapVersion:
+    Assertions:
+      - Assert:
+          Fn::Not:
+            - Fn::Contains:
+                - - "1"
+                  - "2"
+                  - "3"
+                  - "4"
+                  - "5"
+                - Ref: BootstrapVersion
+        AssertDescription: CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.

大まかに挙げるとこの辺りは SAM テンプレート使用時には不要です。

抑制したいもの
Parameters セクションの BootstrapVersion
Resources セクションごとの Metadata->aws:cdk:path
Type: AWS::CDK::Metadata のリソースと、そこから利用される Contitions セクションの CDKMetadataAvailable

これらの抑制手順を以下にまとめました。

cdk.json の設定で抑制できるもの

cdk.json の設定方法 抑制できるもの
"context" プロパティに、"@aws-cdk/core:newStyleStackSynthesis": false を追記 Parameters セクションの BootstrapVersion

設定例はこちら。

cdk.json
   "context": {
+    "@aws-cdk/core:newStyleStackSynthesis": false,
     "@aws-cdk/aws-lambda:recognizeLayerVersion": true,

cdk synth の実行時オプションで抑制できるもの

cdk コマンドの実行時オプション 抑制できるもの
--no-version-reporting Resources セクションごとの Metadata->aws:cdk:path
--no-path-metadata Type: AWS::CDK::Metadata のリソースと、そこから利用される Contitions セクションの CDKMetadataAvailable
--no-staging ./cdk.out ディレクトリに作成される <Stack Name>.template.json ファイル

cdk コマンドの 実行時オプション 一覧

実行例はこちら。

cdk synth --no-staging --no-version-reporting --no-path-metadata > template.yaml

これで SAM テンプレートがかなりすっきりしました!

🌟 SAM 固有の Glabals セクションの使用

aws-cdk-lib.aws_sam を使用しても、CDK では現在 SAM の Globals セクションは定義できないのですが、複数の Lambda に一括で同じ設定をしたい時などに便利なので (無理矢理) 追加できる方法を考えてみました!
やりたいことは cdk synth の標準出力を template.yaml に書き出す時に合わせて Globals セクションも書き出せれば良いので、echocat の標準出力を組み合わせると目的は達成できます。

# echo で愚直に出力したした後に cdk synth
echo "Globals:\n  Function:\n    Timeout: 10\n..."; cdk synth > template.yaml

もしくは

# sam-globals.yaml に書き出したい内容を定義しておいた上で
cat sam-globals.yaml
Globals:
  Function:
    Timeout: 10
:

# cat した後に cdk synth
cat sam-globals.yaml; cdk synth > template.yaml

さすがに echo を使うとコマンドが長くなりやすく、改行コードを考えたりするのが手間になるので、あらかじめファイルを用意した上で cat を使う方が扱いやすいです。

また cdk synth の実行時オプションには、--build というテンプレート出力前に任意のコマンドを実行できるものがあるので、cdk のコマンドのみで完結できるようにしてみました (そこまで実行の手間は変わりませんが) 。

# sam-globals.yaml に書き出したい内容を定義しておいた上で
cdk synth --build='cat sam-globals.yaml' > template.yaml

これで Globals セクションを含んだ SAM テンプレートを CDK でも出力できました!

template.yaml
+Globals:
+  Function:
+    Timeout: 10
+    Tracing: Active
+  Api:
+    TracingEnabled: true
+    EndpointConfiguration:
+      Type: REGIONAL
+    OpenApiVersion: 3.0.3
 Transform: AWS::Serverless-2016-10-31
 Resources:
   BookTable:
     Type: AWS::DynamoDB::Table
     Properties:
       KeySchema:
         - AttributeName: id
           KeyType: HASH
 :

Transform セクションより上にあるのがスッキリしない方もいるかもしれませんが、これでも動くのでひとまず ^^;

ちなみに cdk.json"build" プロパティを追記すれば、コマンド実行時に毎回 --build オプションを使わずに同じことはできますので、お好みでどうぞ。

cdk.json の設定例
+    "build": "cat sam-globals.yaml",
     "context": {

もしくは echo でがんばる方はこちら。

cdk.json の設定例
+    "build": "echo \"Globals:\n  Function:\n    Timeout: 10\n    Tracing: Active\n  Api:\n    TracingEnabled: true\n    EndpointConfiguration:\n      Type: REGIONAL\n    OpenApiVersion: 3.0.3\"",
     "context": {

🌟 SAM テンプレート出力コマンドの簡略化

ここまでの内容を全て盛り込んで SAM テンプレートを作成する場合、コマンドが長くなりすぎて毎回実行するのが面倒になってきました…

# CDK で今回使用しない処理を抑制して、SAM テンプレートを出力するコマンド
cdk synth --no-staging --no-version-reporting --no-path-metadata --build='cat sam-globals.yaml' > template.yaml

そこで package.json"scripts" プロパティを設定することで、npm run <stage> のように独自コマンドを定義できる機能を使用して、SAM テンプレート出力のためのコマンドをより簡単に実行できるようにしてみます!

例えば "cdk2sam": "cdk synth --no-staging --no-version-reporting --no-path-metadata --build='cat sam-globals.yaml'> template.yaml" と追加すると…

package.json の設定例
   "scripts": {
     "build": "tsc",
     "watch": "tsc -w",
     "test": "jest",
     "cdk": "cdk",
+    "cdk2sam": "cdk synth --no-staging --no-version-reporting --no-path-metadata --build='cat sam-globals.yaml'> template.yaml"
   },

長いオプションを入力せずに npm run cdk2sam で出力の工夫をした SAM テンプレートが作成されました 😉

# template.yaml に SAM テンプレートの内容を書き出す
npm run cdk2sam

✅ まとめ

SAM テンプレートが作成できれば、あとは SAM CLI でビルド、テスト、デプロイを実施していくだけです!よく使うコマンドとオプションを自分用にまとめておきます φ(•ᴗ•๑)

コマンド 使いどき
sam build --cached --use-container キャッシュを利用したビルド、コンテナランタイムがあれば Lambda 関数の言語ランタイムを気にせずビルドできる。
sam local invoke --debug -e event.json [FUNCTION_LOGICAL_ID] Lambda 関数にイベント引数を渡してローカル実行。
sam local start-api --debug -p 3000 API Gateway のエンドポイントをローカルで起動、ポート番号も選べる。
sam local start-lambda && aws lambda invoke --function-name "[FUNCTION_NAME]" --endpoint-url "http://127.0.0.1:3001" --no-verify-ssl out.txt SAM で定義した Lambda 関数を、別のコードのテストのため呼び出したい場合などに役立つ。詳細は こちら
sam deploy --no-execute-changeset 変更セットの実行はせず、変更内容の確認のみ。
sam sync --stack-name [STACK_NAME] SAM 以外のリソースも変更セットを作成せずにそのままデプロイできるので、開発環境で手早くデプロイしたい時に便利。

今後 CDK と SAM のより便利な連携が提供されるかもしれませんが、CDK と SAM の使い分けで悩んでる方の参考になれば!

GitHubで編集を提案

Discussion