Serverless Frameworkを使ってIBM Cloud Functionsの複数の関数&リソースを一括デプロイする

この記事はIBM Cloud Advent Calendar2018の13日目の記事です。
https://qiita.com/advent-calendar/2018/ibmcloud

[IBM Cloud Functions](https://console.bluemix.net/openwhisk/)を使うとIBM Cloudを含めた様々なサービスやAPIをクラウド内外から呼び出すのを簡単に行うことができます。
便利ではある反面、複数の関数/他のサービスの認証情報の紐付け/トリガーやルールといったリソースを依存関係をまとめながらIBM Cloudにデプロイするのは少々ではすまないレベルで面倒です。
この依存関係をまとめてデプロイするために、Serverless Frameworkを使おうぜという話です。

[公式の資料](https://console.bluemix.net/docs/openwhisk/openwhisk_goserverless.html?locale=ja)には紹介されているものの、調べた限りIBM Cloud Functions(OpenWhisk)+Serverless Frameworkという組み合わせは少ないみたいなので、これから使う人の一助になれば幸いです。
(あっても[PHPの例](https://akrabat.com/using-serverless-framework-with-openwhisk-php/)くらいしかなかった。。。)

## 時間がない人向け

- 今回使ったソースコードを早く見たい人はこちら
    - https://github.com/sasaken555/sls-simple-api-handler
- Serverless Frameworkのドキュメント
    - https://serverless.com/framework/docs/providers/openwhisk/

## シンプルなCRUD操作のAPIをデプロイする

今回はCRUD操作を行う単純なREST APIを作成します。
元にした題材はIBM Developer Patternsから引っ張ってきました。
--> [API 呼び出しに応答してオートスケーリングするアクションを作成する](https://developer.ibm.com/jp/patterns/build-serverless-api-handlers-3/)
また、2018/12/7に[Node.js v10のサポートが発表された](https://www.ibm.com/blogs/bluemix/2018/12/ibm-cloud-functions-adds-support-for-node-js-10/)ので使ってみます。

### Serverless Frameworkの準備

Node.jsとIBM Cloud CLIが手元のPCにインストール済であることが前提です。まずはServerless FrameworkとIBM Cloud Functionsを使う準備をしましょう。以下のコマンドを打てばOKです。

```shell
$ npm i -g serverless serverless-openwhisk
$ ibmcloud plugin install cloud-functions
$ ibmcloud login -a <endpoint such as api.ng.bluemix.net>
$ ls -l ~/.wskprops
<home directory>/.wskprops
```

Serverless Frameworkのドキュメントをみると"Credentialを設定してくれ"と書いてあったりしますが、ibmcloudコマンドでログイン済であれば勝手に設定されるので深く考えなくて良いです。ホームディレクトリに`.wskprops`があればCredential設定は完了なので次に進みます。

### Serverless Frameworkを使ってOpenWhisk関数を作成する

開発を進めます。Serverless Frameworkは開発・デプロイに必要な一式を事前に用意するためのテンプレートが用意されているので、ありがたく使いましょう。
以下のようにテンプレート名とサービス名を指定します。ここで言う”サービス"とは、複数の関数、その他関数に絡むリソースをまとめたプロジェクトの単位と考えてください。

```shell
$ serverless create --template openwhisk-nodejs --path simple-api-handler
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "<path to application directory>/simple-api-handler"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.34.1
 -------'

Serverless: Successfully generated boilerplate for template: "openwhisk-nodejs"
```

やだ、かっこいい。。。雛形はこれで完成です。
テンプレートはIBM Cloud Functionsで使用できる言語はほぼ網羅しています。~~java-mavenのテンプレートはそのままじゃ動かないのは許さない~~
https://github.com/serverless/serverless/tree/master/lib/plugins/create/templates

- openwhisk-nodejs
- openwhisk-python
- openwhisk-java-maven
- openwhisk-php
- openwhisk-ruby
- openwhisk-swift

関数の開発自体はIBM Cloud Functions(OpenWhisk)の作法に沿って書けばOKです。Serverless Framework側から書き方やライブラリの導入の強制はないので、開発部分は好きな書き方・FWを使用ください。
今回の題材であるAPIは以下のようになりました。

[ディレクトリ構成]

```shell
$ tree .
.
├── README.md
├── package-lock.json
├── package.json
├── serverless.yml
├── src
│   ├── blg
│   │   └── catBlg.js
│   ├── dao
│   │   └── catDao.js
│   └── handler.js
└── test
    └── blg
        └── catBlg.test.js

5 directories, 8 files
```

[関数のエントリーポイント]

```javascript
/**
 * Entrypoint for Cat Oparation.
 */

"use strict";

// Import Business Logic
const catBlg = require("./blg/catBlg");

/**
 * ネコをDBに登録する
 * @param {String} params.__bx_creds.url 接続URL
 * @param {String} params.name 名称
 * @param {String} params.color 色
 * @returns {Promise} the Cloudant result
 */
function createCat(params) {
  return catBlg.create(params);
}

/**
 * キーに合致するネコを取得する.
 * @param {String} params.__bx_creds.url 接続URL
 * @param {String} params.id ID
 * @returns {Promise} the Cloudant result
 */
function getCat(params) {
  return catBlg.getOne(params);
}

/**
 * キーに合致するネコを更新する.
 * @param {String} params.__bx_creds.url 接続URL
 * @param {String} params.name 名称
 * @param {String} params.color 色
 * @returns {Promise} the Cloudant result
 */
function updateCat(params) {
  return catBlg.update(params);
}

/**
 * キーに合致するネコを取得する.
 * @param {String} params.__bx_creds.url 接続URL
 * @param {String} params.id ID
 * @returns {Promise} the Cloudant result
 */
function deleteCat(params) {
  return catBlg.deleteOne(params);
}

exports.createCat = createCat;
exports.getCat = getCat;
exports.updateCat = updateCat;
exports.deleteCat = deleteCat;
```

### 関数と依存関係のある全てのリソースを定義する

ここからが本題です。
Serverless Frameworkでは関数に絡む全てのリソースの定義を `serverless.yml` に記述します。基本は雛形で生成されたYAMLファイルを編集すれば間違いはありません。主に記述するのは、`functions`, `resources` セクションでしょう。以下作成したYAMLファイルの抜粋です。

```yml
service: simple-api-handler

provider:
  name: openwhisk
  runtime: nodejs:10

functions:
  getCat:
    handler: src/handler.getCat
    name: "cats/getCat"
    events:
      - http:
          method: GET
          path: /v1/cats/{id}
          resp: http
  # NOTE: createCat, updateCat, deleteCatもgetCatと同様に書くので省略

plugins:
  - serverless-openwhisk

resources:
  packages:
    cats:
      bind:
        - service:
            name: cloudantnosqldb
            instance: {{ instanceName }}
            key: {{ creadentialKey }}
```

ここでは、1サービス内に4つの関数、関数をキックするイベントとしてAPI Gatewayを使う、依存するサービスはCloudantの特定のインスタンスの想定です。
関数をまとめたパッケージをfunctionsセクションのnameで `<packageName>/<functionName>` と書くことで定義できます。サービスのバインディングはresourcesセクションで定義してますが、特定の関数で定義したい場合は、その関数のセクション内で記述すればOKです。
ちなみに、バインドしたサービスの情報は、関数のパラメータ `__bx_creds` に全部入っています。

IBM Cloud Functions単品だと手作業でやっていたそれぞれの関数のデプロイ、サービス(Cloudant)のバインディング、API定義の作成。。。これがこの1つのYAMLで一括でデプロイできます!!すごい！

### IBM Cloudにデプロイする

さて、最後のステップです。上記で作成した`serverless.yml`を使ってデプロイします。
と言ってもやることは1コマンド叩くだけです。

```shell
serverless deploy
```

これで全関数、サービスのバインディング、API GatewayにAPI登録まで全部完了です！