「Dagger」を試す(CI編)
Daggerは、ワークフローをポータブルに実現するためのランタイムで、これまでのコンテキストではCI/CDで活用されることが多いように思う。以下の記事が最もわかりやすいと思う。
ポイントとしては、DaggerはDockerを使ったコンテナ環境を提供していることで、これにより、
- 再現性
- モジュール性
- 可観測性
- クロスプラットフォームサポート
が実現でき、ローカル環境でもCI/CDプラットフォームでも、同じようにビルドやテストなどが行うことができるというのが強み。
でそれって(マルチ)AIエージェントでも活かせるんでは?(というのが自分の現時点での理解。もしかしたら間違ってるかも知れない。)ってのが以下のポスト
上記の動画
ちょっと気になったので試そうかなと思って、少し調べ始めてたのだけど
おそらく評判になったのだろう、すでに公式サイトのトップにもAIエージェントみたいな謳い文句が載っていて
あと、GitHubレポジトリもなんか色々更新されて以前とは変わっていたので、あらためて確認し直しだなと。
個人的に、(マルチ)エージェントのデプロイをどうするか?みたいなところにはすごく興味があって、Dagger自体を触ったこともないのだけど、どういうものかを理解しつつ自分の目的に合うものかを確かめるために、試してみようと思う。
Quickstartは、CIとAIエージェントそれぞれで用意してあるのだけど、
AIエージェントの方をざっと眺めてみたけど、いまいちピンとこなかったので、一旦はCIの方を進めてから、AIエージェントの方を進める予定。
Introduction
Quickstartに進む前にDaggerとはなんぞや?を軽く押さえておく。
Daggerとは?
Dagger は、構成可能なワークフローのためのオープンソースランタイムです。多数の可動部分と、再現性、モジュール性、可観測性およびクロスプラットフォームサポートの強い要求を持つシステムに最適です。これにより、AI エージェントや CI/CD ワークフローにおいて優れた選択肢となります。
主な機能
- 再現可能な実行エンジン
コンテナ化された関数と宣言型 DAG スケジューラによって実現。- ユニバーサルな型システム
プラットフォームや言語を超えた、強く型付けされた構成と検出のためのシステム。- 強力なデータレイヤー
標準装備のキャッシング、イミュータブルな状態管理、データのトレーサビリティ。- 5 言語向けのネイティブ SDK
Go、Typescript、Python、PHP、Javaに対応、そして今後さらに追加予定。- オープンなエコシステム
数千のモジュールがすぐに利用可能で、全てが言語やプラットフォーム間で相互運用可能。- インタラクティブなコマンドライン環境
迅速なプロトタイピングとデバッグが可能。- 標準装備の可観測性
ディープトレーシング、メトリクス(トークン数を含む)およびログが CLI またはウェブ UI からアクセス可能。- あなたに合わせた適応性
主要な計算・ストレージプラットフォーム、CI システム、言語およびエージェントフレームワークとシームレスに統合。- LLM拡張
任意の LLM エンドポイント(OpenAI、Google、Anthropic、LLama、DeepSeek など)に接続し、Dagger オブジェクトへのアクセスを提供。Dagger は自動的にエージェントループを処理。複雑なフレームワークは不要。アーキテクチャ
Dagger は、統合された形で連携する複数のコンポーネントから構成され、構成可能なワークフローのための最適なプラットフォームを提供します。
Dagger Engine
Dagger Engine は、Dagger プラットフォームの中核となるランタイムです。実行エンジン、ユニバーサル型システム、データレイヤーおよびモジュールシステムを組み合わせています。OCI 互換のシステム上で動作可能で、Dagger API によって制御されます。
Dagger CLI
Dagger CLI は Dagger への主要なエントリーポイントです。ターミナルからの対話的な利用や、シェルスクリプトや CI ランナーからの非対話的な利用が可能な、フル機能かつ使いやすいツールです。Dagger CLI には、ターミナル UI(TUI)と呼ばれるリアルタイム可視化機能も含まれています。Dagger CLI は Dagger Engine のクライアントとして動作し、必要に応じてエンジンを自動的にプロビジョニングできます。
クライアントライブラリ
クライアントライブラリを使用することで、コードから直接 Dagger Engine を制御することが可能です。
Dagger のユニバーサルな型システムを完全に活用するために、クライアントライブラリは自動生成されます: Dagger CLI と SDK が必要なツールを提供します。これにより、クロスランゲージの構成が可能となり、任意の Dagger モジュールを依存関係としてインストールすると、生成されたクライアントは言語に関わらずネイティブバインディングを含みます。
SDK
Dagger SDK は、親しみやすい言語とツールチェーンを用いて Dagger モジュールを開発するためのリソースを提供します。各 SDK は以下を提供します:
- クライアントジェネレーター: ネイティブコードで Dagger API を 消費 するためのもの。
- サーバージェネレーター: ネイティブコードで Dagger API を 拡張 するためのもの。
- 例とリファレンスドキュメント
モジュール
Dagger Modules は、プラットフォームに依存しないソフトウェアコンポーネントで、Dagger の型を 消費 するか、または 拡張 します。
Dagger API
Dagger API は、Dagger Engine をプログラミングするための統一インターフェースです。
GraphQL に基づくユニバーサル型システムを定義しており、どのクライアントからもイントロスペクト可能で、特別に作成されたサーバーによって動的に拡張されます。Dagger Engine の中核機能である実行エンジン、データレイヤーは、組み込み型として利用可能です。
Dagger Cloud
Dagger Cloud は、Dagger に生産環境レベルのコントロールプレーンを補完します。Dagger Cloud の特徴には、パイプラインの可視化と運用上の洞察が含まれます。Dagger Cloud は、パイプラインの各ステップを可視化し、詳細なログにドリルダウンし、操作の実行時間やキャッシュされたかどうかを理解するためのウェブインターフェースを提供します。
Dagger Cloud は、開発環境または CI で実行される組織内のすべての Dagger Engine からテレメトリを収集し、一元的に表示します。これにより、プッシュ前およびプッシュ後のすべてのパイプラインを一目で把握することができます。
Daggerverse
Daggerverse は、Dagger によって運営される無料のサービスで、すべての公開されている Dagger モジュールをインデックスし、簡単に検索・利用できるようにします。
Dagger for CI: Quickstart
今回はローカルのMacでやる。あと、Dagger Cloudでは個人向け無料プランがあるようなので、使うか使わないかは別にしてアカウントを作成しておくこととした。
アカウント作成して組織名を設定すると、以下のようにDAGGER_CLOUD_TOKENが発行される。画面にもあるがおそらくクラウド上でトレーシングを確認するためには必要になるのだろうと思う。
Dagger CLIのインストール
Macの場合はHomebrewでインストールできる。
brew install dagger/tap/dagger
dagger version
dagger v0.16.2 (docker-image://registry.dagger.io/engine:v0.16.2) darwin/arm64
Usageはこんな感じ
dagger help
A tool to run CI/CD pipelines in containers, anywhere
USAGE
dagger [flags]
DAGGER CLOUD COMMANDS
login Log in to Dagger Cloud
logout Log out from Dagger Cloud
DAGGER MODULE COMMANDS
call Call one or more functions, interconnected into a pipeline
config Get or set module configuration
core Call a core function
develop Prepare a local module for development
functions List available functions
init Initialize a new module
install Install a dependency
uninstall Uninstall a dependency
update Update a dependency
EXECUTION COMMANDS
query Send API queries to a dagger engine
run Run a command in a Dagger session
ADDITIONAL COMMANDS
completion Generate the autocompletion script for the specified shell
help Help about any command
version Print dagger version
OPTIONS
-c, --code string Command to be executed
-d, --debug Show debug logs and full verbosity
-i, --interactive Spawn a terminal on container exec failure
--interactive-command string Change the default command for interactive mode (default "/bin/sh")
-m, --mod string Path to the module directory. Either local path or a remote git repo
-E, --no-exit Leave the TUI running after completion
--no-mod Don't load module during shell startup (mutually exclusive with --mod)
--progress string Progress output format (auto, plain, tty) (default "auto")
-q, --quiet count Reduce verbosity (show progress, but clean up at the end)
-s, --silent Do not show progress at all
-v, --verbose count Increase verbosity (use -vv or -vvv for more)
-w, --web Open trace URL in a web browser
Use "dagger [command] --help" for more information about a command.
サンプルアプリケーションをDagger化する
Daggerの実際の使い方を学ぶためのサンプルアプリケーションが用意されている。
Vueで書かれた、"Hello, World"を表示するだけのシンプルなアプリケーションとなっている。このアプリケーションのデリバリーパイプラインをDagger Functionsで作っていく。なお、Daggerではこのことを"Daggerizing"といっている。日本語だと「Dagger化する」って感じになるかな。
ではサンプルアプリをクローンする。
git clone https://github.com/dagger/hello-dagger && cd hello-dagger
中身はこんな感じ。
tree
.
├── README.md
├── cypress
│ ├── e2e
│ │ ├── example.cy.ts
│ │ └── tsconfig.json
│ ├── fixtures
│ │ └── example.json
│ └── support
│ ├── commands.ts
│ └── e2e.ts
├── cypress.config.ts
├── env.d.ts
├── index.html
├── package-lock.json
├── package.json
├── public
│ └── favicon.ico
├── src
│ ├── App.vue
│ ├── assets
│ │ ├── base.css
│ │ ├── logo.svg
│ │ └── main.css
│ ├── components
│ │ ├── HelloWorld.vue
│ │ ├── TheWelcome.vue
│ │ ├── WelcomeItem.vue
│ │ ├── __tests__
│ │ │ └── HelloWorld.spec.ts
│ │ └── icons
│ │ ├── IconCommunity.vue
│ │ ├── IconDocumentation.vue
│ │ ├── IconEcosystem.vue
│ │ ├── IconSupport.vue
│ │ └── IconTooling.vue
│ ├── main.ts
│ ├── router
│ │ └── index.ts
│ ├── stores
│ │ └── counter.ts
│ └── views
│ ├── AboutView.vue
│ └── HomeView.vue
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.vitest.json
├── vite.config.ts
└── vitest.config.ts
14 directories, 36 files
オプションではあるが、Dagger Cloudを使うとパイプラインの可視化ができるので、今回はこれも試してみる。
まずDagger CLIでDagger Cloudにログインする。
dagger login
以下のように認証用のURLが表示されブラウザが自動で立ち上がるので認証する。
Browser opened to: https://auth.dagger.cloud/activate?user_code=XXXX-XXXX
Confirmation code: XXXX-XXXX
認証成功。
ターミナルの方もOK。
Success.
次に、Daggerモジュールをブートストラップする。アプリケーションのルートディレクトリでdagger init
を実行。今回はPython SDKを使う。--source
でDaggerモジュールのソースコードを保存するディレクトリを指定する。
dagger init --sdk=python --source=./dagger
こんな感じで処理が行われる。
最終的にはこんな感じ。
✔ connect 48.0s
✔ moduleSource(allowNotExists: true, disableFindUp: true, refString: ".", requireKind: LOCAL_SOURCE): ModuleSource! 0.0s
✔ .configExists: Boolean! 0.0s
✔ ModuleSource.localContextDirectoryPath: String! 0.0s
✔ ModuleSource.sourceRootSubpath: String! 0.0s
✔ ModuleSource.withName(name: "hello-dagger"): ModuleSource! 0.0s
✔ .withSDK(source: "python"): ModuleSource! 9.7s
✔ .withSourceSubpath(path: "dagger"): ModuleSource! 0.0s
✔ .withEngineVersion(version: "latest"): ModuleSource! 0.0s
✔ .generatedContextDirectory: Directory! 16.3s
✔ .export(path: "/Users/kun432/work/hello-dagger"): String! 0.1s
22:15:46 WRN no LICENSE file found; generating one for you, feel free to change or remove license=Apache-2.0
Initialized module hello-dagger in /Users/kun432/work/hello-dagger
以下のようなモジュールに関するディレクトリ・ファイルが作成される。
-
dagger.json
モジュールのメタデータファイル -
dagger/src/hello_dagger/__init__.py
・dagger/src/hello_dagger/main.py
初期状態で作成されたソースコードのテンプレート -
dagger/pyproject.toml
・dagger/uv.lock
依存管理用ファイル -
dagger/sdk
ローカル開発用
$ tree
追加されたものだけピックアップ。
.
├── LICENSE
(snip)
├── dagger
│ ├── pyproject.toml
│ ├── sdk
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── codegen
│ │ │ ├── pyproject.toml
│ │ │ └── src
│ │ │ └── codegen
│ │ │ ├── __init__.py
│ │ │ ├── __main__.py
│ │ │ ├── cli.py
│ │ │ └── generator.py
│ │ ├── pyproject.toml
│ │ ├── src
│ │ │ └── dagger
│ │ │ ├── __init__.py
│ │ │ ├── _config.py
│ │ │ ├── _connection.py
│ │ │ ├── _engine
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _version.py
│ │ │ │ ├── conn.py
│ │ │ │ ├── download.py
│ │ │ │ ├── progress.py
│ │ │ │ └── session.py
│ │ │ ├── _exceptions.py
│ │ │ ├── _managers.py
│ │ │ ├── client
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _core.py
│ │ │ │ ├── _guards.py
│ │ │ │ ├── _session.py
│ │ │ │ ├── base.py
│ │ │ │ └── gen.py
│ │ │ ├── log.py
│ │ │ ├── mod
│ │ │ │ ├── __init__.py
│ │ │ │ ├── _arguments.py
│ │ │ │ ├── _converter.py
│ │ │ │ ├── _exceptions.py
│ │ │ │ ├── _module.py
│ │ │ │ ├── _resolver.py
│ │ │ │ ├── _types.py
│ │ │ │ ├── _utils.py
│ │ │ │ └── cli.py
│ │ │ ├── py.typed
│ │ │ └── telemetry.py
│ │ └── uv.lock
│ ├── src
│ │ └── hello_dagger
│ │ ├── __init__.py
│ │ └── main.py
│ └── uv.lock
├── dagger.json
(snip)
Dagger Functionを使ってパイプラインを定義する。これはdagger/src/hello_dagger/main.py
に定義していく。初期状態だと以下となっている(docstringだけ日本語に訳した。)
import dagger
from dagger import dag, function, object_type
@object_type
class HelloDagger:
@function
def container_echo(self, string_arg: str) -> dagger.Container:
"""与えられた文字列の引数をそのまま返すコンテナを返す"""
return dag.container().from_("alpine:latest").with_exec(["echo", string_arg])
@function
async def grep_dir(self, directory_arg: dagger.Directory, pattern: str) -> str:
"""指定されたディレクトリのファイルの中から、パターンに一致する行を返す"""
return await (
dag.container()
.from_("alpine:latest")
.with_mounted_directory("/mnt", directory_arg)
.with_workdir("/mnt")
.with_exec(["grep", "-R", pattern, "."])
.stdout()
)
これを以下に置き換える。
import random
import dagger
from dagger import dag, function, object_type
@object_type
class HelloDagger:
@function
async def publish(self, source: dagger.Directory) -> str:
"""アプリケーションをテストし、コンテナをビルドし、公開する"""
await self.test(source)
return await self.build(source).publish(
f"ttl.sh/hello-dagger-{random.randrange(10**8)}"
)
@function
def build(self, source: dagger.Directory) -> dagger.Container:
"""アプリケーションコンテナをビルドする"""
build = (
self.build_env(source)
.with_exec(["npm", "run", "build"])
.directory("./dist")
)
return (
dag.container()
.from_("nginx:1.25-alpine")
.with_directory("/usr/share/nginx/html", build)
.with_exposed_port(80)
)
@function
async def test(self, source: dagger.Directory) -> str:
"""ユニットテストの実行結果を返す"""
return await (
self.build_env(source)
.with_exec(["npm", "run", "test:unit", "run"])
.stdout()
)
@function
def build_env(self, source: dagger.Directory) -> dagger.Container:
"""すぐに使える開発環境を構築する"""
node_cache = dag.cache_volume("node")
return (
dag.container()
.from_("node:21-slim")
.with_directory("/src", source)
.with_mounted_cache("/root/.npm", node_cache)
.with_workdir("/src")
.with_exec(["npm", "install"])
)
以下の4つのDagger Functionsが定義されたDaggerモジュールになっている。
-
publish()
アプリケーションをテストし、コンテナをビルドし、イメージをttl.shレジストリに公開する(他の関数を呼び出すエントリーポイント) -
test()
アプリケーションのユニットテストを実行し、結果を返す。 -
build()
マルチステージビルドを実行し、本番環境で使用可能なアプリケーションとそれをホスト・配信するためのNGINX Webサーバーを組み込んだ最終的なコンテナイメージを返す。 -
build_env()
アプリケーションのビルド環境を持つコンテナを作成する。
なるほど、パイプラインとしてちょっと雰囲気が見えてきた。ちなみに、ttl.shレジストリってのを初めて知ったのだが、匿名でエフェメラルなDockerイメージレジストリらしい。認証なしで誰でも利用でき、イメージに有効期限を付与(最大24時間)できるので、CI/CDパイプラインなどで一時的なビルド成果物を共有するために利用できるらしい。
参考)
では、このパイプラインを実行する。
dagger call publish --source=.
こんな感じでパイプラインが進んでいく。
最終的にこんな感じで処理終了。レジストリにイメージがpushされているのがわかる。
✔ connect 0.3s
✔ load module 7.6s
✔ parsing command line arguments 0.0s
✔ Host.directory(path: "/Users/kun432/work/hello-dagger"): Directory! 0.2s
✔ helloDagger: HelloDagger! 0.7s
✔ .publish(
│ │ source: no(digest: "sha256:a193fa1900535ec7863ee98a6c54193936548dce773ded99055600694baa477f"): Missing
│ ): String! 4m4s
ttl.sh/hello-dagger-XXXXXXXX@sha256:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Full trace at https://dagger.cloud/kun432-org/traces/XXXXXXXXXXXXXXXXXXXX
こんな感じでイメージをpullして実行してみる。
docker run --rm -p 8080:80 ttl.sh/hello-dagger-XXXXXXXX@sha256:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
ブラウザでアクセスすると動作していることが確認できる。
またDagger Cloudを使用している場合は、パイプライン実行後の出力の最後にあるURLにアクセスすると以下のようにパイプラインの各ステップの出力などの「トレース」が確認できる。
ここまでの個人的な印象だと、DockerfileとGitHub ActionsのワークフローYAMLを1つのSDKでラップしてプログラマブルにしたもの、という感じ。
CI/CDのプラットフォームやテスト環境などの環境差異に依存しないのは良さそうだけど、以下の記事にある、
手元のPCで 自分の作成した CI/CDの設定ファイルの結果が確認できるので
従来のような、設定ファイルを作ってcommitしてpushしたけどCI/CDの動作を見てみると、設定のyamlファイルを間違えていたからまた修正のコミットする ということが少なくなりそうな期待があります。
これはいいね。アプリケーションのテストは、レポジトリと関係なく自分の手元で何回でも・どうとでもテストできるのだけど、CI/CDの設定はやってみないとわからない、テストできない、みたいなところがあって、レポジトリの運用とも絡むのでちょっとしんどい。チームで使っている場合などは躊躇する部分でもあるので、ここが手元でテストできる安心感はあるし、CI/CDで動かした場合にも手戻りを少なくできそう。
まだローカルで動かしただけなので続きを進める。
ビルド環境を作成する
1つ前のQuickstartで作成したパイプラインで一連の流れがわかった。ここからはパイプラインの各ステップについて細かく見ていく様子。
まずビルド環境の作成について。build_env
ステージの定義は以下となっていた。
import dagger
from dagger import dag, function, object_type
@object_type
class HelloDagger:
@function
def build_env(self, source: dagger.Directory) -> dagger.Container:
"""すぐに使える開発環境を構築する"""
# 依存ライブラリ用のキャッシュボリュームを作成
node_cache = dag.cache_volume("node")
return (
dag.container()
# Node.jsベースのコンテナを使用
.from_("node:21-slim")
# /src ディレクトリにソースコードを追加
.with_directory("/src", source)
# キャッシュボリュームを/root/.npmディレクトリにマウント
.with_mounted_cache("/root/.npm", node_cache)
# 作業ディレクトリを/srcに変更
.with_workdir("/src")
# `npm install`を実行して依存ライブラリをインストール
.with_exec(["npm", "install"])
)
コードについてはざっくりこんな感じ。
- Dagger Functionは、ソースコードの場所(
source: Directory
)を受け取って、コンテナ(dagger.Container
)を返す。-
dagger.Directory
はローカルディレクトリもしくはリモートのgitレポジトリを指す。
-
- 全てのDagger Functionには、事前に初期化済みの
dag
クライアントが用意されている。-
Container
やDirectory
などのコア型、モジュールで宣言された依存関係へのバインディングなどが含まれる。
-
-
dag
クライアントの.container()
メソッドでContainer
オブジェクトを作成- メソッドチェーンでコンテナの設定を行う
コンテナの設定箇所はもろDockerfileな雰囲気がある。Dockerfileで書くとたぶんこんな感じになるはず。
FROM node:21-slim
VOLUME ["/root/.npm"]
COPY . /src
WORKDIR /src
RUN npm install
これがチェーンで書けるのは面白い。
このDagger Functionを実行してみる。
dagger call build-env --source=.
結果
✔ connect 0.3s
✔ load module 6.8s
✔ parsing command line arguments 0.0s
✔ Host.directory(path: "/Users/kun432/work/hello-dagger"): Directory! 0.0s
✔ helloDagger: HelloDagger! 0.8s
✔ .buildEnv(
│ │ source: no(digest: "sha256:ad8d3e1100afdadde5f0894f668a79e7e9e847c74f075b292e02011eff8d4303"): Missing
│ ): Container! 46.3s
✔ .defaultArgs: [String!]! 0.0s
✔ Container.entrypoint: [String!]! 0.0s
✔ Container.user: String! 0.0s
✔ Container.mounts: [String!]! 0.0s
✔ Container.platform: Platform! 0.0s
✔ Container.workdir: String! 0.0s
_type: Container
defaultArgs:
- node
entrypoint:
- docker-entrypoint.sh
mounts:
- /root/.npm
platform: linux/arm64
user: ""
workdir: /src
Full trace at https://dagger.cloud/kun432-org/traces/XXXXXXXXXXXXXXXXXX
作成したコンテナオブジェクトが返されている。
このコンテナオブジェクトにターミナルでアクセスすることもできる。terminal
を使う
dagger call build-env --source=. terminal --cmd=bash
コンテナのデバッグなどに便利。
ユニットテストの実行
続けてユニットテスト。test
ステージのDagger Functionを追加する。test
はbuild_env
に依存しているので、test
の中でbuild_env
を呼び出している。
import dagger
from dagger import dag, function, object_type
@object_type
class HelloDagger:
@function
async def test(self, source: dagger.Directory) -> str:
"""ユニットテストの実行結果を返す"""
return await (
# 他のDagger Functionを呼び出してテストを実行するためのコンテナを作成
self.build_env(source)
# テストを実行
.with_exec(["npm", "run", "test:unit", "run"])
# コマンドの出力を標準出力で返す
.stdout()
)
@function
def build_env(self, source: dagger.Directory) -> dagger.Container:
"""すぐに使える開発環境を構築する"""
node_cache = dag.cache_volume("node")
return (
dag.container()
.from_("node:21-slim")
.with_directory("/src", source)
.with_mounted_cache("/root/.npm", node_cache)
.with_workdir("/src")
.with_exec(["npm", "install"])
)
では実行
dagger call test --source=.
テストが実行されているのがわかる
結果
✔ connect 0.2s
✔ load module 0.5s
✔ parsing command line arguments 0.0s
✔ Host.directory(path: "/Users/kun432/work/hello-dagger"): Directory! 0.0s
✔ helloDagger: HelloDagger! 0.8s
✔ .test(
│ │ source: no(digest: "sha256:6713b4c9512bdafdbe73918c95d0a8d17dfb43f93b582f56748f5cbe24b90065"): Missing
│ ): String! 1.5s
> hello-dagger@0.0.0 test:unit
> vitest run
RUN v1.6.0 /src
✓ src/components/__tests__/HelloWorld.spec.ts (1 test) 7ms
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 14:14:52
Duration 339ms (transform 40ms, setup 0ms, collect 63ms, tests 7ms, environment 127ms, prepare 57ms)
Full trace at https://dagger.cloud/kun432-org/traces/XXXXXXXXXXXXXXXX
アプリケーションのビルド
続けてアプリケーションのビルド。build
ステージのDagger Functionを追加する。build
もbuild_env
に依存しているので、中でbuild_env
を呼び出している。
import dagger
from dagger import dag, function, object_type
@object_type
class HelloDagger:
@function
def build(self, source: dagger.Directory) -> dagger.Container:
"""アプリケーションコンテナをビルドする"""
build = (
# 他のDagger Functionを呼び出してビルドを実行するためのコンテナを作成
self.build_env(source)
# アプリケーションをビルド
.with_exec(["npm", "run", "build"])
# ビルド出力ディレクトリを取得
.directory("./dist")
)
return (
dag.container()
# 軽量なNGINXコンテナを使用
.from_("nginx:1.25-alpine")
# ビルド出力ディレクトリをコンテナにコピー
.with_directory("/usr/share/nginx/html", build)
# コンテナのポートを公開
.with_exposed_port(80)
)
@function
def build_env(self, source: dagger.Directory) -> dagger.Container:
"""すぐに使える開発環境を構築する"""
node_cache = dag.cache_volume("node")
return (
dag.container()
.from_("node:21-slim")
.with_directory("/src", source)
.with_mounted_cache("/root/.npm", node_cache)
.with_workdir("/src")
.with_exec(["npm", "install"])
)
アプリケーションのビルドではマルチステージビルドが行われている
-
build_env
でビルド用コンテナを作成し、npm run build
でアプリケーションをビルド。ビルド結果はコンテナファイルシステム内の./dist
に出力される -
dag.container()
で今度はNGINXコンテナを作成し、前のステージで作成された./dist
をコピーして、80番ポートでNGINXを起動
ではビルドを実行
dagger call build --source=.
✔ connect 0.3s
✔ load module 6.6s
✔ parsing command line arguments 0.0s
✔ Host.directory(path: "/Users/kun432/work/hello-dagger"): Directory! 0.0s
✔ helloDagger: HelloDagger! 0.7s
✔ .build(
│ │ source: no(digest: "sha256:f768e3211444daa011b7d3a7529de50b1bf94bbee121805a64fdf9957b2919de"): Missing
│ ): Container! 51.4s
✔ .defaultArgs: [String!]! 0.0s
✔ Container.mounts: [String!]! 0.0s
✔ Container.entrypoint: [String!]! 0.0s
✔ Container.workdir: String! 0.0s
✔ Container.platform: Platform! 0.0s
✔ Container.user: String! 0.0s
_type: Container
defaultArgs:
- nginx
- -g
- daemon off;
entrypoint:
- /docker-entrypoint.sh
mounts: []
platform: linux/arm64
user: ""
workdir: ""
Full trace at https://dagger.cloud/kun432-org/traces/XXXXXXXXXXXXXXXXX
build_dev
と同じようにコンテナオブジェクトが返されているが、前回と引数が異なっているのがわかる。
さらに今回の場合はNGINXが起動するので、そのままローカルでコンテナを起動することができる。as-service
を使う。
dagger call build --source=. as-service up --ports=8080:80
ブラウザでアクセスするとこの通り。
コンテナイメージのpublish
最後にコンテナイメージをレジストリにpushする、ということで最初のコードと同じになる。
import random
import dagger
from dagger import dag, function, object_type
@object_type
class HelloDagger:
@function
async def publish(self, source: dagger.Directory) -> str:
"""アプリケーションをテストし、コンテナをビルドし、公開する"""
# 他のDagger Functionを呼び出してテストを実行する
await self.test(source)
# 他のDagger Functionを呼び出してコンテナをビルドしてttl.shにpublishする
return await self.build(source).publish(
f"ttl.sh/hello-dagger-{random.randrange(10**8)}"
)
@function
def build(self, source: dagger.Directory) -> dagger.Container:
"""アプリケーションコンテナをビルドする"""
build = (
self.build_env(source)
.with_exec(["npm", "run", "build"])
.directory("./dist")
)
return (
dag.container()
.from_("nginx:1.25-alpine")
.with_directory("/usr/share/nginx/html", build)
.with_exposed_port(80)
)
@function
async def test(self, source: dagger.Directory) -> str:
"""ユニットテストの実行結果を返す"""
return await (
self.build_env(source)
.with_exec(["npm", "run", "test:unit", "run"])
.stdout()
)
@function
def build_env(self, source: dagger.Directory) -> dagger.Container:
"""すぐに使える開発環境を構築する"""
node_cache = dag.cache_volume("node")
return (
dag.container()
.from_("node:21-slim")
.with_directory("/src", source)
.with_mounted_cache("/root/.npm", node_cache)
.with_workdir("/src")
.with_exec(["npm", "install"])
)
これにより、publish
で個々のDagger Functionをまとめてパイプラインとして実行することもできるし、Dagger Function内で他のDagger Functionを呼ぶ仕組みになっているので個々のステージをCLIで実行して確認することもできるという次第。
Daggerverse
ここまでの例ではDagger APIを使ったパイプラインを自分で定義してきたが、GitHub Actionsのパブリックなワークフローと同様、他の人が作成したDagger Functionsを利用したり、自分のDagger Functionsを公開・共有することができる「Daggerverse」がある
以下で公開されているNode用のDaggeverseモジュールを使って今回のパイプラインを書き直してみる。
まずモジュールをインストールする。
dagger install github.com/dagger/dagger/sdk/typescript/dev/node@789200f43579a799b237c660e2faa79a83404104
✔ connect 0.2s
✔ moduleSource(refString: ".", requireKind: LOCAL_SOURCE): ModuleSource! 0.1s
✔ .configExists: Boolean! 0.0s
✔ ModuleSource.localContextDirectoryPath: String! 0.0s
✔ moduleSource(disableFindUp: true, refString: "github.com/dagger/dagger/sdk/typescript/dev/node@789200f43579a799b237c660e2faa79a83404104"): ModuleSource! 21.6s
✔ .moduleName: String! 0.0s
✔ ModuleSource.withDependencies(dependencies: [xxh3:e0af54a1480cc05a]): ModuleSource! 0.0s
✔ .withEngineVersion(version: "latest"): ModuleSource! 0.0s
✔ .generatedContextDirectory: Directory! 31.0s
✔ .export(path: "/Users/kun432/work/hello-dagger"): String! 0.1s
✔ ModuleSource.sdk: SDKConfig 0.0s
✔ .source: String! 0.0s
✔ ModuleSource.sourceRootSubpath: String! 0.0s
✔ ModuleSource.kind: ModuleSourceKind! 0.0s
✔ ModuleSource.cloneRef: String! 0.0s
✔ ModuleSource.version: String! 0.0s
✔ ModuleSource.commit: String! 0.0s
インストールしたモジュールは、dagger.jsonに依存関係として記録される。
{
"name": "hello-dagger",
"engineVersion": "v0.16.2",
"sdk": {
"source": "python"
},
"dependencies": [
{
"name": "node",
"source": "github.com/dagger/dagger/sdk/typescript/dev/node@789200f43579a799b237c660e2faa79a83404104",
"pin": "789200f43579a799b237c660e2faa79a83404104"
}
],
"source": "dagger"
}
インストールしたモジュールの使い方は、モジュールのページを見るか、またはコマンドで確認することができる。
dagger -m node functions
✔ connect 0.2s
✔ load module 0.9s
Name Description
commands Execute commands in the container.
container -
install Downloads dependencies in the container.
version -
with-npm Add npm as package manager in the container.
with-pnpm Add pnpm as package manager in the container.
with-source Add source to the module container.
with-yarn Add yarn as package manager in the container.
以下のコマンドでもOK。
run dagger -m node call --help
✔ connect 0.3s
✔ load module 0.9s
✔ parsing command line arguments 0.0s
Call one or more functions, interconnected into a pipeline
USAGE
dagger call [options] [arguments] <function>
FUNCTIONS
commands Execute commands in the container.
container -
install Downloads dependencies in the container.
version -
with-npm Add npm as package manager in the container.
with-pnpm Add pnpm as package manager in the container.
with-source Add source to the module container.
with-yarn Add yarn as package manager in the container.
ARGUMENTS
--ctr Container
--version string
OPTIONS
-j, --json Present result as JSON
-m, --mod string Path to the module directory. Either local path or a remote git repo
-o, --output string Save the result to a local file or directory
INHERITED OPTIONS
-d, --debug Show debug logs and full verbosity
-i, --interactive Spawn a terminal on container exec failure
--interactive-command string Change the default command for interactive mode (default "/bin/sh")
-E, --no-exit Leave the TUI running after completion
--progress string Progress output format (auto, plain, tty) (default "auto")
-q, --quiet count Reduce verbosity (show progress, but clean up at the end)
-s, --silent Do not show progress at all
-v, --verbose count Increase verbosity (use -vv or -vvv for more)
-w, --web Open trace URL in a web browser
Use "dagger call [command] --help" for more information about a command.
Full trace at https://dagger.cloud/kun432-org/traces/XXXXXXXXXXXXXXXX
ではDaggerverseモジュールを使用したパイプラインに修正する。
import random
from typing import Annotated
import dagger
from dagger import dag, function, object_type, DefaultPath
@object_type
class HelloDagger:
@function
async def publish(
self,
source: Annotated[dagger.Directory, DefaultPath("/")],
) -> str:
"""アプリケーションをテストし、コンテナをビルドし、公開する"""
await self.test(source)
return await self.build(source).publish(
f"ttl.sh/hello-dagger-{random.randrange(10**8)}"
)
@function
def build(
self,
source: Annotated[dagger.Directory, DefaultPath("/")],
) -> dagger.Container:
"""アプリケーションコンテナをビルドする"""
build = (
# nodeモジュールで作成したビルド環境を使用
dag.node(ctr=self.build_env(source))
.commands()
.run(["build"])
.directory("./dist")
)
return (
dag.container()
.from_("nginx:1.25-alpine")
.with_directory("/usr/share/nginx/html", build)
.with_exposed_port(80)
)
@function
async def test(
self,
source: Annotated[dagger.Directory, DefaultPath("/")],
) -> str:
"""ユニットテストの実行結果を返す"""
return await (
# nodeモジュールで作成したビルド環境を使用
dag.node(ctr=self.build_env(source))
.commands()
.run(["test:unit", "run"])
.stdout()
)
@function
def build_env(
self,
source: Annotated[dagger.Directory, DefaultPath("/")],
) -> dagger.Container:
"""すぐに使える開発環境を構築する"""
return (
# nodeモジュールを使用してビルド環境を作成
dag.node(version="21"). # nodeのバージョン21を使用
with_npm(). # npmを使用
with_source(source). # ソースコードをマウント
install(). # パッケージをインストール
container() # コンテナを作成
)
Nodeモジュールを使用して一番大きく変わったのはbuild_env
だけど、build
やtest
などもdag.node
を使用してモジュールが用意しているメソッドに書き換わっている。
あと、Dagger Functionの引数にDefaultPath("/")
をAnnotated
で追加している。これにより、Dagger CLIで--source
が明示されなかった場合、ソースコードがあるディレクトリ、もしくはGitレポジトリの「トップディレクトリ」が指定されるということになる。
なので、CLIでpublish
する場合は以下となる
dagger call publish
・・・んだけどエラーになるな。
Error: API Error
! process "tsx --no-deprecation --tsconfig /src/sdk/typescript/dev/node/tsconfig.json /src/sdk/typescript/dev/node/src/__dagger.entrypoint.ts" did not complete successfully: exit code: 1
モジュールの問題なのかも。
Quickstartが終わったので、実際にCIに組み込んでみる。
公式ドキュメントには以下のCIプラットフォームとの連携についてのドキュメントがある
- Argo Workflows
- AWS CodeBuild
- Azure Pipelines
- CircleCI
- GitHub Actions
- GitLab CI
- Jenkins
- Tekton
今回はGitHub Actions+GitHub Container Registryを使ってみようと思う。
いくつかやり方はあるようだけどもまずDagger Functionsはこんな感じ。
import os
import random
from typing import Annotated
import dagger
from dagger import dag, function, object_type, DefaultPath, Doc
@object_type
class HelloDagger:
@function
async def publish(
self,
source: Annotated[dagger.Directory, DefaultPath("/")],
registry: Annotated[str, Doc("registry address")],
user: Annotated[str, Doc("registry user")],
token: Annotated[dagger.Secret, Doc("registry token")],
) -> str:
"""アプリケーションをテストし、コンテナをビルドし、公開する"""
await self.test(source)
return await (
self.build(source)
.with_registry_auth(registry, user, token)
.publish(
f"{registry}/{user}/hello-dagger:{random.randrange(10**8)}"
)
)
@function
def build(
self,
source: Annotated[dagger.Directory, DefaultPath("/")],
) -> dagger.Container:
"""アプリケーションコンテナをビルドする"""
build = (
self.build_env(source)
.with_exec(["npm", "run", "build"])
.directory("./dist")
)
return (
dag.container()
.from_("nginx:1.25-alpine")
.with_directory("/usr/share/nginx/html", build)
.with_exposed_port(80)
)
@function
async def test(
self,
source: Annotated[dagger.Directory, DefaultPath("/")],
) -> str:
"""ユニットテストの実行結果を返す"""
return await (
self.build_env(source)
.with_exec(["npm", "run", "test:unit", "run"])
.stdout()
)
@function
def build_env(
self,
source: Annotated[dagger.Directory, DefaultPath("/")],
) -> dagger.Container:
"""すぐに使える開発環境を構築する"""
node_cache = dag.cache_volume("node")
return (
dag.container()
.from_("node:21-slim")
.with_directory("/src", source)
.with_mounted_cache("/root/.npm", node_cache)
.with_workdir("/src")
.with_exec(["npm", "install"])
)
publish
のところで、引数にレジストリ名・ユーザ名・トークンなどを受け取るようにしておく。で、これをCLIで実行してみる。
export GH_PAT=XXXXXXXXXXXXXXXXXXXX
dagger call publish --registry=ghcr.io --user=kun432 --token=env:GH_PAT
引数でレジストリ名などを指定している。ちなみにパスワードやトークンなどは文字列をそのまま渡してもどうもうまくいかず、dagger.Secret
で型指定して、かつ、CLIでも環境変数で渡したらうまくいった。
✔ connect 0.3s
✔ load module 8.6s
✔ parsing command line arguments 0.0s
✔ helloDagger: HelloDagger! 0.8s
✔ .publish(
│ │ registry: "ghcr.io"
│ │ token: ✔ secret(uri: "env://GH_PAT"): Secret! 0.0s
│ │ user: "kun432"
│ ): String! 54.2s
ghcr.io/kun432/hello-dagger:35366056@sha256:c52f7336901bf773012d9c2ca948cfd653abbf74325df87a96cbc41c815e26ab
Full trace at https://dagger.cloud/kun432-org/traces/XXXXXXXXXXX
イメージも登録されていた。このイメージは一旦消しておく。
ではこれをGitHub Actionsで自動的化する。ちょっと雑ではあるが。
name: dagger
on:
push:
branches: [main]
jobs:
publish:
permissions:
contents: read
packages: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: publish
uses: dagger/dagger-for-github@8.0.0
with:
version: "latest"
verb: call
args: publish --registry=$GHCR_REGISTRY --user=$GHCR_USERNAME --token=env:GHCR_TOKEN
cloud-token: ${{ secrets.DAGGER_CLOUD_TOKEN }}
env:
GHCR_REGISTRY: ghcr.io
GHCR_USERNAME: ${{ github.actor }}
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Dagger用のActionを使って、CLIで実行したときと同じようにコマンドを実行するだけである。
pushするとActionが実行され、
コンテナイメージがpushされた。
なお、Dagger CloudのAPIキーをDAGGER_CLOUD_TOKEN
としてGitHubのSecretに設定してActionにセットしておくと、CIのトレースがDagger Cloudで確認できるようになる。
GitHub Actionsの設定がゼロになるわけではないけども、CLIとCIでやっていることがほぼ変わらないのは良さそう。
参考
まとめ
開発プロセス、コード書いてテストするだけなら言語のエコシステムの中で完結するんだけども、コンテナ化したりCI/CDで自動化したり、っていうところをやろうと思うと、求められる知識の幅が一気に広がる感がある。
そのあたりをDaggerさえわかっていれば、って感じで吸収してくれるのは良いと思う。コマンドラインでやってることとCI・CDでやってることが同じってのも色んな意味で安心感がある。ただ、逆に言うとDaggerに依存することになるわけで、そのへんはケースバイケースで判断するかな。
あと、これは個人的な印象かもしれないが、
- GitHub Container Registryへのpushをやろうと思ってドキュメント探しても、ピンポイントでこれというものがなくて、結局APIリファレンスを追いかけることになった。ドキュメントのボリューム自体はそれなりにありそうなんだけども。
- ググったらそれなりにブログの記事なんかも見つかるんだけども、GoのSDKを使っている人がどうも多いみたいで、Pythonで使っている人の記事はあまりなかった。
あたりの情報の見つけやすさ、みたいなものはやや辛い感を感じた。
次は本題のAIエージェントへ