APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

[Dagger] コードでDockerイメージをBuildする

はじめに

こんにちはACS事業部の谷合です。
みなさんDaggerなるツールをご存じでしょうか?
Daggerとは、数か月前にコードでCI/CDを実行するツールとしてCloudNative界隈を騒がせたツールです。 dagger.io このDagger、私が知ったのはTwitterでした。
その時は単にコードでCI/CDを実行するためだけのツールとの認識でしたが、最近思い立って調査してみるとコードでBuildができる機能が生えていましたので、今回は私の主言語であるGo言語でBuildをする方法をご紹介したいと思います。

Dockerfileを用いてBuild

今回は以下のDokcerfileを用いて、Buildしてみました。

FROM golang:1.19

WORKDIR /workspace
COPY go.mod go.mod
RUN go mod download

# Copy the go source
COPY main.go main.go

# Build
RUN CGO_ENABLED=0  go build -a -o myapp main.go

ENTRYPOINT ["/myapp"]

コードとしては以下のようになり、/home/taniai/dagger-testディレクトリにあるDockerfileを読み取り、BuildしてAzure Container Registry(以降ACR)にPushしているだけの単純なコードとなります。

package main

import (
    "context"
    "fmt"
    "os"

    "dagger.io/dagger"
)

func main() {
    ctx := context.Background()
    client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
    if err != nil {
        panic(err)
    }
    defer client.Close()

    addr, err := client.Host().Directory("/home/taniai/dagger-test").
        DockerBuild().
        Publish(ctx, "taniaitest.azurecr.io/myexample:with-dockerfile")
    if err != nil {
        panic(err)
    }
    fmt.Printf("Published to %s", addr)
}

これをBuildすることで以下のような出力が得られます。

$ go run main.go 
4: upload /home/taniai/dagger-test DONE
4: > in host.directory /home/taniai/dagger-test
4: upload /home/taniai/dagger-test DONE

4: upload /home/taniai/dagger-test
4: > in host.directory /home/taniai/dagger-test
4: transferring /home/taniai/dagger-test: 
4: transferring /home/taniai/dagger-test: 2.828KiB [0.01s]
4: upload /home/taniai/dagger-test DONE

3: copy /home/taniai/dagger-test CACHED
3: > in host.directory /home/taniai/dagger-test
3: copy /home/taniai/dagger-test CACHED

3: copy /home/taniai/dagger-test CACHED
3: > in host.directory /home/taniai/dagger-test
3: > in docker build
3: copy /home/taniai/dagger-test CACHED

4: upload /home/taniai/dagger-test DONE
4: > in host.directory /home/taniai/dagger-test
4: > in docker build
4: transferring /home/taniai/dagger-test: 2.828KiB [0.01s]
4: upload /home/taniai/dagger-test DONE

12: [builder 1/6] FROM docker.io/library/golang:1.19@sha256:405b708c4612817e9966bff1469570eaed1ecf02a98055fb43bd6ca7b6401b37
12: > in docker build
12: resolve docker.io/library/golang:1.19@sha256:405b708c4612817e9966bff1469570eaed1ecf02a98055fb43bd6ca7b6401b37 
12: ...

13: [stage-1 1/3] FROM gcr.io/distroless/static:nonroot@sha256:9ecc53c269509f63c69a266168e4a687c7eb8c0cfd753bd8bfcaa4f58a90876f DONE
13: > in docker build
13: resolve gcr.io/distroless/static:nonroot@sha256:9ecc53c269509f63c69a266168e4a687c7eb8c0cfd753bd8bfcaa4f58a90876f [0.07s]
13: [stage-1 1/3] FROM gcr.io/distroless/static:nonroot@sha256:9ecc53c269509f63c69a266168e4a687c7eb8c0cfd753bd8bfcaa4f58a90876f DONE

12: [builder 1/6] FROM docker.io/library/golang:1.19@sha256:405b708c4612817e9966bff1469570eaed1ecf02a98055fb43bd6ca7b6401b37 DONE
12: > in docker build
12: resolve docker.io/library/golang:1.19@sha256:405b708c4612817e9966bff1469570eaed1ecf02a98055fb43bd6ca7b6401b37 [0.08s]
12: [builder 1/6] FROM docker.io/library/golang:1.19@sha256:405b708c4612817e9966bff1469570eaed1ecf02a98055fb43bd6ca7b6401b37 DONE

9: [builder 3/6] COPY go.mod go.mod CACHED
9: > in docker build
9: [builder 3/6] COPY go.mod go.mod CACHED

10: copy / / CACHED
10: > in docker build
10: copy / / CACHED

11: [builder 2/6] WORKDIR /workspace CACHED
11: > in docker build
11: [builder 2/6] WORKDIR /workspace CACHED

14: [stage-1 2/3] COPY --from=builder /workspace/myapp . CACHED
14: > in docker build
14: [stage-1 2/3] COPY --from=builder /workspace/myapp . CACHED

15: [builder 6/6] RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o myapp main.go CACHED
15: > in docker build
15: [builder 6/6] RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o myapp main.go CACHED

16: [builder 5/6] COPY main.go main.go CACHED
16: > in docker build
16: [builder 5/6] COPY main.go main.go CACHED

17: [builder 4/6] RUN go mod download CACHED
17: > in docker build
17: [builder 4/6] RUN go mod download CACHED

18: exporting to image
18: exporting layers 
18: exporting layers [0.00s]
18: exporting manifest sha256:889a6d9431189999abbe39495a8e1526324ef5bbd899d2acfbfab24277a77d0e 
18: exporting manifest sha256:889a6d9431189999abbe39495a8e1526324ef5bbd899d2acfbfab24277a77d0e [0.02s]
18: exporting config sha256:369d542cb26dfdf5b711a00bc3dda04c65d580a6916d177e2bbaaee2f9f79f2c 
18: exporting config sha256:369d542cb26dfdf5b711a00bc3dda04c65d580a6916d177e2bbaaee2f9f79f2c [0.03s]
18: pushing layers 
18: pushing layers [0.35s]
18: pushing manifest for taniaitest.azurecr.io/myexample:with-dockerfile@sha256:889a6d9431189999abbe39495a8e1526324ef5bbd899d2acfbfab24277a77d0e 
18: pushing manifest for taniaitest.azurecr.io/myexample:with-dockerfile@sha256:889a6d9431189999abbe39495a8e1526324ef5bbd899d2acfbfab24277a77d0e [0.47s]
18: exporting to image DONE
Published to taniaitest.azurecr.io/myexample:with-dockerfile@sha256:889a6d9431189999abbe39495a8e1526324ef5bbd899d2acfbfab24277a77d0e

Dockerfileを使用せずにコードでBuild

DaggerはDockerfileを使用しない、コードでのBuildも可能です。
モデルとなるDockerfileは前セクションDockerfileを用いてBuildで記載したDockerfileで、こちらをコードで表現してみます。
WithDirectory関数で/home/taniai/dagger-testにあるファイルをすべて/workspaceディレクトリにコピーして、go buildしてACRにPushしています。

package main

import (
    "context"
    "fmt"
    "os"

    "dagger.io/dagger"
)

func main() {
    ctx := context.Background()
    client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
    if err != nil {
        panic(err)
    }
    defer client.Close()

    project := client.Host().Directory("/home/taniai/dagger-test")
    builder := client.Container().
        From("golang:1.19").
        WithDirectory("/workspace", project).
        WithWorkdir("/workspace").
        WithEnvVariable("CGO_ENABLED", "0").
        WithExec([]string{"go", "build", "-o", "myapp"})

    // publish the container image
    addr, err := builder.Publish(ctx, "taniaitest.azurecr.io/myapp:whithout-dockerfile")
    if err != nil {
        panic(err)
    }
    fmt.Printf("Published to %s", addr)
}

これをBuildすることで以下のような出力が得られます。
ちゃんとBuild & Pushできてますね。

$ go run main.go 
4: upload /home/taniai/dagger-test DONE
4: > in host.directory /home/taniai/dagger-test
4: upload /home/taniai/dagger-test DONE

4: upload /home/taniai/dagger-test
4: > in host.directory /home/taniai/dagger-test
4: transferring /home/taniai/dagger-test: 
4: transferring /home/taniai/dagger-test: 2.828KiB [0.01s]
4: upload /home/taniai/dagger-test DONE

3: copy /home/taniai/dagger-test CACHED
3: > in host.directory /home/taniai/dagger-test
3: copy /home/taniai/dagger-test CACHED

8: resolve image config for docker.io/library/golang:1.19
8: > in from golang:1.19
8: resolve image config for docker.io/library/golang:1.19 DONE

4: upload /home/taniai/dagger-test DONE
4: > in host.directory /home/taniai/dagger-test
4: transferring /home/taniai/dagger-test: 2.828KiB [0.01s]
4: upload /home/taniai/dagger-test DONE

3: copy /home/taniai/dagger-test CACHED
3: > in host.directory /home/taniai/dagger-test
3: copy /home/taniai/dagger-test CACHED

17: pull docker.io/library/golang:1.19
17: > in from golang:1.19
17: resolve docker.io/library/golang:1.19@sha256:405b708c4612817e9966bff1469570eaed1ecf02a98055fb43bd6ca7b6401b37 
17: resolve docker.io/library/golang:1.19@sha256:405b708c4612817e9966bff1469570eaed1ecf02a98055fb43bd6ca7b6401b37 [0.03s]
17: pull docker.io/library/golang:1.19 DONE

16: copy / /workspace CACHED
16: copy / /workspace CACHED

14: exec go build -o myapp
14: exec go build -o myapp DONE

18: exporting to image
18: exporting layers 
18: exporting layers [0.86s]
18: exporting manifest sha256:5b8b72ca28a376a48628094699bc4d76a5f8278380acc4167761fbd54302e453 
18: exporting manifest sha256:5b8b72ca28a376a48628094699bc4d76a5f8278380acc4167761fbd54302e453 [0.04s]
18: exporting config sha256:414bb776d51659fe945dd0186e0e61e77a19b78541a765ff5997eb1af99168fa 
18: exporting config sha256:414bb776d51659fe945dd0186e0e61e77a19b78541a765ff5997eb1af99168fa [0.03s]
18: pushing layers 
18: pushing layers [0.89s]
18: pushing manifest for taniaitest.azurecr.io/myapp:whithout-dockerfile@sha256:5b8b72ca28a376a48628094699bc4d76a5f8278380acc4167761fbd54302e453 
18: pushing manifest for taniaitest.azurecr.io/myapp:whithout-dockerfile@sha256:5b8b72ca28a376a48628094699bc4d76a5f8278380acc4167761fbd54302e453 [0.49s]
18: exporting to image DONE
Published to taniaitest.azurecr.io/myapp:whithout-dockerfile@sha256:5b8b72ca28a376a48628094699bc4d76a5f8278380acc4167761fbd54302e453

Dockerfileを用いないMulti-Stage Build

通常のBuildだけなく、Multi-Stage Buildも可能です。
以下のようなMulti-Stage Buildを行うDockerfileをコードで表現してみます。

# Build the myapp binary
FROM golang:1.19 as builder

WORKDIR /workspace
COPY go.mod go.mod
RUN go mod download

# Copy the go source
COPY main.go main.go

# Build
RUN CGO_ENABLED=0 go build -a -o myapp main.go

# Use distroless as minimal base image to package the myapp binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/myapp .
USER 65532:65532

ENTRYPOINT ["/myapp"]

以下のようなコードとなり、最初にbuilder変数に初回のBuildした結果を入れます。
その後、prodImage変数に次回のBuild結果を入れ、最後にACRにPushしています。

package main

import (
    "context"
    "fmt"
    "os"

    "dagger.io/dagger"
)

func main() {
    ctx := context.Background()
    client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
    if err != nil {
        panic(err)
    }
    defer client.Close()

    project := client.Host().Directory("/home/taniai/dagger-test")
    builder := client.Container().
        From("golang:1.19").
        WithDirectory("/workspace", project).
        WithWorkdir("/workspace").
        WithEnvVariable("CGO_ENABLED", "0").
        WithExec([]string{"go", "build", "-o", "myapp"})

    prodImage := client.Container().
        From("gcr.io/distroless/static:nonroot").
        WithFile("/workspace/myapp", builder.File("/workspace/myapp")).
        WithEntrypoint([]string{"/workspace/myapp"})

    // publish the container image
    addr, err := prodImage.Publish(ctx, "taniaitest.azurecr.io/myapp:multistage")
    if err != nil {
        panic(err)
    }
    fmt.Printf("Published to %s", addr)
}

結果は以下の通りとなり、Multi-Stage Buildができていることが確認できるはずです。

$ go run main.go 
4: upload /home/taniai/dagger-test DONE
4: > in host.directory /home/taniai/dagger-test
4: upload /home/taniai/dagger-test DONE

4: upload /home/taniai/dagger-test
4: > in host.directory /home/taniai/dagger-test
4: transferring /home/taniai/dagger-test: 
4: transferring /home/taniai/dagger-test: 2.828KiB [0.01s]
4: upload /home/taniai/dagger-test DONE

3: copy /home/taniai/dagger-test CACHED
3: > in host.directory /home/taniai/dagger-test
3: copy /home/taniai/dagger-test CACHED

8: resolve image config for docker.io/library/golang:1.19
8: > in from golang:1.19
8: resolve image config for docker.io/library/golang:1.19 DONE

17: pull docker.io/library/golang:1.19
17: > in from golang:1.19
17: resolve docker.io/library/golang:1.19@sha256:405b708c4612817e9966bff1469570eaed1ecf02a98055fb43bd6ca7b6401b37 
17: resolve docker.io/library/golang:1.19@sha256:405b708c4612817e9966bff1469570eaed1ecf02a98055fb43bd6ca7b6401b37 [0.04s]
17: pull docker.io/library/golang:1.19 DONE

16: copy / /workspace CACHED
16: copy / /workspace CACHED

14: exec go build -o myapp
14: exec go build -o myapp DONE

20: resolve image config for gcr.io/distroless/static:nonroot
20: > in from gcr.io/distroless/static:nonroot
20: resolve image config for gcr.io/distroless/static:nonroot DONE

4: upload /home/taniai/dagger-test DONE
4: > in host.directory /home/taniai/dagger-test
4: transferring /home/taniai/dagger-test: 2.828KiB [0.01s]
4: upload /home/taniai/dagger-test DONE

3: copy /home/taniai/dagger-test CACHED
3: > in host.directory /home/taniai/dagger-test
3: copy /home/taniai/dagger-test CACHED

14: exec go build -o myapp DONE
14: exec go build -o myapp DONE

17: pull docker.io/library/golang:1.19 DONE
17: > in from golang:1.19
17: resolve docker.io/library/golang:1.19@sha256:405b708c4612817e9966bff1469570eaed1ecf02a98055fb43bd6ca7b6401b37 [0.04s]
17: pull docker.io/library/golang:1.19 DONE

16: copy / /workspace CACHED
16: copy / /workspace CACHED

24: pull gcr.io/distroless/static:nonroot
24: > in from gcr.io/distroless/static:nonroot
24: resolve gcr.io/distroless/static:nonroot@sha256:9ecc53c269509f63c69a266168e4a687c7eb8c0cfd753bd8bfcaa4f58a90876f 
24: resolve gcr.io/distroless/static:nonroot@sha256:9ecc53c269509f63c69a266168e4a687c7eb8c0cfd753bd8bfcaa4f58a90876f [0.04s]
24: pull gcr.io/distroless/static:nonroot DONE

25: copy /workspace/myapp /workspace/myapp CACHED
25: copy /workspace/myapp /workspace/myapp CACHED

26: exporting to image
26: exporting layers 
26: exporting layers [0.00s]
26: exporting manifest sha256:9d7a9a45835b05c1bbb1527a22ea882c5bc453783762ba507f2a63bd3315219d 
26: exporting manifest sha256:9d7a9a45835b05c1bbb1527a22ea882c5bc453783762ba507f2a63bd3315219d [0.01s]
26: exporting config sha256:ab0284c5c2e4a2560df1e44e23af70d29903758f47dfc5b12d3ad39fa34c0e07 
26: exporting config sha256:ab0284c5c2e4a2560df1e44e23af70d29903758f47dfc5b12d3ad39fa34c0e07 [0.01s]
26: pushing layers 
26: pushing layers [0.13s]
26: pushing manifest for taniaitest.azurecr.io/myapp:multistagebuild@sha256:9d7a9a45835b05c1bbb1527a22ea882c5bc453783762ba507f2a63bd3315219d 
26: pushing manifest for taniaitest.azurecr.io/myapp:multistagebuild@sha256:9d7a9a45835b05c1bbb1527a22ea882c5bc453783762ba507f2a63bd3315219d [0.44s]
26: exporting to image DONE
Published to taniaitest.azurecr.io/myapp:multistagebuild@sha256:9d7a9a45835b05c1bbb1527a22ea882c5bc453783762ba507f2a63bd3315219d

さいごに

いかがでしたでしょうか?
DockerイメージをBuildするときはDockerfile必要でしょ!っと固定概念がありましたが、まさかコードで表現できるとは...
勉強になったと同時に、なかなか尖った検証ができ、楽しい時間を過ごせました。
普段は案件中心に業務を行っていますが、こういったOSSの検証はやはりエンジニアとして心躍るものがありますね。

今回ご紹介したコードでのBuildは、Daggerの本懐であるコードでのCI/CDでこそ活きてくるものだと思います。
GitHub Actionsなど優秀なツールが充実している分、なかなかCI/CDをコード化することはないと思いますが、CI/CD自体を内製化したいといった場合には活きてくる機能なのかなと感じました。

以下にDockerfileコマンドとそれに相当するDagger関数の対応についての記事とMulti-Stage Buildの方法のドキュメントも貼っておきますので、もしご興味あれば是非挑戦してみてください! dagger.io docs.dagger.io

ACS事業部のご紹介

私達ACS事業部はAzure・AKSなどのクラウドネイティブ技術を活用した内製化のご支援をしております。
www.ap-com.co.jp また、一緒に働いていただける仲間も募集中です!
今年もまだまだ組織規模拡大中なので、ご興味持っていただけましたらぜひお声がけください。 www.ap-com.co.jp

本記事の投稿者: 谷合純也
AKS/ACAをメインにインフラ系のご支援を担当しています。
junya0530さんの記事一覧 | Zenn