Skip to main content

56 posts tagged with "oss"

View All Tags

· 2 min read
Shunsuke Suzuki

仕事

  • AWS IAM User を削除する際に force_destroy が true になっているか Conftest でテスト
  • Terraform の State 分割
  • Terraform Modules を別リポジトリで管理して versioning
  • git-secrets を secretlint に移行
    • git-secrets がメンテされてなくて、既知バグが放置されているから
  • CI で terraform fmt によるフォーマットの自動化
  • WIP: AWS WAF の COUNT, BLOCK ログを Firehose で抽出
  • WIP: AWS CodeBuild で Provisioning Error が発生したら自動で Retry
  • WIP: AWS CodeBuild のための GitHub App の開発
  • WIP: AWS SSO について調査

OSS Contribution

Renovate の GitHub Actions のドキュメントの修正をしました。 ドキュメント中に書かれたバージョンを Renovate で自動 update するようにしました。

新たに作った OSS

Blog

· 16 min read

2021-09-04 追記: aqua v0.1.0 から v0.5.0 での変更点

aqua という OSS を開発しているので紹介します。

記事の内容は aqua v0.1.0 に基づきます。将来的に仕様が変わる可能性があります。

aqua とは

aqua は CLI ツールのバージョン管理のための CLI です。 aqua で管理する主な対象は GitHub Release で公開されているツールです。 YAML の設定ファイルを書いてコマンドを実行すると指定したツールをインストールすることができます。

例えば以下のような設定ファイルを書き、 aqua install というコマンドを実行すると jq, conftest などが GitHub Release からダウンロードされ、インストールされます。

packages:
- name: jq
registry: inline
version: jq-1.6
- name: conftest
registry: inline
version: v0.27.0
inline_registry:
- name: jq
type: github_release
repo_owner: stedolan
repo_name: jq
asset: 'jq-{{if eq .OS "darwin"}}osx-amd64{{else}}{{if eq .OS "linux"}}linux64{{else}}win64.exe{{end}}{{end}}'
files:
- name: jq
- name: conftest
type: github_release
repo_owner: open-policy-agent
repo_name: conftest
asset: 'conftest_{{trimPrefix "v" .Package.Version}}_{{title .OS}}_x86_64.tar.gz'
files:
- name: conftest

ちなみに上記の設定ファイルの

  asset: 'conftest_{{trimPrefix "v" .Package.Version}}_{{title .OS}}_x86_64.tar.gz'

の部分では Go の text/templatesprig が使われています。

ツールごとに URL を調べて download して tarball などを展開してインストールしてなどの面倒な作業を aqua で自動化できます。 update も基本的に設定ファイルの version を更新するだけで OK です。

aqua を使うと同じツールの複数のバージョンを管理してプロジェクトによってバージョンを切り替えるといったことも容易にできます。

3 つの主なユースケース

aqua では以下の 3 つの主なユースケースを想定しています。

  • CI/CD で必要なツールの管理
  • ローカルでの開発に必要なプロジェクト(リポジトリ)固有のツールの管理
  • 特定のプロジェクト(リポジトリ)によらないツールの管理

ユースケース1: CI/CD で必要なツールの管理

例えば Terraform の Monorepo の CI で以下のような様々なツールを使っていたとしましょう。

これらを1個1個 curl などを使ってインストールするコードを書くのは面倒ですが、 aqua であれば設定ファイルを宣言的に書いて aqua i を実行すれば終わりです。 新たにツールを追加する場合でも設定ファイルに追記すればよく、スクリプトを更新する必要はありません。 バージョンを明示的に指定できるのでコードを変更してないのに急にツールが更新されることもありませんし、 Renovate の Regex Manager などを使えば更新を自動化することもできます。

ユースケース2: ローカルでの開発に必要なプロジェクト(リポジトリ)固有のツールの管理

あるリポジトリのローカルでの開発に必要なツールを aqua で管理することができます。 リポジトリ直下に aqua.yaml を置いておけば OK です。 バージョンも指定されているので、人によってバージョンが違ったりする問題も解消できます。 aqua.yaml と同じディレクトリに .aqua が作成されるのでそれを .gitignore に追加し、 .aqua/bin を PATH に追加しましょう。 direnv を使い、リポジトリ直下に .envrc を置いて .aqua/bin を PATH に追加すると便利です。

aqua.yaml
.aqua/bin
.envrc

.envrc

PATH_add .aqua/bin

.aqua/bin を PATH に追加しなくても aqua exec -- <コマンド> ... で実行することもできます。

ユースケース3: 特定のプロジェクト(リポジトリ)によらないツールの管理

特定のプロジェクトによらずにツールを laptop にインストールしたい場合にも使えます。 ~/.aqua/global/aqua.yaml に設定ファイルを記述し、 ~/.aqua/global/.aqua/bin を PATH に追加してください。

export PATH=$HOME/.aqua/global/.aqua/bin:$PATH

そして ~/.aqua/global 配下で aqua i を実行すればインストールができます。 ~/.aqua/global を Git で管理して GitHub などでホスティングするのも良いでしょう。

https://github.com/suzuki-shunsuke/my-aqua-config

akoi との違い

ところで、自分は aqua に似たツールとして akoi というツールを公開していて、自分もこれまでこのツールを使ってきました。 aqua と akoi は「CLI ツールのバージョン管理」という目的・ゴールは同じです。 akoi も結構便利なツールですが、 akoi が抱える様々な課題を解決するために aqua を開発しています。 aqua は akoi のいわば後継ツールです。 ただしコードは全く別物ですし、互換性もありません。

akoi と比べた aqua の良い点

  • GitHub Access Token を使ったインストールをサポート
    • private repository をサポート
    • akoi は anonymous なアクセスなので rate limit に引っかかりやすい
  • 管理対象のコマンド実行時にツールのインストールが可能
  • 設定ファイルを更新したあとに install コマンドを実行する必要がない
    • akoi は symbolic link を作り直すために install コマンドを実行する必要がある
  • 管理対象のツールの実体を共有できる
    • project ごとにツールを install する必要がない(計算資源の効率化)
    • akoi と違って ツールによって実体のインストール先は一意に決まるので、干渉することがなく安全に共有できる
  • 事前に archive の中のパスを知っている必要がない
    • akoi は install 時に archive を展開してファイルをコピーし、シンボリックリンクを作成する
      • パスが間違っていると失敗し、 download からやり直しになる
      • そのため、新しいツールを akoi で管理する場合はまず archive の構造を調べる必要がある
    • aqua は install したあとに ~/.aqua 配下を見て file.src を修正すれば良いし、間違っててコマンドの実行に失敗しても download のやり直しとかはない
  • bin_path, link_path ような設定について考えなくて良い
    • akoi は設定ファイルでインストール先などを設定できるようになっている
    • どう設定すべきか悩ましいし、リポジトリによって設定が違ったりして設定を統一するのが難しい
    • aqua はインストール先などが設定できないのでユーザーが迷う必要がない

管理対象のツールの実体を共有できる

aqua はツールの実体を AQUA_ROOT_DIR ~/.aqua にインストールし、共有することができます。 複数のリポジトリで同じバージョンの同じツールを使う場合に共有できるので、 インストールにかかる時間を短縮できますし、無駄にディスク容量を消費することもありません。 設定ファイルによって動的にバージョンを取得するので、共有していてもリポジトリごとに異なるバージョンを使うこともできます。

安全に共有できるようにツールの実体のインストール先はダウンロード元によってユニークかつ一意に決まるようになっています。 ユーザーがカスタマイズすることはできません(ルートディレクトリは変えられますが、ルート以下は変えられません)。

例えば OSX で jq-1.6 のインストール先は以下になります。

.aqua/pkgs/github_release/github.com/stedolan/jq/jq-1.6/jq-osx-amd64/jq-osx-amd64

このように GitHub Release からインストールする場合

  • リポジトリのオーナー
  • リポジトリ名
  • tag
  • GitHub Release のアセット名

などから一意に決まるため、あるリポジトリでは jq をフォークしたものを使うといった場合でも安全に共存することができます。

aqua install を実行するとツールごとに以下のことが実行されます。

  1. .aqua/bin 配下にシンボリックリンクを作成
  2. ダウンロード
  3. tarball などの展開
  4. ~/.aqua 配下にインストール

aqua.yaml の packages に大量のツールが定義されていると、 大量のツールが一度にインストールされることになり、 並列で実行されるとはいえ、都合が悪いこともあるでしょう。

--only-link option をつけて実行すると、シンボリックリンクだけ作成しダウンロードなどは行わないので直ぐに終わります。

$ aqua install --only-link

その状態でツールを実行すると、ツールが自動でインストールされてから実行されるので 本当に必要になってからインストールすることが可能であり、余計なインストールが発生しないので便利です。

コマンド実行時の自動インストール、動的なバージョン切り替えの仕組み

aqua は設定ファイルを更新すると aqua install 実行をしなくても更新が反映される、 ツールがまだインストールされていなくてもツールを実行時に自動でインストールされるという機能があります。

tfenv も .terraform-version を更新すればすぐ反映されますし、 terraform コマンドを実行時にまだ指定したバージョンがインストールされてなかったら自動でインストールされますが、それに似ていますね (ただし tfenv の機能がどう実装されているかは調べてませんし、 aqua を実装する上で参考にしたりはしていません)。

上記の機能が aqua でどう実現されているか簡単に説明します。

例えば aqua で jq をインストールし、 jq -h を実行したとしましょう。 jq を実行すると aqua-proxy を経由して aqua exec -- jq -h が実行されます。 この辺の詳細は aqua-proxy とは を参照してください。 aqua exec は aqua の設定ファイルで指定されたバージョンがインストールされているかチェックし、まだインストールされていなかったらインストールし、コマンド jq -h を実行します。

aqua-proxy とは

aqua-proxy は aqua が内部的に依存しているツールです。 コマンド実行時に aqua 及び aqua で管理するツールのバージョンを動的に変更するために作られました。 aqua のために開発されており、 aqua 以外で使われることは想定していません。

aqua-proxy は aqua installaqua exec を実行した際に自動で ~/.aqua/bin/aqua-proxy にインストールされます。 aqua はツールをインストールする際に .aqua/bin/<ツール> から ~/.aqua/bin/aqua-proxy へのシンボリックリンクを作成するので、 <ツール> を実行すると ~/.aqua/bin/aqua-proxy が呼ばれます。

aqua-proxy は os#Args からツール名を取得し、 aqua exec -- <ツール名> ... を実行します。 これによりコマンド実行時に aqua 及び <ツール> のバージョンを動的に変更することを実現しています。

.aqua/bin/<ツール> から aqua-proxy へのシンボリックリンクは静的であり、 aqua-proxy のバージョンを切り替えることは難しいです。 aqua-proxy の機能・責務が大きくなると aqua-proxy のバージョン管理や aqua との互換性を考えなくてはならなくなります。 aqua-proxy のバージョンをほぼ気にしなくて良いよう、 aqua-proxy は最小限の機能・責務しか持たず、安定的であまり変更されないように設計されています。

プロセスツリーを確認してみる

既に説明したとおり <ツール> を実行した際には実はプロセスツリー的には aqua-proxy => aqua => <ツール> という風になっています。

<ツール> を直接実行した場合と挙動に違いが出ないように以下のようなことに気を配っています。

  • SIGINT, SIGTERM などのシグナルが適切に <ツール> のプロセスまで伝達されるようにする
  • <ツール> の exit code が伝達されるようにする

試しに fzf を実行してみて別のターミナルでプロセスツリーを確認してみます。

$ ls | fzf

fzf が起動しますが、そのままにしておいて別のターミナルでプロセスツリーを確認してみます。 Mac の pstree を使っています。

-+- 83548 foo fzf
\-+- 83549 foo aqua exec -- fzf
\--- 83550 foo /Users/foo/.aqua/pkgs/github_release/github.com/junegunn/fzf/0.27.2/fzf-0.27.2-darwin_amd64.zip/fzf

紛らわしいのですが、最初のプロセスの実体は fzf ではなくて aqua-proxy です。 fzf が aqua-proxy へのシンボリックになっているのでこうなっています。 ここで aqua-proxy に SIGTERM を送ると手元の Mac ではちゃんと子プロセスまで終了しました。

$ kill 83548

この辺のシグナルハンドリングは Windows だと正常に動かないかもしれません。

https://pkg.go.dev/os#Signal

The only signal values guaranteed to be present in the os package on all systems are os.Interrupt (send the process an interrupt) and os.Kill (force the process to exit). On Windows, sending os.Interrupt to a process with os.Process.Signal is not implemented; it will return an error instead of sending a signal.

· 4 min read
Shunsuke Suzuki

今まで仕事に限定して書いてきましたが、 OSS 活動なんかにも触れてもいいんじゃないかと思ったので分かる範囲で書きます。

仕事

  • Docker Image を Docker Hub から ECR へ移行
  • Terraform
    • .terraform.lock.hcl を CI の中で自動で更新(commit, push)できるようにした
      • Terraform に詳しくない人も使うので、自動化したほうが良いと判断
    • tfmigrate を CI に導入
    • (in progress) Terraform Modules を Terraform の Monorepo とは別リポジトリで管理して versioning するようにした
    • Route53 の管理を Roadworker から Terraform へ移行
    • tfmigrate を使ったリファクタリング

Event

OSS Contribution

AWS AppConfig を Terraform で管理できるようにする PR が無事マージされました。

新たに作った OSS

tfmigrator

Terraform Configuration と State をマイグレーションする tfmigrator の CLI をリリースしました。 tfmigrator には紆余曲折有り(?)、時系列的に

  • suzuki-shunsuke/tfmigrator を開発。 CLI
  • YAML の設定ファイルの表現力に限界を感じ、 suzuki-shunsuke/tfmigrator をフォークして Go のライブラリ tfmigrator/tfmigrator を開発
    • 簡単に CLI を実装できるように API も提供
    • ついでに色々改良
      • hcledit のインストールが不要
      • ファイルの in place の更新をサポート
      • dry run のサポート
      • 複数のリソースをまとめて扱えるような API も提供 QuickRunBatch
      • etc
  • 実際に tfmigrator/tfmigrator を使ってみると Go を書くのがちょっと面倒くさい
    • そもそも複雑な rule を一度に適用しようとするのが間違っていると感じた
  • tfmigrator/tfmigrator を使い、 CLI も実装 tfmigrator/cli
    • やはり基本的なユースケースでは YAML 書くほうが楽

Renovate github-tags Datasource Repositories

Renovate の Datasource や Manager でサポートされていない package を Renovate で update するために、 package ように GitHub Repository を作って package のバージョンに合わせて GitHub tag を更新し、 github-tags Datasource として使おうというプロジェクトです。 現状 AWS RDS や AWS Elasticache の engine version 用のリポジトリを作っています。 tag は GitHub Actions を毎日定期実行することで更新します。 詳細はリポジトリの README でも読んでください。

新しいバージョンをリリースした OSS

terraform v0.15.4 から Terraform 以外での変更も plan に出力されるようになって わかりにくいと感じたので、 tfcmt でテンプレート変数追加して見やすくできるようにしました。 Refreshing state のログを除外したり、warning 目立たせたりもできて便利です。

Blog

· 13 min read
Shunsuke Suzuki

terraformer で雑に生成した Terraform の設定ファイル (以下 tf ファイル) と state を分割したくてツールを書きました。

tfmigrator

経緯

miam から Terraform へ移行したい

miam というツールで管理されている大量のリソースを Terraform で管理したくなりました。 多くの AWS Resource は Terraform で管理されていますが、 IAM に関しては miam で管理されています。 なぜ Terraform ではなく miam で管理されているかというと、当時のことは自分には分かりませんが、歴史的な経緯もあると思います。 昔は今よりも Terraform の表現力が豊かではなく、 Ruby で自由にかける miam のほうが扱いやすかったとか、 miam だと miam でリソースを管理することを強制できるため、権限管理を厳格にやるという観点では都合が良いという点もあるかと思います。

ではなぜ Terraform で管理したくなったかというと、 一番大きな理由は miam で頻繁に rate limit に引っかかるようになったからです。 Terraform にしろ miam にしろ CI/CD で test, apply が実行されるようになっています。 miam では毎回全部のリソースを対象に処理が実行されるため、リソースの数が増えるにつれて rate limit に引っかかりやすくなります。 CI を rerun すれば成功するのですが、悪いときは 3 回連続で rate limit に引っかかり、 4 回目でようやく成功するということもありました。

Terraform ではサービスや環境単位で State が分割されており、 CI も PR でファイルが変更された state に対してのみ実行されるため、 rate limit に引っかかることは基本ないようになっています。

他にも色々理由はあるのですが、本題からそれるのでやめておきます。 rate limit だけなら miam でも exclude する機能があるので頑張ればなんとかなる気はします。

やりたかったこと

  • Terraform で既存のリソースを管理したい
    • tf ファイル と state を生成したい
  • Terraform の設定ファイル と state はサービス・環境ごとに分割したい
  • Terraform のリソースパスはヒューマンリーダブルにしたい
    • これは難しければ諦めるのもありだが、できればやりたい

terraformer で自動生成するも色々問題があった

まずは terraformer で雑に tf ファイル と state を生成しました。 今回 terraformer を使うのは初めてで、 terraformer で万事解決なら話は簡単だったのですが、話はそんな簡単ではありませんでした。 これに関しては、自分の問題(使い方を間違っている、ドキュメントをちゃんと読んでいない)なのか、 あるいは terraformer 側の問題なのかよく分かってない部分もあります。

まずドキュメントを読んでもいまいちリソースのフィルタリングの仕方が分かりませんでした。 試しに IAM role の名前を指定してそれだけ import しようとしましたが、なぜか全 IAM リソースが import されてしまいました。

良くわからないので、これは全 IAM リソースを雑に import してからサービス・環境ごとに分割するしか無いかなぁと思いました。

加えて、リソースパスが全然ヒューマンリーダブルではありませんでした。 terraformer としてはこれは仕方ないと思いますが、なんとかリネームしたいと思いました。

最初手作業で始めるも、自動化が必要と悟る

最初特定のサービスに関して手作業で tf ファイル, state を移行する作業を行ってみました。 簡単なシェルスクリプトを書いて半自動化してみました。 tf ファイルの操作には hcledit が便利です。

以下のようなコマンドを使いました。

  • terraform state mv
  • hcledit block get
  • hcledit block mv
  • hcledit block list

で、やってみたものの、なにせ対象が多いので、これを一つ一つ手作業でやるのは大変だし、ヒューマンエラーは避けられないと感じました。 そこでちょっとしたツールを作ることにしました。 手作業で一回やった分手順はイメージできているので、割と簡単にできるだろうと思いました。

tfmigrator

そこで作ったのが tfmigrator です。 今回は AWS の IAM リソースを扱いますが、 tfmigrator は特定の provider などには依存しないツールです。 Terraform CLI と hcledit が必要です。

まず terraformer で IAM リソースを全部 import してきます。

$ terraformer import aws -r iam --compact --path-pattern .

こうすると resources.tf と terraform.tfstate が作られます。

tfmigrator の設定ファイル tfmigrator.yaml を書きます。

---
items:
# 既に Terraform で管理されているものは無視
- rule: '"tags" in Values && Values.tags.ManagedBy == "Terraform"'
exclude: true
- rule: '"name" not in Values'
exclude: true
# `name` に "foo" が含まれているものはサービス foo のリソースとみなして分割
- rule: 'Values.name contains "foo"'
state_out: foo/terraform.tfstate
resource_name: "{{.Values.name}}"
tf_path: foo/resource.tf
# 以下略

tfmigrator の処理の流れをなんとなくそれっぽい擬似言語で表現します。 実際の処理の流れとは若干異なりますが、雰囲気が伝わればと思います。

for resource in state
for item config.items
if item.rule.match(resource)
if item.exclude
# このリソースは処理せず次のリソースの処理に移る
break
# tf の migration (note: 元の tf はそのまま)
hcledit block get resource.path < tf | hcledit block mv resource.path "${resource.type}.${item.resource_name(resource)}" >> item.tf_path
terraform state mv
# 次のリソースの処理に移る
break

各 item の設定の意味はこんな感じです。

  • rule: expr の expression 。この条件にマッチしたリソースをこの item で処理する
  • exclude: true の場合、この item にマッチしたリソースは無視する
  • state_out: terraform state mv-state-out
  • resource_name: 新しいリソース名。デフォルトでは名前はそのまま。 Go の text/template で処理されます
  • tf_path: Terraform の設定ファイルの出力先

設定ファイルを書いたら tfmigrator を実行します。 いきなりマイグレーションをするというよりは、まずは dry run てきなことをして動作を確認したいですね。 -skip-state をつけると terraform state mv を skip し、分割される tf ファイル を新しいファイルに出力だけします。

$ cat *.tf | tfmigrator run -skip-state

生成された tf ファイル を眺めて、良さそうなら -skip-state をとって実行します。

tf ファイル生成時の注意点

tf ファイルの生成は現状追記モードで実行されます。なので -skip-state をつけて複数回実行すると同じ設定が重複して書き込まれることになります。 それが困る場合は実行前に対象ファイルを消してから実行してください。

また、tf ファイルの移行は既存の tf ファイルに対して hcledit block get, hcledit block mv を実行して行われるため、 元の tf ファイルはそのまま残ること、また expression は評価されないことに注意が必要です。 例えば name = var.name のように変数を参照している場合、それもそのまま評価されずに残ります。 とりあえず自分がやりたかったのは terraformer で生成した tf ファイルの移行だったので、そんなに問題にはならないだろうと思っています。

エラーハンドリング

あるリソースの処理でエラーが起こったら即座に異常終了するようにしています。 (当然ですが)ロールバックとかはしません(し、できません)。 エラー出力しつつ次のリソースの処理に移る、というのも考えられますが、間違って terraform state mv されると面倒なので、現状即座に終了するようにしています。 問題のあるリソースを無視したい場合は、 tfmigrator の設定でそのリソースにマッチする item を追加し exclude: true とすればよいでしょう。

このツールの便利なところ

  • expr を用いてリソースを分類できる
  • 設定ファイルに記述し、ワン・コマンドで実行できる
    • レビューできる
    • 後で見返せる

ローカルで試行錯誤しながら複数のコマンドを実行していると、後でなにやったかわからなくなりがちですし、途中で作業を中断したりすると、あとで今どういう状態なのか分からなくなったりします。 ワン・コマンドで実行できるとそういう問題がなくて便利ですね。

肝心の移行はできたのか

まだできていません。移行するために tfmigrator を作ったので、これから移行していこうという段階です。 なので tfmigrator はまだ全然使い込んでないですし、使ってく中で機能修正したりすることもあると思います。

最後に

terraformer で雑に import してきた tf ファイルと state をいい感じに分割するために tfmigrator というツールを作りました。 tfmigrator が役に立つケースは割と限られているというか、日常的に使うようなツールでもないですが、 terraformer で雑に import してきたのは良いが、扱いに困っているなんて人には役に立つかもしれません。

· 2 min read
Shunsuke Suzuki

mercari/tfnotify を Fork して 2 つほど OSS を作りました。

開発の経緯

これまで tfnotify を便利に使わせてもらってたのですが、幾つか改善したいと思うところがあり、本家に PR を投げました。 しかし残念ながらこれまでのところ反応がなく、そこまで本家が活発ではないこと、また他にも色々改修したいところがあったことから、自分でフォークしてメンテすることにしました。 最初は互換性を維持しながら suzuki-shunsuke/tfnotify を開発していました(今もしています)。 しかし、開発を進めるに連れ、自分にとって必要のないプラットフォームなどに関するコードが邪魔であると感じ、それらを消したバージョンを別に開発することにしました。 互換性がなくなることから、名前も変えて tfcmt としました。

https://github.com/suzuki-shunsuke/tfcmt

こういった経緯から、 tfcmt のほうを優先的に開発していますが、 tfcmt で実装した機能を後から suzuki-shunsuke/tfnotify にも実装してたりもします。

Fork 元のバージョン

suzuki-shunsuke/tfnotifymercari/tfnotify v0.7.0 fb178d8 をフォークしました。 一方 tfcmt は suzuki-shunsuke/tfnotify v1.3.3 をフォークしました。

mercari/tfnotify との違い

本家との違いは Release Note とドキュメントを参照してください。

· 2 min read
Shunsuke Suzuki

先日 kreuzwerker/terraform-provider-docker の Collaborator になりました。 kreuzwerker/terraform-provider-docker は Terraform の Docker Provider であり、 Docker コンテナや image, network などを管理できます。 元々は Hashicorp の Official Provider であった terraform-providers/terraform-provider-dockerkreuzwerker/terraform-provider-docker に移管され、 Community Provider になりました。 元のリポジトリは hashicorp org に移され archive されています。

Collaborator になった経緯

リポジトリが移管される際に、メンテナを募集していて過去に contribution していた自分にも声をかけていただきました。

https://github.com/hashicorp/terraform-provider-docker/issues/306

Contributor になった経緯

自分がこの provider に contribution した経緯は、 Terraform の Hands on を書くのに丁度よい provider を探していたことでした。

Hands on の題材として Docker コンテナを作ったりできたらいいんじゃないかなと思って Docker provider を試してみました。 しかし当時の docker_container リソースは read をちゃんとサポートしていませんでした。 なので import や update がまともに動きませんでした。 それを見かねて修正して PR を投げたのが最初です。

その後も幾つか contribution をしました。

なお、 PR を投げたものの、 Hands on は MySQL Provider を使って書きました。

https://techblog.szksh.cloud/terraform-hands-on-with-mysql-provider/

· 3 min read
Shunsuke Suzuki

In this post I introduce how to split a huge .circleci/config.yml.

CircleCI doesn't support to split .circleci/config.yml, so we manage all workflows and jobs configuration into one file .circleci/config.yml. If the repository is Monorepo, the more the number of services increases, the more the size of .circleci/config.yml becomes large and it's hard to maintain .circleci/config.yml. By splitting .circleci/config.yml per service, it makes easy to maintain .circleci/config.yml and we can configure split file's CODEOWNERS.

To split .circleci/config.yml, you have to generate .circleci/config.yml by merging split files and commit both split files and .circleci/config.yml.

circleci config pack

We can merge split files with the command circleci config pack, but I introduce the other tool circleci-config-merge.

CircleCI CLI is an official tool so it's reliable, but I feel the restriction of the file name and the directory structure is a little strict. We have to manage all files on the same directory, and the file path is reflected to generated YAML content.

For the detail of circleci config pack, please see the official document.

https://circleci.com/docs/2.0/local-cli/#packing-a-config

If you can accept the restriction of circleci config pack, I recommend to use it because it is an official tool. But if it is difficult to accept the restriction, maybe circleci-config-merge would help you.

circleci-config-merge

circleci-config-merge is a CLI tool to generate .circleci/config.yml by merging split files.

The usage of circleci-config-merge is like the following.

$ circleci-config-merge merge <file1> [<file2> ...]

There is no restriction of file paths, and the format of split file is same as .circleci/config.yml.

For example, you can manage files on the same directory.

.circleci/
config.yml # generated
src/
service1.yml # split config per service
service2.yml
...

Or you can also manage files on each service directory.

service1/
circleci/
workflow.yml # you can split file freely
jobs.yml
...
service2/
circleci/
config.yml
...
...

circleci-config-merge merges the list of workflow jobs.

For example,

workflows:
build:
jobs:
- foo
workflows:
build:
jobs:
- bar

The workflow build's jobs are merged as the following.

workflows:
build:
jobs: # sort by job name for comparison
- bar
- foo

Test .circleci/config.yml in CI

If you split .circleci/config.yml, you should test in CI whether .circleci/config.yml is generated by merging split files. circleci-config-merge doesn't provide such a feature, but you can implement the test with the other tool like dyff.

I have created an example repository suzuki-shunsuke/example-circleci-config-merge. You can use this example as a reference to split .circleci/config.yml and setup CI.

Use case

Lastly, I introduce a use case of circleci-config-merge. Recently, I split a huge .circleci/config.yml which is over 6,000 lines to about 60 files. It was hard to maintain the original .circleci/config.yml, but by splitting it became easy to maintain .circleci/config.yml. If you are suffer from a huge .circleci/config.yml, let's split it!

Conclusion

In this post I introduced how to split a huge .circleci/config.yml. We can generate .circleci/config.yml by merging split files with circleci-config-merge. Please see the example suzuki-shunsuke/example-circleci-config-merge as a reference to split .circleci/config.yml and setup CI.

· 7 min read
Shunsuke Suzuki

自作の OSS github-ci-monitor の紹介です。

GitHub リポジトリの CI のステータスを定期的に取得し、 DataDog に送ることで、 CI のステータスを監視するツールです。 現状は AWS Lambda で動かすことを想定していますが、他の方法でも動かせるようにするつもりです。

Motivation

モチベーションは、 PR をマージしたあとに CI がこけた場合に通知が欲しいというものです。 マージしたあとに CI が一瞬で終わるなら無事終わるのを見届けてもいいんですが、 数分かかると待ってるのも時間がもったいないです。 しばらくしたあとに結果を確認すればいいんですが、それも面倒くさいですし、普通に忘れます。 そうするとデプロイしたつもりが実は CI がこけてたなんてことが普通にあります。

そういうことにすぐ気づけるよう、 Slack に通知がほしいと思っていました。

仕組み

仕組みは単純です。

GitHub API で各リポジトリのステータスを取得し、 DataDog API でステータスを送信しています。 DataDog API は Service Check API を使っています。 status は以下のようになります。

  • 0: 正常
  • 1: 異常
  • 3: ステータスの取得に失敗

また以下の tag が付きます。

  • owner: リポジトリのオーナー
  • repo: リポジトリ名
  • ref: ブランチ名

各リポジトリのステータスは現状 3 つをサポートしています。

それぞれ on/off を設定でき、複数指定した場合は、どれか 1 つでも失敗していたら status が 1 になります。

Lambda で動かす場合のアーキテクチャ

CloudWatch Events で定期的(5分毎とか)に Lambda Function を実行します。 リポジトリのリストなどの設定は環境変数で渡し、 GitHub Access Token などのクレデンシャルは AWS Secrets Manager 経由で渡します。

実装方針

CI がこけたら通知してほしいという要件を満たす方法は色々あると思います。

まず CI の中でこけた場合に通知を飛ばすようにすることが考えられます。 以下のようなメリットがあります(書いてみたら結構ありますね)。

  • ツールをどっかで動かしたりツール自体を監視したりする必要がない
  • より詳細なメッセージを送れる
    • コマンドの標準エラー出力を含めたり
    • CI のリンク貼ったり、 PR の author をメンションしたりもしやすい
  • CI がこけたらリアルタイムで通知できる
  • ポーリングと違い、無駄に API を叩く必要がない

一方で、これを漏れなく実装するのはけっこう大変だと思います。 例えば CircleCI だと全ての Job でちゃんとハンドリングしないといけなかったりすると思います。 リポジトリが 1 個だけならそれでもいいですが、何十個もあるとなるとだいぶ大変だと思います。

今回のツールのような方式だと対象のリポジトリの CI に一切手を加えずに実装できるのが大きいです。

また、 CI の結果を取得する API として CI サービスが提供する API を使って取得することも考えられます。 しかし、 GitHub API を使えば CI サービス毎に実装したりする必要がなくて楽です。

Slack API を使ってメッセージを投稿するようなことも一瞬考えましたが、 DataDog を使うことで以下のメリットがあります。

  • 送信先やメッセージのテンプレートとかをツールで管理しなくて良い
  • 何度もメッセージを送らないように状態を DB で持たなくて良い
  • アラートを一時的に止めたりするのも簡単

また、時間軸でどれだけ CI が壊れた状態だったか、復旧するのにどのくらい時間がかかったか分かるのもなにかに使えるかもしれません。

今回のツールに限らず、 Slack に直接通知するより DataDog や Sentry を経由したほうが上手くいくことも結構あると思っています。

また、定期実行する方法としては Lambda 以外にも

  • Jenkins
  • 適当なサーバで cron
  • CI サービス
  • k8s の CronJob
  • k8s の Deployment

など色々あると思います。そういう風にも実行できるようにバイナリを今後提供したいと思っています。 Lambda を使うとインフラを管理しなくて良いのがメリットだと思います。

また、 DataDog API で結果を送る push 型のアーキテクチャとは別に、 DataDog Agent + Prometheus Exporter の pull 型もあるんじゃないかなと思います。 そうするとツール側で DataDog API Key が不要になるというメリットがあります。 こちらのパターンも今後実装してみたいと思います。

· 6 min read
Shunsuke Suzuki

自作の CLI ツール matchfile について紹介します。

https://github.com/suzuki-shunsuke/matchfile

この記事の執筆時点で最新バージョンは v0.1.1 です。

変更されたファイルの一覧から実行する必要のあるタスクを導出するための CLI ツールです。 Go で書かれていて、バイナリをダウンロードしてくれば使えます。

Pull Request (以下 PR) の CI では PR で変更されたファイルに応じて 必要なタスク(build, test, lint, etc) だけを実行したかったりします。

そこで、 PR で変更されたファイルパスのリストタスクが依存するファイルパスの条件 を元に、そのタスクを実行する必要があるか判定するためのコマンドとして matchfile を開発しました。

ただし、 matchfile の機能としては PR や CI とは独立しているので、もっと別の目的でも使えるとは思います。

matchfile は PR で変更されたファイルパスのリストタスクが依存するファイルパスの条件 を取得したりする機能はありません。

PR で変更されたファイルパスのリストci-info という自分が作った別のツールを使うと取得できます。

タスクが依存するファイルパスの条件 はタスクに大きく依存するので matchfile はカバーしていません。

matchfile の使い方としては

$ matchfile run <PR で変更されたファイルパスのリストが書かれたファイルへのパス> <タスクが依存するファイルパスの条件が書かれたファイルへのパス>

で、 PR で変更されたファイルパスのリスト のうち一つでも タスクが依存するファイルパスの条件 にマッチすれば true を、マッチしなければ false を標準出力します。 コマンドの exit code で結果を表現することも考えられましたが、そうすると set -e しているときに若干面倒くさいので、標準出力で表現しました。

ごく簡単な例を示します。

$ echo template/foo.tpl > changed_files.txt
$ echo template > template_dependencies.txt
$ matchfile run changed_files.txt template_dependencies.txt
true

タスクが依存するファイルパスの条件 は独自のフォーマットで指定します。 gitignore のフォーマットにインスパイアされていますが、正規表現が使えるなど、独自のフォーマットになっています。 CI の中でシェルスクリプトで動的に生成することを想定し、行指向のフォーマットになっています。 Go 実装のパーサーが提供されたよく知られた行指向の(コマンドで生成しやすい)フォーマットがあれば良かったんですが、見つからなかったので簡単にフォーマットを定義してみました。

[#][!][<kind>,...] <path>
...

1行に1つ条件を書きます。 上から全部評価されます(どれかマッチしても終わりません)。 # はコメントです。行の途中にコメントを書くことはできません。

! は gitignore と似ていますが、その行を評価する時点で評価結果が true であり、! を除いたその行の評価が true の場合、評価結果が false になります。 日本語が下手くそですね。

簡単な例を示すと、 foo/foo.txt 以外の foo ディレクトリ直下のファイル としたい場合、次のようになります。

glob foo/*
!glob foo/foo.txt

kindpath をどう扱うかを示していて、幾つか種類があります。

  • equal: ファイルパスが文字列として完全に一致すればマッチ
  • dir: ファイルパスが <path>/ で始まればマッチ
  • regexp: 正規表現
  • glob: グロブ。 ** はサポートされてません

kind はカンマつなぎで複数指定でき、複数指定した場合は、先に指定したものからマッチするかテストされ、一つでもマッチしたらその条件がマッチするものとして扱われます。

kind の指定は任意で、指定しない場合、 kind は equal,dir,glob として扱われます。

つまり

foo.txt

equal,dir,glob foo.txt

と同じです。

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