docker.sockの権限を変更せずにDooDを実現する

# はじめに
DooD(Docker outside of Docker)において、ホストOSのdocker.sockの権限を変更せずにコンテナ上でdockerコマンドを叩けるようにする方法を紹介します。

:::note warn
可能な限り正しい情報を書こうとしていますが、あくまで参考程度にご覧ください。正確な情報は公式ドキュメント等をご覧ください。
:::

# 達成したいこと
ホストOS上でコンテナを立ち上げ、そのコンテナで動くアプリは、root以外のユーザーでホストOSのdockerを操作することができる。これを、ホストOSのdocker.sockの権限を変更せずに実現したい。(セキュリティ上、rootユーザーでの実行や任意ユーザーへの権限付与は最小にしたいため。)

# 要旨
コンテナ立ち上げ時にdockerの`--group-add`オプションで、ホストOSのdockerグループのGIDを指定することで、達成できる。docker-compose.ymlの場合は`group-add`オプションと環境変数を使う。

# DooDとは？
Docker outside of Dockerの略で、起動したコンテナからホストOSのDockerデーモンを使って、別のコンテナを立ち上げるなど、ホストOSのdockerを操作する方法です。少し話がそれますが、よく比較される方法にDinD(Docker in Docker)という手法もあり、こちらはコンテナにDocker Engineがインストールされ、コンテナ上に別のコンテナを立てることができます。2つの違いについては、こちらの記事[^1]が図などを使って説明しており、わかりやすかったです。

さて、DooDを実現するには、以下のことをする必要があります。

1. 立ち上げるコンテナにDocker CLIをインストールする
2. ホストOSのソケットファイル(docker.sock)をコンテナにバインドする
2. コンテナの実行ユーザーがソケットファイルを読み取れるようにする

それぞれ見ていきましょう

## 1. Docker CLIをインストールする
1については、そもそもCLIがないとdockerコマンドが叩けないので、立ち上げるコンテナにインストールしておく必要があります。公式[^2]を参考に、以下のコードを立ち上げるコンテナのDockerfileに書くことでインストールすることができます。(ベースイメージのOSはubuntuを仮定しています)
```dockerfile:Dockerfile
# Add Docker CLI to ubuntu OS
RUN apt-get update && apt-get install ca-certificates curl gnupg \
    && install -m 0755 -d /etc/apt/keyrings \
    && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
    && chmod a+r /etc/apt/keyrings/docker.gpg \
    && echo \
    "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
    $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
    tee /etc/apt/sources.list.d/docker.list > /dev/null \
    && apt-get update && apt-get install \
    && docker-ce-cli
```

## 2. docker.sockをバインドする
docker.sockは、DockerデーモンがリッスンしているUNIXソケットで、Docker APIのメインのエントリポイントです。よって、DooDを実現する場合、このソケットファイルをコンテナにバインドしておく必要があります。Docker Engineをインストールすると、デフォルトだと`/var/run/docker.sock`にファイルがあると思います。
例えば、コンテナを立ち上げるときに以下のようにバインドします。
```docker:command
docker run -it -v /var/run/docker.sock:/var/run/docker.sock sample
```

## 3. ソケットファイルを読み取れるようにする
2でソケットファイルをバインドして、実行ユーザーをroot以外(例えばubuntu)にしてコンテナを立ち上げ、コンテナ上でいざdockerコマンドを実行してみると、以下のようなエラーが出ます。
```bash
Got permission denied ... /var/run/docker.sock: connect: permission denied
```
ホストOSのソケットファイルの権限を見てみると以下のようになっています。
```bash:command
$ ls -lh /var/run/docker.sock
srw-rw---- 1 root docker 0 Dec 19 08:59 /var/run/docker.sock
```
つまり、docker.sockをrw権限があるのは、rootとdockerグループに属しているユーザーのみなので、permissionエラーがでています。このエラーを解決する方法が以下の3つあると思います。

3-1. コンテナの実行ユーザーをrootにする
3-2. その他のユーザーにdocker.sockのread権限を与える
3-3. コンテナの実行ユーザー(root以外)をホストOSのdockerグループ(正確には、ホストOSのdockerグループと同じGIDを持つグループ)に加える

このうち、3-1, 3-2は達成したいことと反するため、採用しないとします。ということで3-3を実現したいわけですが、3-3の実現方法としても2つあると思います。

3-3-1. Dockerイメージをビルドする際に、コンテナユーザーをホストOSのdockerグループに加える。
3-3-2. Dockerコンテナを立ち上げるときに、コンテナユーザーをホストOSのdockerグループに加える。

はじめは、3-3-1で検討していたのですが、dockerグループのGIDは一定でないため、ビルド時にあらかじめホストOSのdockerグループのGIDを知っておく必要があります。つまり、ビルド環境に依存してしまうため、この方法は断念しました。
色々やり方を調べていたところ、コンテナを立ち上げるときに、指定したGIDのグループにコンテナの実行ユーザーを追加できる機能があることを知りました。`--group-add`オプション[^3]です。

このオプションを使ってコンテナを立ち上げると、`--group-add`で指定されたGIDのグループがコンテナ上で作成され、かつそこにコンテナの実行ユーザーが追加されます。よって、ホストOSのdockerグループのGIDを指定すれば、コンテナの実行ユーザーがバインドされたdocker.sockを読み書きすることが可能になります。

例えば、以下のようなコマンドになります。`--group-add`のshell式はホストOSのdockerグループのGIDを抜き出すためのものです。
```docker:command
docker run -it -v /var/run/docker.sock:/var/run/docker.sock --group-add $(grep docker /etc/group | cut -d: -f3) sample
```

### docker-compose.ymlで書くとどうなるか？
普段はコンテナの設定をdocker-compose.ymlに書いているので、その方法も調べました。結論としては、`group-add`オプション自体はありますが、docker-compose.ymlでshell式を使うことができないので、一度、環境変数(例えばDOCKER_GID)にホストOSのdockerグループのGIDを書き出し、それを使うという方法です。

```yml:docker-compose.yml
version: "3.9"
services:
  sample-app:
    image: sample-app:latest
    volumes:
      - type: bind
        source: "/var/run/docker.sock"
        target: "/var/run/docker.sock"
    group-add:
      - ${DOCKER_GID}
```

```bash:command
$ DOCKER_GID=$(grep docker /etc/group | cut -d: -f3)
$ docker compose up -d
```
ちなみに、group-addオプションについては、こちらのIssues[^4]から知りました(一度削除されたが、復活したみたいです)。また、docker-compose.ymlでshell式が使えないか調べてみたのですが、こちらは見つけることができなかったので、一度環境変数を設定する方法となりました。過去にshell式が使えないか議論しているIssues[^5]もあり、Closeされていますが、見る限り解決されていないと思います。

# 終わりに
なるべく権限変更などをせずに、DooDを実現する方法を紹介しました。少しでも役立つ情報があれば幸いです。また、今回DooDにトライしたのが初なので、もっといい方法があるよという方はシェアいただけると助かります。

# 参考
[^1]: https://qiita.com/t_katsumura/items/d5be6afadd6ec6545a9d
[^2]: https://docs.docker.com/engine/install/ubuntu/
[^3]: https://docs.docker.jp/engine/reference/run.html#additional-groups
[^4]: https://github.com/docker/compose/issues/3328
[^5]: https://github.com/docker/compose/issues/4081