最近話題になっている?Daggerを使用して
typescriptをトランスパイルしS3へdeployするパイプラインを作成してみました。
Daggerとは
Daggerは、CICD向けのポータブルなdevkitです。
DaggerはDocker互換のランタイムでパイプラインが実行されるため、Docker互換のランタイムが動く環境であれば
Daggerをそのまま実行し、同一の結果を得ることができます。
Daggerではパイプラインの記述を一般的な yaml
などではなくcuelangを使用して記述します。
module/packageや型定義などの強力な構文を使用することができます。
チュートリアルなども用意されています。
メリット
上記のことからDaggerを使用することで
- 開発環境とCI環境の統一
- CIロックインをなくすことができる
といったメリットを受けることができます。
Daggerのinstall
brew tapでinstall可能です
brew install dagger/tap/dagger
Daggerのsampleを見る
sampleに関しては、リポジトリのpkg内にあるtestを見る形になります。
(以前はexampleがあったのですが、現在のバージョンのものはリポジトリ内になさそう?過去のversionにはあります)
下記はbashのtest.cueです。
package bash
import (
"dagger.io/dagger"
"dagger.io/dagger/core"
"universe.dagger.io/docker"
"universe.dagger.io/bash"
)
dagger.#Plan & {
actions: test: {
_pull: docker.#Pull & {
source: "index.docker.io/debian"
}
_image: _pull.output
// Run a script from source directory + filename
runFile: {
dir: _load.output
_load: core.#Source & {
path: "./data"
include: ["*.sh"]
}
run: bash.#Run & {
input: _image
export: files: "/out.txt": _
script: {
directory: dir
filename: "hello.sh"
}
}
output: run.export.files."/out.txt" & "Hello, world\n"
}
// Run a script from string
runString: {
run: bash.#Run & {
input: _image
export: files: "/output.txt": _
script: contents: "echo 'Hello, inlined world!' > /output.txt"
}
output: run.export.files."/output.txt" & "Hello, inlined world!\n"
}
}
}
cueではgolangと同様に package
名を指定します。
package bash
その後使用するmoduleをimportします。
import (
"dagger.io/dagger"
"dagger.io/dagger/core"
"universe.dagger.io/docker"
"universe.dagger.io/bash"
)
このあたりはgolangと同様です。
moduleはv0.2.17現在では下記で用意されているものが一覧のようです。
パイプラインの内容は dagger.#Plan & {}
に記述していきます。
dagger.#Plan & {
actions: test: {
_pull: docker.#Pull & {
source: "index.docker.io/debian"
}
_image: _pull.output
// Run a script from source directory + filename
runFile: {
dir: _load.output
_load: core.#Source & {
path: "./data"
include: ["*.sh"]
}
run: bash.#Run & {
input: _image
export: files: "/out.txt": _
script: {
directory: dir
filename: "hello.sh"
}
}
output: run.export.files."/out.txt" & "Hello, world\n"
}
// Run a script from string
runString: {
run: bash.#Run & {
input: _image
export: files: "/output.txt": _
script: contents: "echo 'Hello, inlined world!' > /output.txt"
}
output: run.export.files."/output.txt" & "Hello, inlined world!\n"
}
}
}
このパイプラインは
debianのdocker imageを使用して
_pull: docker.#Pull & {
source: "index.docker.io/debian"
}
_image: _pull.output
hello.sh
を実行し結果を出力するものと
runFile: {
dir: _load.output
_load: core.#Source & {
path: "./data"
include: ["*.sh"]
}
run: bash.#Run & {
input: _image
export: files: "/out.txt": _
script: {
directory: dir
filename: "hello.sh"
}
}
output: run.export.files."/out.txt" & "Hello, world\n"
}
fileではなく script: contents:
で shellを実行するものが定義されています。
runString: {
run: bash.#Run & {
input: _image
export: files: "/output.txt": _
script: contents: "echo 'Hello, inlined world!' > /output.txt"
}
output: run.export.files."/output.txt" & "Hello, inlined world!\n"
}
実行方法
- 準備
dagger project update
- 実行
dagger do タスク名 optionあれば(--log-format=plainや--no-cacheなど)
使用可能なoptionについては dagger do -h
で確認してください。
詳細な構文説明などはドキュメントを参照してください
今回作成したパイプライン
typescriptアプリケーションをcircle ci上のDaggerでトランスパイルし、daggerでのaws cliを通してs3へアップロードするといったものになります。
- 作成したdeployパイプライン
package main
import (
"dagger.io/dagger"
"dagger.io/dagger/core"
"universe.dagger.io/docker"
"universe.dagger.io/bash"
"universe.dagger.io/aws"
"universe.dagger.io/aws/cli"
)
dagger.#Plan & {
client: {
env: {
AWS_ACCESS_KEY_ID: dagger.#Secret
AWS_SECRET_ACCESS_KEY: dagger.#Secret
}
filesystem: {
"./": read: {
contents: dagger.#FS,
exclude: [
"README.md",
".gitignore",
"deploy.cue",
"node_modules",
"dist"
]
}
"output.txt": write: contents: actions.deploy.export.files["/output.txt"]
"./dist": write: contents: actions.build.contents.output
}
}
actions: {
params: {
npmCommand?: string
bucket?: string
}
build: {
buildDeps: docker.#Build & {
steps: [
docker.#Pull & { source: "node:16.15-buster" },
docker.#Copy & {
contents: client.filesystem."./".read.contents
dest: "/app"
},
bash.#Run & {
workdir: "/app"
script: contents: "npm ci"
},
docker.#Run & {
workdir: "/app"
command: {
name: "/bin/sh"
args: ["-c", """
npm run \(params.npmCommand)
"""]
}
},
]
}
contents: core.#Subdir & {
input: buildDeps.output.rootfs
path: "/app/dist"
}
}
deploy: cli.#Command & {
workdir: "/app"
mounts: "/app": {
dest: "/app"
contents: build.contents.output
}
credentials: aws.#Credentials & {
accessKeyId: client.env.AWS_ACCESS_KEY_ID
secretAccessKey: client.env.AWS_SECRET_ACCESS_KEY
}
options: {
region: "ap-northeast-1"
}
unmarshal: false
service: {
name: "s3"
command: "cp ./hoge.js s3://\(params.bucket)/"
}
_build: _scripts: core.#Source & {
path: "_scripts"
}
}
}
}
内容
aws関連の処理を行うのでawsのpackageをimportしています。
合わせてbuild周りでnode環境が必要なためdockerを使用しています。
package main
import (
"dagger.io/dagger"
"dagger.io/dagger/core"
"universe.dagger.io/docker"
"universe.dagger.io/bash"
"universe.dagger.io/aws"
"universe.dagger.io/aws/cli"
)
次にplanで使用する環境変数やoutput先、読み込むファイルなどの設定をしています。
dagger.#Plan & {
client: {
env: {
AWS_ACCESS_KEY_ID: dagger.#Secret
AWS_SECRET_ACCESS_KEY: dagger.#Secret
}
filesystem: {
"./": read: {
contents: dagger.#FS,
exclude: [
"README.md",
".gitignore",
"deploy.cue",
"node_modules",
"dist"
]
}
"output.txt": write: contents: actions.deploy.export.files["/output.txt"]
"./dist": write: contents: actions.build.contents.output
}
}
action部分に関しては実行するbuildコマンドとデプロイ先のバケット名の2つのパラメータを受け取れるようにし
buildとdeployのタスクを作成しています。
actions: {
params: {
npmCommand?: string
bucket?: string
}
build: {
buildDeps: docker.#Build & {
.........
......
...
}
}
deploy: cli.#Command & {
.........
......
...
}
}
buildタスク
buildタスクではdockerのimageを指定(node:16.15-buster)し
必要となる contents
(jsファイルやbuild周りの設定ファイルなど)を /app
下に Copyしています。
その後コマンドを実行しています。
build: {
buildDeps: docker.#Build & {
steps: [
docker.#Pull & { source: "node:16.15-buster" },
docker.#Copy & {
contents: client.filesystem."./".read.contents
dest: "/app"
},
bash.#Run & {
workdir: "/app"
script: contents: "npm ci"
},
docker.#Run & {
workdir: "/app"
command: {
name: "/bin/sh"
args: ["-c", """
npm run \(params.npmCommand)
"""]
}
},
]
}
contents: core.#Subdir & {
input: buildDeps.output.rootfs
path: "/app/dist"
}
}
build結果は dist
に吐かれるようになっているため contents: core.#Subdir
の input
で buildDeps.output.rootfs
を指定 path
も合わせて記述します。
contents: core.#Subdir & {
input: buildDeps.output.rootfs
path: "/app/dist"
}
またこの結果は localの ./dist
に吐かれるように client
にて設定されているので
local上にbuildした結果の .js
が出力されています
"./dist": write: contents: actions.build.contents.output
deployタスク
deployではaws cliパッケージを使用してawsコマンドを使用してs3へdeployしています。
deploy: cli.#Command & {
workdir: "/app"
mounts: "/app": {
dest: "/app"
contents: build.contents.output
}
credentials: aws.#Credentials & {
accessKeyId: client.env.AWS_ACCESS_KEY_ID
secretAccessKey: client.env.AWS_SECRET_ACCESS_KEY
}
options: {
region: "ap-northeast-1"
}
unmarshal: false
service: {
name: "s3"
command: "cp ./hoge.js s3://\(params.bucket)/"
}
_build: _scripts: core.#Source & {
path: "_scripts"
}
}
buildした結果を使うため、 /app
に build.contents
の結果を mountします
mounts: "/app": {
dest: "/app"
contents: build.contents.output
}
v0.2.17時点ではawsパッケージを使用する際は _build
の指定が必須?になっており
今回は _scripts
にcli install shellを設置しています。
_build: _scripts: core.#Source & {
path: "_scripts"
}
実際に実行するcommandに関しては service
にて name(aws service名)
と command(実行するcommand)
を指定します
service: {
name: "s3"
command: "cp ./hoge.js s3://\(params.bucket)/"
}
実行方法
- project updateを行い
cue.mod
を取得します
dagger project update
- deploy taskの実行
実行するタスクを指定します
今回作成したtaskは実行時パラメータを指定する形になっているため指定します
事前にタスク内で使用する環境変数(AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY
)をexportしておきます
dagger do deploy --with 'actions: params: { npmCommand: "build", bucket: "deploy_bucket" }' --log-format=plain
deployを実行することでdeployタスク内で build.contents
buildのoutput結果を呼んでいるためbuild taskも実行されます。
- build taskのみの実行
buildを指定すれば実行されます
dagger do build --with 'actions: params: { npmCommand: "build" }' --log-format=plain
circle ci上での実行
以前はorbが用意されていたのですが、消えてしまっています...
そのため別途install.shを実行する必要があります
version: 2.1
jobs:
deploy:
machine: true
steps:
- run:
name: Install dagger.io
command: |
cd /usr/local
curl -L https://dl.dagger.io/dagger/install.sh | sudo sh
- checkout
- run:
name: dagger project update
command: dagger project update
- run:
name: dagger deploy task
command: |
dagger do deploy --with 'actions: params: { npmCommand: "build", bucket: "deploy_bucket" }' --log-format=plain
workflows:
version: 2
main:
jobs:
- deploy:
context: 定義したcontext
まとめ
daggerを使用することで、どの環境でも同一のパイプラインでCI/CDの実行が可能になります。
どのCIを使用しても動作が変わらないため、一度パイプラインを作成してしまえばlocalで実行しても
CI(github actionsやcircle ciなど)上で実行しても同一の結果得ることができます。
localでも簡単に実行できることから、複数人で開発を行う際にbuild環境の差異などを吸収することができます。
また、yaml
での定義とは違いpackage
として提供も可能なため異なるパイプラインでの再利用なども
可能となり、CI/CDパイプライン作成への時短へも繋がると思います。
まだ v0.2.x系ということもあり提供されている標準パッケージの数などは少ないですが、便利な点も多いので
パイプライン作成時の有力な選択肢としてぜひ一度使ってみてはいかがでしょうか。