🗡️

ベンダーロックインしないCI/CDパイプライン、Daggerを使ってCodeCheckを自動化してみた。

2022/05/14に公開

Dagger とは

https://dagger.io
https://docs.dagger.io/1235/what

公式によると

PORTABLE DEVKIT FOR CI/CD PIPELINES.
Build powerful CI/CD pipelines quickly, then run them anywhere.

とのことです。
特徴としては

  • YAMLではなく「CUE言語」というもので記述する
    • import構文があるのでテンプレートの再利用性が高まる
  • Docker, Buildkitが動作する環境であればどこでも動作する
    • ローカルでも実行できるため、パイプラインの開発・動作確認が容易

があります。

チュートリアル

こちらの手順に従うことで、公式のサンプルをローカルで実行できます。
(シンプルなTodoアプリです)
https://docs.dagger.io/1200/local-dev

構文

上記のTodoアプリで実行されるパイプラインについて抜粋したもので解説します。

todoapp.cue(抜粋)
package todoapp

// 必要になるパッケージはファイル先頭にてこのようにimportします
// Go言語みたいですね
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 内に記述していきます
dagger.#Plan & {
	// client: では動作させる環境に関わる設定を記述するします
	// - Daggerが実行されるホストとの入出力設定
	// - 環境変数の設定
	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: にはdagger-cliから呼び出せるコマンド群が記述されます
	// 他のactionに対する参照が記述されていると、順に実行される
	// ($ dagger do build すると deps->test->build と実行される)
	actions: {
		deps: docker.#Build & {
			steps: [
				// ~~~中略~~~
			]
		}

		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"
			}
		}
	}
}

CodeCheckを実装してみた

今回はtypescriptのCodeCheckをDaggerで実装してみました。

ソースコード
codecheck.cue
package codecheck

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

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

	}
	_frontendModulesMount: "/src/node_modules": {
		dest:     "/src/node_modules"
		type:     "cache"
		contents: core.#CacheDir & {
			id: "frontend-modules-cache"
		}

	}
	client: {
		filesystem: {
			"./backend": read: {
				contents: dagger.#FS
				exclude: [
					"README.md",
					"dist",
					"node_modules",
				]
			}
			"./frontend": read: {
				contents: dagger.#FS
				exclude: [
					"README.md",
					"nginx.conf",
					"node_modules",
				]
			}
		}
	}
	actions: {
		backendDeps: docker.#Build & {
			steps: [
				docker.#Pull & {source: "node:lts-buster"},
				docker.#Copy & {
					contents: client.filesystem."./backend".read.contents
					dest:     "/app"
				},
				bash.#Run & {
					workdir: "/app"
					mounts:  _backendModulesMount
					script: contents: #"""
						yarn
						yarn run prisma:generate
						"""#
				},
			]
		}
		frontendDeps: docker.#Build & {
			steps: [
				docker.#Pull & {source: "node:lts-buster"},
				docker.#Copy & {
					contents: client.filesystem."./frontend".read.contents
					dest:     "/app"
				},
				bash.#Run & {
					workdir: "/app"
					mounts:  _frontendModulesMount
					script: contents: "yarn"
				},
			]
		}
		codecheck: {
			backend: {
				typeCheck: bash.#Run & {
					input:   backendDeps.output
					workdir: "/app"
					mounts:  _backendModulesMount
					script: contents: "yarn type-check"
				}
				lint: bash.#Run & {
					input:   backendDeps.output
					workdir: "/app"
					mounts:  _backendModulesMount
					script: contents: "yarn lint"
				}
				format: bash.#Run & {
					input:   backendDeps.output
					workdir: "/app"
					mounts:  _backendModulesMount
					script: contents: "yarn format"
				}
			}
			frontend: {
				typeCheck: bash.#Run & {
					input:   frontendDeps.output
					workdir: "/app"
					mounts:  _frontendModulesMount
					script: contents: "yarn type-check"
				}
				lint: bash.#Run & {
					input:   frontendDeps.output
					workdir: "/app"
					mounts:  _frontendModulesMount
					script: contents: "yarn lint"
				}
				format: bash.#Run & {
					input:   frontendDeps.output
					workdir: "/app"
					mounts:  _frontendModulesMount
					script: contents: "yarn format"
				}
			}
		}
	}
}

ディレクトリ構成

backend(Fastify) + frontend(React)という構成のリポジトリに対して実装しました。

repository
├── .github
│   └── workflows
│       └── codecheck.yml
├── backend
│   ├── Dockerfile
│   ├── package.json
│   ├── src
│   ├── yarn-error.log
│   └── yarn.lock
├── cue.mod
│   ├── module.cue
│   ├── pkg
│   └── usr
├── dagger
│   └── codecheck.cue
└── frontend
    ├── Dockerfile
    ├── package.json
    ├── src
    └── yarn.lock

依存関係を解決

dagger.#Plan: actions: frontend/backendDeps にて、それぞれの依存関係を解決しています。
今回の構成ではどちらもNode.js環境をセットアップしていますが、例えばbackendがpythonだったとしても同じような記述ができるかと思います。

codecheck.cue
backendDeps: docker.#Build & {
	steps: [
		docker.#Pull & {source: "node:lts-buster"},
		docker.#Copy & {
			contents: client.filesystem."./backend".read.contents
			dest:     "/app"
		},
		bash.#Run & {
			workdir: "/app"
			mounts:  _backendModulesMount
			script: contents: #"""
				yarn
				yarn run prisma:generate
				"""#
		},
	]
}
frontendDeps: docker.#Build & {
	steps: [
		docker.#Pull & {source: "node:lts-buster"},
		docker.#Copy & {
			contents: client.filesystem."./frontend".read.contents
			dest:     "/app"
		},
		bash.#Run & {
			workdir: "/app"
			mounts:  _frontendModulesMount
			script: contents: "yarn"
		},
	]
}

テスト実行

dagger.#Plan: actions: codecheck:にてCodeCheckを実行しています。
ここで、backendとfrontendの間に参照が存在しない(=依存関係がない)ため、この2つは並列実行されます。
同様に typeCheck / lint / formatの間にも参照が存在しないため、すべて並列実行されます。

codecheck.cue
codecheck: {
	backend: {
		typeCheck: bash.#Run & {
			input:   backendDeps.output
			workdir: "/app"
			mounts:  _backendModulesMount
			script: contents: "yarn type-check"
		}
		lint: bash.#Run & {
			input:   backendDeps.output
			workdir: "/app"
			mounts:  _backendModulesMount
			script: contents: "yarn lint"
		}
		format: bash.#Run & {
			input:   backendDeps.output
			workdir: "/app"
			mounts:  _backendModulesMount
			script: contents: "yarn format"
		}
	}
	frontend: {
		typeCheck: bash.#Run & {
			input:   frontendDeps.output
			workdir: "/app"
			mounts:  _frontendModulesMount
			script: contents: "yarn type-check"
		}
		lint: bash.#Run & {
			input:   frontendDeps.output
			workdir: "/app"
			mounts:  _frontendModulesMount
			script: contents: "yarn lint"
		}
		format: bash.#Run & {
			input:   frontendDeps.output
			workdir: "/app"
			mounts:  _frontendModulesMount
			script: contents: "yarn format"
		}
	}
}

実際に使ってみた

これでローカルでは実行できるようになりました。

$ dagger do --log-format=plain -p './dagger' codecheck

ただ、実用性を考えるとGithub Actions上で動作させてPRマージ前に自動で確認したいですね。
そこで、実装したパイプラインを呼び出すためのファイルを作成します。

.github/workflows/codecheck.yml
name: codecheck

on:
  push:
    branches:
      - master
  pull_request:
    types:
      - opened
      - synchronize
      - reopened

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

      - name: Code Check
        uses: dagger/dagger-for-github@v3
        with:
          version: 0.2
          cmds: |
            project update
            do --log-format=plain -p ./dagger codecheck

CodeCheck程度であればおそらく直接Github Actionsに実装してもさほど変わらないかと思います。
しかしながら、今後デプロイの自動化など、より複雑なことをしたくなったときでも同じようなYAMLを作成するだけでパイプラインの設定をDaggerで隠蔽することができます。

PRを作成すると、自動でCodeCheckが実行されました。うれしい。

おわりに

今回はGithub Actions上で実行するCodeCheckをDaggerを用いて実装してみました。
他のCIツール上で動作させる際も .github/workflows/codecheck.yml にあたる、CIツール固有の設定ファイルを差し替えるだけで実行できます。すごいですね。
https://docs.dagger.io/1201/ci-environment

今回実装したパイプラインの問題点として、ローカルで実行する際は依存関係のキャッシュが効くので速いのですが、Github Actions上では毎回 yarn install でパッケージインストールが走っているので正直遅いです。
どうにかキャッシュを効かせられないかと考えながらこの記事を書いていたところ、公式ドキュメントに「Persistent cache in GitHub Actions」というコンテンツが追加されていました。
https://docs.dagger.io/1237/persistent-cache-with-dagger#persistent-cache-in-github-actions

次はキャッシュをフル活用して高速化できないか検証してみたいと思います。

GitHubで編集を提案

Discussion