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

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

2021/10

コンテナ環境が広まっていくと、アプリケーションやサービスが安定運用できるようになる、という話を耳にします。それはそれで事実だと思うのですが、本当にサービスを安定運用するにはアプリケーションが使うバックエンドも安定運用が必要で、そのあたりの話が飛ばされていることが多いと感じることもあります。 今回のブログエントリはそういう話の例として、Node.js からリレーショナルデータベース(今回は PostgreSQL)を使うアプリケーションでどういった点を考慮すべきか、という例を紹介します。


Node.js から PostgreSQL に接続する、というコード自体は node-postgres という npm パッケージを使うことで簡単に実現できます。具体的な方法もググって容易に見つけることができます。

一方、特にクラウドやコンテナ環境においてはマイクロサービス化を踏まえた設計になっていることも珍しくないと思っています。そのようなケースでは「PostgreSQL がメンテナンス状態になる(接続が切れる)」ことを想定する必要があります。アプリケーションとしては「データベース接続が切断される可能性があり、切断されたら再接続する(それも失敗したら再接続を続ける)」という挙動になるような、ちと面倒な実装が求められます:
20211027



そのような具体的に動くサンプルコードを探していたのですが、ピンポイントで見つけることができず、色々試しながら自分で作ってみました。docker で PostgreSQL イメージを起動し、コンテナを止めたり再スタートすることでアプリも再接続することを確認することができるものです:
https://github.com/dotnsf/postgresql_reconnect


なお、以下の内容は PostgreSQL で(一般的なアプリではこれが普通だと思っていますが)コネクションプーリングを使う前提での接続や SQL 実行を想定したコードを紹介します。


【動作確認】
以下 Node.js が導入された PC と、ローカルの docker を使ってアプリケーションの動作確認する場合の手順を紹介します。

まずはソースコード一式を入手してください。上述の Github リポジトリから git clone するか、ダウンロード&展開して postgresql_reconnect/ プロジェクトを手元に用意します。

まずは docker で PostgreSQL を動かします。起動時に DB を作成しますが、特にテーブルやデータを作ることもなく、単に起動するだけ、です:
$ docker run -d --name postgres -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=P@ssw0rd -e POSTGRES_DB=mydb -p 5432:5432 postgres

↑このコマンドはローカルホストに docker エンジンが導入されている前提で、
 ・ユーザー名: admin
 ・パスワード: P@ssw0rd
 ・データベース名: mydb
 ・公開ポート番号: 5432
でオフィシャル PostgreSQL イメージをコンテナとして起動するように指示するコマンドです。各指定オプションを変更して動かすことも可能ですが、後述するサンプルソースコードはこの内容で PostgreSQL インスタンスが生成される前提で記述されているため、ここから変更する場合はこの後に紹介するサンプルコードの内容も変更内容に合わせて適宜編集してから実行してください:
2021102606



まずは切断時の再接続を考慮しないコード: oldapp.js を実行してみます。ちなみに oldapp.js の内容は以下のようになっています(青字部分が PostgreSQL の接続情報赤字部分が接続部分、そしてピンクが SQL 実行部分です):
//. oldapp.js
var express = require( 'express' ),
    app = express();

var PG = require( 'pg' );

//. PostgreSQL
var pg_hostname = 'localhost';
var pg_port = 5432;
var pg_database = 'mydb';
var pg_username = 'admin';
var pg_password = 'P@ssw0rd';

var pg_clinet = null;
var connectionString = "postgres://" + pg_username + ":" + pg_password + "@" + pg_hostname + ":" + pg_port + "/" + pg_database;//+ "?sslmode=verify-full";
var pg = new PG.Pool({
  connectionString: connectionString
});
pg.connect( function( err, client ){
  if( err ){
    //. 初回起動時に DB が動いていない
    console.log( 'no db on startup', err.code );
  }else{
    console.log( 'connected.' );
    pg_client = client;
  }
});

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

//. ping
app.get( '/ping', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  var sql = 'select 1';
  var query = { text: sql, values: [] };
  pg_client.query( query, function( err, result ){
    if( err ){
      console.log( { err } );
      res.status( 400 );
      res.write( JSON.stringify( { status: false, error: err }, null, 2 ) );
      res.end();
    }else{
      //console.log( { result } );
      res.write( JSON.stringify( { status: true, result: result }, null, 2 ) );
      res.end();
    }
  });
});


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

実際に Node.js で動かす場合、まず初回のみ依存ライブラリをインストールするため(Node.js 導入済みの環境で)以下のコマンドを実行します:
$ npm install

そして以下のコマンドで oldapp.js を起動します:
$ node oldapp

起動すると "server starting on 8080 ..." と表示され、サーバーが 8080 番ポートでリクエスト待ち状態になります。また、この oldapp.js の場合は起動直後に PostgreSQL に接続を試みるので "connected." と表示されます:
$ node oldapp
server starting on 8080 ...
connected.

上述の oldapp.js の内容からもわかるのですが、このアプリケーションは "GET /" と "GET /ping" という2つの REST API を処理します。前者は単に { status: true } という JSON を返すだけのものです。また後者は接続した PostgreSQL に対して "SELECT 1" という SQL を実行し、その実行結果を返すものです(PostgreSQL に接続できていればなんらかの結果が返るものです)。

試しに前者を実行してみます。ウェブブラウザで "http://localhost:8080/" にアクセスし、{ status: true } が表示されることを確認します:
2021102601


また後者も実行してみます。同様にウェブブラウザで "http://localhost:8080/ping" にアクセスし、{ status: true, result: { .... } } という文字列が表示されることを確認します:
2021102602



ここまでは普通に成功するはずです。
ここからが本番です。この状態でアプリケーションを動かしたまま PostgreSQL サーバーを止めてみます。docker コマンドで止める場合は
$ docker stop postgres

を実行します(docker デスクトップを使っている場合は稼働中のコンテナの STOP ボタンを押すと止まります):
2021102603



この状態で改めてウェブブラウザで各ページにアクセスするとどうなるか? 期待している挙動という意味では以下のようになると思っています:
・GET / へのリクエストについては(DB を使わないので){ status: true } を返す
・GET /ping へのリクエストについては(DB が止まっているので)「DB エラー」を返す
・(更に)DB が再稼働したら自動的に再接続して、GET /ping に対して SQL 実行結果を返す


ところが、実はこの時点でサーバーはクラッシュしています。$ node oldapp を実行したターミナルには Exception が表示された上にアプリケーションは終了し、プロンプトが表示されてしまっています:
2021102604


つまりサーバーがクラッシュしています。したがって GET /ping どころか、GET / へのリクエストもエラーとなってしまうし、稼働していないので自動再接続もできません:
2021102605


これでは困ってしまいます。簡単なデモ程度が目的の実装であれば oldapp.js の内容でも(とりあえず動くので)いいと思いますが、ある程度安定した連続稼働が求められる実運用を想定するとちょっと心細い状況と言えます。



では oldapp.js と同じ内容で、DB を止めてもアプリケーションが死ぬこともなく、DB が復活したら自動再接続して再び SQL が実行できるようになるような実装はどのようにすればよいでしょうか? その例が newapp.js です:
//. newapp.js
var express = require( 'express' ),
    app = express();

var PG = require( 'pg' );

//. PostgreSQL
var pg_hostname = 'localhost';
var pg_port = 5432;
var pg_database = 'mydb';
var pg_username = 'admin';
var pg_password = 'P@ssw0rd';

var retry_ms = 5000;  //. retry every 5 sec

var connectionString = "postgres://" + pg_username + ":" + pg_password + "@" + pg_hostname + ":" + pg_port + "/" + pg_database;//+ "?sslmode=verify-full";
console.log( 'connecting...' );
var pg = new PG.Pool({
  connectionString: connectionString
});
pg.on( 'error', function( err ){
  console.log( 'db error on starting', err );
  if( err.code && err.code.startsWith( '5' ) ){
    //. terminated by admin?
    try_reconnect( retry_ms );
  }
});

function try_reconnect( ts ){
  setTimeout( function(){
    console.log( 'reconnecting...' );
    pg = new PG.Pool({
      connectionString: connectionString
    });
    pg.on( 'error', function( err ){
      console.log( 'db error on working', err );
      if( err.code && err.code.startsWith( '5' ) ){
        //. terminated by admin?
        try_reconnect( ts );
      }
    });
  }, ts );
}

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

//. ping
app.get( '/ping', async function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  var conn = null;
  try{
    conn = await pg.connect();
    var sql = 'select 1';
    var query = { text: sql, values: [] };
    conn.query( query, function( err, result ){
      if( err ){
        console.log( { err } );
        res.status( 400 );
        res.write( JSON.stringify( { status: false, error: err }, null, 2 ) );
        res.end();
      }else{
        //console.log( { result } );
        res.write( JSON.stringify( { status: true, result: result }, null, 2 ) );
        res.end();
      }
    });
  }catch( e ){
    res.status( 400 );
    res.write( JSON.stringify( { status: false, error: e }, null, 2 ) );
    res.end();
  }finally{
    if( conn ){
      conn.release();
    }
  }
});


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

青字部分が PostgreSQL の接続情報赤字部分が接続部分、そしてピンクが SQL 実行部分です(青字部分は全く同じです)。違いを紹介する前にまずは挙動を確認してみましょう。PostgreSQL を再び稼働状態に戻します:
2021102606


この状態で "$ node newapp" を実行して newapp.js を起動します:
$ node newapp
server starting on 8080 ...
connecting...

同じようなメッセージが表示されて、リクエスト待ち状態になります。まずは先ほど同様に GET / や GET /ping を実行します(実行結果自体は先ほどと同じです):
2021102601

2021102602


ではここでも同様に PostgreSQL を強制停止してみましょう。先ほどはアプリケーションがクラッシュしてリクエスト待ち状態ではなくなってしまいましたが、今回はプロンプトには戻らず、引き続き待ち受け状態が続くはずです:
2021102601


この状態で改めて GET / や GET /ping にアクセスしてみます。GET / は変わらず { status: true } を返し、GET /ping は(DB にアクセスできないので) { status: false, error: ... } という内容になりますが、ちゃんとレスポンスを返すことができています:
2021102601

2021102602


そして止まっていた PostgreSQL を再度スタートします:
2021102606



するとアプリケーションが自動的に再接続を行い、少ししてから GET /ping を実行すると、再び SQL 実行が成功した時の画面が表示されます:
2021102602


これでデータベースにメンテナンスが入っても自動再接続して稼働する、という実用的な挙動が実現できました。


【コード説明】
改めて2つのソースコードを比較します。といっても青字部分は共通なので比較の対象からははずし、赤字の接続部分と、ピンク字の SQL 実行部分を比較します。

まずは前者の自動再接続しない方。接続部分と SQL 実行部分は以下のようでした:
var pg_clinet = null;
var connectionString = "postgres://" + pg_username + ":" + pg_password + "@" + pg_hostname + ":" + pg_port + "/" + pg_database;//+ "?sslmode=verify-full";
var pg = new PG.Pool({
  connectionString: connectionString
});
pg.connect( function( err, client ){
  if( err ){
    //. 初回起動時に DB が動いていない
    console.log( 'no db on startup', err.code );
  }else{
    console.log( 'connected.' );
    pg_client = client;
  }
});

  :
  :

//. ping
app.get( '/ping', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  var sql = 'select 1';
  var query = { text: sql, values: [] };
  pg_client.query( query, function( err, result ){
    if( err ){
      console.log( { err } );
      res.status( 400 );
      res.write( JSON.stringify( { status: false, error: err }, null, 2 ) );
      res.end();
    }else{
      //console.log( { result } );
      res.write( JSON.stringify( { status: true, result: result }, null, 2 ) );
      res.end();
    }
  });
});

接続処理では単純に接続文字列を生成してコネクションプーリングを生成し、直後に connect() を実行してクライアントを1つ取り出しています。そしてこのクライアントをこの後の SQL 実行時に使いまわしています。

また SQL 実行処理でも SQL 文字列を定義して↑で取り出したクライアントを使って実行しています。処理そのものはわかりやすいのですが、一方でこれといった例外発生を考慮した内容でもありません(そのため DB が止まってしまうケースが想定できておらず、アプリケーションのクラッシュを引き起こしてしまう内容でした)。


一方の後者、例外発生を考慮して、切断後に自動再接続できるようにした方の接続部分と SQL 実行部分は以下のようでした:
var retry_ms = 5000;  //. retry every 5 sec

var connectionString = "postgres://" + pg_username + ":" + pg_password + "@" + pg_hostname + ":" + pg_port + "/" + pg_database;//+ "?sslmode=verify-full";
console.log( 'connecting...' );
var pg = new PG.Pool({
  connectionString: connectionString
});
pg.on( 'error', function( err ){
  console.log( 'db error on starting', err );
  if( err.code && err.code.startsWith( '5' ) ){
    //. terminated by admin?
    try_reconnect( retry_ms );
  }
});

function try_reconnect( ts ){
  setTimeout( function(){
    console.log( 'reconnecting...' );
    pg = new PG.Pool({
      connectionString: connectionString
    });
    pg.on( 'error', function( err ){
      console.log( 'db error on working', err );
      if( err.code && err.code.startsWith( '5' ) ){
        //. terminated by admin?
        try_reconnect( ts );
      }
    });
  }, ts );
}

  :
  :

//. ping
app.get( '/ping', async function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  var conn = null;
  try{
    conn = await pg.connect();
    var sql = 'select 1';
    var query = { text: sql, values: [] };
    conn.query( query, function( err, result ){
      if( err ){
        console.log( { err } );
        res.status( 400 );
        res.write( JSON.stringify( { status: false, error: err }, null, 2 ) );
        res.end();
      }else{
        //console.log( { result } );
        res.write( JSON.stringify( { status: true, result: result }, null, 2 ) );
        res.end();
      }
    });
  }catch( e ){
    res.status( 400 );
    res.write( JSON.stringify( { status: false, error: e }, null, 2 ) );
    res.end();
  }finally{
    if( conn ){
      conn.release();
    }
  }
});

まず接続処理ではコネクションプーリングを生成するまでは同じですが、ここではそのまま終了します。connect() を実行してクライアントを取り出す、のは実際に SQL を実行する直前の処理に変更しています。またこのコネクションプーリングを管理する変数 pg を使ってエラーハンドリングを行い、DB 切断時に正しくハンドリングできるようにしています(具体的には数秒待ってから再びコネクションプーリングを生成し、新たに生成したコネクションプーリングに対してもエラーハンドリングを行う、という内容です)。

また SQL 実行時には以下のような処理を加えています:
(1)処理全体を try{ .. }catch{ .. }finally{ .. } で括り、どこで切断しても例外処理できるようにする
(2)try{ .. } 内の実際に SQL を実行する直前に pg.connect でクライアントを取り出す
(3)finally{ .. } 内でクライアントをリリースしてコネクションプーリングに戻す

この3つの処理を加えておくことで DB が突然死んでも正しくハンドリングして再接続(再びコネクションプーリングを作る)を試みるようにしています。また結果的に再接続に時間がかかってしまう場合であってもアプリケーションそのものはクラッシュせずに生き続けるので、(DB にアクセスはできないけど)利用者からのリクエストに返答できるようにしています。



・・・というわけで、公開&提供しているのはあくまでサンプルですが、この考え方で PostgreSQL サーバーの切断時でも自動接続して連続稼働ができるようになると思っています。また PostgreSQL 以外のコネクションを使う RDB を利用する際にも応用できる内容です。 利用するインフラプラットフォームによってはこういったメンテナンス時の再接続方法について特殊な機能が用意されていることもあるので、必ずしもこういう方法をとらないといけない、というわけではないのですが、一つのベストプラクティス的な内容だと思っています。

自分が痛い目にあったことに関わる内容でもあるので、困っている方のお役に立てば何より。


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 的には特別目新しいサービスではないのですが、リモートワーク時代に向いているサービスだと思ったので、改めてその視点で紹介させていただきます。

新型コロナウィルス流行の影響もあり以前よりもリモートワークが広まっています。中にはリモートワークの需要はあるんだけど、リモートから社内システムを利用するための環境構築ができなかったり、料金的な問題があったりして実現できずにいる人や組織もあると思っています。一般的には VPN(Virtual Private Network) と呼ばれる仕組みを導入して、社内システムを安全な形で外部に公開して利用する、という方法が多く用いられていると思っていますが、この「安全な形で外部に公開」するのは単純ではなく、リモートワークする人も専用のソフトウェアを導入&設定する必要があったり、想定する利用量の規模に合わせて専用のサーバーを1台以上追加する必要があり、その見積もりも簡単ではありません。本格的に利用しようとすると、この追加コストだけでもかなりの額になってしまい、結局二の足を踏まざるを得なくなっているケースもあると思います。

そんな要望に対して IBM Cloud からは SecureGateway と呼ばれるサービスが提供されています。通信用のサーバーは1台必要ですが、VPN とは異なる形で社内システムを外部公開することができ、リモートワークする人から見ても(専用ソフトウェアの導入&設定無しで)ほぼそのまま使うことができます。とりあえず実験的に社内の1つのシステムを公開するだけであれば(1ヶ月 500MB という通信量の制約はありますが)無料で試してみることができます。リモートワークの需要は今後も増えることが予想されるので、特に中小企業向けの小規模利用であれば安価に構築できる環境として紹介します:
2021101600


※SecureGateway は「非推奨」サービスという扱いになっていますが、この社会事情も含めて考慮され、現時点では少なくとも 2022 年6月まではサービス継続することがアナウンスされています。


【SecureGateway の仕組み】
まず、このブログエントリで説明する内容の中で想定しているユースケースを紹介します。企業内や自宅内などにネットワークが構築されていて、そのネットワークに接続している状態からであれば利用できるシステム(=これを「機密システム」と呼ぶことにします)があると想定します。機密システムには機密情報の含まれるデータベースや、それらを業務で扱うウェブアプリケーションなどがあります。 問題になるのはこのシステムが(もともとプライベートネットワークアドレスで構築された環境内に存在しているなどの理由で)社内ネットワーク向けに構築されていて、一般的なインターネットからは利用できない状態にあり、そのためそのままリモートワークへ移行できない、というケースです:
2021101501


このような環境でインターネットからのアクセスをさせようとすると、一般的には VPN(Virtual Private Network) という仕組みを構築します。専用の VPN サーバーを内部ネットワーク内に構築し、VPN サーバーへだけはファイアウォールやパケットフィルタリングを通す設定にしたうえで、インターネットから利用する各 PC に VPN クライアントをインストールして、目的の VPN サーバーに接続するための設定をします。これによって各 PC は VPN サーバーを経由してインターネットからでも「仮想的にプライベートネットワーク(Virtual Private Network)内にいるかのような状態」を作り出すことができ、社内の機密システムにアクセスすることができるようになります:
2021101502


ただこの方法には課題もいくつかあります。主なものとしては以下に挙げるものです:
・新たにサーバーを用意した上で、VPN サーバー機能を構築する必要がある
・VPN サーバーだけはインターネットからアクセスできる状態にする必要があるため、事前にファイアウォールやパケットフィルタリングで許可する必要がある
・インターネットから利用する全ユーザー(正確には全 PC)に VPN クライアントをインストールして、VPN サーバーに接続するための設定を行う、という手間がかかる
・VPN クライアントを導入する PC の台数や利用頻度にもよるが、VPN サーバーや社内ネットワーク、社内とインターネットを繋ぐネットワークは全ての VPN クライアントからの接続を処理することができるだけのスペックが必要になる。この見積も難しく、環境を構築した後に問題が発生すると改めて再設計が求められる可能性もある


簡単に言ってしまうと、仕組みの肝となる VPN サーバーを自分で用意する必要があるため、「充分な性能を用意しておく」という見積段階から用意する側の責任範囲となってしまうことにあります(とはいえ VPN の仕組み上、VPN サーバーは接続先である社内に用意する必要があります)。また各 PC 1台ごとに接続のための設定を行うのも面倒です。


一方、今回紹介する IBM Cloud の SecureGateway (以降 "SG" と表記します)は似た概念ですが、この「VPN の面倒な部分」を大幅に簡易化したものです。スペック見積もりから面倒なサーバー部分は IBM Cloud がサービスインスタンスとして用意するので、クライアント増加による負荷対策を考慮する必要はありません(値段は変わりますけどw)。社内に専用のマシン(SG クライアント)を用意する必要がある点は VPN と同じですが、こちらは docker イメージが提供されていて、社内に docker 環境があれば "docker run" だけで簡単に構築できます。

そして個人的に最大のアドバンテージだと思う2つの点があります。1つは利用量の制約(1か月に 500MB 以内の通信)がありますが、機密システムが1つだけであれば無料で環境構築できること、もう1つは、接続 PC 個別に(VPN クライアントに相当するような)専用の設定を行う必要がなく、IBM Cloud 経由でそのまま社内の機密システムにアクセスできるようになる、という2点です:
2021101503


以下ではこの環境を構築する例として「社内ネットワークに接続したラズベリーパイに社外から SSH で接続する」ための設定手順を紹介します。「ラズベリーパイに SSH 接続」する部分はあくまで例※であり、「社内のある機密システムを利用」すると読み替えていただくとより実像がイメージしやすいかもしれません。

※1つの例であることはその通りですが、一般的な Windows でも Linux でもなければ、HTTP サーバーでもない、という意味では比較的特殊なシステムと考えることができ、それでも接続できることを示した例といえます。
2021101504





【SecureGateway の準備】
実際にこの例で SecureGateway を使って機密システムにアクセスできるようにするまでの手順を紹介します。設定手順は大きく3段階ですが、いずれも専門の知識を必要とするものではなく、docker 環境さえあれば②も簡単に実現できます(docker を使わない場合は SG クライアントに相当するアプリケーションをダウンロード&インストールする必要があります):

①IBM Cloud 内に SG サービスインスタンスの作成
②社内ネットワークに SG クライアントの用意
③どの機密システムを利用するか、の設定

2021101505


では3つの段階を順に説明します。まずは①の「SG サービスインスタンスの作成」です:
2021101506




SecureGateway は IBM Cloud から提供されているサービスですが、無料の(アカウント作成時にクレジットカードを登録しない)ライトアカウントでは利用できません。SG 自体は無料でも利用できますが、ライトアカウントでは使えない、という点に注意してください。必要に応じてクレジットカードを登録するなどして、通常アカウントの権限で以下を作業する必要があります。

改めて通常アカウントで IBM Cloud にログインし、「リソースの作成」から "Secure Gateway" を検索して選択します:
2021101507


2021101508


利用プランが選択できますが、"Essential" プランを選択すると料金はかかりません(ただし接続先は1つのみ、一ヶ月間の通信データ量は 500MB 以下という制約があります)。最後に「作成」でサービスインスタンスを作ります。
2021101501


以下のような画面になれば SG のインスタンスが作成されたことになります。簡単でしたね:
2021101502


続いて②の SG クライアントを作成します。こちらは社内ネットワーク内に準備する必要があります:
2021101501


まずは①で作成した SG インスタンスの画面で「ゲートウェイの追加」をクリックします:
2021101502


ゲートウェイの名前を適当に入力して(下図では「自宅ネットワーク」)、「ゲートウェイの追加」ボタンをクリックします:
2021101503


以下のような画面になればゲートウェイが準備できました。ただ、この時点ではまだ①の SG インスタンス側に接続の準備ができただけで、社内ネットワークとは接続できていません(画面右上に赤いアイコンが表示されているのが未接続を意味しています)。社内ネットワーク側の準備をするために「クライアントの接続」をクリックします:
2021101504


以下のような画面が表示され、社内ネットワーク側で行う必要のある SG クライアントの準備手順が表示されます。SecureGateway では SG クライアントを以下の3種類から選ぶことができ、それぞれの手順が表示されています:
 - 専用アプリケーション(社内ネットワークに接続された PC にダウンロードしてインストール)
 - docker コンテナ(社内ネットワークに接続された docker エンジンで実行)
 - 専用ハードウェア(社内ネットワークに追加設置)
2021101505


本ブログエントリでは最も手軽と思われる docker コンテナを使った方法を紹介します。docker アイコンを選択すると、社内の docker エンジンで実行するためのコマンド(1行!)が表示されるので、この内容をコピーします:
2021101506


コピーした内容を社内の機密システムと同じネットワーク上で稼働する docker エンジン環境で実行します。

なお、先ほどの画面で表示されていた内容は
 $ docker run -it ibmcom/secure-gateway-client XXXXX

という内容でしたが、私が確認した環境では、
 $ docker run --net=host -it ibmcom/secure-gateway-client XXXXX

と、"--net=host" というオプションを付けないと正しく実行できませんでした。念のため記載しておきます:
2021101507


docker コンテナが正しく実行されると、画面に "Your Client ID is XXXXXX" と表示されます。この段階で SG クライアントも起動し、SG インスタンスとの接続も完了しています:
2021101508


念のため IBM Cloud 内の SecureGateway ダッシュボード画面を確認すると、クライアントが追加され、正常を示す緑色のアイコンが表示されているはずです:
2021101509


これで①と②が用意できました。続けて③の、具体的な機密システムまでの接続作業を行います。①、②ほど簡単ではありませんが、作業は個別のシステム内容に合わせて順に実行していくだけなのでそれほど難しいことはないと思っています:
2021101501


SG ダッシュボード画面で、まだクライアントを選択していない場合はこのタイミングで選択しておきます:
2021101502


SG クライアントが選択された状態になりました。ここで「宛先(0)」と表示されていることを確認します(登録されている機密システムが0個、という意味です)。ここに新しい機密システムを追加するため、宛先(0) のタブを選択後にプラスのアイコンをクリックします:
2021101503


「宛先の追加」ダイアログが表示され、この画面の質問に答えていく形で宛先を追加できます。まずは「ガイド付きセットアップ」が選択されていることを確認し、リソースの場所として(今回は社内ネットワーク内のラズベリーパイに接続することが目的なので)「オンプレミス」を選択し、「次へ」をクリックします:
2021101504


次に具体的な接続先の IP アドレスとポート番号を指定します。今回の例ではラズベリーパイへ SSH 接続することが目的なので、該当ラズベリーパイの IP アドレスと、SSH のポート番号を指定します。SSH ポート番号は通常 22 ですが、意図的に変更している場合は変更後のポート番号を指定します。

※ここでプライベート IP アドレスである 192.168.xx.xx というアドレスを指定している点に注目してください。プライベートネットワーク内の、プライベート IP アドレスを指定しているので、通常の方法ではインターネットからアクセスすることはできないシステムを指定しています。 また対象システムもウェブアプリケーションだったり、データベースだったり、ホストコンピュータだったり、いろんなパターンがあると思いますが、いずれの場合も IP アドレスおよびポート番号(対象によっては認証の仕組みも)を正しく指定できることが必要です。

その後「次へ」をクリックします:
2021101505


機密システムへ接続する際に用いるプロトコルを指定します。今回は SSH への接続なので「TCP」を選択します(機密システムがウェブアプリケーションであれば HTTP を選択するなど、実際の接続先に合わせて選んでください)。その後「次へ」をクリックします:
2021101506


次の画面で接続に利用する認証方式を選択します。今回の例では不要なので「None」を選択しましたが、ここも実際の接続先に合わせて選択します。「次へ」をクリック:
2021101507


設定後、この仕組みを利用可能な PC に IP アドレス指定をかけることが可能です。制約をかける場合はこの画面で対象 PC の IP アドレスを指定して追加します。特に不要であればそのままでも構いません。「次へ」をクリックします:
2021101508

※例えば機密のデータベースシステムを公開して、特定のパブリックなウェブアプリケーションから利用するような場合であれば、データベースそのものは誰でも使えるような形にはせず、対象のウェブアプリケーションサーバーからしか接続できないようにしたい、といった場合にそのウェブアプリケーションサーバーの IP アドレスを指定する、といった形で制約をかけます。


最後にこの宛先に名前を付けて保存します。下図の例では「ラズパイSSH」という名前を指定しています。そして「宛先の追加」をクリックします:
2021101501


1つ前の画面に戻り、指定した名前の宛先が追加されたことが確認できます。ただこの時点ではまだ宛先の右上に赤い印が表示されていて、接続許可が足りないことがわかります:
2021101502


というわけで、同機密システムへの接続許可を SG クライアントに指定します。docker 画面を再び表示して、以下のように(2回にわけて)入力します:
> A 192.168.0.101:22 1

> S

A(allow) コマンドに続けて 宛先のIPアドレス:宛先のポート番号 1 を指定します(最後の1はワーカーIDと呼ばれるもので、ここは1を指定します)。そして S(show) コマンドで設定内容を確認します:
2021101503


↑このように「宛先の追加」ダイアログで指定したものと同じ内容が docker コンテナの SG クライアントでも指定できていれば成功です。

この時点で先ほどまでダッシュボード画面に表示されていた赤いアイコンは消え、宛先である機密システムへの接続が確立できているはずです:
2021101504


ここまでが SecureGateway によるプライベートネットワークへの接続に必要な設定手順です。繰り返しますが、実際の接続先である機密システムや、この後接続時に使う PC への設定は一切不要です。また SG インスタンスや SG クライアントも非常に簡単に設定できている点がメリットだと考えています。


では実際にインターネットからプライベートネットワーク内の機密システム(=ラズベリーパイの SSH )に接続できるか動作確認してみましょう:
2021101505


まずはダッシュボード画面で接続時に必要な情報を確認します。宛先として作成したアイコンの右下の歯車をクリックします:
2021101506


以下のようなダイアログが表示されます。この「クラウド・ホスト:ポート」と表示されている部分にインターネットからアクセスすると、SG クライアントを通じて内部ネットワークから「リソース・ホスト:ポート」へ接続できるようになっています(この画面を確認したら右上の×をクリックして消します):
2021101507


改めてインターネットにアクセスできるPCから「クラウド・ホスト:ポート」で表示されている宛先に接続します。今回は接続先が SSH サーバーなので SSH クライアントを使って、ホスト名とポート番号を SG インスタンスのダッシュボードで表示されたものを指定して接続を試みます:
2021101508


今回は SSH への接続なので、この後で認証を求められます。内部ネットワークから利用する時と同じユーザー名とパスワード(場合によっては秘密鍵など)を入力します:
2021101501


正しい認証情報が入力できればログインが完了します。このアプリケーションの場合は接続先のホスト名がタイトルウィンドウに表示されますが、内部ネットワークの IP アドレスではなく、SG ダッシュボードで表示されたホスト名が表示されている点に注目してください。VPN を使わずに内部ネットワークとインターネットがトンネリングされ、専用のホスト名とポート番号だけで内部ネットワーク内の機密システムにアクセスすることができました:
2021101502


内部ネットワークからでないとアクセスできなかったシステムに無事にアクセスできました。SecureGateway は Essential プラン(無料)の場合は宛先を1つしか設定できませんが、別の有償プランにすることで1つの SG インスタンスで複数の宛先を指定することもできるようになるので、③の手続きを必要な宛先のぶんだけ繰り返すことで必要な社内ネットワークシステムへのアクセスが可能になります。

中小規模のリモートワーク環境を整える上で、手続きやコストで有力な選択肢になると思っているので、ぜひ試してみてください。

 

JavaScript でクリップボードを操作することは過去に何度かやっていたのですが、これまでは全てテキスト型の情報を扱っていて、バイナリ情報を扱ったことがありませんでした。

JavaScript でバイナリ情報をクリックボードにコピーすることができないのか? というと、そういうことはなく、とりあえず実現できそうだったので共有目的でブログを書きました。

具体的には以下のようなコードを実行することでクリップボードに画像がコピーされた状態を作ることができます。PNG 画像の情報が buffer 変数に入っている状態で、以下のコードを実行します:
  //. Canvas
  var canvas = document.getElementById( 'mycanvas' );
  if( !canvas || !canvas.getContext ){
    return false;
  }

  //. Canvas の内容を PNG 画像として取得
  var png = canvas.toDataURL( 'image/png' );
  png = png.replace( /^.*,/, '' );

  //. バイナリ変換
  var bin = atob( png );
  var buffer = new Uint8Array( bin.length );
  for( var i = 0; i < bin.length; i ++ ){
    buffer[i] = bin.charCodeAt( i );
  }
  var blob = new Blob( [buffer], { type: 'image/png' } ); //. イメージバッファから Blob を生成

  :

 try{
    navigator.clipboard.write([
      new ClipboardItem({
        'image/png': blob
      })
    ]);
  }catch( err ){
    console.log( err );
  }

最初にバイナリデータを Blob 型変数に変換して、ClipboardItem 型変数にしてから navigator.clipboard.write() を実行する、という流れです。最初のバイナリデータは HTML であれば Canvas などから取得したものを想定しています。


このコードを拙作のお絵描きアプリ MyDoodles ※にも実装してみました:
https://mydoodles.herokuapp.com/

※初回はサインアップしてアカウントを作成する必要があります。作成したアカウントでログインすることで PC やスマホでお絵描きが可能になりますが、今回はクリップボードを使う前提で紹介するので、この機能を試す場合は PC のブラウザからログインして、マウスやタッチパネルでお絵描きしてください。


線の色や太さを変えながら、適当なお絵描きをして、最後に「保存」します。保存処理の一部として、上述のクリップボードコピーが実行されます:
2021100701


保存できました。同時に画像がクリップボードにコピーされているはずです:
2021100702


そのまま画像をペーストできるアプリを開いてペースト(CTRL+V など)を実行すると、クリップボードにコピーされたお絵描き画像がペーストされます。画像は特に背景色を指定しない限りは背景が透明な状態でコピーされているので、ペーストすると透過背景の画像として表示されます:
2021100703



JavaScript でもバイナリデータをクリップボードにコピーできることが確認できました。

このページのトップヘ