Dagger Go SDK でポータブルな CI/CD パイプラインを構築する
CI/CD Advent Calendar 2022 の 20 日目の記事です。
先日「3-shake SRE Tech Talk 2022 クリスマス直前会!」というイベントで Dagger についての話をしてきました。
この記事では上記イベントで発表した内容+αについて改めてまとめます。
Dagger とは
Dagger はコンテナで実行されるパイプラインを構築するプログラマブルな CI/CD エンジンです。
Docker の創始者である Solomon Hykes 氏らが中心となって開発しています。
Dagger を使うことで GitHub Actions や CircleCI などといった特定のサービスに依存しないポータブルな CI/CD パイプラインを構築することができます。
ローカルでも実行することが可能なので、例えば CI/CD パイプラインの動作確認のためにわざわざコミット・プッシュする等の手間が省けるといったメリットがあります。
以前は CUE 言語で記述する必要があったのですが、今年の 10 月から 11 月にかけて立て続けに Go SDK・Python SDK・Node.js SDK が発表されました。
これらの SDK を使うことで普段使い慣れているプログラミング言語を使用して CI/CD パイプラインを構築することが可能になりました。
もちろん CUE 言語も引き続き使用することが可能です。
ちなみに最近 GraphQL API や CLI も公開されました。
Dagger の仕組み
Dagger は次のような仕組みで動作します。
- プログラムから Dagger SDK をインポートします。
- Dagger SDK が Dagger Engine を起動して接続します。既に起動されている Dagger Engine がある場合はそれを使用します。
- Dagger SDK が実行するパイプラインを記述した API リクエストを作成し、 Dagger Engine に送信します。
- Dagger Engine は API リクエストを受け取るとコンテナ内でパイプラインを実行します。
- パイプラインの実行が完了すると、 Dagger Engine が結果をプログラムに返します。
dagger.io より引用
Dagger Go SDK をインストールする
検証環境
- Go : v1.19
- Dagger Go SDK : v0.4.1
前提条件
- Go 1.15 以上がインストールされている
- Docker がインストール・起動している
インストール
Dagger Go SDK 自体は普通の Go パッケージなので、 go get
を使用してインストールすることができます。
$ go get dagger.io/dagger@latest
CI/CD パイプラインを構築する
実際に Dagger Go SDK を使用して CI/CD パイプラインを構築していきます。
この記事で使用するサンプルコードは下記リポジトリで管理しています。
1. クライアントを初期化する
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
を使用してログを標準出力に出力するように指定しています。
- 第 2 引数以降には様々なクライアントのオプションを渡すことができます。
ここで一度この 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. コンテナを初期化してカレントディレクトリをマウントする
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. 実行するコマンドを設定・パイプラインを実行する
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 test
とgo 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
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
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 をご参照ください。
まとめ
Go で CI/CD 書けるの楽しい。
Dagger Go SDK はまだ Technical Preview ですが、 CI/CD で最低限必要な機能は一通り揃っているように感じました。
開発もかなり活発なので今後が楽しみです。
参考
Discussion