blog(unstable)

Dagger.ioを使ってみた

November 21, 2022

category : 技術

tags :

description : CI/CDツールのDagger.ioについて、使い方や使用感について書いています


前置き

Dagger.ioというCI/CDパイプラインエンジンがある。

世の中にはGitHub ActionsやAWS CodeBuild, Circle CIなど、さまざまなCI/CDサービスがある。これらはそれぞれ記法が違ったり実行環境に制限があったりと、サービス提供者ごとに良くも悪くも差別化がされている。
どのサービスも使いやすく、便利だったりするが、トレードオフとしてローカルでワークフローのテストができなかったり、他のサービスへの移行1が難しかったりする。
Dagger.ioは、そういうのをなんとかしてポータビリティが高いCI/CDパイプラインを作っちゃおうぜ、というもの。なんかDockerっぽいよね。Dockerの人が作ってるからね

最初はCue言語でしかワークフローが書けなかった(はず)でちょっと足踏みしていたが、いつのまにかGoのSDKPythonのSDKが追加されていた。
個人的にCIを作る必要があったので、せっかくだしということで今回DaggerのGo SDKを使って組んでみた。

この記事はその記録ということになる。

Dagger.ioの仕組み

だいたい下記の図のとおり。
SDK対応の好きな言語でワークフローを書き、好きな言語で実行するとコンテナ上で実行されるという仕組み。他のCI/CDサービスとおおむね同じ。

おおまかなDagger.ioの実行フロー

ワークフローと実行コンテナの間にDagger Engineというのがいる。
これはGitHub ActionsやCodeBuild、CircleCIなどの実行環境に相当するもの。
ユーザに対してはDaggerのAPIの提供、コンテナ側に対してはコンテナの起動・破棄やコマンドの伝達を行なっている(と思われる)。
Dagger Engineは非公開の部分も多く、いまのところはSDKの範囲にない部分の公開ドキュメントは存在しないよ、というのが公式の見解。

ワークフローをGoで書く

仕組みもわかったところで、さっそく実装してみる。

前回の記事にも書いたが、最近textlintを使って日本語をいい感じにlintするやつをちまちまいじっている。
このブログもそのlinterを通すことにしたのだが、さすがに手動は……ということで、今回はDaggerでこれのCIを組みたい。
いちおうこのブログ自体、特定のサービスに依存せず、最悪自前でホスティングできるように意識して作っているので、Daggerとの思想的な相性もいい。
言語は慣れていないが諸事情によりGoを選択した(Rustに対応してたらそっちを選んでいたと思う)。

最終的にできあがったものはkei-s16/techblog-linter-settingsにある。

プロジェクトを立ち上げる

プロジェクトを立ち上げる。

$ go mod init dagger

その後、公式ドキュメントにしたがって依存パッケージを追加する。
現状dockerパッケージの追加におまじないが必要らしいので、そちらも実行する。

$ go get dagger.io/dagger@latest
$ go mod edit -replace github.com/docker/docker=github.com/docker/docker@v20.10.3-0.20220414164044-61404de7df1a---incompatible
$ go mod tidy

最後にファイルを作る。

$ touch main.go

ワークフローを書く

ワークフローはだいたい3段階からなる。

  1. Daggerクライアントの定義
  2. Daggerワークフローを実行するコンテナの起動
  3. Daggerワークフローそのもの

場合によってはここに前処理として環境変数の読み込みが入ったり、ワークフローの結果を受けて別のワークフローを起動する後処理が入ったりする(のだと思う)。

Daggerクライアントの定義

なにをするにもまずDagger Engineとやりとりするクライアントが必要なので、定義する。
クライアントを定義する際に、実行ログをstdoutに出すかどうか、実行ディレクトリをどうするか、などをArgsとして渡すことができる。

client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout), dagger.WithWorkdir("../"))
if err != nil {
  return err
}
defer client.Close()

Daggerワークフローを実行するコンテナの起動

textlintの実行にはnodeの実行環境が必要なので、今回のCIはnodeコンテナ上で実行することにする。

// docker-compose up -dに相当する
node := client.Container().From("node:18.11.0-alpine3.15")

// DockerfileのADDに相当する

Daggerワークフローそのもの

コンテナを起動する処理のあとは、ワークフローを書いていく。

コマンドを実行するには、コンテナインスタンスにExecというFunctionが実装されているので、それを使う。フォーマットはDockerfileのCMDと同じ。
ExecはGitHub ActionsのStepに相当するものとイメージするとわかりやすく、Daggerのワークフロー出力もExec単位で分割して出力される。


  Exec(dagger.ContainerExecOpts{
    Args: []string{"npm", "ci"},
  })

Execは繋げることができる。


  Exec(dagger.ContainerExecOpts{
    Args: []string{"apk", "update"},
  }).
  Exec(dagger.ContainerExecOpts{
    Args: []string{"apk", "add", "git"},
  })

ちなみに、このような書き方は通らないので注意。


  Exec(dagger.ContainerExecOpts{
    Args: []string{"apk", "update", "&&", "apk", "add", "git"},
  })

ExitCodeでステップの処理が通ったかを判定できるので、たとえばチェックに失敗したら--fixを実行する、ということも可能。
……なのだが、どうもちょっと挙動がおかしいようで、現状はできなさそう2


  Exec(dagger.ContainerExecOpts{
    Args: []string{"npx", "textlint", targetDir},
  }).
  ExitCode(ctx)

// NOTE: ExitCodeが常に0を返してくるので、workaroundでerrの有無で判定する
if err != nil {
  return err
}

最終的にこういうものができあがる
書いてみて思ったのが、ワークフローを書いている、というよりDockerfileを書いている感覚3で、かなり不思議な体験だった。

ワークフローをローカルで実行する

ここまでで記述したワークフローは、ローカルでしっかり動かすことができる。

$ go run main.go

ローカルで実行して怒られた図

ワークフローをGitHub Actions上で実行する

そしてこのワークフローはCI/CDサービスのワークフローの中でDaggerを起動することで、そっくりそのまま各種環境でも実行できる(はず)。
サービス側のワークフローで最低限必要なのは、

  1. CI/CDサービスのワークフローに使用言語の実行環境を入れる
  2. CI/CDサービスのワークフロー上でDaggerなど依存パッケージを入れる
  3. ローカルで実行したのと同じコマンドを流す

の3ステップ。必要に応じてAWSやGitHubのトークンを環境変数にセットしたりする手順が挟まる。
見てわかるとおり、Dagger特有の処理が必要なわけではなく、Actions上で特定の言語の実行環境を用意したいケースと同じことをすればいい。

今回はこんなGitHub Actionsを組んだ。

on:
  pull_request:
    types: [synchronize]
    paths: 
      - content/posts/*

jobs:
  textlint-ja:
    name: lint documents
    runs-on: ubuntu-latest
    steps:
      - name: checkout source
        uses: actions/checkout@v2
      - name: setup go
        uses: actions/setup-go@v3
        with:
          go-version: 1.19.3
      - name: go get
        run: go get -v
        working-directory: ./dagger
      - name: run dagger ci
        run: go run main.go
        working-directory: ./dagger

そして実行すると、ローカルと同じ結果が得られた。

GitHubActionsで実行して怒られた図

まとめ

使ってみて、

のはおいしいなと思った。一方で、

のは微妙な気もする。
とはいえ、できてからまだまだ日が浅いツールで、ガンガン新機能・対応言語が追加されている勢いのよさを考えると、これらはすぐに解消されそう。

個人プロジェクトにはガンガン導入していきたい。


コメントや誤りの連絡は、issueにお願いします。

Footnotes

  1. CI/CDサービスを移行したいことなんてある? という向きもあると思うが、Travis CI(と、heroku)のお漏らしとその事後対応を思い出してほしい。

  2. うまい方法を知っている人がいたら教えてください。

  3. 実行環境にDockerを使うのでそれはそうという感じだが