lambdaをローカルで作成してデプロイする。

AWSを弄っているとなにかと必要になるのがslackへの通知関数。
今回はこの通知関数をローカルで作成し、ユニットテスト実行、デプロイまでを試みました。
なお、slackのURLのような平文にしておきたくないものは環境変数ではなくパラメータストアを使って扱いたいと思います。

_contents_
1. ローカルでSlack通知関数を作成
2. mocha + chaiで単体テスト
3. lambdaへデプロイ

# 前提条件

* Slack WebhookのURLを作成・取得済み。
* ローカルに`aws-sam-cli`を導入済み。

# 1. ローカルでSlack通知関数を作成

今回はプロジェクト名を`localSlack`とします。

```
$ sam --version                                         
SAM CLI, version 0.38.0

$ sam init --runtime nodejs10.x
Which template source would you like to use?
	1 - AWS Quick Start Templates
	2 - Custom Template Location
Choice: 1

Project name [sam-app]: localSlack

Quick start templates may have been updated. Do you want to re-download the latest [Y/n]: Y

AWS quick start application templates:
	1 - Hello World Example
	2 - Quick Start: From Scratch
	3 - Quick Start: Scheduled Events
	4 - Quick Start: S3
	5 - Quick Start: SNS
	6 - Quick Start: SQS
	7 - Quick Start: Web Backend
Template selection: 1

-----------------------
Generating application:
-----------------------
Name: localSlack
Runtime: nodejs10.x
Dependency Manager: npm
Application Template: hello-world
Output Directory: .

Next steps can be found in the README file at ./localSlack/README.md

$ tree
localSlack
├── README.md
├── events
│   └── event.json
├── hello-world
│   ├── app.js //関数本体
│   ├── package.json
│   └── tests
│       └── unit
│           └── test-handler.js
└── template.yaml
```

## IAMロール作成

CloudWatchへのログ書き込みとSessionManagerのパラメータストアからの読み取りを許可したロールを作成。ここでは`arn:aws:iam::xxxxxxxxxxxx:role/slackrole`と命名する。

## template.ymlの編集
`template.yml`はCloudFormationでlambdaを作成する時に指定したものと同じです。ここでlambdaに関するロールや環境変数、ランタイムなどの指定を行います。
以下のように整形。

```yaml:template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Globals:
  Function:
    Timeout: 3

Resources:
  notifySlack:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello-world/
      Handler: app.handler
      Runtime: nodejs10.x
      Role: 'arn:aws:iam::xxxxxxxxxxxx:role/slackrole'
      #環境変数でURLを指定する場合には以下。可能ならSSM推奨。暗号化できるし。
      Environment:
        Variables:
          SLACK_WEBHOOK_URL: >-
            https://hooks.slack.com/xxxxxx

```

## event.jsonの編集
lambda関数の実行時に渡すeventを定義します。
今回は、メッセージ送信先のワークスペースとチャンネル、メッセージ内容を指定します。

```json:event.json
{
  "workspace": "SLACK_WEBHOOK_URL",
  "channel": "#test",
  "text": "test message"
}
```

## SSM パラメータストアでの環境変数の設定

|設定項目|設定内容|
|:---:|:---:|
|名前|SLACK_WEBHOOK_URL|
|タイプ|安全な文字列|
|値|"自身のslackのurl"|

取得する場合は以下。

```js
const AWS = require('aws-sdk');
const ssm = new AWS.SSM();
const request = {
    Name: "パラメータ名",
    WithDecryption: "true/false" //復号化の有無を指定。
};
ssm.getParameter(request);
```

## 関数本体の編集

```js:app.js
const AWS = require('aws-sdk');
const ssm = new AWS.SSM();
const https = require('https');
const url = require('url');

exports.handler = function(event, context){
    let slack_url = "";
    (async function () {
        return await ssm.getParameter({Name: event.workspace, WithDecryption: true}).promise();
    }()).then(result => {
        slack_url = result.Parameter.Value;
    }).then(() =>{
        const slack_req_ops = url.parse(slack_url);
        slack_req_ops.method = 'POST';
        slack_req_ops.header = {'Content-Type':'application/json'};
        if(event.text){
            var req = https.request(slack_req_ops, function(res){
                if(res.statusCode == 200){
                    context.succeed('sent message');
                }else{
                    context.fail('[ERROR] status code : ' + res.statusCode);
                }
            });
    
            req.on('error', function(e){
                console.log('[ERROR] problem with request', e.message);
                context.fail(e.message);
            });
            
            let channel = event.channel;
            let text = event.text;
    
            req.write(JSON.stringify({"channel":channel,"text":text}));
            req.end();
        }
    })
};
```

まず手動実行。

```
$ cd localSlack
$ sam local invoke notifySlack --event events/event.json
```

# 2. mocha + chaiで単体テスト

このままだと`context`が定義されてないとエラーするので以下のモジュールを入れる。
リファレンス：https://www.npmjs.com/package/aws-lambda-mock-context

```
$ cd hello-world 
$ npm install
$ npm install aws-sdk 
$ npm install aws-lambda-mock-context
$ ls
app.js		package-lock.json	tests
node_modules		package.json
```

テストコードの作成。

```js:test-handler.js
'use strict';

const index = require('../../app.js');
const chai = require('chai');
const expect = chai.expect;
const context = require('aws-lambda-mock-context');

describe('test', function () {
    it('Nomal test', async () => {
        const ctx = context();
        let event = require('../../../events/event.json');
        let value = "";
        index.handler(event, ctx)
        await ctx.Promise
            .then((val) => {
                //=> succeed() called
                value = val;
            })
            .catch(err => {
                //=> fail() called
                value = err;
                console.log(err);
            });
        expect(value).to.equal("sent message");
    });
});
```

ユニットテストの実行。

```
$ cd hello-world
$ env AWS_SDK_LOAD_CONFIG=true ./node_modules/.bin/mocha ./tests/unit/test-handler.js


  test
    ✓ Nomal test (616ms)


  1 passing (686ms)
```

なお、`AWS_SDK_LOAD_CONFIG`を指定しないで実行すると以下のエラーが発生します。

> UnhandledPromiseRejectionWarning: ConfigError: Missing region in config

ただ、上記の方法で`context.succeed`の値を取得し比較することはできますが、`context.fail`の値を比較しようとすると、エラーログがずらずらと含まれてしまい、比較できませんでした、、（誰か教えて欲しい、、）

# 3. lambdaへデプロイ

s3にバケットを作成したのち、下記コマンドでデプロイする。

```
$ cd localSlack
$ sam package --template-file template.yaml --output-template-file package.yml --s3-bucket バケット名 

$ sam deploy --template-file package.yml --stack-name スタック名 --capabilities CAPABILITY_IAM
```