🧑‍🚀

Dagger Go SDK でポータブルな CI/CD パイプラインを構築する

2022/12/20に公開

CI/CD Advent Calendar 2022 の 20 日目の記事です。

https://qiita.com/advent-calendar/2022/cicd

先日「3-shake SRE Tech Talk 2022 クリスマス直前会!」というイベントで Dagger についての話をしてきました。
この記事では上記イベントで発表した内容+αについて改めてまとめます。

Dagger とは

https://dagger.io/

Dagger はコンテナで実行されるパイプラインを構築するプログラマブルな CI/CD エンジンです。
Docker の創始者である Solomon Hykes 氏らが中心となって開発しています。

Dagger を使うことで GitHub Actions や CircleCI などといった特定のサービスに依存しないポータブルな CI/CD パイプラインを構築することができます。
ローカルでも実行することが可能なので、例えば CI/CD パイプラインの動作確認のためにわざわざコミット・プッシュする等の手間が省けるといったメリットがあります。

以前は CUE 言語で記述する必要があったのですが、今年の 10 月から 11 月にかけて立て続けに Go SDKPython SDKNode.js SDK が発表されました。

https://docs.dagger.io/sdk/go
https://docs.dagger.io/sdk/python
https://docs.dagger.io/sdk/nodejs

これらの SDK を使うことで普段使い慣れているプログラミング言語を使用して CI/CD パイプラインを構築することが可能になりました。
もちろん CUE 言語も引き続き使用することが可能です。

https://docs.dagger.io/sdk/cue

ちなみに最近 GraphQL APICLI も公開されました。

https://docs.dagger.io/api
https://docs.dagger.io/cli

Dagger の仕組み

https://docs.dagger.io/#how-does-it-work

Dagger は次のような仕組みで動作します。

  1. プログラムから Dagger SDK をインポートします。
  2. Dagger SDK が Dagger Engine を起動して接続します。既に起動されている Dagger Engine がある場合はそれを使用します。
  3. Dagger SDK が実行するパイプラインを記述した API リクエストを作成し、 Dagger Engine に送信します。
  4. Dagger Engine は API リクエストを受け取るとコンテナ内でパイプラインを実行します。
  5. パイプラインの実行が完了すると、 Dagger Engine が結果をプログラムに返します。


dagger.io より引用

Dagger Go SDK をインストールする

検証環境

  • Go : v1.19
  • Dagger Go SDK : v0.4.1

前提条件

https://docs.dagger.io/sdk/go/959738/get-started#requirements

  • Go 1.15 以上がインストールされている
  • Docker がインストール・起動している

インストール

Dagger Go SDK 自体は普通の Go パッケージなので、 go get を使用してインストールすることができます。

$ go get dagger.io/dagger@latest

CI/CD パイプラインを構築する

実際に Dagger Go SDK を使用して CI/CD パイプラインを構築していきます。
この記事で使用するサンプルコードは下記リポジトリで管理しています。

https://github.com/koki-develop/dagger-go-sdk-example

1. クライアントを初期化する

main.go
package main

import (
	"context"
	"os"

	"dagger.io/dagger"
)

func main() {
	ctx := context.Background()

	// クライアントを初期化して Dagger Engine に接続する
	// dagger.WithLogOutput でログの出力先を指定できる
	client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
	if err != nil {
		panic(err)
	}
	defer client.Close()
}
  • dagger.Connect でクライアントを初期化して Dagger Engine に接続することができます。
    • 第 2 引数以降には様々なクライアントのオプションを渡すことができます。
      上記の例では dagger.WithLogOutput を使用してログを標準出力に出力するように指定しています。

ここで一度この Go コードを実行してみます ( 初回はそこそこ時間がかかります ) 。

$ go run ./main.go

特に何も出力されずに終了するのですが、 docker ps コマンドを実行してみるとコンテナが 1 つ立ち上がっていることが確認できます。
これが Dagger Engine です。
Dagger SDK で構築したパイプラインはこの Dagger Engine によって実行されるわけですね。

$ docker ps
CONTAINER ID   IMAGE                          COMMAND                  CREATED          STATUS          PORTS     NAMES
2a7caaa312b1   ghcr.io/dagger/engine:v0.3.5   "buildkitd --oci-wor…"   18 seconds ago   Up 17 seconds             dagger-engine-41f71f0036167fcc

2. コンテナを初期化してカレントディレクトリをマウントする

main.go
 package main

 import (
 	"context"
 	"os"

 	"dagger.io/dagger"
 )

 func main() {
 	ctx := context.Background()

 	// クライアントを初期化して Dagger Engine に接続する
 	// dagger.WithLogOutput でログの出力先を指定できる
 	client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
 	if err != nil {
 		panic(err)
 	}
 	defer client.Close()

+	// Docker イメージを取得する
+	container := client.Container().From("golang:1.19")

+	// カレントディレクトリをコンテナにマウントする
+	src := client.Host().Directory(".")
+	container = container.
+		WithMountedDirectory("/src", src).
+		WithWorkdir("/src")
 }
  • client.Container().From("イメージ名") で Docker イメージを取得してコンテナを初期化することができます。
  • client.Host().Directory("ディレクトリのパス") でホストマシンの任意のディレクトリを取得することができます。
    • 上記の例ではカレントディレクトリを取得しています。
  • container.WithMountedDirectory("マウント先のパス", ディレクトリ) を実行してホストマシンのディレクトリをコンテナ内へマウントすることができます。
  • container.WithWorkdir("ディレクトリのパス") で作業ディレクトリを設定することができます。

3. 実行するコマンドを設定・パイプラインを実行する

main.go
 package main

 import (
 	"context"
 	"os"

 	"dagger.io/dagger"
 )

 func main() {
 	ctx := context.Background()

 	// クライアントを初期化して Dagger Engine に接続する
 	// dagger.WithLogOutput でログの出力先を指定できる
 	client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
 	if err != nil {
 		panic(err)
 	}
	defer client.Close()

 	// Docker イメージを取得する
 	container := client.Container().From("golang:1.19")

 	// カレントディレクトリをコンテナにマウントする
 	src := client.Host().Directory(".")
 	container = container.
 		WithMountedDirectory("/src", src).
 		WithWorkdir("/src")

+	// 実行するコマンドを設定する
+	container = container.
+		WithExec([]string{"go", "test", "-v", "./..."}).
+		WithExec([]string{"go", "build"})

+	// パイプラインを実行する
+	if _, err := container.ExitCode(ctx); err != nil {
+		panic(err)
+	}
 }
  • container.WithExec([]string{"コマンド", "引数1", "引数2", ...}) のようにすることで実行するコマンドを設定することができます。
    • 上記の例では go testgo build を実行するように設定しています。
  • 最後に container.ExitCode を実行することでパイプラインの実行が開始されます。

ここまで作成したプログラムを実行してみます。

$ go run ./main.go
出力
#1 resolve image config for docker.io/library/golang:1.19
#1 DONE 4.6s

...省略

#6 go test -v ./...
#0 0.111 go: downloading dagger.io/dagger v0.4.1
#6 0.306 go: downloading github.com/Khan/genqlient v0.5.0
#6 0.307 go: downloading github.com/iancoleman/strcase v0.2.0
#6 0.307 go: downloading github.com/vektah/gqlparser/v2 v2.5.1
#6 0.456 go: downloading github.com/adrg/xdg v0.4.0
#6 0.458 go: downloading github.com/hashicorp/go-multierror v1.1.1
#6 0.458 go: downloading github.com/opencontainers/go-digest v1.0.0
#6 0.484 go: downloading github.com/pkg/errors v0.9.1
#6 0.495 go: downloading golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab
#6 0.501 go: downloading golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
#6 0.507 go: downloading github.com/hashicorp/errwrap v1.1.0
#6 3.202 ?      github.com/koki-develop/dagger-go-sdk-example   [no test files]
#6 DONE 3.3s

#7 go build
#7 DONE 0.4s

作成したパイプラインが実行されてログが出力されることが確認できます。

様々な CI ランナー上で動かしてみる

今度はローカルではなく CI ランナー上で動かしてみます。
とはいえやってることは Go の実行環境をセットアップして先ほど作成したプログラムを実行するだけです。

GitHub Actions

.github/workflows/main.yml
name: ci

on:
  push:

jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-go@v3
        with:
          go-version-file: go.mod
          cache: true
      - name: ci
        run: go run ./main.go

CircleCI

.circleci/config.yml
version: 2.1

orbs:
  go: circleci/go@1.7.1

jobs:
  ci:
    machine:
      image: ubuntu-2204:current
    steps:
      - checkout
      - go/install:
          version: "1.19"
      - go/load-cache
      - go/mod-download
      - go/save-cache
      - run:
          name: ci
          command: go run ./main.go

workflows:
  ci:
    jobs:
      - ci

その他

今回の例ではただコマンドを実行しただけでしたが、他にも Dagger は様々なことができます。
例えば次のようなものです。

  • コンテナ内のファイルやディレクトリをホストマシンに書き込む
  • GitHub リポジトリをクローンする
  • ホストマシンの環境変数をシークレットとして扱う

詳しくは公式ドキュメントpkg.go.dev をご参照ください。

https://docs.dagger.io/sdk/go
https://pkg.go.dev/dagger.io/dagger

まとめ

Go で CI/CD 書けるの楽しい。
Dagger Go SDK はまだ Technical Preview ですが、 CI/CD で最低限必要な機能は一通り揃っているように感じました。
開発もかなり活発なので今後が楽しみです。

参考

https://docs.dagger.io/sdk/go/
https://pkg.go.dev/dagger.io/dagger

Discussion