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

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

タグ:nodejs

Tips 的な小ネタです。

Node.js + Express によるウェブアプリケーションコードの中で、何らかの URL への GET リクエスト(POST とかでもいいですが、処理内容は GET の時と同じなので GET で考えることにします)を受けて処理している時の、アクセス時のフル URL をサーバー側で知る方法です。 なお、ここでの「フル URL 」とは、プロトコル+ホスト名+(デフォルトと異なる場合は)ポート番号+アクセスパス+実行時のURLパラメータ のこととします。

この値はクライアント側の JavaScript を使えば windows.location オブジェクトを参照することで取得できます。ただこちらはあまり意味がないというか、アクセスしたユーザーは自分のブラウザのアドレス欄を見れば URL を確認できるのでわざわざ別途必要になるケースが珍しいはずです。このクライアントサイドでの話ではなく、サーバーサイドの処理内で知る方法、という意味です。

結論としては以下のようなコードで取得することが可能です:
//.  app.js
var express = require( 'express' ),
    app = express();

app.get( '/*', function( req, res ){
  res.contentType( 'text/plain; charset=utf-8' );
  var url = req.protocol + '://' + req.get( 'host' ) + req.originalUrl;
  res.write( url );
  res.end();
});

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

まずルーティングのパス定義部分を '/*' としています。これによってルートパス以下のすべてのパスへの GET リクエストをこのハンドラが受け持つ、と宣言します。

肝心のフル URL ですが、このハンドラ実行時のパラメーター: req(リクエストオブジェクト)を使って、以下のように求めることができます:
 var url = req.protocol + '://' + req.get( 'host' ) + req.originalUrl;

req.protocol にはプロトコル("http" または "https")、req.get( 'host' ) でポート番号まで含めたアクセス時のホスト名、そして req.originalUrl にはアクセス時のフルパスが URL パラメータまで含めた形で取得できます。これらをつなぎ合わせることでアクセス時のフル URL が取得できるので、これをレスポンスで返す、という処理をしています。

試しに実行していくつかの URL パターンでアクセスしてみた所、以下のようにいずれも期待通りの結果になりました:
(http://localhost:8080/)
2021062301


(http://localhost:8080/abc/hello?x=100)
2021062302


(http://localhost:8080/abc/hello?x=100&y=200)
2021062303


1つの環境で複数のサーバー名を持って稼働するサーバーの場合に、「何というホスト名でアクセスされているのか」をサーバー側からも知ることができる、という情報でした。

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


どれだけ需要があるかわかりませんが、docker イメージ(dotnsf/access-url)の形で以下からも公開しています:
https://hub.docker.com/r/dotnsf/access_url


利用可能な Kubernetes クラスタ環境(と接続設定などが済んだ kubectl コマンド)があれば、以下の手順でコマンドを実行することで Deployment と(spec.type = "NodePort" の) Service を作成できます:
$ git clone https://github.com/dotnsf/access_url

$ cd access_url

$ kubectl -f yaml/app_deployment.yaml

IBM Cloud の無料版 IKS(IBM Kubernetes Services) で、上記コマンドを実行して作成したアプリケーションにアクセスした時の様子が以下になります。アクセスした URL が正しくサーバーサイドで取得できている様子が確認できます:
2021062401

 
最後に余談を。上述のクライアントサイド JavaScript による(window.location オブジェクトを用いた)取得方法との取得できる情報の違いについて補足します。

クライアントサイドで取得する場合、サーバーサイドで取得できない情報が1つ取得できます。それが「ハッシュ」と呼ばれる情報で例えば、
 http://xxx.xxx.xxx.xxx/abc/hello?x=1&y=2#here
という URL アドレスの最後の "#here" 部分の情報です。

この情報はクライアントサイドであれば window.location.search を参照することで取得することが可能ですが、サーバーサイドでは取得する方法がありません。ただハッシュ情報はクライアントサイドで(HTML 内の特定位置を参照するなど)利用するためのものであって、サーバーサイドで生成する情報としてはハッシュによる差異はありません。要はサーバーサイドでは意味のない情報であるためサーバー側では取得できなくなっている、ものと思われます。



IBM Cloud から提供されている Db2 on Cloud を、2021/05/23 時点では最新となる v4 API を使ってアクセスするまでを実現した Node.js のサンプルアプリケーションを作りました。アクセストークンを取得して SQL を実行し、その結果を取得するまでの一連の流れを紹介します。


Db2 on Cloud は IBM 製リレーショナルデータベース製品である Db2 をマネージドサービスとして IBM Cloud から提供しているサービスです。IBM Cloud のライトアカウントを作成し、フリープランを選択することで、データは 200MB までなどの制約はありますが無料で利用することも可能です:
2021052301


この Db2 は製品としての経緯もあり、専用(ネイティブ)クライアントライブラリをインストールした上で JDBC/ODBC などから利用することが多かったのですが、近年は Node.js 向けのライブラリなども提供されるようになり、各種プログラミング言語からの利用もできるようになっていました。その API の最新版(v4)では REST API 対応が行われ、(ラズベリーパイなど)専用クライアントライブラリが存在しなかった環境からの利用も可能になりました。

というわけで、今回のブログエントリではこの v4 API を使って Db2 on Cloud のインスタンスにアクセスして SQL を実行し、その実行結果を取得するまでの一連の手順をサンプルアプリケーションとそのコードを参照しつつ紹介します。


【サンプルアプリケーション】
以下で紹介するサンプルアプリケーションの(Node.js 向け)ソースコードはこちらで公開しています:
https://github.com/dotnsf/node-db2


Node.js がインストールされていれば Windows でも Mac でも Linux でも動きます。特にこのアプリケーションは v4 API で作っているので、これまでは(クライアントライブラリが提供されていないため)Db2 にアクセスできなかったラズベリーパイなどの環境からでも利用できる点が特徴になっています。なのでラズパイ環境をお持ちだったらぜひラズパイから利用してみていただきたいです。

サンプルアプリを利用するには、まず IBM Cloud 側の準備が必要です。大まかに以下2段階の準備を行います:
(1)IBM Cloud にログインして Db2 on Cloud サービスインスタンスを作成
(2)作成したインスタンスの接続情報を取得


まず(1)を行います。IBM Cloud に(必要であればアカウントを作成して)ログインし、Db2 サービスインスタンスを作成します:
2021052302


この際に "Lite" プランを選択しておくとデータ量 200MB 上限や、同時接続数 15 などの制約がありますが、無料で利用することができます:
2021052303


(1)のインスタンスを作成したら(2)の準備を行います。作成したインスタンスの「サービス資格情報」タブを選択し、「新規資格情報」ボタンで「サービス資格情報」を1つ追加して、その中身を確認します:
2021052304


以下のような JSON フォーマットの情報を確認することができます:
{
  "db": "BLUDB",
  "dsn": "DATABASE=BLUDB;HOSTNAME=dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net;PORT=50000;PROTOCOL=TCPIP;UID=username;PWD=password;",
  "host": "dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net",
  "hostname": "dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net",
  "https_url": "https://dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net",
  "jdbcurl": "jdbc:db2://dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net:50000/BLUDB",
  "parameters": {
    "role_crn": "crn:v1:bluemix:public:iam::::serviceRole:Manager"
  },
  "password": "password",
  "port": 50000,
  "ssldsn": "DATABASE=BLUDB;HOSTNAME=dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net;PORT=50001;PROTOCOL=TCPIP;UID=username;PWD=password;Security=SSL;",
  "ssljdbcurl": "jdbc:db2://dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net:50001/BLUDB:sslConnection=true;",
  "uri": "db2://username:password@dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net:50000/BLUDB",
  "username": "username"
}

ここで必要になるのは以下の3つの値です:

username
password
hostname


加えて deployment id を取得します。この ID はサービス固有の ID で、すべての REST API 実行時に必要な値です。取得方法はサービスを表示しているブラウザ画面の URL を確認します:
2021052305


おそらくこのようなフォーマットの URL 文字列になっています:
https://cloud.ibm.com/services/dashdb-for-transactions/crn%3Av1%3Abluemix%3Apublic%3Adashdb-for-transactions%3Aus-south%3Aa%2Fxxxxxxxxxxxx%3Axxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx%3A%3A?paneId=manage

この中の "crn" で始まる文字列(? が含まれる場合はその手前まで)を URL デコードした値が deployment id です。URL デコードといってもパターンは2つに決まっていて、"%3A" を ":" に、"%2F" を "/" に変換するだけです。例えば該当の Db2 on Cloud サービスを表示している時の URL 内文字列がこの内容だった場合、
https://cloud.ibm.com/services/dashdb-for-transactions/crn%3Av1%3Abluemix%3Apublic%3Adashdb-for-transactions%3Aus-south%3Aa%2Fxxxxxxxxxxxx%3Axxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx%3A%3A?paneId=manage

deployment id は crn で始まる青字部分を URL デコードした値ということになり、具体的には以下の値となります:
crn:3v1:bluemix:public:dashdb-for-transactions:us-south:a/xxxxxxxxxxxx:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx::

以上、ここまでの作業で Db2 on Cloud のサービスインスタンスを作成し、その username, password, hostname, deployment id の4つの情報を取得することができました。この4つの値が取得できれば、実際にサンプルアプリケーションを動かすことができます。


上述のリポジトリ URL から git clone やダウンロード&展開するなどしてソースコード一式を取得します。そして settings.js をテキストエディタで開いて、先程取得した4つの値を入力して保存します:
2021052306


これでソースコード側に必要な変更も完了です。では実際に Node.js で動かしてみましょう:
$ cd node-db2

$ npm install

$ node app

ここまでのコマンドを実行すると、サンプルのウェブアプリケーションは 8080 番ポートで待ち受ける形で起動します。ウェブブラウザ(別 PC からでも可)からサンプルの HTTP リクエストを送信して、Db2 on Cloud で SQL を実行し、その結果を取得します:
http://(上記の $ node app を実行したマシンの IP アドレス):8080/ping

なお、この GET /ping リクエストは Db2 on Cloud インスタンスに対して以下の SQL を実行した結果のシステム情報を取得するように作られたものです:
select TABNAME, TABSCHEMA, OWNER from syscat.tables fetch first 5 rows only;

実際にウェブブラウザでこの URL にアクセスするとこのような結果になります。上述の SQL を実行した結果が JSON で返ってきました。仮にアプリケーションをラズベリーパイ上で動かしていても同様の結果が取得できているはずです。これまでは Db2 のクライアントライブラリが導入できる環境からでないと実行できなかった SQL を、v4 API では(サーバー上のもの以外には)クライアントライブラリを使わない環境でも実行して結果を取得することができる、ということがわかります:
2021052307


【ソースコード解説】
ではソースコードを見ながら v4 API の解説をします。上述のオペレーションで使っているファイルは変数設定以外は app.js のみなので、全容を確認したい場合はこのファイルを参照ください。

上述で行ったのは1つの SQL を実行して、その結果を取得する、というごく普通のオペレーションですが、v4 API では大きく3つのパートに分かれて処理されています((2)と(3)が別れて処理されている点に注目):

(1)アクセストークン取得
(2)SQL 実行
(3)SQL 結果取得


まず(1)について。これまでの Db2 API では認証時にユーザー名とパスワードがあればログインして SQL を実行することができましたが、v4 API では(今どきらしく)まずアクセストークンを取得して、アクセストークンを付与しながら SQL を実行する必要があります。というわけでアクセストークンの取得手順を紹介します。

具体的には以下のようなコードを実行しているのですが、settings.js に設定した username と password を https://(hostname)/dbapi/v4/auth/tokens に POST しています。またその際の HTTP リクエストヘッダ内で deployment id の値を送付しています(HTTP リクエストヘッダに deployment id を含める、というのは以下すべての REST API で共通です)。成功した場合、返されるオブジェクトの token キーにアクセストークンが付与されて返ってくるので、この値を保存しています(実際のアプリではセッション等に保存しておくべきです):
var access_token = null;
var option = {
  url: 'https://' + settings.hostname + '/dbapi/v4/auth/tokens',
  json: { userid: settings.username, password: settings.password },
  headers: { 'content-type': 'application/json', 'x-deployment-id': settings.deployment_id },
  method: 'POST'
};
request( option, async function( err, res0, body ){
  if( err ){
    console.log( { err } );
  }else{
    if( body && body.token ){
      access_token = body.token;
      console.log( { access_token } );
    }
  }
});


GET /ping にリクエストがあると、このアクセストークンを使って(2)SQL を実行します。app.js では以下のように実行しています:
app.get( '/ping', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  if( access_token ){
    var sql0 = 'select TABNAME, TABSCHEMA, OWNER from syscat.tables fetch first 5 rows only;';
    var option0 = {
      url: 'https://' + settings.hostname + '/dbapi/v4/sql_jobs',
      json: { commands: sql0, limit: 10, separator: ';', stop_on_error: 'no' },
      headers: { 'content-type': 'application/json', 'Authorization': 'Bearer ' + access_token, 'x-deployment-id': settings.deployment_id },
      method: 'POST'
    };
    request( option0, async function( err0, res0, body0 ){
      if( err0 ){
        console.log( { err0 } );
        res.status( 400 );
        res.write( JSON.stringify( { error: err0 }, null, 2 ) );
        res.end();
      }else{
        if( body0 && body0.id ){
            :
            :

GET /ping リクエストへのハンドラとして、まず "select TABNAME, TABSCHEMA, OWNER from syscat.tables fetch first 5 rows only;" という SQL を https://(hostname)/dbapi/v4/sql_jobs に POST して実行しています。この処理が成功すると、返されるオブジェクトに id というキー値が含まれています。

そして(3)では、実行した SQL の結果を取得します。その際には(2)で取得した id キー値を指定して GET https://(hostname)/dbapi/v4/sql_jobs/(id) を実行して、その結果が先程のブラウザ画面のようなフォーマットで取得されていたのでした:
            :
            :
        if( body0 && body0.id ){
          console.log( { body0 } );
          var option1 = {
            url: 'https://' + settings.hostname + '/dbapi/v4/sql_jobs/' + body0.id,
            headers: { 'content-type': 'application/json', 'Authorization': 'Bearer ' + access_token, 'x-deployment-id': settings.deployment_id },
            method: 'GET'
          };
          request( option1, async function( err1, res1, body1 ){
            if( err1 ){
              console.log( { err1 } );
              res.status( 400 );
              res.write( JSON.stringify( { error: err1 }, null, 2 ) );
              res.end();
            }else{
              //. body1 は string
              body1 = JSON.parse( body1 );
              console.log( { body1 } );
              res.write( JSON.stringify( body1, null, 2 ) );
              res.end();
            }
          });
        }else{
          res.status( 400 );
          res.write( JSON.stringify( { error: body0 }, null, 2 ) );
          res.end();
        }
      }
    });
  }else{
    res.status( 400 );
    res.write( JSON.stringify( { error: 'no access_token' }, null, 2 ) );
    res.end();
  }
});


NoSQL とは違って、トランザクションを含む(時間のかかる)処理を実行する可能性もあるため、その間で処理をブロックしないようにこのような仕様で API が用意されているのだと思います。上の例では SQL の実行と結果の取得を続けて行って実行結果を返していますが、(途中に取得できる id をいったんクライアントに返すことで)これら2つの処理を別の REST API で分けて実行させることも可能になります。シングルスレッドの Node.js での実装を考慮すると、こちらのほうがより正しいといえるかもしれません。


【良い点・悪い点】
なんといっても「ラズベリーパイなどの、Db2 クライアントライブラリが提供されていないシステムから Db2 サーバーに接続してクエリーを実行できる」ようになったことが大きな改善点であると感じました。また時間のかかる処理を実行する際にも、まとめて実行して、その間の他のリクエストをブロックすることなく、後から結果を確認できるようになっている点もクラウド時代らしいといえます。

唯一残念な点は「プレースホルダー型の SQL に未対応」な点です。SQL を比較的安全に実行する上でよく使われるプレースホルダー型の SQL("SELECT * from xxxxx where id = ?" で、パラメーターの id を分けて送って実行する SQL)には現時点ではこの REST API では未対応のようで、現時点では開発者が気をつけて実装するか、(クライアントライブラリが提供されているシステムから)従来の専用ライブラリを使って実行する必要がありそうでした。


【参考】
IBM Db2 on Cloud REST API


簡易ブラウザゲームなど、HTML + 画像 + CSS + (クライアントサイド)JavaScript のみで作成できる静的ページの公開手段として Github ページを使っている人は少なくないと思っています。かくいう自分もその一人で、例えば以下のページはすべて自作した上で Github ページを使って公開している例です:
ブラウザ移植版 "MOLEMOLE"
マッチ棒を1本だけ動かして正しい式にする(を自動解答)
JavaScript + CSS + HTML オンラインプレビューワー


あらためて Github ページを簡単に紹介します。

ベースとなっている Github は無料で(も)使えるソースコードのオンラインバージョン管理システムです。個人的には 10 年前は全く別の Subversion というバージョン管理システムを自分で自宅サーバーにインストールして使っていました(一応まだ残ってますが、ほぼ使わなくなりました)が、この 10 年で状況ががらりと変わり、今ではほぼ Github (というか、Git を使ったシステム)がバージョン管理の主流となりました。 Github ページはこの Github の機能の1つとして提供されているもので、Git プロジェクト(の一部)のフォルダをまるごと HTML のドキュメントルートとして公開するというものです。いわば「Github の安定した HTTP サーバーを使って公開する無料のウェブページ」とも言えるものです。あくまで静的コンテンツしか公開できないため、データベースにデータを登録する機能などは使えませんが、CORS の設定や REST API 、AJAX などを併用することで外部ファイルや外部データベースからデータを取得して表示する、といった程度であれば実現できます。自分自身は上述のような簡易アプリケーションに加えて、ドキュメント類やランディングページを公開する際にも便利に使っています。


このように便利な Github ページですが、Github に登録した後は便利なのですが、登録前のコンテンツ作成時にちょっと使いにくいと感じることもありました。例えば以下のようなケースを考えてみます:
2021042901


↑リポジトリの説明に必要な README.md と、Github ページで表示する index.html を同じフォルダに配置しています(このルートフォルダを Github ページで公開する、という想定です)。

index.html 内では imgs/icon.png ファイルを参照しています。また外部 JavaScript である jQuery(https://code.jquery.com/jquery-2.2.4.min.js)の URL を指定して利用しています:
2021042902


ちなみに img/icon.png は「いらすとや」さまの「静かにしてください」のマークを使わせていただきました:
icon


index.html 自体は上述のように比較的シンプルな内容です。(ローカルファイルをフルパス指定して)ブラウザで表示すると以下のような内容が表示されるものです:
2021042903


ちなみに <body> 部はこれだけ:
<body>

<h1>GHP(GitHub Pages) サンプル</h1>

<img src="./imgs/icon.png"/>

</body>

このプロジェクトを(深く考えずに)Github ページで公開する場合の手順を紹介します。そんなに複雑な話ではなく、まず単純に Github にパブリックなプロジェクトを作り、このフォルダ内の全ファイルを同プロジェクトに git push します(以下は自分のリポジトリに push した際の例です):
$ git init

$ git add .

$ git commit -m 'first commit'

$ git branch -M main

$ git remote add origin https://github.com/dotnsf/ghp.git (ここは実際の URL を指定してください)

$ git push -u origin main


2021042904

 
これでプロジェクトのバージョン管理を Github で行えるようになりましたが、今回の目的はバージョン管理というよりも Github ページによるコンテンツ公開なので、もう1ステップ必要になります。同プロジェクトの "Settings" メニューから "Pages" を選び、公開方法(どのブランチのどのフォルダ以下をページとして公開するか)を指定します。今回の場合は main ブランチのルートディレクトリをそのまま公開するだけなので、以下のように指定して "Save" します:
2021042905


すると以下のような画面になり、このコンテンツが Github ページで公開されます(実際にインターネットからアクセスできるようになるまで1分弱かかるようです):
2021042906


表示されている URL にアクセスすると、ちゃんとコンテンツとして公開できていることを確認できます。(オリジナルドメインで GitHub ページを使う設定をしていなければ)HTTPS にも自動的に対応されていて、非常に便利な機能です:
2021042907


ここまでが Github ページの概要および使い方です。index.html の内容を更新したり、新しいファイル(ページ)を追加した場合でも同様にしてプロジェクトの更新ファイルを Github に登録(git push)するだけで公開されるコンテンツの内容も更新されます。静的コンテンツの公開目的には非常に便利なサービスですが、一方で細かな点ではありますが、この方法だと少し非効率に感じる点がないわけではありません。自分の場合は以下の2点で少し不便に感じています。

まず一点目として、ローカルでの動作確認時にブラウザでローカルファイルを開く必要がある、という点があります。上述でもローカルファイルの内容を参照する際には file:/// から始まる URL で(つまりブラウザのメニューや Ctrl + O で「ファイルを開く」を選択してから、目的のフォルダを探し、その中の index.html を指定して)開いていました。自分の場合は普段 Node.js を使ってウェブアプリケーションを作るのですが、その際の動作確認時は http://localhost:8080/ といったシンプルな URL でアプリケーションを開けるようにしています。この差もあって、動作確認するたびにこの「ファイルを開く」オペレーションをするのは少し面倒に感じてしまいます:
2021042903


もう一点は「外部ファイルのプロトコル」についてです。例えば index.html 内で jQuery を呼び出す部分は以下のように記載しています:
<script type="text/javascript" src="https://code.jquery.com/jquery-2.2.4.min.js"></script>

気になるのは赤字の部分、つまり目的のファイルを https 指定で呼び出しています。現実問題として https で指定できるファイルを https で呼び出すことにはセキュリティ面での問題はないとも言えます。

一方で、これまで Node.js でウェブアプリケーションを開発している中でこのような外部 JavaScript の呼び出しを行う必要がある際、http か https かを指定せずに記述することがあります。つまり普段は以下のように指定しています:
<script type="text/javascript" src="//code.jquery.com/jquery-2.2.4.min.js"></script>

この記述方法だと表示するページ(今回の例だと index.html)を呼び出す際に使うプロトコルと同じプロトコルで jquery ファイルを呼び出すことになります。つまりローカルでの動作確認時など http://localhost:8080/ で呼び出している場合は http プロトコルで、実際に Github ページとして https://dotnsf.github.io/ghp/ で呼び出している場合は https プロトコルで、それぞれ呼び出すことになり、外部ファイル呼び出しもアクセス時のセキュリティレベルに合わせて行うことができるようになっています。

ただこの方法はローカルファイルを直接呼び出す際の file プロトコルでアクセスした場合は正しく読み込むことができません(file:///code.jquery.com/jquery-2.2.4.min.js は存在しないため)。ローカルファイルを呼び出す場合はプロトコルを省略せずに明記する必要があり、http か https のいずれかを指定して記述する必要があるのでした。

つまり、Github ページで運用する際には index.html 内の外部ファイル呼び出し部でプロトコルまで指定せずに記述することができるのに、動作確認時はプロトコルを指定して呼び出す必要がある。ということになります。これは非常に面倒な話で動作確認が終わったらプロトコル記述を削除する(Github ページを更新した後に、更にまた手元のファイルを変更する必要が生じた場合はもとに戻す)という運用は面倒だし、だからといってそのためだけにプロトコルを固定して記述しなければならないのだとしたら筋の違う話だと思っています。ローカルで常に HTTP サーバーを動かして、対象フォルダをドキュメントルートにしておけば、動作確認時も http://localhost:8080/ のようにアクセスできるようにはなりますが、それも本末転倒な話です。なんとかして Github ページを作る際もローカル動作確認時だけ HTTP サーバーを使って動作させることができないものか・・・


・・・と、長い前振りでしたが、ここからが本題です。アプリケーション・サーバーや HTTP サーバーを併用しないと解決が難しそうで、かつ Github ページ利用時に余計なコードが公開されないようにしたい、という上述の問題を解決する方法を紹介します。以下に紹介する方法は Node.js + Express を使った方法で、どちらかというとサーバーサイド JavaScript のアプリケーションエンジニア向けですが、言語環境によっては同様のことができる別方法(というか別言語環境)があるかもしれません。

答としてはプロジェクトを以下のようなファイル構成に変更します:
2021043001


Github ページで公開していたコンテンツ関連ファイル(今回の例では index.html と imgs/icon.png)を新たに作成した docs/ フォルダ内に移動しました。プロジェクトのルートフォルダには README.md を残した上で、新たに .gitignore, app.js, package.json ファイルを追加しています。以下、追加したファイルの内容を紹介します。

.gitignore ファイルは一般的なもので、Node.js では node_modules/ フォルダを Git 連携の対象外フォルダとすることが一般的です。というわけで、今回は以下の内容の .gitignore ファイルを使います:
node_modules/
package-lock.json
!.gitkeep

package.json ファイルも特殊なものではなく普通の package.json ファイルです。後述の app.js が express パッケージのみ利用するものなので、このような内容にしました:
{
    "name": "ghp",
    "version": "0.0.1",
    "scripts": {
        "start": "node app.js"
    },
    "dependencies": {
        "express": "^4.17.1"
    },
    "repository": {},
    "engines": {
        "node": "10.x"
    }
}

そして Node.js のメインファイルとも言える app.js 、これも実はごくシンプルな内容のものです:
//. app.js
var express = require( 'express' ),
    app = express();

app.use( express.static( __dirname + '/docs' ) );

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

特筆というほどのことはないのですが、肝といえるのは赤字部分です。Express を使ってウェブアプリケーション内のスタティックコンテンツが含まれるフォルダを docs/ フォルダに指定しています。これにより docs/ フォルダ以下にあるファイルはアプリケーションのルートパスから相対パスで表示できるようになります。つまりブラウザなどのクライアントから ./index.html と指定すると ./docs/index.html ファイルが、./imgs/icon.png と指定すると ./docs/imgs/icon.png ファイルを参照することができるようになります。つまり Github ページで公開するのとほぼ同じ状態をローカルホストで作ることができるようになります。

実際にローカルホストで動作確認用に動かす場合は、Node.js インストール後に以下のコマンドを実行します。特にオプションを指定しない場合は 8080 番ポートで HTTP サーバーが起動します:
$ npm install

$ node app
server starting on 8080 ...

8080 番以外の番号で待ち受けたい場合は起動時に環境変数 PORT に待受ポート番号を指定します(以下は 8081 番で待ち受ける場合):
$ PORT=8081 node app
server starting on 8081 ...

起動後、localhost の待受ポート番号を指定してブラウザでアクセスすれば(Ctrl + O で index.html ファイルを指定して開かなくても)目的のページを開くことができます:
2021050101


この状態であれば file プロトコルではなく http(s) プロトコルを使っているので、 docs/index.html 内の外部 JavaScript 参照部分でのプロトコルも明示する必要がなく、以下のような記述でも(ローカルでの動作確認時も、Github ページでの利用時も)動作するようになります。ローカルでの開発時と Github ページで公開する時の差を意識する必要もなくなりました:
<script type="text/javascript" src="//code.jquery.com/jquery-2.2.4.min.js"></script>

最後にこの変更を Github ページでも反映する必要があります(ルートディレクトリを Github ページとして公開している設定だと index.html ファイルが開かないので)。まずは更新した内容を Github リポジトリにもコミット&プッシュして反映させます:
$ git add . -A

$ git commit -m 'node.js 対応'

$ git push origin main

次に Github ページの設定を変更し、/(ルートフォルダ)ではなく /docs/ フォルダ以下を Github ページとして公開するように変更して Save します:
2021050102


少し待つとこの変更が Github ページにも反映され、先程と同じ URL で同様のページを参照することができるようになります:
2021042907


この状態ができてしまえば、手元でページを編集するときは docs/ フォルダ以下のファイルを対象に変更を加えて、必要に応じて `$ node app` でローカルの HTTP サーバーでコンテンツの出来を確認し、Github にコミット&プッシュすれば自動的に Github ページにも更新が反映されるようになります。

もうちょっとスマートなやり方があるのかもしれませんが、Node.js + Express であればこのように「Express の静的コンテンツフォルダを docs/ にする」+「Github ページの対象フォルダを docs/ 以下にする」という設定を組み合わせることで面倒からの開放は実現できそうでした。


このブログエントリの続きのような、関係ないような内容です:
IBM COBOL for Linux 1.1 を使ってみた

もともとは「過去の COBOL 資産をウェブやクラウド環境で活用するにはどうすればいい?」かを考えていて、最初は CGI 化してウェブからうまく呼び出して使えないか、、、と検討していたのですが、「活用」と呼ぶにはそのための準備がなかなかに面倒そうで頓挫していました。要は COBOL エンジニアが少なくなってメンテナンスも難しくなっている昨今、COBOL 資産をどうやって今の環境で活用するか、というテーマは実際に検討してもキレイな答を見つけるのが難しいことを再認識する結果となっています。

といいつつ、「難しいですね~」で終わらせるのもシャクなので自分なりの答を用意しました。それがブログタイトルでもある Node.js の child_process を使う方法です。


Node.js + Express で普通にウェブアプリを作りつつ、COBOL 資産をサブルーチンのように(子プロセスとして)呼び出して動かし、その実行結果をウェブのレスポンスとする、という方法です。その子プロセスとしてコマンドを動かす際に child_process を利用します。ぶっちゃけ COBOL 資産でなくても応用の効く泥臭い方法ですが、コマンドラインパラメータを受け取って動き、結果を stdout へ出力する一般的なタイプのバイナリであれば再コンパイル無しで使えるという大きなメリットのあるウェブ化手法といえます。

言葉で説明するよりも実際のコードを見ていただくのが早いと思っています。まず COBOL 資産(資産といえる代物じゃないけど)として先日紹介した COBOL 版 Hello World を使います。特にこのファイルは x86_64 Linux 向けの実行可能バイナリになっているので、ダウンロードして `$ chmod +x hello` するだけで動きます(`$ ./hello` で "Hello World!" と出力するだけですが)。とりあえず、これを COBOL 資産とみなすことにします。


そしてこのコマンドを呼び出してレスポンスを返すウェブアプリケーションを Node.js + Express で作ります:
https://github.com/dotnsf/hello-cobol-web


アプリケーションの実体となる app.js の中は以下のようになっています:
//. app.js
var express = require( 'express' ),
    { exec } = require( 'child_process' ),
    app = express();
var command = '/home/dotnsf/src/hello/hello';

app.use( express.Router() );

app.get( '/', function( req, res ){
  res.contentType( 'text/plain; charset=utf-8' );
  
  exec( command, function( err, result, stderr ){
    if( err ){
      res.status( 400 );
      res.write( err.message );
      res.end();
    }else if( stderr ){
      res.status( 400 );
      res.write( stderr );
      res.end();
    }else{
      res.write( result );
      res.end();
    }
  });
});

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starging on " + port + " ..." );

まず赤字部分で child_process ライブラリを呼び出し、シェルコマンドを実行する準備をしています。次に緑字部分で実際に呼び出すコマンド(この場合は上記 hello コマンド)をフルパスで指定します(コマンドラインパラメータが必要な場合はこのコマンド文字列に含めておきます)。

そして実際にコマンドを呼び出しているのが青字部分です。GET / の HTTP リクエストを受けたら hello コマンドを(JavaScript らしく)非同期に実行し、その実行時や実行結果にエラーがあった場合は HTTP ステータスコード 400 で、なかった場合は(デフォルトの)HTTP ステータスコード 200 でそれぞれの結果を text/plain で返すコードになっています。

上述のコードを Node.js で実行し、GET / を実行すると(hello コマンドのフルパスやパーミッションを間違えていなければ)COBOL 資産の実行結果が HTTP の結果として "Hello World!" と表示されるはずです:
2021042601


専用コマンドのインストール部分が、特に有償ツールなどの場合だとコンテナ化までは難しいかもしれません。ただ COBOL 資産に限らず、形態素解析エンジンのような専用インストールが必要なコマンド・アプリケーションをウェブ化するようなケースでも応用できる便利な方法の紹介でした。

まず、今回紹介するのは Node.js + Express で作った API を CORS 対応にする、という、これ自体はシンプルな内容なのですが、この話を考えるに至った経緯を最初にまとめておきます。


【背景】
普段からウェブアプリケーション・サービスを開発しています。詳しいアーキテクチャはともかく、ユーザーのアクセス先となるフロントエンドのアプリケーション・サーバーとバックエンド(データベースなど)があって、クラウドっぽくバックエンドには API サーバー経由でアクセスする、という形態を多く採用しています。

この形態を採用している時に限らない話ですが、「サービスの安定運用」を考えると「いかにフロントエンドを安定させるか」を考慮する必要があります。ネットワークやバックエンド含めたサービス全体のどこかに不具合が生じた場合であっても、ユーザーが最初にアクセスするフロントエンドが動いていれば「画面に障害発生メッセージを出す」ことができるようになります。逆にフロントエンドにアクセス過多を含めた不具合が発生してしまうと、「メッセージで利用者に不具合が発生していることを知らせる」ことすらもできなくなってしまいます。

このフロントエンドサーバーを安定稼働させるための技術として、最近は(docker などの)コンテナ技術であったり、(k8s や OpenShift などの)コンテナのオーケストレーション技術が流行っています。ここまでは「いわゆる一般論」的な話です。

一般論を理解した所で、技術者としての「一般論ではない話」も考えます。自分は業務でもプログラミングや作ったりサービスの運用を行ったりしていますが、業務外でも(つまり個人でも)プログラミングしたり、作ったサービスを公開して運用したりします。2つの異なる立場を持っているわけです(決して珍しくないと思ってます)。前者ではある程度の予算の中でクラウドのサービスを契約したりして、必要であればベンダーが提供するコンテナやコンテナ・オーケストレーションも使って構築することになります。 一方後者では、これらのインフラ構築部分も自腹になるわけです。まあ「これも授業代」と太っ腹に考える人は立派だと思いますが、コンテナ・オーケストレーションまで使おうとするとそれなりに懐も痛む価格だったりします。


要するに「個人開発者としての自分はケチ」なわけで(繰り返しますが、決して珍しくないと思ってますw)、「ケチはケチなりに知恵と作業でなんとかしたい」と考えるわけです。コンテナ技術やコンテナ・オーケストレーション技術を否定するつもりは全くありませんが、「より安価」に「フロントエンドを安定稼働」させる方法はないものか、と:
2021032001


#「コンテナ・オーケストレーションまで自分で構築すればいい」と考える人もいると思うので一応コメントを。技術的にはそのとおりなんですが、目的はあくまで「安価なフロントエンドの安定稼働」です。そう考えると例えば1ノードで構築した場合に目的を達成しているといえるか・・・ では複数ノードを構築する場合、今度は安価といえるのか・・・ となってしまうと考えました。


そんな自分にひらめいた1つの案が「フロントエンドを GitHub Pages にする」方法です。GitHub Pages は GitHub の無料アカウントがあれば使うことのできる「静的ウェブコンテンツの公開サービス」です。GitHub の一部として考えると、容量は(ほぼ)無制限で、アップロード直後にバックアップされ、世界中のウェブサービスの中でも指折りの安定稼働を誇っています。「フロントエンドを安定させたい」という目的だけを考えると、「かなり使える案」だと思っています:
2021032002
(↑フロントエンドを GitHub Pages にして、バックエンドを IBM Cloud にする場合)


もちろんこの方法は万能ではありません。まず GitHub Pages で公開できるコンテンツはウェブアプリケーションではなく「静的ページ」、つまり HTML ページに限られます(この時点で i18n などを考慮するアプリケーションページの公開は難しくなります)。表示データは REST API で取得すれば良いのですが、フロントエンドが静的ページである以上、通信は AJAX に限られてしまいます。その結果、API 側は(クロスオリジン通信をすることになるため)CORS を考慮した設計が必須となります:
2021032003


フロントエンドはウェブアプリケーションではないので、ウェブページをテンプレートから作る、といった便利な手法が使えず、AJAX を駆使したレンダリングを実装しないといけないことも不利な点となりますが、バックエンド側も考慮点の影響が大きな方法ではあると思っています。ただ「安価にフロントエンドを安定稼働」させる面ではイケそうな方法にも感じています。


・・・といった背景がありました。この前提だと REST API 部分も便利なサービスを有償契約して使うのではなく、安価なアプリケーション・サーバーを使って、自前で API サーバーを構築する必要があります(最悪、ここにトラブルがあってもフロントエンド側ではその旨を伝えることができる構成)。というわけで、無料のライトプランが使える IBM Cloud で「Node.js + Express で作った API を CORS 対応にする」ための方法を理解しておく必要がありました。


【Node.js + Express の REST API を CORS 対応する】
こちらはサンプルを用意しておきました:
https://github.com/dotnsf/express-cors


まず settings.js の exports.cors 配列変数内にクロスオリジン通信を許可するオリジン(ドメイン)を登録しておきます。デフォルトでは以下のようになっていて、'http://localhost:8080' と 'https://dotnsf.github.io' からの AJAX 通信を許可するよう設定しています。前者はローカルでの動作確認用ですが、実際に Github Pages で運用を始めた後は削除しておくべきです。後者は僕の Github Pages のドメインです。実際にみなさんが Github Pages でこの API を使う場合は自分自身の Github Pages ドメインに変更してください:
//. settings for CORS
exports.cors = [ 'http://localhost:8080', 'https://dotnsf.github.io' ];

本体とも言える app.js の内部は以下です:
//. app.js
var express = require( 'express' ),
    app = express();

var settings = require( './settings' );

app.use( express.static( __dirname + '/public' ) );

//. CORS
if( settings && settings.cors && settings.cors.length && settings.cors[0] ){
  var cors = require( 'cors' );
  var option = {
    origin: settings.cors,
    optionSuccessStatus: 200
  };
  app.use( cors( option ) );
}

app.get( '/ping', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: 'OK' } ) );
  res.end();
});


var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

赤字以外の部分はごく普通の Node.js + Express を使った REST API アプリケーションです。この例では GET /ping という動作確認用のシンプルな API を1つだけ定義しています(サーバー側に障害等が起きていなければ、返り値は常に { status: 'OK' } という JSON です)。

肝心の CORS の対応をしているのが赤字部分です。settings.js の内容を確認し、exports.cors 配列に有効な値が1つでも含まれていると判断した場合は、cors ライブラリを使って設定されているオリジンを対象に CORS を有効にします。ちなみに settings.js の exports.cors が例えば null とか [](空配列)とかに設定されている場合は CORS の処理が行われないので、CORS の処理としてはデフォルトの「全てのクロスオリジンからの AJAX アクセスを許可しない(同一オリジンからの AJAX アクセスのみ許可する)」ことになります。

あとは必要に応じてベーシック認証を加えるとか、トークン認証を加えるとか(フロントエンドが静的コンテンツだとトークンの管理が面倒そうだけど)するなどして CORS 対応の REST API を構築することができます。ここは手順がわかってしまえば簡単そうですね。


【運用時】
こんな感じでデータベースへの読み書き検索などが REST API 化され、CORS 対応までできていれば Github Pages の HTML からも利用できるので、かなり安定したフロントエンドコンテンツを実現できそうです。この形で REST API が実現できていると、例えばウェブアプリを作るハンズオンでもあらかじめ用意した REST API を Github Pages から呼び出す形で実現できるので、フロントエンドの運用サーバーは無料で(しかもかなり安定したものを)用意できます。更にフロントエンド部分の開発時にはこんなフォルダ構成の Node.js + Express のアプリケーションにしておくと docs/ 以下の静的コンテンツを対象に作って、ローカルで動作確認もして、コードごと Github にあげて docs/ 以下を Github Pages で公開する、といった便利な開発・運用も可能になります:
//. app.js
var express = require( 'express' ),
    app = express();

app.use( express.static( __dirname + '/docs' ) );

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );
(↑メインファイル。静的コンテンツのフォルダを /docs に指定している以外はごく普通のシンプルな Express アプリ)


2021032004
(↑このプロジェクトを Github に上げる)


2021032005
(↑Github Pages の設定で /docs フォルダ以下を公開するよう指定する)


フロントエンドコンテンツは全て AJAX 対応させる必要があり、昨今の流行りとは違う形での実装は必要になります。それはそれで大変だし、普通のウェブアプリケーションで使える便利な技術も使えなくて不便な面も出てくるのですが、"Poorman's stable web contents" みたいに割り切って使うネタを準備しています。まだアイデア先行な面もあって実証実験が必要な段階だと思っていますが、運用していて気づくことがあったらまたこのページに追記します。


このページのトップヘ