Daggerを使用してCIパイプラインを作成する

最近話題になっている？[Dagger](https://dagger.io/)を使用して
typescriptをトランスパイルしS3へdeployするパイプラインを作成してみました。


# Daggerとは
Daggerは、CICD向けのポータブルなdevkitです。

https://docs.dagger.io/

DaggerはDocker互換のランタイムでパイプラインが実行されるため、Docker互換のランタイムが動く環境であれば
Daggerをそのまま実行し、同一の結果を得ることができます。

Daggerではパイプラインの記述を一般的な `yaml` などではなくcuelangを使用して記述します。
module/packageや型定義などの強力な構文を使用することができます。

https://cuelang.org/

チュートリアルなども用意されています。

https://cuetorials.com/

# メリット

上記のことからDaggerを使用することで
+ 開発環境とCI環境の統一
+ CIロックインをなくすことができる

といったメリットを受けることができます。

# Daggerのinstall

brew tapでinstall可能です

```install.sh
brew install dagger/tap/dagger
```

# Daggerのsampleを見る

sampleに関しては、リポジトリのpkg内にあるtestを見る形になります。

https://github.com/dagger/dagger/tree/v0.2.17/pkg

(以前はexampleがあったのですが、現在のバージョンのものはリポジトリ内になさそう？過去のversionにはあります)

下記はbashのtest.cueです。

```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現在では下記で用意されているものが一覧のようです。

https://github.com/dagger/dagger/tree/v0.2.17/pkg/universe.dagger.io

パイプラインの内容は `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
```

+ 実行

```exec.sh
dagger do タスク名 optionあれば(--log-format=plainや--no-cacheなど) 
```

使用可能なoptionについては `dagger do -h`で確認してください。

詳細な構文説明などはドキュメントを参照してください

https://docs.dagger.io/1235/what

# 今回作成したパイプライン

typescriptアプリケーションをcircle ci上のDaggerでトランスパイルし、daggerでのaws cliを通してs3へアップロードするといったものになります。

+ 作成したdeployパイプライン

```deploy.cue
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を設置しています。

https://github.com/dagger/dagger/blob/v0.2.17/pkg/universe.dagger.io/aws/_scripts/install.sh

```
      _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.sh
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が用意されていたのですが、消えてしまっています...

https://docs.dagger.io/1201/ci-environment

そのため別途install.shを実行する必要があります

```config.yml
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系ということもあり提供されている標準パッケージの数などは少ないですが、便利な点も多いので
パイプライン作成時の有力な選択肢としてぜひ一度使ってみてはいかがでしょうか。