Dagger で Node.js アプリをビルドする
CI/CD のパイプラインを定義するためのツール Dagger を使って Node.js アプリのビルドを試してみました。
- Dagger v0.2.7
今回使用したソースは こちら
sample1. echo の実施
まずは以下の処理を Dagger で実施してみます。
- (1) ローカルの
input.txt
ファイルの内容を取得 - (2) alpine イメージでコンテナを実行し、(1) に
_echo
を付けた文字列を echo して/tmp/output.txt
へ出力 - (3) コンテナの
/tmp/output.txt
の内容を取得し、ローカルのoutput.txt
ファイルへ出力
Dagger プロジェクト作成
下記コマンドを実行して Dagger の実行に必要なファイルを作成しておきます。
$ dagger project init $ dagger project update
project init で cue.mod
が作成され、project update で cue.mod/pkg
内へ dagger.io
と universe.dagger.io
のコアモジュールがインストールされます。
パイプライン定義
Dagger では CUE で CI/CD パイプラインを定義する事になっており、dagger.#Plan & { パイプラインの内容 }
という風に記述します。※
CUE は一部 Go 言語風なので紛らわしいのですが、パワーアップした JSON のようなものだと捉えておくと理解し易いかもしれません。
※ 個人的に、dagger.#Plan の内容(cue.mod/pkg/dagger.io/dagger/plan.cue 参照)に対して { ・・・ } の内容をマージしているのだと解釈しています
パイプラインの処理は dagger.#Plan の actions
で定義する事になっており、アクション名(下記の sample
)は任意の名称を付けられるようです。(dagger do 時にアクション名を指定)
actions の処理はコンテナ内で実行する事になるので、ローカルのファイルや環境変数をコンテナとやりとりするために client
が用意されています。
前述の (1) ~ (3) を定義すると以下のようになりました。
sample.cue
package sample1 import ( "dagger.io/dagger" "dagger.io/dagger/core" ) dagger.#Plan & { actions: { sample: { // (2) alpine イメージの pull _alpine: core.#Pull & { source: "alpine:3" } // (1) ローカルの input.txt ファイルの内容 msg: client.filesystem."input.txt".read.contents // (2) コンテナで echo を実行し /tmp/output.txt へ出力 echo: core.#Exec & { input: _alpine.output args: [ "sh", "-c", "echo -n \(msg)_echo > /tmp/output.txt", ] always: true } // (3) コンテナの /tmp/output.txt の内容を取得 result: core.#ReadFile & { input: echo.output path: "/tmp/output.txt" } } } client: { filesystem: { // (1) ローカルの input.txt ファイルの内容を取得(ファイルの内容を取得するために string を設定) "input.txt": read: contents: string // (3) ローカルの output.txt ファイルへ出力 "output.txt": write: contents: actions.sample.result.contents } } }
コンテナの実行に core.#Exec
、コンテナで出力したファイルの内容を取得するために core.#ReadFile
を使っています。
client.filesystem.<パス>.read.contents
の値を string
とする事で ※、パスで指定したローカルファイルの内容を文字列として参照できます。
※ dagger.#FS とするとファイルシステムを参照できる
なお、前処理の output
を次の処理の input
に繋げていく事でパイプライン(処理の順序)を実現しているようです。
実行
それでは、実行してみます。
input.txt の内容を以下のようにしました。
input.txt
sample1
dagger do で任意のアクションを実行します。
実行例
$ dagger do sample [✔] actions.sample ・・・ [✔] client.filesystem."input.txt".read ・・・ [✔] actions.sample.echo ・・・ [✔] actions.sample.result ・・・ [✔] client.filesystem."output.txt".write ・・・
正常に終了し、ローカルファイル output.txt が作成されたので内容を確認すると以下のようになりました。
output.txt の内容確認
$ cat output.txt sample1_echo
sample2. Node.js アプリのビルド
それでは、本題の Node.js アプリのビルドです。
Node.js アプリ
ここでは、以下の TypeScript ファイルを esbuild
を使って Node.js 用にビルドする事にします。
src/app.ts
import * as E from 'fp-ts/Either' console.log(E.right('sample2'))
今回、esbuild による処理は package.json の scripts で定義しました。
package.json
{ "scripts": { "build": "npx esbuild ./src/app.ts --bundle --platform=node --outfile=build/bundle.js" }, "devDependencies": { "esbuild": "^0.14.37" }, "dependencies": { "fp-ts": "^2.11.10" } }
パイプライン定義
ここでは、以下の処理を実施します。
- (1) ローカルの
package.json
とsrc
ディレクトリをコンテナの/app
へコピー - (2)
npm install
を実行 - (3)
npm run build
を実行 - (4) コンテナの
/app/build
の内容をローカルの_build
へ出力
sample1 ではファイル単位でローカル側とやりとりしましたが、こちらはディレクトリ単位で実施するようにしています。
client.filesystem.".".read.contents
を dagger.#FS
とする事でローカルのカレントディレクトリを参照し、include
を使って package.json
と src
以外を除外しています。
ローカルのディレクトリとファイルをコンテナ環境へコピーするために core.#Copy
、コンテナの /app/build
の内容を参照するために core.#Subdir
を使っています。
node_build.cue
package sample2 import ( "dagger.io/dagger" "dagger.io/dagger/core" ) dagger.#Plan & { actions: { build: { _node: core.#Pull & { source: "node:18-alpine" } // (1) ローカルの package.json と src ディレクトリをコンテナの /app へコピー src: core.#Copy & { input: _node.output contents: client.filesystem.".".read.contents dest: "/app" } // (2) npm install を実行(/app をカレントディレクトリに指定) deps: core.#Exec & { input: src.output workdir: "/app" args: ["npm", "install"] always: true } // (3) npm run build を実行(/app をカレントディレクトリに指定) runBuild: core.#Exec & { input: deps.output workdir: "/app" args: ["npm", "run", "build"] always: true } // (4) /app/build の内容を参照 result: core.#Subdir & { input: runBuild.output path: "/app/build" } } } client: { filesystem: { // (1) ローカルの package.json と src を参照 ".": read: { contents: dagger.#FS include: ["package.json", "src"] } // (4) ローカルの _build へ出力 "_build": write: contents: actions.build.result.output } } }
実行
dagger do で実行します。
実行例
$ dagger do build ・・・ [✔] actions.build ・・・ [✔] client.filesystem.".".read ・・・ [✔] actions.build.src ・・・ [✔] actions.build.deps ・・・ [✔] actions.build.runBuild ・・・ [✔] actions.build.result ・・・ [✔] client.filesystem."_build".write ・・・
これにより、ローカルへ下記のファイルが出力されました。
_build/bundle.js
var __create = Object.create; ・・・ // src/app.ts var E = __toESM(require_Either()); console.log(E.right("sample2"));
問題なく実行できました。
_build/bundle.js の動作確認例
$ cd _build $ node bundle.js { _tag: 'Right', right: 'sample2' }