Docker on WSL2 環境構築メモ (2/x) - 設計戦略

前提

導入対象とするマシンは下記の通り。

  • Hardware : CPU = i10900, GPU = GeForce RTX 2080Ti, Mem = 32GB, SSD 1 TB + HDD 2TB
  • ベース OS : Windows 10 Pro バージョン 20H2 (ビルド 19042.685)
  • 仮想 OS : Ubuntu 20.04 LTS on WSL2
  • 環境構築メモ 3/3 を実施した状況

Docker 導入により目指すところ

  • 機械学習環境を構築する
  • アプリケーションやサーバサービスごとにスクラップ&リビルドをできるようにする
  • サービス移行・複製を容易にする (ユーザ定義データのポータブル化)

Docker 内部構成の設計戦略

Docker を解説する書籍は、概念や個々の手順は丁寧に記述されているものの、全体としてサーバサービスをどのように Docker 上に配置・構成すべきかは説明されていない。習熟のためにまず手順を踏んでみることは大事だが、その延長で設計なく環境を構築することはできない *1 ため、少し考えてみた。
赤字部分は Docker を構成する上で実務上は非常に重要であるのにあまり説明されていないところ。

  • サーバサービスごとに1つコンテナをつくる
    • DB サーバとして PostgreSQL サーバを立てる予定
    • Web サーバとして Apache サーバを立てる予定 (当面は必要ないが、将来は学習・推論・メンテの起動トリガーを Web にまとめる)
  • 機械学習の開発環境 (Tensorflow 他) で1つコンテナを作る
    • Tensorflow などはパッケージの想定バージョンが異なると動作しない可能性が高い
    • CPU 動作版、GPU 動作版をコンテナとして分離すべきかどうかは要検討
      (分離する場合は、コンテナイメージをレイヤー設計する必要があるかもしれない)
  • ユーザ定義データは(原則として)各コンテナに 1:1 対応するデータボリュームに格納する
    • 複数のアプリケーション・コンテナから接続されるデータボリューム・コンテナを構築しない
      (各アプリケーション・コンテナがデータボリュームをマウントするデータボリューム・コンテナとなり、そのデータボリュームに自身が扱うユーザ定義データを格納する)
    • ユーザ定義以外のデータはマウントポイントではなく当該コンテナを格納領域とする
      (設定ファイルやログ等は、サービスと一体として管理しないとスクラップ&リビルドした時にサービスやコンテナの状況と整合しなくなるため外部や共有のボリュームに吐き出さず、コンテナイメージとして管理する)
    • 単一のサーバサービスが複数のディレクトリを参照する場合は、複数のデータボリュームをマウントする必要があるかもしれない
  • コンテナおよびユーザ定義データ格納ボリュームは Ubuntu 外にバックアップする
    • ユーザ定義データ格納ボリュームは、データを入れる前の状態 (アクセス権等の初期設定を完了し、データ投入テストをした後に、テストデータをクリーンナップした時点) で1回スナップショットをとる
    • 残念ながら、現行の Windows 10 (20H2) や WSL2 では ext4 形式の外部パーティションを直接マウントすることはまだできないため、バックアップを NTFS 形式別パーティションにおくことにする *2

Docker イメージ & データボリュームのバックアップとリストア

サーバ構築時には手順の検証・確定が必要であり、スクラップ&ビルドするのが常である。Docker を採用する最大の理由がこのスクラップ&ビルドを効率化することである。設計戦略を検証するために、実際にバックアップとリストアをやってみる。

bitnami redmine イメージをサンプルとしてバックアップとリストアしてみる。Redmineプラグインを導入した時などに (バージョン齟齬により) よくサービス起動しなくなるため、頻繁にスクラップ&ビルドする。

前提

Docker Hub から bitnami/redmine イメージを取得する。当該イメージは Docker Compose であるため、coker-compose up -d でコンテナが簡単に立ち上がる。docker-compose.yml の内容は下記のとおり。mariadb がバンドルされており redmine はこれに依拠している。また redmine_data というデータボリュームを作成する。

version: '2'
services:
  mariadb:
    image: 'docker.io/bitnami/mariadb:10.3-debian-10'
    environment:
      - ALLOW_EMPTY_PASSWORD=yes
      - MARIADB_USER=bn_redmine
      - MARIADB_DATABASE=bitnami_redmine
    volumes:
      - 'mariadb_data:/bitnami'
  redmine:
    image: 'docker.io/bitnami/redmine:4-debian-10'
    environment:
      - REDMINE_DB_USERNAME=bn_redmine
      - REDMINE_DB_NAME=bitnami_redmine
    ports:
      - '80:3000'
    volumes:
      - 'redmine_data:/bitnami'
    depends_on:
      - mariadb
volumes:
  mariadb_data:
    driver: local
 redmine_data:
    driver: local

バックアップ

バックアップは簡単。

Docker コンテナのバックアップ

バックアップしたい断面で redmine コンテナを止め、イメージ化する。

docker-compose stop
docker commit {container id} bitnami/redmine:4-debian-10-yyyymmdd
Docker データボリュームのバックアップ

redmine コンテナを止めた断面で下記を実行する。実行前に redmine コンテナがマウントしていたデータボリュームを特定する。

docker run --rm -v {datavolume id}:/bitnami -v $PWD:/backup busybox tar zcvf /backup/bitnami.backup.tgz /bitnami

この1行でやっていることは、以下の通り。コンテナから入 (Docker 内データボリューム) と出 (Docker 外ディレクトリ $PWD) と2方向に接続しているのがポイント。

  • busybox コンテナ (データコンテナ) を走らせる
  • 当該コンテナから redmine のデータボリュームをマウントする
  • 同時に Docker 外のローカルディレクトリ $PWD を当該コンテナ内の /backup にマウントする
  • その後、Docker 内で /bitnami を /backup/bitnami.backup.tgz (コンテナ内 → コンテナ外) へ圧縮バックアップする
  • 処理が終わったら busybox コンテナを殺す

リストア

リストアは少し難しい。下手にやるとコンテナ内の情報が壊れ、redmine コンテナが再起動を繰り返す。

Docler データボリュームの初期化

下記の手順で Docker の初期イメージからデータボリュームの初期断面を復元する。

  1. docker-compose down で現在のコンテナを捨てる (捨てる前に docker-compose.yml を編集すると、停止やその他のコンテナ操作ができなくなる。)
  2. docker volume ps で残っているデータボリュームを特定し、docker volume rm {volume id} で削除する
  3. redmine を初期イメージで起動するように docker-compose.yml を編集する
  4. docker-compose up -d で初期イメージからコンテナを生成し、データボリュームを新規作成する
  5. docker-compose down でそのコンテナを捨てる
  6. redmine をバックアップ断面イメージで起動するように docker-compose.yml を編集する
    併せて、当該イメージがマウントしようとするデータボリュームを特定する
  7. 下記を実行する
docker run --rm -v {datavolume id}:/bitnami -v $PWD:/backup busybox tar zxvf /backup/bitnami.backup.tgz -C /bitnami

やっていることはバックアップの逆。

設計戦略の評価

事前に策定した戦略の1つ

  • ユーザ定義以外のデータはマウントポイントではなく当該コンテナを格納領域とする

は正しかったように思う。というのは、バックアップ&リストアをする際に、ある断面のイメージとデータボリュームをセットにして保管する必要があるからだ。両者に断面ズレがあるとサーバサービスが立ち上がらなくなる可能性が高い。

bitnami/redmine でも、断面ズレでリストアした場合、コンテナの再起動を永遠に繰り返していた。マウントポイントとなっているのは /bitnami だけだが、ツールやプラグインのインストールなどを行うと、システム設定はここ以外にもなされる。そして、ツール類を一定程度インストールした後のこのディレクトリのバックアップ・ファイルは tar で 20 KB、tar zip で 4 KB ほどだった。高々これだけのものを別管理する必要はない。

マウントに関する考察

Docker は -v オプションで Docker 外ディレクトリも Docker 内データボリュームでもマウントできる。バックアップ&リストアでの挙動を観察して、Docker はディレクトリ・マウントを下記のように実行している (と推測するが、裏付けをとったわけではない)。

  • Docker は (初期) イメージの中に環境の全ディレクトリ構造を保持している
  • docker run 時に -v オプションは下記を実行する

参考記事で読んだが、Docker はコンテナ内部のディレクトリ構造も、データボリューム内部のディレクトリ構造も、(そして当たり前だが Docker 外ディレクトリへの参照も) シンボリックリンクで実現し、実環境上のどこかに格納されているあるディレクトリ・ノード (以降) を Docker コンテナ内においては / (以降) とみなすフルパス・エミュレーションにより環境を分離しているらしい。だから busybox などのデータコンテナは非活性でもワークする。そうであれば、上述の推測は外れてはいないのではないか。

汚さないための環境隔離/コンテナ仮想化であるため、-v オプションで指定したマウントポイント以外に外界との接点はないはず。ならば、やるなら / 以降の (ユーザ定義データを除く) すべてをデータボリュームへ格納するか、(ユーザ定義データを除く) すべてをコンテナ内へ閉じ込めるか *3 だ。

フルパス・エミュレーションなら前者と後者で本質的な違いはなく、データボリュームは複数コンテナ間で共有できることしか意義がない。しかし環境分離の観点からすると、これまたユーザ定義データ以外を共有格納する利点がない。

*1:これが環境構築に二の足を踏んでしまう最大の阻害要因。取り回しのしやすさや将来拡張性などを考えていると初手を打てなくなる。

*2:ext4 形式外部パーティションの直接マウントができるようになったら、それはそれで悩むことになる。virtulized ext4 on NTFS on SSD と native ext4 on HDD のどちらが性能面・耐久性面・耐障害性面トータルで効率的か。

*3:ただし、コンテナ起動とともにサービスが立ち上がり何らかのファイルがロックされるため、コンテナ内へ閉じ込めるとディレクトリの一部または全部を抽出できない。抽出するためにはサービスの停止方法を知っておく必要がある (が、ほいほいお手軽イメージ を Docker Hub から pull すると大抵この辺のことが解説されていなくて困る)。