Skip to main content

· 9 min read
Shunsuke Suzuki

先日 aqua v0.1.0 をリリースした記事を書いたばかりですが、 そこから更に開発を続けて v0.5.0 をリリースしたので、変更点を紹介します。

基本的に Release Note に書いてあるとおりです。

  • PATH を project (aqua.yaml) 毎に設定する必要がなくなりました
    • ~/.aqua/bin を PATH に追加すればよくなりました
    • direnv などを使って環境変数を追加する必要がなくなりました
  • install コマンドに --test option を追加し、 file.src の設定が正しいかテストできるようになりました
    • CI で aqua の設定をテストするのに便利
  • GitHub Release だけでなく、任意の URL から tool のダウンロード・インストールができるようになりました
    • Go や helm, Hashicorp の product のような公式サイトからダウンロードするタイプのツールも install できるようになりました
  • Breaking Change: inline_registry の設定の形式を変更しました
  • aqua の設定の再利用性を高める Registry という仕組みを導入しました
  • 簡単な slide を公開しました: https://speakerdeck.com/szksh/introduction-of-aqua

PATH を project (aqua.yaml) 毎に設定する必要がなくなりました

aqua v0.1.0 では symbolic link を aqua.yaml のあるディレクトリの .aqua/bin 配下に作成しており、ここを PATH に追加する必要がありました。 direnv とかを使うと便利ですが、間接的に(?) aqua が direnv のようなツールに依存している形になり、微妙でした。

aqua v0.5.0 では symbolic link を ~/.aqua/bin 配下に作成するため、 .bashrc などで ~/.aqua/bin を PATH に追加しておけば project ごとに環境変数を追加する必要はなくなりました。 ちなみに作成される symbolic link は aqua-proxy へのリンクであり、ツールのバージョンには依存しないので ~/.aqua/bin を共有しても干渉することはありません。

~/.aqua/bin 配下に symbolic link を作って PATH に追加する場合、一つ大きな課題があります (だからこそ v0.1.0 ではプロジェクトごとに symbolic link を作っていました)。 ~/.aqua/bin 配下に symbolic link を作って PATH に追加すると、基本的にそのファイルが呼ばれることになります。 そのツールを aqua で管理しているプロジェクト配下ならそれで良いですが、そうでない場合、本来 aqua 以外でインストールしたものを実行したくても実行できません。 例えば homebrew で jq を install していて、あるプロジェクトでは aqua を使ってバージョンを固定したものを使いたいが、それ以外では homebrew で install したものを使いたいといった場合に問題になります。

この問題を解決するため、 aqua ではツールを呼び出す際に PATH をチェックして aqua-proxy へのリンクとなっているものは除外するというハック(?)のようなことをしています。

install コマンドに --test option を追加し、 file.src の設定が正しいかテストできるようになりました

地味な更新ですが、 aqua の設定を更新した際に CI でテストするのに便利です。 --test option なしだと、 warning は出力しますが、 exit code は 0 になります。

GitHub Release だけでなく、任意の URL から tool のダウンロード・インストールができるようになりました

ちなみに、 GitHub Release で公開されてないようなツールでも、 GitHub リポジトリで versioning されていて Renovate の github_release data source で自動更新できるケースは少なくないと思います。

Breaking Change: inline_registry の設定の形式を変更しました

小さな breaking change ですが、inline_registry の形式が変わりました。

AS IS

inline_registry:
- name: jq
type: github_release
repo_owner: stedolan
repo_name: jq
asset: 'jq-{{if eq .OS "darwin"}}osx{{else}}{{.OS}}{{end}}-{{.Arch}}'
files:
- name: jq

TO BE

inline_registry:
packages:
- name: jq
type: github_release
repo_owner: stedolan
repo_name: jq
asset: 'jq-{{if eq .OS "darwin"}}osx{{else}}{{.OS}}{{end}}-{{.Arch}}'
files:
- name: jq

aqua の設定の再利用性を高める Registry という仕組みを導入しました

これが一番大きな更新です。 aqua を使うにはツールのインストール方法を YAML で記述しないといけませんが、 これはちょっとした手間ですし、新規ユーザーにとって障壁となるでしょう。

ツールとそのバージョンを定義したらインストールできてほしいものです。 ツールとそのバージョンの定義は aqua.yamlpackages の部分なので、それ以外の設定を如何に簡略化するかという話になります。

Registry はツールのインストール方法の設定を、プロジェクト固有のバージョン設定とは独立させ、再利用可能な形で共有する仕組みです。

Registry には現状 4 種類あります。

  • inline regisry: aqua.yaml の中に直接 install 方法を定義する。 v0.1.0 からサポートされている方法
  • github_content registry: GitHub Repository にあるファイルを Registry として参照する方法
  • local registry: GitHub Repository にあるファイルを Regisry として参照する方法
  • standard registry: 自分がメンテしている github_content registry のエイリアス

inline registry

inline registry は従来からあるやつで、 aqua.yaml 内に定義する方法です。

inline_registry:
packages:
- name: jq
type: github_release
repo_owner: stedolan
repo_name: jq
asset: 'jq-{{if eq .OS "darwin"}}osx{{else}}{{.OS}}{{end}}-{{.Arch}}'
files:
- name: jq

シンプルではありますが、コピペする以外に再利用性がありません。

local registry

local registry はローカルにあるファイルを参照する registry です。 絶対パスか、 aqua.yaml からの相対パスを指定します。

github_content registry

ユーザーとしては次のように Registry を定義すればあとは packages で Registry を参照できます。 GitHub Access Token を環境変数 GITHUB_TOKEN として設定する必要があります。

registries:
- name: suzuki-shunsuke/aqua-registry
type: github_content
repo_owner: suzuki-shunsuke
repo_name: aqua-registry
ref: v0.2.0 # renovate: depName=suzuki-shunsuke/aqua-registry
path: registry.yaml

packages:
- name: conftest
registry: standard
version: v0.27.0 # renovate: depName=open-policy-agent/conftest

Registry を公開する

自分で Registry を公開したい場合は GitHub Repository に設定ファイルを置くだけで OK です。 e.g. https://github.com/suzuki-shunsuke/aqua-registry/blob/main/registry.yaml

Standard Registry

Standard Registry も作りました。

https://github.com/suzuki-shunsuke/aqua-registry

jq や gh, kubectl, Terraform など有名なツールはこの Registry を使えばインストールできますが、 当然 PR も受け付けているので、追加してほしいツールがあれば PR を投げてください。

Official Registry を github_content Registry として利用することも当然できますが、 より簡潔に書けるように type: standard という Registry がサポートされています。

AS IS

registries:
- name: standard
type: github_content
repo_owner: suzuki-shunsuke
repo_name: aqua-registry
ref: v0.2.0 # renovate: depName=suzuki-shunsuke/aqua-registry
path: registry.yaml

TO BE

registries:
- type: standard
ref: v0.2.0 # renovate: depName=suzuki-shunsuke/aqua-registry

上 2 つは等価ではありますが、後者のほうが簡潔です。

· 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.

· 10 min read
Shunsuke Suzuki

GitHub Repository の CI に CodeBuild を使う場合、 CodeBuild の Webhook integration (以下 CodeBuild GitHub integration と呼ぶことにします) を使うのが一番自然でしょう。 基本的なユースケースならこれでよいのですが、 GitHub App を活用することでより高度な CI を実現することができます。

解決したい課題

  • Batch Build の課題
    • 起動・終了が遅い
    • 全 build が成功した Batch Build を Retry できない
    • Web UI がわかりにくい
      • 余計な build が起動する
      • build 単体を Retry できない
    • build ごとに条件設定とかできない
    • buildspec を動的に生成できない
  • CodeBuild GitHub integration の課題
    • Build Project ごとに Repository Webhook が 1 つ作られる
    • Filter の条件が限られている(例えば PR label で filter とかできない)
    • 複数の build を実行できない(Batch Build も 1 つとみなした場合の話)
  • CodeBuild の課題
    • Retry した場合 webhook で起動したときの環境変数が設定されない

GitHub App

Amazon API Gateway と Lambda を使って GitHub App を構築します。 Lambda で webhook を受け取り、 AWS SDK を使って build を実行します。

codebuilder-architecture

GitHub App を作成し、 Webhook URL として API Gateway の endpoint を指定します。 internet facing な API Gateway は Repository ごとに作るのではなく、共有で 1 つ作るようにしたほうが良いかと思います。 Lambda は Repository ごとに分けたほうが権限を絞れるし、 Function がシンプルになるし、あるリポジトリのための変更が他のリポジトリに影響することがないので良いでしょう。

Batch Build を使う代わりに Build を複数並列で起動することで、 Batch Build の課題を解決できます。 実行時にパラメータを変えることもできます。 Webhook の Payload も参照できるので、 Payload から得たデータ(PR 番号、 label、PR Author 名、 etc)を環境変数として build にわたすこともできます。

従来は build 内で GitHub API を使って取得していた PR の情報を環境変数として渡せることで build の処理が簡略化されますし、 GitHub API の call 数を減らすこともできます(これは API の rate limit が問題になる場合に重要です)。

PR label による build のフィルタリングなど、 CodeBuild GitHub integration では難しいより複雑な filter も実現できます。

GitHub App であれば Repository Webhook が作られることもありませんし、 Build Project ごとに webhook の設定をする必要もありません。

Assume Role することで別の AWS Account の Build Project の build を実行することもできます。

merged event で merged commit sha で build を実行

PR がマージされたら merged commit の SHA で CI を実行したい場合、 push event を hook するのがおそらく一般的かと思います。

CodeBuild GitHub integration では merged event を hook することもできますが、 この場合 PR の head branch の SHA で build が実行されてしまいますし、 base branch の commit status が更新されません。

しかし push event では関連する PR の情報が取れない(build の環境変数として PR の情報が渡されない)という問題があります。 GitHub API でコミットと関連した PR の一覧が取れますが、複数の PR と関連づいている場合、どの PR なのか特定することができません(特定の条件付きであればできますが)。

そこで GitHub App で merged event (正確には closed event で PR がマージ済の場合) を hook しつつ、 build 起動時に source version として merged commit sha を指定することで webhook から PR の情報を取得しつつ merged commit の SHA で CI を実行できます。

GitHub の Personal Access Token の代わりに GitHub App installation access token を使う

CodeBuild の build 内で GitHub API を使いたい場合、 Personal Access Token を発行するのがシンプルですが、 Personal Access Token にはいくつか課題があります。

  • token 流出のリスク
    • rotation が難しいので有効期限が設定されてない場合が多い
  • rate limit
    • token が organization で広く共有されたりするようになると問題になりやすい
    • rate limit は account 単位なので、同じ account の token を別に作っても意味がない
    • account を増やして org に追加すると、 org のメンバーが増えるのでお金がかかる(まぁ金額的に無視してもよいかもしれませんが)

GitHub App では rotation などを考えなくても一時的な token を発行できるので、セキュリティ的にリスクが低いですし、 GitHub App を作ってもお金はかかりません。 token を発行するには

  • App ID
  • Installation ID (webhook の payload に含まれている)
  • Private key

が必要で、 build 実行時に installation id を環境変数として渡すことで Lambda だけでなく Codebuild でも installation access token が使えます。

もちろん GitHub App を使っても rate limit に引っかかることはありますが、 Personal Access Token に比べて回避しやすいかとは思います。

Lambda から Repository のコードを参照する

https://blog.studysapuri.jp/entry/2020/12/03/080000 では CodeBuild でリポジトリのコードを checkout し、 build の中で動的に buildspec を生成して batch build を実行しています。

同じようなことを Lambda でやろうとした場合、色々制約があります。 Lambda ではリポジトリを checkout してくる代わりに、 build を実行するのに必要な情報を静的に生成してリポジトリにコミットしておきそれを GitHub API で取得するというやり方があります。 その場合、ファイルを生成するコマンドをリポジトリに用意しておき、 ちゃんとファイルが最新になっているか CI の中でチェックし、 なっていなければ CI を fail させるか自動で更新してコミットするというのをやるのが良さそうです。 Git の pre-push hook などで check するのもありかもしれません。

GitHub App の開発をいかに楽にするか

ようは webhook を受け取って build を実行する Lambda Function を実装すればいいだけなのですが、 毎回 0 からコードを書くのはちょっとした手間なので、 Go の簡単な library を作っています。

https://github.com/suzuki-shunsuke/go-github-app-for-aws-codebuild

このライブラリ自体は大したものではないので、皆さんのユースケースや言語に合わせて独自に作っても良いでしょう。

サンプル

簡単なサンプルも書いています。

https://github.com/suzuki-shunsuke/example-github-app-for-aws-codebuild

· 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

· 3 min read
Shunsuke Suzuki

OPA で Table Driven Tests っぽく Policy を Test する方法について考えたので紹介します。

背景

先日 Open Policy Agent Rego Knowledge Sharing Meetup で発表する機会を頂きました。 発表の資料はこちら。 普段他社の事例を聞いたり OPA について話たりする機会がないので、非常に貴重な時間になりました。

その中で deeeet さんが Table Driven Tests っぽくテストしたいというようなことをおっしゃっていました。

だいたいこの辺: https://youtu.be/0YpJhrz6L0A?t=2990

その話を受けて改めて自分で考えてみたところ、できなくはないんじゃないかなという気がしたのでちょっとやってみることにしました。

サンプル

せっかくなので簡単なサンプルを GitHub に用意しました。

https://github.com/suzuki-shunsuke/example-opa-table-driven-tests

今回は aws_cloud_watch_log_group の retention_in_days が設定されていることをチェックする Rule の Test をします。

テストケースを seeds という list で定義し、どれか一つでも false だったら fail するようにしています。 テストケースの中身は

  • msg: テストケースを示すメッセージ。テストが失敗したときの trace に含める
  • resource: rule の input
  • exp: rule の評価結果の期待値

になっています。

test の中身は別の rule を否定しているだけになっていますね。

test_deny_aws_cloudwatch_log_grop_retention_in_days {
not any_deny_aws_cloudwatch_log_grop_retention_in_days
}

この書き方は Universal Quantification (FOR ALL) で説明されています。

Set の比較は !=, == で大丈夫です。 https://www.openpolicyagent.org/docs/latest/policy-language/#sets

    result != seed.exp

test に失敗した場合に、どのテストケースでなぜ失敗したのかわかりやすいように必要な情報を trace で出力するようにしています。

    trace(sprintf("FAIL %s (%d): %s, wanted %v, got %v", ["test_deny_aws_cloudwatch_log_grop_retention_in_days", i, seed.msg, seed.exp, result]))

Conftest の場合、 --trace をつけると出力されます。 Note で grep するとわかりやすいです。

$ conftest verify --trace | grep Note
TRAC | | | | Note "FAIL test_deny_aws_cloudwatch_log_grop_retention_in_days (1): retention_in_days should be greater than 0, wanted {\"aws_cloudwatch_log_group.main: retention_in_days should be set and greater than 0\"}, got set()"

以上、簡単ですが Rego で Table Driven Tests っぽく test を書く方法を紹介しました。

· 5 min read
Shunsuke Suzuki

Terraform で空の AWS Lambda Function を作ろうとした際にちょっとハマったのでやり方を書いておきます。

「空の Lambda Function」という表現は適切ではないかもしれませんが、 Lambda で実行するコードのデプロイは Terraform 以外のツールでやるけど、 Lambda Function の作成は Terraform で行うので、 dummy のコードを指定して Terraform で Lambda を作るという話です。

自分は今は lambroll というツールで Lambda をデプロイしています。 lambroll は Lambda Function も作ってくれるので Terraform で作る必要は必ずしもありません。

しかし Lambda Function に関連するリソースを Terraform で管理する場合、 Lambda Function も Terraform で作ると Lambda Function の ARN や Invoke ARN を参照できます。

また lambroll でデプロイする場合も先に Terraform で IAM Role を作成する必要がありますが、 Terraform で aws_lambda_permission のようなリソースを作成するには Lambda Function が先に作られている必要があるので、 互いに依存関係が発生し、面倒なことになります。

また Lambda Function の削除も Terraform でできるようになります。

なので、 Terraform で Lambda Function を作っておいたほうが色々都合が良いです。

Terraform で作成と削除は行うものの、更新をしたいわけではないので、 ignore_changes = all を指定します。

https://www.terraform.io/docs/language/meta-arguments/lifecycle.html#ignore_changes

Lambda Function を Web UI などから作る場合 Function code はなくても大丈夫ですが、 Terraform で Lambda Function を作る場合、 filenameimage_uri, s3_bucket のいずれかが必須になります。 これは issue もありますが、仕様のようにみえます。

https://github.com/hashicorp/terraform-provider-aws/issues/5945

ECR や S3 に dummy のコードを用意するというのも一つの手ですが、環境に依存するのがあまり良い気がしないので、 archive_file data source を使って dummy の zip ファイルを生成するという方法を取ることにしました。

https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/archive_file

次のようなコードで CI で terraform apply を実行しましたが、 zip file がないと言われて失敗しました。

resource "aws_lambda_function" "main" {
# https://www.terraform.io/docs/language/meta-arguments/lifecycle.html#ignore_changes
# Terraform can create and destroy the remote object but will never propose updates to it.
lifecycle {
ignore_changes = all
}

function_name = "foo"
role = aws_iam_role.main.arn

handler = "bootstrap"
runtime = "provided.al2"
filename = data.archive_file.dummy.output_path
}

data "archive_file" "dummy" {
type = "zip"
output_path = "${path.module}/dummy.zip"

source {
content = "dummy"
filename = "bootstrap"
}
}
Error: unable to load "lambda-base/dummy.zip": open lambda-base/dummy.zip: no such file or directory

しかしローカルで terraform plan, apply を実行してみても再現しませんでした。

CI では Pull Request で plan file を生成して S3 に plan file を upload し、 PR をマージした default branch では terraform plan を実行せずに S3 から plan file をダウンロードして terraform apply を実行しています。

Pull Request の terraform plan の実行結果を S3 に保存して安全に apply | Quipper Product Team Blog

plan file を指定して terraform apply を実行した際には zip file が作成されず、上記のエラーが発生することがわかりました。

関連する issue もありました。 https://github.com/hashicorp/terraform-provider-archive/issues/39

この issue では幾つかの解決方法が紹介されています。ちなみに 2021-06-24 現在 Hashicorp 側からは特に反応がないように見えます。 random_uuid や random_string を使った方法もありますが、 Lambda を作成するだけなら null_resource に依存させるだけで十分のように思えました。

data "archive_file" "dummy" {
type = "zip"
output_path = "${path.module}/dummy.zip"
source {
content = "dummy"
filename = "bootstrap"
}
depends_on = [
null_resource.main
]
}

resource "null_resource" "main" {}

このように null_resource に依存させると terraform plan では zip file が作られず、 terraform apply ではじめて zip file が作られるため、 terraform apply が失敗することはなくなりました。

· One min read
Shunsuke Suzuki
  • SRE チームの新メンバーのオンボーディングのサポート
  • GCP
    • dev からのリクエストに応じて権限付与したり対応
    • Terraform による GCP の管理 CI/CD の整備
    • Workload Identity Federation について調べた
    • Terraform による IAM 管理の仕方を検討
  • Conftest
    • opa fmt によるフォーマット(CI も導入)
    • Policy Testing (CI も導入)
  • Upgrade Terraform to v0.15.4
  • miam の Terraform 移行を検証

· One min read
Shunsuke Suzuki
  • SRE チームの新メンバーのオンボーディングのサポート
  • Lambda の Monorepo
    • 幾つか実際に Function 作った(developer support)
    • 幾つかの Release Strategy の実装・検証
      • シンプルな GitHub Flow
      • Git Flow をアレンジしたリリースフロー
      • Canary Release
      • WIP: AWS AppConfig を用いた Dark Launch
  • IAM User の初期パスワード送信の自動化
  • Terraform
  • GCP の Terraform 管理
    • 調査
    • WIP
  • Conftest
    • 社内の Rego の活用事例をまとめた
    • opa fmt によるフォーマット(CI も導入)
    • Policy Testing (CI も導入)