Docker on WSL2 環境構築メモ (6/x) - Jupyter Notebook への .NET Interactive 追加導入

前提

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

  • 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
  • Docker on WSL2 環境構築メモ (4/x) の手順を終えている

Jupyter コンテナへの .NET Interactive (C# / F# / PowerShell) および Jupyter Lab 導入

Jupyter 上で C# 9.0 と Python を同時に利用したいため、Docker on WSL2 環境構築メモ (4/x) - Tensorflow (CPU 版) 導入 - Crayon's Monologue で作成した Tensorflow + Python3 + Jupyter のコンテナへ .NET Interactive を追加導入する。Jupyter で複数言語を扱うなら Notebook ではなく Lab にしておいた方が便利であるため、Lab も追加導入する。

起動ユーザ (UID) 指定がなければ docker は既定では root 権限でコンテナを起動する。ユーザを切り替えてインストールするものが一部あるが、該当箇所までの一連の手順は root 権限で行う。

実施前確認

追加導入するもの

jupyter labpip install
nodejsapt installバージョン 12.0 以上を求められる
wget
dpkg
apt install
apt-transport-httpsapt install
dotnet-sdk-5.0apt installバージョン 5.0 が利用可能
dotnet-interactivedotnet tool installバージョン 1.0.210803 が利用可能
dotnet-interactive Jupyter Kerneldotnet-interactive install

オリジナルの docker-compose.yml
version: '2'
services:
  tensorflow:
    image: 'tensorflow/tensorflow:latest-py3-jupyter'
    restart: always
    ports:
      - 'XXXX:8888'
    volumes:
      - /mnt/... XXXXX .../JupyterNotebooks:/tf
    command: /bin/bash -c "source /etc/bash.bashrc && jupyter notebook --notebook-dir=/tf --ip 0.0.0.0 --no-browser --allow-root --NotebookApp.token='XXXXXXXXXXXXXX'

Jupyter Lab インストール手順

Jupyter Lab インストール手順

ベースとしている tensorflow/tensorflow:latest-py3-jupyter イメージが pip ベースであるため pip をアップデートをしてから Lab をインストールする。

#install
pip install --upgrade pip
pip install jupyterlab

# comfirm
jupyter lab --help

引数を同じにして jupyter notebook の代わりに jupyter lab をすれば Jupyter Lab が起動する。(後ほど、コンテナのスナップショットをとって) docker-compose.yml の command: を書き換える。

node.js インストール手順

Jupyter Lab を快適に使うために Jupyter Lab Extension を入れたくなる。その際に node.js が必要になるため、あらかじめインストールしておく。

cd ~
curl -sL https://deb.nodesource.com/setup_12.x | bash -
apt update && apt upgrade
apt install nodejs

.NET 5.0 インストール手順

作業準備として .NET 5.0 パッケージをインストールする。

# install wget & dpkg 
apt update && apt upgrade
apt install wget && apt install dpkg

# download package management file
cd ~
wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
dpkg -i packages-microsoft-prod.deb

# install apt-transport-https
apt update && apt upgrade
apt install apt-transport-https -y

# install dotnet sdk 5.0 & dotnet-interactive
apt update && apt upgrade
apt install -y dotnet-sdk-5.0

ユーザ設定手順

ここから先はユーザ依存の環境構築になる。root でも構築できるが、Jupyter Lab はターミナルコンソールも起動可能であり docker コンテナ内を root で何でもできてしまうようになるため、Jupyter 起動用のユーザを作成し、そちらで docker を起動するようにする。

# 1) change root password for security
passwd

# 2) make a new user
adduser {user}
cat /etc/passwd       # check the new user's UID and GID for later use

# 3) add root user to the new user's group 
adduser root {user}
id root               # check whether root belongs to the new group

# 4) delegate jupyter lab extensions directory to the new group
chgrp {user} /usr/local/share/jupyter/lab -R
chmod 775 /usr/local/share/jupyter/lab -R

# 5) change user (to continue installation)
su {user}
echo ''     >> $HOME/.bashrc
echo 'cd ~' >> $HOME/.bashrc
source ~/.bashrc

Jupyter Lab 環境ができ上がると Jupyter のページタブから便利拡張機能を追加できるようになるが、そのままでは Jupyter 起動用ユーザが /usr/local/share/jupyter/lab に書き込み権限を持たないため、上記 3) 4) にて当該ディレクトリを (root も既存権限を保持したまま) 権限移譲する。

以上の設定で Jupyter ページタブから extension 自体は追加・削除できるようになるが、ページタブからは (おそらく上述のディレクトリ配下に extension 関連のサブ) ディレクトリを作成するのに失敗するため、初回だけコマンドラインから機能追加しておく。サンプルは目次作成機能。

jupyter labextension install @jupyterlab/toc

.NET Interactive インストール手順

続いて、前節で作成したユーザで .NET Interactive → C#, F#, PowerShell の Jupyter カーネルという順にインストールしていく。

# confirm in advance
jupyter kernelspec list

# expected result is ...
#    python3            /usr/local/share/jupyter/kernels/python3

# install dotnet-interastive
dotnet tool install --global Microsoft.dotnet-interactive --version 1.0.210803

# install .NET-related Jupyter kernels
~/.dotnet/tools/dotnet-interactive jupyter install

# reconfirm
jupyter kernelspec list

# expected result is ...
#    .net-csharp        $HOME/.local/share/jupyter/kernels/.net-csharp
#    .net-fsharp        $HOME/.local/share/jupyter/kernels/.net-fsharp
#    .net-powershell    $HOME/.local/share/jupyter/kernels/.net-powershell
#    python3            /usr/local/share/jupyter/kernels/python3

# return to root
exit

環境設定

インストールは完了なのだが、コンテナを再起動して Jupyter にアクセスすると C# / F# / PowerShell がメニューにあるものの、コンパイルしてくれず、Starting Kernel. Please Wait ... と表示される。参考記事/ブログ等にはインストールは簡単だと書いてあるだけで起動しない事例は見当たらず、ここからが悩みどころ。結論としては、機能させるために次の2つが必要となる。

  • dotnet-interactive にパスが通っていないため、環境変数設定をしてパスを通す
  • docker-compose.yml が command: で起動するプロセス (コンテナ内の pid = 1) にその環境変数設定を反映させる

Jupyter のプロセスに環境変数が効いておらず dotnet-interactive コマンドが command not found になっているのは容易に想像がつくが、この 2 点目に気付くのがなかなか難しい。

Jupyter が当該コンテナでどう起動されているか、/etc/init.d/ にシェルスクリプトをおいても自動起動しないこと、コンテナ内に入ってからサービス起動すると pid = 1 にならないこと *1、docker-compose.yml を編集するために (コンテナのスナップショットをとらずに) docker-compose down してしまうとコンテナ内で行った環境変数設定が消えてしまう、等々を理解してようやくたどりつく結論である。

結局、環境変数command: で起動される jupyter 以前に、余分な fork をしないで行うのがよく、/etc/bash.bashrc の末尾に追記するのがよいということになった。

# 環境変数の設定
echo ''                                         >> /etc/bash.bashrc
echo '# Environmental settings'                 >> /etc/bash.bashrc
echo 'export PATH=$PATH:$HOME/.dotnet/tools'    >> /etc/bash.bashrc
echo 'export DOTNET_TRY_CLI_TELEMETRY_OPTOUT=1' >> /etc/bash.bashrc

というわけで、上記のとおり環境変数設定の準備をする。2つめの環境変数 DOTNET_TRY_CLI_TELEMETRY_OPTOUTMicrosoft .NET Interactive の利用状況を情報提供しないという設定。

docker-compose restart をすれば環境変数が反映された形で Jupyter が起動する、が、この設定はいま alive であるコンテナでしか活きていないため restart (してサービスが動き出し、永続化されたものと勘違いして docker-compose down) する前にコンテナのスナップショットを取っておく。

コンテナの整備

環境変数保全してコンテナ稼働開始時に最初に起動するプロセスに反映させなければならないため、意外に大事であるコンテナ整備。その手順をここに記す。

コンテナ内部の整理

コンテナのスナップショットをとる前に、コンテナ内の不要なファイルは削除する。

apt clean
rm -rf /var/lib/apt/lists/*
コンテナ整備の手順

コンテナから外に出て、下記の作業を行う。docker-compose stopdocker-compose down を間違わないように。

# stop the running container (DO NOT make it down)
docker-compose stop

# preserve snapshot
docker commit {container id} {image:tag}

# abandon the container brefore editing docker-compose.yml
docker-compose down

# edit docker-compose.yml
editor docker-compose.yml

# start a new container derived from the preserved image
docker-compose up -d

docker-compose.yml の書き換えるべきは3か所。

  • 参照するイメージを変更する
    image: 'tensorflow/tensorflow:latest-py3-jupyter-yyyymmdd'
  • Docker 起動ユーザを変更する
    user: "{uid}:{gid}"
  • 起動コマンドを jupyter note から jupyter lab に変更し --allow-root オプションを外す
    command: /bin/bash -c "source /etc/bash.bashrc && jupyter lab --notebook-dir=/tf --ip 0.0.0.0 --no-browser --NotebookApp.token='XXXXXXXXXXXXXX'

これで Jupyter Lab + C# 9.0 + Python3 + Tensorflow の同時利用ができる。ちなみに python で次のように書くことにより JavaScript も実行できる。

from IPython.display import HTML

javascript = '''
<script type="text/javascript">
    alert("alert output");
    document.write("html output");
</script>
'''

HTML(javascript)

*1:ユーザが docker exec -it /bin/bashインタラクティブに実行した場合はもちろん、command: 起動のプロセスがコンテナ内部でシェルスクリプトをキックしても fork になるせいかダメらしい。

Docker on WSL2 環境構築メモ (5/x) - Bitnami/Redmine 導入

前提

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

  • 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

Bitnami/Redmine + MariaDB 導入

インストール手順 & 実行手順

Docker Hub での Bitnami/Redmine の解説の通り。Docker Compose での提供であるため、起動は簡単。

# install
docker pull bitnami/redmine

# execute
docker-compose up -d

初期設定状況と docker-compose.yml

初期設定は下記のとおり。docker-compose.yml の環境変数設定に追記することで変更可能 (だが、ユーザ関連情報は平文保存になってしまうため docker-compose.yml には書かない)。

初期ユーザ user 初回起動・ログイン直後に変更する
初期パスワード bitnami1 初回起動・ログイン直後に変更する
初期メールアドレス user@example.com
初期言語 en 初回ログイン前に ja にしておくのが無難

設計戦略 のところで述べたように、サーバサービスやシステム設定を格納するディレクトリ構造はコンテナとデータボリュームの間で分割せず一体とした方がバックアップ&リストア等の管理をしやすい。というわけで docker-compose.yml を下記のように編集しておく。

version: '2'
services:
  mariadb:
    image: 'docker.io/bitnami/mariadb:10.3-debian-10'
    restart: always                                    # 常に再起動するように設定しておく
    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'
#   image: 'docker.io/bitnami/redmine:4-debian-10-XXX' # スナップショットをとった場合はイメージを切り替える
    restart: always                                    # 常に再起動するように設定しておく
    environment:
      - REDMINE_DB_USERNAME=bn_redmine
      - REDMINE_DB_NAME=bitnami_redmine
      - REDMINE_LANGUAGE=ja                            # 日本語に設定しておく
    ports:
      - 'XXXX:3000'                                    # Port を変更しておく
    volumes:
#     - 'redmine_data:/bitnami'                        # データボリュームは使わない (システム設定情報の分断を回避するため)
      - '/mnt/.... /redmine_plugins:/redmine_plugins'  # 外部ディスク格納のプラグインパッケージをインストール
                                                       # する場合は参照先をマウントする
    depends_on:
      - mariadb
volumes:
  mariadb_data:
    driver: local
# redmine_data:                                        # データボリュームは使わない
#   driver: local

Redmine のカスタマイズ

本メモを記述する理由はインストール/実行手順の記録ではなく、むしろこちら。

カスタマイズした Redmine の挙動を確認する上で Docker コンテナをサンドボックスとして使い倒す。その際の操作・設定手順を記録することにある。 コンテナ内部の構成が Windows 版 Bitnami Redmine と少々異なるため、また、コンテナにエディタや日本語フォント等がないことをちょっとした工夫で凌ぐため、メモしておくことが重要。

Redmine 標準項目名の編集

日本語の標準項目名は /opt/bitnami/redmine/conf/locales/ja.yml に定義されている。これを (エディタなしで) 編集して挙動確認してみる。

テストケースとして assigned_to (日本語名:"担当者") 項目を "SendTo" にしてみる。 類似項目に assigned_to_role というものがあるため、これを比較対象として編集が exact になされたかを確認する。

# ディレクトリ移動
cd /opt/bitnami/redmine/conf/locales

# ファイルのバックアップ
cp ja.yml ja.yml.bak

# ファイルの編集
cat ja.yml.bak | awk '{ sub(/field_assigned_to:.*/, "field_assigned_to: SendTo"); print }' > ja.yml

# 確認
cat ja.yml | grep field_assigned_to

編集後に docker-compose restartとしてコンテナを立ち上げ直し、Redmine サービスが起動すれば、項目名が変更になっている。他の項目を含めて、あるべき項目を壊したりしてしまうとサービスが起動しなくなる。

docker-compose downdocker-compose up -dとするとコンテナを捨てて作り直すため、この設定は無かったことになる。しかし、プロジェクトやチケットの状況は MariaDB (のマウント先 = データボリューム) に保存されているため、無くならない。

プラグイン・インストールの事前準備

bitnami/redmine はお手頃なのだが、プラグイン・インストールの事前準備に大変手間がかかった *1 ため、ここに記録する。

必要なパッケージ

パッケージインストール方法補足
gcc
pkg-config
apt installビルド用
build-essential
curl
file
git
apt installbrew の依存パッケージ
libmariadbd-devapt installmysql2 (mariadb) の依存パッケージ
(mysql の依存する今は亡き libmysqld-dev の mariadb 版)
brewcurl でバッチのダウンロード&実行apt ではないパッケージ管理コマンド (Mac OS 由来)
nokogiri が apt でまともにインストールできないため必要
libxml2
libxslt
brew installnokogiri (Ruby 用 XML/HTML 用パーサ) の依存パッケージ
bundlergem installgem 管理コマンド
rakegem installRuby on Rails ビルドツール (要バージョン 13.0.1)

事前準備手順

事前準備の手順は下記のとおり *2

ディレクトリ移動
cd /opt/bitnami/redmine/
apt install によるパッケージ導入

docker コンテナ内では sudo 不要で下記のとおり進める。

apt update && apt upgrade
apt install gcc
apt install pkg-config
apt install build-essential curl file git
apt install libmariadbd-dev
brew のインストール

Ubuntu でのインストールパスは linuxbrew であるが、ダウンロード元は linuxbrew という名称が消え、本家 (Mac OS 用) の Homebrew と統合されたらしい *3

brew は root でインストールできない。curl で取り寄せたスクリプトの実行にはかなり時間がかかり、途中でエラー停止したように見えるところがあるが、辛抱強く待つ。

# change user (brew does not allow to be installed by root)
su bitnami

# download and execute script
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

# return to root
exit

# set path
export PATH='/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin':"$PATH"

# confirm brew
brew doctor
nokogiri 依存パッケージのインストール
brew install libxml2 libxslt
bundler と rake のインストールと設定

各 gem が要求する rake のバージョンが /opt/bitnami/redmine/Gemfile.lock に記載されているため、制約にかからないバージョン *4 をインストールし、設定する。また bundle install--no-deployment が廃止予定で使えないため、代わりの設定をする。

# install ruby on rails administrative tools
gem install bundler
gem install rake --version 13.0.1

# confirm available versions of rake
gem list rake

# set rake version to use
rake _13.0.1_ routes
bundle install による gem インストール

bundle install はこのディレクトリで実行しなければならないようだ。

# set configuration preliminarily, instead of using depreciated option '--no-deployment'
bundle config set --local deployment 'false'

# all I want to do is ...
cd /opt/bitnami/redmine/
bundle install

### preserve a docker snapshot, here ###

この節でやりたかった本質は bundle install の 1 コマンド。いままでやってきたのは、このコマンドが通らないための事前準備の事前準備。ふぅ。

プラグインのインストール方法

プラグインのインストール

bundle exec もこのディレクトリで実行しなければならないようだ。

# download plugin package (sample)
cd /opt/bitnami/redmine/plugins
git clone https://github.com/onozaty/redmine-view-customize.git view_customize

# gem install
cd /opt/bitnami/redmine/
bundle config set --local deployment 'false'
bundle install

# package install
bundle exec rake redmine:plugins:migrate RAILS_ENV=production

redmine の再起動

アプリごとの再起動方法を覚えるのが面倒なため、docker から再起動してしまう。

docker-compose restart

*1:手間がかかる理由は依存パッケージが多いこと、とさらに、解説サイトの通りにインストールできないものが多く頻繁に止まってしまうこと。

*2:わかってしまえば簡単だが、解説サイトを読み進めていくと途中途中で作業途中で課題が発生し、その解決のためにさらに解説サイトを探して課題がネストしていき、右往左往するハメになる。

*3:こういう点も、手順が (古い) 解説サイトの通りにいかない理由。

*4:今回の事例では rake 自身が 13.0.1 を要求していた様子。

Docker on WSL2 環境構築メモ (4/x) - Tensorflow (CPU 版) 導入

前提

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

  • 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

Tensorflow (CPU 版) + Python3 + Jupyter Notebook 導入

Tensorflow には CPU 版と GPU 版があるのだが、2021 年 2 月初旬現在、GPU 版を WSL2 上で動かすには Windows 10 Build 21292 (InsiderPreview) が推奨らしい。そこで Build 21292 がプロダクト・リリースされるまで、CPU 版をインストールして準備しておく。OS のバージョンアップを待ちながらアプリを先行導入するなんて通常は考えられないが、それを気楽にできるのが Docker のよいところ。

インストール手順

インストールは簡単。

# tensorflow + python3 + jupyter のイメージを pull
docker pull tensorflow/tensorflow:latest-py3-jupyter

実行手順

実行方法は何を使うかによって 3 通りある。

# Tensorflow Only
docker run -it --rm tensorflow/tensorflow:latest-py3-jupyter bash

# Tensorflow + Python3
docker run -it --rm tensorflow/tensorflow:latest-py3-jupyter python

# Tensorflow + Python3 + Jupyter Notebook Server
docker run -it --rm -v (個人フォルダへのパス):/tf/notebooks -p 8888:8888 tensorflow/tensorflow:latest-py3-jupyter &

注意点がいくつかある。

  • イメージにタグをつけないと Python3 + Jupyter Notebook を包含しない latest イメージを新たに pull してきてしまう
  • Jupyter Notebook サーバーが port 8888 を要求するため Port Relay (さらに必要に応じて Port Forward と Windows Defender) を設定する
  • Jupyter Notebook サーバーが作業フォルダを要求するため個人フォルダへのパス *1 を設定する 
  • Jupyter Notebook へのログイン時にトークンを要求されるが、これが Docker コンテナを作るたびに変化する一方でトークン/パスワード無効化までしてしまうと危険であるため、固定化する設定をする

docker-compose.yml の編集

Jupyter Notebook 関連の引数指定がちょっとめんどうであるため、(見よう見まねで) docker-compose.yml に記述することにした。

  tensorflow:
    image: 'tensorflow/tensorflow:latest-py3-jupyter'
    restart: always
    ports:
      - '8888:8888'
    volumes:
      - '/mnt/... (個人フォルダへのパス) .../JupyterNotebooks:/tf/notebooks'
    command: /bin/bash -c "source /etc/bash.bashrc && jupyter notebook --notebook-dir=/tf --ip 0.0.0.0 --no-browser --allow-root --NotebookApp.token='(固定化するトークン)'"

docker-compose.yml に書くべき情報は、いったん既定通りに走らせてみて docker inspect {container} とすれば取得できる。
Jupyter Notebook 実行時に有効になっているトークンは docker exec -it {container} jupyter notebook list とすれば取得できる。

*1:Jupyter Notebook の作業フォルダの配置は、ゲスト OS Ubuntu 上でもよいが、ホスト OS Windows NTFS を指定している。

Docker on WSL2 環境構築メモ (3/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

ネットワーク設定の目的

Docker コンテナで何らかのサーバサービスを立ち上げる場合、Docker コンテナの外部からアクセスできるようにする必要がある。Docker on Ubuntu on WSL2 場合の外部とは

  1. Docker コンテナ外・ゲスト OS (Ubuntu 仮想端末) 内
  2. ゲスト OS (Ubuntu 仮想端末) 外・ホスト OS (Windows 物理端末) 内
  3. ホスト OS (Windows 物理端末) 外

の3種類ある。1つめの Docker コンテナ ⇔ Ubuntu 仮想端末の通信は Docker 構築の定番テーマである Port Relay (ubuntu -p オプション) の話。3つめの Windows 物理端末外から Ubuntu 仮想端末間の通信は OS 仮想化で定番テーマである Port Forwarding の話。

個人で環境構築している際には外部公開するほどのことはやらないし、かといって Ubuntu 仮想端末内で作業が完了するわけでもない。コンサーンとなるのは2つめの同一端末内の Windows 物理端末 (内) と Ubuntu 仮想端末の間でのみ相互通信したいということになる。

ここまでの構築メモの手順を踏むだけでも Windows 物理端末 (内) と Ubuntu 仮想端末の間の通信が阻まれることはないのだが、後述のとおり仮想端末起動ごとに仮想端末 IP アドレスが揺らぐため、名前解決が面倒くさい。単純に思いつく解決法は hosts ファイルを書き換えることだが、試行錯誤した結果、hosts を動的に書き換えるとセキュリティ・リスクを高め、運用しにくいことから断念することにした *1

結論として、スコープを小さく限定して利用する場合でも Port Forwarding が最も手っ取り早い。Windows 物理端末外からのアクセスまで考慮するか否かはホスト OS (Windows 物理端末) でパケットフィルタリング (Windows Defender) を使うかどうかの違いでしかない。

ネットワーク設定の手順

仮想 OS の IP アドレス

いろいろと調べたところ、ゲスト OS 側 Ubuntu 仮想端末としての IP アドレスはUbuntu 20.04 LTS on WSL2 環境構築メモ (2/4) - GUI 導入で調べたネットワークアダプタ関連情報には登場せず、Ubuntu 側からip a show dev eth0 とやると取得できるらしい。そこで IPv4 アドレスを下記のようにして取得する。

ip a show dev eth0 | awk '$1 == "inet" && $2 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ { sub(/\/[0-9]+$/, "", $2); print $2 }'

仮想端末の IP アドレスはよく調べたくなるため、whereami で調べられるように .bashrcエイリアスを追記しておく。bash ではクォーテーションのエスケープが少し特殊であることに注意。'\''エスケープされた ' だと思えばよい。

alias whereami='ip a show dev eth0 | awk '\''$1 == "inet" && $2 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ { sub(/\/[0-9]+$/, "", $2); print $2 }'\'''

Port Forwarding 設定の前提

Docker で apache web サービスのコンテナを port 80 → 8080 で Port Relay しつつ走らせており、これにホスト OS (Windows) のブラウザから接続するというシナリオを前提とする。実環境で Port を変える必要はないのだが、サーバサービスを Docker で本番/検証/開発環境並行運用するような場面を想定した仮想実験。構成図 (最終形) はこちら

docker run -p 8080:80 -d httpd

Port Forwarding 設定

WSL2 で実行するシェルスクリプトの作成

まず、Ubuntu 仮想端末において管理者権限で port-forward.sh というファイルを作成する。

sudo touch /opt/port-forward.sh

その中身の記述は次のとおり。xx.xx.xx.xxWindows 物理端末の実 IP アドレス。

#!/bin/bash

# obtain ipv4 on virtual os
IP=$(ip a show dev eth0 | awk '$1 == "inet" && $2 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ { sub(/\/[0-9]+$/, "", $2); print $2 }')

# reset and set port forwarding configuration on wsl2
netsh.exe interface portproxy delete v4tov4 listenport=80 listenaddress=localhost
netsh.exe interface portproxy delete v4tov4 listenport=80 listenaddress=xx.xx.xx.xx
netsh.exe interface portproxy add    v4tov4 listenport=80 listenaddress=localhost   connectport=8080 connectaddress=$IP
netsh.exe interface portproxy add    v4tov4 listenport=80 listenaddress=xx.xx.xx.xx connectport=8080 connectaddress=$IP
シェルスクリプトをキックするバッチの作成

Windows 物理端末において %UserProfile%\ あたりに port-forward.batというバッチファイルを作成する。その中身の記述は次のとおり。wsl -u オプションで root 指定必須。wsl -d オプションは仮想 OS ディストリビューションの指定。事前に wsl --list --verbose で正式な識別名を調べておく。

@echo off
wsl -d Ubuntu-20.04 -u root --exec /bin/bash /opt/port-forward.sh

rem コマンドラインから実行した場合に、設定状況を一覧表示する
netsh interface portproxy show v4tov4

このファイルは管理者権限で実行する必要があるため、ショートカットを作成し、その詳細プロパティで「管理者として実行」にチェックを入れる。

実行

このショートカットを実行しておけば、Port Forwarding は有効になっている。 (Ubuntu 仮想 OS をユーザが起動しなくても) WSL2 は Windows 起動時からバックグラウンドで動いているため、Windows 起動時に自動キックとなるようタスクスケジューラに仕込んでおけばよい。

Windows 物理端末のブラウザから http://localhost/ (Port 80) 指定で Docker コンテナの httpd (Port 8080) へアクセスできる。
Ubuntu 仮想端末のブラウザからは http://localhost:8080/ (Port 8080) 指定でなければアクセスできない。仮想端末内では Port Forwarding 設定 (Windows Port 80 → Ubuntu Port 8080) が効いておらず、一方で Port Relay 設定 (Ubuntu Port 8080 → Docker Port 80) が効いているため。

Windows 物理端末外からのアクセス許可

では Windows 物理端末外のブラウザから http://xx.xx.xx.xx/ 物理端末 IP & Port 80 指定でアクセスできるか、というとできない。これは Windows Defender のパケットフィルタリングに阻まれるためである。そこでローカルネットワークや VPN のセグメントからのみ、http (port 80 TCP) アクセスを受け入れるルールを Windows Defender に設定する。

http (port 80 TCP) inbound をローカルネットワークや VPN に許可するルールの作成方法
  1. "Windows Defender ファイアーウォール" を開く
  2. "詳細設定" を押す
  3. "受信の規則" を右クリックし、"新しい規則" を選択する
  4. "規則の種類" で "ポート" を選択し、"次へ" を押す
  5. TCP/UDP は "TCP" を選択、ローカルポートは "特定のローカルポート" を選択、ポート番号に "80" を指定し、"次へ" を押す
  6. "接続を許可する" を選択し、"次へ" を押す
  7. "ドメイン" と "プライベート" を選択し、"次へ" を押す
  8. 受信規則の名前を付け、"完了" を押す
  9. "受信の規則" からいま設定したルールを選択、右クリックでプロパティを選択、 ”スコープ" タブを開く
  10. リモート IP アドレス (Windows 物理端末外側) で "これらの IP アドレス" を選択し、"追加" を押し、"この IP アドレスまたはサブネット" で "xx.xx.xx.xx/xx" (ローカルネットワークや VPN のセグメント) を入力し、"次へ" を押す

構成図 (最終形)

f:id:Crayon:20210130112044j:plain

*1:hosts 格納フォルダ内は Windows 側の管理者権限がないと操作できず、かつ、hosts ファイル自身が Users グループ Readable/Executable でないと機能しない。アクセス権の緩いフォルダで制御して本来のフォルダからシンボリックリンクを設定することもできるがセキュリティ・リスクを高めてしまう。いずれにせよ NTFS アクセス権限を Ubuntu 側から制御するのはかなり面倒であった。

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 すると大抵この辺のことが解説されていなくて困る)。

Docker on WSL2 環境構築メモ (1/x) - Docker 基本操作

Docker 基本操作 (チートシート)

Docker 操作の状態遷移

Docker の状態遷移がわかりづらい (にも関わらず図示した解説を見かけない) ため、誤っている可能性を恐れず、可視化してみた。(利用は自己責任で。)
f:id:Crayon:20210203221706j:plain

Docker レポジトリ (イメージ) の操作

レポジトリ (ID) とは Docker Hub におけるイメージを固有識別子 (ubuntu などのテキスト) で表現したもの。イメージ (ID) とはローカルに保持しているイメージをその固有識別子 (f12345a12b1f などの記号列) で表現したもの。

アクションコマンド補足
イメージの取得docker pull {repository}[:{tag}]{tag} 省略時: latest 指定と同等
イメージ (レポジトリ) 検索は こちら
イメージの一覧表示docker images [-a] [{repository}[:{tag}]]-a 省略時:中間イメージ非表示
{repository}, {tag} 省略または * 指定時:全量表示
イメージの削除docker rmi {repository:tag}
docker rmi {image}
「レポジトリ+タグ (イメージに対するリンク)」の削除が先で、
リンク参照が 0 になるときに「イメージ (実体)」が削除される
イメージの一括削除docker image pruneコンテナから利用・参照中のものを除く
イメージへのレポジトリ
(リンク識別子) 追加
docker tag {image} {repository}[:{tag}]{tag} 省略時: latest 指定と同等

Docker コンテナの操作

サービス起動後 bash プロンプトに戻らないコンテナを操作するときは & を付して docker コマンド自体をバックグラウンドで動かすと吉。

アクションコマンド補足
コンテナの生成+起動docker run
   [-itd] [--name=XXX] [--restart=always]
   {repository}[:{tag}] [command]
-i 指定時:標準入力利用
-t 指定時:標準出力利用
-d 指定時:デタッチモード (バックグラウンド実行)
--name=XXX 省略時:イメージが保持している既定コンテナ名が利用される
--restart:always 指定時:docker や Linux 再起動時にコンテナを再起動する *1
docker run -p {from}:{to} {repository}【Tips】
-p 指定時:Port Relay 設定
from はコンテナ外 Port、to はコンテナ内 Port
docker run -v {from}:{to} {repository}【Tips】
-v 指定時:ディスクマウント設定
from はコンテナ外資源、to はコンテナ内マウントポイント
コンテナの起動docker start [-ai] {container}-a 指定時:アタッチモード (フォアグラウンド実行)
-i 指定時:標準入力利用
docker start -ai $(docker ps -lq)【Tips】
最終生成コンテナの再起動
コンテナの再起動docker restart {container}
コンテナの停止docker stop [-t {seconds}] {container}-t 指定時:指定秒数後に停止
コンテナのバックグラウンド化キー入力Ctrl+PCtrl+Qbash が生きているコンテナのみ有効
コンテナへの接続docker exec -it {container} /bin/bash
コンテナの一覧表示docker ps [-alq] [{container}]-a 指定時:停止中コンテナ表示
-l 指定時:最終生成コンテナを返す
-q 指定時:コンテナ ID 情報のみを返す
{container} 省略時:全量表示
コンテナの固有情報表示docker inspect {container}
コンテナの削除docker rm {container}停止中コンテナのみ削除可能
コンテナの一括削除docker container prune停止中コンテナのみ削除可能
コンテナのイメージ保存docker commit {container} {repository[:tag]}停止中コンテナのみ保存可能

Docker データボリュームの操作

アクションコマンド補足
データボリュームの一覧表示docker volume ls
データボリュームの固有情報表示docker volume inspect {volume}
データボリュームの作成docker volume create {volume}
データボリュームの削除docker volume rm {volume}
データボリュームの一括削除docker volume prune不使用データボリュームのみ削除可能

Docker Compose の操作

docker-compose コマンドはすべて、docker-compose.yml に定義されているコンテナしか対象としない。

アクションコマンド補足
定義済みコンテナの一斉起動docker-compose [-f {file}] up [-d]-f 省略時:./docker-compose.yml を指定したとみなす
-d 指定時: デタッチモード (バックグラウンド実行)
定義済みコンテナの一斉停止docker-compose stop [-t: seconds]-t 指定時:指定秒数後に停止
定義済みコンテナの一斉再起動docker-compose restart
定義済みコンテナの一覧表示docker-compose ps [-aq]-a 指定時:停止中コンテナ表示
-q 指定時:コンテナ ID 情報のみを返す
定義済みコンテナの一斉削除docker-compose rm

その他 Tips

レポジトリ・リストのクリーンアップ

Docker コンテナの内外を問わず、環境構築を終わって運用に入るときにはレポジトリ・リストのクリーンアップを図り、ディスクを効率活用する。

sudo apt clean
sudo rm -rf /var/lib/apt/lists/*

*1:ホスト OS の再起動では非有効。

Ubuntu 20.04 LTS on WSL2 環境構築メモ (4/4) - その他アプリ導入

前提

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

  • 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/4 を実施した状況

その他アプリ導入

VcXsrv をマルチウィンドウモードで起動すれば、Ububtu アプリも Windows アプリと区別なく利用できる (かつ PowerToys Fancy Zone で窓の配置を最適化できる) ため、Docker で仮想化するサーバサービス以外に Ubuntu にインストールするアプリなど不要。と考えていたが、UbuntuWindows 間の疎通確認等でブラウザを含む最小限のアプリはインストールすべきだったため、ここに備忘を残す。

Google Chrome 導入

参考記事の通りに手順を踏めば問題なくインストールされる。

# インストール状況の確認
apt list --installed google*

# ダウンロード元の指定とその確認
sudo sh -c 'echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
cat /etc/apt/sources.list.d/google.list

# 公開鍵のダウンロードと登録
sudo wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -

# パッケージの最新化
sudo apt update

# ダウンロード元の状況確認
apt list google*

# インストール
sudo apt-get install google-chrome-stable

# インストール状況の確認
apt list --installed google*

# Google Chrome 起動
google-chrome &