Skip to main content

12 posts tagged with "buildflow"

View All Tags

· 11 min read
Shunsuke Suzuki

buildflow というツールを開発しているので buildflow というタグをつけて何回かに分けてブログを書きます。

この記事では なぜ buildflow を作ったのかについて説明します。 開発者である自分の好みや置かれた環境などが所々に反映された内容になっています。

解決したい課題

自分は CI/CD の DX の改善に業務として取り組んでいます。 リポジトリはたくさんあり、横断的にメンテナンスしています。 幾つかのリポジトリはモノレポになっており、 CI の複雑さが増していたり、 CI の実行時間が長かったりします。

現在の CI/CD には以下のような問題があると感じています(他にもあるんですが、 buildflow と関係ないので割愛)。

  • 実行時間が長い
    • PR とは関係ない処理(test, build, etc) が実行されている
  • 金銭的に高い
    • 実行時間が長いので無駄にお金がかかっている
    • CI サービスによっては並列度を上げることで実行時間が縮む場合があるが、それでもその分お金がかかる
  • PR とは直接関係ないところで失敗する
    • PR とは関係ない処理(test, build, etc) が実行されていて、それらが flaky で失敗する
  • メンテナンス性が悪い
    • 属人化気味
    • 何をやっているのか分かりにくい
  • 同じような機能を複数のリポジトリで実装・メンテしたくない

これらの問題を解決するために buildflow を開発しました。

buildflow で必要な処理だけを実行する

buildflow では PR の情報を自動で取得し、それらに応じて実行する処理を変更できます。 変更されたファイルに応じてだけでなく、 label や PR の author などでも変更できます。 Tengo script を用いて柔軟なロジックを実装できます。 JSON や YAML の読み込みもサポートしているので、依存関係などの設定を別ファイルで管理することも出来ます。

一部の CI サービスはこれを解決するための機能を提供しています。 CodeBuild は Webhook の Filter で特定のファイルが変更された場合のみ build を実行できますし、 GitHub Actions でも似たようなことが出来ます。

それらで事足りるならそれでも良いでしょう。 それらだけだと難しい場合、 buildflow を使うとより柔軟に対応できるかもしれません。

並列処理による高速化

シェルスクリプトで for loop などで処理していて時間がかかっている場合、 buildflow で並列処理すると高速化するかもしれません。

メンテナンス性

buildflow を使わなくても「必要な処理だけを実行」したり「並列処理で高速化」したりはできるでしょう。 それでも buildflow を開発したのは、楽をするため、メンテナンス性を高めるためです。

PR の情報はよく必要になるので自動で取得するようにしています。

シェルスクリプトで複雑な CI を実装していると、メンテナンス性が悪くなります。 チームメンバーのシェルスクリプトへの習熟度に依存しますが、 シェルスクリプトはエンジニアなら誰でも書ける分全員が習熟しているとは限りませんし、 容易にバグが生まれます。 チームによりますが、Python や Ruby, Go といった他の言語と比べ、 lint や test がされてないことが多いせいもあるとは思います。 アプリケーションのコードは当然 CI で test, lint するのに、 CI とかのシェルスクリプトはしないというのも珍しくないと思います。

あとはサポートされているデータ構造が貧弱だったり、関数の I/F がわかりにくかったり、ググりにくい機能が多かったり、 コマンドのオプションを逐一調べないとわからなかったりします。

余談ですが、 shellcheck や shfmt を使うことをオススメします。 shellcheck を始めて使うと、シェルスクリプトにはこんなに色々罠があるのかと気付かされると思います。

Google の Shell Style Guide では次のように書かれています。

If you are writing a script that is more than 100 lines long, or that uses non-straightforward control flow logic, you should rewrite it in a more structured language now

では Ruby や Python といったスクリプト言語で書いたらどうでしょうか? シェルスクリプトで挙げた問題は解決すると思いますし、非常に自然で合理的な選択だと思います。

それでも buildflow を実装したのには、幾つか課題感があったからです。 まずは処理系、サードパーティのライブラリ、 OS パッケージに依存することです。 サードパーティのライブラリは使わなければいい話ですが、 Ruby や Python を使っていれば使いたいという声も出てくることはあるでしょう。 アプリケーションで同じ言語を使っていればそれとの共存も気にしないといけないかもしれません。 buildflow に限らず、自分は Go の「ワンバイナリで動く」という世界観が非常に好きです。

自分はこれまでシェルスクリプトを Go で書き直すということをやってきました。 その場合以下の2つがありますが、どちらにせよ課題感があります。

  • ビルド済みのバイナリを使う
    • 配布方法を考えないといけない
  • スクリプト言語のように go run で実行する
    • 他のスクリプト言語と同様の問題がある

ロジックとコマンドの分離

シェルスクリプトを何かしらの言語で書き直す場合、 全てをそれらで書きたいわけではありません。 だからこそ、規模が小さいうちはシェルスクリプトで書くのでしょう。 シェルスクリプトで書いたほうが楽な部分もあるのです。

buildflow ではコードを以下の3つに分離します。

  • 設定ファイル(YAML)
  • Tengo script
  • シェルスクリプト(細かいこと言うと、シェルスクリプト以外も実は使えるけど)

こうしてシェルスクリプトで書きにくい部分を分離し、適切な粒度で管理することでメンテナンス性を高めるというのが一つの狙いです。 Tengo script は基本的にデータの整形などに役割を限定し、外部ファイルに切り出せるようにすることで テストしやすいようになっています。

buildflow にはフレームワークとしての側面があり、 buildflow に乗っかることで共通の機能の実装を省いたり、コードを適切に分割してメンテナンス性を維持することができると期待しています。

尤もここはトレードオフがあるでしょう。 上記の 3 つを行き来しないといけなくて辛いというフィードバックをもらったこともあります。 コード分割は必須ではないので YAML にインラインで書くことも出来ますが、あまりおすすめしないのと、 そもそも上記のフィードバックはファイルの行き来だけでなく

  • buildflow の設定
  • Tengo Script
  • シェルスクリプト

という 3 つの異なる言語を行き来するという意味もあるのでしょう。

それはそういう側面もあるでしょう。 これは buildflow の根本的な部分なので変更されることはないと思います。 もし変更するようなら多分別のツールとして作っているでしょう。

まとめ

buildflow は自分が直面している CI/CD の課題

  • 必要な処理だけ実行したい
  • 共通の処理を逐一実装したくない
  • メンテナンス性を高めたい

を解決するために作りました。

buildflow はワンバイナリで動きます。 コードを適切に分離し、シェルスクリプトから複雑なロジックを除去することで、メンテナンス性を高めることを目指しています。

· 3 min read
Shunsuke Suzuki

buildflow というツールを開発しているので buildflow というタグをつけて何回かに分けてブログを書きます。

この記事では buildflow の実行結果の出力フォーマットなどについて説明します。

ちょっと出力はわかりにくいかもしれません。 改善したいと思いつつ、どうあるべきなのかまだ見えてないのでこんな感じになっています。

task の標準出力、標準エラー出力はリアルタイムで出力されます。 また、複数のタスクを並列実行できます。 複数のタスクのログをリアルタイムで出力すると当然混じるので、区別がつくように各行の prefix に timestamp | task name | をつけて出力します。 それでも混じるとわかりにくいので、 phase が完了後に、 phase の全 task のログを混ざらないようにそれぞれ標準エラー出力します。 つまり同じログが 2 回出力されますが 2 回実行されているわけではないです。

==============
= Phase: phase 名 =
==============
10:47:54UTC | task A | + /bin/sh -c echo hello # 実行されるコマンド
10:47:54UTC | task B | + /bin/sh -c echo foo
10:47:54UTC | task A | hello # コマンドの標準(エラー)出力
10:47:54UTC | task A |
... # リアルタイムに出力されるので複数の task のログが混ざる場合がある


================
= Phase Result: phase 名 = # 該当 phase の全 task 完了後に全 task の結果と標準(エラー)出力を出力する
================
status: succeeded
task: task A
status: succeeded
exit code: 0
start time: 2020-10-14T10:47:54Z
end time: 2020-10-14T10:47:54Z
duration: 4.818877ms
+ /bin/sh -c echo hello
hello

...

· 4 min read
Shunsuke Suzuki

buildflow というツールを開発しているので buildflow というタグをつけて何回かに分けてブログを書きます。

この記事では buildflow が自動で Pull Request (以下 PR) の情報を取得してくる機能について説明します。

この機能は GitHub のみサポートしています。 GitLab や BitBucket はサポートしていません。 これは単純に自分が GitHub しか使わないからです。

PR の CI では

  • 変更されたものだけテストする
  • 特定の PR ラベルがついていたら実行する
  • 特定のユーザーの PR だけ処理を変える(bot とか)

のように PR の情報に基づいて挙動を変えたくなったりします。

シェルスクリプトで GitHub API 叩いて情報とってきて jq でパースしてとか、頑張れば別にできるんですが、 毎回そういうコードを書きたくないなと感じていました。

なお、 PR の情報をとってくる機能はデフォルトで無効化されています(GitHub Access Token 必要ですしね)。 設定で pr: true を指定してください。

PR の情報をとってくるには、以下の情報が必要です。

  • repository owner: 設定ファイルで owner を設定するか、自動取得。 owner を設定してある場合はそちらが優先される
  • repository name: 設定ファイルで repo を設定するか、自動取得。 repo を設定してある場合はそちらが優先される
  • pull request number: 自動取得
  • GitHub Access Token: 環境変数 GITHUB_TOKEN または GITHUB_ACCESS_TOKEN を指定してください

取得される情報

以下のパラメータがテンプレートや Tengo script に渡されます。

  • PR: PR の情報: GitHub API のレスポンス body
  • Files: PR で更新されたファイルの一覧: GitHub API のレスポンス body

Files に関してはページネーションされていても全てのファイルが取得できるまで繰り返し API を叩いています。

自動取得の仕組み

各種 CI サービスの組み込みの環境変数からそれらの情報を自動で取得してくれます。

内部的には go-ci-env を使っているので、 PR 情報の自動取得をサポートしている CI サービスは以下のとおりです。

https://github.com/suzuki-shunsuke/go-ci-env#supported-ci-services

PR 番号が環境変数から取得できない場合、 revision から関連する PR のリストを取得し、一番最初の PR とみなします。 これは PR のマージコミットの CI ではマージされた PR の情報を取得することを意図しています。

関連する PR が存在しない場合は取得されるパラメータが nil になるだけで、 buildflow は異常終了したりせずに処理を続行します。

· 7 min read
Shunsuke Suzuki

buildflow というツールを開発しているので buildflow というタグをつけて何回かに分けてブログを書きます。

この記事では buildflow でなぜ Tengo を採用しているのかについて説明します。

https://github.com/d5/tengo

Tengo に関しては https://techblog.szksh.cloud/buildflow-1/ でも多少触れています。

なぜ Tengo を採用しているのかに関しては

  • なぜスクリプト言語を採用しているのか
  • なぜ他の言語ではなく Tengo なのか

の 2 つの観点で話します。

なぜスクリプト言語を採用しているのか

逆にスクリプト言語を採用しない方法としては、 YAML などで独自 DSL のようなものを定義する方法があります。 DSL と言うと大げさかもしれませんが、 AND, OR, NOT といった論理を YAML のようなデータ記述言語で表現しようと思うとそんな感じになると思います。

この方法は扱いたいロジックが単純なものに限られるのであれば問題ないですが、 より柔軟なロジックを表現したいとなった場合に、無理があります。

  • どうやって表現すればいいのか自分で考えないといけない
    • どう頑張っても独自ルールになるため、ユーザーにとって直感的とは言えない
  • 正しく実装しないといけない
  • 仕様をドキュメント化しないといけない

一方、 Go では幾つかのスクリプト言語がサードパーティのライブラリとして実装されており、 buildflow のようなツールに組み込むことが出来ます。

https://github.com/avelino/awesome-go#embeddable-scripting-languages

これらを活用すれば上記の問題は解決できるうえに、非常に柔軟にロジックを実装できます(勿論言語によりますが)。

なぜ他の言語ではなく Tengo なのか

単純に https://github.com/avelino/awesome-go#embeddable-scripting-languages で紹介されているライブラリの中で一番要件にマッチしてそうだったからです。 といっても全てをちゃんとチェックしたわけではありませんが。 Lua とかもあるのでそれでも良かったかもですが、自分は Lua を全然知りません。 あとちゃんとバージョンニングされていたのも理由の一つです。 Tengo より人気のある言語もありましたが、バージョニングされてないという理由で見送ったりしました。

実は Tengo の前に他の言語 antonmedv/expr を採用していたのですが、途中で表現力が足りてないので移行しました。 github-comment でも antonmedv/expr は使ってますし、便利ではあるのですが、 変数が宣言できず、基本ワンライナーで書くしかないので無理だなと判断しました。

https://github.com/suzuki-shunsuke/buildflow/issues/20

buildflow でスクリプト言語に求めているもの

buildflow における Tengo の用途はあくまでロジックの記述、シェルスクリプトでは扱いにくい map 等の操作です。 Tengo で外部コマンドを呼び出したりとかファイルを読み書きしたりとかそういうことは考えていません (てっきりそういうことが出来ない言語なのかと当初思っていましたが、できるようですね)。

Tengo は Python や Ruby, Go といった言語に比べれば言語仕様がコンパクトであり、 よく知らなくてもなんとなく読めるし、簡単にかけると思っています。

また、 Tengo ではテキスト処理などに使える標準ライブラリが提供されています(これがないと辛かったけど、あるので十分)。

なので今の所 Tengo で十分だと考えています。 Tengo よりリッチな言語があったとしても、今の所あまり移行するモチベーションはありません。

Tengo に関する不満

Tengo に関する不満を挙げると以下のようなものがあります。

情報が少ないのに関しては、言語仕様がシンプルなので個人的には今の所困ってません(公式ドキュメント読めば分かる

Tengo script の実行、 Test

よく知らない言語であれば、試しに実行してみたり、ちゃんとテストを書いたりしたいですよね。

実行に関しては公式の方でツールがあったりします。

テストに関しては簡単なツールを別に作りました https://github.com/suzuki-shunsuke/tengo-tester 従来シェルスクリプトでこういうロジックを実装しても「動けばいい」程度に考えていてテストは書かないことが多かったですが、 ロジックだけを Tengo のスクリプトとして切り出し、テストツールも用意することでちゃんとテストを書くようになることを期待しています。

· 3 min read
Shunsuke Suzuki

buildflow というツールを開発しているので buildflow というタグをつけて何回かに分けてブログを書きます。

この記事では buildflow の dynamic task という機能について説明します。 dynamic task では task.items の値でループを回し、複数の task を動的に生成できます。 勿論 task.items はオプションなので、指定しなければ普通の task として扱われます。 task.items を指定する場合、 map か list か、それらを返す Tengo script でないといけません。

---
phases:
- name: main
tasks:
- name: "list {{.Item.Key}} {{.Item.Value.name}}"
command:
command: "echo {{.Item.Key}} {{.Item.Value.name}} {{.Item.Value.age}}"
items:
- name: foo
age: 10
- name: bar
age: 20

上記の設定は dynamic task を使わないとこうなります。

---
phases:
- name: main
tasks:
- name: "list 0 foo"
command:
command: "echo 0 foo 10"
- name: "list 1 bar"
command:
command: "echo 1 bar 20"

パラメータ Item は Key, Value を持ち、 Items が map の場合、それぞれ map の key, value が渡され、 list の場合、 index と value が渡されます。

上記の例は単純すぎてイマイチかもしれませんが、 例えばファイルなどの一覧を返すコマンドの実行結果を元に dynamic task でファイルごとに別の task で並列に処理するとかが考えられそうです。

制約

task.items は phase の最初に評価されます。 つまり同じ phase の task の結果を参照したり出来ません。

これは task.dependency の評価時に、 task のリストが定まっていないと評価できないためです。

ただし、前の phase 及び phase の task の実行結果は参照できるため、 items で特定の task の実行結果を参照したい場合は、phase を分けることになります。

実は dynamic task を実現する上で上記の問題をクリアするために phase という概念を導入したという経緯があったります。

· 2 min read
Shunsuke Suzuki

buildflow というツールを開発しているので buildflow というタグをつけて何回かに分けてブログを書きます。

この記事では buildflow の task の input, output という機能について説明します。 task の input, output は Tengo script で task のパラメータを整形する機能です。

task の command.command や write_file.template など、幾つかの設定では Go の text/template が使えますが、 text/template は複雑なロジックを記述したりするのには向いていません。 そこで task の input で Tengo script を使って必要なデータの整形を行うことで、 template は比較的きれいな状態に保つことが出来ます。

これは MVC モデルで View とロジックを分離するみたいな考え方と似ているかもしれません。

output ではコマンドの実行結果を整形することが出来ます。 例えばコマンドの標準出力をユニークな文字列のリストにしたり出来ます。

task.input は task.when が評価されたあと、 task の command などが実行される前に評価されます。 つまり、 task.when や task.dependency で同じ task の input の結果を参照は出来ません。

· 2 min read
Shunsuke Suzuki

buildflow というツールを開発しているので buildflow というタグをつけて何回かに分けてブログを書こうかなと思います。

この記事では buildflow の設定ファイルを分割する方法について説明します。

buildflow では一部の設定項目について他のファイルのパスを指定して読み込むということが出来ます。 1 つのファイルに全部の設定を書いていると、ファイルが大きくなってメンテナンス性が悪くなったり、 コードオーナーが曖昧になったりするので、そういう場合は分割すると良いでしょう。 コードオーナーが異なる複数のサービスで共通の設定ファイルを用いる場合、ファイルを分割して GitHub の CODEOWNERS を設定するのもよいでしょう。 あまりないかもしれませんが、ファイルを分割すると同じファイルを読み込んで再利用も出来ます。

また、 Tengo script を独立したファイルに分割すると、 test が可能になります。 Tengo script をテストするためのツールとして tengo-tester というツールも開発しているので、そちらをお使いください。

以下のようなファイル読み込みの設定があります。

  • phase.import
  • task.import:
  • task.input_file
  • task.output_file
  • task.when_file
  • command.command_file
  • command.env[].value_file
  • write_file.template_file

ファイルのパスは、絶対パスか、実行中の build の設定ファイルが存在するディレクトリからの相対パスになります。

· 3 min read
Shunsuke Suzuki

buildflow というツールを開発しているので buildflow というタグをつけて何回かに分けてブログを書こうと思います。

この記事では buildflow の Tengo script やテンプレートにパラメータとして渡される変数について紹介します。

buildflow では Tengo script はテンプレートが使える設定項目が多くあります。それらの設定には共通のフォーマットのパラメータが渡されます。

  • PR: Pull Request の情報: GitHub API のレスポンス body
  • Files: Pull Request で更新されたファイルの一覧: GitHub API のレスポンス body
  • Phases: 対象の Phase よりも前の Phase の結果
  • Phase: 対象の Phase
  • Tasks: 対象の Phase の Task の結果
  • Task: 対象の Task
  • Item: dynamic task のパラメータとして渡される
  • Meta: 設定 meta

Phase

  • Status: Phase の実行結果
    • succeeded
    • failed
    • skipped
  • Tasks: Phase の task の実行結果
  • Meta: phase の 設定 meta

Task

type によらず共通

  • Type: task の type
  • Name: task 名
  • Status: Task の実行結果
    • queue
    • running
    • succeeded
    • failed
    • skipped
  • Meta: task の meta の設定
  • Input:
  • Output:

command

  • ExitCode
  • StdOut
  • StdErr
  • CombinedOutput

read_file

  • File
    • Text: ファイルの内容
    • Data: read_file の format を指定した場合、パースされた結果

write_file

  • File
    • Text: ファイルの内容

Template でパラメータを参照する

例えば command の場合

---
phases:
- name: main
tasks:
- name: hello
command:
command: 'echo "{{.Task.Name}}"'

Tengo script でパラメータを参照する

例えば task foo が成功した場合のみ、 task bar を実行したい場合

---
phases:
- name: main
tasks:
- name: foo
command:
command: echo hello
- name: bar
command:
command: echo hello
dependency:
- foo
when: |
task := {}
for t in Tasks {
if t.Name == "foo" {
task = t
break
}
}
result := task.Status == "succeeded"

このように他の task の実行結果や PR の情報などを使ってビルドの挙動を変えることが出来ます。

· 3 min read
Shunsuke Suzuki

buildflow というツールを開発しているので buildflow というタグをつけて何回かに分けてブログを書こうと思います。

この記事では buildflow の task の基本的な設定項目などについて説明します。 数が多いので、個々の設定の詳細はまた別の記事に書きます。

task には幾つか type がありますが、全ての type に共通するパラメータが以下になります。

  • name: task 名。 unique である必要はない。 Go の text/template が使える
  • when: task を実行するか否か。 真偽値か Tengo script
    • when_file で外部ファイルを読み込める
  • dependency: task の依存関係の定義。 task 名のリストか、 Tengo script
  • items: dynamic task の設定。 loop を使って複数の task を動的に生成できる
    • 任意の list か map か、 Tengo script
  • input: Tengo script で task のコマンドのパラメータを生成できる
    • input_file で外部ファイルを読み込める
  • output: Tengo script で task の実行結果を整形できる。他の task が参照して挙動を変えたりできる
    • output_file で外部ファイルを読み込める
  • meta: ユーザーが自由にパラメータを定義できる map

上記の設定は name 以外はオプションです。

task の type としては以下のものがあります。

  • command: 外部コマンドを実行
  • read_file: ファイルを読み込む。ファイルの内容を他の task で参照できる
  • write_file: ファイルを書き込む

command の設定

  • shell, shell_opts: コマンドの実行シェル。デフォルトは /bin/sh -c
  • command: コマンド。 Go の text/template で処理される
    • command_file で外部ファイルを読み込める
  • stdin: コマンドの標準入力。 Go の text/template で処理される
    • stdin_file で外部ファイルを読み込める
  • env: 環境変数。環境変数名と値は Go の text/template で処理される

read_file の設定

  • path: ファイルのパス。Go の text/template で処理される
  • format: ファイルのフォーマット。オプション。 json と yaml をサポート。指定するとパースした結果を他の task が参照できる

write_file の設定

  • path: ファイルのパス。Go の text/template で処理される
  • template: ファイルの内容。Go の text/template で処理される
    • template_file で外部ファイルを読み込める

· 3 min read
Shunsuke Suzuki

buildflow というツールを開発しているので buildflow というタグをつけて何回かに分けてブログを書こうと思います。

この記事では buildflow の概念である build, phase, task について書きたいと思います。

buildflow には Build, Phase, Task という概念があります。 CircleCI の Pipeline, Workflow, Job みたいなものと思ってもらえるとよいと思います。

$ buildflow run

で 1 つの build が実行されます。 build は複数の phase からなり、 phase が 1 つずつ順に実行されます。 phase は複数の task からなり、 task が全て終了すると、その phase も終了となります。 task は並列に実行したり、依存関係を定義したりできます。 task では外部コマンドを実行したりできます。

設定ファイルでは phases, tasks をそれぞれ配列で指定します。

---
phases:
- name: setup
tasks:
- name: hello
command:
command: echo hello
- name: foo
command:
command: echo foo
- name: build
tasks:
- name: hello
command:
command: echo hello
- name: foo
command:
command: echo foo
dependency:
- hello
- name: post build
tasks:
- name: hello
command:
command: echo hello

上の例では 3 つの phase setup, build, post build が順に実行されます。 デフォルトではどれかの phase が失敗するとそれ以降の phase は実行されません(この挙動は変えられます)。

task も phase 同様配列で指定しますが、配列の順序に意味はありません。 依存関係がない限り、並列で実行されますし、実行順序は不定です。