SwaggerでLambdaのデバッグ環境を作る(1)


自分は、AWS LambdaのNode.jsを使うことが多いため、Lambdaで動作するNode.jsがローカルに立ち上げたSwaggerによるRESTful環境で動作すると都合がよいです。
また、ローカルに立ち上げたSwaggerによるRESTful環境でデバッグしたNode.jsのコードがそのままAWSのLambdaで動作すると都合がよいです。

そこで、SwaggerによるRESTful環境で、AWS LambdaのNode.jsが動作する環境を構築します。

最初は、デバッグ環境を構築し、簡単なRESTful呼び出しを実装し、Lambdaに配備するところまでです。
次回以降は、それを使って、AlexaやActions on GoogleやClovaのデバッグにも流用する手順を示したいと思います。

話はそれますが、最近じわじわ下記の記事のいいねが増えているようです。下記の記事をパワーアップさせたものですので、ぜひ本記事も参考にして下さい。
　[Swagger定義ファイルにSecurityDefinitionsを定義する](https://qiita.com/poruruba/items/a384b34408fbba3b5e47)

Swaggerとはなんぞは、の方は、以下を参考にしてください。
　[SwaggerでRESTful環境を構築する](https://qiita.com/poruruba/items/72d8c85f8923bc78b918)

Gitに上げておきます。
　https://github.com/poruruba/swagger_template
ですので、使うときは以下を実行するだけです。

```
> npm install -g swagger-node
> git clone https://github.com/poruruba/swagger_template
> cd swagger_template
> npm install
> npm start
```

以下、本記事の続編です。

* [SwaggerでLambdaのデバッグ環境を作る(2)：Helperライブラリを使ったLambdaの書き方](https://qiita.com/poruruba/items/dd2cb33d591914107925)
* [SwaggerでLambdaのデバッグ環境を作る(3)：Dialogflowをデバッグする](https://qiita.com/poruruba/items/eddba0e0deba4d770c13)
* [SwaggerでLambdaのデバッグ環境を作る(4)：Alexaをデバッグする](https://qiita.com/poruruba/items/0e13eb7146f4d59ebd4e)
* [SwaggerでLambdaのデバッグ環境を作る(5)：Clovaをデバッグする](https://qiita.com/poruruba/items/8f2f3a5191305f5c7f2e)
* [SwaggerでLambdaのデバッグ環境を作る(6)：GCPのCloud Endpointsをデバッグする](https://qiita.com/poruruba/items/27f585926d9c58d90d4c)
* [SwaggerでLambdaのデバッグ環境を作る(7)：AWS S3トリガをデバッグする](https://qiita.com/poruruba/items/67528767d6abf9799ed5)

# Swaggerプロジェクトを作成する

まずは、通常通り、Swaggerプロジェクトを作成します。
途中で聞かれるフレームワークにはexpressを選択します。

> swagger project create lambda_lab
> cd lambda_lab
> npm install

次に、app.jsを以下のコードに書き換えます。

```javascript:app.js
'use strict';

var SwaggerExpress = require('swagger-express-mw');
var express = require('express');
var app = express();
module.exports = app; // for testing

app.use(express.static('public'));

var cors = require('cors');
app.use(cors());
require('dotenv').config();

var session = require('express-session');
app.use(session({
    secret: "secret key",
    resave: false,
    saveUninitialized: false,
    cookie: { secure: true }
}));

var config = {
  appRoot: __dirname // required config
};

var jwt_decode = require('jwt-decode');

config.swaggerSecurityHandlers = {
  basicAuth : function (req, authOrSecDef, scopesOrApiKey, cb) {
    try{
      if( req.headers.authorization ){
        var basic = req.headers.authorization.trim();
        if(basic.toLowerCase().startsWith('basic '))
          basic = basic.slice(6).trim();

        var buf = new Buffer(basic, 'base64');
        var ascii = buf.toString('ascii');

        req.requestContext = {
          basicAuth : {
            basic : ascii.split(':')
          }
        };
      }else{
        cb(new Error("Authorization not defined"));
        return;
      }

      cb();
    }catch(error){
      cb(error);
    }
  },
  tokenAuth : function (req, authOrSecDef, scopesOrApiKey, cb) {
    try{
      if(scopesOrApiKey){
        var decoded = jwt_decode(scopesOrApiKey);
        req.requestContext = {
          authorizer : {
            claims : decoded
          }
        };
      }else{
        cb(new Error("Authorization not defined") );
        return;
      }

      cb();
    }catch(error){
      cb(error);
    }
  },
  jwtAuth: function (req, authOrSecDef, scopesOrApiKey, cb) {
    try{
      if( req.headers.authorization ){
        var decoded = jwt_decode(req.headers.authorization);
        req.requestContext = {
          jwtAuth : {
            claims : decoded
          }
        };
        var claims = {
          claims : decode,
          issuer: decoded.iss,
          id: decoded.sub,
          email: decoded.email
        };
        var buffer = new Buffer(JSON.stringify(claims));
        req.headers['x-endpoint-api-userinfo'] = buffer.toString('base64');
      }else{
        cb(new Error("Authorization not defined") );
        return;
      }

      cb();
    }catch(error){
      cb(error);
    }
  },
  apikeyAuth: function (req, authOrSecDef, scopesOrApiKey, cb) {
    try{
      if( scopesOrApiKey ){
        req.requestContext = {
          apikeyAuth : {
            apikey : scopesOrApiKey
          }
        };
      }else{
        cb(new Error("X-API-KEY not defined") );
        return;
      }

      cb();
    }catch(error){
      cb(error);
    }
  }
};

SwaggerExpress.create(config, function(err, swaggerExpress) {
  if (err) { throw err; }

  // install middleware
  swaggerExpress.register(app);

  var port = Number(process.env.PORT) || 10080;
  console.log('http PORT=' + port);
  app.listen(port);

  var https = require('https');
  var fs = require('fs');
  try{
    var options = {
      key:  fs.readFileSync('./cert/server.key'),
      cert: fs.readFileSync('./cert/server.crt'),
      ca: fs.readFileSync('./cert/JPRS_DVCA_G2_PEM.cer')
  //    key: fs.readFileSync('./cert/oreore/private-key.pem'),
  //    cert: fs.readFileSync('./cert/oreore/certificate.pem')
    };
    var sport = Number(process.env.SPORT) || 10443;
    var servers = https.createServer(options, app);
    console.log('https PORT=' + sport );
    servers.listen(sport);
  }catch(error){
//    console.log(error);
    console.log('can not load https');
  }
});
```

次に、静的HTMLファイルを配置するためのフォルダを作成します。

```bash
> mkdir public
```

もし、SSL証明書がある場合は、以下のフォルダを作成したうえで配置します。CERTファイルのファイル名は、app.jsを参照してください。

```bash
> mkdir cert
```

次に、以下のnpmモジュールをインストールします。

```bash
> npm install --save cors
> npm install --save dotenv
> npm install --save jwt-decode
> npm install --save express-session
```

dotenvを使っていますので、とりあえず中身が空の設定ファイルも作成しておきます。

```bash
> touch .env
```

次に、`api/controllers`の配下に、以下のrouting.jsを配置します。

```javascript:routing.js
'use strict';

/* 関数を以下に追加する */
const func_table = {
//  "test-func" : require('./test_func').handler,
//  "test-dialogflow" : require('./test_dialogflow').fulfillment,
};
const alexa_table = {
//  "test-alexa" : require('./test_alexa').handler,
};
const lambda_table = {
//  "test-lambda" : require('./test-lambda').handler,
//  "forward_lambda" : require('./forward_lambda').handler,
};
const express_table = {
//    "test-clova": require('./test-clova-skill').handler,
};
/* ここまで */

/* 必要に応じて、バイナリレスポンスのContent-Typeを以下に追加する */
const binary_table = [
//  'application/octet-stream',
];
/* ここまで */

var exports_list = {};
for( var operationId in func_table ){
    exports_list[operationId] = routing;
}
for( var operationId in alexa_table ){
    exports_list[operationId] = routing;
}
for( var operationId in lambda_table ){
    exports_list[operationId] = routing;
}
for( var operationId in express_table ){
    exports_list[operationId] = express_table[operationId];
}

module.exports = exports_list;

function routing(req, res) {
//    console.log(req);

    var operationId = req.swagger.operation.operationId;
    console.log('[' + operationId + ' calling]');

    try{
        var event;
        var func;
        if( func_table.hasOwnProperty(operationId) ){
            event = {
                headers: req.headers,
                body: JSON.stringify(req.body),
                path: req.swagger.apiPath,
                httpMethod: req.method,
                queryStringParameters: req.query,
                requestContext: ( req.requestContext ) ? req.requestContext : {}
            };

            event.Host = req.hostname;

            func = func_table[operationId];
            res.func_type = "normal";
        }else if( alexa_table.hasOwnProperty(operationId) ){
            event = req.body;

            func = alexa_table[operationId];
            res.func_type = "alexa";
        }else if( lambda_table.hasOwnProperty(operationId) ){
            event = req.body.event;

            func = lambda_table[operationId];
            res.func_type = "lambda";
        }else{
            console.log('can not found operationId: ' + operationId);
            return_error(res, new Error('can not found operationId'));
            return;
        }
        res.returned = false;

//        console.log(event);

        var context = {
            succeed: (msg) => {
                console.log('succeed called');
                return_response(res, msg);
            },
            fail: (error) => {
                console.log('failed called');
                return_error(res, error);
            },
            original_req: req
        };

        var task = func(event, context, (error, response) =>{
            console.log('callback called');
            if( error )
                return_error(res, error);
            else
                return_response(res, response);
        });
        if( task instanceof Promise || (task && typeof task.then === 'function') ){
            task.then(ret =>{
                if( ret ){
                    console.log('promise is called');
                    return_response(res, ret);
                }else{
                    console.log('promise return undefined');
                    return_none(res);
                }
            })
            .catch(err =>{
                console.log('error throwed: ' + err);
                return_error(res, err);
            });
        }else{
            console.log('return called');
//            return_none(res);
        }
    }catch(err){
        console.log('error throwed: ' + err);
        return_error(res, err);
    }
}

function return_none(res){
    if( res.returned )
        return;
    else
        res.returned = true;

    res.statusCode = 200;
    res.type('application/json');

    if( res.func_type == 'alexa' ){
        res.json({});
    }else if(res.func_type == 'lambda'){
        res.json({ body: null });
    }else{
        res.json({});
    }
}

function return_error(res, err){
    if( res.returned )
        return;
    else
        res.returned = true;

    res.status(500);
    res.json({ errorMessage: err.toString() });
}

function return_response(res, ret){
    if( res.returned )
        return;
    else
        res.returned = true;

    if( ret.statusCode )
        res.status(ret.statusCode);
    for( var key in ret.headers )
        res.set(key, ret.headers[key]);

//    console.log(ret.body);

    if (!res.get('Content-Type'))
        res.type('application/json');

    if( binary_table.indexOf(res.get('Content-Type')) >= 0 ){
        var bin = new Buffer(ret.body, 'base64')
        res.send(bin);
    }else{
        if( res.func_type == 'alexa'){
            res.json(ret);
        }else if( res.func_type == 'lambda'){
            res.json({ body: ret });
        }else{
            if( ret.body || ret.body == "")
                res.send(ret.body);
            else
                res.json({});
        }
    }
}
```

api/controllers/hello.jsは使わないので削除しても問題ありません。
api/swagger/にあるSwaggerファイルは、以下に書き換えます。

```yaml:swagger.yaml
swagger: '2.0'
info:
  version: 'first version'
  title: Lambda Laboratory Server
host: localhost:10011
basePath: /

schemes:
  - http
  - https

consumes:
  - application/json
produces:
  - application/json

securityDefinitions:
  basicAuth:
    type: basic
  tokenAuth:
    type: apiKey
    in: header
    name: Authorization
  apikeyAuth:
    type: apiKey
    in: header
    name: X-API-KEY
#  jwtAuth:
#    authorizationUrl: ""
#    flow: "implicit"
#    type: "oauth2"
#    x-google-issuer: "https://cognito-idp.ap-northeast-1.amazonaws.com/【CognitoのプールID】"
#    x-google-jwks_uri: "https://cognito-idp.ap-northeast-1.amazonaws.com/【CognitoのプールID】/.well-known/jwks.json"
#    x-google-audiences: "【CognitoのアプリクライアントID】"

paths:
  /swagger:
    x-swagger-pipe: swagger_raw

definitions:
  Empty:
    type: "object"
    title: "Empty Schema"

  CommonRequest:
    type: object
  CommonResponse:
    type: object
```

以上で完成なのですが、何かと便利なユーティリティをapi/helpers配下に配置します。

```javascript:response.js
class Response{
    constructor(context){
        this.statusCode = 200;
        this.headers = {'Access-Control-Allow-Origin' : '*'};
        if( context )
            this.set_body(context);
        else
            this.body = "{}";
    }

    set_error(error){
        this.body = JSON.stringify({"err": error});
        return this;
    }

    set_body(content){
        this.body = JSON.stringify(content);        
        return this;
    }
    
    get_body(){
        return JSON.parse(this.body);
    }
}

module.exports = Response;
```

```javascript:redirect.js
class Redirect{
    constructor(url){
        this.statusCode = 302;
        this.headers = {'Location' : url};
        this.body = null;
    }
}

module.exports = Redirect;
```

以上で完成です。
`public/index.html`を作成し、起動してみましょう

```bash
> swagger project start
```

または

```bash
> node app.js
```

http://localhost:10080 にアクセスし、さきほどのindex.htmlが表示されるか確認しましょう。

# ポート番号の変更方法

ポート番号は環境変数で指定可能です。

.envに以下を指定します。

```conf:.env
PORT=80  ← HTTPのポート番号
SPORT=443   ← HTTPSのポート番号
```

# エンドポイントの追加

それでは早速、Swagger定義ファイルにエンドポイントを追加して、実体を実装してみます。

まずは、GETの場合。
例えば、以下を追記します。

```yaml:swagger.yaml
・・・
  /test-get:
    get:
      x-swagger-router-controller: routing
      operationId: test-get
      responses:
        200:
          description: Success
          schema:
            $ref: "#/definitions/CommonResponse"
・・・
```

実体を実装します。

```
> mkdir api/controllers/test-get
> vi api/controllers/test-get/index.js
```

例えばこんな感じです。

```javascript:index.js
'use strict';

const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/';
const Response = require(HELPER_BASE + 'response');
const Redirect = require(HELPER_BASE + 'redirect');

exports.handler = async (event, context, callback) => {
	console.log(event.queryStringParameters);
	return new Response({ message: 'GET : Hello World' });
};
```

route.jsのfunc_table の部分に、実体を実装するコードの場所を指定します。

```javascript:routing.js
const func_table = {
//  "test-func" : require('./test_func').handler,
"test-get" : require('./test-get').handler,
};
```

"test-get" : ・・・ のところには、swagger定義ファイルに指定したoperationIdに指定した値を指定します。
これにより、operationIdで指定したエンドポイント「/test-get」への呼び出しが、api/controllers/test-get/index.js に処理が渡されます。

次に、POSTの場合。

```javascript:swagger.js
・・・
  /test-post:
    post:
      x-swagger-router-controller: routing
      operationId: test-post
      parameters:
        - in: body
          name: body
          schema:
            $ref: "#/definitions/CommonRequest"
      responses:
        200:
          description: Success
          schema:
            $ref: "#/definitions/CommonResponse"
・・・
```

（ちなみに、CommonRequestやCommonResponseとしているのは、これ自体あまり意味はないのですが、こうしておくことで、API Gatewayに上げたときに、無名のモデルができるてしまうのを避けるためです）

それでは実体を実装します。

```
> mkdir api/controllers/test-post
> vi api/controllers/test-post/index.js
```

例えばこんな感じです。

```javascript:index.js
'use strict';

const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/';
const Response = require(HELPER_BASE + 'response');
const Redirect = require(HELPER_BASE + 'redirect');

exports.handler = async (event, context, callback) => {
	var body = JSON.parse(event.body);
	console.log(body);
	return new Response({ message: 'POST : Hello World' });
};
```

実体を実装するコードの場所を指定します。

```javascript:routing.js
const func_table = {
//  "test-func" : require('./test_func').handler,
"test-get" : require('./test-get').handler,
"test-post" : require('./test-post').handler,
};
```

実体の場所をtest-getとtest-postで同じにすることもできます。
その場合は、event.handlerの中で、event.pathにエンドポイント名が入っていますので、処理分岐するようにします。

作成したフォルダを、Visual Studio Codeで開けば、ブレイクポイントを入れたりして、console.logが出力されたりと、簡単にデバッグできることがわかると思います。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/d234434c-327d-a028-6edf-d9511f1007c5.png)

この状態で、しっかり実装・デバッグしておきましょう。

# AWS LambdaにHelperライブラリのLayerを追加

AWS Lambdaに配備していくのですがその前に、複数のLambdaで共通利用するHelperライブラリを配備しておきます。
response.jsやredirect.jsは、共通的に使うため、AWS LambdaのLayer機能を使って、Lambdaでも共通化しましょう。
一度上げておけば、LambdaのコードをアップするごとにHelperライブラリを挙げる必要がなくなります。
（このLayerという機能は最近追加された機能で、大変便利ですので、どんどん活用しましょう。）

まずは、共通利用したいファイル(response.jsとredirect.js)をまとめてZIP化します。
そして、AWS管理コンソールからLambdaを開き、左側のナビゲータからLayerを選択します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/0d8764fe-641a-66ab-6a45-6d84cbc3a9b4.png)

「Layerの作成」ボタンを押下します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/13406d0e-9bae-fb43-b32d-5f399dab67d9.png)

名前は何でもよいですが、例えば、「helpers」とします。
先ほどZIP化したファイルを指定し、互換性のあるランタイムとして「Node.js 8.10」を選択し、最後に「作成」ボタンを押下します。
これで、複数のLambdaで共用できるようになりました。
（成功した後に表示されるバージョン番号は、同じ名前でアップロードした回数に依存します）

# AWS Lambdaにコードを配置

それではいよいよ完成したコードをAWS Lambdaに配備してみましょう。

AWS Lambdaの管理コンソールから、Lambda関数を作成します。
ランタイムは、Node.js 8.10を選択します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/6cd6f7d0-b054-f377-7b41-320afb7a6335.png)

関数コードには、index.jsの中身をコピペします。
次に、HelperライブラリのLayerを追加します。DesignerのところにあるLayerを選択して、追加したhelperを選択します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/891c91eb-29cc-e9dc-a565-7229cd86aad3.png)

また、環境変数のところに、HELPER_BASEを追加し、値として、「/opt/」を指定します。
（Layer追加したライブラリはこのディレクトリに配置されるためです）

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/baad917a-9e49-b2f0-2cc1-405177a44d5b.png)

「test-post」も同様に実施します。

# API GatewayにSwagger定義を追加

次に、API GatewayのWeb管理コンソールを開きます。
そして、「＋APIの作成」ボタンを押下します。

新しいAPIの作成として、「Swagger からインポート」を選択します。
そして、swagger.yamlをコピペしましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/ea7d99d9-4261-f496-63b4-f596576b91e2.png)

「インポート」ボタンを押下します。
警告が出ますが、気にせず、「インポートして警告を無視する」ボタンを押下してインポートを進めます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/409bdb5f-92c0-0916-6b29-d9e9f0524efc.png)

（Swagger定義ファイルに指定したinfo:titleがそのままAPIの名前になるため、変える場合はSwagger定義ファイルを変更してからインポートしてください）

/swaggerは、AWS Lambdaでは動作しないため、削除します。「リソースの削除」で削除できます。

次に、各エンドポイントにLambdaを割り当てます。
ここで、忘れずに「Lambdaプロキシの統合を使用」のチェックボックスはOnにします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/ad3443d3-cda1-6841-809c-80b9fed47e9d.png)

もう一つ忘れずに、「CORSの有効化」をしておきましょう。
 
![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/f3491501-fde5-13c8-d710-b3fb53bcf271.png)

「test-post」も同様に実施します。

最後に、APIのデプロイをします。ステージ名は例えば「v1」とします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/26eff5ed-60c1-3e82-8283-7d2b399aaeeb.png)

特に問題がなければ、「URLの呼び出し」のところにURLが割り当てられているのがわかります。

動作確認には、POSTMANが使いやすいです。
さきほどのURLに、「test-get」を付けてGET呼び出し、「test-post」を付けてPOST呼び出しをしてみてください。
無事に、レスポンスが返ってくれば成功です。

#後記

swagger-nodeって、Node.js v12では動かなくなっているっぽい。。。
急遽、swagger-node使わない版を作りました。。。

poruruba/express_template
　https://github.com/poruruba/express_template

以上です。