現代のソフトウェア開発において、CI/CDパイプラインは不可欠なインフラです。しかし、GitHub Actions、GitLab CI、Jenkins、CircleCIなど、各プラットフォームは独自の設定記法を持ち、一度構築したパイプラインを別のプラットフォームに移植するのは容易ではありません。いわゆるベンダーロックインの問題です。
さらに、YAMLベースのパイプライン定義は、複雑になるほど可読性やデバッグ性が低下します。ローカルでの実行が困難なことも多く、「CIで落ちたから修正してプッシュ、また落ちた」というサイクルに陥りがちです。Daggerはこうした課題を解決するために生まれたツールです。
DaggerはDockerの創設者ですSolomon Hykes氏が立ち上げたプロジェクトで、CI/CDパイプラインをプログラマブルに定義するためのエンジンです。最大の特徴は、パイプラインをPython、Go、TypeScriptなどの汎用プログラミング言語で記述できる点です。コンテナベースで動作するため、ローカルマシンでもCI環境でも全く同じパイプラインを実行できます。
Daggerのコアコンセプトはシンプルです。すべての操作はコンテナ内で実行され、入力と出力はファイルシステムとして表現されます。これにより、再現性の高いビルドプロセスを保証します。
まずはDagger CLIのインストールから始めよう。
# macOS
brew install dagger/tap/dagger
# Linux
curl -fsSL https://dl.dagger.io/dagger/install.sh | sh
# バージョン確認
dagger version
Dagger SDKはプロジェクトの言語に合わせて選択します。ここではPython SDKを使用します。
# Python SDKのセットアップ
pip install dagger-io
# Daggerモジュールの初期化
dagger init --sdk=python --name=my-pipeline
簡単なPythonプロジェクトのテストとビルドを行うパイプラインを作成してみましょう。
import dagger
import anyio
async def main():
async with dagger.Connection() as client:
# ソースコードをコンテナに取り込む
src = client.host().directory(".", exclude=["**/__pycache__", ".venv"])
# Pythonコンテナを準備
python = (
client.container()
.from_("python:3.12-slim")
.with_directory("/app", src)
.with_workdir("/app")
.with_exec(["pip", "install", "-r", "requirements.txt"])
)
# テストの実行
test_result = await (
python
.with_exec(["pytest", "tests/", "-v"])
.stdout()
)
print("テスト結果:")
print(test_result)
anyio.run(main)
このスクリプトでは、dagger.ConnectionでDaggerエンジンに接続し、コンテナベースでテストを実行しています。client.host().directoryでローカルのソースコードを取り込み、Pythonコンテナ内でpytestを実行する流れです。
実際のプロジェクトでは、リント、テスト、ビルド、デプロイといった複数のステージを定義する必要があります。Daggerでは関数を組み合わせることで自然にこれを表現できます。
import dagger
import anyio
async def lint(client: dagger.Client, src: dagger.Directory) -> str:
"""コードの静的解析を実行"""
return await (
client.container()
.from_("python:3.12-slim")
.with_directory("/app", src)
.with_workdir("/app")
.with_exec(["pip", "install", "ruff"])
.with_exec(["ruff", "check", "."])
.stdout()
)
async def test(client: dagger.Client, src: dagger.Directory) -> str:
"""テストを実行"""
return await (
client.container()
.from_("python:3.12-slim")
.with_directory("/app", src)
.with_workdir("/app")
.with_exec(["pip", "install", "-r", "requirements.txt"])
.with_exec(["pytest", "tests/", "-v", "--tb=short"])
.stdout()
)
async def build_image(client: dagger.Client, src: dagger.Directory) -> str:
"""コンテナイメージをビルドしてレジストリにプッシュ"""
image = (
client.container()
.from_("python:3.12-slim")
.with_directory("/app", src)
.with_workdir("/app")
.with_exec(["pip", "install", "-r", "requirements.txt"])
.with_entrypoint(["python", "main.py"])
)
digest = await image.publish("registry.example.com/myapp:latest")
return digest
async def main():
async with dagger.Connection() as client:
src = client.host().directory(".", exclude=["**/__pycache__", ".venv"])
# リントとテストを並列実行
lint_result, test_result = await anyio.gather(
lint(client, src),
test(client, src),
)
print(f"Lint: {lint_result}")
print(f"Test: {test_result}")
# ビルドとプッシュ
digest = await build_image(client, src)
print(f"Published: {digest}")
anyio.run(main)
Pythonのasync/awaitを活用して、リントとテストを並列実行しています点に注目してほしいです。YAMLでは表現しにくい並列処理やエラーハンドリングも、プログラミング言語なら自然に記述できます。
Daggerの大きな利点は、どのCIサービスからでも呼び出せることです。GitHub Actionsでの使用例を示す。
# .github/workflows/ci.yaml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dagger/dagger-for-github@v6
with:
version: "latest"
verb: run
args: python ci/pipeline.py
CIサービス側の設定はDaggerの呼び出しだけで済むため、極めてシンプルになります。パイプラインのロジックはすべてDaggerのコード側に集約されるので、CIサービスを乗り換える際もワークフローファイルを書き換えるだけでよいです。
ビルド時間の短縮にはキャッシュが重要です。Daggerではキャッシュボリュームを使うことで、依存パッケージのダウンロードなどを効率化できます。
pip_cache = client.cache_volume("python-pip")
python = (
client.container()
.from_("python:3.12-slim")
.with_directory("/app", src)
.with_workdir("/app")
.with_mounted_cache("/root/.cache/pip", pip_cache)
.with_exec(["pip", "install", "-r", "requirements.txt"])
)
cache_volumeで定義したキャッシュは実行間で永続化されるため、二回目以降のビルドではpipのダウンロードが大幅にスキップされます。
Daggerの導入メリットを整理すると、以下の通りです。ローカルとCIで全く同じパイプラインを実行できるためデバッグが容易になること、プログラミング言語で記述するため条件分岐やエラーハンドリングが自然に表現できること、CIサービスに依存しないポータブルなパイプラインを構築できること、そしてコンテナベースのため再現性が高いことです。
一方で注意点もあります。Daggerエンジンの起動にはDockerが必要であり、Docker-in-Dockerが制限された環境では動作しない場合があります。また、まだ発展途上のプロジェクトですため、APIの破壊的変更が発生する可能性はゼロではありません。チームメンバー全員がPythonやGoに習熟していない場合、YAMLベースのパイプラインの方が敷居は低いこともあります。
Daggerは、CI/CDパイプラインの構築に新しいパラダイムをもたらすツールです。YAML地獄から脱却し、テスト可能でポータブルなパイプラインを構築したいチームにとって、有力な選択肢となるでしょう。まずは既存パイプラインの一部をDaggerに移植してみることから始めることを推奨します。