Help us understand the problem. What is going on with this article?

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を定義する

Swaggerとはなんぞは、の方は、以下を参考にしてください。
 SwaggerでRESTful環境を構築する

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プロジェクトを作成する

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

swagger project create lambda_lab
cd lambda_lab
npm install

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

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ファイルを配置するためのフォルダを作成します。

> mkdir public

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

> mkdir cert

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

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

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

> touch .env

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

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ファイルは、以下に書き換えます。

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配下に配置します。

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;
redirect.js
class Redirect{
    constructor(url){
        this.statusCode = 302;
        this.headers = {'Location' : url};
        this.body = null;
    }
}

module.exports = Redirect;

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

> swagger project start

または

> node app.js

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

ポート番号の変更方法

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

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

.env
PORT=80HTTPのポート番号
SPORT=443HTTPSのポート番号

エンドポイントの追加

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

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

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

例えばこんな感じです。

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 の部分に、実体を実装するコードの場所を指定します。

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の場合。

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

例えばこんな感じです。

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' });
};

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

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

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

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

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

image.png

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

AWS Lambdaにコードを配置

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

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

image.png

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

image.png

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

image.png

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

API GatewayにSwagger定義を追加

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

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

image.png

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

image.png

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

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

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

image.png

もう一つ忘れずに、「CORSの有効化」をしておきましょう。

image.png

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

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

image.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

以上です。

poruruba
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away