Dockerのネットワーク、基礎理解は万全ですか？【Dockerコンテナ・グレートジャーニー④】

# Dockerコンテナグレートジャーニー
Dockerコンテナを０から理解する旅、Dockerコンテナ・グレートジャーニー第4回です。
今回はDockerのネットワークについて、少しだけ深い部分に踏み込みつつ、イメージを交えてカジュアルに解説していきます。ネットワークをなんとなく使うのは卒業して、しっかりとしたイメージの土台の上で考えられるようになりましょう！

## 対象
* Dockerのネットワークについてよく知らない人
* イメージ図を交えてインプットしたい人
* Dockerコマンドを、単なる暗記ではなくきちんと理解したい人

## 旅路（インデックス）
長いので記事を分割しています。
1. [そもそも仮想化とは？　仮想化ではないシステムとは？](https://qiita.com/Toyo_m/items/3060fc5961d62a840bee)
1. [Dockerコマンド基礎（環境構築～ubuntu/httpdコンテナでのコマンド実行）](https://qiita.com/Toyo_m/items/94de894a8558d94b02d1)
1. [Dockerのストレージについて](https://qiita.com/Toyo_m/items/95a2543c07dc5b126208)
1. Dockerのネットワークについて<font color=orange>【⇦本記事】</font>
1. [Docker-compose でコンテナをまとめる](https://qiita.com/Toyo_m/items/ef14658629d3118003ea)
1. [Docker image と Dockerfile](https://qiita.com/Toyo_m/items/5713f85a9d5ff8d9cc0d)
1. [AWS ECS で Dockerコンテナを走らせる](https://qiita.com/Toyo_m/items/90df3c90353ed6fb3fd8)

# 【本題】Dockerのネットワークについて
Dockerのネットワークは使用するだけなら難しくはなく、コンテナ間通信をするにも2つほどコマンドを暗記すれば問題なく使用できます。
とはいえ、中身で何が起こっているのかや、なぜこのコマンドが必要なのか、といった基礎的なことをすっ飛ばしていくと、設計に精彩を欠いたり意図せぬバグに遭遇して解決策が浮かばなかったりするでしょう。
というわけで、今回はDockerがなぜつながるのかというところから、実際にネットワークを動作させるまでを解説していきます。

* __前提__
今回はIPやポート、それに関連する用語について触れますが、一般的な技術であるため解説しません。この辺りがよくわからない方は、まずはそちらを見ていただくか、この記事をふわっとニュアンスでとらえていただければと思います。

## なぜ`-p 8080:80`のようにポートを指定するだけでコンテナと通信できるようになるのか？
以前の記事（[Dockerコマンド基礎](https://qiita.com/Toyo_m/items/94de894a8558d94b02d1#docker-%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B-httpd%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%81%A7%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%82%92%E8%B5%B7%E5%8B%95%E3%81%99%E3%82%8B)）で解説したの基本コマンドを再掲します。
``` shell
docker container run -d --name server_test -p 8080:80 httpd:2.4
```
このコマンドを実行すると、ホストの8080番ポートとコンテナの80番ポートがポートフォワーディングされ、コンテナに接続できるのでした。ブラウザで接続してみると以下のような感じになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/fab10aa2-d8dc-c3f4-ef09-1fef64eb2b98.png)

なぜサーバーからファイルが取得できたのかということで、以前は下の図のような解説をしていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/fab38f17-8a6f-633c-2492-5d96fc15f77d.png)

この図から、あたかもホストの8080番ポートとコンテナの80番ポートが接続されます。ですがこの図では、実はIP層が省略されています。
それを確認するために、まずはDockerにはネットワークの状態を確認します。コンテナの各種設定を確認する`docker container inspect <コンテナ名>`というコマンドがあります。先ほどのhttpdコンテナを起動した状態でコマンドを実行してみます。
以下はコマンド結果です。かなり長文で返されるので、関係のある"Network"の部分だけ抜粋しました。
```
・・・(略)・・・
            "Name": "/server_test",
            ・・・(略)・・・
            "Networks": {
                "bridge": {
                    ・・・(略)・・・
                    "Gateway": "172.17.0.1",
                    "IPAddress": "172.17.0.2",
                    ・・・(略)・・・
                }
            }
```
IPとポートが以下のような構成であることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/f4442bf7-60f2-4024-ed79-6aae8d74c487.png)
ホストの8080番ポートと80番ポートが直接つながっているのではなく、Docker内に割り振られたIPによる通信を挟んでいるのです。

またDockerネットワーク全体にも別途IPが割り振られています。私の環境では、Dockerのネットワークたる`vEthernet`には`192.168.144.1/20`というローカルアドレスが割り振られていました。これはWindowsであれば`ipconfig`コマンドを打つとわかります。以下はコマンド結果です。
``` shell
イーサネット アダプター vEthernet (WSL):
   接続固有の DNS サフィックス . . . . .:
   リンクローカル IPv6 アドレス. . . . .: **********
   IPv4 アドレス . . . . . . . . . . . .: 192.168.144.1
   サブネット マスク . . . . . . . . . .: 255.255.240.0
   デフォルト ゲートウェイ . . . . . . .:
```

というわけなので、Dockerのネットワークは`192.168.144.1/20`の内部に構築されているネットワークになります。Docker内のネットワークはホストとは独立しており、ホストから「ping 172.17.0.2」と直接打ち込んでもコンテナ内には通信できないということです。

画像にするとこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/2881b4bb-37f1-18fc-bb45-9fc204ae8a4b.png)

ともあれ、難しい話も挟みましたが、重要なのはDockerネットワーク内のコンテナには、ホストのネットワークとは別のローカルIPアドレスが割り振られているということですね。
これがわかれば、コンテナ間での通信が実現できます。

## コンテナ間通信
DockerコンテナにIPアドレスが割り振られていることがわかりました。同一ネットワーク内でIPアドレスとポートがわかっていればコンテナ同士で色々な通信ができます。

通信のテスト環境として今回は
* ubuntuコンテナ
* httpdコンテナ

の2つのコンテナを立ち上げて、実際に通信をしてみましょう。
さっそく以下のコマンドで、2つのコンテナを立ち上げます（先ほど作成したhttpdコンテナはそのままでも構いません）。
``` shell
docker container run -it --name linux_test ubuntu:22.04
# 今回作成するコンテナは外部からアクセスしないので、`-p 8081:80`を付けていません。
docker container run -d --name server_local httpd:2.4
```

現状のイメージはこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/cf591880-562c-f3c3-8fbd-a893ee4816c3.png)
（※IPの割り当ては異なる可能性があります。新しいコンソールで`docker network inspect bridge`とするとIPが確認できます。このコマンドの意味は付録で解説します）

Ubuntuコンテナに入った状態で、pingを送ってみます。
例のごとくUbuntuコンテナにはpingがインストールされていないので、インストールから始めます。あとで使うのでついでにcurlもインストールします。
``` shell
apt-get update
apt-get install inetutils-ping
apt-get install curl
```
インストール出来たら、実際にpingコマンドで疎通確認します。
``` shell
ping 172.17.0.1 # ゲートウェイに対して
ping 172.17.0.4 # 今回立てたサーバーに対して
```

Ubuntuからサーバーコンテンツにアクセスしてみます。ホストからの通信では、localhostの8080番ポートでしたが、Docker内で通信を行う場合はコンテナのIPアドレスを直接指定して、かつ80番ポートを直接指定してください。`127.~~`と`172.~~~`で紛らわしいですので注意してください（筆者は2敗中）
``` shell
curl http://172.17.0.4:80
```
`<html><body><h1>It works!</h1></body></html>`と帰ってくれば成功です。外部とは繋がっていないhttpdコンテナでも、内部からならアクセスできましたね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/3c54b6f3-2dd5-ec17-abb1-94cad8e7f983.png)

## IPアドレスを指定する方法の欠点
先ほどIPアドレスを指定して通信しましたが、これは実際に運用する場合の実装としては悪手です。
なぜなら、コンテナにどのIPアドレスが割り振られるかは固定ではないからです。
（`--ip`オプションを使えば固定にできるようですが、運用上の制限が大きく柔軟性がありません）

例えば、コンテナを削除して立ち上げなおした結果、以下のようなIP構成になったとします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/80bcc486-8a9e-9549-7ae3-f2f792b4a559.png)
先ほどはubuntuコンテナから`172.17.0.4:80`を指定して通信していましたが、IP構成が変わることで自分自身のIPになってしまっています。
(一応`--ip`オプションを使えばIPを固定にできますが、運用上は好ましくありません)

### コンテナ名を指定して通信する
IPの指定はリスクがあったので、コンテナ名「server_local」を指定して通信ですよね。
`Docker run --link`オプションを使用すれば、コンテナ名を指定した通信ができます。
試しに実験してみましょう。ubuntuコンテナを削除して作り直します。
（※公式では`--link`オプションは非推奨なのですが、ネットワークの動作をステップ倍ステップで理解するにはちょうどいいので使用します）

``` shell
# コンテナ削除
docker container stop linux_test
docker container rm linux_test
# --link オプションを使用して再作成
docker container run -it --link server_local:server_local --name linux_test ubuntu:22.04
```
Ubuntuに入るので、IPアドレスではなくコンテナ名を指定して通信してみましょう。
（パッケージを再インストールするのを忘れずに。。。）
``` shell
apt-get update
apt-get install curl
curl http://server_local:80
```
`<html><body><h1>It works!</h1></body></html>`と帰ってくれば成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/eafdf6d6-3c60-92a6-d9c2-82590022f695.png)
これで、IPアドレスが変わってもコンテナ名が同じであれば通信できます。

なのですが、、、最初に述べた通りこの`--link`オプションはdockerの公式から非推奨となっています。
現在は通信したいコンテナが1つなのでいいですが、例えばコンテナが10, 100, 1000....と増えたとき、どうなってしまうでしょうか？　--linkコマンドを1000個並べますか？　考えたくもないですね。

もちろんこの問題を解決するるベストプラクティスも存在するので、最後にそれを説明します。

# Dockerのネットワークのベストプラクティス
本記事の本題にして、Dockerの推奨設定にして、最も簡単なセクションです。

Dockerでオリジナルのネットワークを定義するのに必要なのは、下記のコマンドだけです。
``` shell
docker network create my_network
```
これでネットワークが作成されました。とっても簡単ですね。
ではこのコマンドを打ったことで具体的に何が起こったのかを説明します。まずは作成したネットワークの詳細を見ていきます。下のコマンドは、ネットワークの詳細を確認するコマンドです。
``` shell
docker network inspect my_network
```
これを実行すると、今しがた作成したネットワークのゲートウェイとそのIPアドレスを確認できます。私の環境では「172.19.0.1」がゲートウェイになっていました。
以上を踏まえてネットワーク図を書くとこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/f5e3005a-4766-4e42-3070-c4ee7fbd62bc.png)
これだけでは中身が空のネットワークが作成されただけなので、コンテナをこのネットワークに参加させていきます。
コマンドは、コンテナの起動時に`--net`オプションを付与すればOKです。
``` shell
docker container run -d --name httpd_net_1 --net my_network httpd:2.4
docker container run -d --name httpd_net_2 --net my_network httpd:2.4
```
なおDockerのネットワークの場合はコンテナを作り直さなくても、既存のコンテナをアタッチすることもできます。connectコマンドで先ほど作ったUbuntuコンテナをネットワークに参加させます。
``` shell
# docker network connect <ネットワーク名> <アタッチするコンテナ名>
docker network connect my_network linux_test
```
これで新しく作成したhttpdコンテナ2つとubuntuコンテナ(linux_testという名前)が、オリジナルのネットワークに参加しました。
現状を図にするとこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/16852ec9-c8f9-dffd-96a1-36cdf9077f89.png)
下記コマンドで、ネットワークに参加していることを確認できます。
``` shell
docker network inspect my_network
```

### 作成されたネットワーク内でのコンテナ間通信
先ほどはわざわざ`--link`コマンドを使用してコンテナ名での通信を行っていましたが、`docker network create`で作成されたネットワークには __同一ネットワーク内であればデフォルトでコンテナ名を指定した通信できる__ というメリットがあります。
さっそく試してみましょう。

```
# ubuntuに入る
docker exec -it linux_test /bin/bash
# サーバー１、２にアクセス
curl http://httpd_net_1:80
curl http://httpd_net_2:80
```
今回も「`<html><body><h1>It works!</h1></body></html>`」と表示されれば成功です。
`docker network create`とするだけで、DNS機能の完備されたnetwork範囲が生成されるのですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/6b04e9e1-40ab-d206-0a9a-52bbdf9b51ce.png)

なお余談ですが、コンテナは複数のネットワークに同時に参加できます。あらかじめネットワークを作っていれば、
```
docker network connect my_net_1 my_container
docker network connect my_net_2 my_container
docker network connect my_net_3 my_container
```
ですべてのネットワークに参加可能です。

<br><br>
というわけで、`docker network create`コマンドを使用したネットワーク運用が、Dockerでは基本となります。
これだけのことを知るのにだいぶ遠回りをしましたが、遠回りのぶんだけDockerネットワークに関する解像度が上がっていれば幸いです。

## その他のDockerネットワーク知識
### デフォルトで用意されているネットワーク
あなたがDockerをインストールしたその時から、実はデフォルトで3種類のネットワークが既に用意されています。
試しに、Docker内のネットワーク一覧を表示する`docker network ls`コマンドを打ちこんでみてください。
```
PS C:\Users\******> docker network ls
NETWORK ID     NAME           DRIVER    SCOPE
fd654a676b24   bridge         bridge    local
6347e4cfd60d   host           host      local
d47b704a415e   my_network     bridge    local
f2d893fb1873   none           null      local
```
今回作成した`my_network`以外にも、`bridge`,`host`,`none`というネットワークが用意されています。
実運用上では主にbridgeが用いられるため、デフォルトで用意されているbridgeネットワークについて軽く説明します。

#### Bridgeネットワーク
docker network create を実行することで、ユーザー定義のネットワーク領域が生成されることがわかりました。ですが、今までネットワークを指定せずに`Docker run`をしてきても、ネットワークには一応参加できていましたね。
これは、Dockerが最初から提供しているデフォルトのネットワークが存在するからです。このデフォルトのネットワークを『bridge』と呼び、IPで通信はできるものの、DNSによる名前解決はできないようになっています。
下画像の黄色い部分が、今回のbridgeネットワークになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/848b8daa-12d5-d3d7-d9e3-6f73ae5818c0.png)

#### host, none
* host: ホスト側とすべてのポートを1対1で関連付けるネットワークです。
* none: 外部とのネットワークを完全に遮断しているネットワークです。

これらのネットワークは、Bridgeと違ってIPアドレスを持ちません。

### ネットワークの詳細をチェックするコマンド
記事中で`docker network inspect bridge`というコマンドについて触れましたが、ここまで来たならコマンドの意味は分かるかと思います。
* docker network inspect: ネットワークの詳細を表示するコマンド
* bridge: Dockerにデフォルトで用意されているネットワーク

つまり、Dockerにデフォルトで用意されているネットワークの詳細を確認した、ということになります。

### オーバーレイネットワーク
この単語について簡単に説明します。
最近ではコンテナを単体で扱うのではなく、クラスタ単位で扱うオーケストレーションツールが主流になっています。オーケストレーションツールを使ったコンテナ制御では、物理的的に離れた場所にあるコンテナ同士も連携させることができます。
このような場合に活用されるのが、オーバーレイネットワークです。オーバーレイネットワークにより、物理的に離れたネットワーク同士も一つのネットワーク上にあるかのように扱うことができます。
以下のサイトなんかが参考になりそうでした。

https://developers.cyberagent.co.jp/blog/archives/3132/#:~:text=Docker%E3%82%AA%E3%83%BC%E3%83%90%E3%83%BC%E3%83%AC%E3%82%A4%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF%E3%81%AF%E3%83%9E%E3%83%AB%E3%83%81,%E3%81%99%E3%82%8B%E3%81%93%E3%81%A8%E3%81%8C%E5%8F%AF%E8%83%BD%E3%81%A7%E3%81%99%E3%80%82

# 最後に
以上でDockerネットワークに関する話は終了です。
Dockerのネットワークは深く知らなくても`docker network create ~~~`コマンドと`--net ~~~`オプションの基本的な使い方さえ理解していれば大抵は問題ないでしょう。しかし、ネットワークの内部動作を理解していると、問題発生時の対処やシステムの進化に対応する能力が大きく向上するはずです。
ですので、Dockerのネットワークに触れる際は今回の話を是非思い出してみてください。

また、今回でDockerをコンテナの基本的な講義は終了です。第1回から読んでいただいた方は本当にお疲れ様です。
次回からはDocker-composeやDockefileのような上位の技術に触れていくのでお楽しみに！
（次回更新までは少し期間が空くかと思います。ご了承ください……）

# 参考

公式サイトのネットワークの説明。日本語なのでとてもありがたい。

https://docs.docker.jp/engine/reference/commandline/network_inspect.html

https://docs.docker.jp/v1.12/engine/userguide/networking/default_network/binding.html

IPアドレスを指定したコンテナの立ち上げ方について参考にしました

https://zaki-hmkc.hatenablog.com/entry/2021/02/26/234357

最初はこちらの書籍で勉強しました

https://www.amazon.co.jp/%E3%81%95%E3%82%8F%E3%81%A3%E3%81%A6%E5%AD%A6%E3%81%B6%E3%82%AF%E3%83%A9%E3%82%A6%E3%83%89%E3%82%A4%E3%83%B3%E3%83%95%E3%83%A9-docker%E5%9F%BA%E7%A4%8E%E3%81%8B%E3%82%89%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E6%A7%8B%E7%AF%89-%E5%A4%A7%E6%BE%A4-%E6%96%87%E5%AD%9D/dp/4296106422