Docker コンテナに対するポートフォワーディングはプロジェクト外部からの接続に対してのみ行われる

言いたいことはタイトルの通りである。 Docker にはポートフォワーディング機能があり、これを使うとコンテナのポートをホストのポートに割り当てることができる。

この目的から考えれば、ポートフォワーディングがプロジェクト内部のコンテナからの接続に対して働かないのは当たり前なことのように思えるが、本当にそうであることを確かめたかったので実験してみた。

結論だけ知りたい場合は、実験部分を飛ばして最後の「ねたばらし」の部分だけ読んでほしい。

もしかすると本稿の内容には間違いがあるかもしれないので、その場合はご指摘いただきたい。

実験の手順

適当な場所に test ディレクトリを作り、その中で以下のような docker-compose.yml を作成する。

version: "3.9"

services:
  nginx:
    image: nginx
    ports:
      - 8080:80
    networks:
      - mynetwork
      - default

  client1:
    image: curlimages/curl
    command: "sh -c 'sleep 10 && curl nginx:80 && curl nginx:8088'"
    networks:
      - mynetwork
    depends_on:
      - nginx

  client2:
    image: curlimages/curl
    command: "sh -c 'sleep 20 && curl nginx:80 && curl nginx:8088'"
    networks:
      - default
    depends_on:
      - nginx

networks:
  mynetwork:

この状態で docker-compose up すると、以下のコンテナが作成される。

  • nginx
    • 80 番ポートで接続を待ち受ける。これはホストの 8080 番ポートにバインドされている。
  • client1, 2
    • それぞれ異なるネットワークから nginx コンテナに curl して通信がつながるか確認する。

さらに、以下のネットワークが作成される。

  • test_default という名前の bridge ネットワーク(暗黙的に作られる
  • test_mynetwork という名前の bridge ネットワーク

これら全体の構成を図にすると、以下のようになる。

この構成のもとで host、client1、client2 からそれぞれ nginx コンテナの 80 番と 8080 番ポートに curl して、接続が通じるか調べる。

実験の結果

host から curl した結果を以下に示す。80 番ポートへの接続は失敗し、8080 番ポートへの接続は成功している。

$ curl localhost:80
curl: (7) Failed to connect to localhost port 80: Connection refused

$ curl localhost:8080
<!DOCTYPE html>
<html>
(省略)
</html>

次に、client1 の curl 実行ログを以下に示す。 前半部分は nginx:80 に対する接続で、成功している。後半は nginx:8080 に対する接続で、失敗している。

% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
<!DOCTYPE html>
<html>
(省略)
</html>
100   615  100   615    0     0   597k      0 --:--:-- --:--:-- --:--:--  600k

% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
curl: (7) Failed to connect to nginx port 8080 after 0 ms: Connection refused

最後に、client2 の curl 実行ログを以下に示す。 client1 のときと同じような結果となった。

% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
<!DOCTYPE html>
<html>
(省略)
</html>
100   615  100   615    0     0   599k      0 --:--:-- --:--:-- --:--:--  600k

% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
curl: (7) Failed to connect to nginx port 8080 after 0 ms: Connection refused

実験の考察

まず、host からコンテナの接続について考える。

そもそも host 自体は 80 番ポートを LISTEN していないので、ここへの接続が失敗するのは自明である。 また、8080 番ポートへ接続すると、以下のようにポートフォワーディングが行われ、接続に成功する。

次に、コンテナからコンテナへの接続について考える。

各コンテナから nginx:80 には以下のようにアクセスできるが......

どのようなネットワーク構成であっても Docker プロジェクト内からの接続にはポートフォワーディングが効かないため、8080→80 の変換が行われず、結果として nginx:8080 への接続は以下のように失敗する。

ねたばらし

では、どうしてポートフォワーディングは外部からの接続のみに対して効くのだろうか?

Docker のドキュメント を読めば分かるが、Docker サーバが起動するとホストの iptables にマスカレーディングルールが追加される。今回の実験の構成を起動したときの iptables を実際に確認してみると、以下のようになっていることがわかる。

$ sudo iptables-legacy -t nat -L -n
(省略)

Chain DOCKER (2 references)
target     prot opt source               destination
(省略)
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:8080 to:172.27.0.2:80

iptables の DNAT ルールは Destination Network Address Translation、つまりネットワークパケットの宛先アドレスを書き換える機能を持つ。 上記の例では、dpt(destination port:宛先ポート)が 8080 な TCP パケットの行き先を、172.27.0.2(nginx コンテナのアドレス)の 80 番ポートに書き換えている。 このルールによってポートフォワーディングが実現され、コンテナの特定のポートをホストの特定のポートに紐付けることが可能となる。 この他にも通信をうまく取り扱うためのルールが複数追加されるので、気になる方はぜひ確かめてみてほしい。

一方、コンテナ間の通信にはこのルールが適用されないため、結果としてポートフォワーディングも行われないのである。

結論

Docker コンテナに対するポートフォワーディングはプロジェクト外部からの接続に対してのみ行われる。

この実験を通して、普段あまり意識しないような Docker の内部の処理を垣間見ることができ、とても興味深いわねえという気持ちになった。

2021 述懐

早いもので、大学生活も3年が過ぎようとしている。 今年あった出来事を思い出し、来年をどう過ごすかについて考えるために、ここに振り返りを述べる。

学習

今年度の前半は、Linux カーネルをいじることを通して、オペレーティング・システムの何たるかについて学習した。

システムコールを追加してみたり、デバイスドライバなどのカーネルモジュールを作成してみたりなど、多岐にわたる活動を行ったが、 これらを通してカーネルの内部構造を知ったり、カーネルランドで動くプログラムで使える様々な API について知るなどした。

後半はネットワークを極める実験を行っている。 仮想環境上でネットワークを構築し、プログラムを動かしたりパケットキャプチャをしたりして、ネットワークの振る舞いやプロトコルの詳細などを学習する。 昔からネットワークに興味があり、さらにちょうど RTX1200 を導入するなどして自宅の環境を整えていた頃だったので、とても面白く受講できている。

来年から研究をすることになるが、私はシステムプログラムを扱う研究室に配属された。前半に学んだ内容とドンピシャなので、この道を究められるように頑張りたい。

WORD

WORD というのは Microsoft Office Word ではなく、情報科学類の学類誌の名前である。この雑誌の編集長を1年間務めることになった。 編集部の皆から原稿を集めて取りまとめたり、大学とのやりとりを行うのが仕事である。

コロナ禍という状況もあり、なかなか顔を合わせて会話することができなかったり、親睦を深めるのが難しかったりする。 必然的にオンライン中心のコミュニケーションになってしまうが、そこでの意思疎通の難しさと、私の能力不足を感じた。 また私自身、時間の認識に弛みがあってキビキビと動くことができず、編集部の人々に大きな迷惑をかけてしまった。

そのような中、編集長の役職をすることができたのは、先輩方や同級生、後輩からの助力のおかげである。 本当にありがとうございました。

来年は編集長ではないが WORD を辞めるわけではないので、積極的に記事を執筆していきたい。

労働

今年から、プログラミングなどをするアルバイトを始めた。 Web 周りの開発をしたり、インフラを整えたりするのが主な仕事内容である。

JavaScript を書いたり、クラウドコンピューティングサービスをいじるなどする貴重な機会となった。 大学の授業では、このような実務的な作業をすることがあまりないため、初めて触れる知識が多く勉強になった。

さらに、副次的な効果として金銭に少しゆとりができ、楽しい生活を送れるようになった。

資格

資格を取ると話のネタになるし、知識も得られるから一石二鳥じゃね?と思ったので、いろいろな資格を取ることにした(現に今話のネタになっており、早速役立っている)。 今年とった資格は、応用情報技術者と乙種第4類危険物取扱者である。

応用情報は、周りの人々が皆受験しており、そこまで難しくなさそうだったので受験した。試験の1ヶ月前から勉強をしようと思いながら、気が付けば試験3日前になっており、慌てて対策を始めた。Web サイト上で過去問を繰り返し解き、分からなかったところを都度調べるという具合に勉強を進めた。本番はあまり自信がなかったが、何とかギリギリで合格することができた。過去問の類題が出ることが多いので、それを繰り返しやっていればある程度は対策できるように思う。

乙4は、何となく有名で面白そうだったので受験した。こちらも気づいたら試験2日前とかになっていたので、B○○K ○FF で教本を購入して読んだり、YouTube で対策動画を視聴したり、過去問を解いたりして対策した。本番はそれなりに自信があり、結果も合格だった。 こちらは応用情報以上に過去問ゲーなので、ひたすら過去問を解くのが良いだろう。化学の問題も出るが、センター試験程度の知識があれば楽勝で解ける。

来年はさらに多くの資格を取りたい。今狙っているのはネットワークスペシャリストアマチュア無線技師などである。余裕があったら小型船舶なんかも面白いかもしれない。

運転

つくばというのは不便な街である。 一見すると様々な店が軒を連ね、何を買うにも不自由しないと思われがちであるが、これらは大学から遠く離れた場所にあるのが一般的であり、原動機つきの乗り物を使わないと移動に苦労することになる。

そのような現実を感じたこともあり、原付バイクを購入した。昔ながらの2ストエンジンで、排ガス規制などどこ吹く風と言わんばかりの白煙をあげながら走っている。燃料をモリモリ食うかわりにパワーは強力で、とても快調に移動することができる。

はじめは楽しく乗っていたが、所詮は原付バイクである。30キロまでしか合法的にスピードを出すことができず、また二段階右折などという厄介な法規もついて回る。 さらに皆さんよくご存知だとは思うが、つくばの道路は無法地帯である。ものすごい速度を出して走る自動車たちの横を30キロでチンタラ走るのは邪魔者以外の何者でもなく、長距離を移動するのには非常に難儀する。ただ、短距離移動には重宝しており、最近の生活の友になっている。

さて、少し良いものを味わってしまうと、さらに良いものを手に入れたくなるのが人間の性である。自動車だ。 折しも周りの人々が自動車を手に入れたり、実家の自動車を使ってドライブを楽しんでいる話を耳にするようになり、猛烈に自動車が欲しくなった。 しかし、そんなお金が湧いて出てくるはずもなく、自動車所有という夢は未だ遠い。

カーシェアというのを使うと、手ごろな料金で車を利用することができる。車は持てずとも運転の欲求が消えることはないのでこれを使ってたまにドライブをするが、とても楽しい。先日初めて首都高に行ってきたが、景色が爽快で道も面白く、いい時間を過ごすことができた。ただ、かなり難しい構造もあり、一瞬ヒヤリとする場面もあった。ふとした瞬間に事故が起こるという現実と、安全運転することの大切さを感じた。私は運転初心者であることを肝に銘じ、これからも安全第一でドライブを楽しみたい。

結び

来年をどう過ごすかは、来年の自分が決めることである。来年のことを言うと鬼に笑われるので。