Docker & WSL と格闘してみた！

:::note warn
この記事は2024年5月27日に弊社ブログに公開された記事の移植記事となります。
:::

皆さんお久しぶりです。@2626です。最近も生成AIの大流行が続いていて、OpenAIやMicrosoft、Googleなど、多くの企業が様々なサービスを提供しています。私はその進展に追いつくのがやっと（おそらく追いつけていない）で、AIの仕組みにわくわくしつつも、頭がパンクしそうな日々を過ごしています。

@2626の所属しているチームでは、Dockerを使用しております。PCの入れ替えの際にDockerも入れなおす必要があるのですが、そのときにハマってしまった経験を備忘録として共有したいと思います。同じようなエラーにハマってしまった人の助けになれば幸いです。


# Docker とは？
Docker とはそもそも何でしょうか？ [Wikipedia](https://ja.wikipedia.org/wiki/Docker) には下記のように書かれているようです。
> コンテナ仮想化を用いてアプリケーションを開発・配置・実行するためのオープンプラットフォームである。

少し難しいですね。簡単に説明すると、「**PC の中に仮想的に PC のような環境を別に作り出す技術**」です。従来の仮想化技術と比較して、OSの管理が不要な分、高速かつ軽量で扱うことができます。

Dockerを利用すると、自分のPC環境を汚さずに開発環境を作成し、異なる開発環境を独立して管理することができます。私の大好きな技術の1つです。詳しい説明は今回の主旨と外れてしまうので割愛しますので、各種Webサイトを調べてみてください。~~説明できるほどの知識がないから逃げているわけではない。~~


# いざ本題へ
新しい PC が届き、「サクサク動くし快適！」と意気揚々としながら PC の設定を無事に終えます。「さぁ PC の設定もほどほどに、開発に戻るか」とコンテナを作成し、コンテナ内のアプリケーションを実行したところ…

```Java
INFO  [restartedMain] - HikariPool-1 - Starting...
ERROR [restartedMain] - HikariPool-1 - Exception during pool initialization.
java.sql.SQLRecoverableException: IO Error: The Network Adapter could not establish the connection (CONNECTION_ID=*****)
```

あれ、うまくコンテナ内のアプリケーションが実行できません。このエラーの細かい話は省略しますが、どうやら、**社内にあるデータベースに接続できない**ようです。問題解消のために、まずは状況を整理することから始めました。


# PC 環境について
私は Windows11 を使用しています。Docker は Windows に直接導入することはできないので、WSL に Docker Engine をインストールして使用しています。ちなみに普段使いの方は [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/) というアプリケーションを使用することで、Windows 上からコンテナやイメージの操作ができ、Powershell やコマンドプロンプトから操作することも可能です。

しかし、Docker Desktop は「[**従業員数250名を超える企業、または年間売上高1000万ドルを超える企業でDocker Desktopを商用利用するには、有料サブスクリプション（Pro、Team、またはBusiness）が必要です。**](https://www.docker.com/ja-jp/pricing/)」の記載の通り、一部のユーザを除いて有料化されています。弊社でも有料化の対象になっています…

一応、「Docker は便利だから使いたい…」でも「有料化は嫌だ…」という方のために、こんな方法があったりします。
- [Docker Engine を直接インストール](https://docs.docker.com/engine/install/)して、Docker Desktop を介さずに操作をする
- [Podman Desktop](https://podman-desktop.io/) や [Rancher Desktop](https://rancherdesktop.io/) を導入する

なお、Podman Desktop や Rancher Desktop は執筆時点（2024年5月23日）では無料で商用利用することもできますが、今後もそうであるという保証は全くないので、ご利用の際は各自ご確認ください。


# エラーの状態の確認
閑話休題。実際にエラーを調べてみると、下記のようなことが分かりました。

## エラーの状態
- 「社内で VPN なし」「社内で VPN あり」「社外で VPN あり」いずれの場合も再現する
- 他メンバー PC では再現するが、証拠 PC では再現しない
- 我々のチームに支給されているデスクトップ PC 2台でも再現しない

この結果から、Dockerのネットワーク設定に問題があると考えました。調査を進めた結果、比較的すぐに問題が見つかりました。どうやら以前のPCに設定していたことを忘れていたようです。

## WSL の入れ直し
問題解決の一環として、WSL (Windows Subsystem for Linux) を再インストールすることにしました。以下の手順でWSLを削除し、再インストールします。

### WSL のアンインストール
まず、以下のコマンドでWSLをアンインストールします：
```bash
> wsl --unregister Ubuntu-24.04
```

なお、Ubuntu-24.04 には実際にインストールされているディストリビューション名を適宜入れてください。

### WSL のインストール
次に、以下のコマンドでWSLをインストール可能なディストリビューション一覧を確認します：
```bash
> wsl -l -o
インストールできる有効なディストリビューションの一覧を次に示します。
'wsl.exe --install <Distro>' を使用してインストールします。

NAME                                   FRIENDLY NAME
Ubuntu                                 Ubuntu
Debian                                 Debian GNU/Linux
kali-linux                             Kali Linux Rolling
Ubuntu-18.04                           Ubuntu 18.04 LTS
Ubuntu-20.04                           Ubuntu 20.04 LTS
Ubuntu-22.04                           Ubuntu 22.04 LTS
Ubuntu-24.04                           Ubuntu 24.04 LTS
OracleLinux_7_9                        Oracle Linux 7.9
OracleLinux_8_7                        Oracle Linux 8.7
OracleLinux_9_1                        Oracle Linux 9.1
openSUSE-Leap-15.5                     openSUSE Leap 15.5
SUSE-Linux-Enterprise-Server-15-SP4    SUSE Linux Enterprise Server 15 SP4
SUSE-Linux-Enterprise-15-SP5           SUSE Linux Enterprise 15 SP5
openSUSE-Tumbleweed                    openSUSE Tumbleweed
```

表示された一覧から目的のディストリビューションを選び、インストールします：
```bash
> wsl --install Ubuntu-24.04
```

### WSL バージョンの変更
実は、WSL にはバージョン 1 と 2 がありますが、Dockerを使用するためにはバージョン2が必要です。以下のコマンドでバージョンを2に設定します：
```bash
> wsl --set-version Ubuntu-24.04 2
```

その他の詳細なコマンドは[こちら](https://learn.microsoft.com/ja-jp/windows/wsl/basic-commands)をご覧ください。


# WSL に割り振られる IP アドレス
WSL を起動すると、WSL 自体に IP アドレスが割り振られます。さらに Docker も起動している場合、Docker が外部とやり取りするためのインターフェースが WSL 上に作成されます。そのインターフェース名は docker0 です。正確には、Docker をインストールして作られるデフォルトのブリッジネットワーク（bridge）と外部ネットワークをやり取りするためのインターフェースです。

## Docker ネットワークの確認
bridge と名付けられたネットワークは以下のコマンドで確認できます：
```bash
$ docker network inspect bridge
```

このコマンドの出力例は次の通りです：
```bash
[
    {
        "Name": "bridge",
        "Id": "35eb11e70cdabfc429eb870dec78dfc1ff16845526b2fbef4cc916ab23255bd3",
        "Created": "2024-05-23T12:26:28.469626292+09:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "192.168.200.0/24",
                    "Gateway": "192.168.200.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]
```

## Docker インターフェース docker0 の問題解決
docker0 は、初期値として 172.17.0.1/16 が割り振られています。しかし、これが社内のどこかのセグメントと競合していたようです。この問題を解決するには、WSL の /etc/docker/daemon.json を確認（または作成）し、以下の設定を追加します：
```bash
$ sudo vi /etc/docker/daemon.json
```
```bash
{
    "bip": "192.168.200.1/24"
}
```

IP アドレスは任意のものを設定可能ですが、他のネットワークと競合しないようなものを選びます。この設定を追加したら、WSL を再起動します。
```bash
$ ip a show docker0
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 02:42:38:d0:6d:81 brd ff:ff:ff:ff:ff:ff
    inet 192.168.200.1/24 brd 192.168.200.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:38ff:fed0:6d81/64 scope link
       valid_lft forever preferred_lft forever
```

はい、無事 IP アドレスが反映されました。これでめでたしめでたし…
```bash
INFO  [restartedMain] - HikariPool-1 - Starting...
ERROR [restartedMain] - HikariPool-1 - Exception during pool initialization.
java.sql.SQLRecoverableException: IO Error: The Network Adapter could not establish the connection (CONNECTION_ID=XXXXXXXXXXXXXXXXXXXXXXXXX)
```

どうやらダメみたいですね…。ということで、もう少し詳しく確認してみます。すると、
- 「社内で VPN なし」「社内で VPN あり」「社外で VPN あり」いずれの場合もエラーが再現する。

から
- 「社内で VPN あり」「社外で VPN あり」の場合はエラーが再現する。
- 「社内で VPN なし」の場合はエラーが再現しない。（ちゃんと接続できる）

という状態になっていました。この結果から、VPNとWSLの間に何かしらの問題があることが疑われます。さらなる調査が必要そうです…

# 仮想イーサネットの IP アドレスの確認
まず、ネットワーク設定を確認します。WSL を起動すると、WSL が Docker とやり取りするインターフェースだけでなく、WSL が Host マシン側とやり取りするためのインターフェースが作成されます。特に設定しなければ eth0 という名前が付けられます。

以下のようにして eth0 の詳細を確認します：
```bash
$ ip a show eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:15:5d:14:09:ac brd ff:ff:ff:ff:ff:ff
    inet 10.1.0.88/24 brd 10.1.0.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::215:5dff:fe14:9ac/64 scope link
       valid_lft forever preferred_lft forever
```

ここでは 10.1.0.88/24 が割り振られていますね。

### Windows側のネットワーク設定
次に、Windows側のネットワーク設定を確認します。以下のコマンドを使用します：
```bash
> ipconfig
イーサネット アダプター vEthernet (WSL (Hyper-V firewall)):

   接続固有の DNS サフィックス . . . . .:
   リンクローカル IPv6 アドレス. . . . .: fe80::b51c:a205:6f16:66de%54
   IPv4 アドレス . . . . . . . . . . . .: 10.1.0.1
   サブネット マスク . . . . . . . . . .: 255.255.255.0
   デフォルト ゲートウェイ . . . . . . .:
```

ここでは、10.1.0.1 が設定されています。

### IP アドレスの固定方法
**eth0 のゲートウェイが vEthernet になっている**必要がありますが、実際になっていない人もいるようです。この設定を固定するには、レジストリエディタの コンピューター\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss を確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3904801/34eaeded-2980-1982-a5f0-adbb41059add.png)

ここの、「NatGatewayIpAddress」「NatNetwork」を設定することで、IP アドレスを静的に設定することが可能です。（この画像では既に設定後です）

# .wslconfig と wsl.conf の設定方法と WSL のバージョン確認
### WSLの設定ファイルについて
次に WSL の設定ファイルを見ていきます。ちなみに WSL の設定ファイルには2種類あって、.wslconfig と wsl.conf です。それぞれの役割と違いについて説明します。

### .wslconfig
.wslconfig は全ての WSL の環境に影響を及ぼします。WSL 自体は Windows PC に対して 1 つですが、ディストリビューションは複数導入することが可能です。

以下のコマンドで導入されているディストリビューションを確認できます：
```bash
> wsl -l -v
  NAME            STATE           VERSION
* Ubuntu-24.04    Running         2
  Ubuntu-22.04    Stopped         2
  Ubuntu-20.04    Stopped         2
```

これらの全てのディストリビューション環境に統一的に設定したい場合、.wslconfig を使います。なお、作成したファイルは `C:\Users\[ユーザ名]` に保存します。

### wsl.conf
対して、wsl.conf は各ディストリビューション環境ごとに個別に設定したい場合に使用します。例えば、以下のように設定します：
```bash
$ cat /etc/wsl.conf
[boot]
systemd=true
```

### .wslconfigの設定
これらの違いが分かったところで、.wslconfig の設定を確認していきます。
```bash
[wsl2]

# WSL2の軽量仮想マシンで使用する最大メモリサイズを指定する
# 未指定時のデフォルト値はPC搭載メモリの50%または8GBのうち、少ない方の値
memory=16GB
swap=0
localhostForwarding=true

[experimental]
autoMemoryReclaim=gradual
```

- memory=16GB：WSL2の仮想マシンが使用する最大メモリサイズを16GBに設定
- swap=0：スワップ領域を無効化
- localhostForwarding=true：localhostでアクセスしたときにそのパケットをWSLに流す設定
- autoMemoryReclaim=gradual：メモリの自動回収を徐々に行う設定

その他の設定に関する説明は[こちら](https://learn.microsoft.com/ja-jp/windows/wsl/wsl-config)をご確認ください。


# WSL のバージョンについて
WSLのバージョンは、ディストリビューションの種類ではなく、WSLそのもののバージョンを指します。以下のコマンドで確認できます：
```bash
> wsl -v
WSL バージョン: 2.1.5.0
カーネル バージョン: 5.15.146.1-2
WSLg バージョン: 1.0.60
MSRDC バージョン: 1.2.5105
Direct3D バージョン: 1.611.1-81528511
DXCore バージョン: 10.0.25131.1002-220531-1700.rs-onecore-base2-hyp
Windows バージョン: 10.0.22631.3593
```

現在のバージョンは2.1.5.0です。このバージョンを変更することで問題が解決するか試してみます。まずは、必要なファイル（Microsoft.WSL_X.X.X.X_x64_ARM64.msixbundle）を[ダウンロード](https://github.com/microsoft/WSL/releases?page=1)してください。その後、下記コマンドを順に実行していきます：
```bash
> wsl --shutdown
> $Package = Get-AppxPackage MicrosoftCorporationII.WindowsSubsystemforLinux -AllUsers
> Remove-AppxPackage $Package -AllUsers
> Add-AppxPackage [path to Microsoft.WSL~.msixbundle]
```

この手順を試しましたが、何も変わりませんでした。原因は不明ですが、次の対策に進むことにします。うまく行かなかったので切り替えて次、行きましょう！（ちなみにこの辺で一回心折れてます）

# ネットワークのトラブルシューティング
ネットワークの問題を解決するために、さまざまなコマンドを使用してどのインターフェースが異常を起こしているのか確認します。ここでは、頻繁に使用したコマンドとその目的を紹介します。

## ping
知らないエンジニアは~~人権がはく奪される~~いないであろう ping です。ping コマンドは、ICMPパケットを送信し、ネットワーク層での接続を確認するために使用します。通信できたからといってポートが開放されているわけではないので注意が必要です。

### Windows、Ubuntu
```bash
> ping 192.168.0.1 
```
```bash
$ ping 192.168.0.1
```

## ipconfig (ip a)
IP アドレスを調べるためのコマンドです。ping とセットで知っておくと便利です。Ubuntu ではかつて IP アドレスを調べる代表的なコマンドとして ifconfig が知られていましたが、現在は非推奨になっています。

### Windows
```bash
> ipconfig
```

### Ubuntu
```bash
$ ip a
```

## curl
ping や ipconfig と並んで有名なコマンドの1つですね。私は名前解決ができているのか確かめるためにこのコマンドを使用しました。

### Windows、Ubuntu
```bash
> curl www.google.com
```

```bash
$ curl www.google.com 
```

## traceroute (tracert)
パケットのルートを追跡するためのコマンドです。ポートを指定することで、そのポートが開放されているのか確認することも可能です。

### Windows
```bash
> tracert XXX.XXX.XXX.XXX
```

### Ubuntu
```bash
$ traceroute XXX.XXX.XXX.XXX [-p port]

$ traceroute XXX.XXX.XXX.XXX -p [port]
traceroute to XXX.XXX.XXX.XXX (XXX.XXX.XXX.XXX), 30 hops max, 60 byte packets
 1  [PC name] (10.1.0.1)  0.553 ms  0.460 ms  0.436 ms
 2  10.0.100.18 (10.0.100.18)  18.035 ms  18.012 ms  17.991 ms
 3  10.0.100.28 (10.0.100.28)  18.006 ms  17.983 ms  17.960 ms
 4  10.0.100.1 (10.0.100.1)  17.899 ms  17.873 ms  17.848 ms
 5  10.0.0.21 (10.0.0.21)  29.572 ms  29.548 ms  29.502 ms
 6  10.0.0.22 (10.0.0.22)  29.394 ms  22.624 ms  22.571 ms
 7  XXX.XXX.XXX.XXX (XXX.XXX.XXX.XXX)  22.529 ms  29.664 ms  28.911 ms
```

## nc(netcat)
nc コマンドは、接続ができたかどうかを確認するためのコマンドです。ポート番号を指定可能です。

### Ubuntu
```bash
$ nc -vz [ip address] [port]

$ nc -vz XXX.XXX.XXX.XXX [port]
Connection to XXX.XXX.XXX.XXX [port] port [tcp/*] succeeded!
```

これらを色々と駆使していくうちに、次のようなことが分かってきました。

1. Docker コンテナに割り振られている eth0 インターフェース <-> WSL 内の eth0 インターフェース間は ping が実行可能
2. Windows の VPN 用 インターフェース 🡪 外部 DB は接続できる
3. Windows の VPN 用 インターフェース 🡪 Windows の WSL の インターフェースは接続できる
4. WSL の eth0 インターフェース 🡪 Window の全てのインターフェースは接続できない
5. Windows の VPN 用 インターフェース 🡪 WSL の全てのインターフェースは接続できない
6. 特に、Windows から WSL への ping にも関わらず、[知らない IP] で TTL が期限切れになっている（パケットが [知らない IP] から 使用 PC にレスポンスできない状態になっている）

```bash
> ping 10.1.0.88

10.1.0.88 に  ping を 送信しています 32 バイトのデータ：
XXX.XXX.XXX.XXX からの応答：転送中に TTL が期限切れになりました。
XXX.XXX.XXX.XXX からの応答：転送中に TTL が期限切れになりました。
XXX.XXX.XXX.XXX からの応答：転送中に TTL が期限切れになりました。
XXX.XXX.XXX.XXX からの応答：転送中に TTL が期限切れになりました。

10.1.0.88 の  ping 統計：
    パケット数：送信 = 4、受信 = 4、損失 = 0 (0% の損失)、
```

これらの結果から、WSL <-> Windows 間のネットワークに問題がありそうだと考えられます。そして、ようやく原因にたどり着くことができました。

# WSLとVPNのネットワーク問題の解決
### 問題点の発見
結論から言うと、原因はルーティングテーブルの設定不具合です。ルーティングテーブルの設定を見直していきます。

### ルーティングテーブルの確認
まず、route print コマンドを使用し、ルーティングテーブルを確認します。
```bash
> route print
[略]
IPv4 ルート テーブル
===========================================================================
アクティブ ルート:
ネットワーク宛先        ネットマスク          ゲートウェイ       インターフェイス  メトリック
          0.0.0.0          0.0.0.0            リンク上       [VPN interface]      1
         10.1.0.1  255.255.255.255            リンク上          10.1.0.1    271
        127.0.0.0        255.0.0.0            リンク上         127.0.0.1    331
        127.0.0.1  255.255.255.255            リンク上         127.0.0.1    331
  127.255.255.255  255.255.255.255            リンク上         127.0.0.1    331
[略]
```

あれ…？10.1.0.XXX のパケットの流れ先が、[VPN interface] 以外ない…。

なるほど、確かにこれなら、10.1.0.88 のパケットが [VPN interface] 以外にルーティングする先がないから WSL の中にパケットが来ないんやな…となりました。~~感心してる場合じゃない。~~ 無いなら追加してあげます。

### ルーティングテーブルの修正
下記のコマンドを実行して、ルーティングテーブルに追加します。
```bash
> route -p add 10.1.0.0 mask 255.255.255.0 10.1.0.1 metric 15
 OK!
```

再度ルーティングテーブルを確認してみます。
```bash
> route print
[略]
IPv4 ルート テーブル
===========================================================================
アクティブ ルート:
ネットワーク宛先        ネットマスク          ゲートウェイ       インターフェイス  メトリック
          0.0.0.0          0.0.0.0            リンク上       [VPN interface]      1
         10.1.0.0    255.255.255.0            リンク上          10.1.0.1     30
         10.1.0.1  255.255.255.255            リンク上          10.1.0.1    271
        127.0.0.0        255.0.0.0            リンク上         127.0.0.1    331
        127.0.0.1  255.255.255.255            リンク上         127.0.0.1    331
  127.255.255.255  255.255.255.255            リンク上         127.0.0.1    331
[略]
```

### 問題の解決
実際にコンテナ内の Web アプリケーションを実行してみます。
```bash
2024/05/23 07:15:42 INFO  [restartedMain] - HikariPool-1 - Starting...
2024/05/23 07:15:46 INFO  [restartedMain] - HikariPool-1 - Added connection oracle.jdbc.driver.T4CConnection@b276838
2024/05/23 07:15:46 INFO  [restartedMain] - HikariPool-1 - Start completed.
```

うおおおおお！つながった～～～～～！ということで無事解決です。~~この問題が解決した後、この日の仕事のやる気が出なかったのは言うまでもありません。~~


# まとめ
Docker を使い始めてから 3 年近く経ちますが、まだまだ知らないことがたくさんあると感じました。また、ネットワーク関連はあまり得意ではないため、確認を後回しにしてしまった点も反省すべきところです。

## 反省点と今後の対策

### ネットワークの基礎知識
まず、ネットワーク周りの問題を迅速に解決するために、基礎知識をもう少し深める必要があります。ルーティングテーブルについては応用情報で勉強したことがあるくらいだったので、今回の出来事で理解が深まったと思います。

### 設定の永続化
今回のルーティングテーブルの設定は、PC の再起動で消えてしまう問題が残っています。-p オプションで永続化できるとされていますが、私の環境ではうまくいかないため、現状は手動で設定を行っています。この辺りは設定してくれるようなスクリプトなり、何かしらの対策は用意したいですね。

### VPN と WSL の設定の不明点が残っていること
そもそも他の人で起きなかった問題が@2626と@mura_mattyoの PC だけに起きたのか最後まで分かりませんでした。証拠 PC では正しくルーティング設定ができていることも不明点です。根本的に解決してくれるのが一番な気がします。


### 感謝の意
今回のデバッグに関して、再現を確認してくれた証拠さん、むらまっちょさんにはこの場を感謝したいです。また、こちらのエラーを親身になって確認してくれた、情シスの〇〇さんにも感謝したいです。~~唐揚げ 1 個あげます。~~