AWS CDKでAWS SAMのテンプレートを出力してデプロイする


# はじめに
これまではSAMのテンプレートをYMLでがりがり書いていましたけど、定義したいリソースが増えてくるとYMLファイルが長くなり、作業効率がよくありません。作業効率を向上させるために、AWS CDKを使ってテンプレートを書こうと思い、CDKからSAMテンプレートを出力し、デプロイできることを確認してみました。

# 環境について
本記事は下記環境で実行しています。
- AWS SAM CLI version 0.39.0
- AWS CDK 1.76.0 (build c207717)
- typescript Version 4.1.2
- VSCode バージョン 1.51.1
- Docker Desktop 2.1.0.5
- macOS Catalina バージョン 10.15.7

# 手順
それでは、CDKからSAMテンプレートを作成する手順です
## CDK
### CDKのプロジェクトを作成します
```
% mkdir adventcalander2020; cd adventcalander2020
% cdk init --language=typescript
```
- `cdk init`を実行する時にディレクトリが空じゃないと怒られます

### CDKのバージョンが古い時は最新版にしておきます
これはやってもやらなくてもよいですが、CDKは頻繁にバージョンがアップしますので、新しいプロジェクトの時は最新を使ったほうがよいと思います。最新にする時には[npm-check-updates](https://www.npmjs.com/package/npm-check-updates)を使っています。

```
% ncu -u
（出力は割愛）
% npm install
（出力は割愛）
% which cdk
./node_modules/.bin/cdk  <-- ローカルの.binが先になるようにパスをきっています
% cdk --version
1.76.0 (build c207717)  <-- この記事を書いている時の最新版
```
### Lambda 関数を用意します
今回はAPIGatewayからのリクエストに対してレスポンスを返すLambda関数を用意しました。なので、超シンプルです

``` app.py
import json

def lambda_handler(event, context):
    return {
        'statusCode': 200,
        'isBase64Encoded': False,
        'headers': {
            'Content-Type': 'application/json'
        },
        'body': json.dumps({
            'message': 'Hello World.',
        }),
    }
```

### CDKを使ってSAMのテンプレートを記述します
コーディング中に必要となるモジュールはインストールします

```
npm install -s @aws-cdk/aws-sam
```

CDKのstackはこちらです。CDKのコードを書き終えたら**cdk ls**で認識されていることを確認して、**cdk synth**で出力します.

``` lib/adventcalander2020-stack.ts
import * as cdk from '@aws-cdk/core';
import * as sam from '@aws-cdk/aws-sam';
import { Role, ServicePrincipal, ManagedPolicy } from '@aws-cdk/aws-iam';

export class Adventcalander2020Stack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const environment = this.node.tryGetContext('env')

    // The code that defines your stack goes here
    const lambdaRole = new Role(
      this,
      'lambdaRole',{
        assumedBy: new ServicePrincipal("lambda.amazonaws.com"),
        managedPolicies: [
          ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')
        ],
        path: "/lambda/",
      }
    );
    const lambdaFunctionHelloWorld = new sam.CfnFunction(
      this,
      'lambdaFunctionHelloWorld',{
        codeUri: "lambda/HelloWorld/src",
        functionName: `${environment}-HelloWorld`,
        handler: 'app.lambda_handler',
        runtime: 'python3.8',
        role: lambdaRole.roleArn,
        autoPublishAlias:`${environment}`,
        events:{
          GetMethod:{
            type: "Api",
            properties:{
              path: "/helloworld",
              method: "GET",
            }
          }
        }
      }
    );

  }
}

```

```
% cdk ls
Adventcalander2020Stack
% cdk synth > template.yml 
(出力は省略）
```
これでSAMテンプレートが出力されます。

## SAM
テンプレートが用意できたら**sam**の出番です。
Lambdaから応答があるかどうかの確認をします。事前に**Docker**を起動しておきます。

```
% sam local start-api
Mounting lambdaFunctionHelloWorld at http://127.0.0.1:3000/helloworld [GET]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2020-12-05 06:15:03  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
```
これで起動できたので、`curl`や`Postman`でAPIをコールします。

```
% curl http://127.0.0.1:3000/helloworld 

{"message": "Hello World."}%               
```
Lambdaが動作していることが確認できたのでdeployします。

```
sam deploy --guided
```
初回のdeployは`--guided`を付けて実行すると設定が`samconfig.toml`というファイルに保存されるのでよいと思います。
無事にdeployできたらAWSコンソールでURLを調べて、呼び出してみます。

```
curl https://******.execute-api.ap-northeast-1.amazonaws.com/Prod/helloworld
{"message": "Hello World."}%
```
はい、無事に呼び出せませたね。

# パラメータはCDKとSAMのどちらで吸収するのがいいのか？
実運用すると、環境は異なるけど１つのテンプレートで運用することが多いと思います。その場合、環境別のパラメーターをどうするのか？という問題が出てくると思います。
## CDKの場合
いろんな方法があると思いますが、私の場合は*cdk.json*に環境別のパラメータを記述し、それを読み込む方法をよく使います。

``` cdk.json
{
  "app": "npx ts-node bin/cdk.ts",
  "context": {
   "stg":{
     "BucketName":"stgbucket"
    },
    "prod":{
     "BucketName":"prodbucket"
    }
  }
}

```

``` bin/adventcalander2020.ts
const app = new cdk.App();
const environment: string = app.node.tryGetContext('env')
const params = app.node.tryGetContext(environment)
```
このように*params*で読み込んで、コンストラクターに渡してあげて、下のように使います。

``` lib/adventcalander2020.ts
const contentBucket = new s3.Bucket(this, "ContentBucket", {
     bucketName: `${props.BucketName}`,
})
```
cdkのコマンドを実行する時には、**cdk synth -c env=prod**とすれば、*cdk.json*の内容を読み込んでくれます。

## SAMの場合
SAMでパラメータを渡したい場合は、CDKではパラメータは読み込まず、CloudFormationの組み込み関数である、**Fn::Ref**や**Fn::Sub**のままテンプレートを出力できるようにします。
CloudFormationパラメータをCDKで以下のように宣言します。

``` lib/adventcalander2020.ts
    const environment = new cdk.CfnParameter(this, 'environment',{
      type: 'String',
    });
```
宣言をされたパラメータは、以下のように使います。

``` lib/adventcalander2020.ts
        functionName: cdk.Fn.sub(`\${environment}-HelloWorld`),
        handler: 'app.lambda_handler',
        runtime: 'python3.8',
        role: lambdaRole.roleArn,
        autoPublishAlias: cdk.Fn.ref('environment'),
```
これで出力してみると、CloudFormationの組み込む関数のままテンプレートを出力できています。

``` template.yml
Parameters:
  environment:
    Type: String
（省略）
      AutoPublishAlias:
        Ref: environment
      FunctionName:
        Fn::Sub: ${environment}-HelloWorld
```
## どっちがいいの？
どっちでも結果は同じなので、どっちを選択してもよいですが、実際の開発になると複数人で作業することになるし、CI/CDをどうするのかという話も絡んでくるので、チームの中でやりやすい方法でよいかと思います。

# 最後に
SAMのテンプレートもCDKで出力できることを確認できました。
SAMテンプレートの中で、Lambda関数を複数定義したい時には、TypeScriptのループ処理、関数やクラスを使えばとんでもなく長くなるCloudFormationのYAMLから開放されます。また、VSCodeのコード補完が使えるメリットがあります。
CDKのSAMには、High-level Constructsは無く、Low-level Constructsしかありませんが、メリットだけでデメリットは無いと思いますので、がんがんCDKを使ってSAMテンプレートを出力していきたいと思います。


# 参考にしたほうがよいサイト
CDKやSAMを使ったことがない場合は、以下のサイトを参考にするといいと思います。

- [AWS サービス別資料](https://aws.amazon.com/jp/aws-jp-introduction/aws-jp-webinar-service-cut/)
    - このページにある、*AWS Serverless Application Model*と*AWS Cloud Development Kit (CDK)*はまず最初に読みましょう。
- [CDK workshop](https://cdkworkshop.com/20-typescript.html)
    - はじめてCDKを触るときにはこのworkshopをやるとよいと思います。
- [aws-samples/aws-cdk-examples](https://github.com/aws-samples/aws-cdk-examples)
    - CDKのサンプルがたくさんありますので参考になります。


---
:christmas_tree: [FORK Advent Calendar 2020](https://qiita.com/advent-calendar/2020/fork)
PREV :arrow_backward: [canvasとJSで、目に優しいオリジナルテトリス](https://qiita.com/yuzukisakioka/items/6c226f619fb12d8902ab)