CI pipeline as CodeによるPush and Pray🙏からの脱却

CIパイプラインを作成、メンテしたことがある方なら一度は経験したことがあるかもしれませんが、CIパイプラインが問題を抱えた場合に、修正コードをプッシュし祈りながら待つ、という苦行を延々と繰り返されることがよくあります。なぜなら、CIパイプラインが動く環境と同一環境をローカル環境に用意することは難しく、ローカル環境でのCIパイプラインの事前検証結果は（あまり）当てにならないからです。
その他にも以下のような要因によってCIパイプラインの保守コストが増大する、デリバリーのボトルネックになる等の課題があります：

* ローカル環境では問題なく動くが、JenkinsやGithub Action環境では動かない
* CIパイプラインを細かく区切ると動くけど、通して流すと動かない
* CIパイプラインが大作の場合には結果が出るまで２０～３０分かかってトライ＆エラーのループを回すのに時間がかかる、等々

このような課題に対して、CIパイプラインそのものもモダンなアプローチで開発すべきソフトウェアであると捉えて、CI/CDパイプラインをコード化するツール/サービスとして生まれたのが `Dagger` です。このDaggerを利用した `CI Pipeline as Code`というモダンなアプローチについて本記事では紹介したいと思います。

# 本記事でお伝えする内容

* Dagger概要・コンセプト
    * 先日のKubeCon+CloudNativeCon Nourth America 2023のCo-located Eventとして開催されたAppDeveloperConのセッション内容[^1]とSolomon HykesによるDaggerに関するPodcast[^2]の内容を踏まえた内容です
* Daggerのハンズオン
    * Daggerで定義したサンプルCIパイプラインを以下3種の環境で動かしてみた結果のシェア(詳しい手順には触れません/参考情報を共有するのみです)
        * ローカル
        * Github Actions
        * Argo Workflows
    * CI/CDパイプラインの記載方法については触れない
* Daggerの現状
    * Community Callで紹介があった最新プロジェクトZenithの説明[^3]を読み解きます

[^1]: [CNCF-Hosted Co-located Events North America 2023: Run Your CI in Code, Not YAML - Kyle Pen...](https://colocatedeventsna2023.sched.com/event/1Rj6A/run-your-ci-in-code-not-yaml-kyle-penfound-dagger)
[^2]: [The Kubelist Podcast | Ep. #37, Dagger with Solomon Hykes | Heavybit](https://www.heavybit.com/library/podcasts/the-kubelist-podcast/ep-37-dagger-with-solomon-hykes)
[^3]: [Introduction to Project Zenith - YouTube](https://www.youtube.com/watch?v=Pt_AsbQilAM)

# Dagger概要

Dockerの創業者の一人として知られるSolomon Hykesが、Dockerを離れた後、新たに立ち上げたDevOpsプラットフォームです。Daggerは、オープンソースのCI/CD（Continuous Integration/Continuous Deployment）環境構築エンジン（[dagger/dagger: Application Delivery as Code that Runs Anywhere](https://github.com/dagger/dagger)）と有償のDagger Cloudにより構成されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39615/c41e0627-3340-0aba-337f-9e6007309738.png)

> Powerful, programmable CI/CD engine that runs your pipelines in containers — pre-push on your local machine and/or post-push in CI

[dagger.io | Dagger Engine](https://dagger.io/engine) より

## Daggerが解決する課題

開発チームの規模が大きくになるにつれて、CI/CDパイプラインがペインポイントの一つとして挙げられます。例えば、あるチーム内でサイロ化されたCI/CDパイプライン環境は、そのチームに閉じた環境で利用し続ける場合には、作業効率を高める役割を果たすと思いますが、他の開発チーム間でコラボレーションをする場合において（例：フロントエンドチームがバックエンドチームのステージング環境を使ってテストしたい等）、CI/CDパイプラインの共有はボトルネックになります（そもそもCI/CDパイプラインを共有する、というアイデア以前に環境自体の公開や環境構築手順書を作成＆共有して構築を利用者に委ねる、等のパタンもあるかもしれません）。

このように複数のチームが連動するプロジェクトで重要なのは、各チームが利用する様々の言語、ツール等、で構成されたサービスを全てCI/CDパイプラインで統合して一気通貫で実行環境を選ばずに実行できるようにすることである、とDaggerでは提唱しています。関係する全てのチームで、この統合したCIパイプライン環境を継続的に利用できない限り、それはCI/CDパイプラインとは言えない、とまで言っています。

そんなボトルネックを解決するために、以下4つの代表的な特徴を持ってDaggerは生まれています。

* For instant feedback
    * 環境を選ばずにCI/CDパイプラインが実行できるので素早い事前検証が可能になる。CI環境にPushして初めて問題を検出する、ということが減らせる
* For maximum portability
    * CI/CDパイプラインの実装をSingle Source of Truthとして1つ開発すれば、様々なCIエンジンに対して、パイプラインを作り直す必要がない
* YAML＆シェルスクリプトからの解放
    * CI/CDの専任者でない限りYAMLの超大作であるパイプラインの修正やレビューは困難である。慣れ親しんだプログラム言語（例えばアプリケーション開発に使用した言語）でパイプラインを開発可能にすることで可読性や保守性を上げる
* 高速化
    * ファイル操作やビルド、再実行不要なテストなどを含めてキャッシュを利用することでパイプラインの実行時間を短縮する
    * キャッシュやDAG（[Directed acyclic graph: 有向非巡回グラフ](https://ja.wikipedia.org/wiki/%E6%9C%89%E5%90%91%E9%9D%9E%E5%B7%A1%E5%9B%9E%E3%82%B0%E3%83%A9%E3%83%95)）により依存関係を自動で解釈し、並行実施可能なパイプラインを見極めてパイプラインの実行時間を短縮する
    * "Daggerized"されたCI/CDパイプラインは一般的な方法で構成されたパイプラインより２～１０倍高速とのこと

これらの特徴によって、CI環境で起きる問題のデバッグや修正がフィードバックループの長いトライ＆エラー作業になることを防ぐことが可能になります。

## コンセプト

* CI/CDパイプラインを好きな言語でコード化できる
* CI/CDパイプラインもソフトウェアとしてテストが可能である
* ローカルでもGithub ActionでもJenkinsでも、いずれの環境でも同じコードでCI/CDパイプラインを動かすことができる
    * Single Source of TruthがCI/CDパイプラインの構成についても適用できる
    * 組織や環境間で一貫性のあるLintバージョンや環境変数を適用できる
    * docker-composeなどでコンテナの配備を別ファイルで管理する必要もない
    * 以下のイメージ[^1]である
* アプリのコンテナ化ではなく、パイプラインのコンテナ化に重点を置いている
    * パイプラインのポータビリティ性を出すには欠かせないコンセプトになります。アプリケーションがコンテナ化されている必要はない

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39615/16dadb65-9477-99f5-8047-1fc6dbdda91a.png) 

ちなみに、Daggerと呼ばれる所以は、パイプラインの一連の操作をグラフの一種であるDAG（Directed acyclic graph: 有向非巡回グラフ）で記述するためのAPIを提供するためです。グラフ内の各ノードは、パイプライン上の単純な操作に分けられ、そのグラフのノード間のエッジである線は、あるノードから次のノードに流れるデータを表します。したがって、1つのノード、1つの操作としては、「このコンテナでこのコマンドを実行するか」「このDockerイメージをPullするか」「このGitリポジトリをCheckoutするか」等が考えられます。これら１つ１つがビルディングブロックになっており、適切な構成モデルと抽象化モデルを用意することで、これらのビルディング ブロックから任意の CI/CD パイプラインを構築することが可能になります。このコンセプトの上に、更にキャッシュの概念が適用されます。キャッシュの利点として、パイプラインを正しく設計されていれば、各ノード（操作）に対して、エッジであらわされている全ての入力と出力を把握でき、各入出力のDigestを計算・記録することで、基本的にDAGを介して、パイプライン実行時の操作と発生する入出力データの完全なマップが得られます。これにより、パイプライン内の操作の一部をキャッシュを利用して、高速化しています。Docker Build で使われているのと同じ技術（Buildkit）をDagger内部でも利用しているようです。



## Daggerによって恩恵を受ける人

（現状は）サービスを既にデリバリーしていて、CI/CDパイプラインの選定・開発・改善に専任者を設けるような規模以上のチームに適しています。CI/CD環境の改善や再構築をする際に、そこがデリバリーサイクルのボトルネックになりえます。そんなときにDaggerの恩恵にあずかれます。

# Dagger動作検証

参照しているサンプルプログラムでは、ローカル環境＋Github Actionsを例にして、そのポータビリティ性をデモしている。ここでは、それら２つの環境に加えて、我が家のKubernetesクラスタ上にあるArgo workflow環境にも、ポータビリティ性があるか検証することにする。

## 環境

### ターゲットとなるアプリケーション

以下のような2 Tier構成の簡易テストサイトを対象とします
* https://github.com/kpenfound/greetings-api

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39615/d057feb4-b886-1ce9-d224-4602b1f39af6.png)

### CI/CDパイプライン

今回CI/CDパイプラインはGolangで記載したものを利用します。このパイプラインからは、Daggerverseと呼ばれるDagger向けモジュールのエコシステム上に公開されたコンポーネント（Frontendのデプロイ先であるNetlify向けI/Fを提供するモジュールとBackendのデプロイ先であるFly.io向けI/Fを提供するモジュール）を流用しています。

:::note info
Daggerverseとは
今回のシステム構成で利用しているHugoやfly.io、NetlifyをDaggerのコード内からコントロールするためのモジュールがDaggerverse上で公開されており、コミュニティによる開発、メンテ、モジュールの追加等、エコシステムを形成している。ちなみに各モジュールは任意の言語で開発が可能で、例えば、Pythonで書かれたfly.ioのモジュールをGoで書かれたCI/CDパイプラインから利用することもできるので、組織内のスキルセットに合わせてモジュールの開発やパイプラインの開発で利用する言語を選択できます。
* https://daggerverse.dev (currently experimental)
:::

今回ハンズオンでは、以下３種の環境でDaggerを利用したCI(/CD)パイプラインの動作確認します。

* ローカル環境
* Github Action
* ArgoWorkflow on おうちK8sクラスター

## ローカル環境での動作確認

1. 手元でテスト＆実行＆デプロイまでを実施してみました (daggerのインストールは省略。詳細は、[Install the Dagger CLI | Dagger](https://docs.dagger.io/quickstart/729236/cli) 参照 ）

```shell
$ git clone https://github.com/kpenfound/greetings-api.git
$ cd greetings-api
$ dagger call -m ./ci test --dir "."
✔ dagger call test [26.77s]
┃ ok      github.com/kpenfound/greetings-api      0.011s
• Engine: f4748445d6ba (version v0.9.3)
⧗ 54.32s ✔ 238 ∅ 93
```
テストできました! 
なおDagger Cloudにサインアップしてトークンを取得し、環境変数`DAGGER_CLOUD_TOKEN`にセットした状態でテストを回すとDagger Cloudにログや結果が自動で連携されて、クラウド環境で結果の確認ができるようです。

```shell
$ export DAGGER_CLOUD_TOKEN=******
$ dagger call -m ./ci test --dir "."
✔ dagger call test [5.66s]
┃ ok      github.com/kpenfound/greetings-api      0.004s
• Cloud URL: https://dagger.cloud/runs/2eaf7db4-864a-4c58-bda8-4e02b7670af4
• Engine: f4748445d6ba (version v0.9.3)
⧗ 10.55s ✔ 77 ∅ 113
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39615/6220f92d-2d27-50f4-9cb9-ee44a6a8df23.png)

続いてローカルでサービスを動かしてみます。

```shell
$ dagger up -m ./ci -p 8080,8081 serve --dir "."
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39615/979bcb38-295d-5de4-2014-30f71706950e.png)

ローカルで実行もできました！続いて、各種SaaS（Docker, fly.io, Netlify）へのデプロイです！（fly.io、Netlify、Dockerのユーザー作成やデプロイ先の事前準備は別途必要です）

```shell
$ export FLY_TOKEN=******
$ export NETLIFY_TOKEN=******
$ export DOCKER_USER=******
$ export DOCKER_TOKEN=******
$ dagger call -m ./ci deploy --dir "." --fly-token $FLY_TOKEN --netlify-token $NETLIFY_TOKEN --registry-user $DOCKER_USER --registry-pass $DOCKER_TOKEN
```

:congratulations: https://nekia.netlify.app/ 
CORSの設定が何故かFly.ioで適用されずFrontendからBackendのAPI呼出しが失敗しますが、Docker HubのイメージPush、およびNetlify/Fly.ioへのデプロイも無事できました！

## Github Actionsでの動作確認

kpenfound/greeting-apiをgithub上でforkして、そのままコード修正するとGithub ActionsとしてCIが動き始めます。CIを定義する設定自体は以下の通り、シンプルであり、パイプラインに関する設定は、先ほどローカル環境で動かしたGoで設定されたものをそのまま利用する形になります。

```yaml
name: ci
on:
  push:
    branches: [main]
  pull_request:
jobs:
  ci:
    name: ci
    runs-on: ubuntu-latest
    steps:
      - name: Dagger
        uses: kpenfound/dagger-action@main
        with:
          args: ci-remote --commit $GITHUB_SHA
          module: github.com/kpenfound/greetings-api/ci
          cloud-token: ${{ secrets.DAGGER_CLOUD_TOKEN }}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39615/e7f43e0a-32c3-5ae4-3888-1ae0dd5a1026.png)

:::note warn
残念ながら、CIが失敗しています。コミュニティに問合せ中です
* https://discord.com/channels/707636530424053791/1117139064274034809/1181595355712856176
:::


## Argo Workflows on K8sクラスターでの動作確認

まずは以下のHelm ChartでDagger engineをDaemonSetとしてクラスタにインストールします。なお、K8sクラスター上へのDaggerのセットアップに関しては以下を参照しました。

* [Use Dagger with Argo Workflows | Dagger](https://docs.dagger.io/324301/argo-workflows/)
* [Run Dagger on Kubernetes | Dagger](https://docs.dagger.io/194031/kubernetes/)

```shell
$ helm upgrade --create-namespace --install --namespace dagger dagger oci://registry.dagger.io/dagger-helm
```

Daggerを利用するArgo Workflowsの定義には以下のマニフェストを利用します。ポイントは、ワークフロー内のコンテナは `/var/run/dagger` 配下のソケットを通じて、各ノード上にDaemonSetとしてインストールされたDagger Engineにアクセスするという点です。Dagger CLIはコンテナ内で直接ダウンロードしています。

```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: gomod-cache
spec:
  storageClassName: longhorn
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 3Gi
---
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: dagger-in-argo-
spec:
  entrypoint: dagger-workflow
  volumes:
    - name: dagger-conn
      hostPath:
        path: /var/run/dagger
    - name: gomod-cache
      persistentVolumeClaim:
        claimName: gomod-cache
  templates:
    - name: dagger-workflow
      inputs:
        artifacts:
          - name: project-source
            path: /work
            git:
              repo: https://github.com/kpenfound/greetings-api.git
              revision: "main"
          - name: dagger-cli
            path: /usr/local/bin
            mode: 0755
            http:
              url: https://github.com/dagger/dagger/releases/download/v0.9.3/dagger_v0.9.3_linux_arm64.tar.gz
      container:
        image: golang:1.21.0-bookworm
        command: ["sh", "-c"]
        args: ['dagger call -m ./ci test --dir "."']
        workingDir: /work
        env:
        - name: "_EXPERIMENTAL_DAGGER_RUNNER_HOST"
          value: "unix:///var/run/dagger/buildkitd.sock"
        - name: "DAGGER_CLOUD_TOKEN"
          valueFrom:
            secretKeyRef:
              name: dagger-cloud
              key: token
        volumeMounts:
          - name: dagger-conn
            mountPath: /var/run/dagger
          - name: gomod-cache
            mountPath: /go/pkg/mod
```

上記マニフェストを`argo`CLIにより、サブミットすることでworkflowをトリガーしてみます。

```shell
$ argo submit workflow.yaml --watch -n argo-workflows
```

以下の通り、Daggerの起動方法を利用するCIエンジンの作法に従って基本的に1つのCI/CDパイプラインのコードを使いまわすことができます。スクショ見せられてもピンとこないとは思いますが、一応できましたが非常に時間がかかっており、我が家のK8sクラスターを載せているRaspberry Piが非力なのか何かおかしいのか、は切り分けていません :bow: 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39615/40d27e0e-e86b-531e-c3d9-49e5e2bb15fa.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39615/873155f9-9d3f-cbf5-1eb3-d230d4bd8830.png)

 
# Daggerの現状

最後にDaggerの今後について簡単にご紹介します。以前Dagger Community Call[^3]があり、その中で最新の取り組みについて紹介がありました。以下の3つの（関連した）アクションに取り組む、Project Zenithと呼ばれるエンハンスがコミュニティで進められているとのことです。
[^4]: [Introduction to Project Zenith - YouTube](https://www.youtube.com/watch?v=Pt_AsbQilAM)

* Dagger CLIのUX改善
    * ShellやYAMLにより親しんでいる人向けに、Daggerをシェルスクリプトなどから直接使うためのCLIのUX向上に取り組んでいます（シェルからの利用に耐えられるようにインターフェースを拡充）
* Daggerのエントリーポイント（導入手順）の標準化推進
    * Daggerを使ってCI/CDパイプラインを作る場合、各ユーザーが独自のツールや設定やディレクトリ構成を使って利用しており、チームをまたいだCI/CDパイプライン資産の共有を難しくしている問題があります。様々なツールにこのCI/CDパイプラインを連携させる場合に、各チームはカスタムなDagger実装をフレキシブルに対応させているのが現状です。これがDaggerエコシステムの発展にも障壁になっていると捉えており、利用時の標準化を進めることでこれらの問題を解決しようとしています
* Dagger Extentionへの対応
    * 複雑なCIパイプラインをパッケージ化して、異なるチーム間、異なる開発言語間でそのパイプラインを使いまわせるようにするという対応です

Solomon HykesのProject Zenithの説明を見る限り、個人的には、現実装に対するフィードバックや得た知見を元に、実装を大きく見直すようなリブートのような扱いにProject Zenithがなると読み取れました。今後も要注目です。

Happy Dagger life! :smiley: