GoでDockerを操る [Docker Engine SDK]
こちらはDocker Advent Calendar 2020 5日目の記事です!
qiita.com
(2020/12/16:Dockerfileを用いたビルドについて追記しました。)
はじめに
DockerはDocker Engine API (旧 Docker Remote API)という仕組みをもっており、Unix Domain Socket の通信によってイメージやコンテナに対する操作ができます。さらに、Docker Engine API をGo言語から扱いやすくするために、Docker Engine SDK というものも提供されています。これはDocker Engine API の呼び出しをラップしてくれる言語パッケージで、これを使うことで比較的簡単にGo言語からDockerを操ることができます。我々が普段叩いてるdocker run ...
というようなコマンドも、このDocker Engine SDK を使って実装されています (https://github.com/docker/cli でコードを読むことができます)。
しかしビルドの方法に難があり、私自身諦めかけていたのですが、GitHub Issueを読み込むことでビルド方法が判明しました!この記事では、Docker Engine SDK のビルド方法・各種サンプルコードについて解説します。
環境
この後は以下のような環境で作業しています。
$ uname -a Linux ik1-429-46507 4.15.0-123-generic #126-Ubuntu SMP Wed Oct 21 09:40:11 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux $ go version go version go1.15.4 linux/amd64 $ docker version Client: Docker Engine - Community Version: 19.03.13 API version: 1.40 Go version: go1.13.15 Git commit: 4484c46d9d Built: Wed Sep 16 17:02:36 2020 OS/Arch: linux/amd64 Experimental: false Server: Docker Engine - Community Engine: Version: 19.03.13 API version: 1.40 (minimum version 1.12) Go version: go1.13.15 Git commit: 4484c46d9d Built: Wed Sep 16 17:01:06 2020 OS/Arch: linux/amd64 Experimental: false containerd: Version: 1.3.7 GitCommit: 8fba4e9a7d01810a393d5d25a3621dc101981175 runc: Version: 1.0.0-rc10 GitCommit: dc9208a3303feef5b3839f4323d9beb36df0a9dd docker-init: Version: 0.18.0 GitCommit: fec3683
Docker Engine SDK とは
Dockerはサーバークライアントモデルとして設計されており、Dockerデーモン(Docker Engine)がサーバーで、我々が普段叩いているdockerコマンド(Docker CLI)がクライアントです。
このサーバー・クライアント間を結ぶものがDocker Engine API であり、それをGo言語から扱いやすくしたものがDocker Engine SDK です。
Docker Engine API のドキュメント : Docker Engine API v1.40 Reference
Docker Engine SDK のドキュメント (GoDoc) : client - GoDoc
※ただし、このGoDocの内容はmasterブランチの最新のものであり、latestより先のバージョンのものだと思われます。client · pkg.go.dev では、バージョンを指定できますが、Docker(moby)のバージョンをGo言語側で読み取れていないらしく、こちらでもlatestのドキュメントはありませんでした。
Docker Engine SDK を使うコードをビルドする
Docker のドキュメントサイトの中に"Develop with Docker Engine SDKs" というページがあります。
docs.docker.com
ここにはgo get github.com/docker/docker/client
とすることでDocker Engine SDK を使うことができると書かれていますが、現在はこれでは上手くいきません。以下のようにlatest バージョンを明示する必要があります(2020年12月5日の時点ではv19,02,14)。
$ go mod init sample # go module を使う場合 go: creating new go.mod: module sample $ go get github.com/docker/docker@v19.03.14 # latest のバージョンを指定してgo get go: downloading github.com/docker/docker v17.12.0-ce-rc1.0.20201201034508-7d75c1d40d88+incompatible go: github.com/docker/docker v19.03.14 => v17.12.0-ce-rc1.0.20201201034508-7d75c1d40d88+incompatible
これでビルドの準備が整いました。試しに次のコードをビルドしてみます。
package main import ( "context" "fmt" "github.com/docker/docker/api/types" "github.com/docker/docker/client" ) func main() { ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { panic(err) } cli.NegotiateAPIVersion(ctx) containers, err := cli.ContainerList(ctx, types.ContainerListOptions{}) if err != nil { panic(err) } for _, container := range containers { fmt.Println(container.ID[:12], container.Image) } }
これをgo build
でビルドします。
$ go build # ビルド $ ls # sample が生成されている go.mod go.sum main.go sample
先程のmain.c
がビルドされ、実行ファイルsample
が生成されます。これはdocker ps
のように、起動中のコンテナ一覧を表示するものです。
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 1a041b31e5c1 ubuntu:latest "/bin/sh -c 'while :…" 46 hours ago Up 46 hours confident_mendel 4915a43653bb alpine "/bin/sh -c 'while :…" 2 days ago Up 2 days condescending_wu $ $ ./sample 1a041b31e5c1 ubuntu:latest 4915a43653bb alpine
このように、起動中のコンテナのIDとイメージ名が表示されるはずです。
※コンテナが1つも起動していない場合には何も表示されません。
※docker
コマンドの実行にsudo
が必要な場合には、このsample
を実行する際にもsudo
が必要になります。
このとき、go.mod
は以下のようになっています。
module sample go 1.15 require ( github.com/containerd/containerd v1.4.3 // indirect github.com/docker/distribution v2.7.1+incompatible // indirect github.com/docker/docker v17.12.0-ce-rc1.0.20201201034508-7d75c1d40d88+incompatible github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/gogo/protobuf v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/sirupsen/logrus v1.7.0 // indirect google.golang.org/grpc v1.34.0 // indirect )
このgo.mod
を使い回すことで、他のコードもビルドすることができます。
サンプルコード集
以下のDocker のドキュメントサイトにあるサンプルコードに加えて、コンテナ内でコマンドを実行する例・コンテナ内のシェルに接続する例を紹介します。
docs.docker.com
※解説しやすくするために、一部改変した部分があります。
run
docker run alpine echo hello world
に相当するコードです。
package main import ( "context" "io" "os" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" ) func main() { ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv) if err != nil { panic(err) } cli.NegotiateAPIVersion(ctx) reader, err := cli.ImagePull(ctx, "docker.io/library/alpine", types.ImagePullOptions{}) if err != nil { panic(err) } io.Copy(os.Stdout, reader) resp, err := cli.ContainerCreate(ctx, &container.Config{ Image: "alpine", Cmd: []string{"echo", "hello world"}, Tty: false, }, nil, nil, nil, "") if err != nil { panic(err) } if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil { panic(err) } statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) select { case err := <-errCh: if err != nil { panic(err) } case <-statusCh: } out, err := cli.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ShowStdout: true}) if err != nil { panic(err) } stdcopy.StdCopy(os.Stdout, os.Stderr, out) }
ImagePull
でイメージをpull (このときのログを出力)ContainerCreate
でイメージからコンテナを作成ContainerStart
でコンテナを起動ContainerWait
で処理を待つContainerLogs
で起動後のログを取得
という流れになっています。REST APIが裏にあるSDKなだけあって、とてもわかり易い構造になっていますね。
ここでNegotiateAPIVersion(ctx)
というメソッドを紹介します。これは、その名の通りDocker EngineとDocker Engine APIのバージョンを調整するという便利なメソッドです。
これをビルドして実行すると、以下のようになります。
$ ./sample {"status":"Pulling from library/alpine","id":"latest"} {"status":"Pulling fs layer","progressDetail":{},"id":"188c0c94c7c5"} {"status":"Downloading","progressDetail":{"current":29128,"total":2796860},"progress":"[\u003e ] 29.13kB/2.797MB","id":"188c0c94c7c5"} {"status":"Downloading","progressDetail":{"current":862645,"total":2796860},"progress":"[===============\u003e ] 862.6kB/2.797MB","id":"188c0c94c7c5"} {"status":"Downloading","progressDetail":{"current":1943989,"total":2796860},"progress":"[==================================\u003e ] 1.944MB/2.797MB","id":"188c0c94c7c5"} {"status":"Verifying Checksum","progressDetail":{},"id":"188c0c94c7c5"} {"status":"Download complete","progressDetail":{},"id":"188c0c94c7c5"} {"status":"Extracting","progressDetail":{"current":32768,"total":2796860},"progress":"[\u003e ] 32.77kB/2.797MB","id":"188c0c94c7c5"} {"status":"Extracting","progressDetail":{"current":1998848,"total":2796860},"progress":"[===================================\u003e ] 1.999MB/2.797MB","id":"188c0c94c7c5"} {"status":"Extracting","progressDetail":{"current":2796860,"total":2796860},"progress":"[==================================================\u003e] 2.797MB/2.797MB","id":"188c0c94c7c5"} {"status":"Pull complete","progressDetail":{},"id":"188c0c94c7c5"} {"status":"Digest: sha256:c0e9560cda118f9ec63ddefb4a173a2b2a0347082d7dff7dc14272e7841a5b5a"} {"status":"Status: Downloaded newer image for alpine:latest"} hello world
実行結果のうち、波括弧( "{ }" )で囲まれた出力はイメージをpullした時のログで、最後の"hello world"はContainerLogs
で取得したログです。
たしかに、イメージのpull・コンテナの作成・起動・ログ出力ができていることがわかります。
run -d
docker run -d bfirsh/reticulate-splines
に相当するコードです。
package main import ( "context" "fmt" "io" "os" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" ) func main() { ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { panic(err) } cli.NegotiateAPIVersion(ctx) imageName := "bfirsh/reticulate-splines" out, err := cli.ImagePull(ctx, imageName, types.ImagePullOptions{}) if err != nil { panic(err) } io.Copy(os.Stdout, out) resp, err := cli.ContainerCreate(ctx, &container.Config{ Image: imageName, }, nil, nil, nil, "") if err != nil { panic(err) } if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil { panic(err) } fmt.Println(resp.ID) }
こちらのコードでは、先程と違ってコンテナの処理を待つような記述をしていません。Docker Engineへコンテナの起動を伝えた後で、このクライアントはすぐに終了するため、結果的にコンテナはバックグラウンドで起動されます。
これをビルドして実行すると、以下のようになります。
$ ./sample {"status":"Pulling from bfirsh/reticulate-splines","id":"latest"} {"status":"Pulling fs layer","progressDetail":{},"id":"e110a4a17941"} {"status":"Pulling fs layer","progressDetail":{},"id":"9cb73a4f4dd9"} {"status":"Pulling fs layer","progressDetail":{},"id":"544bc6b7ec68"} {"status":"Downloading","progressDetail":{"current":252,"total":252},"progress":"[==================================================\u003e] 252B/252B","id":"9cb73a4f4dd9"} {"status":"Verifying Checksum","progressDetail":{},"id":"9cb73a4f4dd9"} {"status":"Download complete","progressDetail":{},"id":"9cb73a4f4dd9"} {"status":"Downloading","progressDetail":{"current":23789,"total":2310286},"progress":"[\u003e ] 23.79kB/2.31MB","id":"e110a4a17941"} {"status":"Downloading","progressDetail":{"current":251,"total":251},"progress":"[==================================================\u003e] 251B/251B","id":"544bc6b7ec68"} {"status":"Verifying Checksum","progressDetail":{},"id":"544bc6b7ec68"} {"status":"Download complete","progressDetail":{},"id":"544bc6b7ec68"} {"status":"Downloading","progressDetail":{"current":814295,"total":2310286},"progress":"[=================\u003e ] 814.3kB/2.31MB","id":"e110a4a17941"} {"status":"Downloading","progressDetail":{"current":1886645,"total":2310286},"progress":"[========================================\u003e ] 1.887MB/2.31MB","id":"e110a4a17941"} {"status":"Verifying Checksum","progressDetail":{},"id":"e110a4a17941"} {"status":"Download complete","progressDetail":{},"id":"e110a4a17941"} {"status":"Extracting","progressDetail":{"current":32768,"total":2310286},"progress":"[\u003e ] 32.77kB/2.31MB","id":"e110a4a17941"} {"status":"Extracting","progressDetail":{"current":720896,"total":2310286},"progress":"[===============\u003e ] 720.9kB/2.31MB","id":"e110a4a17941"} {"status":"Extracting","progressDetail":{"current":2310286,"total":2310286},"progress":"[==================================================\u003e] 2.31MB/2.31MB","id":"e110a4a17941"} {"status":"Pull complete","progressDetail":{},"id":"e110a4a17941"} {"status":"Extracting","progressDetail":{"current":252,"total":252},"progress":"[==================================================\u003e] 252B/252B","id":"9cb73a4f4dd9"} {"status":"Extracting","progressDetail":{"current":252,"total":252},"progress":"[==================================================\u003e] 252B/252B","id":"9cb73a4f4dd9"} {"status":"Pull complete","progressDetail":{},"id":"9cb73a4f4dd9"} {"status":"Extracting","progressDetail":{"current":251,"total":251},"progress":"[==================================================\u003e] 251B/251B","id":"544bc6b7ec68"} {"status":"Extracting","progressDetail":{"current":251,"total":251},"progress":"[==================================================\u003e] 251B/251B","id":"544bc6b7ec68"} {"status":"Pull complete","progressDetail":{},"id":"544bc6b7ec68"} {"status":"Digest: sha256:67cfd7db171e3de3551c209cfa24c7ae3757d54806d6b8191994a917f3e92723"} {"status":"Status: Downloaded newer image for bfirsh/reticulate-splines:latest"} 1c01e439e468accac069b1ffeb03b98dab8ad33368d0d1b74e5ed9314d7e1cb7
バックグラウンドで起動されたコンテナを確認してみましょう。
$ docker ps # コンテナがバックグラウンドで起動されている CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 1c01e439e468 bfirsh/reticulate-splines "/usr/local/bin/run.…" 9 minutes ago Up 9 minutes gifted_cohen $ docker container logs gifted_cohen --tail="10" # 起動させたコンテナのログを取得 (末尾10桁) Reticulating spline 670... Reticulating spline 671... Reticulating spline 672... Reticulating spline 673... Reticulating spline 674... Reticulating spline 675... Reticulating spline 676... Reticulating spline 677... Reticulating spline 678... Reticulating spline 679...
たしかに、バックグラウンドでコンテナが起動しています。
ps
docker ps
に相当するコードです。
package main import ( "context" "fmt" "github.com/docker/docker/api/types" "github.com/docker/docker/client" ) func main() { ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { panic(err) } cli.NegotiateAPIVersion(ctx) containers, err := cli.ContainerList(ctx, types.ContainerListOptions{}) if err != nil { panic(err) } for _, container := range containers { fmt.Println(container.ID, container.Image) } }
ContainerList
で起動中のコンテナ一覧を取得しています。
これをビルドして実行すると、以下のようになります。
$ ./sample 1c01e439e468 bfirsh/reticulate-splines 1a041b31e5c1 ubuntu:latest
実際のdocker ps
の出力と見比べてみると、コンテナの情報を正しく取得できていることがわかります。
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 1c01e439e468 bfirsh/reticulate-splines "/usr/local/bin/run.…" 50 minutes ago Up 50 minutes gifted_cohen 1a041b31e5c1 ubuntu:latest "/bin/sh -c 'while :…" 2 days ago Up 2 hours confident_mendel
container stop
docker container stop $(docker ps -q)
に相当するコードです。つまり、起動中のコンテナをすべて停止させるコードです。
※実行する前に、停止させてはいけないコンテナが無いか確かめてください。
package main import ( "context" "fmt" "github.com/docker/docker/api/types" "github.com/docker/docker/client" ) func main() { ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { panic(err) } cli.NegotiateAPIVersion(ctx) containers, err := cli.ContainerList(ctx, types.ContainerListOptions{}) if err != nil { panic(err) } for _, container := range containers { fmt.Print("Stopping container ", container.ID[:10], "... ") if err := cli.ContainerStop(ctx, container.ID, nil); err != nil { panic(err) } fmt.Println("Success") } }
先程のContainerList
を利用して、起動中のコンテナのIDを取得し、それら1つ1つを停止させています。
これをビルドして実行すると、以下のようになります。
$ docker ps # 2つのコンテナが起動中 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 1c01e439e468 bfirsh/reticulate-splines "/usr/local/bin/run.…" 59 minutes ago Up 8 seconds gifted_cohen 1a041b31e5c1 ubuntu:latest "/bin/sh -c 'while :…" 2 days ago Up 8 seconds confident_mendel $ ./sample Stopping container 1c01e439e4... Success Stopping container 1a041b31e5... Success $ docker ps # 起動中のコンテナは無くなった CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES $ docker ps -a # 起動していたコンテナは停止している CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 1c01e439e468 bfirsh/reticulate-splines "/usr/local/bin/run.…" About an hour ago Exited (137) 9 minutes ago gifted_cohen 1a041b31e5c1 ubuntu:latest "/bin/sh -c 'while :…" 2 days ago Exited (137) 9 minutes ago confident_mendel
プログラムを実行する前後にdocker ps
をしています。たしかに、起動していた2つのコンテナが停止されいてることがわかります。
(STATUSがUp 8 seconds
からExited (137) 9 minutes ago
となっているように、間に9分程時間が飛んでいるのは、Twitterしてたからです。ごめんなさい。)
container logs
こちらはコンテナのログを取得するサンプルコードです。まずは、下記のようにログを取得するコンテナを作成します。
$ docker run -d alpine /bin/sh -c "while :; do sleep 10; echo hello; done" 1f16dfd46ff0463631d6c218cbdf46409d838fd58c262b8148c89c55262a1451 $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 1f16dfd46ff0 alpine "/bin/sh -c 'while :…" 13 seconds ago Up 12 seconds determined_kare
作成したコンテナは10秒ごとに"hello"と出力するというものです。また、このコンテナのIDは1f16dfd46ff0で、NAMESはdetermined_kareとなっていることがわかります。
下記のコードは、docker container logs determined_kare
に相当するものです。
package main import ( "context" "os" "github.com/docker/docker/api/types" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" ) var ID string = "1f16dfd46ff0" func main() { ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { panic(err) } cli.NegotiateAPIVersion(ctx) options := types.ContainerLogsOptions{ ShowStdout: true, ShowStderr: true, } out, err := cli.ContainerLogs(ctx, ID, options) if err != nil { panic(err) } stdcopy.StdCopy(os.Stdout, os.Stderr, out) }
これをビルドして実行すると、以下のようになります。
$ ./sample hello hello hello hello $ docker container logs determined_kare hello hello hello hello
ログの取得ができています。
iamge list
docker iamge list
に相当するコードです。
package main import ( "context" "fmt" "github.com/docker/docker/api/types" "github.com/docker/docker/client" ) func main() { ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { panic(err) } cli.NegotiateAPIVersion(ctx) images, err := cli.ImageList(ctx, types.ImageListOptions{}) if err != nil { panic(err) } for _, image := range images { fmt.Println(image.ID) } }
これをビルドして実行すると、以下のようになります。
$ ./sample sha256:f643c72bc25212974c16f3348b3a898b1ec1eb13ec1539e10a103e6e217eb2f1 sha256:d6e46aa2470df1d32034c6707c8041158b652f38d2a9ae3d7ad7e7532d22ebe0 sha256:b1666055931f332541bda7c425e624764de96c85177a61a0b49238a42b80b7f9 $ docker image list REPOSITORY TAG IMAGE ID CREATED SIZE ubuntu latest f643c72bc252 9 days ago 72.9MB alpine latest d6e46aa2470d 6 weeks ago 5.57MB bfirsh/reticulate-splines latest b1666055931f 4 years ago 4.8MB
それぞれのイメージのIDが出力されていることがわかります。
image pull
docker image pull alpine
に相当するコードです。
package main import ( "context" "io" "os" "github.com/docker/docker/api/types" "github.com/docker/docker/client" ) func main() { ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { panic(err) } cli.NegotiateAPIVersion(ctx) out, err := cli.ImagePull(ctx, "alpine", types.ImagePullOptions{}) if err != nil { panic(err) } defer out.Close() io.Copy(os.Stdout, out) }
これをビルドして実行すると、以下のようになります。
$ docker image list REPOSITORY TAG IMAGE ID CREATED SIZE ubuntu latest f643c72bc252 9 days ago 72.9MB bfirsh/reticulate-splines latest b1666055931f 4 years ago 4.8MB | $ ./sample {"status":"Pulling from library/alpine","id":"latest"} {"status":"Pulling fs layer","progressDetail":{},"id":"188c0c94c7c5"} {"status":"Downloading","progressDetail":{"current":29128,"total":2796860},"progress":"[\u003e ] 29.13kB/2.797MB","id":"188c0c94c7c5"} {"status":"Downloading","progressDetail":{"current":883126,"total":2796860},"progress":"[===============\u003e ] 883.1kB/2.797MB","id":"188c0c94c7c5"} {"status":"Downloading","progressDetail":{"current":1866166,"total":2796860},"progress":"[=================================\u003e ] 1.866MB/2.797MB","id":"188c0c94c7c5"} {"status":"Verifying Checksum","progressDetail":{},"id":"188c0c94c7c5"} {"status":"Download complete","progressDetail":{},"id":"188c0c94c7c5"} {"status":"Extracting","progressDetail":{"current":32768,"total":2796860},"progress":"[\u003e ] 32.77kB/2.797MB","id":"188c0c94c7c5"} {"status":"Extracting","progressDetail":{"current":1474560,"total":2796860},"progress":"[==========================\u003e ] 1.475MB/2.797MB","id":"188c0c94c7c5"} {"status":"Extracting","progressDetail":{"current":2796860,"total":2796860},"progress":"[==================================================\u003e] 2.797MB/2.797MB","id":"188c0c94c7c5"} {"status":"Pull complete","progressDetail":{},"id":"188c0c94c7c5"} {"status":"Digest: sha256:c0e9560cda118f9ec63ddefb4a173a2b2a0347082d7dff7dc14272e7841a5b5a"} {"status":"Status: Downloaded newer image for alpine:latest"} $ docker image list REPOSITORY TAG IMAGE ID CREATED SIZE ubuntu latest f643c72bc252 9 days ago 72.9MB alpine latest d6e46aa2470d 6 weeks ago 5.57MB bfirsh/reticulate-splines latest b1666055931f 4 years ago 4.8MB
プログラムを実行する前には無かったAlipineのイメージが追加されていることがわかります。
container commit
docker container commit <コンテナ名> helloworld
に相当するコードです。
ここでは、/
以下にhelloworld
というファイルを作成したAlipineのコンテナを作成し、それをhelloworldという名前でcommitしています。
package main import ( "context" "fmt" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" ) func main() { ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { panic(err) } cli.NegotiateAPIVersion(ctx) createResp, err := cli.ContainerCreate(ctx, &container.Config{ Image: "alpine", Cmd: []string{"touch", "/helloworld"}, }, nil, nil, nil, "") if err != nil { panic(err) } if err := cli.ContainerStart(ctx, createResp.ID, types.ContainerStartOptions{}); err != nil { panic(err) } statusCh, errCh := cli.ContainerWait(ctx, createResp.ID, container.WaitConditionNotRunning) select { case err := <-errCh: if err != nil { panic(err) } case <-statusCh: } commitResp, err := cli.ContainerCommit(ctx, createResp.ID, types.ContainerCommitOptions{Reference: "helloworld"}) if err != nil { panic(err) } fmt.Println(commitResp.ID) }
これをビルドして実行すると、以下のようになります。
$ docker image list REPOSITORY TAG IMAGE ID CREATED SIZE ubuntu latest f643c72bc252 9 days ago 72.9MB alpine latest d6e46aa2470d 6 weeks ago 5.57MB bfirsh/reticulate-splines latest b1666055931f 4 years ago 4.8MB $ ./sample sha256:0d5fd95305dbb347d056e3057a60cacb00fd61788b36cd18adbf6b624da1f999 $ docker image list REPOSITORY TAG IMAGE ID CREATED SIZE helloworld latest 0d5fd95305db 3 seconds ago 5.57MB ubuntu latest f643c72bc252 9 days ago 72.9MB alpine latest d6e46aa2470d 6 weeks ago 5.57MB bfirsh/reticulate-splines latest b1666055931f 4 years ago 4.8MB
プログラムを実行すると、新たにhelloworldというイメージが生成されています。シェルに接続してみると、helloworld
というファイルが生成されていることから、たしかにcommitできていることがわかります。
$ docker run -it helloworld /bin/sh / # ls bin etc home media opt root sbin sys usr dev helloworld lib mnt proc run srv tmp var / # exit
container exec
docker container exec -t <コンテナ名> ls /
に相当するコードです。
ここでは接続先のコンテナを以下のように作成しました。
$ docker run -d ubuntu:latest /bin/sh -c "while :; do sleep 10; done" $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 1a041b31e5c1 ubuntu:latest "/bin/sh -c 'while :…" 2 days ago Up 51 minutes confident_mendel
package main import ( "log" "os" "github.com/docker/docker/api/types" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" "golang.org/x/net/context" ) var ( ID string = "1a041b31e5c1" cmd []string = []string{"ls", "/"} ) func main() { ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv) if err != nil { panic(err) } cli.NegotiateAPIVersion(ctx) config := types.ExecConfig{ AttachStderr: true, AttachStdout: true, Tty: true, Cmd: cmd, } IDResp, err := cli.ContainerExecCreate(ctx, ID, config) if err != nil { log.Panic(err) } resp, err := cli.ContainerExecAttach(ctx, IDResp.ID, types.ExecStartCheck{}) if err != nil { log.Panic(err) } defer func() { if err := resp.Conn.Close(); err != nil { log.Panic(err) } log.Println("connection closed") }() stdcopy.StdCopy(os.Stdout, os.Stderr, resp.Reader) }
これをビルドして実行すると、以下のようになります。
$ ./sample bin dev home lib32 libx32 mnt proc run srv tmp var boot etc lib lib64 media opt root sbin sys usr 2020/12/05 23:42:47 connection closed $ docker container exec -t confident_mendel ls / bin dev home lib32 libx32 mnt proc run srv tmp var boot etc lib lib64 media opt root sbin sys usr
ls
コマンドを実行できていることがわかります。
container exec /bin/bash
docker container exec -it confident_mendel /bin/bash
に相当するコードです。つまり、コンテナのシェルに接続するコードです。
ここでは先程のcontainer execと同じコンテナに接続しています。
package main import ( "io" "log" "os" "github.com/docker/docker/api/types" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" "golang.org/x/crypto/ssh/terminal" "golang.org/x/net/context" ) var ( ID string = "1a041b31e5c1" cmd []string = []string{"/bin/bash"} ) func main() { ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv) if err != nil { panic(err) } cli.NegotiateAPIVersion(ctx) config := types.ExecConfig{ AttachStdin: true, AttachStderr: true, AttachStdout: true, Tty: true, Cmd: cmd, } exec, err := cli.ContainerExecCreate(ctx, ID, config) if err != nil { log.Panic(err) } resp, err := cli.ContainerExecAttach(ctx, exec.ID, types.ExecStartCheck{}) if err != nil { log.Panic(err) } defer func() { if err := resp.Conn.Close(); err != nil { log.Panic() } log.Println("connection closed") }() fd := int(os.Stdin.Fd()) if terminal.IsTerminal(fd) { state, err := terminal.MakeRaw(fd) if err != nil { log.Panic(err) } defer terminal.Restore(fd, state) w, h, err := terminal.GetSize(fd) if err != nil { log.Panic(err) } if w < 0 || h < 0 { log.Panic("terminal size error", " w: ", w, " h:", h) } resizeOptions := types.ResizeOptions{ Height: uint(h), Width: uint(w), } if err := cli.ContainerExecResize(ctx, exec.ID, resizeOptions); err != nil { log.Panic(err) } } go io.Copy(resp.Conn, os.Stdin) stdcopy.StdCopy(os.Stdout, os.Stderr, resp.Reader) }
これをビルドして実行すると、以下のようになります。
$ ./sample root@1a041b31e5c1:/# ls bin dev home lib32 libx32 mnt proc run srv tmp var boot etc lib lib64 media opt root sbin sys usr root@1a041b31e5c1:/# uname -a Linux 1a041b31e5c1 4.15.0-123-generic #126-Ubuntu SMP Wed Oct 21 09:40:11 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux root@1a041b31e5c1:/# cat /etc/issue Ubuntu 20.04.1 LTS \n \l root@1a041b31e5c1:/#
Dockerfile
Dockerfileを読み込んでコンテナイメージをビルドするサンプルです。カレントディレクトリにDockerfileがある時、docker image build . -t name:tag
と同じような動作をします。
package main import ( "archive/tar" "bytes" "context" "io" "io/ioutil" "log" "os" "github.com/docker/docker/api/types" "github.com/docker/docker/client" ) var ( dockerfileName string = "Dockerfile" imageNameAndTags []string = []string{"name:tag"} ) func main() { ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv) if err != nil { log.Panic(err) } cli.NegotiateAPIVersion(ctx) buildOptions := types.ImageBuildOptions{ Dockerfile: dockerfileName, Remove: true, Tags: imageNameAndTags, } res, err := cli.ImageBuild( ctx, getArchivedDockerfile(dockerfileName), buildOptions, ) if err != nil { log.Panic(err) } defer res.Body.Close() io.Copy(os.Stdout, res.Body) } func getArchivedDockerfile(dockerfile string) *bytes.Reader { // read the Dockerfile f, err := os.Open(dockerfile) if err != nil { log.Panic(err) } defer func() { if err := f.Close(); err != nil { log.Panic(err) } }() b, err := ioutil.ReadAll(f) if err != nil { log.Panic(err) } // archive the Dockerfile tarHeader := &tar.Header{ Name: dockerfile, Size: int64(len(b)), } buf := new(bytes.Buffer) tw := tar.NewWriter(buf) defer tw.Close() err = tw.WriteHeader(tarHeader) if err != nil { log.Panic(err) } _, err = tw.Write(b) if err != nil { log.Panic(err) } return bytes.NewReader(buf.Bytes()) }
動作の確認のために以下のようなDockerfileを用意しました。
FROM alpine:3.8 RUN /bin/sh -c 'echo "hello world!"'
プログラムをビルドして、このDockerfileと同じディレクトリで実行すると以下のようになります。
$ ./sample {"stream":"Step 1/2 : FROM alpine:3.8"} {"stream":"\n"} {"status":"Pulling from library/alpine","id":"3.8"} {"status":"Pulling fs layer","progressDetail":{},"id":"486039affc0a"} {"status":"Downloading","progressDetail":{"current":22282,"total":2207101},"progress":"[\u003e ] 22.28kB/2.207MB","id":"486039affc0a"} {"status":"Downloading","progressDetail":{"current":907702,"total":2207101},"progress":"[====================\u003e ] 907.7kB/2.207MB","id":"486039affc0a"} {"status":"Downloading","progressDetail":{"current":1874358,"total":2207101},"progress":"[==========================================\u003e ] 1.874MB/2.207MB","id":"486039affc0a"} {"status":"Verifying Checksum","progressDetail":{},"id":"486039affc0a"} {"status":"Download complete","progressDetail":{},"id":"486039affc0a"} {"status":"Extracting","progressDetail":{"current":32768,"total":2207101},"progress":"[\u003e ] 32.77kB/2.207MB","id":"486039affc0a"} {"status":"Extracting","progressDetail":{"current":1900544,"total":2207101},"progress":"[===========================================\u003e ] 1.901MB/2.207MB","id":"486039affc0a"} {"status":"Extracting","progressDetail":{"current":2207101,"total":2207101},"progress":"[==================================================\u003e] 2.207MB/2.207MB","id":"486039affc0a"} {"status":"Pull complete","progressDetail":{},"id":"486039affc0a"} {"status":"Digest: sha256:2bb501e6173d9d006e56de5bce2720eb06396803300fe1687b58a7ff32bf4c14"} {"status":"Status: Downloaded newer image for alpine:3.8"} {"stream":" ---\u003e c8bccc0af957\n"} {"stream":"Step 2/2 : RUN /bin/sh -c 'echo \"hello world!\"'"} {"stream":"\n"} {"stream":" ---\u003e Running in a29ade0133a1\n"} {"stream":"hello world!\n"} {"stream":"Removing intermediate container a29ade0133a1\n"} {"stream":" ---\u003e 9cd174e92745\n"} {"aux":{"ID":"sha256:9cd174e92745d435e66c01687f7b35f13ceffa1dfc60cbcdb6c3b6896abf2715"}} {"stream":"Successfully built 9cd174e92745\n"} {"stream":"Successfully tagged name:tag\n"} $ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE name tag 9cd174e92745 45 seconds ago 4.41MB alpine 3.8 c8bccc0af957 10 months ago 4.41MB
Dockerfileを用いたイメージのビルドができています。SDKにDockerfileの内容を渡す際、TARで圧縮する必要があることに注意です。