ベンダーロックインしないCI/CDパイプライン、Daggerを使ってCodeCheckを自動化してみた。
Dagger とは
公式によると
PORTABLE DEVKIT FOR CI/CD PIPELINES.
Build powerful CI/CD pipelines quickly, then run them anywhere.
とのことです。
特徴としては
- YAMLではなく「CUE言語」というもので記述する
- import構文があるのでテンプレートの再利用性が高まる
- Docker, Buildkitが動作する環境であればどこでも動作する
- ローカルでも実行できるため、パイプラインの開発・動作確認が容易
があります。
チュートリアル
こちらの手順に従うことで、公式のサンプルをローカルで実行できます。
(シンプルなTodoアプリです)
構文
上記のTodoアプリで実行されるパイプラインについて抜粋したもので解説します。
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で実装してみました。
ソースコード
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だったとしても同じような記述ができるかと思います。
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: {
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マージ前に自動で確認したいですね。
そこで、実装したパイプラインを呼び出すためのファイルを作成します。
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ツール固有の設定ファイルを差し替えるだけで実行できます。すごいですね。
今回実装したパイプラインの問題点として、ローカルで実行する際は依存関係のキャッシュが効くので速いのですが、Github Actions上では毎回 yarn install
でパッケージインストールが走っているので正直遅いです。
どうにかキャッシュを効かせられないかと考えながらこの記事を書いていたところ、公式ドキュメントに「Persistent cache in GitHub Actions」というコンテンツが追加されていました。
次はキャッシュをフル活用して高速化できないか検証してみたいと思います。
Discussion