IBM Cloud上のWatsonアプリをローカル環境で開発・デバッグする際のTips

(2018-07-01 新しい方式の認証について追記)

# はじめに

最近、IBM Cloud上でサンプルのWatsonアプリケーションを自分で作ることが何度かあったのですが、表題のことに関してまとまった情報がなかなかなくて、調べるのに苦労したので、自分の備忘録を兼ねて記載します。

## 認証情報
WatsonなどほとんどのIBM Cloudサービスは通常サービス(正確にはインスタンス)固有のuserid, passwordを持っていて、それをどのように設定するかという問題です。

まず、以下はIBM Cloudにおける基礎的な知識

IBM CloudではCloudFoundaryアプリケーションに、Watsonなどのサービスを「バインド」することがGUIからもコマンドラインからも可能です。
これを行うことにより、CloudFoundaryアプリケーション側では、次のような値が環境変数VCAP_SERVICESに設定されます。(usernameとpasswordは伏せ字にしています)

```json:
{
    "conversation": [
        {
            "credentials": {
                "url": "https://gateway.watsonplatform.net/conversation/api",
                "username": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
                "password": "xxxxxxxxxxxx"
            },
            "syslog_drain_url": null,
            "volume_mounts": [],
            "label": "conversation",
            "provider": null,
            "plan": "free",
            "name": "conversation-1",
            "tags": [
                "watson",
                "ibm_created",
                "ibm_dedicated_public",
                "lite"
            ]
        }
    ]
}
```

IBMがWatson Developers Cloudで提供しているライブラリを利用してWatson APIを呼び出すと、自動的にこの認証情報を見つけ出してAPI呼出し時の引数とするようになっています。なので、IBM Cloud上で動かすことが前提で、かつライブラリ経由でWatson APIを呼び出すのであれば、基盤的にサービスのバインドだけ気にしておけば、コードにその記載をする必要はありません。
また、API呼出しをライブラリ経由でなく直接行う場合も、次のようなコードで同じように環境変数から認証情報を取得することが可能です。
Pythonのアプリケーションから、ライブラリを使わずにDiscovery Newsを呼ぶ出すためのサンプルコードイメージを下記に示します。


```js:
import requests
environment_id = 'system'
collection_id = 'news'
vcap = json.loads(os.getenv("VCAP_SERVICES"))['discovery']        
username = vcap[0]['credentials']['username']
password = vcap[0]['credentials']['password']    

endpoint = "https://gateway.watsonplatform.net/discovery/api/v1/environments/"+environment_id+"/collections/"+collection_id+"/query?version=2017-09-01&"
    try:
        get_url = endpoint+"query=title:("+combo+")|enriched_title.entities.text:("+combo+")&count=50&return=title,url"
        results = requests.get(url=get_url, auth=(username, password)) 
        response = results.json()
```

問題は、この環境はあくまでIBM Cloud上で利用可能なものであるため、ローカル環境では別の方法を考える必要がある点です。
ここはいろいろな流儀があるのですが、試行錯誤の結果、私がたどり着いたのは次のように方法でした。以下に、Node.jsのサンプルコードを記載します。


```js:
// 構成情報の取得
const fs = require('fs');
if (fs.existsSync('local.env')) {
  console.log('構成情報をlocal.envから取得します');
  require('dotenv').config({ path: 'local.env' });
}

// Discoveryインスタンスの生成
const watson = require('watson-developer-cloud');
const discovery = new watson.DiscoveryV1({
    version_date: '2017-08-01'
});
```

ポイントは次のような点です。

* npmのモジュールとして"fs"と"dotenv"を利用します。

* リポジトリの中に以下のような雛形ファイル local.env.sampleを含めておいて、ローカル環境テスト時にはこれをlocal.envにコピーして利用します。
(デフォルトではローカルの構成ファイル名は ".env" なのですが、こうするとMacのFinderのデフォルト設定で見えないなど、ガイド書くのが面倒になるので、操作を簡単にする目的で設定ファイル名はあえて "local.env"としています。)

```
CONVERSATION_USERNAME=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
CONVERSATION_PASSWORD=xxxxxxxxxxxx
```

* npmモジュールのdotnevのconfigメソッドは、上のような書式の設定ファイルを読み込み、環境変数CONVERSATION_USERNAME, CONVERSATION_PASSWORDに値を設定します。
* DevelpersCloudのライブラリ new watson.XXXXはインスタンス生成時に環境変数XXXX_USERNAME, XXXX_PASSWORDの設定があると、そこから認証情報を取得し、API呼出し時のパラメータとします。
XXXXの部分は、どのAPIを呼び出すかにより変わります。現在利用可能なAPIでのXXXXのそれぞれの名称は次のとおりです。
例えばSTTであればSPEECH_TO_TEXT, TTSであればTEXT_TO_SPEECHとなります。

```
CONVERSATION
DISCOVERY
DOCUMENT_CONVERSION
LANGUAGE_TRANSLATOR
NATURAL_LANGUAGE_CLASSIFIER
NATURAL_LANGUAGE_UNDERSTANDING
PERSONALITY_INSIGHTS
RETRIEVE_AND_RANK
SPEECH_TO_TEXT
TEXT_TO_SPEECH
TONE_ANALYZER
VISUAL_RECOGNITION
```

(2018-07-01追記)
シドニーにインスタンスを作ると、APIKeyによる新しい認証方式となります。
その場合、上記設定ファイル``local.env``は次のようにかわります。
利用するwatson-dveloper-cloudのバージョンは3.5.3以上を使うように注意して下さい。

```
CONVERSATION_IAM_APIKEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
CONVERSATION_URL=https://gateway-syd.watsonplatform.net/discovery/api
```


ということで、上に示したようなシンプルなNode.jsコードで

* IBM Cloud環境でWatsonサービスとのバインドがされている場合、バインド情報から認証情報を取得
* ローカル環境の場合、ローカルファイルのlocal.envファイル(ソースにある雛形local.env.sampleをコピーして作る)から認証情報を取得

という目的を実現することが可能となります。
あとは、.gitignoreと.cfignoreにlocal.envを記載しておいて、ローカル以外にこの設定ファイルが出ていかないよう注意して下さい。

## (補足) IBM Cloud上でサービスの生成、アプリケーションのバインドを行う方法
上で説明したように、IBM Cloud環境であれば、サービスのバインドさえしてあればコード側でuserid, passwordについては何も考える必要なくなります。
しかし、初心者向けにアプリケーションのバインドの手順を示すとき、UIだと画面遷移が多くて結構面倒です。この点に関してもいろいろ試行錯誤の結果、以下の方式が一番シンプルだとの結論に達しました。

### サービス、認証情報をCFコマンドで作成

(例) Discovery Serviceを生成する場合

```
$ cf create-service discovery lite discovery-1
$ cf create-service-key discovery-1 myKey
```

### バインドはmanifest.ymlに記述

(例)　上記で生成したサービスをバインドしたい場合


```
---
declared-services:
  discovery-1:
    label: discovery
    plan: lite
applications:
- path: .
  memory: 128M
  services:
  - discovery-1
```

ここまでやっておくと、cf pushコマンド一発でアプリケーションバインドまでできるので、手順書がとてもシンプルになります。

## その他のID情報設定

例えばConversation APIのWORDSPACE_IDのように、APIによってはuserid, password以外のID情報が必要な場合があります。
この場合、Developers Cloudのライブラリでも特別な方法は用意されていないので、環境変数などからID値を取得してAPI呼出し時にパラメータで設定する形になります。
ちなみに、環境変数の値をアプリケーションから使う場合、Node.jsでは次のようなコードになります。processという変数はNode.jsがフレームワークとして事前に用意している変数なので、アプリケーション側で何か特別な宣言などを行う必要はありません。

```
var classifier_id = process.env.CLASSIFIER_ID;
```
### IBM Cloud上の場合
IBM Cloud上の場合は、管理機能で環境変数を設定できるので、それを使って設定する形になります。UIによる方法、コマンドラインによる方法の両方がありますが、コマンドラインの方がガイドがシンプルかと思います。コマンドの書式は次の通りです。

```
$ cf set-env <app_name> <env_name> <env_value>
```

### ローカルの場合
ローカルの場合は、上に説明したlocal.envに、追加したい環境変数も定義すればいいです。

## ホストがlistenするIPアドレス、ポート番号
IBM Cloud上でサービスを動かす場合、listenするポート番号については特別なお作法が必要です。具体的には、VCAP_APP_PORTで示されたポートを指定する形になります。
ローカルでも使えるコードの雛形はNode.jsの場合、以下のようになります。
下記のサンプルコードはローカル実行の場合、6010番固定で動く形になっています。

```js:
// VCAP_APP_PORTが設定されている場合はこのポートでlistenする (IBM Cloudのお作法)
var port = process.env.VCAP_APP_PORT || 6010;
app.listen(port, function() {
  // eslint-disable-next-line
  console.log('Server running on port: %d', port);
});
```

ListenするIPアドレスについても127.0.0.1などを明示的に指定しないようにして下さい。デフォルトは0.0.0.0(すべての宛先アドレスが有効)で、この形でIBM Cloud環境でも動かすようにします。

## 前提パッケージ・起動コマンド

前提パッケージ、起動コマンドは設定ファイルで指定する形になります。
Node.js と Pythonの場合、具体的には次のような手順です。

### Node.jsの場合
Node.jsの場合、構成ファイルは package.jsonファイルとなります。
一番簡単にpackage.jsonファイルを作るには、カレントディレクトリにserver.jsファイルを用意した後、"npm init -f"コマンドを実行して生成します。

その後、追加モジュール導入時は

```
npm install <package_name> --save
```

という形で必ず --save オプションを付けるようにして下さい。これにより、pacckage.jsonファイル中のdependenciesに必要モジュールが記録され、IBM Cloudでのデプロイ時にもモジュール不足が起きないようになります。

### Pythonの場合
Pythonの場合、動かすのに最低限必要な構成ファイルは次の2つです。

#### requirements.txt
Pythonでは、追加モジュールの導入は pip コマンドで行いますが、それで導入したモジュール名をこのファイルに列挙します。下記にサンプルを添付します。

```
requests
flask
```

#### Procfile
起動コマンドをこのファイルで指定します。サンプルは以下の通りです。

```
web: python server.py
```

これだけの規則を守れば、自由にローカル・IBM Cloud環境どちらでも動くアプリケーションが作れると思います。
面白いIBM Cloudアプリケーションを作って下さい!