電通総研 テックブログ

電通総研が運営する技術ブログ

Next.jsのローカルとGitHub Actionsで Daggerを実行してみた

こんにちは。Xイノベーション本部ソフトウェアデザインセンターの陳です。
CI推進活動の一環として、話題のCI/CDツールDaggerを使ってみました。
この記事では、DaggerでNext.jsプロジェクトのCIを構築して、ローカルとGitHub Actionsで実行する方法について紹介します。

Daggerについて

みなさんはどのCI/CDツールを使っていますか?私が所属する部署ではGitHub Actionsを使うことが多いです。
従来のCI/CDでは、GitHub ActionsやCircle CIなどのサービスごとにCI/CDスクリプトを作成する必要があります。 移行するときには移行先のサービスに合わせてスクリプトを書き換えないので、大変なことになります。

一方、Daggerを使えばCI/CDスクリプトをポータブルにできます。
Daggerとは、特定の基盤を依存せずに、CI/CDパイプラインを素早く構築しどこでも実行できるツールです。
Daggerを利用して作成したCI/CDスクリプトは、GitHub ActionsやCircle CIなど様々なサービスで共通に使えます。
Dagger はDockerの創始者のSolomon Hykesが立ちあげた会社です。DaggerはDockerコンテナ上で動作します。 CI/CDスクリプトの設定ファイルはCUE言語で記述します。

環境構築

今回はNext.js+TypeScriptのプロジェクトを使ってDaggerでCIを構築してみました。
ここからは環境構築の手順をまとめます。
まずは公式ドキュメントに従って、Daggerをインストールします。 私はmacOSを使っています。Homebrewを使って簡単にインストールできました。

$ brew install dagger/tap/dagger

WindowsLinuxのインストール方法は公式ドキュメントに記載されていますので、ここでは省略します。 こちらのコマンドでバージョンおよびインストールされた場所を確認できます。

$ dagger version
$ type dagger

続いて、プロジェクトのルートディレクトリで以下のコマンドを実行し、Daggerを初期化します。

$ dagger project init

ルートディレクトリでcue.modのフォルダが作成されます。 以下のコマンドを実行してDaggerのパッケージをインストールします。

$ dagger project update

pkgフォルダーに以下の2つのパッケージが作成されました。 - dagger.io - Daggerエンジン本体を動かすためのアクション(core actions)が実装されたパッケージ - universe.dagger.io - bash、yarn、go、PythonなどDaggerから提供された複合アクションが実装されたパッケージ

また、Daggerの全ての動作はDockerコンテナ上で実行されるため、Docker DesktopなどのDockerを実行できる環境のセットアップも必要です。 上記の手順でDaggerを利用できるようになりました。

ローカルでDaggerを実行してみた

dagger.cueファイルの作成

DaggerでCIを動かすためには、以下のようにCUE言語でCIスクリプトを作成する必要があります。

package main

import (
    "dagger.io/dagger"
    "dagger.io/dagger/core"
    "universe.dagger.io/bash"
    "universe.dagger.io/docker"
    "universe.dagger.io/netlify"
)

dagger.#Plan & {
    client: {
        filesystem: {
          // ...
        }
        env: {
          // ...
        }
    }

    actions: {
        deps: docker.#Build & {
          // ...
        }
        test: bash.#Run & {
          // ...
        }
        build: {
            run: bash.#Run & {
              // ...
            }
            contents: core.#Subdir & {
              // ...
            }
        }
        deploy: netlify.#Deploy & {
          // ...
        }
    }
}

dagger.#Planの中でactionsを宣言し、actionsの中でビルドやテストなどのアクションを宣言できます。
actions以外にもclientdagger.#Planの中で宣言し、ローカルファイルの読み取りや書き込み、環境変数の参照などができます。詳細はこちらを参照してください。 アクションの実行は以下のコマンドを使います。

$ dagger do {アクション名}

今回は公式のサンプルを参考に以下のCIスクリプトを作成しました。
ルートディレクトリでdagger.cueファイルを作成します。

//dagger.cue

package main

import (
    "dagger.io/dagger"
    "dagger.io/dagger/core"
    "universe.dagger.io/yarn"
)

dagger.#Plan & {
    actions: {
        checkoutCode: core.#Source & {
            path: "."
            exclude: [
                "node_modules",
                ".next",
                ".swc",
                "*.cue",
                "*.md",
                ".git",
            ]
        }

        build: {
            install: yarn.#Install & {
                source: checkoutCode.output
            }
            build: yarn.#Script & {
                source: checkoutCode.output
                name:   "build"
            }
            test: yarn.#Script & {
                source: checkoutCode.output
                name:   "test"
            }
        }
    }
}

Daggerのuniverseのyarnパッケージを使って、ビルドとテストのアクションをbuildに定義しました。以下のコマンドを実行すると、インストール、ビルド、テストのアクションが順番に実行されます。

$ dagger do build

以下のログが表示され、ビルドおよびテストが完了しました。

Jestのエラー

testactionsの中で直接に宣言しテストを動かすと、jestの以下のエラーが発生しました。

Field  Value
logs   """\n  yarn run v1.22.17\n  $ jest --maxWorkers=1\n  jest-haste-map: Haste module naming collision: test\n    The following files share their name; please adjust your hasteImpl:\n      * <rootDir>/cue.mod/pkg/universe.dagger.io/yarn/test/data/foo/package.json\n      * <rootDir>/cue.mod/pkg/universe.dagger.io/yarn/test/data/bar/package.json\n\n  Done in 6.81s.\n\n  """

jest.config.jsに以下の設定を追加するとこのエラーを解消できました。

//jest.config.js

const customJestConfig = {
  modulePathIgnorePatterns: ["<rootDir>/cue.mod"],
};

ローカルでアプリケーションを動かしてみた

Next.jsではnext devのコマンドを実行することで、開発モードでアプリケーションをローカルで起動できます。daggerを利用する場合は、actionsに以下のコードを追加します。

//dagger.cue

dagger.#Plan & {
    actions: {
        checkoutCode: core.#Source & {
            path: "."
        }
        dev: yarn.#Script & {
            source: checkoutCode.output
            name:   "dev"
        }
    }
}    

dagger do devコマンドを実行すると、localhost:3000でアプリケーションを確認できます。 ただし、daggerの全ての動作はDockerコンテナ上で実行されるため、コードを変更したらdagger do devを再実行(つまりコンテナを更新)しないと反映されません。

universeパッケージのyarnとbash

Daggerでyarnのアクションを実行する際に、universeパッケージのuniverse.dagger.io/yarn以外にも、universe.dagger.io/bashを利用してシェルスクリプトで実行できます。
例えば、インストール、ビルド、テストを順番に実行させたい場合は以下のactionsを宣言します。

//dagger.cue

package main

import (
    "dagger.io/dagger"
    "dagger.io/dagger/core"
    "universe.dagger.io/bash"
)

dagger.#Plan & {
    actions: {
        checkoutCode: core.#Source & {
            path: "."
        }

        build: {
            //yarnとbashがインストールされたイメージをpullする
            pull: docker.#Pull & {
                source: "node:lts"
            }
            //コンテナのfilesystemにファイルをコピーする
            copy: docker.#Copy & {
                input:    pull.output
                contents: checkoutCode.output
            }
            //コンテナでbashスクリプトを実行する
            build: bash.#Run & {
                input: copy.output
                script: contents: """
                    yarn install --frozen-lockfile
                    yarn run build
                    yarn run test
                    """
            }
        }
    }
}

ただし、bashyarnより実行時間が長いです。ローカルでくインストール、ビルド、テストのアクションを行してみた結果、yarnの実行時間はbashより1分以上も早くなります。

  • bash 1分35秒

  • yarn 13秒

bashyarn両方が使えるようでしたら、より実行時間の短いyarnを利用したほうがよさそうですね。 npmを利用したい場合は、yarnのような直接に使える公式パッケージが実装されていないので、bashを使います。 詳細は公式のサンプルを参照してください。

既存のDockerfileの実行もできる

以下のように、unversedockerパッケージを利用すれば、外部のDockerfileを読み込んで実行できます。
またDockerfileの内容をdocker.#Dockerfile & {}にも記述できます。詳細はこちらをご参照ください。

//dagger.cue
package main

import (
    "dagger.io/dagger"
    "dagger.io/dagger/core"
    "universe.dagger.io/docker"
)
dagger.#Plan & {
    actions: {
        checkoutCode: core.#Source & {
            path: "."
        }
        build: docker.#Dockerfile & {
            source: checkoutCode.output
        }
    }
}    

GitHub ActionsでDaggerを実行してみた

ルートディレクトリで.github/workflowsフォルダを作成し、以下のようにyamlファイルにGitHub Actionsの設定を記述します。pushをトリガーにGitHub Actionsを走らせ、アプリケーションのビルドおよびテストを実行させます。

//build-test-on-push.yml
on:
  push:
name: Build and Test on Push
jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Build and Test
        uses: dagger/dagger-for-github@v3
        with:
          cmds: |
            project update
            do build

dagger/dagger-for-githubを利用して、dagger.cueに定義されたアクションをGitHub Actions上で簡単に動かせます。 従来であれば、GitHub Actions上にNode.jsをインストールして、yarn installやビルド、テストそれぞれの設定をしないといけないです。Daggerを利用する場合は、既存のCUEファイルをGitHub Actions上に実行させればOKなので、GitHub Actionsのyamlファイルもだいぶスッキリします。

使ってみた感想

CIスクリプトをローカルとGitHub Actionsで同じように実行できるのは便利ですね。ローカルで通ったテストをGitHub Actionsで実行するとなぜか通らなくなった、みたいなことも回避できそうです。また、Daggerを使うと、yamlファイルで頑張ってCIスクリプトを書かなくていいので、GitHub Actionsでの設定もだいぶ簡単になります。
今回はGitHub ActionsでDaggerを試してみましたが、他にもCircle CI、GitLab、Jenkinsなど多数のCIサービスに対応しています。サービス間の移行をより簡単に実現できるので、他のCIサービスでもDaggerを使ってみたいですね。
ただし、使いづらいところもあります。GitHub Actionsなどの設定が簡単になりますが、CUE言語やDaggerのパッケージの使い方を理解しないとDaggerの利用が難しく感じます。また現時点で、Daggerの公式サイトにあるuniverseパッケージに関するドキュメントはあまり詳しくなく、複雑なCI/CDを構築したい場合はコードを読んで試行錯誤を重ねる必要があるかもしれません。

まとめ

この記事では、DaggerでNext.jsプロジェクトのCIを動かしてみたことについてまとめました。
Daggerを使って基盤に依存しないポータブルなCIを簡単に構築できます。みなさんも是非使ってみてください。


私たちは同じチームで働いてくれる仲間を探しています。今回のエントリで紹介したような仕事に興味のある方、ご応募お待ちしています。

執筆:@chen.xinying、レビュー:@yamashita.tsuyoshiShodoで執筆されました