🤖

OSS ワークフローツール Concourse を使ってみる

2023/08/11に公開

CI/CD 開発を実現するための OSS のワークフローツールは世の中に様々ありますが、今回はその中の一つである concourse というツールを使ってみます。

Concourse について

concourse は CI/CD 開発の自動化を実現するためのシンプルな pipeline 実行ツールです。CNCF cloud native project のうち platinum member として位置づけられており、Github のスター数は現時点において 7k となっています。

concourse の特徴は公式サイトトップページの Key Feature にまとまっていますが、個人的に以下の部分がメリットであると感じました。

  • Github workflow に近い概念で pipeline を定義できる
    • Concourse では job, step, task といった実行単位で pipeline を定義します。これらは Github workflow や Gitlab workflow における job, step 等に近い概念となっているので、既にそれらに慣れ親しんでいる人にとってはすぐに使えるかと思います。
    • それほど複雑な概念でもないため、あまり馴染みのない人でも使っていくうちに使用感は掴めてくると思います。
  • yaml 形式での記述
    • ワークフローは yaml 形式で記述します。独自の記述形式を使う他の OSS ワークフローツールと比較すると学習コストが低くすぐに使えます。
  • Web UI でワークフローの依存関係が見える
    • 簡素ではありますが web UI が備え付けられており、作成したワークフローの依存関係や実行順を視覚的に確認できます。
  • 管理するコンポーネントが少ない
    • concourse は大別して web, worker, データベースの 3 つのコンポーネントから構成されます。一般にコンポーネント数が増えるとそれだけ管理の煩雑さが増すため、コンポーネントが少ないということは管理が容易になるというメリットとなり得ます。

Project - Why make Concourse? で以下のような記述があり、CI/CD に必要な機能を十分に提供しつつ複雑になりすぎない設計を理念としているように感じます。

We built Concourse to be a CI system that lets you sleep easier at night. A CI that's simple enough to fully grok and easy to manage as your project grows; in both the complexity of the product and the size of your team. We wanted to build a CI with strong abstractions and fewer things to learn, so that it can be easier to understand and so that Concourse can age gracefully.

インストール

さっそく Concourse を使っていきます。
OS は ubuntu 22.04 を使用しました。concourse 自体は docker コンテナで動作するので、OS が影響するのは CLI の fly (後述) をインストールする部分ぐらいかと思います。

concourse のインストール

concourse は Install に書いてあるように web node, worker node, postgresql (データベース) のコンポーネントから構成されます。本番環境での運用は複数台の worker node を立てて冗長性を考量したりする必要がありますが、今回は手っ取り早く使用感を試すので docker-compose を使ってインストールします。

docker-compose を使ったインストールは Quick Start に手順が書いてあります。まずは docker-compose.yml をダウンロードします。

$ curl -O https://concourse-ci.org/docker-compose.yml

外部から web UI にアクセスできるようにするため、docker-compose.yml 内の CONCOURSE_EXTERNAL_URL をこのマシンの IP アドレス (192.168.3.52) に設定しておきます。

docker-compose.yml
-      CONCOURSE_EXTERNAL_URL: http://localhost:8080
+      CONCOURSE_EXTERNAL_URL: http://192.168.3.52:8080

docker compose を実行すると postgresql コンテナと web node, worker node が一体化した concourse コンテナの 2 つが起動します。

$ docker ps
CONTAINER ID   IMAGE                 COMMAND                  CREATED        STATUS        PORTS                                       NAMES
f3031a2f3400   concourse/concourse   "dumb-init /usr/loca…"   26 hours ago   Up 26 hours   0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   concourse-concourse-1
45d2642ee8d7   postgres              "docker-entrypoint.s…"   26 hours ago   Up 26 hours   5432/tcp                                    concourse-concourse-db-1

ブラウザで http://192.168.3.52:8080 を開き、 docker-compose.yml に記述したユーザ名とパスワード (デフォルトでは test, test) でログインすると web UI にアクセスできます。

fly のインストール

fly は pipeline の登録や実行など concourse に関する操作を行う CLI ツールです。インストールは手順に書いてあるようにコンテナで起動した concourse からのダウンロード、もしくは concourse の Github release から直接ダウンロードすることが出来ます。

$ curl 'http://localhost:8080/api/v1/cli?arch=amd64&platform=linux' -o fly \
    && sudo chmod +x ./fly && sudo mv ./fly /usr/local/bin/

fly completion でコマンド補完を有効化しておくと便利。

# bash
source <(fly completion --shell bash)

# zsh
source <(fly completion --shell zsh)

機能

concourse における実行単位

pipeline 作成に入る前に、concourse における作業の実行単位について簡単に説明します。
concourse における実行単位は以下のような階層構造から成り立っています。

pipeline
└── job
    └── step
        └── task

concourse の中で最小の実行単位となっている task から順に説明します。

task

  • concourse の中で実行される最小の実行単位。
  • 例えば ls -l でファイルを参照する、python で main.py を実行するなど、基本的にシェルで実行する1 つのコマンドに対応。複数のコマンドをまとめて実行することもできる。
  • task はそれぞれ独立したコンテナで実行される。これが Github workflow 等との大きな違い。したがって yaml 内では task 毎に使用するコンテナイメージを指定する。
    • python ファイルを実行する場合は python が実行可能なイメージを指定するなど、task 内での実行コマンドに合わせたイメージを選択する必要がある。
    • イメージは docker hub にあるものや private registry にある独自のイメージを使用できる。
    • task 毎に独立した環境で実行するという仕組みは同じ OSS ワークフローツールであるtekton に近い構成になっている。

step

  • step は job の中で何を実行するかを定義するアクションのまとまりのようなもの。
  • 例えば get step は github や remote ソースからファイル等をダウンロードしてくる、put step は task 内で生成したファイル等を他の task で参照できるようにするなどがある。

job

  • job は複数の step のまとまり。github workflow における job とだいたい同じ。
  • 例えば github からソースを取得、ソースからコンテナイメージをビルド、レジストリに push するような一連の動作を行いたい場合、以下の step を組み合わせて 1 つの job として定義する。
    • get step: Github からソースを取得
    • task1 step: コンテナイメージをビルドするコマンドを実行
    • task2 step: レジストリにイメージを push するコマンドを実行

pipeline

  • 1 つ、および複数の job から構成される一連のワークフローのようなもの。

厳密には異なる部分もありますが、github workflow における key 名と比較するとだいたい以下のように対応しています。

Concourse Github workflow
pipeline (1 つのワークフローファイル)
job jobs
step steps
task run

使ってみる

concourse を使って pipeline の作成、実行を行います。

pipeline の作成・実行

あまり面白みはありませんが、ドキュメント Hello World Pipeline に書かれているチュートリアル用のサンプル pipeline を実行してみます。

pipeline は 1つの yaml ファイルに対応しているので、pipeline の中でどのような job を実行するかを yaml で定義します。

hello-world.yml
jobs:
- name: hello-world-job
  plan:
  - task: hello-world-task
    config:
      # Tells Concourse which type of worker this task should run on
      platform: linux
      # This is one way of telling Concourse which container image to use for a
      # task. We'll explain this more when talking about resources
      image_resource:
        type: registry-image
        source:
          repository: busybox # images are pulled from docker hub by default
      # The command Concourse will run inside the container
      # echo "Hello world!"
      run:
        path: echo
        args:
          - "Hello world!"

これは busybox コンテナ環境を作成し、その中で echo "Hello world!" を実行するというサンプルコードになっています。

pipeline の登録から実行は全て fly で実行します。
まずログイン。

fly -t tutorial login -c http://192.168.3.52:8080 -u test -p test

この pipeline を hello-world という名前で登録。

fly -t tutorial set-pipeline -p hello-world -c hello-world.yml

pipeline を実行可能にする。

$ fly -t tutorial unpause-pipeline -p hello-world

pipeline を実行。

$ fly -t tutorial trigger-job --job hello-world/hello-world-job --watch
started hello-world/hello-world-job #1

initializing
initializing check: image
selected worker: f3031a2f3400
selected worker: f3031a2f3400
INFO: found existing resource cache

selected worker: f3031a2f3400
running echo Hello world!
Hello world!
succeeded

Hello world! がコンテナ内で実行されたコマンドの出力結果で、他は concourse pipeline 実行時の system message となっています。
上記の例で selected worker: f3031a2f3400 とありますが、今回は worker node を 1 つしか立てていないのでこの node で上記のコンテナが実行されました、worker node が複数個ある場合はタスク毎に node が選択されるかと思います。

task 間でのファイルの共有

前述の通りそれぞれの task は独立したコンテナとして実行されるため、そのままでは前の task で作成したファイルを次の task で参照することができません。task 間でファイルとを共有するには task スキーマの 1 つである inputs, outputs を使用します。これらにより task 内で作成されるファイル等 (concourse 内では artifacts と呼ばれる) を他の task で参照することができます。

outputsconfig 以下で指定します。以下のように name を設定すると、このタスクが実行される前に name に指定したディレクトリがコンテナ内に作成されます。デフォルトでは name に対応するディレクトリが作成されますが、path で別のディレクトリを指定することもできます。
outputs を設定した上でファイルをディレクトリ内に mv 等する処理を書くことで、この task 内で作成したファイルを他のタスクで参照することができます。

- task: example
  config:
    outputs:
      - name: output
        path: output-dir # optional
    run:
      ...

他タスクの outputs によるファイルを参照するには inputs を使用します。inputs も outputs と同様に name を指定することで対応するディレクトリを作成し、その中にあるファイルを参照することができます(もちろん他の task で outputs を定義しておく必要があります)。

- task: example
  config:
    inputs:
      - name: input
        path: input-dir # optional
    run:
      ...

試しに inputs, outputs でファイルを共有する以下のような pipeline を作成します。

jobs:
  - name: first step
    plan:
      - task: first
        config:
          platform: linux
          image_resource:
            type: registry-image
            source:
              repository: busybox
          outputs:
            - name: first-output
          run:
            path: touch
            args:
              - first-output/first.txt
      - task: second step
        config:
          platform: linux
          image_resource:
            type: registry-image
            source:
              repository: busybox
          inputs:
            - name: first-output
          outputs:
            - name: second-output
          run:
            path: sh
            args:
              - -c
              - |
                pwd
                tree
                touch second-output/second.txt
      - task: third step
        config:
          platform: linux
          image_resource:
            type: registry-image
            source:
              repository: busybox
          inputs:
            - name: first-output
            - name: second-output
          outputs:
            - name: third-output
          run:
            path: sh
            args:
              - -c
              - |
                pwd
                tree

この pipeline を実行した際の task の出力を順に見ていきます。
はじめに、second step のタスクによる出力結果は以下のようになりました。

/tmp/build/352f7829
.
├── first-output
│   └── first.txt
└── second-output

2 directories, 1 files
  • /tmp/build/352f7829 が作業ディレクトリ。
  • inputs に指定した first-output ディレクトリが作成されており、first step タスクで作成した first.txt が格納されている。
  • outputs に指定した second-output ディレクトリが作成されている。

次に、third step のタスクによる出力結果は以下のようになりました。

/tmp/build/34fb3300
.
├── first-output
│   └── first.txt
├── second-output
│   └── second.txt
└── third-output

3 directories, 2 files
  • /tmp/build/34fb3300 が作業ディレクトリ。
  • inputs に指定した first-output ディレクトリが作成されており、first step タスクで作成した first.txt が格納されている。
  • inputs に指定した second-output ディレクトリが作成されており、second step タスクで作成した second.txt が格納されている。
  • outputs に指定した third-output ディレクトリが作成されている。

pipeline 実行の出力全体は以下のようになります。

started multi-job/first #8

initializing
initializing check: image
selected worker: f3031a2f3400
selected worker: f3031a2f3400
INFO: found existing resource cache

selected worker: f3031a2f3400
running touch first-output/first.txt
initializing
initializing check: image
selected worker: f3031a2f3400
INFO: found existing resource cache

selected worker: f3031a2f3400
running sh -c pwd
tree
touch second-output/second.txt

/tmp/build/352f7829
.
├── first-output
│   └── first.txt
└── second-output

2 directories, 1 files
initializing
initializing check: image
selected worker: f3031a2f3400
INFO: found existing resource cache

selected worker: f3031a2f3400
running sh -c pwd
tree

/tmp/build/34fb3300
.
├── first-output
│   └── first.txt
├── second-output
│   └── second.txt
└── third-output

3 directories, 2 files
succeeded

このように inputs, outputs を使うことで task 間でのファイル共有が出来ることが確認できました。
一般的なワークフロー処理において、ある task 内で作成したファイル等を後続の処理で使用するというのはよくあるケースなので、inputs, outputs を使うことで簡単に実現できるというのは大きなメリットかと思います。

その他

private registry からイメージを取得する

task で使用するコンテナイメージを docker hub から取得するのはもちろん可能ですが、ローカルに立てた docker registry などの private registry から pull することもできます。やり方は簡単で、image_resource.source.repository に private registry のドメインを含むイメージのパスを指定します。ただし、task を実行する worker node において private registry のドメイン名が名前解決できる必要があります (node の /etc/hosts に記述する、dns サーバを立ててそちらにアクセスするように設定するなど)。

  • ユーザ認証を有効化している場合は username, password にそれぞれ指定します。
  • registry を自己署名証明書を使って HTTPS 化している場合、ca_certs に CA 証明書の中身を指定します。

以下は private registry mydomain.comcommon/busybox を task 実行のコンテナイメージとして使用する pipeline の例です。

jobs:
  - name: private
    plan:
      - task: hello-task
        config:
          platform: linux
          image_resource:
            type: registry-image
            source:
              repository: mydomain.com/common/busybox
              tag: latest
              username: <username>
              password: <password>
              ca_certs:
                - |
                  -----BEGIN CERTIFICATE-----
                  ...
                  -----END CERTIFICATE-----
          run:
            path: ls
            args:
              - "-l"

private repository からソースを pull する

ローカルに立てた Gitlab などの private repository からソースをダウンロードする場合、type: git とした resource において source.uri に private repository の URL を指定すれば pull できます (基本的には上記の private registry を使うと同じ)。

resources:
  - name: my-repo
    type: git
    check_every: never
    source:
      uri: https://mydomain.com/[owner]/[repository].git
      branch: main
      username: <username>
      password: <password>
jobs:
  - name: test
    plan:
      - get: my-repo
      - task: hello-task
        config:
          platform: linux
          image_resource:
            type: registry-image
            source:
              repository: busybox
          inputs:
            - name: my-repo
          run:
            path: ls
            args:
              - "-l"

イメージの build & push

CI/CD において pipeline 内で git repository からソースを持ってきてコンテナイメージをビルドし、別のイメージレジストリに push すると言った処理はよくある使用例ですが、ドキュメントの 1.14.3.1 Building and Pushing an Image にやり方が載っています。

pipeline の例としては The Entire Pipeline に書いてあり、こちらを試したところ手元でもコンテナイメージのビルドと push ができました。

troubleshooting

検証時に起こったトラブルと対処法。

private registry が名前解決出来ない

pipeline の中でローカル環境に立てた docker registry からイメージを pull しようとした際、 registry の名前解決ができないことがありました。
原因と挙動までは追いきれていませんが、以下のように docker-compose.yml を修正した上でコンテナを再作成することで解決しました。

  • CONCOURSE_WORKER_CONTAINERD_DNS_SERVER をコメントアウト
  • CONCOURSE_WORKER_CONTAINERD_DNS_PROXY_ENABLE を true に設定
  • node 側で docker registry を名前解決できるように /etc/hosts に追加
docker-compose.yml
services:
  concourse:
    environment:
      # CONCOURSE_WORKER_CONTAINERD_DNS_SERVER: "8.8.8.8"
      CONCOURSE_WORKER_CONTAINERD_DNS_PROXY_ENABLE: true

worker node 上の container runtime に関する設定はドキュメントの Configuring Runtimes に詳細が書いてあります。
また、DNS に関する troubleshooting は Troubleshooting and fixing DNS resolution で述べられています。この辺りを参照にしながら上記のように設定したところ問題が解決しました。
おそらく、CONCOURSE_WORKER_CONTAINERD_DNS_PROXY_ENABLE を設定することで worker node の DNS 設定を使用するようになるため、worker node 側で private registry を名前解決できるように設定することでコンテナ側でも名前解決できるようになったと思われます。

まとめ

concourse の基本的な機能を使ってワークフロー処理を試してみました。使用感としては Github workflow に近い感じてワークフロー内で行う処理を記述できます。
また、concourse では今回扱った機能以外にも以下のような機能に対応しています。こちらも機会があれば使いたいと思います。

docker や kubernetes でインストール可能、CNCF project の一部であるという点から、concourse はクラウドネイティブなワークフローツールであるとも言えます。
同じようなクラウドネイティブなワークフローツールには Tekton 等がありますが、それらと比較すると学習コストが低く、簡単に使い始めることができる点が concourse の特徴であると思いました。

Discussion