Docker ネットワークで遊ぶ

ソフトウェアエンジニアって物理現象を相手にするの嫌いですよね(偏見)。LAN ケーブル買ってきて断線とかしてたら最悪。ってことで今日は Docker network で仮想的なネットワークを作って遊んでみる。秋葉原で何も買ってこなくてよいので楽チン。

Docker network の機能を使うと、起動した Docker コンテナが他のコンテナと通信できたりする。もちろん外部との通信もできる。今回は Docker network のうち host ネットワークと bridge ネットワークを使って遊んでみたメモ。ホストマシンは ubuntu 20.04。

簡単なサンプルサーバーを作る

Docker を起動する準備として、まずは nodejs の express フレームワークを使ってシンプルな Web アプリを作ってみる。 3150 番ポートにアクセスされると Hello World! と返すだけのサーバアプリである。以下の通りに index.js を実装した。

const express = require('express')
const app = express()
const port = 3150

app.get('/', (req, res) => {
    res.send('Hello World!\n')
})

app.listen(port, () => {
    console.log(`Example app listening at http://localhost:${port}`)
})

この Web アプリの依存関係をインストールするための package.json は以下。

{
  "name": "exp_server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.1"
  }
}

index.js と package.json を同じディレクトリに置いて npm install を実行すれば自動的に依存ライブラリがインストールされる。とはいっても、今回はそのインストールを Docker イメージのビルド時にやってしまうため、ただ2つのファイルを手元に作っておけばよい。

今回用いるコンテナイメージを作成するための Dockerfile は以下の通り。

FROM ubuntu:18.04

RUN apt update \
    && apt install -y sudo curl \ 
    && curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - \
    && sudo apt-get install -y nodejs

WORKDIR /root
COPY index.js /root
COPY package.json /root

RUN npm install

CMD node index.js

ubuntu:18.04 をベースとして 、nodejs や npm を NodeSource からダウンロードしてきて、コンテナ内にインストールする。Docker Hub の node イメージ を使ってもよいのかもしれないが、ubuntu からコマンド叩くほうが個人的には何やってるか分かりやすいので今回はこの形を採用した。

index.js と package.json を手元に置いて、以下のコマンドを実行することにより exp_ubuntu という名前のコンテナイメージをビルドする。

$ docker build -t exp_ubuntu .

しばらく待ったらコンテナイメージのビルドが完了する。以下のコマンドで確認できる。

$ docker images
REPOSITORY    TAG       IMAGE ID       CREATED              SIZE
exp_ubuntu    latest    c8f01578eecc   About a minute ago   270MB

これで Docker コンテナを起動する準備が完了した。

host ネットワーク

まずは host ネットワークから作ってみる。以下のページを参考にした。

Use host networking | Docker Documentation

host ネットワークは Docker を動かしているホストマシンと同一のネットワークである。host ネットワーク上でコンテナを起動することは、Docker コンテナをホストマシン上の1つのアプリとして実行するような感じになる。

以下のコマンドにより、先ほど作成した Web アプリのコンテナイメージを host ネットワーク上で起動する。server1 というコンテナ名を付けている。--network host というオプションによって、接続するネットワークとして host を指定しているのがポイント。

$ docker run -d --network host --name server1 exp_ubuntu
$ docker container ls
CONTAINER ID   IMAGE        COMMAND                  CREATED         STATUS         PORTS     NAMES
4975bdef4adb   exp_ubuntu   "/bin/sh -c 'node in…"   8 seconds ago   Up 6 seconds             server1

ブラウザから http://localhost:3150/ にアクセスすると Hello World! というメッセージが表示される。3150 番のポートでリクエストを受け付けていることが確認できた。

ちなみに、これはホストマシン上で以下のコマンドを起動したときの挙動と全く変わらない。

$ node index.js

host ネットワーク上のコンテナとして Web アプリを起動する利点は、環境構築をコンテナという隔離されたところでできる点だけでなく、性能の最適化を行うときに便利だったりするらしい。

起動中のコンテナの中身を覗いてみる。lsof コマンドによって 3150 番のポートで LISTEN されていることが確認できる。

$ docker exec -it server1 bash
~# apt install -y lsof
~# lsof -i
COMMAND PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
node      7 root   18u  IPv6 706707      0t0  TCP *:3150 (LISTEN)
~# exit

docker network inspect コマンドで host ネットワークの状況を確認できる。

$ docker network inspect host
[
    {
        "Name": "host",
        ...
        "Containers": {
            "4975bdef4adbdf2b520d9b0d35838e46dc9cdc6ab4e31496830ae7625c93e55e": {
                "Name": "server1",
                "EndpointID": "75088badf4fd62ed7edf0f1265a0177214ddcfa9e172f961b9ff143f7f8feb2d",
                "MacAddress": "",
                "IPv4Address": "",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

Web アプリを起動している server1 コンテナがネットワーク上に登録されていることが確認できた。

最後に server1 を停止してコンテナを削除する。

$ docker container stop server1
$ docker container rm server1

bridge ネットワーク

つぎに bridge ネットワーク上でコンテナを起動してみる。bridge ネットワークはもっともオーソドックスな Docker ネットワークであり、同じネットワーク内のコンテナ同士の通信を実現する。ホストマシンとは異なるネットワークであるのがポイントである。以下を参考にした。

Use bridge networks | Docker Documentation

bridge ネットワーク上で Web アプリを起動してみる。せっかくなので2つのコンテナを起動してみる。それぞれ server1, server2 という名前を付けている。--network bridge というオプションによって bridge ネットワーク上でコンテナを起動することを指定している。ただし bridge ネットワークはデフォルトのネットワークなので、このオプションは無くてもよい。

$ docker run -d --network bridge -p2001:3150 --name server1 exp_ubuntu
$ docker run -d --network bridge -p2002:3150 --name server2 exp_ubuntu

-p2001:3150 というオプションによって、ホストマシン側に公開するポートを指定している。具体的には、ホストマシン側の 2001 番ポートが bridge ネットワーク側の 3150 番ポートに対応する。実際、http://localhost:2001/http://localhost:2002/ にアクセスすると Web アプリにアクセスできる。

bridge ネットワークを inspect してみる。

$ docker network inspect bridge
[
    {
        "Name": "bridge",
        ...
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16"
                }
            ]
        },
        ...
        "Containers": {
            "328253b90470aa98c8419aec36c36610b113e6c2393ce5fd3ff37c261efb366d": {
                "Name": "server1",
                "EndpointID": "0d26a79bd7969059afc12765a4e207887c6ffccfda7d2bdf93162ae3df7e4d76",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            },
            "99b67c70283e1f1111430bb2c6c126e967a422522196ed636356b4be2c7e9642": {
                "Name": "server2",
                "EndpointID": "a7167ff0b5562125fb14a07872d4fb96eb9c0ca4a2b017a68892ff1ba864a945",
                "MacAddress": "02:42:ac:11:00:03",
                "IPv4Address": "172.17.0.3/16",
                "IPv6Address": ""
            }
        },
        ...
    }
]

server1server2 はそれぞれ、bridge ネットワーク内の IP アドレス 172.17.0.2172.17.0.3 が割り当てられていることが分かる。

ポートの対応関係は docker ps コマンドで確認できる。PORTS 列にまさにポートの対応関係が示されている。

$ docker ps
CONTAINER ID   IMAGE        COMMAND                  CREATED         STATUS         PORTS                    NAMES
5385113b3b6d   exp_ubuntu   "/bin/sh -c 'node in…"   7 minutes ago   Up 7 minutes   0.0.0.0:2002->3150/tcp   server2
5717a0502c50   exp_ubuntu   "/bin/sh -c 'node in…"   7 minutes ago   Up 7 minutes   0.0.0.0:2001->3150/tcp   server1

ちなみに、ホストマシン上で ip コマンドを叩くと docker0 という仮想ブリッジが存在することが確認できる。ホストマシンと bridge ネットワーク上のコンテナはこのブリッジ経由で通信される。

$ ip a show docker0
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:36:1e:b2:60 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:36ff:fe1e:b260/64 scope link 
       valid_lft forever preferred_lft forever

所感

手軽に Docker コンテナ同士をネットワークで接続できるのでいろいろ実験してみたくなる。自分でネットワーク構成考えてみて、各ネットワークにコンテナを配置する遊びとかできそう。簡易的なロードバランサーとか実装してみたくなった。