Go言語でDocker SDKを使いDockerfileからイメージをビルドする手法

Docker SDK for Goを使ってリモートのDockerデーモン上でイメージをビルドする際、Dockerfileと関連ファイルをそのまま読み込ませると「Cannot locate specified Dockerfile」エラーが発生する。これはDockerビルドコンテキストがtarアーカイブとしてパッケージ化されている必要があるためだ。標準ライブラリのarchive/tarを用いて手動でtarアーカイブを作成する方法を示す。

ディレクトリ全体をtarにパッケージ化

package main

import (
	"archive/tar"
	"errors"
	"io"
	"os"
	"path/filepath"
)

func archiveDirectory(baseDir, outPath string) error {
	outFile, err := os.Create(outPath)
	if err != nil {
		return err
	}
	defer outFile.Close()

	tw := tar.NewWriter(outFile)
	defer tw.Close()

	return filepath.Walk(baseDir, func(path string, fi os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		rel, err := filepath.Rel(baseDir, path)
		if err != nil {
			return err
		}
		hdr, err := tar.FileInfoHeader(fi, rel)
		if err != nil {
			return err
		}
		if err := tw.WriteHeader(hdr); err != nil {
			return err
		}
		if !fi.Mode().IsRegular() {
			return nil
		}
		f, err := os.Open(path)
		if err != nil {
			return err
		}
		defer f.Close()
		_, err = io.Copy(tw, f)
		return err
	})
}

func main() {
	if err := archiveDirectory("./buildctx", "context.tar"); err != nil {
		panic(err)
	}
}

特定ファイルのみをtarにパッケージ化

package main

import (
	"archive/tar"
	"io"
	"os"
)

func archiveFiles(paths []string, outPath string) error {
	outFile, err := os.Create(outPath)
	if err != nil {
		return err
	}
	defer outFile.Close()

	tw := tar.NewWriter(outFile)
	defer tw.Close()

	for _, p := range paths {
		f, err := os.Open(p)
		if err != nil {
			return err
		}
		stat, err := f.Stat()
		if err != nil {
			f.Close()
			return err
		}
		hdr := &tar.Header{
			Name: p,
			Mode: int64(stat.Mode().Perm()),
			Size: stat.Size(),
		}
		if err := tw.WriteHeader(hdr); err != nil {
			f.Close()
			return err
		}
		if _, err := io.Copy(tw, f); err != nil {
			f.Close()
			return err
		}
		f.Close()
	}
	return nil
}

func main() {
	if err := archiveFiles([]string{"Dockerfile", "app.py"}, "build.tar"); err != nil {
		panic(err)
	}
}

アーカイブ作成からイメージビルドまで統合

package main

import (
	"archive/tar"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"path/filepath"

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

func createTarStream(baseDir string) (io.Reader, error) {
	buf := new(bytes.Buffer)
	tw := tar.NewWriter(buf)
	defer tw.Close()

	err := filepath.Walk(baseDir, func(path string, fi os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		rel, err := filepath.Rel(baseDir, path)
		if err != nil {
			return err
		}
		hdr, err := tar.FileInfoHeader(fi, rel)
		if err != nil {
			return err
		}
		if err := tw.WriteHeader(hdr); err != nil {
			return err
		}
		if !fi.Mode().IsRegular() {
			return nil
		}
		f, err := os.Open(path)
		if err != nil {
			return err
		}
		defer f.Close()
		_, err = io.Copy(tw, f)
		return err
	})
	if err != nil {
		return nil, err
	}
	return buf, nil
}

func main() {
	cli, err := client.NewClientWithOpts(
		client.WithHost("tcp://10.0.0.12:2376"),
		client.WithAPIVersionNegotiation(),
	)
	if err != nil {
		panic(err)
	}

	// メモリ上でtarストリームを生成(ファイルに書き出さない)
	buildCtx, err := createTarStream("./buildctx")
	if err != nil {
		panic(err)
	}

	opts := types.ImageBuildOptions{
		Dockerfile: "Dockerfile",
		Tags:       []string{"myapp:latest"},
	}

	resp, err := cli.ImageBuild(context.Background(), buildCtx, opts)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	dec := json.NewDecoder(resp.Body)
	for {
		var msg map[string]interface{}
		if err := dec.Decode(&msg); err != nil {
			break
		}
		if stream, ok := msg["stream"]; ok {
			fmt.Print(stream)
		}
	}
}

上記の統合例では、tarアーカイブをファイルに保存せずにbytes.Bufferでメモリ上に生成し、直接ImageBuildに渡している。これにより一時ファイルの作成・削除が不要になり、よりクリーンな実装となる。

タグ: Go Docker Docker SDK イメージビルド tar

8月27日 00:18 投稿