YAML地獄の不満
以前、中規模のマイクロサービスをGitHub ActionsからGitLab CIに移行するのに、週末を丸々潰したことがあります。簡単なはずでした。しかし、独自の構文を翻訳し、環境変数の不整合を修正し、ランナーが失敗するのを待つだけで72時間を費やしました。それは、すべてのDevOpsエンジニアが忌み嫌う「コミット、プッシュ、待機、失敗」のループでした。
従来のCI/CDツールは、パイプラインを静的な設定ファイルとして扱います。クラウドのランナーしか理解できない長い文字列をYAMLファイルに記述します。そのロジックをローカルでテストすることは、actのようなツールを使わない限りほぼ不可能であり、それらも本番環境を正確に再現できないことがよくあります。Daggerは、ロジックを「設定」から「コード」に移すことで、この問題を解決します。
Daggerは、Python、Go、TypeScriptなど、すでに使い慣れた言語でパイプラインを記述できるプログラム可能なエンジンです。CIインフラを実際のソフトウェアとして扱います。つまり、MacBook Pro上で実行したのと全く同じパイプラインを、高スペックなクラウド環境でも実行できるのです。
クイックスタート:5分で最初のDaggerパイプラインを作成
実際に動作するパイプラインを構築してみましょう。読みやすく業界標準であるPythonを使用します。開始する前に、Daggerがコンテナを実行するために使用する Dockerがインストールされていることを確認してください。
1. Dagger CLIのインストール
# Homebrewを使用しているmacOSユーザーの場合
brew install dagger/tap/dagger
# またはインストールスクリプトを使用する場合
curl -L https://dl.dagger.io/dagger/install.sh | sh
sudo mv ./bin/dagger /usr/local/bin/dagger
# 動作確認
dagger version
2. 環境のセットアップ
依存関係をクリーンに保つために、新しいディレクトリを作成し、仮想環境を初期化します。
mkdir dagger-demo && cd dagger-demo
python3 -m venv .venv
source .venv/bin/activate
pip install dagger-io
3. パイプラインロジックの記述
main.pyという名前のファイルを作成します。このスクリプトは、Node.jsをプルし、依存関係をインストールして、テストを実行するコンテナを定義します。
import sys
import anyio
import dagger
async def main():
async with dagger.connection(dagger.Config(log_output=sys.stderr)) as client:
# ローカルのソースコードを読み込む
src = client.host().directory(".")
# 実行環境を構築する
container = (
client.container()
.from_("node:18-alpine")
.with_directory("/src", src)
.with_workdir("/src")
.with_exec(["npm", "install"])
.with_exec(["npm", "test"])
)
# パイプラインを実行し、出力を取得する
result = await container.stdout()
print(result)
if __name__ == "__main__":
anyio.run(main)
4. ローカルでの実行
dagger run python main.py
Daggerがイメージをプルし、ステップを実行します。ここでテストがパスすれば、GitHub Actionsでもパスします。もう推測に頼る必要はありません。
Daggerの仕組み:その内部構造
Daggerは単にスクリプトを順番に実行するだけではありません。操作の有向非巡回グラフ(DAG)を構築します。with_execでステップを定義すると、Daggerはそれを即座に実行するのではなく、グラフに追加します。実行は、stdout()やpublish()のように結果を要求したときに初めて行われます。
Daggerエンジンの威力
通常はDockerコンテナである永続的なエンジンが、重い処理を担当します。このエンジンが実行とインテリジェントなキャッシュを管理します。常にクリーンな状態から開始することが多い標準的なランナーとは異なり、Daggerは非常に効率的です。最近のReactプロジェクトでは、Daggerのレイヤーキャッシュにより、2回目以降の実行でnpm installの時間が90秒からわずか3秒に短縮されました。
モジュール化されたコンテナ
パイプラインを関数の集合として考えてください。関数間でコンテナを受け渡すことで、モジュール化されたワークフローを作成できます。ある関数でセキュリティのリンターを実行し、別の関数で本番用のビルドを行うといったことが可能です。
def run_lint(container: dagger.Container):
return container.with_exec(["npm", "run", "lint"])
def build_binary(container: dagger.Container):
return container.with_exec(["npm", "run", "build"])
1つのパイプラインで、あらゆるCIプロバイダーに対応
Daggerを使えば、GitHubやGitLabの設定は単純な「シム(仲介役)」になります。YAMLファイルはDaggerスクリプトを呼び出すだけで済むため、記述量を80%以上削減できるでしょう。
例:GitHub Actions
.github/workflows/ci.ymlは、単なるトリガーになります。
name: CI
on: [push]
jobs:
dagger:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dagger/dagger-for-github@v5
with:
verb: run
args: python main.py
例:GitLab CI
.gitlab-ci.ymlも、ロジックとしてはほぼ同一です。
run-dagger:
image: docker:latest
services: [docker:dind]
script:
- curl -L https://dl.dagger.io/dagger/install.sh | sh
- ./bin/dagger run python main.py
もし来月、CircleCIやJenkinsに切り替えることになっても、テストを書き直す必要はありません。同じPythonスクリプトを呼び出すだけで済みます。
より良いパイプラインのための戦略
1. 機密情報の安全な取り扱い
認証情報のハードコーディングは絶対に避けてください。Daggerには、機密データがビルドログやイメージレイヤーに漏洩するのを防ぐシークレット管理機能が組み込まれています。
gh_token = client.set_secret("github-token", os.environ["GITHUB_TOKEN"])
container = container.with_secret_variable("GH_TOKEN", gh_token)
2. 永続的なキャッシュボリュームの使用
CIにおいてスピードは正義です。Daggerのキャッシュボリュームを使用して、node_modules、.m2、~/.cache/pipなどのディレクトリを異なる実行間で永続化させます。これにより、ビルド時間を数分短縮できる場合があります。
cache = client.cache_volume("node-deps")
container = container.with_mounted_cache("/src/node_modules", cache)
3. CIコードを本番コードのように扱う
Since you are using Python, use its full power. Organize your pipeline with classes, split logic into modules, and even write unit tests for your CI logic. I have seen YAML files grow into 2,000-line unreadable monsters. Dagger allows you to keep things clean using standard design patterns.
Daggerへの移行には、特に長年YAMLを書いてきた人にとっては意識の転換が必要です。しかし、CIの失敗をローカルで数秒でデバッグできるメリットは、生産性において非常に大きな勝利となります。もう、CI構文のテスト場として Gitの履歴を汚す必要はないのです。

