PythonでDockerを自動化する:BashスクリプトからプロフェッショナルなSDKへ

Python tutorial - IT technology blog
Python tutorial - IT technology blog

Docker CLIを手動入力するよりPythonを使うべき理由

30個ものコンテナをセットアップし、それぞれに異なるnetwork設定、volume、環境変数を割り当てた経験はありますか?シェルスクリプトとdocker runコマンドだけで管理しようとすると、エラーハンドリングはすぐにカオスな状態になってしまいます。

以前、200行のBashスクリプトでCI/CDシステムを運用していました。「Container AのhealthcheckがOKになったらContainer Bを起動する」という要件が加わるまでは、なんとかうまくいっていました。しかしこのあたりからBashは「息切れ」してきます。そこでDocker SDK for Pythonへの移行を決断しました。そのプロジェクトは今や2000行のコードに成長しましたが、オブジェクト指向の構造のおかげで、メンテナンスは今でも非常に楽です。

コードでDockerを操作することで、繰り返し作業を自動化できます。さらに、Dockerをウェブアプリに組み込んでユーザー向けのサンドボックスを作成したり、独自の監視ツールを構築したりすることも可能です。

仕組みを理解する:Docker ClientとDocker Daemon

コードを書く前に、PythonがDockerとどのように通信するかを理解しておく必要があります。DockerはClient-Serverモデルで動作します。Dockerをインストールすると、Docker Daemonと呼ばれるバックグラウンドサービスが/var/run/docker.sockのUnixソケットを通じて待ち受けます。

docker-pyライブラリはClientとして機能します。DaemonにHTTP REST APIリクエストを送信してcontainerの作成やimageの削除などを指示します。シンプルに言えば、ターミナルで入力するすべてのコマンドに対応するPython SDKの関数が存在します。

ライブラリのインストール

おなじみのpipコマンド一行で完了します:

pip install docker

実践:Dockerを操作するスクリプトを書く

1. Docker Clientへの接続

まずは接続の確立です。通常、SDKはシステムの環境変数からデフォルト設定を自動的に読み取ります。

import docker

try:
    client = docker.from_env()
    # バージョン情報を取得して接続を確認する
    v = client.version()
    print(f"接続成功!Docker Engine v{v['Version']}")
except Exception as e:
    print(f"接続エラー: {e}")

2. Imageの管理:PullとCheck

docker pull nginx:latestと入力する代わりに、コードで実行します。ちょっとしたコツ:pullする前にローカルにimageが既に存在するか確認することで、帯域幅を節約し、不要な3〜5秒の待ち時間を省けます。

# Docker HubからImageをPullする
print("nginx imageを確認・取得中...")
image = client.images.pull("nginx", tag="latest")
print(f"Image ID準備完了: {image.short_id}")

# 現在のImageを一覧表示する
for img in client.images.list():
    print(f"Tag: {img.tags}")

3. Containerの操作:Run・Stop・Logs

これが最も重要な部分です。SDK経由でcontainerを起動することで、detachportsmem_limitなどのパラメータを細かく制御できます。

# containerを起動する(docker run -d -p 8080:80 と同等)
container = client.containers.run(
    "nginx", 
    detach=True, 
    ports={'80/tcp': 8080}, 
    name="web_app_prod"
)

print(f"Container {container.name} が起動しています。")

# リアルタイムでログを取得する
logs = container.logs(tail=10)
print(f"最新10行のログ: {logs.decode('utf-8')}")

# クリーンアップ
container.stop()
container.remove()

実体験から学んだ教訓:containerには必ずname属性を設定してください。Dockerに「agitated_hopper」のようなランダムな名前を自動生成させると、後でコードからcontainerを追跡・管理するのが大変なことになります。

4. 独自Networkの構築

サービス同士が安全に通信できるよう、Dockerのデフォルトのbridgeを使うのではなく、専用のnetworkを作成することを常にお勧めします。

# 存在しない場合はnetworkを作成する
net_name = "internal_api_net"
existing_nets = client.networks.list(names=[net_name])

if not existing_nets:
    client.networks.create(net_name, driver="bridge")
    print(f"Networkを作成しました: {net_name}")

実践的な教訓:スクリプトを途中でクラッシュさせないために

スクリプトが大きくなるにつれ、コードの整理の仕方が毎晩ぐっすり眠れるかどうかを左右します。以下は私が常に実践している3つのルールです:

  • 詳細なエラーキャッチ:except Exceptionだけに頼らないでください。docker.errorsからNotFoundやAPIErrorなどの具体的なエラーをキャッチし、それぞれのケースに応じた処理を用意しましょう。
  • Wrapper Classの構築:あちこちで直接runコマンドを呼び出さないでください。DockerManagerクラスを作成しましょう。例えば、smart_run関数は同名の古いcontainerを自動的に削除してから新しいcontainerを起動します。
  • リソースのクリーンアップ(Housekeeping):自動化プロセスは不要なcontainerや「dangling」imageを残しがちです。client.containers.prune()を活用して、定期的にディスク容量を解放しましょう。
def clean_system():
    client = docker.from_env()
    # 停止したcontainerをクリーンアップしてRAM/Diskを解放する
    deleted_containers = client.containers.prune()
    print(f"クリーンアップ完了: {deleted_containers.get('SpaceReclaimed', 0)} bytes")

まとめ

手動コマンド入力からDocker SDKへの移行は、Infrastructure as Codeの考え方に近づくための第一歩です。最初は.shファイルを書くより難しく感じるかもしれませんが、Pythonの制御能力はシステムをはるかに安定させてくれます。今日からまず小さなクリーンアップスクリプトを書いてみてください!

Share: