Dockerコンテナで開いているポートを取得

前回はDockerコンテナで開いているポートをホスト側から取得する方法を解説しました。

今回はそれをスクリプトにしてみました。あまり汎用とは言い難いのですが、ご参考までに。

目的

ホスト上のスクリプトでDockerコンテナ上のプロセスで開いているポートを取得する

必要なもの

スクリプトを動作させるには以下が必要です。

  • Linux環境
  • docker
  • bash
  • awk
  • python
  • PyPiのpsutilパッケージを改変したもの

最後の項目が怪しげですね…

準備

psutilが必要な上に改変しないといけないので、pythonの実行環境の分離が必要です。今回はvenvを使用します。

$ python3 -m venv env
$ . env/bin/activate
(env) $ pip install -U pip setuptools
(env) $ pip install psutil==5.9.7

ここまででvenv環境に切り替えてpsutilが入っています。バージョンを指定しているのは以下のパッチを当てるからです。

psutil.diff

--- a/env/lib/python3.10/site-packages/psutil/_pslinux.py 2024-01-18 13:14:03.036936938 +0900
+++ b/env/lib/python3.10/site-packages/psutil/_pslinux.py 2024-01-18 04:47:49.197056641 +0900
@@ -991,6 +991,10 @@ class Connections:
         ret = set()
         for proto_name, family, type_ in self.tmap[kind]:
             path = "%s/net/%s" % (self._procfs_path, proto_name)
+            if pid is not None:
+                path_for_pid = "%s/%s/net/%s" % (self._procfs_path, pid, proto_name)
+                if os.path.exists(path_for_pid):
+                    path = path_for_pid
             if family in (socket.AF_INET, socket.AF_INET6):
                 ls = self.process_inet(
                     path, family, type_, inodes, filter_pid=pid)

このパッチは、procfs上のプロセスのfdからソケットを調べる際に、システム全体のネットワークではなく、プロセスごとのネットワークを見るように修正しています。dockerコンテナのソケットはシステム全体のネットワークには現れず、取得できないので。

パッチの当て方は以下のとおりです。

$ patch -p1 <psutil.diff

これで準備が整いました。

スクリプト

get_port.sh

#!/bin/sh
container=$1
if [ "$container" = "" ]; then
    echo "usage: \n\t$0 container_id\n"
    exit 1
fi
pid=$(docker inspect $container | awk '/"Pid":.*/{print $2;}' | sed 's/,$//')
sudo ./env/bin/python get_listen_ports.py $pid

最初に探しているpidは引数で指定したコンテナIDのルートプロセスのホスト上でのpidです。このプロセスとその子孫プロセスがそのコンテナ上で動いているプロセスになるので、そのプロセスが開いているポートを取得しているのが次のpythonスクリプトです。

dockerのプロセスは一般ユーザーでは見れないことが多いので、sudoで動かしていますが、その際にvenv環境のpythonを使う必要があるので、ちょっとややこしい書き方になっています。

get_listen_ports.py

import psutil

def get_ppid2pid_list_and_target(pid=1):
    result={}
    target=None

    for p in psutil.process_iter():
        if p.pid == pid:
            target = p
        ppid = p.ppid()
        pid_list = result.get(ppid)
        if not pid_list:
            pid_list = []
            result[ppid] = pid_list
        pid_list.append(p)

    if target is None:
        raise Exception(f'{pid} is not found.')

    return (result, target)

def traverse_tree(ppid2pid_list: dict, p: psutil.Process, func, depth: int = 0, args = None):
    func(p, depth, args)
    l = ppid2pid_list.get(p.pid)
    if l:
        for p in l:
            traverse_tree(ppid2pid_list, p, func, depth + 1, args)

def print_process(p: psutil.Process, depth: int, args):
    print('  ' * depth + f'{p.pid}: {p.name()}')

def get_name(p: psutil.Process, depth: int, args):
    pid_name = args
    pid_name[p.pid] = p.name()

def get_open_port(p: psutil.Process, depth: int, args):
    pid_proto_cons = args
    proto_cons = {}
    pid_proto_cons[p.pid] = proto_cons
    protos = ('tcp4', 'tcp6')
    for proto in protos:
        cons = []
        proto_cons[proto] = cons
        for con in p.connections(proto):
            if con.status == 'LISTEN':
                cons.append(con)

def main(pid: int):
    ppid2pid_list, target = get_ppid2pid_list_and_target(pid)
    pid_proto_cons = {}
    traverse_tree(ppid2pid_list, target, get_open_port, 0, pid_proto_cons)
    pid_name = {}
    traverse_tree(ppid2pid_list, target, get_name, 0, pid_name)
    for pid, proto_cons in pid_proto_cons.items():
        for proto, cons in proto_cons.items():
            for con in cons:
                print(f'{pid}({pid_name[pid]}): {proto}: {con.laddr.port}')

if __name__ == '__main__':
    import sys
    args = sys.argv
    if len(args) == 1:
        main(psutil.Process().pid)
    else:
        main(int(args[1]))

使い方

venv環境に入る必要はありませんが、venv環境のあるディレクトリでしか動かないスクリプトになっています。他の場所で動く必要があれば、自分で変えてください(venv環境のpythonを実行すればいいだけ)。

$ sh get_port.sh some-mysql
56566(mysqld): tcp6: 33060
56566(mysqld): tcp6: 3306
$

some-mysqlの部分はコンテナID(コンテナ名)です。

まとめ

Dockerコンテナから開いているポートを簡単に調べられるスクリプトを作成した。

本当はpsutilにパッチを送ろうかと思ったのですが、githubアカウントを作るために使えるメアドがなく(できればこのドメインのユーザーにしたい)、断念しました。まあPR出しても通らなそうな気がするけど。