Dockerにおけるmacvlanという技術

Dockerのコンテナネットワークにおいて外部ネットワークとの接続でmacvlanを使ったので、その仕様や特性について述べます。

# 初めに
複数dockerコンテナを立ち上げ、外部ネットワークから特定のコンテナに対してIPを用いた直接通信しようとした際にmacvlanを使うと解決したので述べます。
前提知識として、以下の知識があれば理解しやすいかと思われます。
* Dockerを触ったことがある
* コンテナネットワークについてある程度の知識がある
* ネットワークインタフェース周りの話についてある程度の知識がある

 # Dockerのデフォルトネットワーク
 macvlanの話をする前に、Dockerのデフォルトネットワークについて軽く触れます。
 内容としてはDocker-docs-jaからの内容からこの記事に関する部分についてざっくり解説しているので、詳細な説明はリンク先で確認してください。

https://docs.docker.jp/engine/userguide/networking/dockernetworks.html
 
 ホストマシン(コンテナホスト)にDockerをインストールすると自動的に3つのネットワークが作成されます。

 ```sh:sh
 $ docker network ls
NETWORK ID          NAME                DRIVER
7fca4eb8c647        bridge              bridge
9f904ee27bf5        none                null
cf03ee007fb4        host                host
 ```
 
 これらのネットワークは、コンテナを実行する際に ``` --net ```フラグで指定可能であり、下記の表は大まかな説明です。
 
 | 名前 | 説明 |
|:-----------|:------------|
| bridge   | ホストマシンのカーネル内で動作するソフトウェアブリッジで、フラグ指定しない場合にコンテナが接続される|
| none     | コンテナはネットワークインタフェースを持たない状態で起動する      |
| host     | コンテナはホストマシンのネットワークスタックを直接利用|

```none```に関しては、説明通りなのでこの記事では触れません。
```bridge```と```host```についてはもう少し詳細を述べます。

## bridgeネットワーク
ホストマシン内でネットワークインタフェースを確認すると```docker0```というインタフェースが作成されていることが確認できます。
```sh:sh
ubuntu@ip-172-31-36-118:~$ ifconfig
docker0   Link encap:Ethernet  HWaddr 02:42:47:bc:3a:eb
          inet addr:172.17.0.1  Bcast:0.0.0.0  Mask:255.255.0.0
          inet6 addr: fe80::42:47ff:febc:3aeb/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:9001  Metric:1
          RX packets:17 errors:0 dropped:0 overruns:0 frame:0
          TX packets:8 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:1100 (1.1 KB)  TX bytes:648 (648.0 B)
```
この```docker0```は、Dockerがデフォルトで作成する仮想ブリッジインタフェースであり、コンテナ間通信やホストとの通信を仲介します。詳細については以下のようになっています。

```sh:sh
$ docker network inspect bridge
[
   {
       "Name": "bridge",
       "Id": "f7ab26d71dbd6f557852c7156ae0574bbf62c42f539b50c8ebde0f728a253b6f",
       "Scope": "local",
       "Driver": "bridge",
       "IPAM": {
           "Driver": "default",
           "Config": [
               {
                   "Subnet": "172.17.0.1/16",
                   "Gateway": "172.17.0.1"
               }
           ]
       },
       "Containers": {},
       "Options": {
           "com.docker.network.bridge.default_bridge": "true",
           "com.docker.network.bridge.enable_icc": "true",
           "com.docker.network.bridge.enable_ip_masquerade": "true",
           "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
           "com.docker.network.bridge.name": "docker0",
           "com.docker.network.driver.mtu": "9001"
       }
   }
]
```

デフォルトネットワーク以外にも、ユーザ定義でブリッジネットワークやオーバーレイネットワークを作成できますがここでは割愛します。
ここで重要なのは、```docker0```がデフォルトで Network Address Translation(NAT) を利用するということです。
つまり、**コンテナの内部IPはホストマシンの外部ネットワークからは直接アクセスすることが不可能**です。
この問題は、ポートフォーワードやリバースプロキシを使うことで回避できますが、オーバーヘッドが増加したり、レイテンシが発生したりするため留意する必要があります。

## hostネットワーク
```host```ネットワークを指定すると、コンテナは独自の仮想ネットワークを持たず、ホストマシンと同じネットワークインタフェースを使用します。
特徴としては以下のような特徴があります。

1. **ネットワークの分離がない**
    * コンテナには、ホストマシンのIPアドレスが適用される
    * コンテナは、コンテナ独自のIPを不保持
2. **ポートマッピングが無効**
    * ```-p```オプションによるポートマッピングが無効
    * ホストマシンとコンテナは同じポートを利用
    * ポート競合に留意する必要あり
3. **パフォーマンス向上の可能性**
    * 仮想ネットワークを利用しないためレイテンシが削減
    * ホストマシンとコンテナ間での通信が多い場合に効果が発揮
4. **セキュリティリスク**
    * コンテナがホストマシンに完全に統合されることから、他のコンテナやホスト上のプロセスと直接通信可能
    *  コンテナの分離が必要な環境では避けるべき

```host```ネットワークの場合では、ホストマシンとコンテナは同一のIPであるため、**IPアドレスによる外部ネットワークからの直接指定は不可能**です。

# macvlanネットワーク
ようやく本題のmacvlanについて語っていくのですが、その前に軽く関連する知識について述べます。
既知の方は読み飛ばしてください。

## 関連知識
### Virtual LAN(VLAN)
物理的なネットワークの構成に関係なく、論理的にネットワークを分割する技術です。
スイッチなどのネットワーク機器を用いて、異なるネットーワークセグメントを作成し、特定のグループ内のデバイス同士のみが通信可能となるようにします。
VLANには以下の種類があります。
* ポートベースVLAN：スイッチの特定ポートをVLANに割り当て
* タグVLAN(IEEE 802.1Q)：VLAN IDをパケットに付与して、異なるVLANを識別
* MACアドレスベースVLAN：MACアドレスごとにVLANを割り当て

今回の記事ではMACアドレスベースVLAN(主役)とタグVLAN(少しだけ)の話が関連します。

### Macvlan(Linux)
Linuxカーネルが提供する仮想ネットワークインタフェースの一種で、1つの物理ネットワークインタフェース(NIC)を仮想的に複数のインターフェースに分割する技術です。
Macvlanは以下のような特徴を持ちます。

* ```veth```や```bridge```と異なり各仮想インタフェースに異なるMACアドレスを割り当てる
* 物理NICを共有しながら異なるIPを利用可能
* ホストと仮想NIC間の通信制限(デフォルトではホストと仮想NICで通信不可能だが、別途```bridge```や```passthru```を利用することで解決可能)

## Dockerにおけるmacvlanネットワーク(本題)
Dockerでは、Macvlanドライバを使用すると、コンテナに物理NICと同じL2ネットワーク上の**独立したMACアドレスとIPアドレスを割り当てることが可能**となります。デフォルトのDockerネットワークとは異なり、コンテナがホストマシンとは完全に独立したネットワークデバイスとして振る舞うという特徴があります。

### macvlanネットワークの機能
以下に主な機能を記載します。

1. 各コンテナが個別のIPアドレスとMACアドレスを保持
    * デフォルトネットワークでは、NATを利用してIPアドレスを共有するが、macvlanではコンテナごとに異なるIPアドレスとMACアドレスを保持可能
    * このMACアドレスは静的に指定および固定が可能
2. デフォルトではホストマシンとコンテナ間の直接通信は不可能
    * macvlanネットワーク作成時にparentを明示的に指定し、```bridge```モードを使用することでホストマシンとの通信も可能
    * 同一のmacvlanに存在する他のコンテナに対してはL2での通信が可能 
3. コンテナがホストの物理ネットワークと統合
    * 同一LAN内の他のデバイス(ルータ、スイッチ、他のサーバー)とも直接通信可能
    * DHCPサーバからコンテナに対してIPアドレスを割り当てることも可能

これらの機能により、ホストマシンに接続しているL2スイッチなどから見た場合、物理NICでは単一だが論理的には、ホストマシンとコンテナがそれぞれ独立したデバイスに見えます。
利用するときの注意点として、1つの物理インタフェースが複数のMACアドレスを割り当て可能とするため、ネットワーク機器に**プロミスキャスモード機能が必要**となります。

![スクリーンショット 2025-02-10 2.15.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636687/37a8eb69-b48d-8703-d5de-c9c1ebef3011.png)


### タグあり(VLAN付き)macvlan
* VLAN IDを指定することで、コンテナごとに異なるVLANへ接続可能
* VLAN IDを持つネットワークはスイッチの設定が必要
* 複数のVLANにまたがるコンテナネットワークを作成し、セグメントを分離可能

例：VLAN 100 (```192.168.100.0/24```)に接続するmacvlanネットワークの作成
```sh:sh
docker network create -d macvlan \
  --subnet=192.168.100.0/24 \
  --gateway=192.168.100.1 \
  -o parent=eth0.100 \
  my_vlan100_net
```
```eth0.100```はVLAN100に属する仮想NICです。
VLAN 100内のデバイスと通信可能になります。
VLANタグが付与されるため、パケット長が変わります。(MTU 1504以上を許容する設定に変更することで解決可能)


### タグなしmacvlan
* VLANタグ(802.1Q)が付与されない
* コンテナはホストの物理NICが属するデフォルトVLAN(通常はVLAN ID 1)を使用

例：物理NIC```eth0```を使用し、コンテナが```192.168.2.x```のネットワークに直接接続する場合
```sh:sh
docker network create -d macvlan \
  --subnet=192.168.1.0/24 \
  --gateway=192.168.1.1 \
  -o parent=eth0 \
  my_macvlan_net
```
コンテナは```192.168.1.0/24```のネットワークに直接接続します。
ルータや他のデバイスとも直接通信が可能となります。
VLANタグがないため、パケット長の変化はありません。

### タグありとタグなしの使い分け

 | 項目  | タグあり(Tagged) | タグなし(Untagged) |
|:-----------|:-----------|:------------|
| VLAN IDの管理   | VLAN IDを設定| なし|
| ネットワークの分離 | VLANごとに分離      | 物理ネットワークと同じ|
| スイッチの設定     | VLANタグ対応が必要| 不要|
|用途| マルチテナントやセグメント化　| シンプルなネットワーク統合|

### 設定と使用例(タグなし)
(1)macvlanネットワークを作成
```sh:sh
docker network create -d macvlan \
  --subnet=192.168.1.0/24 \
  --gateway=192.168.1.1 \
  -o parent=eth0 \
  my_macvlan
```
(2)macvlanネットワークにコンテナを接続
```sh:sh
docker run -it --rm --network=my_macvlan --name my_container alpine sh
```
```my_container```は```192.168.1.x```のIPを持ち、物理ネットワーク上のデバイスと通信可能になります。

### 設定と使用例(タグあり)
(1)macvlanネットワークを作成
```sh:sh
docker network create -d macvlan \
  --subnet=192.168.100.0/24 \
  --gateway=192.168.100.1 \
  -o parent=eth0.100 \
  my_vlan100_net
```
(2)macvlanネットワークにコンテナを接続
```sh:sh
docker run -it --rm --network=my_vlan100_net --name vlan_container alpine sh
```
```vlan_container```はVLAN 100に属し、他のVLANとは分離されます。

## まとめ
今回の記事では主にDockerにおけるmacvlanネットワークについて記載しました。
コンテナをL2で独立した端末のように見せたい場合や、IPによる直接通信をしたい場合に活用すると良いと思います。

## おまけ(1コンテナ1プロセス)
コンテナを触れたことがある人なら、一度は聞いたことがある「１コンテナ１プロセス」の原理ですがその詳細についてはご存知でしょうか。
大前提の理由として以下のものがあります

* コンテナはプロセスの実行単位として扱われる
* シンプルな設計で管理しやすい
* プロセスのスケールや監視が容易になる
* フェイル時に迅速に復旧可能

これらの原則に基づき、コンテナは単一のプロセスを起動するために使用されます。

### PID 1 zombie reaping problem
Linuxシステムでは、すべてのプロセスに一意のプロセスID(PID)が割り当てられ、親プロセスと子プロセスの関係が形成される。

#### PID 1の役割
* Linuxカーネルはシステム起動時に```init```(または```systemd```)プロセスをPID 1に割り当てる
* PID 1はすべての孤立した子プロセスを引き継ぎ、適切に```wait()```してゾンビプロセスを回収する責任を持つ

コンテナ環境においては、コンテナのエントリーポイントとなるプロセスがPID 1になります。一般的に```ENTRYPOINT```や```CMD```で指定されたプロセスがPID 1になります。

#### zombie process
* プロセスが```exit()```すると、その終了ステータスは親プロセスが```wait()```するまで保持される
* 親プロセスが適切に```wait()```しない場合、子プロセスのエントリーがプロセステーブルに残り、ゾンビプロセスとなる
* ゾンビプロセスが大量に発生すると、システムリソース（プロセステーブル）が枯渇し、システムの不安定化を引き起こす可能性

#### コンテナにおけるzombie process problem
コンテナ内のプロセスがPID 1の場合、適切に子プロセスを管理しないとゾンビプロセスが発生しやすくなります。
* 多くのアプリケーションはPID 1として動作することを想定していないため、子プロセスの```wait()```を適切に処理しない
* その結果、ゾンビプロセスが増加し、コンテナの安定性に悪影響を与える

対処としては```tini```や```dumb-init```などを使用しプロセスを管理する方法が挙げられます。
設計思想の他にこういう理由もあり、1コンテナ1プロセスを推奨しているのだと思われます。

https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/

## 参考記事まとめ

https://docs.docker.jp/engine/userguide/networking/dockernetworks.html

https://docs.docker.jp/network/macvlan.html

https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/