Multipass で docker 環境を整える方法

本記事は株式会社インティメート・マージャーの [Advent Calendar 2023](https://qiita.com/advent-calendar/2023/intimatemerger) の記事になります。


## はじめに
この記事では Multipass を使って docker の開発環境を整える方法をご紹介していきます。

先駆者の方が開拓した記事を参考にさせて頂き環境構築していましたが、自分の環境ではさくっとは動きませんでした。
いくつかのハマりどころを解決するための助けになる記事を書いていければと思います。


## なぜやるのか？

コストをかけずに開発体験を向上させたい！ というモチベーションからです。
インティメート・マージャーでは、クラウド上にセットアップした開発環境を主に利用しています。

- （富豪な構成ではないため）開発中のサービスをいくつも起動していると重くなる
- Docker Desktop も使っているが、利用頻度の割にコスパが悪い ＆ 重いのも気になる
- Mac のリソースを活用した方が良いユースケースもありそう

あたりを考慮し、いくつかのソリューションを比較した結果 Mutipass を選択しました。

- Lima
    - 書き込み可能でマウントすると、何らかのバグでデータが損失するというバグある[^1]
- Rancher Desktop 
    - Engine は Lima
- OrbStack
    - 全員に配布するにはそこそこなお値段が・・・（動作は軽いのは魅力的）

## 手順

### Multipass のインストール

Docker Desktop や docker をインストールしていた場合は事前に削除しておくと良いかと思います。
Multipaass は homebrew を利用してインストール、もしくは公式サイトから[ダウンロード](https://multipass.run/download/macos)してインストールしましょう。

```
$ brew instll --cask multipass 
$ brew install docker docker-compose docker-buildx
```

docker は `--cask` オプションをつけないと、CLI のみがインストールできます。
インストール後は、ExtraDirs の設定を入れるのを忘れずにしましょう。

```.docker/config.json
{
        "cliPluginsExtraDirs": [
                "/opt/homebrew/lib/docker/cli-plugins"
        ]
}
```

DEPRECATED を出し続けるいけてない状態になります。
```
DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
            Install the buildx component to build images with BuildKit:
            https://docs.docker.com/go/buildx/
```

ハイパーバイザーのバックエンドの設定を確認。
```
$ multipass get local.driver
```
Mac のデフォルトは qemu になってるのでイントールしておきます。[^2] 
```
$ brew install qemu
```

### 設定ファイルの作成

Multipass は cloud-init をサポートしてます。 手動でコマンドうつより自動化しちゃいましょう。（過去の自分は他人です）
- 任意の公開鍵で default user で ssh アクセスできるようにする
    - Multipass の Open shell は default user を利用している
    - 自分のPC内の仮想マシンなのでよしとした
- docker cli 実行時に管理者権限がなくても実行できるようにする
    - デフォルトユーザーを必要なグループに追加
- 必要なソフトウェアのインストールする
    - avahi-daemon はなくても動きますが、ホスト名でアクセスできるようになるので追加しておくのがオススメです
- Docker Engine の設定を追加する
    - `TCP:2375` で Listen させる
    - Name Server として `8.8.8.8` を利用するように指定する

```multipass.yml
#cloud-config
locale: en_US.UTF8
timezone: Asia/Tokyo
package_upgrade: true
users:
  - name: ubuntu
    sudo: ALL=(ALL) NOPASSWD:ALL
    ssh-authorized-keys:
      - ssh-rsa YOUR-SSH-KEY-HERE

packages:
  - git
  - curl
  - jq
  - gcc
  - make
  - cmake
  - gnupg
  - uidmap
  - unzip
  - avahi-daemon
  - ca-certificates
  - lsb-release
  - qemu-user-static

bootcmd:
  - printf "[Resolve]\nDNS=8.8.8.8" > /etc/systemd/resolved.conf
  - [systemctl, restart, systemd-resolved]

runcmd:
  - curl -fsSL https://get.docker.com | sudo bash
  - [
      sed,
      -i,
      's/ExecStart=\/usr\/bin\/dockerd -H fd:\/\//ExecStart=\/usr\/bin\/dockerd -H tcp:\/\/0.0.0.0:2375 -H fd:\/\//',
      /lib/systemd/system/docker.service
    ]
  - [
      sed,
      -i,
      's/X11Forwarding yes/X11Forwarding no/',
      /etc/ssh/sshd_config,
    ]
  - [systemctl, daemon-reload]
  - [systemctl, restart, docker]
  - [systemctl, reload, ssh]

system_info:
  default_user:
    groups: [docker,adm]
```

sshの鍵をコピペするのも面倒なので、スクリプトにしておくのが良いかと思います。

```
$ cat cloud-init.sh
#!/bin/sh

cd $(dirname $0)
PUBLIC_KEY=${1:-~/.ssh/dev_ed25519.pub}
AUTHORIZED_KEYS=$(cat $PUBLIC_KEY)

cat << __EOF__
#cloud-config
locale: en_US.UTF8
timezone: Asia/Tokyo
package_upgrade: true
users:
  - name: ubuntu
    sudo: ALL=(ALL) NOPASSWD:ALL
    ssh-authorized-keys:
      - ${AUTHORIZED_KEYS}

（snip)

__EOF__
$ ./cloud-init.sh > multipass.yaml
```

cloud-init の設定ファイルを指定しつつ、VMインスタンスの立ち上げます。

```
$ multipass launch -c 2 -m 4G -d 60G -n docker 22.04 --cloud-init ~/multipass.yaml
```

cloud-init がちゃんと成功しているかは、以下のコマンドで確認できます。

```
$ multipass exec docker -- tail -1 /var/log/cloud-init.log
2023-12-01 19:20:55,036 - handlers.py[DEBUG]: finish: modules-final: SUCCESS: running modules for final
```
VMを削除したい場合は、以下が便利です。
delete でゴミ箱に移動、purge で完全に削除、と2回の操作が必要ですがコマンド一発で行うことができます。
確認もなく完全に削除されますのでご利用は計画的に。
 
 ```
 $ multipass delete docker --purge
 ```

### ホストOSのディレクトリをマウント
Dev Container を使う方、Docker Desktop に慣れている方は `Users` をマウントしておくのがオススメです。

```
$ multipass mount /Users docker:/Users
```

マウントを含めて作成した VM の情報は以下のコマンドで確認できます。

```
$ multipass info docker
Name:           docker
State:          Running
IPv4:           192.168.64.13
Release:        Ubuntu 22.04.3 LTS
Image hash:     41a8587b4994 (Ubuntu 22.04 LTS)
CPU(s):         2
Load:           0.05 0.09 0.09
Disk usage:     6.4GiB out of 58.1GiB
Memory usage:   355.9MiB out of 3.8GiB
Mounts:         /Users => /Users
                    UID map: 502:default
                    GID map: 20:default
```

VM の IPアドレスを取得するときには、以下のコマンドも便利です。

```
$ multipass info docker --format json | jq -r '.info["docker"].ipv4[0]'
```

### Docker Context の切り替え

[Docker Context](https://docs.docker.com/engine/context/working-with-contexts/) を利用することで複数のノードを切り替えることができます。
`multipass-docker` という context を追加し、デフォルトで利用するるように設定します。

```
$ docker context create multipass-docker --description "Multipass Docker VM" --docker "host=tcp://docker.local:2375"
$ docker context use multipass-docker
```

avahi-daemon をインストールしているのでホスト名を利用できます。VMの再作成をしても設定を更新する必要がないので便利です。

これで準備が完了です！
docker や docker compose が動くようになりました。早速 nginx を起動してみましょう。

```
$ docker run -it --rm -p 80:80 nginx
```
別のターミナルからアクセスしてみます。ホスト名でアクセスできます。

```
$ curl http://docker.local/
<!DOCTYPE html>
<html>
 :
```
ホスト名がうれしいのは、Cookie の検証がしやすくなるからです。
3rd Party Cookie の検証をするためには https を利用する必要があります。 このあたりをどのように解決しているか？ については、別の記事で触れたいと思っています。
## トラブルシュート

正直にいうと、ここからがこの記事のメインです。（短い！）
私の環境ではアプリケーションをビルドしたり実行しようとすると、コンテナからの通信がおかしいことがありました。

- 外部のサービスにアクセスできない
- あまり見たことないHTTPのステータスコードでAPIアクセスが失敗する
- イメージの build に失敗する

このあたりの切り分けと対策について、まとめておきたいと思います。

### 名前解決できてるか確認する

コンテナの中から外部サービスにアクセスできない場合は、ちゃんと名前解決できてるか確認してみましょう。

```
$ nslookup www.google.com
```

名前解決ができてない場合は、DNSまわりの設定が抜けている可能性があります。
nslookup / host などのコマンドはデフォルトの ubuntu / debian のイメージには含まれていないため、 dnsutils のインストールが必要です。[^3]

コンテナの実行時に dnsの設定をできるオプションは、問題の切り分けに利用できます。

```
$ docker run -it --rm --dns=8.8.8.8 ubuntu bash
```

### ビルド時に apt が失敗する

dnsutils を入れたイメージを作りたいと思ったが build オプションに --dns はない？ 詰んだ・・・と思ったら、ネットワークを `host` に切り替えてみてください。

```
$ docker build ./ -t test --network host
```

ワークアラウンドとしては、これが一番楽で早いと思います。

### タイムアウトする、普段はあまり見ないステータスコードで失敗する

い、意味が分からない・・・・という状態になったら [MTUの問題](https://tech.virtualtech.jp/entry/2022/06/28/184526) を思い出しましょう。


MTUの値を指定し ping[^4] を実行することで、効率よく通信が行えているか確認をすることができます。

```
$ ping -s パケットサイズ -M do 宛先
```

Mac の場合は少しだけオプションが異なります。

```
$ ping -D -s パケットサイズ 宛先 
```

たいていの場合 MTU は 1500 に設定されているかと思います。 

pingで送信するパケットサイズを指定した場合、指定したサイズ以外に28バイト（IPヘッダ 20バイト, ICMPヘッダ 8バイト)が追加されるためまずは 1472 から試して行きます。

```
$ ping -s 1472 -c 1 -M do www.google.com
```

パケットロスなどをしていたらビンゴです。最適な値を探していきましょう。

パケットサイズを少しづつ減らしていき通信可能なサイズを探していきます。
[docker0 ブリッジのMTUを調整](https://platform9.com/kb/openstack/no-connectivity-to-docker-containers-within-instance)をする必要があります。

```/etc/docker/daemon.json
{ "mtu": 1450 }
```

### よく落ちる（multipass が固まってる）

ハイパーバイザーのバックエンドの設定を確認し、必要なものをインストールされているか確認しましょう。
```
$ multipass get local.driver
```

qemu が未インストールでも、multipass のインスタンスは起動しdockerも利用できたのですが、定期的に multipass のインスタンスが固まる状態に悩まされていました。
僕の環境では qemu をインストールしたところ状況が改善しました。

## おわりに

Multipass を使った docker の開発環境を作る手順とトラブルシュートついて取り上げてみました。
この記事がやってみた！けどなぜか動かない・・・といったときの助けになれば幸いです。

明日の投稿もお楽しみに！

https://qiita.com/advent-calendar/2023/intimatemerger

[^1]: [トップページ](https://github.com/lima-vm/lima#warning-caution-make-sure-to-back-up-your-data)での注意書きがなくなったので解消してるかも？ （Fix されたcommit があって消した訳ではなさそう）
[^2]: （ちゃんと検証しきれてませんが） qemuインストール前は multipass のインスタンスが固まり purge するしかない状況になることが、そこそこの頻度で発生していました。
[^3]: Network関連を検証するだけなら必要なコマンドが組み込み済の busybox が便利です
[^4]: iputils-ping のインストールが必要です