socat でコンテナ内のコマンドを TCP ソケット経由で実行する

標準入出力を利用するコマンドを外部からネットワーク経由で実行したいことありませんか?

コンテナを使っているとまれにありますよね。例えばデータサイエンティストが実装したシンプルな Python スクリプトとか。

socat を使って TCP ソケット経由で外部からコマンドを実行できるようにしてみました。


課題

例えば 1マイクロ秒の誤差もないすごい date コマンドを持っているコンテナがあるとします。このコンテナは docker run date:latest を実行すると日時を標準出力に表示します。

この date コマンドをホスト側から実行するのは容易ですが、別のコンテナから実行したくても直接コマンドを実行する方法が見つかりませんでした。

socat.png

これを実現するために ssh や Docker on Docker で date コンテナを実行する方法もありますが、呼出元のコンテナに Docker をインストールしたり docker.sock を共有したり事前準備がやや面倒です。

この記事では上記のような「別コンテナのコマンドを実行して標準出力を取得したい」ケースを socat を使って解決します。


具体的な例

前の記事でヘッドレス Chrome を使用して HTML を標準出力にダンプするコンテナを作成しました。このコンテナを実行すると対象のウェブサイトの HTML が出力されます。

$ docker pull grohiro/headless-chrome

$ docker run grohiro/headless-chrome http://www.google.com/
#=> (HTMLが出力される)

コンテナの内部では gooogle-chrome コマンドを実行して HTML をダンプしています。このコンテナを socat を使って TCP ポート 9000 経由で外部から実行できるようにします。


コンテナイメージの作成

まずは上記の Chrome コンテナに socat を追加したイメージを作成します。そしてヘッドレス Chrome を起動するラッパースクリプトも追加します。

Dockerfile

FROM grohiro/headless-chrome

USER root
RUN apt-get install -y -qq socat
COPY ./chrome.sh /
ENTRYPOINT ["socat", "tcp4-listen:9000,fork,reuseaddr", "system:/chrome.sh"]

chrome.sh

#!/bin/sh

read URL
google-chrome --headless --no-sandbox --dump-dom --start-maximized --disable-gpu "$URL"

ファイルを作成したら Docker イメージをビルドします。名称は headless-chrome-socat です。

$ docker build -t headless-chrome-socat .

作成したコンテナを実行すると TCP ポート 9000 が socat にバインドされて LISTEN 状態になります。このポートに送信したデータは chrome.sh の標準入力に渡されます。

chrome.sh は標準入力の文字列を Chrome の引数として実行します。Chrome は指定された URL を読み込み HTML を標準出力にダンプします。

socat はこの標準出力をリクエスト元のコンテナにレスポンスとして返します。

実行元のコンテナはソケットの入力を読み込むことで Chrome がダンプした HTML を取得することができます。

socat2.png


動作確認

まずはホスト側から手動で URL を入力して HTML が出力されるか確認します。

以下のコマンドで作成したコンテナのポート 9000 を 127.0.0.1:9000 にマッピングして起動します。

$ docker run -p 9000:9000 headless-chrome-socat

ポート 9000 に接続して URL を送信してみましょう。

$ nc 127.0.0.1 9000

http://www.google.com
#=> (HTMLが出力される)

入力した http://www.google.comchrome.sh 経由でヘッドレス Chrome に渡されて HTML が出力されました。


docker-compose

ここまでできたら docker-compose.yml を定義します。

docker-compose.yml

services:

crawler:
# コマンド実行元のコンテナ
image: php:7.5-slim
chrome:
# ヘッドレス Chrome コンテナ
image: headless-chrome-socat

この docker-compose.ymlcrawler コンテナの中から tcp://chrome:9000 に URL を送信すると Chrome が実行されて HTML が取得できるようになりました。

例えば crawler コンテナ内の PHP からは fsockopen() を使ってデータを読み書きできます。

$response = "";

$fp = fsockopen('chrome', '9000');
if ($fp) {
fwrite($fp, "https://www.google.com\n");
while (!@feof($fp)) {
$response .= fgets($fp, 4096);
}
}