自分は、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でLambdaのデバッグ環境を作る(2):Helperライブラリを使ったLambdaの書き方
- SwaggerでLambdaのデバッグ環境を作る(3):Dialogflowをデバッグする
- SwaggerでLambdaのデバッグ環境を作る(4):Alexaをデバッグする
- SwaggerでLambdaのデバッグ環境を作る(5):Clovaをデバッグする
- SwaggerでLambdaのデバッグ環境を作る(6):GCPのCloud Endpointsをデバッグする
- SwaggerでLambdaのデバッグ環境を作る(7):AWS S3トリガをデバッグする
Swaggerプロジェクトを作成する
まずは、通常通り、Swaggerプロジェクトを作成します。
途中で聞かれるフレームワークにはexpressを選択します。
swagger project create lambda_lab
cd lambda_lab
npm install
次に、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を配置します。
'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: '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配下に配置します。
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;
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に以下を指定します。
PORT=80 ← HTTPのポート番号
SPORT=443 ← HTTPSのポート番号
エンドポイントの追加
それでは早速、Swagger定義ファイルにエンドポイントを追加して、実体を実装してみます。
まずは、GETの場合。
例えば、以下を追記します。
・・・
/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
例えばこんな感じです。
'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 の部分に、実体を実装するコードの場所を指定します。
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の場合。
・・・
/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
例えばこんな感じです。
'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' });
};
実体を実装するコードの場所を指定します。
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が出力されたりと、簡単にデバッグできることがわかると思います。
この状態で、しっかり実装・デバッグしておきましょう。
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を選択します。
「Layerの作成」ボタンを押下します。
名前は何でもよいですが、例えば、「helpers」とします。
先ほどZIP化したファイルを指定し、互換性のあるランタイムとして「Node.js 8.10」を選択し、最後に「作成」ボタンを押下します。
これで、複数のLambdaで共用できるようになりました。
(成功した後に表示されるバージョン番号は、同じ名前でアップロードした回数に依存します)
AWS Lambdaにコードを配置
それではいよいよ完成したコードをAWS Lambdaに配備してみましょう。
AWS Lambdaの管理コンソールから、Lambda関数を作成します。
ランタイムは、Node.js 8.10を選択します。
関数コードには、index.jsの中身をコピペします。
次に、HelperライブラリのLayerを追加します。DesignerのところにあるLayerを選択して、追加したhelperを選択します。
また、環境変数のところに、HELPER_BASEを追加し、値として、「/opt/」を指定します。
(Layer追加したライブラリはこのディレクトリに配置されるためです)
「test-post」も同様に実施します。
API GatewayにSwagger定義を追加
次に、API GatewayのWeb管理コンソールを開きます。
そして、「+APIの作成」ボタンを押下します。
新しいAPIの作成として、「Swagger からインポート」を選択します。
そして、swagger.yamlをコピペしましょう。
「インポート」ボタンを押下します。
警告が出ますが、気にせず、「インポートして警告を無視する」ボタンを押下してインポートを進めます。
(Swagger定義ファイルに指定したinfo:titleがそのままAPIの名前になるため、変える場合はSwagger定義ファイルを変更してからインポートしてください)
/swaggerは、AWS Lambdaでは動作しないため、削除します。「リソースの削除」で削除できます。
次に、各エンドポイントにLambdaを割り当てます。
ここで、忘れずに「Lambdaプロキシの統合を使用」のチェックボックスはOnにします。
もう一つ忘れずに、「CORSの有効化」をしておきましょう。
「test-post」も同様に実施します。
最後に、APIのデプロイをします。ステージ名は例えば「v1」とします。
特に問題がなければ、「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
以上です。