オブジェクトの広場はオージス総研グループのエンジニアによる技術発表サイトです

コンテナ・マイクロサービス

さわって理解する Docker 入門

第6回 Docker Engine の仕組みを体感しよう
オージス総研
樋口 匡俊
2017年12月13日

本連載では、Docker に興味はありつつもまだ触ったことのない方向けに、実際に Docker を触って理解していただくための記事を提供します。今回は Docker Engine の仕組みについて、クライアント・サーバーモデルの観点から説明します。Docker Engine の API を呼び出したり、リモート接続するなどして、Docker Engine の仕組みを体感的に理解していきましょう。

Docker Engine の概要

Docker Engine は、Docker の中核をなすソフトウェアです。本連載で度々登場している docker rundocker build などのコマンドや、それらのコマンドにより実行されるイメージのビルド、コンテナの起動といったさまざまな処理を行うものをまとめて Docker Engine と呼びます。

単に Docker と言った場合、この Docker Engine を指す場合もあれば、Docker Hub のような周辺サービスやツールも含めた Docker Platform を指す場合もあります。Docker Platform のように広い意味での Docker と区別するときに、Docker Engine という言葉は使われます。

公式ドキュメントによると、Docker Engine は主に以下の3つのコンポーネントで構成されています。

  1. Docker CLI
  2. Docker Engine API
  3. Dockerデーモン

Docker CLI は、docker rundocker build などの Dockerコマンドを実行するコマンドラインツールです。Docker CLI は、入力された Dockerコマンドに応じて Docker Engine API を呼び出します。Dockerデーモンは Linux のデーモンプロセスで、Docker Engine API が呼び出されるのを待ち受けています。Dockerデーモンは、呼び出された Docker Engine API に応じて、イメージのビルドやコンテナの起動などを行います。

このように、Docker Engine はクライアント・サーバーモデルのアプリケーションであると言えます。クライアントである Docker CLI が、Docker Engine API を介して、サーバーである Dockerデーモンに処理を要求し、応答を受け取っているのです。

Docker Engine 概要

Unixソケット通信とTLS通信

上の図のように、Docker CLI が Dockerホストの中にあるか外にあるかによって、Dockerデーモンとの通信方式は異なります。

Docker CLI が Dockerホストの中にある場合は、Unixドメインソケット(以下、Unixソケット)を用いて Dockerデーモンと通信します。Unixソケットは、同じマシンの中のプロセス同士で通信を行うことができる仕組みです。前回docker-machine ssh コマンドを用いて Dockerホストにログインしてから、Dockerホストの中にある Docker CLI のコマンドを実行していましたので、Unixソケット通信を行っていたわけです。

Docker CLI が Dockerホストの外にある場合は、TCPソケットを用いて Dockerデーモンと通信します。TCPソケットの場合、HTTP をそのまま利用するのではなく、何らかのセキュリティ対策を実施することが推奨されています。その一つが TLS です。Docker Engine は TLS を用いて、HTTP を暗号化した HTTPS で通信を行うとともに、クライアントとサーバーを信頼できるものに限定することができます。

そのような TLS通信を行うためには、証明書の作成などさまざまな作業が必要ですが、Docker Machine を用いて Dockerホストを作成した場合は、それらの作業は自動的に行われます。本記事でもこの Docker Machine の自動設定を利用して TLS通信を行います。(注:自動設定を利用する理由は、簡単に TLS通信を体験できるからであり、高い安全性が保証されるからではありません。)

事前準備

以上説明したことについて、ここから実際に Docker Engine をさわって理解を深めていきましょう。概要は以下のとおりです。

  • 事前準備
  • curl による API の呼び出し(Unixソケット通信)
  • API の呼び出しによる「Hello World」(Unixソケット通信)
  • Docker CLI のインストール
  • Docker CLI によるリモート接続(TLS通信)
  • 補足:プロキシ、証明書の再作成

まずは前回と同様に、事前準備として Windows 7 または 10VirtualBoxPowerShell を用意してください。

また、以下のように Docker Machine を用いて Dockerホストを作成してください。

PS> docker-machine create myhost

この create コマンドを実行すると、Dockerホストの中の Docker のバージョンは最新になります。しかし、筆者が利用した Docker のバージョンは「v17.09.0-ce」ですので、ひょっとしたら Docker のバージョンが異なるために、以下で紹介する内容が実行できないかもしれません。その場合は、以下のように create コマンドの --virtualbox-boot2docker-url オプションにバージョンを指定して Dockerホストを作成してください。

PS> $url = 'https://github.com/boot2docker/boot2docker/releases/download/v17.09.0-ce/boot2docker.iso'
PS> docker-machine create --virtualbox-boot2docker-url $url myhost

Dockerホストが作成できたらログインしましょう。

PS> docker-machine ssh myhost

ログインできたら Docker CLI を用いて「Hello World」ができることを確認しましょう。

docker@myhost:~$ docker run hello-world
(中略)
Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

ここまでは、前回と同じですね。

改めてログを読んでみると、Docker CLI (Docker client) と Dockerデーモン (Docker daemon) の処理について順番に説明されています。このログには書かれていませんが、Docker CLI は Dockerデーモンとやり取りするために Docker Engine API を呼び出しています。Docker Engine API は、Docker CLI でなくとも呼び出すことが可能です。そこで、次に curl を用いて Docker Engine API を呼び出してみましょう。

curl による API の呼び出し(Unixソケット通信)

curl は言わずと知れた HTTP などさまざまな通信を行えるツールです。curl は最初から Dockerホストにインストールされていますが、念のため curl のバージョンを確認してみましょう。

docker@myhost:~$ curl --version
curl 7.49.1 (x86_64-pc-linux-gnu) libcurl/7.49.1 OpenSSL/1.0.2h zlib/1.2.8
Protocols: dict file ftp ftps gopher http https imap imaps pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: AsynchDNS Largefile NTLM NTLM_WB SSL libz TLS-SRP UnixSockets

curl のバージョンが 7.40.0 以上であれば OK です。curl はバージョン 7.40.0 から Unixソケットが利用できます。 以下のように、Unixソケットを用いて Docker Engine API を呼び出してみましょう。

docker@myhost:~$ curl --unix-socket /var/run/docker.sock http:/version
{
    "Version":  "17.09.0-ce",
    "ApiVersion":  "1.32",
    "MinAPIVersion":  "1.12",
    "GitCommit":  "afdb6d4",
    "GoVersion":  "go1.8.3",
    "Os":  "linux",
    "Arch":  "amd64",
    "KernelVersion":  "4.4.89-boot2docker",
    "BuildTime":  "2017-09-26T22:45:38.000000000+00:00"
}

--unix-socket オプションに指定されている /var/run/docker.sock は Docker のソケットのパスです。http:/version は Docker のバージョン情報を取得する API です。バージョン情報は JSON 形式で出力されています(見やすいように整形しています)。"ApiVersion": "1.32" というのが API のバージョンです。バージョン 1.32 のドキュメントにはさまざまな API がサンプル付きで記載されています。それらを参考にしながら、次は「Hello World」をやってみましょう。

API の呼び出しによる「Hello World」(Unixソケット通信)

はじめに、コンテナを作成します。

curl --unix-socket /var/run/docker.sock \
    -H "Content-Type: application/json" \
    -d '{"Image": "hello-world", "Tty": true}' \
    -X POST http:/containers/create
{"Id":"469a115ce858fc7ae41639dc3ec7bf354e88015cc98b2540a4c8a7598e01445e","Warnings":null}

イメージ名「hello-world」が -d '{"Image": "hello-world", ... という風に JSON形式で指定されています。-X POST は HTTP の POST メソッドを利用するという意味です。http:/containers/create はコンテナを作成する API です。

出力結果の {"Id":"469a11... は、作成されたコンテナの ID です。この ID を利用してコンテナを起動します。

docker@myhost:~$ curl --unix-socket /var/run/docker.sock \
    -X POST http:/containers/469a11/start

http:/containers/469a11/start がコンテナを起動する API です。起動するコンテナは、このようにコンテナID の先頭数文字 (469a11) で指定できます。

作成した「hello-world」イメージのコンテナは、起動するとログを出力してすぐに停止するコンテナです。正常に停止したかどうか確認してみましょう。

docker@myhost:~$ curl --unix-socket /var/run/docker.sock -X POST http:/containers/469a11/wait
{"StatusCode":0}

http:/containers/469a11/wait は、コンテナが停止するまで待機し、終了コードを StatusCode として返してくれる API です。StatusCode は 0 ですので、コンテナの処理は正常終了しています。

コンテナのログを確認してみましょう。

docker@myhost:~$ curl --unix-socket /var/run/docker.sock http:/containers/469a11/logs?stdout=1

Hello from Docker!
This message shows that your installation appears to be working correctly.
(以下略)

docker run コマンドで「Hello World」を実行した時と同じログが出力されていますね。呼び出す API は少し異なりますが、docker run コマンドもこのように Docker Engine API を介して Dockerデーモンとやり取りを行っています。

Docker CLI のインストール

さて、次は Docker CLI によるリモート接続(TLS通信)を行うために、Windows に Docker CLI をインストールしましょう。インストールは Docker Machine と同様に、バイナリファイルをダウンロードして Path を設定するだけです。

Docker CLI の Windows 64ビット版のバイナリファイルは、こちら からダウンロードできます。もしも見当たらない場合は、公式マニュアルを参照してください。

Docker CLI のバージョンは、Dockerホストの Docker のバージョンとなるべく合わせてください。バージョンが異なると、正常に動作しない可能性が高くなります。バージョン v17.09.0-ce のファイル名は「docker-17.09.0-ce.zip」です。

zipファイルをダウンロードしたら、展開して中に入っている docker.exe ファイルを 環境変数 Path に追加してください。PowerShell における Path の設定方法については、前回を参考にしてください。

Path を設定したら、以下のコマンドを実行しましょう。

PS> docker version
Client:
 Version:      17.09.0-ce
 API version:  1.32
 Go version:   go1.8.3
 Git commit:   afdb6d4
 Built:        Tue Sep 26 22:40:09 2017
 OS/Arch:      windows/amd64
error during connect: Get https://%2F%2F.%2Fpipe%2Fdocker_engine/v1.32/version:
open //./pipe/docker_engine: The system cannot find the file specified.
In the default daemon configuration on Windows, the docker client must be run elevated to connect.
This error may also indicate that the docker daemon is not running.

このように、上半分の「Client:」に Docker CLI のバージョン情報が出力されていればインストール完了です。

下半分にはエラーメッセージが出力されていますね。次のリモート接続の設定を行うことで、下半分には Dockerホスト側の情報が出力されるようになります。

Docker CLI によるリモート接続(TLS通信)

リモート接続をするためには、対象の Dockerホストの情報を設定する必要があります。以下では設定方法を2つ紹介します。

(1)docker のオプションを設定する方法

PowerShell で、以下の Docker Machine のコマンドを実行してみてください。

PS> docker-machine config myhost
--tlsverify
--tlscacert="C:\\Users\\Taro\\.docker\\machine\\machines\\myhost\\ca.pem"
--tlscert="C:\\Users\\Taro\\.docker\\machine\\machines\\myhost\\cert.pem"
--tlskey="C:\\Users\\Taro\\.docker\\machine\\machines\\myhost\\key.pem"
-H=tcp://192.168.99.100:2376

この config コマンドは、リモート接続するための docker のオプションを出力するコマンドです。--tls で始まるオプションには、Docker Machine が自動的に作成した証明書のパスなどが指定されています。-H=tcp://192.168.99.100:2376 は TLS通信を受け付ける「myhost」の IPアドレスとポート番号です。

これらのオプションをコピーし、以下のように docker version コマンドに追加して実行してみましょう。(各行の末尾に付いている「`」はバックティック(バッククオート)で、PowerShell では長いコマンドを複数行に分けて書きたいときに使います。)

PS> docker --tlsverify `
--tlscacert="C:\\Users\\Taro\\.docker\\machine\\machines\\myhost\\ca.pem" `
--tlscert="C:\\Users\\Taro\\.docker\\machine\\machines\\myhost\\cert.pem" `
--tlskey="C:\\Users\\Taro\\.docker\\machine\\machines\\myhost\\key.pem" `
-H=tcp://192.168.99.100:2376 version

Client:
 Version:      17.09.0-ce
 API version:  1.32
 Go version:   go1.8.3
 Git commit:   afdb6d4
 Built:        Tue Sep 26 22:40:09 2017
 OS/Arch:      windows/amd64

Server:
 Version:      17.09.0-ce
 API version:  1.32 (minimum version 1.12)
 Go version:   go1.8.3
 Git commit:   afdb6d4
 Built:        Tue Sep 26 22:45:38 2017
 OS/Arch:      linux/amd64
 Experimental: false

リモート接続に成功し、「Server:」の情報が出力されました。

しかし docker コマンドを実行するたびに、毎回このような長いオプションを付けることはなるべく避けたいですね。そこで、次のように環境変数を設定してみましょう。

(2)環境変数を設定する方法

リモート接続に必要な環境変数は、Docker Machine と PowerShell を用いて以下のように1行で設定できます。

PS> docker-machine env myhost | Invoke-Expression

このコマンドを実行した後、続けて同じ PowerShell 上で docker versiondocker run hello-world を実行してみてください。バージョン情報が出力され、「Hello World」が実行できるはずです。

それでは、このコマンドは一体何をしているのでしょうか?分割して見てみましょう。

まず、以下のようにパイプライン記号「|」の左側のコマンドを実行して下さい。

PS> docker-machine env myhost
$Env:DOCKER_TLS_VERIFY = "1"
$Env:DOCKER_HOST = "tcp://192.168.99.100:2376"
$Env:DOCKER_CERT_PATH = "C:\Users\Taro\.docker\machine\machines\myhost"
$Env:DOCKER_MACHINE_NAME = "myhost"
$Env:COMPOSE_CONVERT_WINDOWS_PATHS = "true"
# Run this command to configure your shell:
# & "C:\Users\Taro\Scripts\docker-machine.exe" env myhost | Invoke-Expression

$Env: という文字列が数行出力されていますね。$Env:前回説明した通り PowerShell における環境変数です。変数名や設定されている値から推測できるとおり、これらは TLS通信に関する環境変数です。このように Docker Machine の env コマンドは、リモート接続したい Dockerホスト に関する環境変数を、文字列として出力してくれます。

ただ、env コマンドは単に文字列を出力してくれるだけにすぎません。出力された文字列を、PowerShell のコマンドとして実行しなければ、環境変数は設定されません。

文字列を実行してくれるのが、PowerShell の Invoke-Expression コマンド(省略形 iex)です。つまり、docker-machine env myhost | Invoke-Expression は、Docker Machine の env コマンドで出力された文字列を PowerShell の Invoke-Expression コマンドで実行し、リモート接続に必要な環境変数を設定していたのです。

このように、Docker CLI は環境変数によって接続先を切り替えることができます。今回作成した「myhost」以外にもいくつか Dockerホストを作成して試してみるとよいでしょう。

以上で、Docker CLI によるリモート接続(TLS通信)ができるようになりました。以下では補足として、リモート接続で困ったときの対処法を2点紹介したいと思います。

補足1:プロキシ

env コマンドを利用して環境変数を設定しても、以下のようにエラーが発生する場合があります。

PS> docker-machine env myhost | Invoke-Expression
PS> docker version
(中略)
error during connect: Get https://192.168.99.100:2376/v1.32/version: Service Unavailable

原因の一つとして、会社や学校の HTTPプロキシを経由することで、Dockerホストの IPアドレス(192.168.99.100)まで辿り着けないということが考えられます。その場合は、以下のように --no-proxy オプションを追加しましょう。

PS> docker-machine env --no-proxy myhost
$Env:DOCKER_TLS_VERIFY = "1"
$Env:DOCKER_HOST = "tcp://192.168.99.100:2376"
$Env:DOCKER_CERT_PATH = "C:\Users\Taro\.docker\machine\machines\myhost"
$Env:DOCKER_MACHINE_NAME = "myhost"
$Env:COMPOSE_CONVERT_WINDOWS_PATHS = "true"
$Env:NO_PROXY = "192.168.99.100"
# Run this command to configure your shell:
# & "C:\Users\Taro\Scripts\docker-machine.exe" env --no-proxy myhost | Invoke-Expression

--no-proxy オプションを追加することで、下から3行目に $Env:NO_PROXY = "192.168.99.100" が追加で出力されましたね。$Env:NO_PROXY は、プロキシを経由しない IPアドレスを設定する環境変数です。このように $Env:NO_PROXY に「myhost」の IPアドレスを指定すれば、Docker CLI はプロキシを経由せずに「myhost」にリモート接続しようとします。

補足2:証明書の再作成

Docker Machine を使い続けていると、ときどき以下のようなエラーメッセージが出力されることがあります。

PS> docker-machine env myhost | Invoke-Expression
Error checking TLS connection: Error checking and/or regenerating the certs:
There was an error validating certificates for host "192.168.99.101:2376":
x509: certificate is valid for 192.168.99.100, not 192.168.99.101
You can attempt to regenerate them using 'docker-machine regenerate-certs [name]'.
Be advised that this will trigger a Docker daemon restart which might stop running containers.

原因の一つとして、証明書を作成した時(Dockerホストを作成した時)の IPアドレスと、今回 Dockerホストを起動したときの IPアドレスが異なることが考えられます。Docker Machine は Dockerホストを起動する時に IPアドレスを動的に割り当てるため、Dockerホストを複数作成していると、このようなエラーがよく発生します。

この場合、エラーメッセージにも書いてあるとおり regenerate-certs コマンドで証明書を再作成してみましょう。

PS> docker-machine regenerate-certs myhost
Regenerate TLS machine certs?  Warning: this is irreversible. (y/n): y
Regenerating TLS certificates
Waiting for SSH to be available...
Detecting the provisioner...
Copying certs to the local machine directory...
Copying certs to the remote machine...
Setting Docker configuration on the remote daemon...

これで再びリモート接続ができるようになるはずです。

おわりに:Docker Engine と「assemble」

今回は、Docker Engine の仕組みについて説明しました。API の呼び出しやリモート接続を通じて、クライアント・サーバーモデルとも言える Docker Engine の仕組みを体感できたのではないかと思います。

さいごに、最近の Docker社の動向と Docker Engine の関係について簡単に紹介し、まとめとしたいと思います。

Docker Engine という “呼び名” が明示的に使われたのは、2014年6月の DockerCon が初めてと言ってよいでしょう。この DockerCon では、libcontainer や libswarm など、現在開発されているコンポーネントの源流となるコンポーネントも発表されています。どうやら、それまで「Docker」として一括りに呼んでいたものを、この頃から少しづつコンポーネントとして明確に分けていこうとしていたようです。

今回紹介した Docker Engine の3つのコンポーネントも、それぞれ Docker の進化とともにコンポーネントとしての形を明確にしてきたものです。そのうち、Dockerデーモンは更にいくつかのコンポーネントに分かれています。例えば containerd は、Dockerデーモンのコンテナに関する処理をコンポーネントとして切り出したものです。現在 containerd は Cloud Native Computing Foundation(CNCF)という組織のプロジェクトとして開発が進められています。

こうしたコンポーネント化の流れは、今年4月の DockerCon 2017 にて発表された Moby プロジェクトにより、新たな段階に入ったと言えるでしょう。

Moby とは、いわば自分なりの Docker を作るためのフレームワークです。Moby プロジェクトのトップページには以下のように書かれています。

An open framework to assemble specialized container systems without reinventing the wheel.

この文で注目すべきは「assemble」という単語です。「assemble」とは「集める」「組み立てる」という意味です。Moby はさまざまなコンポーネントを集めて、Docker のようなコンテナ・システムを組み立てるためのオープンなフレームワークというわけです。

Docker社は今後、さまざまなコンテナ・システムを「assemble」する手段として Moby を提供するとともに、Moby を活用して優れた形に「assemble」したコンテナ・システムの一つとして Docker を提供していこうとしています。Docker を中心とするコンテナ技術は変化が激しく、次々と新しいプロダクトが登場して来ますが、それらはコンポーネントなのか、コンポーネントを「assemble」した成果なのか、という観点で眺めてみると、理解が早まるかと思います。