Help us understand the problem. What is going on with this article?

Go言語でdockerを操作する【イメージのpull,コンテナ作成,exec等】

初めに

goでdockerを操作する機会があったので,備忘録も兼ねて記事化します.

なお,Qiita投稿,go言語,docker APIすべてにおいて初心者なので,ミス等あると思いますが優しく指摘していただけると幸いです.

本記事で用いたコードはここにおいています.

参考文献

公式

https://godoc.org/github.com/docker/docker/client
https://docs.docker.com/engine/api/v1.40/
https://github.com/moby/moby/tree/master/client

個人ブログ等

http://otiai10.hatenablog.com/entry/2017/05/24/163701
https://kuroeveryday.blogspot.com/2017/09/docker-remote-api-with-golang.html

環境

OS: Ubuntu 18.04.3
Go: 1.10.4 linux/amd64
docker: 19.03.2, build 6a30dfc

現状確認

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

imageもcontainerも何もない状態からスタート

下準備

dockerAPIをgo getする.

$ go get github.com/docker/docker/api/

続いて,必要なライブラリ群をインポートしておく.

package main

import (
    "context"
    "fmt"
    "bufio"
    "encoding/json"

    "github.com/docker/docker/client"
    "github.com/docker/docker/api/types"
    "github.com/docker/docker/api/types/container"
)

func main()  {
    cli, err := client.NewClientWithOpts(client.FromEnv)
    if err != nil {
        panic(err)
    }
    ctx := context.Background()
}

これでGoでdockerを操作する準備が整った.

注意

APIとdockerのバージョンが対応していないと以下のようなエラーメッセージが出る.

panic: Error response from daemon: client version <xxx> is too new. Maximum supported API version is <yyy>

回避策は,

cli, err := client.NewClientWithOpts(client.FromEnv)

の部分を

cli, err := client.NewClientWithOpts(client.WithVersion("<yyy>"))

のように書き換える.
ちなみにdockerとAPIのバージョンの対応関係は
https://docs.docker.com/develop/sdk/
の中程参照.

ImageをPullしてみる.

いろいろ遊ぼうにもまずimageがないと何もできないので適当にpullしてみる.
軽量なalpineをpullする.
まずはコードを記載する.

func main(){
//前略
    resp, err := cli.ImagePull(ctx, "alpine", types.ImagePullOptions{})
    if err != nil {
        panic(err)
    }

    defer resp.Close()

    payload := struct {
        ID             string `json:"id"`
        Status         string `json:"status"`
        Progress       string `json:"progress"`
        ProgressDetail struct {
            Current uint16 `json:"current"`
            Total   uint16 `json:"total"`
        } `json:"progressDetail"`
    }{}

    scanner := bufio.NewScanner(resp)
    for scanner.Scan() {
        json.Unmarshal(scanner.Bytes(), &payload)
        fmt.Printf("\t%+v\n", payload)
    }
}

これでalpineのイメージが手に入る.

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
alpine              latest              961769676411        6 weeks ago         5.58MB

pullのメイン処理は以下の部分.

resp, err := cli.ImagePull(ctx, "alpine", types.ImagePullOptions{})

第2引数にpullしたいイメージ名を指定する.
alpine:20190228のようにタグ付きで指定することも出来る.
何も指定しない場合はlatestがpullされる.

第3引数にはpull時のオプションを指定できる.
指定できるオプションは以下の通り.

type ImagePullOptions struct {
    All           bool   // If true,all tags for the given image to be pulled.
    RegistryAuth  string // RegistryAuth is the base64 encoded credentials for the registry
    PrivilegeFunc RequestPrivilegeFunc
    Platform      string //Platform in the format os[/arch[/variant]]
}

コンテナ作成

pullしたalpineをもとにコンテナを作成する.

//前略
    containerConfig := &container.Config{
        Image: "alpine",
        Cmd: []string{"echo", "Hello World"},
    }

    containerBody,err := cli.ContainerCreate(ctx,containerConfig,nil,nil,"hello")

    if err != nil {
        panic(err)
    }

コンテナ確認

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS               NAMES
1570c1f8b27a        alpine              "echo 'Hello World'"   21 seconds ago      Created                                 hello

container.Config(第二引数),
container.HostConfig(第三引数),
network.NetworkingConfig(第四引数)
に指定できるのはそれぞれリンク先の通り.

今回は,イメージの指定とコマンドの指定のみ行っている.
なお,第五引数はコンテナの名前を指定する.

コンテナ起動

作ったコンテナを起動させる.

err = cli.ContainerStart(ctx,containerBody.ID,types.ContainerStartOptions{})
    if err != nil {
        panic(err)
    }

指定できるオプション -> types.ContainerStartOptions

続いて,コンテナの出力をもらってくる.

containerLogsOptions :=types.ContainerLogsOptions{
        ShowStdout:true,
    }

    respContainer ,err := cli.ContainerLogs(ctx,containerBody.ID,containerLogsOptions)
    if err != nil {
        panic(err)
       }

    defer respContainer.Close()
    scanner = bufio.NewScanner(respContainer)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }

これで実行するとHello Worldが出力される.

types.ContainerLogsOptionsでは,標準出力をもらいたいため,ShowStdouttrueにしている.

コンテナ一覧の取得

    containers, err := cli.ContainerList(ctx, containerListOptions)
    if err != nil {
        panic(err)
    }

    for _, container := range containers {
        fmt.Println(container.ID, container.Names[0], container.Image)
    }

(なんでコンテナ名が[]stringで返ってくるのだろう...)

オプション
types.ContainerListOptions

All: trueにするとrunning以外のコンテナも出力する,
SinceBeforeにはcontainer name 若しくは container id を入れるっぽい(未検証)

docker execをする.

docker exec hoge <container>みたいなことをしてみる.
さっきのコンテナはすぐにHello World後に死んでしまうので新たなコンテナを作る.

    containerConfig := &container.Config{
        Image: "alpine",
        Cmd: []string{"/bin/sh"},
        Tty: true,
    }

    hostConfig := &container.HostConfig{
        AutoRemove: true,
    }

    containerBody,err := cli.ContainerCreate(ctx,containerConfig,hostConfig,nil,"exectest")

    if err != nil {
        panic(err)
    }

    //コンテナの起動

    err = cli.ContainerStart(ctx,containerBody.ID,types.ContainerStartOptions{})
    if err != nil {
        panic(err)
    }

(ちなみに,hostConfigAutoRemove: trueを指定するとコンテナが停止したときに勝手に消えてくれる.docker run --rmと同じ)

コンテナの生存確認

$ docker ps 
2660a4b70fa1        alpine              "/bin/sh"           8 seconds ago       Up 7 seconds                            exectest

このrunningのコンテナに,docker execを行う.

//docker exec
    execConfig := types.ExecConfig{
        AttachStdout:  true,
        Cmd:          []string{"echo","Hello Japan"},
    }

    idRes, err := cli.ContainerExecCreate(ctx, containerBody.ID, execConfig)
    if err != nil {
        panic(err)
    }

    res, err := cli.ContainerExecAttach(ctx, idRes.ID, types.ExecStartCheck{})

    if err != nil {
        panic(err)
    }

    defer res.Close()

    stdout := new(bytes.Buffer)
    stderr := new(bytes.Buffer)
    _, err = stdcopy.StdCopy(stdout, stderr, res.Reader)
    if err != nil {
        panic(err)
    }
    fmt.Println(stdout.String())

ここの処理だけ直感的な操作とは違って少し詰まった.

コマンドライン上での同等のコマンドは,

$ docker exec exectest echo "Hello Japan"
Hello Japan

なので,一回API叩くだけだと思い相当の関数を探していたがどうやらAPI上では,
1. execの準備(ContainerExecCreate)
2. execの実行(ContainerExecAttach)
となるようだ.まぁ分かってしまえばなんてことないのだが.
2つの関数のオプションは,
types.ExecConfig

types.ExecStartCheck
である.

コンテナの削除

先程作ったコンテナを削除

//コンテナの削除
    containerRemoveOptions := types.ContainerRemoveOptions{
        Force: true,
    }
    err = cli.ContainerRemove(ctx,containerBody.ID,containerRemoveOptions)
    if err != nil {
        panic(err)
    }

types.ContainerRemoveOptionsにはForce: trueを指定し強制削除した.

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away