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

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

タグ:http

とある要件を実現するツールを作りました。同じことに悩む人がいた場合を想定して、ツールをソースごと公開することにしました。

某ウェブプラットフォームのサービス終了が決まり(わかる人はこれだけで何の話か推測されそうだけど・・)、現在稼働中のウェブアプリケーションを引っ越しすることになりました。引っ越しそのものはさほど難しくないのですが、問題は「サービスの URL が変わってしまう」ことでした。

図に示すとこのような感じです。これまで運用していたサービスの運用環境を A から B に引っ越しした結果、これまでの URL とは異なる URL で引き続き運用することになりました:
2022070601


これまで使っていたユーザーに対しても「サービスの URL が変わった」ことを知らせてあげたいのですが、具体的にどうするべきでしょうか?A で運用中の画面に「URL が変更になった」と注意書きを含めて、改めて B にアクセスしてもらうこともできます。が、もう少し気の利くやり方として「A にアクセスしたら自動的に B に転送させて、B で運用中の画面で URL が変更になった旨を記載しておく(そのままブックマークできるようにする)」という方法もあります。


この後者の方法を実現するためには A にアクセスした利用者に対して "301" という HTTP ステータスコードと、続けて変更先の URL を Location ヘッダに含めて返すことで実現できます。この HTTP ステータスコード 301 は "Moved Permanentaly" を意味していて「URL が(一時的ではなく)恒久的に変更になった」ことを示しています。続けて Location ヘッダに新しい URL を含めておくことでウェブブラウザ側で新しい URL に自動的に遷移してくれます。つまり「A にアクセスしたら自動的に B に移動させる」ことが実現できます(そして B 側で「URL が変わったので現在のページをブックマークして」といったメッセージを記しておく、といった対応になります)。ウェブページの引っ越しを行う場合の一般的な手段でもあります:
2022070602



問題となるのは、この「301 という HTTP ステータスコードを返す」機能です。A 側のサービスでそのような転送機能や転送の設定が提供されていればそれを使えばいいのですが、必ずしも提供されていないことも考えられます。そのようなケースに対応するため、今回「アプリケーションの機能として 301 HTTP ステータスコードと、引っ越し先 URL を返すアプリケーション」を作ったので、公開することにしました。これまで A で動いてたアプリケーションの代わりにこのアプリケーション(下図の app)をデプロイすることで、A へのアクセスがあった場合に、新しい B への URL に無条件で転送させることができるようになります:
2022070603


このような挙動を実現するための Node.js アプリケーションのソースコードを以下で公開しました:
https://github.com/dotnsf/301movedpermanently

動作確認する場合は、Node.js が導入済みの環境にソースコードをダウンロードするか git clone して、環境変数 URL に転送先の URL を指定して実行します。なおアプリケーションはデフォルトで 8080 番ポートで待ち受けますが、このポート番号を変えたい場合は環境変数 PORT に指定して実行してください:
$ git clone https://github.com/dotnsf/301movedpermanently

$ cd 301movedpermanently

$ npm install

$ URL=https://www.yahoo.co.jp/ node app (全てのリクエストを Yahoo! トップページに転送する場合)

起動後にアプリケーションにアクセスすると、全てのリクエストが環境変数 URL で指定した値(上の例だと https://www.yahoo.co.jp/)に転送されます。なおコード内ではメソッド/パス/パラメータに関係なく、全てのリクエストを GET リクエストに変換して転送しています。GET/HEAD メソッド以外のリクエストについては HTTP ステータス 308 を返して対応するケースもありますが、今回のような「サービスごと引っ越し」のケースでは新 URL のトップページに転送することが多いと思うので、今回は説明を控えます(下の青字部分が実行されます):
app.all( '*', function( req, res ){
  if( post_redirect ){
    var method = req.method;
    if( method == 'GET' || method == 'HEAD' ){
      //. https://developer.mozilla.org/ja/docs/Web/HTTP/Status/301
      res.status( 301 );
    }else{
      //. https://developer.mozilla.org/ja/docs/Web/HTTP/Status/308
      res.status( 308 );
    }
  }else{
    res.status( 301 );
  }

  res.set( 'Location', url );
  res.end();
});

古いサイト A が動いている間だけ有効な強制転送方法ですが、他に方法がない場合はこんな感じで古いサイトを訪ねた人を強制的に B へ転送する方法が有効だと考えています。

なお、docker イメージとしても公開しているので、移行元で docker が使える環境であればこちらを使っていただくのが手っ取り早いと思っています:
dotnsf/301movedpermanently


HTTP プロトコルを使っていると、「HTTP ステータスコード(以下、「ステータスコード」)」を意識することがあります:
http-status-codes


ステータスコードは HTTP リクエストを実行した結果を端的に表す3桁の数字です。最も有名かつ一般的なステータスコードは 200 で "OK" の意、つまり「リクエストが成功した場合のステータスコード」(「HTTP リクエストが成功すると 200 というステータスコードが返ってくる」と理解するとわかりやすいかも)です。

200 以外だと 404("Not Found" URL やファイルが存在しない場合など)や 403("Forbidden" アクセス権限がない場合など)あたりが比較的多く目にする機会があるものでしょうか。まあ本来あってはならない 500("Internal Server Error" サーバー内部エラー)とかもたまに見ることありますけど。。 なお規格として定義されているステータスコードの一覧はこちらを参照してください:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Status


ともあれ、特にアプリケーションを開発したり運用したりしていると、「必ずしも 200 のステータスコードばかりが返ってくるわけではない」ことを意識する必要に迫られることがあります。なんらかの原因でエラーが発生してしまうことはあるし、どういうエラーとなった場合にどのような挙動にするのか、ということまで考慮して開発・運用する必要がある、という意味です。


一方でそこまで意識していたとしても、開発した/運用中のアプリケーションがエラーに遭遇するのは稀なケースのはずです。「こういうエラーの時はこういう挙動にする」ための準備をしていても、思いのままに任意のエラーを出せるわけではないのです。


そんなケースを想定して、「任意の HTTP ステータスコードを返す」アプリを作ってみました。URL パラメータに返してほしいステータスコードを指定して GET リクエストを送ると、レスポンスは指定したステータスコードが返ってくる※、というものです。

※本来のあるべき挙動とは異なります。要はアプリケーションとしては正しく動いている(ステータスコードは 200 になるべき)にも関わらず、本来返すべきステータスコードとは異なる 404 等を返す、という挙動をします。あくまで上述のようなケースのデバッグ用および動作確認用アプリケーションです。


アプリケーションのソースコードはこちらで公開しています:
https://github.com/dotnsf/statuscode_generator

また Docker イメージもこちらから取得可能です:
https://hub.docker.com/r/dotnsf/statuscode_generator


ローカルの Docker エンジンを使って動かす場合は、以下のように指定して実行します(8080 番ポートでアクセス可能にする場合):
$ docker run -d --name statuscode-generator -p 8080:8080 dotnsf/statuscode-generator

起動しているアプリケーションに GET /status リクエストを実行します。URL パラメータ code を付けて実行すると、指定したステータスコードが返されます(デフォルトは 200):
$ curl -v http://localhost:8080/status

*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /status HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
< Date: Sun, 24 Oct 2021 05:32:20 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
< Transfer-Encoding: chunked
<
{
  "code": 200,
  "reference_url": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status",
  "timestamp": 1635053540644
* Connection #0 to host localhost left intact

$ curl -v http://localhost:8080/status?code=404

*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /status?code=404 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 404 Not Found
< X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
< Date: Sun, 24 Oct 2021 05:33:03 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
< Transfer-Encoding: chunked
<
{
  "code": 404,
  "reference_url": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status",
  "timestamp": 1635053583162
* Connection #0 to host localhost left intact

なお、起動時に環境変数 ALLOW_CORS が設定されていると、指定した値のサーバーからであれば CORS リクエストを受け付けるようになります(デフォルトでは CORS 無効):
(例 全てのサーバーからの CORS リクエストを受け付ける場合)
$ docker run -d --name statuscode-generator -e ALLOW_CORS=* -p 8080:8080 dotnsf/statuscode-generator

この ALLOW_CORS=* が指定された状態で起動しているサーバーを heroku 上に用意しておきました。heroku 無料枠で動かしているので初回アクセス時は(停止状態から起動するまで)少し時間がかかると思いますが、よかったらお使いください:
https://statuscode-generator.herokuapp.com/


なお、この任意のステータスコードを返す API は GET /status で直接実行できますが、(上述の heroku アプリのように) GET / でアクセスすると簡易 UI が現れ、簡単な動作確認ができます。返してほしいステータスコードを指定して AJAX ボタンをクリックすると、AJAX で GET /status?code=(指定したステータスコード) が実行され、その結果が画面下部に表示されます(下図は 403 を指定して実行した時の例):
2021102401


特にクラウドやコンテナ環境などで、アプリケーション・サーバーとは別にエッジサーバーがあってリバースプロクシーを使っている場合などリバースプロクシー側の動作設定をする場合に便利かと思い作ってみました(アプリケーションサーバーエラーが起こった場合はエッジサーバー側で特定のエラーページに遷移させる、といった設定をしている場合の動作確認時など)。自分以外にこれが役立つ人がどのくらいいるかわからないのですが、よかったら使ってください。役立つことがあれば嬉しいです。

なお、ステータスコードとして 1xx(百番台)を指定した場合ですが、HTTP ステータスコードとしては指定したものが返されます。が、1xx は実行途中の経過を返すものだったりして、実際の挙動という点ではこの続きがないと不自然な挙動となります。そのあたりまで考慮されているわけではない点をご了承ください。






IBM Cloud から提供されている 30 日間無料 Kubernetes サービスIBM Kubernetes Service 、以下 "IKS")環境を使って利用することのできるコンテナイメージを1日に1個ずつ 30 日間連続で紹介していきます。

環境のセットアップや制約事項については Day0 のこちらの記事を参照してください。

Day 2 は超有名な Apache HTTP イメージをデプロイする例を紹介します。
eyecatch-apache


【イメージの概要】
このイメージは昔から HTTP サーバーとして広く使われてきた Apache HTTP サーバーです。静的なコンテンツの公開だけでなく、CGI などの仕組みと組み合わせてアプリケーション・サーバーとして利用されることが多いという印象です。


【イメージのデプロイ】
Apache HTTP イメージのデプロイ用 YAML ファイルを公開したので、このファイルを指定してデプロイします。以下のコマンドを実行する前に Day 0 の内容を参照して ibmcloud CLI ツールで IBM Cloud にログインし、クラスタに接続するまでを済ませておいてください。

そして以下のコマンドを実行します:
$ kubectl apply -f https://raw.githubusercontent.com/dotnsf/yamls_for_iks/main/apache.yaml

以下のコマンドで Apache HTTP 関連の Deployment, Service, Pod, Replicaset が1つずつ生成されたことと、サービスが 30080 番ポートで公開されていることを確認します:
$ kubectl get all

NAME                          READY   STATUS    RESTARTS   AGE
pod/apache-76695bf577-pj9w5   1/1     Running   0          4m2s

NAME                 TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
service/apache       NodePort    172.21.151.226   <none>        80:30080/TCP   4m4s
service/kubernetes   ClusterIP   172.21.0.1       <none>        443/TCP        26d

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/apache   1/1     1            1           4m4s

NAME                                DESIRED   CURRENT   READY   AGE
replicaset.apps/apache-76695bf577   1         1         1       4m4s

この後に実際にサービスを利用するため、以下のコマンドでワーカーノードのパブリック IP アドレスを確認します(以下の例であれば 161.51.204.190):
$ ibmcloud ks worker ls --cluster=mycluster-free
OK
ID                                                       パブリック IP    プライベート IP   フレーバー   状態     状況    ゾーン   バージョン
kube-c3biujbf074rs3rl76t0-myclusterfr-default-000000df   169.51.204.190   10.144.185.144    free         normal   Ready   mil01    1.20.7_1543*

つまりこの時点で(上述の結果であれば)アプリケーションは http://169.51.204.190:30080/ で稼働している、ということになります。早速実行してみます。ウェブブラウザか curl コマンドを使って、アプリケーションの URL(上述の方法で確認した URL)にアクセスしてみます:
20210723_apache01


分かる人にはおなじみの(笑)Apache HTTP サーバーの初期起動メッセージ "It works!" が表示されました。無事にデプロイできて、IKS 環境で動いている状態が作れました。



【YAML ファイルの解説】
YAML ファイルはこちらを使っています:
apiVersion: v1
kind: Service
metadata:
  name: apache
spec:
  selector:
    app: apache
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
    nodePort: 30080
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: apache
spec:
  replicas: 1
  selector:
    matchLabels:
      app: apache
  template:
    metadata:
      labels:
        app: apache
    spec:
      containers:
      - name: apache
        image: httpd
        ports:
        - containerPort: 80

Deployment 1つと、Service 1つのごくごくシンプルな YAML ファイルですが、一応解説を加えておきます。アプリケーションそのものは 80 番ポートで動作するように作られているため、NodePort 30080 番を指定して、外部からは 30080 番ポートでアクセスできるようにしています(NodePort として指定可能な番号の範囲は 30000 ~ 32767 です、指定しない場合は空いている番号がランダムに割り振られます)。また ReplicaSet は1つだけで作りました。


デプロイしたコンテナイメージを削除する場合はデプロイ時に使った YAML ファイルを再度使って、以下のコマンドを実行します。不要であれば削除しておきましょう:
$ kubectl delete -f https://raw.githubusercontent.com/dotnsf/yamls_for_iks/main/apache.yaml


【紹介したイメージ】
https://hub.docker.com/_/httpd


【紹介記録】
Dayカテゴリーデプロイ内容
0準備準備作業
1ウェブサーバーhostname
2Apache HTTP
3Nginx
4Tomcat
5Websphere Liberty
6データベースMySQL
7phpMyAdmin
8PostgreSQL
9pgAdmin4
10MongoDB
11Mongo-Express
12Redis
13RedisCommander
14ElasticSearch
15Kibana
16CouchDB
17CouchBase
18HATOYA
19プログラミングNode-RED
20Scratch
21Eclipse Orion
22Swagger Editor
23R Studio
24Jenkins
25アプリケーションFX
262048
27DOS Box
28VNC Server(Lubuntu)
29Drupal
30WordPress

このブログエントリを書くきっかけとなったのはこの記事でした:
2021年5月のWebサーバ利用シェア、「Nginx」が「Apache」を初めて上回る


ウェブサーバー(HTTP サーバー)のシェア争いで、ついに Nginx が Apache を抜いて首位にたった、というものです。この2つの合計だけで全体の 70% 前後あるという「2トップ」ですが、少しずつ差を縮めた結果、5月になってわずかながら Nginx のシェアが Apache を上回ったようでした。

・・・で、その結果は置いといて(苦笑)、自分自身が気になったのは順位では6位に位置づけられている "Node.js" です:



Nginx や Apache は、例えば WordPress や Drupal のような有名な CMS のウェブサーバーとしても使われていたりするし、他にも Ruby on Rails を使う場合のウェブサーバーとして利用されるので、多くのプログラミング言語/コンテンツ管理システムのウェブサーバーとして採用されていることがシェアが高い背景として想像できます。

その観点で見た場合、6位の "Node.js" は異色といえます。なぜならば Node.js はあくまでプログラミング言語であって、これ自体はウェブサーバーではないからです。統計としては「Node.js で作ったウェブサイトのページ」をカウントしているのだと想像できるし、(Node.js がウェブサーバーではない以上)他に分類のしようがないことも理由だと思いますが、いろんな言語で作られたページのウェブサーバーである Nginx や Apache がランクインしている中では「違うカテゴリーの人が混ざってる」感は否めません。

そして最も気になったのは「そもそも、どうやって Node.js で作ったページだとわかったのか?」でした。Nginx や Apache だとわざと 404 エラーを出してそのエラー画面から調べる、という方法もあるのですが、Node.js では(特に明示しない限り)味気ない 404 エラーがでるだけなので調べようがないはずです。どうやって Node.js ウェブページのシェアを数えたのだろう? という興味が湧いて調べてみたのでした。

結論としてはごくシンプルで、404 エラーを出さなくても HTTP レスポンスヘッダの中に答がありました:
2021052501


Node.js でウェブアプリを作る際にほぼ 100% 使われるであろう Express ライブラリを使ってアプリを作って起動し、そこに curl コマンドを HTTP レスポンスヘッダのみ返すようなオプションをつけて実行してリクエストとレスポンスのヘッダ情報も確認しました。するとレスポンスヘッダの中に "X-Powered-By: Express" という項目がありました。なるほど、「この情報が含まれるページは Node.js 製」と判断したんだろうな。。


で、この情報がわかると、同様にウェブサーバーを併用しない開発言語である Python はどうなっているんだろう? と気になりました。ウェブサーバーランキングには入ってないけど、Python で作ったウェブページかどうかも Node.js と同様にして調べることができるのだろうか・・・と。

で、こちらも Flask ライブラリを使ってアプリを起動し、同様に curl コマンドで HTTP レスポンスヘッダを取得するモードで実行してヘッダを確認すると・・・ こちらはレスポンスヘッダの中に "Server: Werkzeug/0.14.1 Python/3.6.7" というわかりやすい項目がありました。Python の場合は「ウェブページを作る際のライブラリはほぼ 100% Flask」とは言い切れない所がありますが、まあでも同様に調べることはできそうですね。
2021052502


(おまけ)ところで、このブログエントリの中でも使っているのですが、「curl コマンドで HTTP レスポンスヘッダのみ調べる」方法を調べました。複数のオプションを組み合わせる形で実現しているのですが、結論としてはこんな感じでした:
$ curl -D - -s -o /dev/null http://localhost:nnnn/

3つのオプションを組みあわせて実現しています:
  • -D - : dump header オプション、標準出力にヘッダを出力する
  • -s : slient オプション、途中経過を出力しない
  • -o /dev/null : output オプション、(ヘッダ以外の)本体を /dev/null へ出力して廃棄


【参照】
https://qiita.com/yousan/items/fcc15e1046939c465ab7

いま自分の空き時間を使って、過去に Node.js + Cloudant を使って(普通のウェブアプリケーションとして)作っていたウェブサービスの Node-RED 環境への移植に挑戦しています。要はサーバーサイド JavaScript 実行環境である Node.js を使って過去に開発したウェブアプリケーション(画面や REST API)を、Node-RED の HTTP リクエスト/レスポンスノードや、HTML テンプレートノードを使っても動くように移植することに挑戦している、ということです。それなりに実績のある Node.js アプリケーションを Node-RED 上でも動かすことができれば、プラットフォームとしての Node-RED のポテンシャルを証明することができるのではないか、と考えています。

これを具体的に進めようとすると、まずウェブ画面は(i18n とかを考慮しなければ)HTML テンプレートノードを使えば一通りのことはできると思っています。要するに HTML テンプレートの中に HTML や CSS, フロントエンド JavaScript を含めてしまえば、見た目や挙動含めて一通りの画面を作ることはできると思っています。

問題は REST API 部分です。例えばデータベース(今回は Cloudant)のデータを読み書きするインターフェースを REST API で用意しておき、フロントエンドの画面から REST API を呼び出すことでデータの読み書き更新削除を行うことができるようになります(理論上は)。この REST API を Node-RED の HTTP リクエストノードと、HTTP レスポンスノードと、function などのノードを駆使して必要な機能を実装することができるかどうかが移植の可否になりそうだと思っています。まあ普通にデータを読み書き更新削除検索・・・する程度であれば標準の Cloudant ノードの機能範囲内でできそうな感触を持っています。


さて、今とある Node.js + Cloudant 製ウェブアプリケーションの Node-RED プラットフォームへの移植を設計している中で1つの壁に当たってしまいました。上述のように「普通の」データの読み書きの REST API 化はさほど問題にならないのですが、Cloudant が持つ特徴を使った部分が普通のデータのように扱うことができず、一筋縄ではいかない内容でした。結論としてはなんとなく解決の目処はたったと思っているのですが、その内容と解決までの経緯を含めて以下にまとめてみたので、興味ある方はご覧いただきたいです。


さて、問題となった Cloudant が持つ特徴を使った部分です。Cloudant は NoSQL 型(JSON 型)データベースですが、特殊な JSON フォーマットで格納することでバイナリデータを格納することができます。またその格納されたバイナリデータを(Content-Type 含めて)出力することもできます。この機能を使うことで、例えば画像データを Cloudant に格納して、画像データとして出力する、といったことも可能です。この機能は Cloudant の各種 SDK からも便利に使えるよう関数化されていたりします。

実は Cloudant のこのバイナリデータ格納機能を使っている場合が Node-RED 移植をする上でのネックとなります。Node.js などのプログラミング言語で Cloudant を利用する場合(特に上述の機能を使ってバイナリデータを Cloudant に格納する要件が含まれる場合)、Cloudant の REST API や各種 SDK を使ってデータの読み書きを実装することになります。上述のバイナリデータの読み書きも同様です。バイナリデータを書き込んだり、バイナリデータを Content-Type 含めて(つまり画像データであれば画像データとして)取り出して出力したりできます。実際にアプリケーション開発の中でこの機能を使って実装していることは(個人的にはバイナリデータの格納先に Cloudant を使うことが多いので)珍しくありません。

しかし、この部分を Node-RED に移植できるか? となると話は変わってきます。まず Node-RED からは Node.js 向けの Cloudant SDK を利用することはできません。function ノードの中でがんばって  Cloudant の REST API を呼び出すような JavaScript を書けば Node-RED でできるかもしれません(認証情報をどのように管理するかの問題は残ります。またどうせ JavaScript でゴリゴリ書くというのであれば、そもそも Node-RED をプラットフォームに選択しない方が正しいような気もします)。 この問題を標準の Cloudant in/out ノードだけでバイナリデータの読み書きを扱うことはできないか? と読み替えて考えることにしました。


【方法1 正攻法】
そもそも何が正攻法なのか、という問題もありますが、実は標準の Cloudant in ノード(Cloudant にデータを格納するノード)はバイナリデータを格納することもできます。上述の Cloudant のバイナリデータ格納機能は単に JSON データフォーマットをうまい具合に指定することで実現しているので、データを格納する点までは少しの工夫で実現できるのでした。

ただし、この方法の問題点は格納時ではなく取り出し時にあります。標準の Cloudant out ノード(Cloudant からデータを取り出すノード)は _id 値を指定してデータを取り出すことはできるのですが、肝心のこの部分がバイナリデータ格納を意識することなく、普通に JSON データとして取り出してしまうことしかできないのでした。特殊なフォーマットで格納することでバイナリデータ格納を実現しているのですが、この特殊なフォーマットに合わせた取り出しができないため、書き込むことはできても読み出せない、という問題が残ってしまうのでした。。


【方法2 BASE64 エンコードを利用して独自実装】
なんとなく解決の目処が立っているのがこちらの方法です。データの読み書きそのものは Cloudant の標準ノードを使うのですが、扱うバイナリデータは格納前に BASE64 でエンコードして(標準 Cloudant in ノードで)格納します。そして取り出す際も普通に標準 Cloudant out ノードで取り出した後に該当部分を BASE64 でデコードします。最後に HTTP レスポンスノードの属性で Content-Type ヘッダを指定して、デコード結果(画像バイナリ)を返信する、という方法です。プログラミングによるカスタマイズを駆使した、いかにもプログラマーらしい方法ですが、こちらの方法であれば格納時だけでなく取り出し時にも問題なく実現できそうです。

試しにフローを作ってみました。Github でも公開したので良かったらこちらからフローをダウンロードするなどして後述の手順で試してみてください:
https://github.com/dotnsf/nodered_cloudant_binarydata_io


【方法2 サンプルフローの使い方】
このサンプルを使って、実際にバイナリデータ(画像データ)を Node-RED で読み書きできることを確認してみます。

まずは Node-RED 環境を用意します。個別に用意していただいても構いませんが、最終的に Cloudant データベースを用意する必要もあるので、IBM Cloud を使って用意する方法がおすすめです。なお IBM Cloud を使ってここに書かれた方法で Node-RED 環境を構築した場合は、始めから Cloudant-in / Cloudant-out ノードがインストールされた状態になっているので、後述のこれら Cloudant 関連ノードのインストールは不要です。無料のライトアカウントを使って構築することもできる内容なので、Node-RED 環境がない人が試す上でおすすめの方法ではあります。

上記以外の方法で(普通にインストールするなどして)Node-RED 環境を用意した場合は node-red-node-cf-cloudant ノードを別途インストールする必要があります。右上のメニューから「パレットの管理」を選択し、「ノードを追加」から "node-red-node-cf-cloudant" を検索して追加してください:
2021032101

2021032102


併せて IBM Cloud にログインして Cloudant サービスを追加して利用できるようにしておいてください。繰り返しますが、このあたりあまり詳しい自信がない場合は上述の方法で IBM Cloud 環境内に Node-RED 環境を Cloudant データベースや Cloudant ノードなどとまとめて用意するのがおすすめです。


Node-RED が準備できたら、上述の Github リポジトリを使ってサンプルのフローを構築します。この flow.json ファイルがサンプルのフローそのものです。リンク先のテキスト内容をまとめてコピーし、Node-RED の右上メニューから「読み込み」を選択します:
2021032103


読み込みのダイアログで「クリップボード」を選択し、コピーしていた内容をペーストします。そして「新規のタブ」を選択し、最後に「読み込み」ボタンをクリックします:
2021032104


するとこのようなフロー画面が再現されるはずです:
2021032105


このままだとまだ2つの Cloudant ノード(画面上では "mydb" と表示されている2つの水色ノード)が未接続で使えません。どちらかをダブルクリックして設定ダイアログを表示します。すると Service 欄が一瞬だけ空のまま表示されますが、IBM Cloud の Node-RED 環境であれば接続済みの Cloudant サービスを見つけて接続してくれます。Service 欄に Cloudant サービス名が表示されたら「完了」ボタンをクリックします(もう1つの Cloudant ノードも同様にして Service 欄が埋まった状態にします):
2021032106


このように2つの Cloudant ノードの右上に表示されていた赤い印が2つとも消えればサンプルを動かすための準備は完了です。画面右上の「デプロイ」ボタンでデプロイして動作前の準備は完了です:
2021032101


改めてこのタブを見ると、3つの HTTP リクエストを処理するフローが定義されています:
#HTTP リクエスト処理内容
1GET /home画像ファイルアップロード画面
2POST /file画像ファイルアップロード処理
3GET /file(?_id=XXXX)アップロードした画像ファイルを画像として取り出す処理


1番目の GET /home は後述の 2 と 3 の動作を確認するための UI として、ファイルを指定してアップロードできる画面を表示するものです。実際に /home へアクセスすると、以下のような画面が表示されます:
2021032201


非常にシンプルなファイルアップロード機能を持ったページです。「ファイルを選択」ボタンを選んでローカル PC からファイル(今回は画像ファイル)を選択して「送信」ボタンをクリックです。「送信」すると、2番目 POST /file が実行されて、選択したファイルが Cloudant に格納される、というものです。

ここで試しに以下の画像ファイルを指定してアップロードしてみます(お好きな画像で試してください):
dotnsf_logo_200x200


画像ファイルを指定して「送信」します:
2021032202


こんな感じの HTTP POST の結果が表示されます(実際のアプリでは AJAX を使うなどしてこの結果をそのまま表示しないようにします):
2021032203


この後に Cloudant のダッシュボードなどから mydb データベースの中を確認するとデータが1件追加されているはずです:
2021032204


表示を JSON 形式などに切り替えると、格納されたデータファーマットも確認できます(type に画像フォーマット、data に base64 でエンコードした画像バイナリデータが格納されています):
2021032205


このデータの id 値を確認します(上図だと c7c3eb8e3b9ac0fffcd45c1beea6c62a )。この値と3番目の GET /file を使って格納された画像を表示してします。ウェブブラウザで /file?_id=(id の値) にアクセスして、アップロードした画像が表示されることを確認します:
2021032206
(↑アップロードした画像が復元できた!)


Node-RED を使ってバイナリ(画像)ファイルを Cloudant に格納し、また Node-RED から画像を復元することも実現できることがわかりました。


【方法2 解説】
Node-RED の HTTP リクエストでバイナリデータを格納したり、Node-RED の HTTP リクエストで格納したバイナリデータを取り出すことができる、ということがわかりました。以下はこれを実現している上記フローの解説です。

まず画面 UI である GET /home ですが、これはごく普通に enctype="multipart/form-data" を指定したフォームを定義しているだけです。テンプレートノードの中身は以下の内容の HTML です:
<html>
<body>
<form method="POST" action="/file" enctype="multipart/form-data">
<input type="file" accept="image/*" capture="camera" name="image" id="image"/>
<input type="submit" value="送信"/>
</form>
</body>
</html>

次に POST /file の各ノードを説明します。まず HTTP in ノード(POST /file と書かれたノード)はファイルアップロードに対応する処理を行うため「ファイルのアップロード」にチェックを入れている点に注意してください:
2021032207


また直後の function ノードの内容は以下のようになっています。アップロードされたファイルは msg.req.files に配列で格納されます(今回はファイル1つだけですが、配列の0番目に格納されます)。その mimetype と buffer を取り出し、buffer を base64 エンコードして msg.payload に格納し直して、最後にタイムスタンプを追加する、という処理を行っています(Cloudant データベースにはこのフォーマットで格納されていたはずです):
2021032208
//. アップロードしたファイルを base64 エンコーディング
var type = msg.req.files[0].mimetype;
var img64 = new Buffer( msg.req.files[0].buffer ).toString( 'base64' );

//. 独自フォーマット化
msg.payload.type = type;
msg.payload.data = img64;

//. タイムスタンプを追加
msg.payload.timestamp = ( new Date() ).getTime();

return msg;


この function ノードで処理された msg.payload の内容を Cloudant out ノードが受け取って格納します。このノードでは「Only store msg.payload object?」にチェックを入れて、ヘッダ情報などを格納しないようにしています。これで指定したバイナリファイルを(base64 エンコードして)Cloudant に格納する(同時に _id が割り振られます)、までの処理を実現しています:
2021032209


最後に GET /file(?_id=XXXX) のノードを紹介します。まず Cloudant in ノードでは特別な処理は行っておらず、パラメータとして与えられた _id を使って Cloudant の mydb 内を検索して結果を返す内容にしています:
2021032201


直後の function ノードでは mydb から取り出した結果を画像に戻す処理をしています。上述の function ノードの逆を行う形で、msg.payload.data の値を base64 デコードして画像バイナリに戻して msg.payload に代入し直しています:
2021032202
//. base64 エンコードされているバイナリデータをデコード
if( msg.payload && msg.payload.data ){
  msg.payload = new Buffer( msg.payload.data, 'base64' );
}

return msg;


その結果を HTTP レスポンスノードに渡して処理は終了です。が、このノードでは HTTP ヘッダをカスタマイズし、"Content-Type: image/png" を付けています。つまり直前の function ノードで取り出した画像のバイナリを画像(image/png)として送信するための処理を最後に加えています:
2021032203


これらのノードや処理を組み合わせることで Node-RED の HTTP リクエストからバイナリデータを Cloudant に格納したり、格納したデータからバイナリデータを取り出して Content-Type ヘッダを付けて返す、といった一連の処理を実現していました。


この例は Cloudant にバイナリデータを格納する場合のサンプルでしたが、おそらくほぼ同様の方法で他のデータストアにも応用できると思っています。



このページのトップヘ