JAWS frameworkからServerlessにアップデートした話

サーバーレスフレームワークのJAWSが名前を変えて[Serverless](https://github.com/serverless/serverless)に変わりました。
JAWSを使ってAPIを作っていたので、ちょうど良いのでアップデートしてみました話です。

# 変更点

* [Differences From JAWS](https://github.com/serverless/serverless#differences-from-jaws)

公式にある通り、変更点は下記になります。

* 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**

```sh
$ jaws create project
$ cd jaws-XXXX
$ aws module create my-module my-function
```

```sh
$ 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**

```sh
$ sls create project
$ cd serverlessXXXX
$ sls module create --module my-module --function my-function
```

```sh
$ 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.env
ADMIN_AWS_PROFILE=[Your Profile]
```

**Serverless**

```:admin.env
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**

```json:jaws.json
{
  "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**

```json:s-project.json
{
  "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**

```json:aws_modules/[ModuleName]/awsm.json
{
  "name": "my-module",
  "version": "0.0.1",
  "location": "https://github.com/...",
  "author": "",
  "description": "",
  "resources": {
    "cloudFormation": {
      "LambdaIamPolicyDocumentStatements": [],
      "ApiGatewayIamPolicyDocumentStatements": [],
      "Resources": {}
    }
  }
}
```

**Serverless**

```json:back/modules/[ModuleName]/s-module.json
{
  "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**

```json:aws_modules/[ModuleName]/[FunctionName]/awsm.json
{
  "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**

```json:back/modules/[ModuleName]/[FunctionName]/s-function.json
{
  "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](https://github.com/serverless/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**

```js:aws_modules/[ModuleName]/[FunctionName]/handler.js
'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**

```js:back/modules/[ModuleName]/[FunctionName]/handler.js
'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部分も一箇所だけ変更されています。

```js
require('jaws-core-js/env');
```

```js
var ServerlessHelpers = require('serverless-helpers-js').loadEnv();
```

これに伴って依存するライブラリも変わっているので、合わせて更新してください。

#### 5. ステージの更新

JAWSからServerlessのアップデートで最も難関なところでデプロイ済みのステージをどう更新するかにあります。

JAWSではスタック名が`[Stage Name]-[Project Name]-r`でしたが、`[Project Name]-[Stage Name]-r`に変更されています。
そのため、 既存の環境をそのまま更新する訳にはいかず、無停止で切り替えるには一時的に環境を二重に作ってエンドポイントを切り替える必要があります。
（他にも何かうまい方法はあるかもです）

1. `s-project.json`からアップデートするステージ情報を削除
2. `$ sls stage create --stage [Stage Name] --region [Region]`で新規にリージョンを作成
3. `$ sls resources deploy --stage [Stage Name] --region [Region]`で依存リソースを更新
4. `$ sls function deploy --stage [Stage Name] --region [Region]`でLambda Functionを更新
5. `$ sls endpoint deploy --stage [Stage Name] --region [Region]`でAPIGatewayを更新
6. Route53で新しく作成したAPI Gatewayを指定
7. 古い環境のスタック、S3を削除

上の手順で無停止に切り替え可能なはずですが、実際にやった訳ではないので良しなにやってください。
（まだ、開発中だったので停止しても大丈夫だったので助かりました）

# タスクの作成

ここまででアップデートは完了しましたが、このままでは開発がやりにくいのでいくつかタスクを作成します。

## タスクの多段実行

Serverlessになってnodeプロジェクトが多段になりました。
このままでは毎回モジュール以下まで移動してタスクを実行する必要があります。

そこで、親プロジェクトから子プロジェクトのタスクを呼び出せるように変更します。

```
$ npm install --save-dev @k-kinzal/each
```

```json:back/modules/[ModuleName]/package.json
"scripts": {
  "test": "mocha test/**/*.spec.js"
}
```

```json:package.json
"scripts": {
  "test": "each back/modules/* -c -y 'npm test'"
}
```

lintなど他にもタスクがあれば同様に親プロジェクトから呼び出せるように変更してください。

蛇足ですが、シンプルに書く方法なかったので`@k-kinzal/each`を作りましたが正直筋悪いなぁと思ってます。
何か良い方法をご存知の方がいたら教えてください。

## リソースの更新

Serverlessでは基本的に

```sh
$ sls module install [URL]
```

でモジュールを更新したときのみ、`cloudformation/resources-cf.json`が更新されます。
しかし、これでは開発中のモジュールのリソースを反映できないので、モジュールで設定したリソースを反映するタスクを作成します。

```js:gulpfile.js
'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;
  }
});
```

```sh
$ gulp update-resources-cf
```

## ローカルサーバーの作成

Serverlessになって嬉しいことの一つでローカルサーバーを立ち上げるプラグインの[serverless-serve](https://github.com/Nopik/serverless-serve)が提供されるようになりました。
ただ、Serverlessのプラグイン機構はちょっとダサくてnpmの管理に乗せることができません。（plugins以下にpackage.jsonを配置すれば出来るけどツラいです・・・）
ツラいですが`plugins`以下に該当のプラグインを直接配置して使用します。

```sh
$ cd plugins
$ git clone git@github.com:Nopik/serverless-serve.git
```

```json:s-project.json
"plugins": [
  {
    "path": "serverless-serve"
  }
]
```

```json:package.json
"scripts": {
  "test": "$(which sls) serve start"
}
```

```sh
$ npm run serve
```

これでローカルサーバーを立ち上げることができます。
`--init`で起動時に実行するスクリプトを指定できるので、DynamoDB localなどローカルで起動する必要があるものはここで指定してください。

これでローカル開発も捗る！と言いたいところですが、このままではコードを更新する度にローカルサーバーを再起動してあげる必要があります。
それはあまりにも面倒臭いので[node-dev](https://github.com/fgnass/node-dev)を使って更新時に自動でサーバーを再起動できるようにしてあげます。

```sh
$ npm install --save-dev node-dev
```

```json:package.json
"scripts": {
  "serve": "node-dev $(which sls) serve start"
}
```

```sh
$ npm run serve
```

ちなみに、Serverlessはglobal installしてほしいので`$(which sls)`で解決するようにしていますが、直接npm installしてプロジェクト内に配置しても良いとは思います。

# おわりに

というわけで何とかJAWSからServerlessにアップデートすることができました。
たまにこういった後方互換のない大きな変更が入るのであれですが、APIGateway+Lambda環境を作るにはServerlessがやっぱり便利です。

そのうちプロダクション環境での運用が始まったら、その話でも書こうと思います。