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

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

本ブログエントリは IBM Cloud アドベントカレンダー 2021 に参加しています。12/3 ぶんのネタです。

2021 年になった IBM Cloud 関連の変更の中でもインパクトの多かったものの1つがアカウント作成に関することだったと感じています。Bluemix 時代からの特徴でもあった新規に作成したアカウントでの Cloud Foundry ランタイム無料枠がなくなったことに加え、新規アカウント登録時にクレジットカードの登録が必要となりました。経緯など詳しくはソリューションブログを参照ください:

新規アカウント登録時にクレジット・カード情報の入力が必須になりました | IBM ソリューション ブログ


ちょっとわかりにくいのが「新規アカウント登録時」と同時にクレジットカードを登録する必要がある、という表現です。「IBM Cloud を使うにはアカウント(以前の言い方だと「組織」)が必要」であることはその通りなのですが、2021/11/08 時点では「クレジットカードを登録しないと使えない」というわけではありません。「自分のアカウントを登録せずに(ログインアカウントだけ作って)使う」ことができるのでした。これは上記 URL でも紹介されている方法で、例えば IBM Cloud を使ったハンズオンセミナーなどを開催したい場合に、主催者が利用者の使用料金を肩代わりする形で支払い、利用者はアカウントだけ作って無料で(クレジットカード登録も不要で)利用する、といったケースを想定しています。

ただ上記 URL ではその具体的なアカウント作成手順などが紹介されているわけではないため、ちょっとわかりにくい部分もあると感じました。というわけで、本ブログエントリではこのような使い方をする場合の(利用者の)アカウント作成手順を紹介します。なおスクリーンショットを含めた情報は 2021/12/03 時点のものであることをご了承ください。


【クレジットカード登録なしで IBM Cloud アカウントを作成できる条件】
クレジットカード登録せずに IBM Cloud アカウントを作成するには条件があります。そもそも 2021/12/03 時点で普通にここからアカウントを作成しようとすると、途中でクレジットカードの登録が求められます。スタート地点からして違う方法が必要だったりします。

その条件がこちら:
既存のベーシックアカウント(またはエンタープライズアカウント。要はクレジットカードを登録済みか請求書登録など、支払いの準備が済んでいるアカウント)からの招待を受けたメールアドレスで作成すること

やみくもにアカウントを新規に作成しようとすると、普通の方法でアカウントを登録することになり、この場合はクレジットカード登録が必要です。その方法ではなく、既にベーシックアカウントを所有している人からの招待を受けてアカウントを作成する場合のみクレジットカード登録を回避できる、というものです。なお、この方法で招待された人がアカウントを作成した場合、その人の利用によって料金が発生した場合は招待した人に支払いが請求される点に注意してください。あくまで「クレジットカード登録が不要」なだけで、無料で使えるわけではありません(使った分は招待した人が支払うことになるので、そこまで理解した上で招待してください)。


【既存アカウントからユーザーを招待する方法】
上述の点を注意した上で、では具体的に既存 IBM Cloud アカウントからユーザーを招待する方法を紹介します。繰り返しになりますが、この方法で招待されたユーザーが IBM Cloud の有償サービスを利用した場合は招待した人に請求される点に注意してください(無料で参加できるハンズオンセミナーの主催などを想定しています)。

まず招待する人が IBM Cloud にログインします。招待する人はクレジットカード登録が済んでいて、有償アカウント契約済みであると想定します。

ログイン後、画面上部のメニューに ID と一緒に自分の名前(組織)が表示されていることを確認します。多くのケースでここは1つしか選択肢がないので問題ないと思いますが、もし自分の名前とは異なる組織が表示されている場合は、赤枠部分をクリックして表示される選択肢から自分の名称を選択します(この後招待するユーザーが有償サービスを使った場合、最初にここで選択したユーザー組織に請求されることになります):
2021110901


自分の組織が選択されていることを確認した上で、その左横にあるメニューから 管理→アクセス(IAM) を選択します:
2021110902


アクセス権付与を行う画面に移動します。ここで画面左のメニューから ユーザー を選択すると、選択した組織を利用することができるユーザーの一覧が表示されます(初めてこの機能を使う場合は自分だけが表示されているはずです)。この組織を使うことができるユーザーを招待する形で登録します。画面右の ユーザーの招待 ボタンをクリックします:
2021110903


以下のようなユーザーを招待する画面が表示されます。ここから招待するユーザーのメールアドレスと、それぞれのユーザーに与える権限を指定して招待していきます。以下、順に説明していきます:
2021110904


まずは招待するユーザーのメールアドレスを入力します。複数の場合はカンマか改行で区切ります(1回で指定できる最大ユーザー数は 100 です)。入力したメールアドレスの数が画面右側にユーザー数として表示されるので、数が間違っていないことをここで確認します。このままだと権限の指定がまだなので(利用権限のない状態で招待されてしまうので)、まだ招待ボタンを押さずに作業を続けます:
2021110905


招待するユーザーへ与える権限を設定するため ユーザーへの追加のアクセス権限の割り当て と書かれた箇所をクリックして展開します:
2021110906


以下のような画面が表示されるので、アクセス権を与えたい内容にそって指定します。大きく(1)Cloud Foundry ランタイム (2)クラシック・インフラストラクチャー(IaaS) (3)IAM サービス (4)アカウント管理 の4つのカテゴリに分かれていて、それぞれ個別に指定します。デフォルトはいずれも「なし」なので、権限を与える必要のあるものだけを指定して設定していきます(例えば「アカウント管理」の設定をしない場合、招待されたユーザーはアカウント情報を設定することも見ることもできない、ということになります):
2021110907


単に「ハンズオンセミナーなどで使ってもらう」だけであれば、(4)アカウント管理 については設定不要です。後は使ってもらう有償サービスの内容に沿って権限を与えていきます。以下は(アプリケーションサーバーとなる) Cloud Foundry ランタイムと(マネージド・データベースなどの) IAM サービスだけを使ってもらう想定(IaaS サービスを使わせない想定)の例を紹介します。

まず(1)Cloud Foundry 利用権限を設定します。ここでは「ハンズオンセミナーで Cloud Foundry ランタイムを作って動かす」ことを想定して、ランタイムを作る権限まで含めて指定することにします。

画面から "Cloud Foundry" と書かれた四角を選択します:
2021110908


最初に Cloud Foundry を使う組織を選択します(端的に言うと「招待されたユーザーが使った分を誰が払うか」を指定します)。今回は(この作業を行っている)自分の組織に請求させるため、選択肢の中から自分のメールアドレスを指定します。

次に(招待するユーザーの)組織内での役割を指定します。ここではリソースの表示はできるが変更権限のない「監査員」を選択しています(実際の作業目的に合わせて指定します)。

更に「どのリージョン/スペースで、どのような権限を与えるか」を指定します。全てのリージョン/全てのスペースに、全ての権限を与えることも可能ですが、目的が明確になっている場合は、その目的のための最小限の権限を与えるのが安全です。以下の例では「シドニー」リージョンの「dev」スペースのみに「開発者」としての権限を与えています。 権限の指定ができたら最後に右下の 追加 ボタンをクリックします:
2021110909


追加ボタンをクリックすると、指定した権限が「割り当て」に追加されます。招待前であれば追加した内容を削除したり、編集することができます:
2021110910


今回は IAM サービスも利用できるような権限を付与します。ここも実際に作業する上で必要な権限を与えていくのですが、今回は特定のサービスだけを使わせるのではなく、全てのサービスを作成できるような内容で招待する例を紹介します。指定が完了したら 招待 ボタンをクリックします:
2021110911


追加ボタンをクリックすると、指定した権限が「割り当て」に追加されます。この作業を招待されたユーザーの権限として必要なだけ繰り返しますが、今回はここまでの権限を与えた状態でユーザーを招待することにします。画面右下の 招待 ボタンをクリックします:
2021110912


ユーザーを招待する側の作業はこれで終わりです。


【招待を受けたユーザーの登録方法】
続いて上記作業によって招待を受けたユーザー側の手順を説明します。

まずメールアドレスを指定されて招待されたユーザーには以下のようなメールが IBM Cloud から届きます。このメール本文内にある Join now と書かれたリンク部分をクリックします:
2021110901


IBM Cloud のユーザー登録ページに遷移します。

この時、該当ユーザー(メールアドレス)が既に IBM Cloud のアカウントに登録済みであった場合はメールアドレス、氏名とも既に既存の登録情報が入力されている状態で開き、「アカウントに追加」というボタンだけが押せる状態でページが開きます。この場合は アカウントに追加 ボタンをクリックすることで招待された組織が使えるようになります。

該当ユーザーがまだ IBM Cloud に未登録であった場合はメールアドレスのみ入力済みの状態でページが開き、氏名部分は入力可能なテキストフィールドになっています。ここに氏名を入力し、最後に アカウントに追加 ボタンをクリックし、次の画面でパスワードを指定します:
2021110902


※たまーに、ここでこの状態のままになるんですよね。。その場合はログイン画面に移動しちゃってください:
2021110903


IBM Cloud に未登録だったユーザーがこの手順で登録した場合のみ、IBM Cloud の使用許諾画面に遷移するので、内容を確認して続行してください。これで IBM Cloud に登録完了です:
2021110904


この方法で招待されたユーザーが IBM Cloud にログインすると、画面上部の組織アカウント一覧には招待してくれた人の名前が表示され、他の選択肢を選ぶことはできません。そしてこの状態でしか使うことができないため、このユーザーが有償サービスを利用した場合は組織アカウントのオーナー(つまり招待した人)に請求される、という仕組みとなります:
2021110905


普通にアカウントを登録するとクレジットカード登録が必須となってしまったので、企業内で社員向けのハンズオンセミナーを実施しようとしても各ユーザーがクレジットカード登録をしないといけないのでは面倒ですが、この方法であれば(一括でまとめて払う人の有償アカウント登録のみ必須ですが)アカウント作成時のハードルは以前通りの簡単な手順で登録できます。


コンテナ環境が広まっていくと、アプリケーションやサービスが安定運用できるようになる、という話を耳にします。それはそれで事実だと思うのですが、本当にサービスを安定運用するにはアプリケーションが使うバックエンドも安定運用が必要で、そのあたりの話が飛ばされていることが多いと感じることもあります。 今回のブログエントリはそういう話の例として、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 は実行途中の経過を返すものだったりして、実際の挙動という点ではこの続きがないと不自然な挙動となります。そのあたりまで考慮されているわけではない点をご了承ください。






このページのトップヘ