コンテナベースのCI/CDツール「Dagger」を試してみよう

なんか良さそうなのでやってみます。

公式の[Getting Started](https://docs.dagger.io/getting-started)をベースに書きます。英語が読める人はそっちの方が良いかもしれません。

あと以下はMacでの手順なので、Mac以外の人もできれば公式サイトを見てください。

僕もDagger歴1日なので、不正確な点もあるかもしれませんが、誤りを見つけた場合は優しく教えてもらえると助かります。

## インストール

```shell
brew install dagger/tap/dagger
```

Homebrewでインストールできます。

## 手順

Daggerのリポジトリにはサンプルアプリが入っているので、今回はこれを利用します。Daggerのリポジトリをcloneして、一応後で変更が入っても大丈夫なように`v0.2.4`のブランチに移動しておきます。

```shell
git clone https://github.com/dagger/dagger
cd dagger
git checkout v0.2.4
```

サンプルのディレクトリ（今回は`create-react-app`コマンドで作成されたであろうToDoアプリ）に移動して、早速ビルドしてみましょう。あ、事前にDocker Desktopは起動しておいてくださいね。

```shell
cd pkg/universe.dagger.io/examples/todoapp
dagger do build
```

しばらく待つとビルドが完了して、`_build/index.html`が作成されます。

```shell
[✔] actions.build.run.script
0.1s
[✔] actions.deps
0.1s
[✔] client.filesystem."./".read
0.2s
[✔] actions.test.script
0.1s
[✔] actions.test
0.7s
[✔] actions.build.run                                                                                       5.4s
[✔] actions.build.contents                 
0.0s
[✔] client.filesystem."./_build".write
0.2s
```

見ての通り、依存関係が良い感じに解決され、テストも良い感じに実行され、最後に良い感じにビルドされています。

ビルドが完了したら、`open _build/index.html`でもブラウザにドラッグ&ドロップでも何でも良いですが、表示できることを確認しておきましょう。

![:Users:takumi:Desktop:スクリーンショット 2022-04-20 18.33.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/477136/94617c52-6c64-59af-8d51-6f355daab7d6.png)

クールですね。

遅く感じるかもしれませんが気にしないでください。Linuxだと8倍早く動きます。

さて、1コマンドで良い感じにビルドまで動きましたが、まだ疑問はあるでしょう。次の2つです。

- 他のCIツールと比べて、これの何が嬉しいの？
- 設定はどうなってるの？

前者は単純で一番大きいのは**Dockerさえ動けば、どこでも動かせる**というものです。開発チームの方針やプラットフォームの料金が変わって違うCIツールに乗り換えたいと思ったとき、GitHub ActionsやCircleCIやGitLabの使い方を1から覚え直す必要はありません。例えば、GitHub ActionsでDaggerを用いたいならアプリケーションルートに`.github/workflows/todoapp.yml`を置いて、以下のように書けば大丈夫です。

```yaml:.github/workflows/todoapp.yml
name: todoapp

on:
  push:
    # Trigger this workflow only on commits pushed to the main branch
    branches:
      - main

# Dagger plan gets configured via client environment variables
env:
  # This needs to be unique across all of netlify.app
  APP_NAME: todoapp-dagger-europa
  NETLIFY_TEAM: dagger

jobs:
  dagger:
    runs-on: ubuntu-latest
    steps:
      - name: Clone repository
        uses: actions/checkout@v2

      # You need to run `dagger project init` locally before and commit the cue.mod directory to the repository with its contents
      - name: Deploy to Netlify
        uses: dagger/dagger-for-github@v2
        # See all options at https://github.com/dagger/dagger-for-github
        with:
          version: 0.2
          # To pin external dependencies, you can use `project update github.com/[package-source]@v[n]`
          cmds: |
            project update
            do deploy
        env:
          # Get one from https://app.netlify.com/user/applications/personal
          NETLIFY_TOKEN: ${{ secrets.NETLIFY_TOKEN }}
```

少し長いですが気にしないでください。これは[公式ページ](https://docs.dagger.io/1201/ci-environment)から引用したものです。見ての通り、アプリケーション特有のものはほとんどありません。強いて言えば、アプリ名と環境変数くらいです。他は**どんなアプリケーションでも共通の設定を使い回すことができます**。GitHubアクション以外でももちろん同様ですし、公式ページには各プラットフォーム用の設定ファイルが置いてあるので、自分で考える必要はありません。

さらにメリットを言えば、Daggerはローカルでも動きます。CIツールの設定のデバッグにかかる時間は目に見えて減少するでしょう。もっと言えば、開発環境でもDaggerのパイプラインを用いたって良いのです。

さて、問題は「Dagger自体の設定ファイルをどうするか」ですね。

今回使ったサンプルアプリの設定ファイルを見てみましょう。

```go:todoapp.cue
package todoapp

import (
	"dagger.io/dagger"
	"dagger.io/dagger/core"
	"universe.dagger.io/alpine"
	"universe.dagger.io/bash"
	"universe.dagger.io/docker"
	"universe.dagger.io/netlify"
)

dagger.#Plan & {
	_nodeModulesMount: "/src/node_modules": {
		dest:     "/src/node_modules"
		type:     "cache"
		contents: core.#CacheDir & {
			id: "todoapp-modules-cache"
		}

	}
	client: {
		filesystem: {
			"./": read: {
				contents: dagger.#FS
				exclude: [
					"README.md",
					"_build",
					"todoapp.cue",
					"node_modules",
				]
			}
			"./_build": write: contents: actions.build.contents.output
		}
		env: {
			APP_NAME:      string
			NETLIFY_TEAM:  string
			NETLIFY_TOKEN: dagger.#Secret
		}
	}
	actions: {
		deps: docker.#Build & {
			steps: [
				alpine.#Build & {
					packages: {
						bash: {}
						yarn: {}
						git: {}
					}
				},
				docker.#Copy & {
					contents: client.filesystem."./".read.contents
					dest:     "/src"
				},
				bash.#Run & {
					workdir: "/src"
					mounts: {
						"/cache/yarn": {
							dest:     "/cache/yarn"
							type:     "cache"
							contents: core.#CacheDir & {
								id: "todoapp-yarn-cache"
							}
						}
						_nodeModulesMount
					}
					script: contents: #"""
						yarn config set cache-folder /cache/yarn
						yarn install
						"""#
				},
			]
		}

		test: bash.#Run & {
			input:   deps.output
			workdir: "/src"
			mounts:  _nodeModulesMount
			script: contents: #"""
				yarn run test
				"""#
		}

		build: {
			run: bash.#Run & {
				input:   test.output
				mounts:  _nodeModulesMount
				workdir: "/src"
				script: contents: #"""
					yarn run build
					"""#
			}

			contents: core.#Subdir & {
				input: run.output.rootfs
				path:  "/src/build"
			}
		}

		deploy: netlify.#Deploy & {
			contents: build.contents.output
			site:     client.env.APP_NAME
			token:    client.env.NETLIFY_TOKEN
			team:     client.env.NETLIFY_TEAM
		}
	}
}
```

まだ普及していない形式の設定ファイル用の言語ですので、初めて見るという方も多いのではないでしょうか。

こんなことを告げるのは非常に心苦しくはあるのですが、**Daggerを使うにはこのCUEという言語を覚える必要があります**。

僕も最初は「余計なことしやがって」と思いましたが、JSONやYAML、TOMLなどには無い利点もあり一概に否定はできません。大きな特徴は

- 型と値が同じように扱える
- Goのような文法で外部のCUEファイルをimportできる（実はJSONやYAMLもimportできる）

という点でしょう。

テクニックとして便利なものもいくらかあって、例えば

```go
docker.#Copy & {
  contents: client.filesystem."./".read.contents
  dest:     "/src"
},
```

という箇所が途中でありますが、この2行目のclientというのはその前にこのファイル内で定義したものです。

このように同一ファイル内で責任を分離して繰り返し同じ記述をするのを避けたり、変更に強くしたりできます。

このようにCUE言語が便利なのは確かですし、Daggerが流行ればCUE言語を使う機会も増えると思うので、先行投資だと思って覚えておきましょう。CUE言語の[公式ドキュメント](https://cuelang.org/docs/)もかなりクオリティが高いです。



さて、それでは設定ファイルの書き方の解説を……と行きたいところですが、ぶっちゃけ見たまんまです。

重要なのは以下の部分です。

- 最初にpackage名（なんでも大丈夫）を宣言して、importしたいパッケージを選択します。細かいパッケージの詳細は省略します。公式サイトを読んでください。
- `client`は`dagger.#Plan`が持っているフィールドなので名前は変更できません。ざっくり言えばホストと通信できるもので、今回のようにファイルや環境変数を読んだりできます
- `actions`内に好きなようにアクションが定義できます。`dagger do アクション名`とすれば設定ファイルの`actions`内のアクションを良い感じに依存を解決しながら実行できます。`input`で指定しているものに依存しているので、先んじてそれが実行されるイメージですね。

できることはサンプルに載っている事以外にもあるのですが、全部は説明できないのでこのくらいにしておきます。

この設定ファイルを書く部分がDaggerを使う上での一番の障害になってくると思うので、まずは小さい用途でCUE言語を使ってみて慣れていくのが良いと思います。

## まとめ

- Daggerは便利！
- CI/CDにかけていた時間が大幅に減るかも
- CIプラットフォームを変更するコストもかからないよ
- CUE言語を頑張って習得しよう！！

ということで[Cuetorials](https://cuetorials.com/)というシャレの効いたCUE入門サイトのリンクを貼っておくので、この記事を読み終えた方は今すぐ入門しに行きましょう。

ほな。