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

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

タグ:ibm

Node-RED の HTTP ノード(HTTP in ノードと HTTP Response ノード)を使うと簡単に REST API を作ることができて便利です。自分もデータベースへの CRUD 操作を作る際などによく使っています。

が、この方法で作った REST API にはクロスオリジン制約(いわゆる CORS)が付きます。例えば https://xxxx.mybluemix.net/ というホストで Node-RED を動かしている場合、作成する REST API のエンドポイント URL は https://xxxx.mybluemix.net/getdata とかになるわけですが、この API を AJAX などのブラウザ上の JavaScript から呼ぼうとすると、同一サーバー上の( https://xxxx.mybluemix.net/**** というアドレスのページの) HTML からでないとエラーになってしまうのでした。サーバーサイドのプログラムから実行することはできるのですが、ブラウザ上の JavaScript から実行するには同一ホストからでないといけない、という制約が付くのでした(ま、この制約自体はある方が一般的ですけど)。

この CORS の制約を外して、外部の(https://xxxx.mybluemix.net/ 以外の)ページやローカルシステム上ページの JavaScript からでもこの API を呼べるようにする、そのための設定方法と手順を紹介します。

まず Node-RED で REST API を作成します。今回は以下のような HTTP in ノードと、Function ノードと、HTTP Response ノードをつなげただけのシンプルな REST API を用意しました:
2018101801


HTTP in ノードの設定は以下のように GET /corstest で呼び出せるような設定にしています:
2018101802


Function ノードは以下のような JavaScript を記述し、実行時のタイムスタンプ値を JSON で返す、という関数にしています:
msg.payload = { timestamp: ( new Date() ).getTime() };
return msg;

2018101803


HTTP Response ノードにはこの段階では特に手を加えません。配置しただけの状態のまま接続してデプロイします。これで REST API 側は準備できました。

次に HTML ファイルを用意します。今回はサーバー上ではなくローカルシステム上に以下のような内容の HTML ファイルを用意しました:
<html>
<head>
<meta charset="utf8"/>
<title>CORS テスト</title>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script>
function corstest(){
  $.ajax({
    type: 'GET',
    url: 'http://xxxx.mybluemix.net/corstest',  // 上記で作った REST API のエンドポイントURL
    success: function( result ){
      console.log( result );
    },
    error: function( err ){
      console.log( "error" );
      console.log( err );
    }
  });
}
</script>
</head>
<body>
<input type="button" value="CORS" onClick="corstest()"/>
</body>
</html>

この HTML ファイルをブラウザから(Ctrl+O などでファイルを指定して)開くと、"CORS" と書かれたボタンが1つだけ配置されたページが開きます:
2018101807


HTML を見るとわかるのですが、このボタンをクリックすると GET https://xxxx.mybluemix.net/corstest という API が実行され、成功するとその結果が、失敗すると "error" というメッセージに続いてエラーメッセージが、それぞれ表示される内容になっています。なおこのエンドポイント URL の xxxx 部分が実際に作成した Node-RED 環境のホスト名にあわせて変更してください。


ブラウザのコンソールを開いて(F12)、この CORS ボタンをクリックします。現状は CORS の対策を何もしていないので当然のようにエラーになります。エラーの内容はコンソールに表示され、原因はクロスオリジン制約のようです。これをどうにかしたい、というのが今回のテーマです:
2018101804


では、この REST API の実行が成功するよう API 側をカスタマイズします。Node-RED のフロー画面に戻って、HTTP Response ノードをダブルクリックして編集状態にします。そして「ヘッダ」と書かれた欄の「+追加」という部分をクリックし、HTTP Response ヘッダを追加します。そして左側(ヘッダ名)の欄には Access-Control-Allow-Origin と、そして右側(ヘッダ値)の欄には *(どのドメインからのリクエストでも許可するの意)とそれぞれ入力し、最後に「完了」→「デプロイ」します:
2018101805


この設定によって REST API の実行結果を返す際のヘッダに Access-Control-Allow-Origin: * という一行が追加されて返るようになり、このヘッダによってクロスオリジンが許可されているとブラウザ側からも判断され、期待通りの結果が得られるようになります。再度 CORS ボタンをクリックして REST API を実行するとコンソールにはリクエストが成功した時の結果が表示されるようになりました:
2018101806


CORS の制約を理解した上で外す(あるいは特定のドメイン名やホスト名を指定した上で許可する)、という点に注意してください。





IBM Cloudant のバイナリ・アタッチメント機能を使って、簡易的なファイルサーバー代わりに使う方法を紹介します。具体的には curl コマンドを使って手元の画像ファイル(でなくてもよい)をアップロードし、URL を指定して画像を表示できるようにする、というものです。

とりあえず以下の作業を行うには IBM Cloudant のデータベースと curl コマンドが必要です。前者は IBM Cloud のライトアカウントを使って無料で入手することも可能です。IBM Cloud にログイン後、IBM Cloudant のインスタンスを(無料であれば Lite プランで)作成しておいてください:
2018101601


また、以下のコマンド実行時に必要になるため、この Cloudant サービスのアクセス情報を確認しておきます。サービスを選択し、「サービス資格情報」と書かれたタブを選んで、「資格情報の表示」をクリックします(「資格情報の表示」がなかったら新規に作成してからクリックします):
2018101603


すると以下のような表示が画面下に出てきます:
2018101604


この中の username の値と、password の値を後でコマンド実行する際に指定することになります。どこかにメモしておくか、コピー&ペーストできるようにしておいてください。


また curl コマンドは使う方のシステムにあわせて適宜入手してください。Linux や MacOS であればたいてい標準で入っているはずですが、Windows の場合は別途インストールが必要です。私はこちらのをダウンロードしてインストールしました:
https://curl.haxx.se/download.html

実際に以下の作業を行う前に、IBM Cloudant 内に今回の作業の対象となるデータベースを用意します。今回は testdb という名称のデータベースを1つ用意しました。このデータベースを簡易ファイルサーバー代わりに使う方法を紹介します:
2018101602


まずはファイルを保存する手順です。今回は仮に cloudant.png という名前の以下のような画像ファイルを保存することにします:
cloudant1


コマンドプロンプト(Linux や MacOS の場合はターミナル)を開き、この cloudant.png が存在しているフォルダに移動して、以下のコマンドを入力します:
$ curl https://(username の値):(password の値)@(username の値).cloudant.com/testdb/(作成するドキュメントの _id)/(作成するアタッチメントの名称) -X PUT -H "Content-Type: image/png" --data-binary @cloudant.png

仮に username の値が "user1"、password の値が "pass1"、ドキュメントIDを "doc001"、アタッチメント名を "att001" とすると以下のようなコマンドになります:
$ curl https://user1:pass1@user1.cloudant.com/testdb/doc001/att001 -X PUT -H "Content-Type: image/png" --data-binary @cloudant.png

このコマンドが成功すると以下のような JSON が返されます:
{ "ok": true, "id": "doc001", "rev": "1-xxxxxxxx" }


コマンド実行が成功すると同じディレクトリにある cloudant.png ファイルを Cloudant の testdb にアップロードします。なお Content-Type の異なるファイルをアップロードする場合は -H オプションで指定する Content-Type を変えて指定してください。


Cloudant に保存したファイルを取り出す場合は以下の URL をブラウザで指定します:
https://(username の値):(password の値)@(username の値).cloudant.com/testdb/(作成するドキュメントの _id)/(作成するアタッチメントの名称)

同様に username の値が "user1"、password の値が "pass1"、ドキュメントIDを "doc001"、アタッチメント名を "att001" とすると以下のような URL になります:
https://user1:pass1@user1.cloudant.com/testdb/doc001/att001

ブラウザのアドレス欄に指定すると、こんな感じで表示できます:
2018101605



Cloudant のアタッチメント機能を使った簡易的なファイル保存のやりかたを紹介しました。Node-RED でウェブページを作る際の静的画像をどうするか、という問題を比較的簡単に解決する方法の1つだと思っています。


IBM Cloudant (Apache CouchDB) にあまり詳しくない人が他のデータベースと同じ感覚でデータを扱っている時に、特に既存データを更新している時にふと気づくことがあります。例えば以下のような現象を目の当たりにした時、何が起こっているのか正しく理解できるでしょうか?


IBM Cloudant のダッシュボード画面にアクセスし、今回は "testdb" という名称のデータベースを IBM Cloudant 上に新規に作成しました。以下の手順はすべてこのデータベースを対象に行います(CouchDB でも同様の結果になります)。作成したばかりなのでまだドキュメント数はゼロです:
2018100201


testdb データベースを選択した画面です。普通はここで testdb 内のドキュメント一覧が表示されますが、まだ1つも存在していないので "No Documents Found" と表示されています。ここでドキュメントを新規に作成するため "Create Document" ボタンをクリックします:
2018100202


新規に JSON ドキュメントを作成する画面に切り替わります。Cloudant(CouchDB) のドキュメントは "_id" というユニーク ID を含める必要があります(API 経由で _id を含めずに作ると自動的に割り振られます)。自動的に設定された "_id" 以外に "name" というキーを作り、適当な値(下図では "kkimura")を設定して "Create Document" ボタンをクリックします(JSON ドキュメントなので "_id" キーの最後にカンマをつけることを忘れずに):
2018100203


先程のドキュメントが作成され、ドキュメント一覧に1つのドキュメントが表示されるようになりました:
2018100204


ちなみに、この段階でデータベース一覧に戻ると testdb データベースのドキュメント数もゼロから 1 に変わっていることが確認できます:
2018100205


またドキュメント一覧からこのドキュメントを選択するとドキュメントの確認/編集画面になります。"_rev" という先ほど指定しなかったキーと値が追加されていますが、こちらは後で説明します:
2018100206


ここまでは特別におかしな所はないと思います。この文書を編集するあたりから Cloudant 特有のクセというか、「あれ?」と感じる所が出てくるようになってきます。

この画面から JSON ドキュメントを編集してみます。試しに "name" の値を(下図では "Kei Kimura" に)変更し、"Save Changes" ボタンをクリックします:
2018100207


変更内容が保存されて、ドキュメント一覧に戻ります。既存文書を編集して保存したので文書数は変わらずに1つのままです。ではこの文書を選択して開いてみます:
2018100208


"name" の値が "Kei Kimura" になった文書が開きました。が、よく見ると "_rev" の値が先程と異なっています。最初に作った直後は "1-" で始まる値だったのが、 "2-" で始まる値になっています。ここは変更しなかったはずなんですが・・・:
2018100209


また、このタイミングでデータベース一覧の画面に戻ると、testdb の文書数は1のままなんですが、データベースサイズが微妙に増えています。これほどの差がでるような変更をしたつもりはないのですが・・・:
2018100210


更にこの文書を開いて、再度 "name" 値を "kkimura" に変更して(元に戻して)みます。値を変更して "Save Changes" ボタンをクリックします:
2018100211


すると(中を開いて確認してもいいのですが)また "_rev" の値が変わっていることが一覧からもわかります。今度は "3-" で始まる値になっていました:
2018100212


この辺りから「???」と感じることが増えてきました。では最後にこの文書を削除してみます。一覧からチェックをつけてゴミ箱ボタンをクリックします:
2018100213


削除すると一覧からは文書は消えて、元通りの "No Documents Found" が表示されます:
2018100214


しかしデータベース一覧に戻って testdb を見ると、文書数は "0" ですが、横に!マークが付いています。また文書を削除した割にはデータベースサイズがあまり減っていないように見えます:
2018100215


この!マーク部分にマウスカーソルをあわせると、"This database has just 0 docs and 1 deleted docs" と表示されます。このメッセージの意味はいったい・・・:
2018100216


ドキュメントに勝手に "_rev"(と "_id")が付与されること、編集して保存すると "_rev" の値が勝手に変更されること、文書を削除してもデータベースサイズが減らないこと、文書を削除した時の謎のメッセージ、・・・ と、この辺りが Cloudant(CouchDB) を始めて使うと戸惑う点でしょうか? 前置きが長くなってしまいましたが、以下にこの謎を解くための説明を記載します。


上記の振る舞いを理解するには、まず自動付与される2つの値 "_id" と "_rev" の意味と役割を正しく理解する必要があります。

"_id" はいわゆる「文書 ID」です。この値はデータベース内でユニークな値をなっており、各文書を一意に取得することができるキー値となっています。正しい ID 値が与えられるだけで(他の絞り込み条件がなくても)データベース内から目的の文書を特定して取得することができます。ID 値については普通のデータベースでも扱うものなので、あまり難しくないと思っています。

一方、もうひとつの "_rev" 、こちらは IBM Cloudant(CouchDB) の特徴的な予約語となっており、「文書のリビジョン」を管理する値となっています。「リビジョン」は「バージョン」と読み替えていただいてもいいです。

上記の例だと、最初に "name" = "kkimura" という値で文書を作成しました。この時点ではこの文書のリビジョン(バージョン)は 1 で、"_rev" 値は "1-" で始まる値になっていました:
2018100204


次に同じ文書を "name" = "Kei Kimura" と変更して保存しました。この時点でこの文書のリビジョンは 2 となり、"_rev" 値も "2-" で始まる値に更新されました:
2018100208


更に同じ文書を "name" = "kkimura" に戻して保存しました。この時点でこの文書のリビジョンは 3 となり、"_rev" 値も "3-" で始まる値に更新されました:
2018100212


つまり "_rev" 値は "_id" 値で決まる文書のバージョンを管理する役割を持って自動的に更新されるシステム値ということになります。ただ Cloudant(CouchDB) でドキュメントが更新される際にはもう1つの特徴があります。

実は Cloudant(CouchDB) ではドキュメントが更新されることはほぼなく、「新しいドキュメントが新しい "_rev" 値を持って新規作成」されます。つまり厳密には同じ "_id" 値を持った複数のドキュメントがデータベース内には存在しているが、その中で最も大きな "_rev" 値を持ったドキュメントだけが有効になります。論理的にドキュメントを更新したつもりでいても、物理的には古いドキュメントは消えずに残っていて、新しいドキュメントが同じ "_id" 値&新しい "_rev" 値で作成されるのでした。なお最新でないリビジョンのドキュメントは _id 値を指定してドキュメントを取得する時に { revs_info: true } というオプションを指定することで取得することができます(このオプションをつけない限り、最新 _rev のものだけで取得できます):
http://docs.couchdb.org/en/stable/api/document/common.html


上記で Cloudant(CouchDB) のドキュメントが更新されることは「ほぼ」ないと書いたのですが、厳密にはあります。それが文書削除時です。Cloudant(CouchDB) の文書削除はいわゆる「ソフトデリート(論理削除)」であって、「ハードデリート(物理削除)」ではありません。文書に削除フラグ( { _deleted: true } )をつけて更新し、最新 "_rev" の文書が削除されているようにすることで、論理的に文書が削除されたことにしています。そしてこの論理削除を行う際には _id 値だけではなく、_rev 値と合わせて指定して、「この ID 値の、このリビジョンの文書を削除する」ことを明示的に指定する必要があります。論理的には _id 値だけで削除できそうな感覚を持ってしまいますが、その場合はまずその _id 値を持ったドキュメントの最新リビジョンを取得し、取得したドキュメントから _rev 値を取り出し、改めて _id 値と _rev 値を指定して論理削除する、という流れになります。


これらの部分を理解していると、文書を更新したり、削除した時にデータベースサイズが増える謎が理解できると思います。要は物理的に書き換えたり、物理的に削除しているわけではなく、新リビジョンのドキュメントを追加したり、削除フラグをつけたりしているだけなので、(別途物理削除するまでは)データベースサイズという観点では減ることがないのでした。








 

IBM Cloud から提供されている IBM CloudantApache CouchDB をベースとしたマネージドな NoSQL データベースのサービスです:
2018090300


ベース製品が同じなので、例えば REST API レベルでは互換性があります。注意が必要な点として自分が気づいた限りでは IBM Cloudant は標準で Apache Lucene ベースの検索機能が有効になっており、インデックスとなる Design Document を用意することでテキスト検索が可能になる、ということが挙げられますが、それ以外に大きな差はありません。 一方で IBM Cloud から提供されているライトアカウント(無料プラン)でも IBM Cloudant を利用することができるので、わざわざ Apache CouchDB を用意しなくても気軽に使うことができる DBaaS としてとても手軽で便利だと思っています:
2018090301


さて、自分は業務のプログラミングでは主に Node.js を使うのですが、Node.js のパッケージライブラリには IBM Cloudant 用のものと、Apache CouchDB 用のもの、両方が存在しています:
(IBM Cloudant)
2018090302

(Apache CouchDB)
2018090303


仮に対象となるデータベースが IBM Cloudant であれば前者の方が簡単に使えるという印象を持っています。ただし IBM Cloudant 用ライブラリは IBM Cloud 上の IBM Cloudant を想定していることもあり、例えばオンプレミス上の Apache CouchDB に対して使えるものではありません。

一方、Apache CouchDB 用ライブラリはローカルや社内サーバー、クラウド上にある Apache CouchDB データベース全般に対して利用することが可能です。この対象はクラウド上の IBM Cloudant であっても構いません。要するにこちらのライブラリを使えば Apache CouchDB だけでなく IBM Cloudant にも接続できる、ということです。


実際にこういった需要がどれだけあるのかわからないのですが、例えばあるシステムを作る際に、そのデータストアとして、
(1) 試しに動かす場合は IBM Cloud 上の IBM Cloudant を使って気軽に開発/テストを行い、
(2) 本番運用ではオンプレミスな Apache CouchDB を利用する(IBM Cloudant の独自機能は使わない想定)

といったことが接続先の切り替えだけでできると便利です。ただこれを実現するためには IBM Cloudant 用の便利なライブラリを使って開発しまうと (2) の本番の時に問題が起こってしまいます。以下では IBM Cloudant に対しても Apache CouchDB 用ライブラリ(以下 node-couchdb)を使ってアクセスするように実装してみたコードを紹介します。ベースが同じ製品なのでできることはできるんですが、そのための手順と注意点を含めて紹介します。


【準備】
まず Node.js のコードを記述する前に上述の node-couchdb を npm install しておきます:
$ npm install node-couchdb


【データベース接続】
node-couchdb を使って IBM Cloudant に接続します。こんな感じのコードを記述します:
var dblib = require( 'node-couchdb' );

var option = {
  auth: {
    user: 'username',
    pass: 'password'
  },
  'host': 'username.cloudant.com',
  'protocol': 'https',
  'port': 443
};
var db = dblib( option );

usernamepassword の部分にはそれぞれ IBM Cloudant の username と password を指定します(localhost の Apache CouchDB に接続する場合は option = {} で接続できます)。これで IBM Cloudant との接続ができました。ここで取得した db を使って、以下の主要な操作を行うことができます。


【主要な操作】

ドキュメント追加
insert() メソッドにデータベース名(以下の例では 'testdb')を指定して、ドキュメントを追加します。取得前に db.uniqid() でユニーク ID を取得し、_id に設定している点に注意してください:
var doc = { name: 'dotnsf', height: 170.0 };  //. 追加するドキュメント
db.uniqid().then( function( id ){
  doc._id = id[0];
  db.insert( 'testdb', doc ).then( function( body, headers, status ){
    console.log( body );
  }).catch( function( err ){
    console.log( err );
  });
});

ドキュメント読み取り
同様に get() メソッドにデータベース名と id を指定してドキュメントを取得します:
db.get( 'testdb', id ).then( function( doc, headers, status ){
  console.log( doc );
}).catch( function( err ){
  console.log( err );
});

ドキュメント削除
del() メソッドにデータベース名と id と rev を指定して、データベースからドキュメントを削除します。以下の例では一度 get() メソッドを実行して id 値から rev 値を取り出してから del() を実行しています :
db.get( 'testdb', id ).then( function( doc, headers, status ){
  db.del( 'testdb', doc.data._rev ).then( function( data, headers, status ){
    console.log( data );
  }).catch( function( err ){
    console.log( err );
  });
}).catch( function( err ){
  console.log( err );
});

ビューを指定してドキュメント一覧取得
あらかじめ作成したビューを指定して、そのビューに含まれるドキュメントの一覧を取得します。以下の例ではデザイン名 : library, ビュー名 : byname というビューを指定して文書一覧を取得しています :
db.get( 'testdb', '_design/library/_view/byname', {} ).then( function( data, headers, status ){
  if( data && data.data ){
    var docs = data.data.rows;
    console.log( docs );
  }
}).catch( function( err ){
  console.log( err );
});


IBM Cloudant の npm だと最初にデータベース名を指定してそのデータベースのオブジェクトを取得した上で各種操作を行う、、という流れなんですが、Apache CouchDB 版だと毎回データベース名と一緒に各種操作を行う、、という点が大きな違いだと思いました。ただその辺りさえ理解していればまあ大丈夫かな。。


 

Node-RED を使うことで IoT データの収集や Web API の実装などが非常に簡単に実現できます。このブログでも何度か紹介していますし、公開されている外部モジュールを使って更にカスタム機能を追加することも可能です。

今回紹介するのは HTTP in ノードに認証機能を追加する node-red-contrib-httpauth ノードです。これを使うと Node-RED に標準装備されている HTTP in ノード(HTTP リクエストノード)に Basic 認証や Digest 認証を簡単に追加することができるようになります:
2018082900


実際に使う場合は、Node-RED の画面右上のメニューから「パレットの管理(Manage Pallette)」を選びます:
2018082901


設定ダイアログが表示されたら、"Palette" の "Install" タブで "httpauth" と検索します。すると node-red-contrib-httpauth ノードが見つかるので、"install" ボタンをクリックしてノードを追加します:
2018082902


インストールが成功すると以下のような表示になります。ここから実際にノードが使えるようになります:
2018082903


この時点でパレットにも "http auth" というノードが追加されていることが確認できます:
2018082904


実際に使う場合、http in ノードの直後に http auth ノードを配置します。この例では http in ノードの直後に http auth ノードを配置し、その後ろに(いつも使っているような)template ノードや function ノードを配置して、最後に http response ノードで HTTP リクエスト可能な API を作りました:
2018082905


template ノードの中身はシンプルにしています(認証が成功するとこの文字列が表示される、というテストです):
2018082906


そして http auth ノードに認証内容を設定します。この例では Basic 認証でレルム文字列は MyRealm 、そしてユーザー名 : user1 &パスワード : pass1 を設定しました。この状態でデプロイします:
2018082907


デプロイ後にウェブブラウザでこの API にアクセスすると、先程設定した http auth が機能し、指定した内容の認証が行われます。具体的にはユーザー名とパスワードを問い合わせるダイアログが表示され、先程指定した内容が入力されないと先へ進めません:
2018082908


上記で設定した内容(ユーザー名 : user1、パスワード : pass1)が正しく入力されると HTTP リクエストが正しく実行され、設定していた文字列が表示されます:
2018082909


この http auth ノードを使うことで、Node-RED で作成する API や Web ページに簡単に Basic 認証をかけることができそうです。


 

このページのトップヘ