Docker Engine APIでサンドボックス環境を試してみる
概要
CI自体の開発や、教育支援システム開発などでは、何度も同じ環境でコードを実行し、再現性のある結果を出力する必要があります。
CIの例はイメージしやすいと思いますが、教育支援システムにプログラムの自動採点機能を設けるとすると、その採点ロジックは実際にコードを実行し、期待する結果が出力できているかを照合する処理を実装します。
このような機能は、サンドボックス用の Docker や Kubernetes に対して、サーバーサイドアプリケーションからコンテナ作成依頼をする形で実現されると思います。
自分自身、そういったユースケースを考えることが多かったため、興味の範疇で上記を実現するためのスニペットを書いてみました。
試すこと
Docker SDK を使用して、以下を試してみたいと思います。
- コンテナを作成
- コンテナでのコマンド実行
- 非同期での標準出力の拾い上げ
準備物
- コンテナ内で実行するサンプルコード
- コンテナを作成、実行、標準出力を取得するためのクライアント実装
まずは 1 から。
次のコードを使用して、30秒間、毎秒ログを吐きます。
package main import ( "context" "log" "os" "time" ) func main() { l := log.New(os.Stdout, "echo: ", 0) ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) defer cancel() l.Println("start") for { select { case <-ctx.Done(): l.Println("finish") return default: l.Println("hello") time.Sleep(time.Second) } } }
このコードをビルドし、Docker Image (echo:latest
) を作ります。
次に、2 のクライアントコードを示します。
Docker SDK を使用して、「コンテナ作成 -> コンテナの開始 -> Exec -> コンテナの標準出力をコピーして標準出力に表示」を行います。
package main import ( "bytes" "context" "fmt" "os" "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" "github.com/google/uuid" ) type runConfig struct { Image string Tag string Cmd []string } func main() { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() c := runConfig{ Image: "echo", Tag: "latest", Cmd: []string{"/echo"}, } if err := run(ctx, c); err != nil { fmt.Println(err) os.Exit(1) return } } func run(ctx context.Context, runCfg runConfig) error { cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return fmt.Errorf("new client error: %w", err) } u, err := uuid.NewUUID() if err != nil { return fmt.Errorf("new uuid error: %w", err) } name := runCfg.Image + "_" + u.String() containerCfg := &container.Config{ Image: fmt.Sprintf("%s:%s", runCfg.Image, runCfg.Tag), Tty: true, } resp, err := cli.ContainerCreate(ctx, containerCfg, nil, nil, nil, name) if err != nil { return fmt.Errorf("create container error: %w", err) } defer func() { cli.ContainerRemove(ctx, name, types.ContainerRemoveOptions{Force: true}) }() containerID := resp.ID if err := cli.ContainerStart(ctx, containerID, types.ContainerStartOptions{}); err != nil { return fmt.Errorf("start container error: %w", err) } // wait for container starting for { resp, err := cli.ContainerInspect(ctx, containerID) if err != nil { return fmt.Errorf("inspect container error: %w", err) } if !resp.State.Running { if resp.State.Dead { return fmt.Errorf("container dead") } if e := resp.State.Error; e != "" { return fmt.Errorf("container error: %s", e) } if c := resp.State.ExitCode; c != 0 { return fmt.Errorf("container exited code: %d", c) } time.Sleep(500 * time.Millisecond) continue } break } execCfg := types.ExecConfig{ AttachStderr: true, AttachStdout: true, Cmd: runCfg.Cmd, } exec, err := cli.ContainerExecCreate(ctx, containerID, execCfg) if err != nil { return fmt.Errorf("create exec error: %w", err) } out, err := cli.ContainerExecAttach(ctx, exec.ID, types.ExecStartCheck{}) if err != nil { return fmt.Errorf("attach exec error: %w", err) } stdo := new(bytes.Buffer) stde := new(bytes.Buffer) end := make(chan struct{}, 1) defer close(end) go func() { _, err := stdcopy.StdCopy(stdo, stde, out.Reader) if err != nil { panic(fmt.Errorf("copy error: %w", err)) } end <- struct{}{} }() ticker := time.NewTicker(time.Second) defer ticker.Stop() for { select { case <-ticker.C: fmt.Println(stdo) case <-end: return nil } } }
ticker
の周期を短くすることで、よりリアルタイムに標準出力を拾えるようになります。
まとめ
教育支援システムのユースケースを考えるならば、プログラムコードをコンテナに埋め込んだり、プログラムコードから Docker Image を作成したりする処理も必要になります。
その場合は、cli.ImageBuild(...)
やcli.ImageCreate(...)
を使用することでイメージを用意することもできるので、上記と組み合わせて実現できそうです。