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

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

タグ:docker

2月も中旬に差し掛かろうとするこのタイミングで今年最初のブログエントリです。サボり癖を付けないようにしないと。。

久しぶりに phpMyAdmin を使ってみました。MySQL を使っている環境で開発していると、この手の GUI クライアントアプリが欲しくなる機会は多いはずです。アプリ開発でなくても WordPress のような CMS を使っている場合でも、こういった DB に直接アクセスできるツールがあるとちょっとしたデータ変更などにも便利ですよね:
2026021100


私自身は 10 年以上前に WordPress のカスタマイズをする機会が多かったこともあり、phpMyAdmin のヘビーユーザーでした。当時はまだ「コンテナ」の知識も乏しく、物理/仮想サーバーに直接 PHP や httpd 、そして phpMyAdmin をインストールして使ってました。今回はイメージの便利さを覚えたこともあり、モジュールを直接インストールするのではなく、DockerHub で見つけた公式 phpMyAdmin イメージで挑戦してみました(ついでに MySQL も DockerHub の公式イメージを使い、docker compose を使って構築してみました)。

【ただ普通に使う】
ただ普通に DockerHub の公式イメージを使って MySQL と phpMyAdmin を起動するならこのような docker-compose.yaml を用意するだけです:
version: '3'

services:
  db:
    image: mysql:5.7
    volumes:
      - ./mysql:/var/lib/mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root_passw0rd
      MYSQL_DATABASE: mydb
      MYSQL_USER: myuser
      MYSQL_PASSWORD: mypass

  phpmyadmin:
    depends_on:
      - db
    image: phpmyadmin
    ports:
     - "3001:80"
    restart: always
    environment:
      - PMA_ARBITARY=1
      - PMA_HOSTS=mydb
      - PMA_USER=myuser
      - PMA_PASSWORD=mypass

上の例では MySQL の root パスワードを root_passw0rd、作成する DB 名は mydb 、アクセスするユーザーは myuser で、そのパスワードは mypass としています(同じユーザーとパスワードで phpMyAdmin からも接続しています)。なので phpMyAdmin で実行できる内容はこのユーザーに与えられた権限次第、ということになります。MySQL は ID とパスワードだけで接続できるよう、MySQL 5.7 を指定して利用します。

また docker compose 実行時のディレクトリに mysql/ というフォルダを作って、MySQL からボリュームマウントしています(MySQL コンテナを再起動してもデータが残るようにしています)。

また phpMyAdmin は 3001 番ポートから接続できるようポートフォワードしました。

この docker-compose.yaml をカレントディレクトリに用意し、mysql/ ディレクトリも作った上で以下のコマンドを実行すると MySQL と phpMyAdmin がコンテナとして起動し、3001 番ポートを指定してブラウザでアクセスすると phpMyAdmin の GUI にアクセスできます:
$ docker compose up -d

2026021101


ここまではさほど難しくありません。


【何が問題か?】
さて、上記までは特別な要素はなくて、一般的な docker-compose と MySQL, phpMyAdmin の知識だけで実現できる内容でした。ところがこのままでは都合悪いケースが結構あります。

なんといっても、URL だけで phpMyAdmin が利用できるということは「認証なしに(データベースのユーザーIDやパスワードを知らなくても)データの内容にアクセスできてしまう」ことになります。例えば MySQL 側であるデータベースに対する参照(SELECT)権限だけを持ったユーザーを作り、そのユーザー ID を docker-compose.yaml 内に記載すれば、phpMyAdmin はそのユーザー権限で使うことになるため、参照しかできない(データの作成や更新、削除はできない)という制約をかけることはできます。ただそれでも、パスワードに相当するセキュリティ要素無しにデータの内容が見れてしまうのは考え物です。

そういった場合の設定として、多くのケースでは phpMyAdmin を開く前に認証を求めるような設定を追加します。代表的な1つとして、ベーシック認証と呼ばれる ID とパスワードの組み合わせを追加するケースも珍しくありません。

ところが phpMyAdmin でベーシック認証を加えるには phpMyAdmin 側というよりも HTTP サーバー側にそのための設定が必要です。物理/仮想サーバーを使って MySQL や phpMyAdmin を用意した場合は同時になんらかの HTTP サーバーもインストールしているはずなので、その中にベーシック認証を使うための設定をすればいいのですが、今回のように phpMyAdmin のコンテナイメージを使っていると、ある程度コンテナの中身も理解していないと、そのためのカスタマイズが困難です。

以下はそのための、つまり phpMyAdmin の公式コンテナイメージを使って HTTP サーバー部分にベーシック認証を追加するための設定方法です。


【phpMyAdmin にベーシック認証をかける】
上述のように phpMyAdmin 公式コンテナでベーシック認証を有効にするには、コンテナイメージ内の HTTP サーバー部分でベーシック認証を有効にする必要があります。具体的には docker-compose.yaml 内で phpMyAdmin を利用する際の HTTP サーバーにベーシック認証を追加するようにイメージをビルドし直してからデプロイするようにカスタマイズします。

そのための手順を以下で紹介します。まず docker-compose.yaml と同じディレクトリにあらかじめ以下3つのファイルを用意しておきます:
・.htpasswd
・.htaccess
・Dockerfile.phpmyadmin

".htpasswd" ファイルはベーシック認証のユーザー ID とパスワードの組み合わせを定義するファイルです。具体的には Linux のターミナルで docker-compose.yaml と同じディレクトリに移動し、以下のコマンドを実行して作成します(実行するとパスワードの入力を求められるので、確認含めて2回パスワードを入力します):
$ htpasswd -c .htpasswd (ユーザーID)

仮にユーザー ID を "admin"、パスワードを "P@ssw0rd" とすると、以下のような内容の .htpasswd ファイルが作成されます(ユーザー ID である admin はそのまま表示されていますが、パスワード部分はエンコードされたものになります):
admin:$apr1$OaTHdA4B$vGmbnAxkB5b./q5zXPJHN.

もう1つのファイル .htaccess は以下の内容で用意します:
AuthType Basic
AuthName "Please enter your password"
AuthUserFile /var/www/html/.htpasswd
Require valid-user

"/var/www/html/.htpasswd" ファイルを参照してベーシック認証を実施し、その中に書かれたユーザーID/パスワードと一致しないと認証エラーになる、という内容です。

最後のファイル Dockerfile.phpmyadmin には phpMyAdmin コンテナのビルド内容を記述します。具体的には以下の内容そのままで用意します:
FROM phpmyadmin
COPY ./.htaccess /var/www/html/.htaccess 
COPY ./.htpasswd /var/www/html/.htpasswd

Dockerfile を書いたことがあると内容の意味も理解できると思います。これは「phpMyAdmin (公式コンテナイメージ)をベースに、(上で用意した).htaccess ファイルをコンテナの /var/www/html/.htaccess に、.htpasswd ファイルをコンテナの /var/www/html/.htpasswd ファイルにそれぞれコピーする」という内容が記述されています。

最後に docker-compose.yaml ファイルを一部書き換えて上記のビルドが実行されるようにします:
version: '3'

services:
  db:
    image: mysql:5.7
    volumes:
      - ./mysql:/var/lib/mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root_passw0rd
      MYSQL_DATABASE: mydb
      MYSQL_USER: myuser
      MYSQL_PASSWORD: mypass

  phpmyadmin:
    depends_on:
      - db
    build: 
      context: ./
      dockerfile: Dockerfile.phpmyadmin
    image: phpmyadmin
    ports:
     - "3001:80"
    restart: always
    environment:
      - PMA_ARBITARY=1
      - PMA_HOSTS=mydb
      - PMA_USER=myuser
      - PMA_PASSWORD=mypass

docker-compose.yaml ファイルに上記赤字部分を追加します。ここでは「Dockerfile.phpmyadmin を使ってコンテナをビルドする」よう指示されています。

ここまでの変更を行って、再度 "docker compose up -d" を実行すると、今度は phpmyadmin コンテナをデプロイする前に上述のビルドが実施され、HTTP サーバーにベーシック認証が追加された状態でデプロイされます。

ここまでの手順が正しく実行されていれば、次に 3001 番ポートを指定してブラウザでアクセスすると、ベーシック認証が求められ、正しいユーザーIDとパスワード(上記例の場合は admin と P@ssw0rd)を入力しないと phpMyAdmin GUI は表示されません:
2026021102


最低限、このようなベーシック認証で守られていれば URL が分かってもデータの内容にはアクセスできなくなります。MySQL データを GUI で見ることができる phpMyAdmin は便利ですが、インターネットアクセスを許可する場合は最低限このような方法でデータを守る必要があります。最近はコンテナ環境を使うケースも増えていると思うので、そんな人の参考になれば。


自分で作って公開しているサービスの中で使うために MQTT over WebSocket サーバーが必要になり、DockerHub の eclipse-mosquitto イメージを使って同環境を構築したので、その手順を紹介します。
2023040900



【MQTT over WebSocket とは】
「WebSocket(HTTP) プロトコルで実現する MQTT」です。MQTT ブローカーに対して Publisher や Subscriber の機能をウェブのクライアント(=ブラウザ)から使いたい場合に、ウェブクライアントから直接 MQTT を使うことはできませんが、HTTP ベースである WebSocket 経由で使うことができます(そのためのライブラリも公開されています)。このような「WebSocket 対応した MQTT 」が MQTT over Socket で、そのような MQTT ブローカー環境を構築するための手順を以下で紹介しています。


【事前準備】
今回は MQTT ブローカーとして DockerHub から公式イメージとして公開されている eclipse-mosquitto を使い、これを MQTT over WebSocket 対応させます。 というわけで eclipse-mosquitto イメージが対応した docker 環境(linux/386, linux/amd64, linux/arm/v6, linux/arm64/v8, linux/ppc64le, linux/s390x)を事前にご用意ください。


また以下のような mosquitto.conf ファイルが必要です。これは mosquitto の動作設定用ファイルなのですが、これとは別の設定が必要な場合は、それらの設定の最後にこの4行の内容を追加して mosquitto.conf ファイルを用意してください(ここでは /tmp/mosquitto.conf というファイルパスで作成したものとします):
listener 1883
allow_anonymous true
listener 11883
protocol websockets

※ちなみにこの4行が意味する設定内容は以下の通りです。この設定を変える場合は(数値などを)適宜変更してください:

  • 1883 番ポートで MQTT を送受信する(変更せずこのまま)
  • 11883 番ポートで MQTT over WebSocket を送受信する(使っていないポート番号なら任意)
  • 認証無し

docker コンテナの起動だけであれば以上の準備でいいのですが、後述の動作確認までを行う場合は Node.js の実行環境が必要になります。必要な場合は Node.js のインストールまで済ませておいてください。



【起動】
ここまでの準備ができていれば起動は簡単です:
$ docker run -d -p 1883:1883 -p 9001:9001 -p 11883:11883 -v /tmp/mosquitto.conf:/mosquitto/config/mosquitto.conf --name mosquitto eclipse-mosquitto

外部に公開するポートとして 1883, 9001, 11883 を指定し、用意した mosquitto.conf ファイルを使って eclipse-mosquitto イメージを実行しています。


【動作確認】
以下のサンプルソースコードを使って動作確認します:
https://github.com/dotnsf/MoWS

このアプリケーションそのものは「簡易 SNS」です。タイトルとメッセージを編集&送信すると、その内容を一覧画面で確認できます。一覧画面をリロードするとその時点までに送信されたメッセージの一覧が表示されますが、一覧表示後に新しいメッセージが送信された場合、通常のアプリであればリロードするまでは新しいメッセージは表示されないのですが、このサンプルアプリでは MQTT over WebSocket を併用することで新しいメッセージが自動的に画面最上部に追加されるようになっています。


実際に動かしてみましょう。"git clone" 等でソースコードをダウンロードします。そしてルートフォルダ直下にある .env ファイルをテキストエディタで開き、以下の内容に書き換えて保存します:
MOWS_URL=ws://xxx.xxx.xxx.xxx:11883
MQTT_URL=mqtt://xxx.xxx.xxx.xxx
MQTT_PORT=1883
MQTT_TOPIC=yellowmix/mytest/topic

この内容の、"xxx.xxx.xxx.xxx" 部分は上で起動した MQTT ブローカーコンテナの IP アドレス、11883 は同ブローカーで MQTT over WebSocket が待ち受けるポート番号、1883 は MQTT が待ち受けるポート番号です。また MQTT_TOPIC には適当なトピック文字列を指定してください(よくわからなければこの例の内容そのままでも構いません)。

※なおデフォルト状態の MOWS_URL は "wss://test.mosquitto.org:8081" と設定されているので、プロトコル部分も "wss" から "ws" に変更する必要がある点に留意してください。

そして動作確認用アプリケーションを起動します:
$ npm install
$ node app

アプリケーションは 8080 番ポートで起動します。ウェブブラウザを2つ(同じブラウザの別ウィンドウでも可)を開いて、1つは一覧画面である "http://localhost:8080/" にアクセスします:
2023040901


そしてもう1つは編集画面である "http://localhost:8080/edit" にアクセスします:
2023040902


編集画面内の「タイトル」欄と「本文」欄に適当な文字列を入力します。最後に「送信」ボタンをクリックします:
2023040903


全て正しく動作していると、一覧画面をリロードしていなくても、編集画面で入力されたメッセージが一覧画面に表示されているはずです。編集画面に入力された値はバックエンド側で MQTT パブリッシュされ、MQTT over WebSocket でサブスクライブしていた一覧画面に内容とともに通知され、リロードしていなくてもその内容が表示できています:
2023040904


同様にして編集画面から送信するたびに一覧画面が更新されていく様子が確認できます:
2023040905


ちなみに送信されたメッセージはアプリケーションが起動している間はメモリ内に保存されているので、一覧画面をリロードするとそれまでに送信されたメッセージが新しい順に並ぶ様子も確認できます(データを適当な DB に永続保存して、そこから読み出すように改良すれば一応の SNS っぽくなると思います)。MQTT over WebSocket を使って一覧が自動更新される簡易 SNS が実現できている様子を確認できているはずです。


【まとめ】
趣味アプリの1つとして SNS っぽいものを作っているのですが、その中で要件としてでてきた「一覧の自動更新」機能を実現するための手段として MQTT over WebSocket を使う例を紹介しました。ウェブブラウザの JavaScript で実装された MQTT over WebSocket ライブラリを使って MQTT サブスクライブを実装することで比較的簡単に実現できることがわかりました。


コンテナ環境が広まっていくと、アプリケーションやサービスが安定運用できるようになる、という話を耳にします。それはそれで事実だと思うのですが、本当にサービスを安定運用するにはアプリケーションが使うバックエンドも安定運用が必要で、そのあたりの話が飛ばされていることが多いと感じることもあります。 今回のブログエントリはそういう話の例として、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 を利用する際にも応用できる内容です。 利用するインフラプラットフォームによってはこういったメンテナンス時の再接続方法について特殊な機能が用意されていることもあるので、必ずしもこういう方法をとらないといけない、というわけではないのですが、一つのベストプラクティス的な内容だと思っています。

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


タイトルどおりの内容のエラーが発生したので、その原因と解決策を探った記録をブログにまとめました。

環境としては以下の図のようなものです。1台の Windows 10 PC の中に Docker Desktop を導入・起動し、MySQL サーバーのイメージからコンテナを作って 3306 番ポートで公開起動しました。この環境に WSL(2) の MySQL CLI を使って、localhost:3306 の MySQL サーバーにログインする、というだけの内容なのですが、ログイン時にコネクションエラーが発生してしまう、という症状が出ていました:
20210612


具体的に紹介します。今回使った MySQL のコンテナイメージはこちらです。厳密には mariadb を使っています。同様の現象が発生すると思うので、他の MySQL 系イメージでも構いません:
https://hub.docker.com/r/linuxserver/mariadb


Docker Desktop を使ってこのイメージからコンテナを作成します。WSL を起動するなどして docker コマンドが使える状態で以下のコマンドを実行します(目的は Docker Desktop 内のコンテナとして起動することなので、このコマンド自体は WSL から実行しなくても構いません):
$ docker run -d --name=mariadb -e PUID=1000 -e PGID=1000 -e MYSQL_ROOT_PASSWORD=root -e TZ=Asia/Tokyo -e MYSQL_DATABASE=mydb -e MYSQL_USER=user -e MYSQL_PASSWORD=P@ssw0rd -p 3306:3306 --restart unless-stopped linuxserver/mariadb

こんな感じで、Docker Desktop 内のコンテナとして MySQL が起動します:
2021061301


指定したオプションはコンテナイメージのドキュメントを参考に指定しています。上の例では root のパスワードは root 、利用ユーザーは user 、利用ユーザーのパスワードは P@ssw0rd 、利用データベース名を mydb、コンテナ名は mariadb、などを指定しています(適当に変更いただいても構いません)。ポート番号に -p 3306:3306 を指定しているので、ローカルホストの 3306 番ポートからも(公開されているので)アクセスできるはずです。事前準備はここまで。

ではこの方法で Docker Desktop 内に起動した MySQL(mariadb) に、WSL から MySQL CLI で接続してみます。上述のオプションであれば以下のコマンドで接続できるはずです:
$ mysql -u user -pP@ssw0rd -h localhost mydb

しかし実際には以下のようなエラーが表示されてしまいます:
$ mysql -u user -pP@ssw0rd -h localhost mydb
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2 "No such file or directory")

ユーザー名やパスワード、データベース名は間違っておらず、接続先も localhost だから間違えようがないはずです。ポート番号もデフォルトの 3306 のままなので特別なオプションも不要のはず・・・ ではなぜエラーになってしまうのでしょうか?


改めてエラーメッセージをよく見ると "Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock'" と表示されています。ちなみにこのファイルは存在していません(/var/run/mysqld というディレクトリが存在していません):
$ ls -la /var/run/sqld/mysqld.sock
ls: cannot access '/var/run/sqld/mysqld.sock': No such file or directory

このエラーメッセージでわかる人もいると思うのですが、MySQL コマンドは localhost が対象の場合はデフォルトで「ソケット接続」という方法で接続を試みます。そしてこのソケット接続をする場合の情報を /var/run/sqld/mysqld.sock というファイルで管理しているため、このファイルを読み込もうとしているのでした。

しかし、今回の環境では WSL 内には MySQL サーバーは起動していません。WSL 内からの docker コマンドで起動してはいますが、実体は Windows にインストールされた Docker Desktop のコンテナとして起動しています。要は「WSL と同じマシン上で動いてはいて、ポートも公開されているけど、WSL から直接ソケット接続できる状態で動いていない」のです。これがエラーの原因で、エラーメッセージの理由でもあります。

では、エラーなしに WSL から MySQL に接続するにはどのようなコマンドにすればいいでしょうか? 答はシンプルで「ソケット接続ではなく、TCP 接続するようなオプションを指定」することで解決できます。具体的には以下のように --protocol=TCP オプションを付けて実行します:
$ mysql -u user -pP@ssw0rd --protocol=TCP -h localhost mydb
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 11
Server version: 10.4.18-MariaDB-1:10.4.18+maria~bionic-log mariadb.org binary distribution

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [mydb]>

今度は無事に接続できました。「localhost の罠」にかかった時の話でした。


docker を使って複数の WordPress 環境を立ち上げる手順をスクリプト化してみました。

普通に1つの WordPress 環境を作るだけであれば(特に docker-compose を使えば、yaml ファイルを1つ用意するだけで)簡単に作れます。詳しくはここでは紹介しませんが、"docker WordPress" などでググると多くの紹介ページが見つかります。

ただ今回自分が作りたかった環境はこれらとは少し異なり、1つのホスト内に複数の独立した WordPress 環境を作る、というものでした。具体的には MySQL サーバーは1つだけ用意した上で、ポート番号で分離して1つ目の環境は localhost:8081 で、2つ目の環境は localhost:8082 で、・・・といった具合に、それも簡単に後から WordPress 環境を追加/削除できるよう考慮してスクリプト化して公開しました:
https://github.com/dotnsf/docker-wordpress

2021030700


※ここで方法で作成した WordPress 環境を実際にインターネットに公開する場合は DNS と連動したポートフォワーディングができる環境があれば、全ての WordPress 環境に 80 番ポート(http)や 443 番ポート(https)でアクセスできるようになると思っています。が、そちらについては環境依存になるので本ブログでは触れません。


利用方法については README.md で紹介していますが、一応ここでも説明します:

まず前提条件として docker が導入された環境が必要です(docker-compose は使わないので導入する必要はありません)。また用意したシェルスクリプトは Linux などの bash などで動かすことを想定しています。ただ docker コマンドが使える環境下であれば、(例えば Windows であれば、シェルスクリプトファイルの拡張子を .sh から .bat などに変更するだけで)使えるはずです。 なお、以下の内容については Windows10 の WSL2(Ubuntu 18.04) 環境で動作を確認しています。


docker 導入済みのシステムで docker を起動後、最初に MySQL イメージと WordPress イメージをダウンロードしておきます。実際には後述のシェルスクリプト実行時にダウンロードされていないと判断されれば docker が自動で最新イメージをダウンロードした上で実行してくれるので、この手順は必須ではありません。ただ最初に1回実行しておくことで後述のスクリプトが軽快に動くようになるので特に理由がなければこのタイミングでダウンロードしておくことを推奨します:
$ docker pull mysql

$ docker pull wordpress

次に今回の作業用に用意したシェルスクリプトをダウンロードして、実行可能な状態に設定します:
$ git clone https://github.com/dotnsf/docker-wordpress

$ cd docker-wordpress

(UNIX 環境の場合)
$ chmod 755 *.sh

(Windows 環境の場合 以下、拡張子を .sh から .bat に変更して実行)
> ren *.sh *.bat

まず docker 環境でデータベースである MySQL サーバーを起動します(以下のコマンドで 3306 番ポートで MySQL サーバーが起動します):
$ ./docker_run_mysql.sh

なお、この MySQL コンテナに接続してコンテナの中身を確認する場合は、以下のコマンドでターミナルにアタッチ可能です(exit で元のホストに戻ります):
$ docker exec -it mysql /bin/bash

そして docker 環境内に WordPress サーバーを起動します。この際に「何番目の WordPress 環境か」を意味するインデックス番号をパラメータとして指定します(以下の例では 1 を指定しています):
$ ./docker_run_wordpress.sh 1

このコマンドが成功すると、8081 番ポートで wordpress1 という名前の docker コンテナが起動します。ウェブブラウザで http://localhost:8081/ にアクセスすると、WordPress の初期設定画面に遷移して、サイト名やログイン設定などを指定して利用を開始できます:

2021030701


2021030702


2021030703


2021030704


2021030705


2021030706


とりあえず1つ目の WordPress 環境はこれだけで作れました。次の環境を作ることもできますが、この1つ目の WordPress 環境に関する操作を一通り説明しておきます。

この WordPress コンテナに接続してコンテナの中身を確認する場合は、以下のコマンドでターミナルにアタッチ可能です(exit で元のホストに戻ります):
$ docker exec -it wordpress1 /bin/bash

コンテナを停止する場合は以下のコマンドを実行します:
$ docker stop wordpress1

停止したコンテナを再起動する場合は以下のコマンドを実行します:
$ docker start wordpress1

停止したコンテナを削除する場合は以下のコマンドを実行します(コンテナを削除し、MySQL データベースも drop してデータごと削除します):
$ ./docker_rm_wordpress.sh 1


では1つ目の WordPress 環境を起動したまま、2つ目の WordPress を追加で起動してみます。以下のコマンドを実行します:
$ ./docker_run_wordpress.sh 2

このコマンドが成功すると、8082 番ポートで wordpress2 という名前の新しい docker コンテナが起動します。ウェブブラウザで http://localhost:8082/ にアクセスすると、新しい WordPress の初期設定画面に遷移し、同様にサイト名やログイン設定などを指定して利用を開始できます(それぞれが独立した環境なので起動済みの WordPress1 側には変化や影響はありません):

2021030707


2021030708


2021030709


wordpress2 のコンテナについても wordpress1 環境同様に docker コマンドで操作可能です。

後は必要なだけこの操作を繰り返すことで、WordPress 環境をコマンド1回ずつ追加していくことができます。ポート番号が利用中でなければ、おそらく好きなだけ起動できるはず(ポート番号が 8081 から始まるルールを変更したい場合は docker_run_wordpress.sh スクリプト内で適当な値に変更してください)。


そこそこのスペックを持った docker 導入済みのサーバーが1台インターネット上にあれば、DNS やポートフォワーディングなどと組み合わせることで複数の WordPress 環境を好きなタイミングで好きなだけ簡単に構築することができるようになると思っています。docker 環境なのでコンテナごと消してしまえば元のホスト環境を汚すこともなく元に戻せます(開発環境だと大事!)。

なお Github 上に公開したシェルスクリプトはそのまま利用することができますが、特にインターネットに公開する WordPress の場合、セキュリティの観点からパスワード類は変更してから利用することを推奨します。その場合は以下の部分を(すべて同じ文字列に)変更してください:
(docker_run_mysql.sh ファイル内)
- docker コマンド実行時の環境変数 MYSQL_ROOT_PASSWORD の値

(docker_run_wordpress.sh ファイル内)
- docker コマンド実行時の環境変数 WORDPRESS_DB_PASSWORD の値

(docker_rm_wordpress.sh ファイル内)
- docker コマンド実行時のパラメータ -p に続いて指定されている文字列

このページのトップヘ