複数のDockerComposeプロジェクトでLocalQualityの開発体験を得る

## はじめに

M1 Macを使い始めたことをきっかけに、コンテナベースのWeb開発に移行しました。
各実行環境をコンテナに隔離しつつ、エディタや各種GUIクライアントとの連携において、ローカルと同等の開発体験を得るために行ったことと、途中でハマったことを書いていこうと思います。

## 環境
* M1 Mac Monterey 12.3
* Docker Desktop 4.6.1
* Visual Studio Code 1.66.1
* Remote - Containers 0.231.5（本記事ではこちらの設定は割愛します。）

## 前提と要件
今回対象にしたのは、下記のような構成の既存のWebサービスです。
開発体験という観点で下記を要件として設定します。

要件①：コンテナA-webのHTMLレスポンスに含まれる、コンテナB-webのiframeをホストのブラウザで表示できること。
要件②：ホストからコンテナA-dbとコンテナB-dbに接続できること。
![image-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365169/71b78c47-c2e3-5aaf-7f64-7c4a5b8930f2.png)

## 結論

### ホスト → コンテナA-web
至極一般的ですが、ポートバインディングしたうえで、コンテナA-webのDjangoアプリケーションを`0.0.0.0:8000`でLISTENすれば、ホストからは当該アドレスでコンテナA-webに到達可能です。
```yml
# docker-compose-A
services:
  web:
    ports:
      - 8000:8000
```
```bash
# コンテナA-web
python manage.py runserver 0.0.0.0:8000
```
```sh
# Host
curl http://0.0.0.0:8000
```

### コンテナA-web → コンテナB-web
異なるDocker Networkに属するコンテナ同士では通信ができないので、同じネットワークに接続します。今回はdocker-compose-Bのネットワークで統一しました。
`external: true`によって、`network-B`が`docker-compose-A`のライフサイクルの外側で管理されていることを示し、新たなリソースとして作成されることを防ぎます。
```yml
# docker-compose-A
services:
  web:
    networks:
      - network-B
  db:
    networks:
      - network-B

networks:
  network-B:
    external: true
```

そのうえでコンテナA-webからコンテナB-webに対しては、ネットワークエイリアス（`B-web`）で通信します。エイリアスは他にもあるので、適当なものを選びます。
```sh
docker inspect ContainerB
```
```
"NetworkSettings": {
    "Networks": {
        "network-B": {
            "Aliases": [
                "xxxxxxxxxx",
                "B-web"
...略
```
```sh
# コンテナA-web
export SERVICE_B_ORIGIN="http://B-web:8001"
```

### ブラウザ → コンテナB-web （iframe）
前提として関連サービスであるBのURLは、上記で設定した環境変数を元に組み立てるので、コンテナA-webのレスポンスに含まれるiframeのsrcは、コンテナB-webのネットワークエイリアスとなります。これをホストが名前解決できるように、ホストの `/etc/hosts` を編集します。
```
# /etc/hosts
0.0.0.0 B-web
```
かつコンテナB-webのDjangoは`0.0.0.0:8001`でLISTENします。
すべてのネットワークインターフェイスにバインドすることで、コンテナA-webからの通信が可能になります。
```sh
# コンテナB-web
python manage.py runserver 0.0.0.0:8001
```

https://stackoverflow.com/questions/20778771/what-is-the-difference-between-0-0-0-0-127-0-0-1-and-localhost

### ホスト → コンテナA-db, コンテナB-db
それぞれを違うポートで起動し、ポートバインディングしました。
一度Host networkでの起動を試みましたが、断念しています。詳細については続きをご参照ください。
```yml
# docker-compose-A
services:
  db:
    ports:
      - 5432:5432
    command: -p 5432

# docker-compose-B
services:
  db:
    ports:
      - 5433:5433
    command: -p 5433
```

## ハマったこと
### Host Networkで起動したPostgreSQLコンテナに接続できない。
2つのdocker-composeを同じネットワークに接続しなくとも、コンテナA*をHost networkで起動すればいいんでね？とやってみましたが、ホストからPostgreSQLコンテナに接続することができませんでした。
```yml
# docker-compose-A
services:
  web:
    network_mode: host
```
```sh
# Host
psql -U foo -d bar -p 5432

# Is the server running locally and accepting connections on that socket?
```

Dockerのドキュメントを改めて読んだところ、Host networkはLinuxのみでMacはサポートされていないという記述がありました。
```
The host networking driver only works on Linux hosts, and is not supported on Docker Desktop for Mac, Docker Desktop for Windows, or Docker EE for Windows Server.
```

https://docs.docker.com/network/host/

MacやWindowsの場合、docker daemonはVM上で動作するため、`network_mode: host`はVM自身を指すことになり、手元のMacからはアクセスできないということだと解釈しています。つまりMacとDockerコンテナの連携は、ポートバインディング一択となりそうです。

## 終わりに
今回は主にdocker-composeの設定周りが主となりましたが、LocalQualityの開発体験を得るには、これらに加えてエディタとコンテナの連携が必須と思います。Visual Studio CodeのRemote Containersの個人的なベストプラクティスについては、別記事でまとめられればと思います。