Fumi2kick ScrapBook

mastodon top へ戻る

Docker で Mastodon を運用していると IPv6 アドレスしか持たない他のインスタンスと 通信ができないという問題がある。

それを回避するため、Docker コンテナ内部から IPv6 NAT 経由で IPv6 通信をさせる。

実際に www.mofgao.space を運営する際に引っかかったところと その打開策としての docker-ipv6nat の使い方。

記述日

前提

解決策

robbertkl/docker-ipv6nat を使って IPv6 においても IPv4 と同様に docker bridge 上で NAT 接続させるようにする。

概説

Docker のコンテナにおけるデフォルトのネットワークは、docker bridge 上に独自のセグメントを用意され 外部とは dockerd が用意する NAT 経由でアクセスをしている。

自動的にアドレスが振られて NAT 経由でのアクセスができるようになるあたりは host の dockerd が 面倒を見てくれているが、それは IPv4 のみで IPv6 においてはそういった気軽さはない。 実際に plane なディストリビューション環境コンテナを起動して見てみるとコンテナ内部では IPv6 が 使えないことが確認できる。

docker の推奨している IPv6 環境の説明 を読むと「手作業で static routing を設定してコンテナ一つ一つに IPv6 を振りなよ」というやたら面倒な手順が書いてある。 サーバーとして公開したり、全てのデバイスが一意のアドレスでアクセスできるという IPv6 の理念的には正しいのかも しれないけれども、内部から IPv6 通信したいだけという向きには面倒過ぎるし、なによりコンテナを作り直すと設定し直し というあたりが docker の気軽さをオミットしていると感じる。

docker-ipv6nat の機能

docker-ipv6nat を起動すると dockerd が IPv4 で行っていたような bridge 上へのネットワーク割り当てと、NAT による 外部へのアクセスを実装してくれる。

docker-ipv6nat を起動した後、IPv6 を有効にした bridge network を docker で作成。 その作成したネットワークをコンテナに割り当てることにより、IPv6/IPv4 の両方が host で中継され ネットワークアクセスができるようになる。

docker-ipv6nat の使い方

前準備

host が IPv6 をルーティングできるように ip6_tables モジュールをカーネルに組み込まないといけない。 これが host 側での唯一の準備。

host 側で lsmod して ip6_tables が居ない場合、/etc/modules に追記するなどする。

# echo "ip6_tables" >> /etc/modules

再起動後 lsmod して ip6_tables が表示されていれば OK

ちなみに IPv4 の ip_tables は Docker が使うために既に組み込まれているはず。

ipv6nat コンテナの起動

robbertkl/docker-ipv6nat に書いてある手順に従っていく。

host 側のルーティングテーブルを変更するので、dockerd の sock や特権を渡してやる必要がある。

# docker run -d --restart=always -v /var/run/docker.sock:/var/run/docker.sock:ro --privileged --net=host robbertkl/ipv6nat 

以下のような docker-compose でまとめておくと次回起動が楽になる。

version: '3'
services:
  driver:
    image: robbertkl/ipv6nat
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    privileged: true
    network_mode: "host"

docker-compose からの起動は

# docker-compose -f (上記ファイル名) up -d

docker-ipv6nat コンテナを起動するまでが下準備で、これは環境としてずっと起動しておくのが良い。

IPv6 が有効なブリッジの作成

docker network ls で確認できる bridge (network) に docker-ipv6net を経由するものを追加する。

コマンドからの作成は

# docker network create --ipv6 --subnet=fd00:0001:0002::/64 net6

subnet は docker bridge 内でコンテナに割り振られるアドレスの範囲なので適当に設定する。 fd00::/8 は IPv6 におけるプライベートアドレス相当。

通常は docker-compose で各種サーバーのコンテナを起動するであろうから、docker-compose 内に networks: を追記する形となる。

version: '2.1'

networks:
  net6:
    driver: bridge
    enable_ipv6: true
    ipam:
      driver: default
      config:
      - subnet: fd00:0001:0002::/64

docker-compose の version が 2.1 となっているのはなぜか 3.0 だと enable_ipv6 が存在しなかったため。

上記設定で net6 が作成されて、そこを利用することで、IPv6 が通じるようになる。

コンテナからの利用

コンテナの起動時に networks を指定(上記例では net6)するだけとなる。

docker-compose で記述すると

version: '2.1'

networks:
  net6:
    driver: bridge
    enable_ipv6: true
    ipam:
      driver: default
      config:
      - subnet: fd00:0001:0002::/64

services:
  debian:
    image: debian
    networks:
      - net6

これで debian コンテナに IPv6 が付与される。

コンテナ内からネットワークインタフェースを確認すると subnet 下のアドレスが割り振られていることが確認できる。

root@c0892abf99d0:/# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
5: eth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.18.0.2/16 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fd00:1:2::2/64 scope global nodad 
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe12:2/64 scope link 
       valid_lft forever preferred_lft forever

Mastodon への適用

Mastodon を起動するための docker-compose.yml を修正し、上記 networks を追加する。

www.mofgao.space での具体例は以下の通り。参考程度とし volumes での永続化などはそれぞれの環境に読み替えて欲しい。

version: '2.1'

networks:
  net:
    driver: bridge
    enable_ipv6: true
    ipam:
      driver: default
      config:
      - subnet: fd00:0001:0002::/64

services:
  web:
    restart: always
    image: gargron/mastodon
    env_file: env.production
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    networks:
      - net
    ports:
      - "3000:3000"
    volumes:
      - /var/lib/mastodon/public/assets:/mastodon/public/assets
      - /var/lib/mastodon/public/packs:/mastodon/public/packs
      - /var/lib/mastodon/public/system:/mastodon/public/system
      - /etc/localtime:/etc/localtime:ro

  streaming:
    restart: always
    image: gargron/mastodon
    env_file: env.production
    command: npm run start
    networks:
      - net
    ports:
      - "4000:4000"
    volumes:
      - /etc/localtime:/etc/localtime:ro
 
  sidekiq:
    restart: always
    image: gargron/mastodon
    env_file: env.production
    networks:
      - net
    command: bundle exec sidekiq -q default -q mailers -q pull -q push
    volumes:
      - /var/lib/mastodon/public/system:/mastodon/public/system
      - /etc/localtime:/etc/localtime:ro

これを起動することで sidekiq が IPv6 で問い合わせ、発行してくれるようになる。

Docker で IPv6 対応の Mastodon インスタンスを作るには

なんでこのようなことをしなければいけなかったのか、その解説。

リバースプロキシとその裏方

Mastodon は公開インスタンスとして設置する場合必ず https 化する必要がある。 そのため、前段に nginx や caddy といった https サーバーを置き、Mastodon への リクエストはリバースプロキシとして後段の本体に渡すといった構成を通常取ることになる。

リバースプロキシとWEBアプリ "fig1"

この際リバースプロキシから後段(Mastodon)への接続は内部外部問わず TCP/IP で行われるが、 内側の話なのでだいたい IPv4 で行われていることが多い。

前段サーバーと後段サーバー "fig2"

fig1 で後段はサーバー内部の通信だが、わかりやすくするためサーバーを分けてみた。

後段が IPv4 であったとしても、リバースプロキシが受けたリクエストに対してはリバースプロキシに 向けて返すため最終的に IPv6 のレスポンスは正しく帰って行く。

故に前段である httpd 兼リバースプロキシが IPv6 アドレスを持っていて、DNS で AAAA レコードを 持っていれば上記図のような構成であっても「IPv6 対応 OK」と言ってしまっても構わない。

Docker 標準ではコンテナ側 bridge は IPv4 のみでだいたい上記の様な構成になっている。 なので通常は host が IPv6 を持っていて通信できる状況になっていれば、IPv6 でのサービス提供は ほとんど問題が無い。

Mastodon on docker における問題点

Mastodon を docker で構成した場合、おおよそ次のような図となる。

Mastodon on docker "fig3"

Mastodon は 3種類のコンテナで動作しているが、そのうち web と streaming については リバースプロキシからの応答となるため先出の fig2 に相当し問題無く動作する。

WEB UI は IPv6 アクセスできるしストリームAPIも動作するので、この構成で 一見 IPv6 対応できているように見えるが、 実際は IPv4 を持たないインスタンスとの疎通ができない。

問題となるのが sidekiq で、ここは非同期で動作しておりリクエストとは関係なく自発的に 処理をこなしていく。 この部分が他のインスタンスと通信を行っているのだが、 ここの通信についてはリバースプロキシを介さず直接行われることになる。

sidekiqの通信 "fig4"

つまり sidekiq においては A, B といったリバースプロキシ経由の通信経路を持たず、 動作している環境のデフォルトルーティングに沿って通信を行おうとする。

sidekiq が通信を行おうとしている経路 C、ここで IPv6 通信ができなかった場合 IPv6 しか持たないインスタンスへ繋がらずユーザー情報の取得も記事の publish もできない という状態に陥る。

Docker の標準状態ではコンテナ間を繋ぐネットワークは IPv4 しか無いため、 sidekiq からの IPv6 リクエストが全て通じないという状況となっていた。

この状況を回避するには sidekiq を分離するとか、sidekiq コンテナのネットワークを host 直結に してしまうとかが考えられるがその内の解の一つが コンテナ内部からIPv6通信できるようにする というものであった。

通常のインスタンスは IPv4/IPv6 の両方を持っていて IPv4 でも通信ができるためあまり問題には ならないのだが、IPv6 しか持っていないというインスタンス相手に対しこのような対応が必要となる。