[AWS SAM] Lambda Layerの定義（nodejs）

# 目的

[前回記事](https://qiita.com/takmot/items/c54578e24e2c61c15d1c)で[cfn-response モジュール](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-lambda-function-code-cfnresponsemodule.html)を使用したが、
インライン実装にしか対応しておらず不便なためLayerとして実装する

# ファイル構成

```yaml
lambda-layer/
 ├─ layers/
 │    └─ cfn-response-layer/
 │              └─ nodejs/
 │                   ├─ node_modules/  # [npm i cfn-response] で生成
 │                   ├─ cfn-response/
 │                   │       ├─ package.json　# [npm init] で生成
 │                   │       └─ index.js  # Layerとして提供する機能
 │                   │
 │                   ├─ package.json
 │                   └─ package-lock.json  # [npm i cfn-response] で生成
 │
 └─ template.yaml # SAMテンプレート
```

※`nodejs`フォルダは名前変更不可
※`index.js`ファイルは名前変更不可ではないが、別名とした場合は`npm init`した際にファイル名の指定が必要

# SAMテンプレート

```yaml:template.yaml
  CfnResponse:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: !Sub ${RootStackName}_cfn-response
      ContentUri: layers/cfn-response-layer
      CompatibleRuntimes:
        - nodejs12.x
      RetentionPolicy: Delete
```

Layerとして提供する機能は`nodejs`フォルダに入れることが決められているので、
`ContentUri`には、その1個手前までのパスを指定

# cfn-response モジュール実装
## index.js

`index.js`にcfn-response モジュールの機能を実装する
[github](https://github.com/LukeMizuhashi/cfn-response/blob/master/index.js)で公開されているcfn-response モジュールのコードを基本コピペする
`SUCCESS`, `FAILED`の定義値と、send関数を提供する

```javascript:index.js
const https = require('https');
const url = require('url');

module.exports = {
  SUCCESS: 'SUCCESS',
  FAILED: 'FAILED',
  send(
    event,
    context,
    responseStatus,
    responseData,
    physicalResourceId,
    noEcho,
  ) {
    return new Promise((resolve, reject) => {
      const responseBody = JSON.stringify({
        Status: responseStatus,
        Reason: `See the details in CloudWatch Log Stream: ${context.logStreamName}`,
        PhysicalResourceId: physicalResourceId || context.logStreamName,
        StackId: event.StackId,
        RequestId: event.RequestId,
        LogicalResourceId: event.LogicalResourceId,
        NoEcho: noEcho || false,
        Data: responseData,
      });

      console.log('Response body:\n', responseBody);

      const parsedUrl = url.parse(event.ResponseURL);
      const options = {
        hostname: parsedUrl.hostname,
        port: 443,
        path: parsedUrl.path,
        method: 'PUT',
        headers: {
          'content-type': '',
          'content-length': responseBody.length,
        },
      };

      const request = https.request(options, response => {
        console.log(`Status code: ${response.statusCode}`);
        console.log(`Status message: ${response.statusMessage}`);
        resolve(context.done());
      });

      request.on('error', error => {
        console.log(`send(..) failed executing https.request(..): ${error}`);
        reject(context.done(error));
      });

      request.write(responseBody);
      request.end();
    });
  },
};
```

## `nodejs/cfn-response/package.json`

`nodejs/cfn-response/`以下で`npm init`を実行する
全てデフォルト値で回答すると以下のような`package.json`が生成される

```json:nodejs/cfn-response/package.json
{
  "name": "cfn-response",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}
```

## `nodejs/package.json`

`package.json`は内容`{}`のみで作成
`npm install <パッケージ名>`を実行すると追記される

## `node_modules`および`package-lock.json`

`nodejs`フォルダ以下で`npm i cfn-response`を実行すると生成される
`index.js`内で`https`、`url`パッケージを使用しているが、これらはnodejsの標準モジュールのためインストールは不要

# Layerの使用

## SAMテンプレート

```yaml
  ApplyNotificationFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub ${RootStackName}-ApplyNotificationFunction
      CodeUri: handlers
      Role: !GetAtt 'ApplyNotificationFunctionRole.Arn'
      Layers:
        - !Ref CfnResponse  # 先ほどSAMテンプレートで定義したLayerを参照
```

## コード

```javascript
const cfnResponse = require('cfn-response');

exports.handler = async (event, context) => {
  try {
    if (event.RequestType !== 'Create') {
      await cfnResponse.send(event, context, cfnResponse.SUCCESS);
      return;
    }
    ...

```