サーバーレスフレームワークのJAWSが名前を変えてServerlessに変わりました。
JAWSを使ってAPIを作っていたので、ちょうど良いのでアップデートしてみました話です。
変更点
公式にある通り、変更点は下記になります。
- Node V4
- Name & Filename Changes
- New Function JSON Format
- One Set Of Lambdas Per Region
- AWS-Recommended Workflow
- Removed CloudFormation Support For Project Lambdas
- 1 REST API Containing Your Project's Stages
- Stage Variable Support
- Plugin Architecture
大枠としての構成はほぼ同じですが、ファイル名やJSONの形など細々と変更が入っています。
マイグレーションツールみたいなのもあるかなと思いきや、そんなものは存在しないので目視でdiffを取ってJAWSからServerlessへアップデートすることになります。
プロジェクト構成
JAWSとServerlessではproject create
で作成したときの構成に差があります。
JAWS
$ jaws create project
$ cd jaws-XXXX
$ aws module create my-module my-function
$ tree
.
├── README.md
├── admin.env
├── aws_modules
│ └── my-module
│ ├── awsm.json
│ └── my-function
│ ├── awsm.json
│ ├── event.json
│ ├── handler.js
│ └── index.js
├── cloudformation
│ └── dev
│ └── us-east-1
│ └── resources-cf.json
├── jaws.json
├── lib
├── node_modules
│ └── jaws-core-js
├── package.json
└── tests
Serverless
$ sls create project
$ cd serverlessXXXX
$ sls module create --module my-module --function my-function
$ tree
.
├── README.md
├── admin.env
├── back
│ └── modules
│ └── my-module
│ ├── lib
│ │ └── index.js
│ ├── my-function
│ │ ├── event.json
│ │ ├── handler.js
│ │ └── s-function.json
│ ├── node_modules
│ │ └── serverless-helpers-js
│ ├── package
│ ├── package.json
│ └── s-module.json
├── cloudformation
│ └── resources-cf.json
├── plugins
│ └── custom
└── s-project.json
細々とした差はありますが、一番の違いはプロジェクトルートからpackage.json
が排除されたところにあります。
JAWSではAPI全体を一つのnodeプロジェクトとして構成していましたが、Serverlessではmodule一つ一つをnodeプロジェクトとして構成し、moduleをより分離しやすいように凝縮されています。(個人的にはJAWSの方がアプリケーションを管理しやすくて好みでした・・・)
JAWSからServelessへのアップデートは基本的には構造の差を埋めることが大部分を占めますが、アプリケーションからモジュールへの思想の変化にどう対応するかも重要な要素になります。
アップデート
1. admin.envの更新
JAWSで使用するAWSのprofileを指定していたadmin.env
が変わりました。
JAWS
ADMIN_AWS_PROFILE=[Your Profile]
Serverless
SERVERLESS_ADMIN_AWS_ACCESS_KEY_ID=[Your Access Key]
SERVERLESS_ADMIN_AWS_SECRET_ACCESS_KEY=[Your Secret Key]
これまではprofile名を指定して~/.aws/credentials
に記載したアクセスキー、シークレットキーを使用していたのを、直接admin.env
に記載するようになっています。
これによってキーをリポジトリにコミットしなければいけない形になってしまいましたが、一応そのあたりの救済処置として環境変数を使用することができます。
$ export SERVERLESS_ADMIN_AWS_ACCESS_KEY_ID=[Your Access Key]
$ export SERVERLESS_ADMIN_AWS_SECRET_ACCESS_KEY=[Your Secret Key]
2. awsm.jsonの変換
JAWSではプロジェクト、モジュール、関数の設定にjaws.json
、awsm.json
を使用していましたが、Serverlessではs-project.json
、s-module.json
、s-function.json
とそれぞれ用途毎にファイル名が変わりました。
各jsonに記載する内容自体はそのままですが、微妙に中身が変わっています。
s-project.json
JAWS
{
"name": "jaws-EkxSkpiBx",
"version": "0.0.1",
"location": "https://github.com/...",
"author": "",
"description": "",
"domain": "myapp-EywrkTsB.com",
"stages": {
"dev": [
{
"region": "us-east-1",
"iamRoleArnLambda": "[Lambda IAM Role]",
"iamRoleArnApiGateway": "[APIGateway IAM Role]",
"jawsBucket": "[S3 Bucket]"
}
]
}
}
Serverless
{
"name": "serverless4kPiJajrx",
"version": "0.0.1",
"profile": "serverless-0",
"location": "https://github.com/...",
"author": "",
"description": "",
"domain": "myapp-nkhi1psh.com",
"stages": {
"development": [
{
"region": "us-east-1",
"iamRoleArnLambda": "[Lambda IAM Role",
"regionBucket": "[S3 Bucket]"
}
]
},
"custom": {},
"plugins": []
}
変更点は各ステージのプロパティ名が変わったのと、API Gatewayの設定が削除されたところになります。
ステージ情報は基本的にそのまま流用可能ではありますが、デプロイ済みのステージを一度削除して作り直す必要があるため、アップデートする場合はstatges
以下を削除してしまって構いません。
s-module.json
JAWS
{
"name": "my-module",
"version": "0.0.1",
"location": "https://github.com/...",
"author": "",
"description": "",
"resources": {
"cloudFormation": {
"LambdaIamPolicyDocumentStatements": [],
"ApiGatewayIamPolicyDocumentStatements": [],
"Resources": {}
}
}
}
Serverless
{
"name": "my-module",
"version": "0.0.1",
"profile": "aws-0",
"location": "https://github.com/...",
"author": "",
"description": "",
"custom": {},
"cloudFormation": {
"lambdaIamPolicyDocumentStatements": [],
"resources": {}
},
"runtime": "nodejs"
}
変更点は各プロパティ名や順序の変更とAPIGatewayのPolicyが削除されたところになります。
CloudFormation用のプロパティがresources.cloudFormation.Resources
だったのが、cloudFormation.resources.Resource
に変わっているので気をつけて下さい。
s-function.json
JAWS
{
"lambda": {
"envVars": [],
"deploy": false,
"package": {
"optimize": {
"builder": "browserify",
"minify": true,
"ignore": [],
"exclude": [
"aws-sdk"
],
"includePaths": []
},
"excludePatterns": []
},
"cloudFormation": {
"Description": "",
"Handler": "aws_modules/my-module/my-function/handler.handler",
"MemorySize": 1024,
"Runtime": "nodejs",
"Timeout": 6
}
},
"apiGateway": {
"deploy": false,
"cloudFormation": {
"Type": "AWS",
"Path": "my-module/my-function",
"Method": "GET",
"AuthorizationType": "none",
"ApiKeyRequired": false,
"RequestTemplates": {},
"RequestParameters": {},
"Responses": {
"400": {
"statusCode": "400"
},
"default": {
"statusCode": "200",
"responseParameters": {},
"responseModels": {},
"responseTemplates": {
"application/json": ""
}
}
}
}
}
}
Serverless
{
"functions": {
"My-moduleMy-function": {
"custom": {
"excludePatterns": [],
"envVars": []
},
"handler": "modules/my-module/my-function/handler.handler",
"timeout": 6,
"memorySize": 1024,
"endpoints": [
{
"path": "my-module/my-function",
"method": "GET",
"authorizationType": "none",
"apiKeyRequired": false,
"requestParameters": {},
"requestTemplates": {
"application/json": ""
},
"responses": {
"400": {
"statusCode": "400"
},
"default": {
"statusCode": "200",
"responseParameters": {},
"responseModels": {},
"responseTemplates": {
"application/json": ""
}
}
}
}
]
}
}
}
変更点として大きく構造は変わっていますが基本的にはプロパティ名や順序の変更になります。
JAWSのときにあったoptimize
はserverless-optimizer-pluginとして切り出されています。
必要な場合はインストール後に設定をcustom
プロパティ以下に移動させてください。
3. CloudFormationテンプレートの統合
JAWSからServerlessのアップデートで一番嬉しいこととして、CloudFormationのテンプレートが統合された。
JAWSでは
.
└── cloudformation
└── dev
└── us-east-1
└── resources-cf.json
と、[Stage Name]/[Region]/resources-cf.json
とステージとリージョン毎にテンプレートを作成していたのが、Serverlessでは
.
└── cloudformation
└── resources-cf.json
と一つに統合されています。
また、同一ディレクトリに生成されていたlambda-cf.json
も生成されなくなり、resources-cf.json
のみを管理すれば良い形になっています。
4. Handlerの更新
JAWS
'use strict';
/**
* AWS Module: Action: Lambda Handler
* "Your lambda functions should be a thin wrapper around your own separate
* modules, to keep your code testable, reusable and AWS independent"
*/
require('jaws-core-js/env');
// Modularized Code
var action = require('./index.js');
// Lambda Handler
module.exports.handler = function(event, context) {
action.run(event, context, function(error, result) {
return context.done(error, result);
});
};
Serverless
'use strict';
/**
* Serverless Module: Lambda Handler
* - Your lambda functions should be a thin wrapper around your own separate
* modules, to keep your code testable, reusable and AWS independent
* - 'serverless-helpers-js' module is required for Serverless ENV var support. Hopefully, AWS will add ENV support to Lambda soon :)
*/
// Require Serverless ENV vars
var ServerlessHelpers = require('serverless-helpers-js').loadEnv();
// Require Logic
var lib = require('../lib');
// Lambda Handler
module.exports.handler = function(event, context) {
lib.respond(event, function(error, response) {
return context.done(error, response);
});
};
LambdaのHandler部分も一箇所だけ変更されています。
require('jaws-core-js/env');
var ServerlessHelpers = require('serverless-helpers-js').loadEnv();
これに伴って依存するライブラリも変わっているので、合わせて更新してください。
5. ステージの更新
JAWSからServerlessのアップデートで最も難関なところでデプロイ済みのステージをどう更新するかにあります。
JAWSではスタック名が[Stage Name]-[Project Name]-r
でしたが、[Project Name]-[Stage Name]-r
に変更されています。
そのため、 既存の環境をそのまま更新する訳にはいかず、無停止で切り替えるには一時的に環境を二重に作ってエンドポイントを切り替える必要があります。
(他にも何かうまい方法はあるかもです)
-
s-project.json
からアップデートするステージ情報を削除 -
$ sls stage create --stage [Stage Name] --region [Region]
で新規にリージョンを作成 -
$ sls resources deploy --stage [Stage Name] --region [Region]
で依存リソースを更新 -
$ sls function deploy --stage [Stage Name] --region [Region]
でLambda Functionを更新 -
$ sls endpoint deploy --stage [Stage Name] --region [Region]
でAPIGatewayを更新 - Route53で新しく作成したAPI Gatewayを指定
- 古い環境のスタック、S3を削除
上の手順で無停止に切り替え可能なはずですが、実際にやった訳ではないので良しなにやってください。
(まだ、開発中だったので停止しても大丈夫だったので助かりました)
タスクの作成
ここまででアップデートは完了しましたが、このままでは開発がやりにくいのでいくつかタスクを作成します。
タスクの多段実行
Serverlessになってnodeプロジェクトが多段になりました。
このままでは毎回モジュール以下まで移動してタスクを実行する必要があります。
そこで、親プロジェクトから子プロジェクトのタスクを呼び出せるように変更します。
$ npm install --save-dev @k-kinzal/each
"scripts": {
"test": "mocha test/**/*.spec.js"
}
"scripts": {
"test": "each back/modules/* -c -y 'npm test'"
}
lintなど他にもタスクがあれば同様に親プロジェクトから呼び出せるように変更してください。
蛇足ですが、シンプルに書く方法なかったので@k-kinzal/each
を作りましたが正直筋悪いなぁと思ってます。
何か良い方法をご存知の方がいたら教えてください。
リソースの更新
Serverlessでは基本的に
$ sls module install [URL]
でモジュールを更新したときのみ、cloudformation/resources-cf.json
が更新されます。
しかし、これでは開発中のモジュールのリソースを反映できないので、モジュールで設定したリソースを反映するタスクを作成します。
'use strict';
var fs = require('fs');
var glob = require('glob');
var gulp = require('gulp');
var path = require('path');
gulp.task('update-resources-cf', function () {
var lambdaPolicies = glob.sync('./back/modules/*/s-module.json').map(function(p) {
return require(path.resolve(p)).cloudFormation.lambdaIamPolicyDocumentStatements;
}).reduce(function(a, b) {
return a.concat(b);
});
var resources = glob.sync('./back/modules/*/s-module.json').map(function(p) {
return require(path.resolve(p)).cloudFormation.resources;
}).reduce(function(a, b) {
return Object.assign(a, b);
});
glob.sync('./cloudformation/resources-cf.json').forEach(function(p) {
var cf = require(path.resolve(p));
cf.Resources.IamPolicyLambda.Properties.PolicyDocument.Statement =
cf.Resources.IamPolicyLambda.Properties.PolicyDocument.Statement.concat(lambdaPolicies).filter(unique);
cf.Resources = Object.assign(cf.Resources, resources);
fs.writeFileSync(path.resolve(p), JSON.stringify(cf, null, 2));
});
function unique(x, i, self) {
var index = self.findIndex(function(y, j, self) {
try {
require('assert').deepEqual(x, y);
return true;
} catch (e) {
return false;
}
});
return i === index;
}
});
$ gulp update-resources-cf
ローカルサーバーの作成
Serverlessになって嬉しいことの一つでローカルサーバーを立ち上げるプラグインのserverless-serveが提供されるようになりました。
ただ、Serverlessのプラグイン機構はちょっとダサくてnpmの管理に乗せることができません。(plugins以下にpackage.jsonを配置すれば出来るけどツラいです・・・)
ツラいですがplugins
以下に該当のプラグインを直接配置して使用します。
$ cd plugins
$ git clone git@github.com:Nopik/serverless-serve.git
"plugins": [
{
"path": "serverless-serve"
}
]
"scripts": {
"test": "$(which sls) serve start"
}
$ npm run serve
これでローカルサーバーを立ち上げることができます。
--init
で起動時に実行するスクリプトを指定できるので、DynamoDB localなどローカルで起動する必要があるものはここで指定してください。
これでローカル開発も捗る!と言いたいところですが、このままではコードを更新する度にローカルサーバーを再起動してあげる必要があります。
それはあまりにも面倒臭いのでnode-devを使って更新時に自動でサーバーを再起動できるようにしてあげます。
$ npm install --save-dev node-dev
"scripts": {
"serve": "node-dev $(which sls) serve start"
}
$ npm run serve
ちなみに、Serverlessはglobal installしてほしいので$(which sls)
で解決するようにしていますが、直接npm installしてプロジェクト内に配置しても良いとは思います。
おわりに
というわけで何とかJAWSからServerlessにアップデートすることができました。
たまにこういった後方互換のない大きな変更が入るのであれですが、APIGateway+Lambda環境を作るにはServerlessがやっぱり便利です。
そのうちプロダクション環境での運用が始まったら、その話でも書こうと思います。