docker-pyで docker daemonを遠隔操作してみようのコーナー


どうもはにおかさいです。
dockerをLinuxサーバーにおいて、別の端末やサーバーからdockerを操作したくなりました。
dockerではrestfulなAPIが提供されていまして、それをdocker-pyから呼び出すことができます

前準備

Docker側でTCPを受け付けられるようにします。
今回のやり方だとだれでもアクセスできるので、危険です。実験用のローカル以外ではどうにか適宜対策すること。

tcpをあける

TCPで待ち受けするために、Dockerの起動時オプションを変更する必要があります。
その方法についてはこちらをご覧くださいませ。

CentOS7のわたしの環境の場合:/usr/lib/systemd/system/docker.service
ExecStart に -H 0.0.0.0:5555 と追記してください。 とするとポート5555でhttp通信を受け付けることができます。

docker.service
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock -H 0.0.0.0:5555

読み込み

[root@localhost ~]# systemctl daemon-reload
[root@localhost ~]# service docker restart

Python側から遠隔操作してみよう

上から下に読むと変数の意味がわかります。また、 stack はstringのcontainer IDです。

イニシャライズ

docker APIを初期化するには、下記のようなコードが必要です。

# coding: utf-8
import docker

c = docker.APIClient(base_url='tcp://192.168.1.4:5555', timeout=5)

情報の表示

print(c.version())
print(c.info())
print(docker.version)
{'Platform': {'Name': 'Docker Engine - Community'}, 'Components': [{'Name': 'Engine', 'Version': '18.09.5', 'Details': {'ApiVersion': '1.39', 'Arch': 'amd64', 'BuildTime': '2019-04-11T04:13:40.000000000+00:00', 'Experimental': 'false', 'GitCommit': 'e8ff056', 'GoVersion': 'go1.10.8', 'KernelVersion': '3.10.0-957.10.1.el7.x86_64', 'MinAPIVersion': '1.12', 'Os': 'linux'}}], 'Version': '18.09.5', 'ApiVersion': '1.39', 'MinAPIVersion': '1.12', 'GitCommit': 'e8ff056', 'GoVersion': 'go1.10.8', 'Os': 'linux', 'Arch': 'amd64', 'KernelVersion': '3.10.0-957.10.1.el7.x86_64', 'BuildTime': '2019-04-11T04:13:40.000000000+00:00'}
{'ID': 'UL47:X6SQ:GP6M:V6PT:IKC2:UBSO:T4HK:SQM3:HG34:T5Q5:3JPW:VS5Q', 'Containers': 0, 'ContainersRunning': 0, 'ContainersPaused': 0, 'ContainersStopped': 0, 'Images': 4, 'Driver': 'overlay2', 'DriverStatus': [['Backing Filesystem', 'xfs'], ['Supports d_type', 'true'], ['Native Overlay Diff', 'true']], 'SystemStatus': None, 'Plugins': {'Volume': ['local'], 'Network': ['bridge', 'host', 'macvlan', 'null', 'overlay'], 'Authorization': None, 'Log': ['awslogs', 'fluentd', 'gcplogs', 'gelf', 'journald', 'json-file', 'local', 'logentries', 'splunk', 'syslog']}, 'MemoryLimit': True, 'SwapLimit': True, 'KernelMemory': True, 'CpuCfsPeriod': True, 'CpuCfsQuota': True, 'CPUShares': True, 'CPUSet': True, 'IPv4Forwarding': True, 'BridgeNfIptables': True, 'BridgeNfIp6tables': True, 'Debug': False, 'NFd': 22, 'OomKillDisable': True, 'NGoroutines': 38, 'SystemTime': '2019-04-18T23:06:19.983023943+09:00', 'LoggingDriver': 'json-file', 'CgroupDriver': 'cgroupfs', 'NEventsListener': 0, 'KernelVersion': '3.10.0-957.10.1.el7.x86_64', 'OperatingSystem': 'CentOS Linux 7 (Core)', 'OSType': 'linux', 'Architecture': 'x86_64', 'IndexServerAddress': 'https://index.docker.io/v1/', 'RegistryConfig': {'AllowNondistributableArtifactsCIDRs': [], 'AllowNondistributableArtifactsHostnames': [], 'InsecureRegistryCIDRs': ['127.0.0.0/8'], 'IndexConfigs': {'docker.io': {'Name': 'docker.io', 'Mirrors': [], 'Secure': True, 'Official': True}}, 'Mirrors': []}, 'NCPU': 2, 'MemTotal': 1907965952, 'GenericResources': None, 'DockerRootDir': '/var/lib/docker', 'HttpProxy': '', 'HttpsProxy': '', 'NoProxy': '', 'Name': 'localhost.localdomain', 'Labels': [], 'ExperimentalBuild': False, 'ServerVersion': '18.09.5', 'ClusterStore': '', 'ClusterAdvertise': '', 'Runtimes': {'runc': {'path': 'runc'}}, 'DefaultRuntime': 'runc', 'Swarm': {'NodeID': '', 'NodeAddr': '', 'LocalNodeState': 'inactive', 'ControlAvailable': False, 'Error': '', 'RemoteManagers': None}, 'LiveRestoreEnabled': False, 'Isolation': '', 'InitBinary': 'docker-init', 'ContainerdCommit': {'ID': 'bb71b10fd8f58240ca47fbb579b9d1028eea7c84', 'Expected': 'bb71b10fd8f58240ca47fbb579b9d1028eea7c84'}, 'RuncCommit': {'ID': '2b18fe1d885ee5083ef9f0838fee39b62d653e30', 'Expected': '2b18fe1d885ee5083ef9f0838fee39b62d653e30'}, 'InitCommit': {'ID': 'fec3683', 'Expected': 'fec3683'}, 'SecurityOptions': ['name=seccomp,profile=default'], 'ProductLicense': 'Community Engine', 'Warnings': ["WARNING: API is accessible on http://0.0.0.0:5555 without encryption.\n         Access to the remote API is equivalent to root access on the host. Refer\n         to the 'Docker daemon attack surface' section in the documentation for\n         more information: https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface"]}
4.0.0-dev

コンテナ作成

stack = c.create_container(image='uphy/ubuntu-desktop-jp:18.04',detach=True,host_config=c.create_host_config(port_bindings={8080: ('0.0.0.0', 8080)},storage_opt={"size":"1g"}),stdin_open=True)
print(stack["Id"])
print(stack["Warnings"])
print(c.start(stack))

ポートは{コンテナ内ポート:(IP,ホストのポート)}で一対一対応とします。
また、storage-optはホストが対応していれば利用可能です。原則としてすべてのargsがdocker-pyに実装されています。

イメージのpull

for line in c.pull("uphy/ubuntu-desktop-jp:18.04", stream=True):
 print(line)

forで回して待機しておかないと、あとあとの処理のときにimage取得完了前にコンテナを作成しかねません。

コンテナのexport

con = c.export("eee126213653")
f = open('./busybox-latest.tar', 'wb')
for chunk in con:
 f.write(chunk)
f.close()

コンテナIDを指定することで「export」できます。exportはdocker exportと似た振る舞いをします。

コンテナのimport

何故か読み込み方法が複数あります。
repository,tagは指定しないと以下のようになります。

#<none>              <none>              c49ad9dfe280        12 minutes ago      1.42MB

tarとして読み込む

c.import_image_from_file(filename="./busybox-latestgetimage.tar",repository="title-busy",tag="latest")

dataとして読み込み。

c.import_image_from_data(data=open("./busybox-latestgetimage.tar","rb").read())

urlで指定して、tarfileを取得。

c.import_image_from_url(url="foo.com")

イメージのsave

image = c.get_image("busybox:latest")

f = open('./busybox-latestgetimage.tar', 'wb')
for chunk in image:
 f.write(chunk)
f.close()

image save tarballでイメージを取得. docker saveに似ています。 https://github.com/docker/docker-py/blob/02e660da12ecb5cf755b17ffd850ce3da21ecec0/docker/models/images.py#L87

イメージのload

docker.api.image.ImageApiMixin.get_image (docker save)で取得したデータをdocker loadのように読み込めます

tar = tarfile.open("./busybox-latestgetimage.tar", "r|")
print(c.load_image(data=tar))
#busybox             latest              af2f74c517aa        11 days ago         1.2MB
c.load_image(data=open("./busybox-latestgetimage.tar","rb").read())

コンテナ内の指定ディレクトリでtarを作る

f = open('./sh_bin.tar', 'wb')
bits, stat = c.get_archive(stack, '/dirname')
print(stat)
for chunk in bits:
 f.write(chunk)
f.close()

コンテナ内の指定ディレクトリにtarballを展開する

c.put_archive(stack,"/path",tardata)

tardataはbytesです。

コンテナの停止

c.stop(stack)

サンプル

参考

https://nisenabe.hatenablog.com/entry/2014/01/21/104701
https://docker-py.readthedocs.io/en/1.2.3/port-bindings/