Feedforce Developer Blog

フィードフォース開発者ブログ

docker-compose での MySQL の疎通確認で telnet を使う時に自動でコネクションを切る

こんにちは、id:tsub511 です。

TELNET プロトコルには全く馴染みがないのですが、今回たまたま使う機会があり、かつ調べても割と見つけられない情報だったので記事を書いてみました。

curl で TELNET プロトコルを使う

curl は HTTP/HTTPS 以外のプロトコルも使うことができます。

curl のドキュメントを確認すると、サポートしているプロコトルは DICT, FILE, FTP, FTPS, GOPHER, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, POP3, POP3S, RTMP, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET and TFTP のようです。

$ man curl # in macOS

curl(1)                                                                                                                    Curl Manual                                                                                                                    curl(1)



NAME
       curl - transfer a URL

SYNOPSIS
       curl [options / URLs]

DESCRIPTION
       curl is a tool to transfer data from or to a server, using one of the supported protocols (DICT, FILE, FTP, FTPS, GOPHER, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, POP3, POP3S, RTMP, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET and TFTP). The command
       is designed to work without user interaction.

       curl offers a busload of useful tricks like proxy support, user authentication, FTP upload, HTTP post, SSL connections, cookies, file transfer resume, Metalink, and more. As you will see below, the number of features will make your head spin!

       curl is powered by libcurl for all transfer-related features. See libcurl(3) for details.
...

例えば以下のように指定することで TELNET プロトコルで対象のサーバーに接続することが可能です。

$ curl telnet://localhost:3306

ユースケース

docker-compose ではコンテナ間の依存関係を depends_on で定義できますが、コンテナ内でサーバーなどが立ち上がるまでは待ってくれません。

そこで、以下のドキュメントに書いてあるような方法でコンテナ間で依存しているサーバーに対するヘルスチェックを行うことで解決できます。

docs.docker.com

実際には以下のスクリプトで MySQL サーバーの起動を待ってから別のコンテナを実行するような仕組みにしていました。

#!/bin/bash

host="$1"
shift
cmd="$@"

until mysql -h "$host" -u root -e 'show databases' > /dev/null 2>&1; do
  >&2 echo "MySQL is unavailable - sleeping"
  sleep 1
done

>&2 echo "MySQL is up - executing"
exec $cmd

ただ、この方法だと mysql コマンドがコンテナ内にインストールされている必要があります。

本番環境でのコンテナの実行を考慮すると、mysql のクライアントはインストールする必要がなかったので mysql コマンド以外の方法で MySQL サーバーの起動を確認する必要がありました。

そこで、ベースイメージの都合でたまたま curl がインストールされていたので curl の TELNET プロトコルを使うことにしました。

変更後のスクリプトが以下になります。

#!/bin/bash

host="$1"
shift
cmd="$@"

until echo 'quit' | curl telnet://$host:3306 > /dev/null 2>&1; do
  >&2 echo "MySQL is unavailable - sleeping"
  sleep 1
done

>&2 echo "MySQL is up - executing"
exec $cmd

変更したのは 7 行目のみで、mysql コマンドを curl に置き換えています。

こうすることで mysql のクライアントをインストールせずに MySQL サーバーが起動するのを待ってから別のコンテナを実行することができるようになりました。

解説

例えば以下のように実行すると、TELNET プロトコルを使って疎通確認ができます。

$ docker run -d -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=1 mysql
$ curl -s -o /dev/null telnet://localhost:3306

ただしこのままだと Ctrl+C などでコネクションを切るまで curl が実行されたままになります。

TELNET プロトコルは対話型であるため、コネクションを張りっぱなしになるという認識です。

Ctrl+C が必要ということは、上述したスクリプトでは使えません。

ではどうすれば疎通確認後に自動でコネクションを切れるでしょうか。

実は以下のように quit を curl に標準入力で渡すことで解決できます。

$ docker run -d -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=1 mysql
$ echo 'quit' | curl -s -o /dev/null telnet://localhost:3306

quit とは何かというと、telnet コマンドでは quit というコマンドを指定することで telnet の接続を切ることができます。

$ man telnet # in macOS with brew install telnet

TELNET(1)                 BSD General Commands Manual                TELNET(1)

NAME
     telnet -- user interface to the TELNET protocol

SYNOPSIS
     telnet [-468EFKLNacdfruxy] [-S tos] [-X authtype] [-e escapechar] [-k realm] [-l user] [-n tracefile] [-s src_addr] [host [port]]

DESCRIPTION
     The telnet command is used to communicate with another host using the TELNET protocol.  If telnet is invoked without the host argument, it enters command mode, indicated by its prompt (``telnet>'').  In this mode, it accepts and executes the commands
     listed below.  If it is invoked with arguments, it performs an open command with those arguments.

     Options:

...

quit       Close any open TELNET session and exit telnet.  An end of file (in command mode) will also close a session and exit.

そして quit を使った telnet コマンドを自動的に終了するための方法が以下の記事で紹介されていました。

qiita.com

上記の記事を参考に、curl でも同様の方法を試してみたら動いた、ということになります。

ただし、curl に標準入力を渡すことで TELNET プロトコルにコマンドを渡すことができる、という挙動自体は curl のドキュメントを確認しても見つけられませんでした。

公式の情報で裏が取れない限りは当記事の事例のように開発環境でのみ使った方が良いかもしれません。