Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

CI/CD 파이프라인 엔진 Dagger

Dagger는 코드로 관리할 수 있는 CI/CD 파이프라인으로 Docker를 만든 Solomon Hykes가 2021년부터 시작한 프로젝트다.

Dagger 로고

Solomon Hykes가 Docker, Inc에서 물러나서 쉬고 난 이후 Docker 만들 당시 함께 했던 사람들과 새로운 프로젝트를 시작한다고 2021년 글을 올렸다. 당시에는 뭘 만드는지도 알려주지 않았지만, 관심 있는사람은 저장소에 접근하게 해준다고 손을 들었더니 Dagger에 접근할 수 있는 권한을 주어 Solomon Hykes가 만드는 새 프로젝트가 CI/CD 파이프라인이라는 것을 알게 되었다.(지난 2년간 업무로 배포 시스템을 만들고 있어서 더 관심이 간 것도 있다.)

이렇게 만든 프로젝트를 2022년 3월 정식으로 공개했다. 공개 당시 BuildKitCUE 기반으로 만들어졌지만, 지금은 Dagger 엔진을 놓고 GraphQL API를 이용해서 Go, Node.js, Python, CUE 언어로 사용할 수 있도록 지원하고 있다.

Dagger

Dagger는 CI/CD 파이프라인이다. 쉽게 말하면 GitHub ActionsCircleCI 등에서 코드 검사를 하거나 테스트를 돌리고 빌드나 배포를 수행하는 자동화 파이프라인은 Dagger를 이용해서 만들 수 있다.

Dagger의 몇 가지 특징이 있는데 정의한 모든 파이프라인은 OCI 컨테이너 이미지로 동작한다. 그래서 Docker 같은 OCI 컨테이너 호환 런타임만 있다면 어디서나 돌릴 수 있고 Dagger의 장점 중 많은 부분인 이식성, 호환성 등은 모두 여기에서 나온다고 생각한다. CI/CD를 돌리는 경우 로컬에서는 잘 되는데 CI/CD에서만 잘 안되는 경우가 있는데 Dagger 같은 경우는 파이프라인을 로컬에서 사용할 수 있으므로(다른 말로 하면 로컬 개발에서도 Dagger를 그대로 사용할 수 있으므로) 이런 문제를 최소화할 수 있다. 즉, 로컬에서 개발할 때 필요한 파이프라인도 CI랑 똑같이 쓰는 걸 권장한다고 생각한다. 이렇게 하면 로컬의 작업을 업데이트하면서 CI 쪽은 빠뜨린다거나 하는 실수도 방지할 수 있고 내가 가장 관심 있는 부분이기도 하다.

사용자 삽입 이미지

위 그림처럼 일반적으로 Docker로 생각할 수 있는 OCI 런타임이 있고 모든 파이프라인은 이 런타임 위에서 동작한다. 그리고 이를 제어하는 Dagger Engine이 있고 SDK에서 자신이 편한 언어로 이 Dagger Engine을 제어하는 구조로 되어 있다. SDK를 사용할 때 프로그램이 Dagger 엔진에 새로운 세션을 열거나 기존 세션에 접속한다. 엔진과 통신할 때는 wire 프로토콜을 사용한다고 하는데 이 wire 프로토콜은 아직 문서화되어 있지 않고 비공개인 상태인데 나중에는 달라질 수 있다고 한다. 지금은 프로그램이 사용할 수 있는 SDK의 API만 문서화 되어 있다.

SDK는 파이프라인을 정의하는 API 요청을 엔진에 보내고 엔진을 결과를 계산할 때 필요한 Directed Acyclic Graph(DAG)를 만들어서 작업을 처리한다. 파이프라인의 모든 작업이 완료되면 그 결과를 SDK에 다시 보내주고 SDK는 파이프라인의 결과를 다른 파이프라인의 입력으로 사용한다고 한다.

Dagger CUE SDK

CUE SDK는 구성 언어(Configuration Language)인 CUE를 사용하는 SDK이다.(CUE에 대해서는 이전 글을 참고) 이전에 CUE에 관한 글을 썼던 이유도 Dagger를 보기 위해서였다. 띄엄띄엄 보느라 시간이 오래 걸리긴 했지만, Dagger 문서를 계속 공부하고 있었는데 그사이에 SDK를 만들어서 공개하더니 구조가 약간 달라지었다. 그래서 Dagger를 쓸 때 내가 CUE로 사용하진 않을 것 같지만 CUE 쪽에 문서가 더 많기도 하고 현재 시점에서는 가장 처음 지원했던 언어라서 이 글에서도 CUE SDK를 먼저 살펴보려고 한다.

Dagger는 v0.3.x로 넘어오면서 SDK 방식으로 바뀌었는데 CUE는 아직 v0.2 방식에 머물러 있다. 관련 이슈를 보면 새로운 v0.3에 맞는 새로운 CUE SDK를 릴리스할 계획으로 보인다. 그래서 현재 v0.2.x와 관련된 문서를 보고 정리를 하고 있기 때문에 다른 언어의 SDK와는 방식이 약간 다를 수 있다. 그런데도 이미 보고 있던 거기도 하고 한번 정리해 두는 게 계속 보는 데 좋을 것같아서 정리해둔다.

CUE보다는 내가 편한 언어의 SDK를 쓸 가능성이 더 높아서 여기서는 간단히 CUE SDK를 살펴보고 후에 다시 다른 언어의 SDK를 보려고 한다. CUE의 사용법은 다른 SDK와는 꽤 다를 것이라는 점을 참고하기를 바란다.

Dagger CUE SDK 설치

설치는 OS별로 설치 문서를 참고하면 된다. 여기서는 macOS로 설치할 건데 나는 Homebrew 대신 curl을 이용해서 직접 설치했다.

$ curl -L https://dl.dagger.io/dagger-cue/install.sh | VERSION=0.2.232 sh

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  8679  100  8679    0     0   8833      0 --:--:-- --:--:-- --:--:--  8856
sh debug downloading files into /var/folders/xv/_43lttq56ss67_3x72vj9fsh0000gn/T/tmp.4d6rF4AE
sh debug http_download https://dagger-io.s3.amazonaws.com/dagger-cue/releases/0.2.232/dagger-cue_v0.2.232_darwin_arm64.tar.gz
sh debug http_download https://dagger-io.s3.amazonaws.com/dagger-cue/releases/0.2.232/checksums.txt
sh debug display shell completion instructions

dagger-cue has built-in shell completion. This is how you can install it for:

  BASH:

    1. Ensure that you install bash-completion using your package manager.

    2. Add dagger completion to your personal bash completions dir

      mkdir -p /bash-completion/completions
      dagger-cue completion bash > /bash-completion/completions/dagger

  ZSH:

    1. Generate a _dagger completion script and write it to a file within your $FPATH, e.g.:

      dagger-cue completion zsh > /usr/local/share/zsh/site-functions/_dagger

    2. Ensure that the following is present in your ~/.zshrc:

      autoload -U compinit
      compinit -i

    zsh version 5.7 or later is recommended.

  FISH:

    1. Generate a dagger.fish completion script and write it to a file within fish completions, e.g.:

      dagger-cue completion fish > ~/.config/fish/completions/dagger.fish

sh info installed ./bin/dagger-cue

현재 폴더에 ./bin/dagger-cue가 설치되므로 PATH 환경변수에 이를 추가하면 된다.

$ dagger-cue version
dagger 0.2.232 (cbcb5061e) darwin/arm64


로컬 파이프라인의 사용

Dagger에서 예시로 사용할 수 있는 정적 사이트인 todoapp을 제공하고 있다. 이 저장소를 클론 받은 뒤에 의존성 설치를 위해 dagger-cue project update를 실행한다. dagger-cue project 명령어는 프로젝트를 관리하는 명령어이고 project update는 의존성을 다운로드 받아서 설치한다.

$ dagger-cue project update
8:03PM INFO  system | installing all packages...
8:03PM INFO  system | installed/updated package dagger.io@0.2.232
8:03PM INFO  system | installed/updated package universe.dagger.io@0.2.232

예시 프로젝트에 이미 dagger.cue가 정의되어 있으므로 빌드를 바로 실행해보자. 로컬에 Docker가 실행되어 있지 않으면 실행할 때 오류가 발생한다. 앞에서 Dagger 엔진이 있다고 설명했는데 이 SDK에 Dagger 엔진이 포함되어 있으므로 따로 실행하거나 하진 않아도 된다.

$ dagger-cue do build
[✔] actions.build.container                  41.1s
[✔] actions.source                            0.2s
[✔] actions.build.install.container.script    0.0s
[✔] actions.build.install.container          35.3s
[✔] actions.build.container.script            0.0s
[✔] actions.build.install.container.export    0.0s
[✔] actions.build.container.export            0.0s
[✔] client.filesystem."./build".write         0.1s
Field  Value
logs   """\n  yarn run v1.22.17\n  $ react-scripts build\n  Creating an optimized production build...\n  Compiled with warnings.\n\n  ./src/App.js\n    Line 110:7:  The element ul has an implicit role of list. Defining this explicitly is redundant and should be avoided  jsx-a11y/no-redundant-roles\n\n  Search for the keywords to learn more about each warning.\n  To ignore, add // eslint-disable-next-line to the line before.\n\n  File sizes after gzip:\n\n    40.12 KB  build/static/js/2.a228fa91.chunk.js\n    1.55 KB   build/static/js/main.af3de9df.chunk.js\n    1.46 KB   build/static/css/main.9149988f.chunk.css\n    782 B     build/static/js/runtime-main.89495321.js\n\n  The project was built assuming it is hosted at ./.\n  You can control this with the homepage field in your package.json.\n\n  The build folder is ready to be deployed.\n\n  Find out more about deployment here:\n\n    bit.ly/CRA-deploy\n\n  Done in 5.19s.\n\n  """

마지막 로그는 좀 빌드 중에 나온 로그로 보이는데 버그인지 좀 보기 어렵게 출력된다. 그래도 dagger-cue do build는 잘 실행된 것을 알 수 있고 내 맥을 기준으로는 76초 정도가 걸렸다. 마지막 client.filesystem."./build".write를 보면 ./build 폴더에 빌드 결과가 작성된 것을 알 수 있는데 정적 웹사이트이므로 빌드된 결과 파일이 이 폴더에 생성된다.

$ python -m http.server -d build
Serving HTTP on :: port 8000 (http://[::]:8000/) ...

python으로 웹서버를 실행하면서(위 명령어는 python 3에서 동작한다.) build 디렉터리를 루트 디렉터리로 지정하면 아래와 같은 웹사이트를 볼 수 있다.

todoapp 화면

특별한 작업을 하지 않아도 Dagger가 캐싱해주기 때문에 빌드를 다시 실행하면 아까와는 달리 2초도 안 걸리는 걸 볼 수 있다.

$ dagger-cue do build
[✔] actions.build.container                    0.9s
[✔] actions.build.install.container            0.7s
[✔] actions.source                             0.0s
[✔] actions.build.install.container.script     0.0s
[✔] actions.build.container.script             0.0s
[✔] actions.build.install.container.export     0.0s
[✔] actions.build.container.export             0.0s
[✔] client.filesystem."./build".write          0.1s
Field  Value
logs   """\n  yarn run v1.22.17\n  $ react-scripts build\n  Creating an optimized production build...\n  Compiled with warnings.\n\n  ./src/App.js\n    Line 110:7:  The element ul has an implicit role of list. Defining this explicitly is redundant and should be avoided  jsx-a11y/no-redundant-roles\n\n  Search for the keywords to learn more about each warning.\n  To ignore, add // eslint-disable-next-line to the line before.\n\n  File sizes after gzip:\n\n    40.12 KB  build/static/js/2.a228fa91.chunk.js\n    1.55 KB   build/static/js/main.af3de9df.chunk.js\n    1.46 KB   build/static/css/main.9149988f.chunk.css\n    782 B     build/static/js/runtime-main.89495321.js\n\n  The project was built assuming it is hosted at ./.\n  You can control this with the homepage field in your package.json.\n\n  The build folder is ready to be deployed.\n\n  Find out more about deployment here:\n\n    bit.ly/CRA-deploy\n\n  Done in 5.19s.\n\n  """


플랜과 액션

간단한 데모를 실행해 봤으니 좀 더 자세히 살펴보자. 예제 프로젝트에는 dagger.cue라는 파일이 있고 여기에서 파이프라인이 정의되어 있다. .cue 파일은 로딩이 되므로 파일명이 dagger.cue일 필요는 없고 옆에 localdev.cue 파일도 존재해서 서로 참조할 수 있다. 이 파일에서 플랜과 액션을 정의하게 된다.

// dagger.cue
package todoapp

import (
  "dagger.io/dagger"

  "dagger.io/dagger/core"
  "universe.dagger.io/netlify"
  "universe.dagger.io/yarn"
)

dagger.#Plan & {
  actions: {
    // Load the todoapp source code 
    source: core.#Source & {
      path: "."
      exclude: [
        "node_modules",
        "build",
        "*.cue",
        "*.md",
        ".git",
      ]
    }

    // Build todoapp
    build: yarn.#Script & {
      name:   "build"
      source: actions.source.output
    }

    // Test todoapp
    test: yarn.#Script & {
      name:   "test"
      source: actions.source.output

      // This environment variable disables watch mode
      // in "react-scripts test".
      // We don't set it for all commands, because it causes warnings
      // to be treated as fatal errors.
      // See https://create-react-app.dev/docs/advanced-configuration
      container: env: CI: "true"
    }

    // Deploy todoapp
    deploy: netlify.#Deploy & {
      contents: actions.build.output
      site:     string | *"dagger-todoapp"
    }
  }
}

위 파일을 보면 플랜은 dagger.#Plan으로 시작하고 이 안에 actions로 여러 액션이 정의되어 있다. 위에서는 source, build, test, deploy가 있는데 앞에서 dagger-cue do build를 했을 때 여기에 정의된 build 액션이 실행된 것이다. 즉, 다른 액션도 다음과 같이 실행할 수 있다는 의미이다.

$ dagger-cue do source
[✔] actions.source    0.0s

dagger-cue project info 명령어로도 이 액션 목록을 확인할 수 있다.

$ dagger-cue project info

Current dagger project in: /Users/outsider/demo/todoapp
+--------+------------------------------+
| ACTION | DESCRIPTION                  |
+--------+------------------------------+
| source | Load the todoapp source code |
| build  | Build todoapp                |
| test   | Test todoapp                 |
| deploy | Deploy todoapp               |
+--------+------------------------------+

이 CUE 파일의 사용법을 자세히 보기 위해서 아래처럼 세부 내용은 없애고 구조만 보면 다음과 같다.

// dagger.cue
package todoapp

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

dagger.#Plan & {
  actions: {
    source: core.#Source & {}
    build: yarn.#Script & {}
    test: yarn.#Script & {}
    deploy: netlify.#Deploy & {}
  }
}

package todoapp로 패키지를 선언해서 시작한다.

플랜에서 사용할 패키지를 임포트한다. 패키지는 dagger 저장소에서 확인할 수 있는데 dagger.io/dagger 패키지에 기반이 플랜 등 기본적인 패키지가 있고 dagger.io/dagger/core의 내용은 문서에서 확인할 수 있다. universe.dagger.io에는 여러 생태계를 지원하기 위한 패키지가 포함되어 있다.(앞에서 설명한 대로 이 CUE 패키지는 v0.2.x 릴리스에만 존재하므로 차후 사용 방법이 달라질 수 있다.)

이후 선언에서 사용하는 dagger.#Plan, core.#Source, yarn.#Script, netlify.#Deploy는 이 패키지 임포트로 사용할 수 있게 된 것이다.

플랜은 dagger.#Plan로 시작하는데 여기서는 client 필드를 이용해서 클라이언트와 상호작용할 수 있다. 예를 들어 파일을 읽고 쓰거나 환경변수를 읽거나 소켓을 사용하는 등의 동작을 할 수 있다.

acitons에서는 작 액션을 정의하고 아래의 deploy 액션에서 actions.build.output을 사용한 것처럼 다른 정의를 참조해서 파이프라인을 더 강력하게 정의할 수 있다.

deploy: netlify.#Deploy & {
  contents: actions.build.output
  site:     string | *"dagger-todoapp"
}

액션을 좀 더 살펴보면 앞에서 보았듯이 액션은 dagger-cue do로 실행할 수 있는데 액션에는 Core 액션과 Composite 액션이 있다. Core 액션은 앞에서 임포트했던 dagger.io/dagger/core에 정의된 액션이고 문서에서 보듯이 파일시스템, 시크릿, 컨테이너 이미지, git, http 등의 기본 동작을 지원한다. Composite 액션은 이름대로 다른 액션을 조합해서 만든 액션이다.

액션의 생명주기는 정의(Definition), 통합(Integration), 탐색(Discovery), 실행(Execution)의 4가지 단계를 가진다.

정의(Definition)

CUE로 액션을 정의하는 과정으로 입력과 출력과 하위액션을 어떻게 연결하는지 정의한다. 다음은 간단한 액션의 정의다.

package main

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

// Write a greeting to a file, and add it to a directory
#AddHello: {
    // The input directory
    dir: dagger.#FS

    // The name of the person to greet
    name: string | *"world"

    write: core.#WriteFile & {
        input: dir
        path: "hello-\(name).txt"
        contents: "hello, \(name)!"
    }

    // The directory with greeting message added
    result: write.output
}

여기서 dirname은 입력으로 외부의 값을 받을 수 있고 write는 다른 액션의 정의를 담고 있는 하위 액션이고 result는 외부에서 참조할 수 있는 출력이다.

통합(Integration)

정의를 실행하려면 플랜으로 통합되어야 하는데 다음은 위 #AddHello 정의를 통합한 예시이다.

package main

import (
    "dagger.io/dagger"
)

dagger.#Plan & {
    // Say hello by writing to a file
    actions: hello: #AddHello & {
        dir: client.filesystem.".".read.contents
    }
    client: filesystem: ".": {
        read: contents: dagger.#FS
        write: contents: actions.hello.result
    }
}


탐색(Discovery)

플랜으로 통합되면 사용자가 dagger-cue do --help 명령어로 액션을 찾을 수 있다.

$ dagger-cue do --help
Usage:
  dagger-cue do <action> [subaction...] [flags]

Options


Available Actions:
 hello Say hello by writing to a file

이 결과를 보려면 앞의 정의와 통합에서 설명한 두 파일을 저장한 뒤 dagger-cue project initdagger-cue project update를 실행해야 한다.

실행(Execution)

이 액션을 dagger-cue do로 실행할 수 있다.

$ dagger-cue do hello
[✔] client.filesystem.".".read   0.1s
[✔] actions.hello.write          0.1s
[✔] client.filesystem.".".write  0.1s


원격 CI에서의 실행

당연히 OCI 컨테이너 런타임만 있으면 어디서나 실행할 수 있으므로 CI 서비스에서도 사용할 수 있다. Dagger 문서를 보면 GitHub Actions, TravisCI, CircleCI, GitLab, Jenkins, Tecton, AzurePipeline에서 사용할 수 있는 예제를 제공하고 있다.

GitHub Actions같은 경우 Docker를 지원하기 때문에 Dagger에서 지원하는 dagger-for-github을 사용해서 바로 사용할 수 있다. Docker를 지원하는 않은 CI의 경우에는 Docker를 따로 실행하도록 지정해야 한다.

name: todoapp

on:
  push:
    # Trigger this workflow only on commits pushed to the main branch
    branches:
      - main

# Dagger plan gets configured via client environment variables
env:
  # This needs to be unique across all of netlify.app
  APP_NAME: todoapp-dagger-europa
  NETLIFY_TEAM: dagger

jobs:
  dagger:
    runs-on: ubuntu-latest
    steps:
      - name: Clone repository
        uses: actions/checkout@v2

      # You need to run `dagger-cue project init` locally before and commit the cue.mod directory to the repository with its contents
      - name: Deploy to Netlify
        uses: dagger/dagger-for-github@v3
        # See all options at https://github.com/dagger/dagger-for-github
        with:
          version: 0.2
          # To pin external dependencies, you can use `project update github.com/[package-source]@v[n]`
          cmds: |
            project update
            do deploy
        env:
          # Get one from https://app.netlify.com/user/applications/personal
          NETLIFY_TOKEN: ${{ secrets.NETLIFY_TOKEN }}

Solomon Hykes가 만들었다고 Docker처럼 Dagger의 영향력이 있다고 생각하진 않는다. 하지만 꽤 흥미로운 접근이라고 생각하고 최근 CD 파이프라인을 만들고 있으면서 인사이트를 얻을 수 있는 부분이 많다고 생각하며 보고 있다.

2022/12/29 00:55 2022/12/29 00:55