まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タグ:bluemix

前2回の続きです:
IBM Bluemix にカスマムサービスを追加する(1/3)
IBM Bluemix にカスマムサービスを追加する(2/3)


前回作成した REST API を実際に IBM Bluemix のカスタムサービスとして登録し、利用するまで(正確には使用を終えてカスタムサービスから削除するところまで)の手順を紹介します。なおこの手順はコマンドラインツールである cf が必要になるので、インストールしていない場合は各自の環境にあった cf をあらかじめダウンロード&インストールしておいてください:
https://github.com/cloudfoundry/cli/releases


また、今回カスタムサービスとして登録する REST API は前回紹介した app.js が https://dotnsf-operation.mybluemix.net/ にデプロイされて動いているものとします。以下の説明でのこのホスト名部分を実際にみなさんがデプロイしたホスト名に合わせて読み替えてください:
2017021401

2017021401


まず最初に、以下で登録するカスタムサービスをバインドして動作確認するためのランタイムを用意しておきます。動作確認用のアプリケーション(後述)を PHP で用意したので、IBM Bluemix 上に PHP のランタイムを1つ作成します(以下の例では teyande-php-env という名前で作成しています)、なおこの段階ではサービスは何もバインドしないでください:
2017021402


このランタイムに以下の内容の PHP アプリケーションをプッシュします:
https://github.com/dotnsf/phpEnv


上記アプリケーション(index.php)はシンプルに「ランタイムサーバー上の全環境変数とその値を表示する」というアプリケーションです。プッシュ後にサーバーにアクセスすると以下のような画面になります。この時点ではサービスを1つもバインドしていないので、環境変数 VCAP_SERVICES の値は空オブジェクトを示す "{}" となっていることが確認できるはずです:
2017021403


ここまでで準備は完了です。では前回紹介したサービスを IBM Bluemix のカスタムサービスとして登録した上でインスタンス化し、このランタイムにバインドした上で、この環境変数がどのように変わるかを確認してみます。

コマンドプロンプトかターミナルを起動し、上記の PHP ランタイムと同じデータセンター(この場合は US-SOUTH)の自分の組織に cf コマンドでログインします:
2017021404


カスタムサービスを利用するにためには Cloud Foundry の「サービスブローカー」(カスタムカタログのように理解してください)に登録しておく必要があります。まず現在のサービスブローカー一覧を確認するため "cf service-brokers" コマンドを実行して、この時点では何も登録されていないことを確認します:
2017021301
 ↑"No service brokers found"(何も登録されていません)です


ではサービスブローカーを新規に1つ作成します。以下のパラメータを付けてコマンドを実行します:
> cf create-service-broker サービスブローカー名 認証ユーザー名 認証パスワード サービスの基本URL --space-scoped
2017021302


今回、サービスブローカー名は "dotnsf-operation" とします。認証のユーザー名とパスワードは dotnsf-operation の Catalog API などが実行される時用に指定した auth_user / auth_pass の値を指定します(詳しくは前回の記事参照)。サービスの基本 URL は今回 "https://dotnsf-operation.mybluemix.net" です。そして最後にオプションの "--space-scoped"(同一組織内のユーザーが使えるサービスとするための指定)を加えています。

コマンドが成功したことを確認し、再度 "cf service-brokers" を実行してみます。先程は結果に何も含まれていませんでしたが、今度は直前に作成したばかりの "dotnsf-operation" サービスブローカーが、指定した URL で登録されていることを確認してください:
2017021303


これで dotnsf-operation API がカスタムカタログに登録された状態になりました。ただ IBM Bluemix のウェブ UI ではカスタムカタログを表示する画面が存在していないので、この時点ではまだウェブ UI には現れませんが、cf からは同様にインスタンスを作成することができるようになっています。

というわけで、続けて cf でこのサービスをインスタンス化してみます:
> cf create-service サービスブローカー名 プラン名 サービス名

サービスブローカー名は dotnsf-operation の Catalog API(GET /v2/catalog) 内で定義したもの(今回の場合は dotnsf-operation-service)を指定します。プランも同様に Catalog API 内で定義したもの(今回は "free" と "enterprise" の2つ)の中から指定できますが、今回は "free" を指定しています。最後に IBM Bluemix 内でのサービス名として "dotnsf-operation-1" と指定しています:
2017021304


ここまでの手続きが完了すると、dotnsf-operation サービスがインスタンス化され、ウェブ UI の利用中サービス一覧の中にも表示されるようになります:
2017021301


この時点でサービスを選択し、管理タブを開くと Dashboard API で定義した内容が表示されるはずです(https で Dashboard API にアクセスできることが条件です):
2017021000


ではこのサービスを最初に作成した PHP ランタイムにバインドしてみましょう。PHP ランタイムの画面に移動し、接続タブの「既存に接続」をクリックします:
2017021302


現在利用中のサービスインスタンスの一覧が表示されます。その中に "dotnsf-operation-1" という名前のサービスが含まれているはずなので、これを選択して「接続」をクリックします。そしてこの内容をランタイムの環境変数にも反映させるため「再ステージング」します:
2017021303


改めて同ランタイムの接続タブを参照すると、"dotnsf-operation-1" サービスが追加されていることを確認できます。ここで「資格情報の表示」をクリックします:
2017021304


今回の dotnsf-service の Bind API では credentials 情報として uri だけを追加していました。そのためこのサービスインスタンスの資格情報にも uri 1つだけが credentials 情報として表示されていることが確認できるはずです:
2017021305


改めてランタイムのトップ画面にアクセス(リロード)します。今回は先程と異なり dotnsf-operation-1 サービスがバインドされているので、環境変数 VCAP_SERVICES は空オブジェクトではなく、dotnsf-operation-1 の情報が表示されているはずです。credentials 情報も先程確認したものと同じものが表示されています:
2017021306



・・・というわけで、IBM Bluemix のカスタムサービスを作成して、登録して、インスタンス化して、ランタイムとバインドして環境変数に反映させる、という一連の作業ができることを確認できました!

最後に使い終わったサービスブローカーを削除する手順を紹介しておきます。まず(同サービスブローカーを使っているランタイムからのバインドを全て解除した上で)サービス一覧画面からインスタンス化されたサービスを削除します:
2017021305


サービスが削除されるとウェブ UI からは見えなくなります。が、まだサービスブローカーには登録されているので、以下の cf コマンドでサービスブローカーからも削除します:
> cf delete-service-broker サービスブローカー名
2017021306


このコマンドが成功すると、サービスブローカー一覧からも削除されます。念のため "cf service-brokers" コマンドで確認します:
2017021307




久しぶりの超大作ブログとなり、3回に分けて紹介しましたが、IBM Bluemix のカスタムサービスを作るために実装しないといけない内容や、実装後の登録方法などを紹介できたつもりです。自分が作った REST API を IBM Bluemix に統合して使いたい場合や、公式なサードパーティ API としての登録を検討する場合に必要となる実装がどんなものかを説明しました。

今回紹介した内容はあくまで「同一組織内でのみ有効なカスタムサービス」の登録方法および手順の紹介でしたが、全 IBM Bluemix ユーザー向けに実装する場合であっても同様の API 実装は必要になります。そういったエコシステムを活性化する上での役立つ情報になれば幸いです。


(参考)
 https://www.ibm.com/blogs/bluemix/2017/01/extend-bluemix-service-broker/

前回の続きです。IBM Bluemix に独自のカスタムサービスを追加する手順を紹介しています:
IBM Bluemix にカスマムサービスを追加する(1/3)

前回紹介した Node.js で実装済みの REST API を IBM Bluemix のカスタムサービスとして利用できるように改良する、その改良内容を紹介します。


前回のおさらいになりますが、改良前の API はこちらです:
https://github.com/dotnsf/dotnsfOperation/blob/master/app0.js
//. app0.js
var express = require( 'express' ),
    cfenv = require( 'cfenv' ),
    appEnv = cfenv.getAppEnv(),
    app = express();


//. Service APIs
app.get( '/', function( req, res ){
  var html = '<title>My Broker</title>';
  html += '<h1>My Broker</h1>';
  html += '<p>ドキュメントは<a target="_blank" href="./doc">こちら</a>を参照ください</p>';
  res.writeHead( 200, [ { 'Content-Type': 'text/html; charset=UTF8' } ] );
  res.write( html );
  res.end();
});

app.get( '/:operation/:x/:y', function( req, res ){
  var operation = req.params.operation;
  var x = parseFloat( req.params.x );
  var y = parseFloat( req.params.y );
  var z = 0;

  if( operation == 'plus' ){
    z = x + y;
  }else if( operation == 'minus' ){
    z = x - y;
  }else if( operation == 'multiply' ){
    z = x * y;
  }else if( operation == 'divide' ){
    z = x / y;
  }

  res.writeHead( 200, [ { 'Content-Type': 'text/plain' } ] );
  res.write( '' + z );
  res.end();
});

app.get( '/doc', function( req, res ){
  var html = '<title>My Operation Document</title>';
  html += '<h1>My Operation Document</h1>';
  html += '<p>This is a document for my operation</p>';
  html += '<hr/>';
  html += '/plus/x/y => return ( x + y )<br/>';
  html += '/minus/x/y => return ( x - y )<br/>';
  html += '/multiply/x/y => return ( x + y )<br/>';
  html += '/divide/x/y => return ( x / y )<br/>';
  res.writeHead( 200, [ { 'Content-Type': 'text/html; charset=UTF8' } ] );
  res.write( html );
  res.end();
});

var service_port = appEnv.port ? appEnv.port : 1337;
app.listen( service_port );
console.log( "server starting on " + service_port );


最終型を先に紹介しますが、これを以下のように改良します(追加箇所を赤くしています):
https://github.com/dotnsf/dotnsfOperation/blob/master/app.js
//. app.js
var express = require( 'express' ),
    basicAuth = require( 'basic-auth-connect' ),
    bodyParser = require( 'body-parser' ),
    cfenv = require( 'cfenv' ),
    uuid = require( 'uuid' ),
    appEnv = cfenv.getAppEnv(),
    app = express();

app.use( bodyParser.urlencoded( {
    extended: true
}));
app.use( bodyParser.json() );

//. API Version
const X_BROKER_API_VERSION = 2.0;
const X_BROKER_API_VERSION_NAME = 'X-Broker-Api-Version';

//. Plans
const plan0 = {
  'id': uuid.v4(),
  'name': 'free',
  'description': 'Free plan',
  'free': true
};
const plan1 = {
  'id': uuid.v4(),
  'name': 'enterprise',
  'description': 'Enterprise plan',
  'free': false
};

//. service endpoint
var service_host = 'dotnsf-operation.mybluemix.net';
var service_port = appEnv.port ? appEnv.port : 1337;
var service_base = service_host; // + ':' + service_port;
var service_instance = 'http://' + service_base + '/operation/';
var service_dashboard = 'http://' + service_base + '/operation/dashboard';

//. Service
var my_service_id = uuid.v4();
var my_service = {
  'id': my_service_id,
  'name': 'dotnsf-operation-service',
  'description': '四則演算 API',
  'bindable': true,
  'tags': [ 'private' ],
  'plans': [ plan0, plan1 ],
  'dashboard_client': {
    'id': uuid.v4(),
    'secret': 'secret-1',
    'redirect_uri': 'http://bluemix.net'
  },
  'metadata': {
    'displayName': 'My operational service',
    'longDescription': 'WebAPI で四則演算を行う',
    'providerDisplayName': 'dotnsf',
    'documenttationUrl': 'http://' + service_base + '/doc',
    'supportUrl': 'https://stackoverflow.com/questions/tagged/ibm-bluemix'
  }
};


//. Auth for /v2/*
const auth_user = 'username';
const auth_pass = 'password';
app.all( '/v2/*', basicAuth( function( username, password ){
  return username === auth_user && password === auth_pass;
}));


//. Cloud Foundry Broker APIs

//. Catalog
app.get( '/v2/catalog', function( req, res ){
  console.log( 'GET /v2/catalog' );

  var api_version = req.get( X_BROKER_API_VERSION_NAME );
  if( !api_version || X_BROKER_API_VERSION > parseFloat( api_version ) ){
    res.writeHead( 412, [ { 'Content-Type': 'text/plain' } ] );
    res.write( 'Precondition failed. Missing or imcompatible ' + X_BROKER_API_VERSION_NAME + '. Expecting version ' + X_BROKER_API_VERSION + ' or later.' );
    res.end();
  }else{
    var services = {};
    services['services'] =[];
    services['services'][0] = my_service;
    res.writeHead( 200, [ { 'Content-Type': 'application/json' } ] );
    res.write( JSON.stringify( services ) );
    res.end();
  }
});

//. Provision
app.put( '/v2/service_instances/:instance_id', function( req, res ){
  var instance_id = req.params.instance_id;

  var contentType = req.get( 'Content-Type' );
  if( contentType != 'application/json' ){
    res.writeHead( 415, [ { 'Content-Type': 'text/plain' } ] );
    res.write( 'Unsupported Content-Type: expecting application/json.' );
    res.end();
  }

  var provision_details = req.body;
  //. {
  //    "service_id": "<service-guid>",
  //    "plan_id": "<plan-guid>",
  //    "organization_guid": "<org-guid>",
  //    "space_guid": "<space-guid>"
  //  }
  console.log( 'PUT /v2/service_instances/' + instance_id );
  console.log( provision_details );

  //. Provisioning process here.

  //. Return basic service information
  var new_service = {};
  //new_service['dashboard_url'] = service_dashboard + '/' + instance_id;
  new_service['dashboard_url'] = service_dashboard + '/' + instance_id;
  res.writeHead( 200, [ { 'Content-Type': 'application/json' } ] );
  res.write( JSON.stringify( new_service ) );
  res.end();
});


//. Deprovision
app.delete( '/v2/service_instances/:service_id', function( req, res ){
  var service_id = req.params.service_id;

  console.log( 'DELETE /v2/service_instances/' + service_id );

  //. Deprovisioning process here.

  //. Return basic information
  res.writeHead( 200, [ { 'Content-Type': 'application/json' } ] );
  res.write( JSON.stringify( {} ) );
  res.end();
});

//. Bind
app.put( '/v2/service_instances/:service_id/service_bindings/:binding_id', function( req, res ){
  var service_id = req.params.service_id;
  var binding_id = req.params.binding_id;

  var contentType = req.get( 'Content-Type' );
  if( contentType != 'application/json' ){
    res.writeHead( 415, [ { 'Content-Type': 'text/plain' } ] );
    res.write( 'Unsupported Content-Type: expecting application/json.' );
    res.end();
  }

  var binding_details = req.body;
  //. {
  //    "plan_id": "<plan-guid>",
  //    "service_id": "<service-guid>",
  //    "app_guid": "<app-guid>"
  //  }
  console.log( 'PUT /v2/service_instances/' + service_id + '/service_bindings/' + binding_id );
  console.log( binding_details );

  //. Binding process here.

  //. Return result to the Bluemix Cloud Controller
  var result = {};
  result['credentials'] = {};
  result['credentials']['uri'] = 'http://' +  service_base + '/';
  res.writeHead( 200, [ { 'Content-Type': 'application/json' } ] );
  res.write( JSON.stringify( result ) );
  res.end();
});

//. Unbind
app.delete( '/v2/service_instances/:service_id/service_bindings/:binding_id', function( req, res ){
  var service_id = req.params.service_id;
  var binding_id = req.params.binding_id;

  console.log( 'DELETE /v2/service_instances/' + service_id + '/service_bindings/' + binding_id );

  //. Unbinding process here.

  //. Return basic information
  res.writeHead( 200, [ { 'Content-Type': 'application/json' } ] );
  res.write( JSON.stringify( {} ) );
  res.end();
});



//. Service related functions
app.all( '/operation/:instance_id', function( req, res ){
  var instance_id = req.params.instance_id;

  console.log( req.method + ' /operation/' + instance_id );

  //. Return service information
  var service_info = {};
  service_info['greeting'] = instance_id;
  res.writeHead( 200, [ { 'Content-Type': 'application/json' } ] );
  res.write( JSON.stringify( service_info ) );
  res.end();
});

app.get( '/operation/dashboard/:instance_id', function( req, res ){
  var instance_id = req.params.instance_id;

  console.log( req.method + ' /operation/' + instance_id );

  //. Return hard-coded HTML
  var html = '見つかっちゃった。。。(*/ω\*)';
  res.writeHead( 200, [ { 'Content-Type': 'text/html; charset=UTF8' } ] );
  res.write( html );
  res.end();
});

app.all( '/operation/:instance_id/:binding_id', function( req, res ){
  var instance_id = req.params.instance_id;
  var binding_id = req.params.binding_id;

  var contentType = req.get( 'Content-Type' );
  if( contentType != 'application/json' ){
    res.writeHead( 415, [ { 'Content-Type': 'text/plain' } ] );
    res.write( 'Unsupported Content-Type: expecting application/json.' );
    res.end();
  }

  //. Return service information
  var service_info = {};
  service_info['instance_id'] = instance_id;
  service_info['binding_id'] = binding_id;
  res.writeHead( 200, [ { 'Content-Type': 'application/json' } ] );
  res.write( JSON.stringify( service_info ) );
  res.end();
});



//. Service APIs
app.get( '/', function( req, res ){
  var html = '<title>My Broker</title>';
  html += '<h1>My Broker</h1>';
  html += '<p>ドキュメントは<a target="_blank" href="./doc">こちら</a>を参照ください</p>';
  res.writeHead( 200, [ { 'Content-Type': 'text/html; charset=UTF8' } ] );
  res.write( html );
  res.end();
});

app.get( '/:operation/:x/:y', function( req, res ){
  var operation = req.params.operation;
  var x = parseFloat( req.params.x );
  var y = parseFloat( req.params.y );
  var z = 0;

  if( operation == 'plus' ){
    z = x + y;
  }else if( operation == 'minus' ){
    z = x - y;
  }else if( operation == 'multiply' ){
    z = x * y;
  }else if( operation == 'divide' ){
    z = x / y;
  }

  res.writeHead( 200, [ { 'Content-Type': 'text/plain' } ] );
  res.write( '' + z );
  res.end();
});

app.get( '/doc', function( req, res ){
  var html = '<title>My Operation Document</title>';
  html += '<h1>My Operation Document</h1>';
  html += '<p>This is a document for my operation</p>';
  html += '<hr/>';
  html += '/plus/x/y => return ( x + y )<br/>';
  html += '/minus/x/y => return ( x - y )<br/>';
  html += '/multiply/x/y => return ( x + y )<br/>';
  html += '/divide/x/y => return ( x / y )<br/>';
  res.writeHead( 200, [ { 'Content-Type': 'text/html; charset=UTF8' } ] );
  res.write( html );
  res.end();
});

app.listen( service_port );
console.log( "server starting on " + service_port );

追加した箇所をで記載しています。通常の Web API を IBM Bluemix(Cloud Foundry) サービスとして対応させる場合には少なくともこれだけの追加が必要になる、と理解してください。

具体的な追加の内容は8種類12個の Web API を追加しています。以下にそれぞれの内容を紹介します。

(1) Catalog API (GET /v2/catalog)

(コマンドライン版の)サービス一覧に表示される情報を取得する API です。リクエスト時に HTTP ヘッダ 'X-Broker-Api-Version' が設定されており、その値が実装 API バージョン(この場合は 2)以上でない場合はエラーとします。実行後の返り値についてはソースコード内の変数: services 参照していただきたいのですが、返り値のオブジェクトの中にサービス名称や説明、選択可能なプランや資料情報 URL などを含めます。


(2) Provision API (PUT /v2/service_instances/:instance_id)

サービスをプロビジョンして作成する際にコールされる API です。HTTP ヘッダ 'Content-Type' の値が 'application/json' 以外の場合はエラーとします。実際にはパラメータ :instance_id の値を使ってインスタンスを生成して管理するところまでを実装する必要があるのですが、今回のサンプル内ではそこまでは実装していません。実行後の返り値についてはソースコード内の変数: new_service を参照してください。


(3) Deprovision API (DELETE /v2/service_instances/:instance_id)

サービスでデプロビジョンして削除する際にコールされる API です。実際にはパラメータ :instance_id の値を使って管理中のインスタンス情報を削除するところまでを実装する必要があるのですが、今回のサンプル内ではそこまでは実装していません。実行後の返り値は空オブジェクトです。


(4) Bind API (PUT /v2/service_instances/:instance_id/service_bindings/:binding_id)

サービスのバインドを作成する際にコールされる API。HTTP ヘッダ 'Content-Type' の値が 'application/json' 以外の場合はエラーとします。実際にはパラメータ :instance_id , :binding_id の値を使ってバインドを生成して管理するところまでを実装する必要があるのですが、今回のサンプル内ではそこまでは実装していません。実行後の返り値の中にランタイムの環境変数 VCAP_SERVICES に含まれる credentials 情報を含めます(つまり実行時用の id や password が必要な場合は、ここで作成して返り値に含める、という実装を行ってください)。具体的にはソースコード内の変数: result を参照してください。


(5) Unbind API (DELETE /v2/service_instances/:instance_id/service_bindings/:binding_id)

サービスをアンバインドして削除する際にコールされる API です。実際にはパラメータ :instance_id , :binding_id の値を使って管理中のバインド情報を削除する(例えば (4) で生成した username と password を無効にする、など)ところまでを実装する必要があるのですが、今回のサンプル内ではそこまでは実装していません。実行後の返り値は空オブジェクトです。


↑上記 (1) ~ (5) の API 実行時には Basic 認証をかけておきます。


(6) Instance Info APIs (PUT,GET,DELETE /operation/:instance_id)

サービスインスタンス情報を作成(PUT)/取得(GET)/削除(DELETE)するための API です。具体的な返り値はコースコード内の変数: service_info を参照してください。


(7) Bind Info APIs (PUT,GET,DELETE /operatoin/:instance_id/:binding_id)

サービスインスタンスとのバインド情報を作成(PUT)/取得(GET)/削除(DELETE)するための API です。HTTP ヘッダ 'Content-Type' の値が 'application/json' 以外の場合はエラーとします。具体的な返り値はコースコード内の変数: service_info を参照してください。


(8) Dashboard (GET /operation/dashboard/:instance_id)

サービスの管理情報やダッシュボードとして表示する画面です。IBM Bluemix のウェブ画面に表示される内容です:
2017021401

  ↑この「管理」タブの内容です

なお、この中身が正しく表示されるためには https でこの API が実行できる必要があります。具体的な返り値の例はコースコード内の変数: html を参照してください。



元のコード(app0.js)に、上記8つの API を追加実装することで IBM Bluemix のサービスとしての最低限の振る舞いが可能になります。また (1) ~ (5) の API については認証を付けることができるので、そのための実装も追加したものが新しいコード(app.js)、ということになります。この追加実装のためにいくつかのミドルウェアも追加で必要になったため、新しいコードではその部分も含めて追加しています。


そして、パッケージ情報を記述したファイル(packages.json)や、IBM Bluemix 上で実行するための manifest.ymlREADME.md までを追加した一通りのセットがこちらです:
https://github.com/dotnsf/dotnsfOperation

2017021402


IBM Bluemix 環境で動かす場合の使い方は README.md にも記載していますが、ダウンロードするか git clone した後に manifest.yml 内のアプリ名、ホスト名、ドメイン名をランタイム環境に合わせて編集します。加えて app.js 内に "dotnsf-operation.mybluemix.net" というホスト上で、"dotnsf-operatoin-service" というサービス名で動かすことを前提とした内容が記述されているので、該当箇所を実際の内容に合わせて書き換えます。また Basic 認証は username : password となっているので、ここを変更する場合はコード内の auth_user および auth_pass 変数を書き換えてください。

こうしてコードを追加した app.js を IBM Bluemix にプッシュすることでサービスが稼働することになります。なお、サービスそのものは IBM Bluemix 内で稼働させる必要はありませんが、上記 (8) の API は https 対応している必要があるので、IBM Bluemix 以外の環境で動かす場合は https 対応できる環境下で稼働させてください。


実際に dotnsf-operation.mybluemix.net 上にプッシュして動かしている様子が以下になります:
2017021402
 ↑元々の機能+αが実装された API を動かしています。

これで IBM Bluemix のサービスとして統合可能な Web API の用意ができました。次回は実際にこの API を IBM Bluemix のカスタムサービスとして登録し、ランタイムにバインドして使ってみる様子とその手順を紹介します。


(追記)
続きはこちら:
http://dotnsf.blog.jp/archives/1064290977.html

IBM Bluemix には Watson や IoT など、数多くの IBM /サードパーティ API がサービスとして登録されており、利用者はこれらを活用してアプリケーションの高速な開発ができるようになります:
2017021301


またランタイム(アプリケーションサーバー)とサービスをバインド(紐付け)することで、ランタイムの環境変数からサービスへの接続情報(ユーザー名、パスワードなど)を動的に参照することができるようになります。これによって接続情報を外出しする必要がなくなり、非常にセキュアな外部 API 連携が可能になる仕組みが提供されています:
2017021302


さて、このような IBM Bluemix 環境において、「標準では登録されていない API をサービスとして利用したい」という要望を(少なくとも自分が知る限りでも何件か)いただいています。もちろんサードパーティ製 API として正規に登録する手順というものもあります。ただこちらは Cloud Foundry Service として最低限必要ないくつかの API(後述)を実装した上で申請し、条件や審査といったプロセスを経て IBM Bluemix サードパーティサービスとして登録されるものになります。この正規の手順について、詳しくはこちらを参照ください:
https://developer.ibm.com/marketplace/docs/vendor-guide/how-do-i-integrate/integrating-bluemix/


一方、いくつかの制約事項がある中で、この申請や審査といった手順を通さずに自身のサービスを IBM Bluemix のカスタムサービスとして利用可能にする方法もあり、以下のブログで紹介されています:
https://www.ibm.com/blogs/bluemix/2017/01/extend-bluemix-service-broker/


上記エントリによると、その制約事項の中で大きなものは以下の4つです:
(1) IBM Bluemix の Web UI には 100% 統合されない
(2) 同一組織の中だけで利用可能なサービスとして登録される
(3) Web API 自体は https で運用されている必要がある(オプション)
(4) 課金の仕組みが必要であれば別途実装する


(1) について補足すると、IBM Bluemix はオープンソース製品である Cloud Foundry をベースに IBM が機能拡張したものなのですが、(あまり知られていませんが)Web ブラウザから操作できるようにしているのも IBM の拡張によるものです(CloudFoundry 自体は cf というコマンドラインツールを使って操作するものです)。この Web の UI は Bluemix のサービスラインナップとも密に統合されており、以下で紹介するカスタムサービスで追加したサービスは Bluemix Web UI には反映される部分とされない部分が出てきてしまいます(例えば追加したカスタムサービスはカタログ画面には表示されません)。基本的には cf ツールを使ってサービスを追加してバインドして・・という手順をとって実行する前提の方法になります。

(2) はカスタムサービスの利用可能な範囲についてです。通常の Bluemix サービスは Bluemix ユーザー全員に公開されて利用可能になりますが、以下の方法で追加したカスタムサービスの場合は同一の組織内(または同一のスペース内)だけで利用可能なサービスになります。異なる組織のユーザーからは利用できない方法であることをご了承いただきます。

(3) はその API 自体が http に加えて https でも稼働する必要がある、という条件です。この (3) に関しては(動くか動かないかだけの条件であれば、http だけでも動くので)必須ではないのですが、Bluemix の Web UI から参照するページの一部が https 必須になっており、https 非対応だとそのページが正しく表示されない、という問題が生じるのでした。繰り返しますがそのページがブラウザから正しく表示されるかどうかと、API として実行できるかどうかは別の問題なので、その意味では必須ではない(が、対応されている方が好ましい)とお考えください。要するにピュアな Cloud Foundry サービスとして認識させるための要件ではなく、IBM Bluemix 統合のための要件であるという意味です。

(4) は今回作成するカスタムサービスは(複数のプランを実装することはできますが)Bluemix 上では無料サービスとして動く、ということを意味しています。課金が必要な場合は正規の手順でサードパーティサービスとして登録いただくか、あるいは別途課金の仕組みをサービス側に実装する必要がある、ということです。


上記のような制約事項がある中で、自分で作った Web サービスの API を IBM Bluemix のカスタムサービスとして統合する手順を紹介します。全体的に長くなりそうなので紹介は3回に分け、第一回目である今回は上記の解説に加えて「自分で作った(IBM Bluemix に統合するためのカスタマイズをする前の) Web サービス API 」の紹介をすることにします。もちろん最終的には皆さん自身で作った API をカスタマイズしていただくことになると思いますが、そのカスタマイズ前の題材の紹介であると理解してください。


Web API は非常にシンプルな、「四則演算を行う API 」とします。今回用意したサンプルは Node.js で実装されています:
https://github.com/dotnsf/dotnsfOperation/blob/master/app0.js


今回対象とする上記コードは以下のような内容です。まあ普通の Node.js + Express による API 定義(/:operation/:x/:y)に加え、/ にアクセスがあった場合の表示と、/doc に簡易ドキュメントを用意しています:
//. app0.js
var express = require( 'express' ),
    cfenv = require( 'cfenv' ),
    appEnv = cfenv.getAppEnv(),
    app = express();


//. Service APIs
app.get( '/', function( req, res ){
  var html = '<title>My Broker</title>';
  html += '<h1>My Broker</h1>';
  html += '<p>ドキュメントは<a target="_blank" href="./doc">こちら</a>を参照ください</p>';
  res.writeHead( 200, [ { 'Content-Type': 'text/html; charset=UTF8' } ] );
  res.write( html );
  res.end();
});

app.get( '/:operation/:x/:y', function( req, res ){
  var operation = req.params.operation;
  var x = parseFloat( req.params.x );
  var y = parseFloat( req.params.y );
  var z = 0;

  if( operation == 'plus' ){
    z = x + y;
  }else if( operation == 'minus' ){
    z = x - y;
  }else if( operation == 'multiply' ){
    z = x * y;
  }else if( operation == 'divide' ){
    z = x / y;
  }

  res.writeHead( 200, [ { 'Content-Type': 'text/plain' } ] );
  res.write( '' + z );
  res.end();
});

app.get( '/doc', function( req, res ){
  var html = '<title>My Operation Document</title>';
  html += '<h1>My Operation Document</h1>';
  html += '<p>This is a document for my operation</p>';
  html += '<hr/>';
  html += '/plus/x/y => return ( x + y )<br/>';
  html += '/minus/x/y => return ( x - y )<br/>';
  html += '/multiply/x/y => return ( x + y )<br/>';
  html += '/divide/x/y => return ( x / y )<br/>';
  res.writeHead( 200, [ { 'Content-Type': 'text/html; charset=UTF8' } ] );
  res.write( html );
  res.end();
});

var service_port = appEnv.port ? appEnv.port : 1337;
app.listen( service_port );
console.log( "server starting on " + service_port );

肝となる API (/:operation/:x/:y)は4つの機能をもち、それぞれ以下のような URI で実装されます:
URI関数の処理内容
/plus/x/yx + y を実行した結果を返す/plus/2.3/3 → 5.3
/minus/x/yx - y を実行した結果を返す/minus/2.3/3 → -0.7
/multiply/x/yx * y を実行した結果を返す/multiply/2.5/-3 → -7.5
/divide/x/yx / y を実行した結果を返す/divide/5/4 → 1.25


実際に上記の URI に数値パラメータを入れてアクセスすると、実行結果を(text/plain で)画面に表示します:
2017021303


また、API とは別にドキュメントルートにアクセスがあった場合のページと、
2017021401


API の簡易ドキュメントページが用意されています:
2017021402


ここまでのアプリケーション(というか API)は上記の app0.js をそのまま Node.js で実行すれば動かすことができますので、興味がある方は実際に試してみてください。

そして次回は、この app0.js に対して IBM Bluemix のカスタムサービスとして統合するための変更を加える、その内容や手順を紹介します。IBM Bluemix(Cloud Foundry) のサービスとして利用するためには(サービスのインスタンス化や削除、バインド/アンバインドの仕組みを実現するためには)どのような実装が必要になるのか、といった辺りを紹介する予定です。


(追記)続きはこちら:
http://dotnsf.blog.jp/archives/1064347464.html


IBM Bluemix で Context Path Route(以下、「コンテキストルーティング」)を行う方法が紹介されていました:
Context path routes for your Bluemix Cloud Foundry apps
https://www.ibm.com/blogs/bluemix/2017/01/context-path-routes-hour-bluemix-cloud-foundry-apps/



コンテキストルーティング」とは URL のパス部分を使ったルーティングの仕組みです。以下の例を使って紹介します。 "myapp.yourbluemix.net" というホストが稼働しており、ここでは色々な機能が稼働しています。例えばメインのポータル機能は http://myapp.yourbluemix.net/ で、ドキュメント管理機能は http://myapp.yourbluemix.net/docs/ 以下で、サーバー管理機能は http://myapp.yourbluemix.net/admin/ 以下で実装されている、といった具合です。ユーザーからは同じホスト名(myapp.yourbluemix.net)にアクセスしており、同一のサーバーを使っているように見えています:
2017020901


コンテキストルーティングを使うと、実際にはより強固&柔軟な構成で実現できます。例えば上記で紹介した3つの機能を全て別々のサーバー上に実装した上で、ユーザーにそれぞれのホストに(異なるホスト名へ)アクセスさせるのではなく、同じホスト名でのアクセスを可能にします。そして URL のパス部分でどのサーバーを使うかのルーティングを定義します。以下の例では /docs 以下にアクセスする場合は mydocs.yourbluemix.net へ、/admin 以下にアクセスする場合は myadmin.yourbluemix.net へ、それ以外は myapp.yourbluemix.net へ、といった具合です:
2017020902


これがコンテキストルーティングです。これを IBM Bluemix 環境のアプリケーションでも行う方法が上記ブログにて紹介されていました。ブログで紹介されていたのは Node.js ランタイムと Python ランタイムを使った方法でした。自分は Node.js と PHP で試してみたところ少し挙動が変わっていたようでした。それがランタイムの種類の違いに起因しているのかどうかは分からないのですが、、自分で動作確認した方法を以下に紹介します。なお実際に以下の手順を実行するには Bluemix アカウントと cf コマンドラインツールが必要になるので、事前に自分の環境にあった cf をダウンロードしてインストールしておいてください。

まずシステム構成、およびルーティングルールを以下のようなものとします:
2017020903

- 2台のランタイムおよびアプリケーションを用意する:
  - dotnsf-cr.mybluemix.net (Node.js)
  - dotnsf-cr-php.mybluemix.net (PHP)
- ユーザーがアクセスする URL は http://dotnsf-cr.mybluemix.net/ 以下のみ
- dotnsf-cr.myblutemix.net/phpapp/ 以下へのアクセスを dotnsf-cr-php.mybluemix.net/phpapp 以下の PHP アプリケーションにルーティングする。それ以外は dotnsf-cr.mybluemix.net の Node.js アプリケーションが処理する
- PHP アプリケーションが稼働する dotnsf-cr-php.mybluemix.net への直接アクセスはエラーとして処理する


Bluemix らしく、2つの異なるランタイムで仮想的な1つのシステムを作ってみることにしました。で、こんなソースコードを用意しました:
https://github.com/dotnsf/BluemixContextRoutingSample


上記サイトよりソースコードをダウンロード&展開するか git clone して入手してください。展開後に manifest.yml をテキストエディタで開き、以下の赤字部分を自分用のものに変更してください:
applications:
# Node.js app
- name: dotnsf-cr
  memory: 256M
  routes:
  - route: dotnsf-cr.mybluemix.net
  path: ./node_app/
# PHP app
- name: dotnsf-cr-php
  memory: 256M
  routes:
  - route: dotnsf-cr.mybluemix.net/phpapp
  path: ./php_app/

Node.js ランタイムには dotnsf-cr 、PHP ランタイムには dotnsf-cr-php という名前を付けており、最終的にはどちらも dotnsf-cr.myblutemix.net という名前でアクセスできるようにコンテキストルーティングを設定する、という内容です(つまりこの1つの manifest.yml で2つのランタイムの定義をしています)。上記の赤字部分を皆様のアプリケーション名に合わせて変えて使ってください。また US-SOUTH 以外のデータセンターを使う場合はドメインを(例えば eu-gb.mybluemix.net などに)適宜変更してください。


こうして作成したソースコードを cf ツールでプッシュします。manifest.yml 内に(2つのランタイムそれぞれを作るための)必要な情報が全て記述されているので1回の "cf push" だけで実行されます:
2017020901

↑まずは Node.js アプリケーションである dotnsf-cr のプッシュから始まります


2017020902

↑Node.js アプリケーションのビルドが行われている様子です


2017020903

↑ Node.js アプリケーションのプッシュが完了した様子です。そして間髪をいれずそのまま PHP アプリケーションである dotnsf-cr-php のプッシュが始まります。


2017020904

↑途中でルーティング処理が行われている様子が確認できます。


2017020905

↑PHP アプリケーションのプッシュも終わりました。dotnsf-cr-php という名前のアプリケーションですが、その URL が dotnsf-cr.myblutemix.net/phpapp に設定されていることがわかります。


この状態でダッシュボードを確認するとこのようになっているはずです。Node.js と PHP 2つのランタイムが追加されて実行中になっています。これらのルーティング先はどちらも同じものになっていることを確認してください。
2017020909


この状態で http://dotnsf-cr.mybluemix.net/ にアクセスすると Node.js 内の public/index.html が表示されます:
2017020906


また https://dotnsf-cr.mybluemix.net/about にアクセスすると、Node.js の app.js 内で定義された内容に従ったメッセージが表示されます:
2017020907


一方、http://dotnsf-cr.mybluemix.net/phpapp/ にアクセスすると、このパスの場合は PHP ランタイムにルーティングされ、PHP ランタイム内の index.php が表示されるはずです:
2017020908


というわけで、Bluemix でもコンテキストルーティングが実現できることが確認できました。他のランタイム環境でも、3種類以上であってもルーティングの設定だけで同様に実現できると思います。

ビジュアルデータフローエディタの Node-RED は、IoT を始めとするデータの取り込みや加工、書き出しを視覚的に行う便利なツールです。更に IBM Bluemix 環境であれば、「ボイラープレート」と呼ばれるテンプレート機能を使うことで、サーバー管理とかミドルウェアインストールとかを意識することなく、簡単に Node-RED 環境を構築して、すぐに使い始めることができます。


が、簡単すぎるが故の課題もあります。典型的な例の1つが「バージョンアップ」です。まあクラウドの宿命といえなくもないのですが、ミドルウェアやアプリケーションのバージョン管理をどうするか、という課題です。クラウド環境の場合、バージョン管理含めてクラウド業者に手放したい人もいれば、バージョン管理は自分でやりたいという人もいるので、1つの正解というものが存在しない、難しい問題ではあります。Bluemix では新規にサーバーを作る際のミドルウェア/アプリケーションバージョンは原則最新のものが用意されますが、一度作ったサーバーのミドルウェア/アプリケーションバージョンが勝手に変更されることはありません。つまり使い続ける間は利用者が管理する必要があります。 

・・・と、ここまではいいのですが、問題は「最初の一歩が簡単すぎる&中で何がどう動いているか分からなくても使い始めることができる故、バージョンアップがやけに難しく感じる」ことです(苦笑)。

一応難しく(というか、ややこしく)なっている理由を解説すると、Bluemix 環境では Node.js サーバーがランタイムとして用意されます。そしてその上に Node-RED アプリケーションが導入されて動いているわけですが、このアプリケーション部分である Node-RED のバージョンアップをする必要があるわけです。この Node-RED のバージョンアップの際に、前提となる Node.js のバージョンも合わせて上げる必要が生じるケースもあります。また Node.js では npm というパッケージ管理の仕組みが使われていて、npm の作法でバージョンを管理する必要があります。 普通の Node.js 環境の場合、自分で npm を管理したり、npm に指示を出すような設定ファイルを用意したりするのですが、Bluemix はその辺りを全く知らなくても(事前に何も用意しなくても)インターネット上に Node-RED 環境が作れてしまうのです。で、バージョンアップの段階になってこれらの用意がないことが話をややこしくする要素になるのでした。


という背景の説明はここまでにして、以下は実際に(数ヶ月前のバージョンが古かった頃から動いているような)Bluemix 上の Node-RED をバージョンアップする方法を紹介します。

まずは Node-RED 環境にアクセスし、画面右上のハンバーガーメニューから Node-RED のバージョンを確認します。この図では "0.13.1" というバージョンになっていることが確認できます。2017/Feb/09 時点での Node-RED の最新バージョンは "0.16.2" なので、この環境はバージョンアップが可能な状態にある、ということになります:
2017020801


(Node.js や)Node-RED のバージョンアップのためには Node-RED のスターターコードと呼ばれるファイル一式か、IBM DevOps サービス等を使ったソースコード一式が必要になります。バージョンアップの対象となる Node-RED 環境を作った際にこれらの環境ごと用意されているのであれば、そのソースコードを用意してください。 以下はスターターコードも IBM DevOps サービスも使わず、ソースコードが手元にない状態からの入手方法になります。

改めて IBM Bluemix にログインし、対象の Node-RED ランタイムのプロジェクトを選択します。そして「開始」タブを開くと、Node-RED のカスタマイズの節内に "DOWNLOAD STARTER CODE" と書かれたボタンがあります(バージョンによって微妙に表現が異なるかもしれません)。ここをクリックしてスターターコードの zip ファイルをダウンロードします:
2017020802


ダウンロードした zip ファイルを展開します。この中に package.json というファイルがあるので、これをテキストファイルで開きます。もともとスターターコードをダウンロード済みであったり、IBM DevOps サービス等でソースコードを管理済みだった場合もお手元のコードの package.json を開いてください:
2017020803


package.json ファイルの中身を確認してみます:
2017020901


この中で利用する各コンポーネントモジュールとそのバージョンを管理しています。まず Node-RED 自体は
      :
  "node-red": "0.x"
      :

と指定されていました。これは「0.ナントカの中で最新のもの」を使うよう指定されていることになり、この指定であれば現時点での最新版 Node-RED である 0.16.2 が使われることになります。もしこのような指定になっていなかった場合はこのように変更してください。

次に Node.js のバージョンを確認します。Node-RED v0.16.0 からは Node.js のバージョンが 4.0 以上のみをサポートしており、Node.js のバージョンが古いと最新版の Node-RED は動きません。そこで Node.js のバージョンも合わせておく必要があります。こちらについては
      :
  "node": "4.x"
      :

と指定されている必要があります。もしこのような指定になっていなかった場合はこのように変更してください。


ここまでの変更・確認の上で(必要であれば他のモジュールや public フォルダ以下に静的ファイルを追加した上で)、cf コマンドでプッシュするか、IBM DevOps サービスを使って Deploy してください。

再デプロイ後に改めて Node-RED のバージョンを確認します:
2017020805


↑最新版になっていることが確認できれば成功です。

 

このページのトップヘ