【Docker】Docker Composeで踏み台サーバー経由のMySQLサーバーにphpMyAdminでつなぐ

タイトルの通り、Docker Composeで踏み台サーバー経由の[MySQL](https://www.mysql.com/)サーバーに[phpMyAdmin](https://www.phpmyadmin.net/)でつなぐ方法を記載します。

# 環境

この記事の作業は、以下の環境で実施しました。

- macOS Ventura (Intel)
- [Rancher Desktop](https://rancherdesktop.io/)

※Rancher Desktopを使用していますが、Docker Desktopでもおそらく問題ありません。

# 実現したいことと条件

- MySQLサーバーが2台（複数）ある
- MySQLサーバーはそれぞれ踏み台サーバーを経由する必要がある
- 踏み台サーバーはパスフレーズ付きの鍵認証である
  - 鍵については共通のものを使用
- MySQLサーバーをphpMyAdminで操作したい

# 構成

## ファイルを作成

以下のファイルを作成します。

## `.env`

使用する環境変数を以下のように定義します。以下の値は一例ですのでそれぞれの環境に合わせ書き換えてください。

  ```.env
  BASTION_KEY_FILE=~/.ssh/id_rsa
  BASTION_KEY_PASS=~/.ssh/id_rsa_pass
  BASTION1_HOST=bastion1.example.com
  BASTION1_USER=user1
  DB1_HOST=db1.example.com
  BASTION2_HOST=bastion2.example.com
  BASTION2_USER=user2
  DB2_HOST=db2.example.com
  ```

上記、環境変数の内容について説明します。

- `BASTION_KEY_FILE`
  踏み台サーバーのSSHの秘密鍵ファイルのパス

- `BASTION_KEY_PASS`
  踏み台サーバーのパスフレーズが書かれたテキストファイルのパス

- `BASTION*_HOST`
  踏み台サーバーのホスト名（IPアドレス）

- `BASTION*_USER`
  踏み台サーバーに接続するときのユーザー名

- `DB*_HOST`
  MySQLサーバーのホスト名（踏み台サーバーからアクセスできるもの）
    
## `docker-compose.yml`

以下に`docker-compose.yml`を以下のように定義します。
サービスは、2台の踏み台サーバー(`db1`,`db2`)、phpMyAdmin(`pma`)として定義しています。

  ```.yml
  version: "3"
  services:
    db1: &db_bastion_base
      image: alpine
      volumes:
        - $BASTION_KEY_FILE:/root/.ssh/id_rsa:ro
        - $BASTION_KEY_PASS:/root/.ssh/pass:ro
      environment:
        BASTION_HOST: $BASTION1_HOST
        BASTION_USER: $BASTION1_USER
        DB_HOST: $DB1_HOST
      healthcheck:
        test: "nc -zv localhost 3306"
        interval: 1s
        timeout: 10s
        retries: 3
        start_period: 1s
      command: >
        sh -c "
        apk add --update-cache --no-cache openssh-client sshpass &&
        sshpass -f /root/.ssh/pass -P 'Enter passphrase for key' ssh $$BASTION_USER@$$BASTION_HOST -L *:3306:$$DB_HOST:3306 -N -o StrictHostKeyChecking=no"
    db2:
      <<: *db_bastion_base
      environment:
        BASTION_HOST: $BASTION2_HOST
        BASTION_USER: $BASTION2_USER
        DB_HOST: $DB2_HOST
    pma:
      image: phpmyadmin/phpmyadmin
      ports:
        - 8096:80
      environment:
        PMA_HOSTS: db1,db2
        PMA_PORT: 3306
      depends_on:
        db1:
          condition: service_healthy
        db2:
          condition: service_healthy
  ```

- `&db_bastion_base`,`*db_bastion_base`
  `db1`と`db2`でほとんどの定義が共通です。そのためアンカーとエイリアスを使用し定義の重複定義を避けています。
  `&db_bastion_base`がアンカーで、`*db_bastion_base`がエイリアスです。なお、これはYAMLの仕様です。
  `environment:`以外は共通定義のため、`db2`では省略できるようにしています。

- `- $BASTION_KEY_PASS:/root/.ssh/pass:ro`
  ホスト機で定義されたパスフレーズが書かれたテキストファイルをマウントしています。各定義ファイルやコマンドライン引数で直接指定しないように配慮しこのようにしました。（イメージやコマンドライン履歴に残らないように）
  これが完全に安全な方法かはわかりませんが、比較的安全と思われるためこの方法にしました。
  パスフレーズを消してしまう方法も考えられましたが、その方法も避けました、

- `environment:`(`db1`,`db2`)
  `db1`,`db2`それぞれの接続情報の環境変数定義です。

- `healthcheck:`
  踏み台サーバーへの接続が確立しているか確認するための定義です。後述の`depends_on:`で使用しています。

- `command:`
  `apk`コマンドを使用し必要なツールをインストールしています。
  その後`ssh`コマンドを使用し踏み台サーバーへ接続しMySQLのポートのトンネルを作成しています。
  なお、その際にパスフレーズ入力が求められるため`sshpass`を使用し自動化しています。

- `PMA_HOSTS`
  複数のMySQLにつなぐため、そのホスト名（上記で定義した踏み台サーバーのサービス名）をカンマ区切りで指定します。
  この環境変数の変数名が複数形であることに注意してください。（1つでけホスト名を指定するための`PMA_HOST`もあります）

- `depends_on:`
  このサービスを起動するための依存関係を定義しています。条件を`service_healthy`とすることにより、上記で定義した、`db1`,`db2`の`healthcheck:`がパスした後にこのサービスが起動されるようになります。

# 最後に

[MySQL Workbench](https://www.mysql.com/jp/products/workbench/)使えばいいじゃんという意見もありそうですが、値の変更時にBIT型のものが`'1'`とかでクエリが生成されエラーになって更新されないという現象がありました。しかも表向きエラーになるわけでもなく気が付けないこともあったのがきっかけでこの方法を取ってみました。
これ以外にも色々なツールはありますが、なんだかんだでphpMyAdminが使いやすいというのもあります。（個人の感想です）