『JSJ 358: Pickle.js, Tooling, and Developer Happiness with Anatoliy Zaslavskiy』を聴いて
僕のお気に入りのPodcastの一つにJavaScript Jabberというものがあります。
「JavaScript」とPodcastのタイトルには書いてありますが、ゴリッとJavaScriptのテクニカルな話をするわけではないです(昔はそうだったかも)。 どちらかと言うとJavaScript界隈に何かしら関わりのある人物をゲストに呼んで、その人と一つのテーマについて1時間ほどトークをするという流れです。 ホストは Charles Max Wood氏 です。Ruby Rogues, Adventures in Angularなど様々なPodcastを配信している方です。
なんとなーくジョギングなりしながら聴いているのですが、先日聴いたエピソードが結構面白かったので紹介したいと思います。英語ですが、トピックに興味がある方はぜひ聴いてください!
Pickle.js, Tooling, and Developer Happiness
今回のゲストはAnatoliy Zaslavskiy氏というHover Inc.所属のSenior JavaScript R&D Developerでした。
当初のテーマはきっとAnatoliy氏が作ったPickle.jsと自動テストに関する話だったと思うのですが、途中からDeveloper Happinessに話が逸れ(?)ます。
このDeveloper Happinessに関する議論が特に興味深かったです。
目次
- [00:00] イントロ(Pickle.jsの紹介)
- [11:38] テスト文化にまつわるよくある問題
- [17:30] 自動テストの意義
- [28:24] Employee Retention(従業員をどう定着させるか)
- [38:24] なぜSeniorよりJunior Developerを雇うほうがスケールするのか
- [47:10] ツールやライブラリを作っていく上で注意すること
- [54:15] Picks
時間は、Podcast内でトピックが話されているだいたいの時間を示してます。Picksとは、毎回Podcastの最後に参加者が全員何かしら記事だったり技術ネタだったり、視聴者にシェアしたい内容を紹介するコーナーです。
Pickle.js
ゲストのAnatoliy氏が作っているPickle.jsというのはe2eテストフレームワークです。
僕もまだ触れていないので、このPodcastと公式サイトから得た情報をまとめると
- Cucumber っぽく(Gherkin Syntaxで)ノンプログラマーでもテストを書ける
- 既存のテストフレームワーク(執筆時点ではCypressだけ)を拡張して実装しているので、理論上はテストフレームワークを変えてもテストが動く
- ページ内の要素を選択するためのセレクターは一箇所にKey Valueで定義し、そのKeyをテストの文章内で使用できる
ポイントは以下のスライドでも簡潔に紹介されています。
僕の中ではpython製の Robot Framework に似てるのかなという印象があります。
自動テストがなぜ必要か?
なぜ自動テストが必要なのかというトピックについて、「自動テストの無い大企業でのプロジェクトの問題」が例に出されています。
- プロジェクトに多数のエンジニア(数100)が関わっている
- ある機能を開発者が実装する
- QAがその機能をテストして、1週間後にバグなどフィードバックする
- 開発者はバグを修正してQAに再び渡す
- また1週間後に、今度はさらに発生したバグなどフィードバックする
ここでの大きな問題としては開発者が、自分の実装した範囲が他にどのような影響を及ぼすのかということを知る術が無い(コード全体を熟知しない限り)。
そして、何かを壊してしまった場合に即座にフィードバックを得ることができないという点です。
流れが悪いために時間はどんどん過ぎていき、エンジニアとしてはほとんどバグを直していることしかしていないという負のスパイラルに陥ります。
自動テストを導入することで、フィードバックのスピードが劇的に早くなるため、上の問題の改善に大きく貢献するというトークをしています。
自分が行った変更が 既存の機能を壊していない ということが 一定のレベル保証される ことで、開発者も変更を加えるときに心理的安全性を保つことができるようになります。
トーク内でも「Eliminate the fear of change(変更することへの恐怖を排除する)」することで「Innovative(革新的)」になれると言っています。
この意見には僕もとても賛同しているのですが、「一定のレベル保証される」というところが、「良いテスト」を書いていることが前提になっていることを忘れちゃいけないのかなとは思います。
「良いテスト」の定義がまた難しいのと、トーク内にも特に触れられていない部分なのですが、以下の書籍の内容が僕は好きです。
The Way of the Web Tester: A Beginner's Guide to Automating Tests
- 作者: Jonathan Rasmusson
- 出版社/メーカー: Pragmatic Bookshelf
- 発売日: 2016/10/02
- メディア: ペーパーバック
- この商品を含むブログを見る
日本語版はこちら
- 意味のあるテストになっているか
- 壊れやすいテストを書いていないか
- 本当に意図したテストを行っているか(テスト自体がバグってないか)
などいろいろな観点がありますが、「ただテストをたくさん書けばいい」というわけではないという点がポイントです。
Developer HappinessとEmployee Retention
後半はDeveloper HappinessとEmployee Retentionに関する話題になります。ここが結構ヒートアップして前半の内容が薄れちゃいます(笑)
なぜエンジニアは会社を辞めるのか?
エンジニアが会社を辞める理由としてCharles氏は2つの観点を述べています。
- 人(People)
- プロジェクトそのもの(Project)
自分と「合わない」人と毎日仕事を続けるのはしんどいですし、そもそもプロジェクトが解決しようとしている問題に興味が無ければモチベーションも上がりません。 これには僕も同意見です。これに加えて「給料」も結構大きな理由なのではないかと思います。
同じ仕事条件で、より給料の高いオファーがあったとしたら会社を変えるのは結構自然だと思います。
ただし、「仕事条件」は僕の中では「仕事場の環境(人)」も含まれていると思うので、 単純な比較にはならない とも思います。
Anatoliy氏は「辞める」のは「Unhappy」だからであり、「Unhappiness」は「Cognitive dissonance(認知的不協和)」から来ると述べています。
- 自分がやるべきこと
- 自分が実際にやっていること
のギャップに苦しんでいるが、その「現状の状態」に慣れ過ぎちゃっているがために、どうしたら改善できるか 想像ができない状況に陥ってしまう と。
この状況に陥ったときにどうすべきかの意見がCharles氏とAnatoliy氏で異なった点が面白いです。
IT業界は今のところ人手不足なので、転職してしまうのが一番シンプルだとCharles氏は述べています。
それに対してAnatoliy氏は恋愛を例に出します。
「好きだと思った女性と付き合うが、付き合ってみたらなんか違ったので別れてまた違う女性と付き合う。」
ということを繰り返して良いのかどうかという点に注目します。
「問題は付き合っている女性側にあるのではなく、自分にあるのではないか。」
という視点の切り替えが必要なのではないかと問います。
この視点の切り替えとは「課題の分離」を行うということなのかなと僕は感じました。
- 自分の課題
- 他者の課題
を整理することで、自分の課題に気づいたり、他者の課題に踏み込んでしまっていることに気づいたりできると思います。
「課題の分離」について以下2つの書籍を最近読んで僕も「なるほどー」とうなづく点が多かったので合わせて載せておきます!
- 作者: 岸見一郎,古賀史健
- 出版社/メーカー: ダイヤモンド社
- 発売日: 2013/12/13
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (113件) を見る
「課題の分離」を行うことで、「現状」を改善する道筋が見えてくるかもしれないというのは確かにその通りだなと感じました。
ただし、それでも尚道筋が見えない環境、抜け出した方が良い環境(Toxic Environment)もあるというのは双方で一致している意見でしたし、僕もそう思います。
補足:僕は付き合う相手を変えることについて否定的ではないです。自分に本当に合う人をそうやって見つけられるとも思います!
エンジニアのRetentionを上げる
エンジニアのRetentionを上げるポイントとして述べられているのは「エンジニアは多種多様であると理解すること」と言っています。
- いろんな感情を持っている
- 興味の対象は人それぞれ
- ワークスタイルも人それぞれ
人それぞれで異なるものをうまく組み合わせて活用(utilize)できるようにする、と述べていますがこれはとても難しいと思います。
人・組織・リーダーシップに関わってくる話だと僕は捉えていますが、まだまだ僕はこの点について勉強中で、特に以下の書籍を読んで整理したいと思っています。
エンジニアリング組織論への招待 ~不確実性に向き合う思考と組織のリファクタリング
- 作者: 広木大地
- 出版社/メーカー: 技術評論社
- 発売日: 2018/02/22
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (3件) を見る
スモール・リーダーシップ チームを育てながらゴールに導く「協調型」リーダー
- 作者: 和智右桂
- 出版社/メーカー: 翔泳社
- 発売日: 2017/09/11
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (4件) を見る
- 作者: トム・デマルコ,ティモシー・リスター,松原友夫,山浦恒央
- 出版社/メーカー: 日経BP社
- 発売日: 2013/12/18
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (10件) を見る
また、 僕のブログメンターでもある カックさん の以下記事も参考になる情報満載なので要チェックです!
まとめ
Developer Happinessの議論がとても勉強になりました。「これだ!」という単純な答えが無いのが難しいんですけど、様々な人のこう言った話を聴きながら、自分の生き方も改善していきたいと強く感じました。
後半のトークが濃すぎてPickle.jsの話がだいぶ薄れた印象があります(笑)Pickle.jsのコンセプトは面白いので試してみたいです。
参考
- Testabilityという観点で画面(View)の状態をカタログのように可視化して管理できるStorybookが紹介されました
- Picksで紹介されたDevOpsの本。物語になっていて読みやすくて面白い本です。
- 作者: ジーン・キム,ケビン・ベア,ジョージ・スパッフォード,榊原彰,長尾高弘
- 出版社/メーカー: 日経BP社
- 発売日: 2014/08/12
- メディア: 単行本
- この商品を含むブログ (2件) を見る
OPAでANDとORの条件を組み合わせる
OPAのSlackでやりとりされている内容とかが流れていってしまうのが勿体無いので残していこうと思います。(将来的に索引にできたら良いかなと)
上図のようなパターンを考えてみましょう。文で表現すると以下のとおりです。
p1, p2, p3, additional_pがすべてtrueの場合に許可する。additional_pはp9またはp10がtrueであれば良い。
これをRegoで書くと次のようになります。
package play default allow = false # this rule says "allow is true if p1 and p2 and p3 and additional_p" allow { p1 p2 p3 additional_p } p1 { input.x == 1 } p2 { input.y == 2 } p3 { input.z == 3 } # additional_p is true if p9 or p10 additional_p { p9 } additional_p { p10 } p9 { input.a == "foo" } p10 { input.a == "bar" }
ポイントは以下のとおりかと!
- ルールから別のルールを参照できる
- ルールの中身に書いた式は暗黙的にANDとして扱われる(allowの中のp1, p2, p3, additional_p)
- 同名のルールは暗黙的にORとして扱われる(additional_p)
試してみましょう!
次のJSONをInputに入れてみます。
{ "a": "bar", "x": 1, "y": 2, "z": 3 }
そうするとOutputは次のようになります。
{ "result": { "additional_p": true, "allow": true, "p1": true, "p10": true, "p2": true, "p3": true } }
true
の結果が全部返ってくるのでちょっと見づらいですけど、 allow
が true
なのがわかります。
Inputの "a": "foo"
とした場合でも allow
は true
です!
なぜなら p9 OR p10
であれば( input.a
が foo
または bar
であれば ) additional_p
は true
だからです。
Rego Playgroundは下になりますので実際にInputを変えてみてOutputを確認してみると動きの違いを実際にみることができます!
おわりに
- PolicyのORやANDは暗黙的に行われる
- ルールの中からルールを呼び出すことでルールを整理できる
参考
CircleCIで実行バイナリをDLしてCI内でキャッシュしながら使う
CI内でどこかしらから実行バイナリをダウンロードして、それを使うことがあると思います。 最近では僕はOPAのテストをCircleCIで実行したかったので、OPAのバイナリをダウンロードしてテストを実行しました。
毎回バイナリをダウンロードしてCircleCIを回すのもコストが高いので、CircleCIのキャッシュを使う方法について共有します!
実行バイナリをDockerイメージに入れてそれをCIで使う方法もありますが、今回は実行バイナリをダウンロードして使う方法でやります!
キャッシュせずにやった場合
まずはキャッシュのことを考えずにCIしてみると、以下のような流れになります。(OPAの例です)
version: 2 jobs: build: docker: - image: circleci/buildpack-deps:jessie steps: - checkout - run: name: Download OPA command: | mkdir .bin wget -O .bin/opa https://github.com/open-policy-agent/opa/releases/download/v0.10.6/opa_linux_amd64 sudo chmod +x .bin/opa - run: name: Test Policies command: .bin/opa test .
- チェックアウト
- OPAのバイナリをGitHubからダウンロードして実行権限付与
- OPAのテスト実行
という感じです。特につまるところは無いと思います。
ただしこれにはパフォーマンスの問題があって、CIの度にOPAのバイナリをGitHubからダウンロードしてくるのが無駄です。 このバイナリもそんなにしょっちゅう変わるものでも無いので、ダウンロードが一度で済ませられたら良いですよね。
そこで使えるのがCircleCIのキャッシュです!
キャッシュして効率化
CircleCIには save_cache
と restore_cache
という便利な機能があります。
キーを基にCircleCI側にファイルやディレクトリなどをキャッシュしておくことができます。
よく見る場面としては、依存パッケージなどをキャッシュしておくときです。( node_modules
や vendors
ディレクトリなど)
ポイントとなるのがキャッシュの「キーの決め方」です。
まずはあまり工夫をせずにキャッシュの手順を加えて見ます。
version: 2 jobs: build: docker: - image: circleci/buildpack-deps:jessie steps: - checkout - restore_cache: key: opa-cache-0.10.6 - run: name: Download OPA command: | if [ ! -f .bin/opa ]; then mkdir .bin wget -O .bin/opa https://github.com/open-policy-agent/opa/releases/download/v0.10.6/opa_linux_amd64 sudo chmod +x .bin/opa fi - run: name: Test Policies command: .bin/opa test . - save_cache: key: opa-cache-0.10.6 paths: - .bin
- チェックアウト
- キャッシュがあれば復元
- OPAのバイナリが 無ければ GitHubからダウンロードして実行権限付与
- OPAのテスト実行
- OPAのバイナリをキャッシュしておく
restore_cache
と save_cache
のキーには opa-cache-0.10.6
を使っています。
これは、OPAのバイナリのバージョンが変わったらキャッシュもちゃんと変わるようにするためです。
ただし、これってバージョンが変わるたびに少なくとも3箇所( 0.10.6
となってる部分)は変えないといけないので微妙ですよね。
ということでどうにか1箇所に集約できないかと考えてしまいます。
ちょっとトリッキーなのが、環境変数が restore_cache
や save_cache
の key
に使えない点です(少なくとも僕の2019/04/19時点での認識では)。
- restore_cache: key: opa-cache-${OPA_VERSION}
上記のように書けるとすっきりしそうなんですけど、これができません。ではどうするかというと、以下のフォーラムでやり方を見つけることができました。
次のように書き換えることができます。
- run: name: Setup Environment Variables and Version file command: | echo 'export OPA_VERSION="0.10.6"' >> $BASH_ENV echo "${OPA_VERSION}" > _opa_version_ - restore_cache: key: opa-cache-{{ checksum "_opa_version_" }}
- 環境変数OPA_VERSIONにバージョン番号を入れて
- バージョン番号を
_opa_version_
というファイルにリダイレクト _opa_version_
のchecksum
をキャッシュのキーとして使う
言われてみれば「あ、その手があったか!」ってなります。
ということでこの方法で config.yml
を書き換えると下のようになります。
version: 2 jobs: build: docker: - image: circleci/buildpack-deps:jessie steps: - checkout - run: name: Setup Environment Variables and Version file command: | echo 'export OPA_VERSION="0.10.6"' >> $BASH_ENV echo "${OPA_VERSION}" > _opa_version_ - restore_cache: keys: - opa-cache-{{ checksum "_opa_version_" }} - run: name: Download OPA command: | if [ ! -f .bin/opa ]; then mkdir .bin wget -O .bin/opa https://github.com/open-policy-agent/opa/releases/download/v${OPA_VERSION}/opa_linux_amd64 sudo chmod +x .bin/opa fi - run: name: Test Policies command: .bin/opa test . - save_cache: key: opa-cache-{{ checksum "_opa_version_" }} paths: - .bin
こうすることで下の一行を変えるだけで、OPAの実行バイナリのバージョンを切り替えることができます!
echo 'export OPA_VERSION="0.10.6"' >> $BASH_ENV
今の所の僕の最適解はこのやり方になるのですが、他にも良い方法があればぜひとも知りたいところです!
おわりに
- 依存ライブラリ以外にもCircleCIの
save_cache
やresotre_cache
は使える - 一工夫すればキャッシュのキーに環境変数で設定したバージョン番号を指定できる
参考
ちょうど「CircleCI 福岡ミートアップ」が開催されて、その中でもキャッシュについて触れている発表があったようです!
OPAのテストをCircleCIに乗せる
OPAのPolicyをテストする方法について前回は紹介しました!
OPAのバイナリさえあれば opa test
コマンドで簡単にテストが実行できます。
ということでOPAのPolicyをCIに乗せて継続的にPolicyのテストができるようにしてみます。
どんなCIでもできますが、今回はCircleCIで試してみたのでその方法を共有します。
リポジトリの準備
Policyを書いたRegoファイルは 前回 のものをそのまま使います。
ファイルの構成は以下のとおり。これがGitHubのリポジトリに置いてあることを前提にします。
. ├── iam.rego └── iam_test.rego
ここに .circleci/config.yml
を作ります。
mkdir .circleci # エディタで編集 vim .circleci/config.yml
YAMLの中身は↓
version: 2 jobs: build: docker: - image: circleci/buildpack-deps:jessie steps: - checkout - run: name: Setup Environment Variables and Version file command: | echo 'export OPA_VERSION="0.10.6"' >> $BASH_ENV echo "${OPA_VERSION}" > _opa_version_ - restore_cache: keys: - opa-cache-{{ checksum "_opa_version_" }} - run: name: Download OPA command: | if [ ! -f .bin/opa ]; then mkdir .bin wget -O .bin/opa https://github.com/open-policy-agent/opa/releases/download/v${OPA_VERSION}/opa_linux_amd64 sudo chmod +x .bin/opa fi - run: name: Test Policies command: .bin/opa test . - save_cache: key: opa-cache-{{ checksum "_opa_version_" }} paths: - .bin
これで概ね準備完了です!
書いてる内容はちょっと多いのですがやってることは下の通り。
- リポジトリのチェックアウト
- ダウンロードするOPAのバージョンを環境変数に設定
- OPAバイナリのキャッシュがあれば復元する
- キャッシュが無ければダウンロードして実行権限与える
- Policyを
opa test
でテストする - 次から再利用できるようにOPAバイナリをキャッシュしておく
CircleCIを設定
以下の公式ドキュメントにもありますが、CircleCIに対象となるリポジトリを追加します。
下の画面に表示されている「Start building」をクリックすることでCIが走ります!
正常にCIが走ればテストも通るはずなので、下のように成功画面になります。
では、わざとテストが失敗するように下のように iam_test.rego
を編集しましょう!
新しいbranchで変更し、pushしてからPRを作ってみます。
pushした時点でCIが走るのでPRをGitHub上で作るころには失敗して、下のような画面になっているはずです!
このように、CIに乗せて継続的にPolicyのテストも実行していくことができます。
テスト好きにはたまらないですね!僕はテスト大好きなのでワクワクしてしまいます。
今回のコードは以下リポジトリに置いてますので、参考になればと思います!
おまけ
バッジもREADMEに載せてちょっと今風にしてみました!
CircleCIでプロジェクトの Settings に行って、Status Badgesに移動します。
すると、README.mdとかに貼り付けられるCircleCIのステータスバッジが下図のように記載されています。
これを貼り付ければ、下のようにCIの状況が表示されるバッジを載せることができます!
こういう取り組みも地味にモチベーションに繋がったりするので大事にしたいです。
OPAでテストコードを書く
OPAでPolicyをRegoで書いていくという話は何度かしてきました。
では、そのPolicyを正しく書けたかどうかはどう判断するのでしょう? これまで扱ってきた記事ではInputを用意してOutputをチェックしてました。 それって大変ですし、Policyが鬼のようにたくさんあった場合、デグレチェックも大変です。
そこで登場するのがOPAのテストコードの仕組みです!公式ドキュメントでも詳しい説明があります。
本記事では、以前とりあげたIAM Policyの例を使ってテストコードを書いてみます!
上の記事でテストしたパターンは以下の4つです。
- iam:ChangePasswordが 許可 されるパターン
- s3:ListAllMyBucketsが 許可 されるパターン
- s3:ListBucketが 許可 されるパターン
- s3:CreateBucketが 拒否 されるパターン
対象となるRegoは以下のとおり。
package aws allow { input.action == "iam:ChangePassword" } allow { input.action == "s3:ListAllMyBuckets" } allow { actions_match resources_match } actions_match { actions := ["s3:List.*","s3:Get.*"] action := actions[_] regex.globs_match(input.action, action) } resources_match { resources := ["arn:aws:s3:::confidential-data","arn:aws:s3:::confidential-data/.*"] resource := resources[_] regex.globs_match(input.resource, resource) }
テストの書き方
Regoのテストは(当たり前かもしれませんが)Regoで書きます!
ルールに test_
というprefixをつけることでOPAが暗黙的にテストだと認識します。
opa test
を実行した場合もOPAは test_
で始まるルールを集めてテストを実行するようになってます。
では、さっそくテストを順に書いていってみましょう!
テスト用のルールはどこに書いてもいいんですけど、 iam_test.rego
ファイルを作ってそこに書きましょう。
パッケージは同じにしておきたいので、先頭に package aws
を忘れずに書きます。
iam:ChangePasswordが許可されるパターン
まずは以下のルールをテストしてみましょう。
allow { input.action == "iam:ChangePassword" }
これはすごくシンプルで、 input.actionがiam:ChangePasswordだったら許可する
です。
そのままですね!テストも実はかなりそのまま書くことができます。
test_change_password_action_allowed { allow with input as { "action": "iam:ChangePassword" } }
test_
以降はわかりやすい、好きな名前をつけると良いです。
で、テストしたいルール(この場合 allow
)を書いて、
input
の中身を指定したいので with
と as
を使ってテストデータを注入します。
as
以降にはJSONをそのまま書くことができます!
OPAのバイナリを持ってる場合は以下のコマンドでテストが実行できます。
注意:iam.regoとiam_test.regoがあること
opa test .
VSCodeで拡張を入れている方は OPA: Test Workspace
というコマンドが用意されているので、
それを実行することで結果を見ることも可能です。
s3:ListAllMyBucketsが許可されるパターン
次にテストしたいパターンも上のパターン同様です(actionが違うだけ)。
allow { input.action = "s3:ListAllMyBuckets" }
先程と同様な考えでテストを書くと、次のようになります。
test_s3_ListAllMyBuckets_allowed { allow with input as { "action": "s3:ListAllMyBuckets" } }
s3:ListBucketが許可されるパターン
次にテストしたいのは以下のパターンです。
allow { actions_match resources_match } actions_match { actions = ["s3:List.*","s3:Get.*"] action = actions[_] regex.globs_match(input.action, action) } resources_match { resources = ["arn:aws:s3:::confidential-data","arn:aws:s3:::confidential-data/.*"] resource = resources[_] regex.globs_match(input.resource, resource) }
言葉で表すと arn:aws:s3:::confidential-data配下のresourceであればs3:List系あるいはs3:Get系のactionを許可
になります。
それでは input
を調整してテストしてみましょう!
test_s3_ListBucket_to_confidential_data_allowed { allow with input as { "action": "s3:ListBucket", "resource": "arn:aws:s3:::confidential-data/12345678" } }
これもわかりやすいですね!
s3:ListBucket
が arn:aws:s3:::confidential-data/12345678
に対して許可されることをテストしてます。
opa test .
してテストが通ることを確認しましょう。
s3:CreateBucketが拒否されるパターン
最後に拒否されるパターンも見てみましょう!
test_s3_CreateBucket_to_confidential_data_not_allowed { not allow with input as { "action": "s3:CreateBucket", "resource": "arn:aws:s3:::confidential-data/12345678" } }
action
を s3:CreateBucket
にしてみました。
ここのポイントは、 拒否されること
をテストするということです!
テストをよく見ると not allow
になっていることに気づきましたでしょうか?
そうです、 not
を使うことで拒否されることのテストができます。
テストを実行してみましょう!
$ opa test . data.aws.test_change_password_action_allowed: PASS (7.772µs) data.aws.test_s3_ListAllMyBuckets_allowed: PASS (1.735µs) data.aws.test_s3_ListBucket_to_confidential_data_allowed: PASS (1.239µs) data.aws.test_s3_CreateBucket_to_confidential_data_not_allowed: PASS (978ns) -------------------------------------------------------------------------------- PASS: 4/4
全部PASSしているはずです!
そして、VSCodeでやっている人は OPA: Toggle Workspace Coverage
を実行するとカバレッジも見れますよ!
おわりに
ということがわかりましたー。CIと組み合わせてPolicyのロバストネスを上げれそうですね!
2019-04-12追記:下の記事でCI連携についても書きました!
参考
OPAのVisual Studio Code拡張機能
機能
できるようになることは次のとおり(2019/03/31時点)
- 保存時に構文チェック(現時点でエディタ上で表示はされません)
- OPAのpackageを評価
- 選択中の箇所を評価
- 選択中の箇所を部分評価(Partial Evaluation)
- 選択中の箇所を評価して、トレースを表示
- 選択中の箇所を評価してプロファイルを表示
- ワークスペース内のテストを実行
- ワークスペース内のカバレッジ表示の切り替え
- 選択中の箇所のカバレッジ表示切り替え
準備するもの
OPAのバイナリを忘れずに!
opa
コマンドが叩けることが前提になっているので、ちゃんとインストールしておきましょう。
opa
に PATH
を通しておくか、VSCodeの設定で Opa: Path
にバイナリへのパスを直接指定することもできます!
お試し
拡張機能ページでGIFの紹介アニメーションがあるんですけど、サラッとどんどん進んじゃうので、一個一個見ていきます!
まず下の内容で simple.rego
を書きます。
package http.authz default allow = false allow { input.method = "GET" input.path = ["salary", user] input.user = user }
文でこのポリシーを言い表すと
- デフォルトは許可しない
- 下が全て満たされていれば許可
- 入力
method
が"GET"
- 入力
path
が["salary", user]
である - 入力
user
がpath
のuser
と一致
- 入力
となります。
ワークスペースのルートに input.json
を置くと、その中身をOPAの入力としてお試し実行することができます。
まずは次の内容にしてみましょう!
{ "user": "bob", "method": "GET", "path": ["salary", "bob"] }
bob
は bob
自身の salary
を見ようとしてるので、これは許可されるはずです!
Regoの allow
の部分をvscodeで選択して Cmd + Shift + p
して、OPA: Evaluate Selection
すると、この部分だけ評価できます。
やってみましょう。
期待通り、この結果は true
となります!
では、 path
を bob
から alice
に変えてみるとどうなるでしょ。
{ "user": "bob", "method": "GET", "path": ["salary", "alice"] }
もう一度 allow
を選択して実行すると次の通り。
自分の salary
じゃなくなってるので false
になってますね!
次はルールのBodyを選択して評価します!
input.method
は GET
なので結果は true
ですね!もう一行増やして実行してみましょう。
この結果の output.json
は、複数行関わった場合に何を基準にしているかちょっとわかっていないです。
予想としては ["salary", "alice"] = ["salary", user]
になって、 user
に alice
が代入されているのかなと思っています。そしてその結果が output.json
に出ているのではないかと。(この評価方法が正しいかは別途確認したいです)
では最終行も含めて実行してみましょう!
これは最終的に "bob" = "alice"
しようとして、 true
にならないので、結果は空っぽになります。
input.json
の path
を alice
から bob
に戻しましょう!
{ "user": "bob", "method": "GET", "path": ["salary", "bob"] }
そして実行してみると次のとおり!
何かしら戻り値が返ってくるのがわかります。
上でも返ってきたのと同様 user
に関するJSONなのですが、なぜこの値になるのかは現段階でちょっとわからないです。
allow
を選択して実行すると直感的な true
が返ってくるのですが。。。
おわりに
本記事ではOPAのVSCode拡張機能を紹介しました! 個人的にかなりうれしいのは下の2項目です。
- シンタックスハイライト
input.json
用意すれば即時実行できる
まだまだ使いこなせていないのでこの記事もわかったことが増え次第更新していきたいです!
IAM PolicyをOPAで書く
OPAシリーズです!今回は公式ドキュメントの「Comparison to Other Systems」について見てみます。 世の中に存在しているPolicyのパターンをOPAで実装した場合、どうなるかというのを教えてくれている項目です。
この中でも「Amazon Web Services IAM」をOPAで実装した場合について本記事では見ていきます。
IAM PolicyをOPAで書くとどうなるか
AWSのIAMではユーザーやロール、グループに加えてリソースに対してPolicyをアタッチできます。Policyは「 誰が
、 何に
、 何を
して 良いか
、あるいは ダメか
」を定義することができます。
次のIAMのPolicyを想定してみます。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "FirstStatement", "Effect": "Allow", "Action": ["iam:ChangePassword"], "Resource": "*" }, { "Sid": "SecondStatement", "Effect": "Allow", "Action": "s3:ListAllMyBuckets", "Resource": "*" }, { "Sid": "ThirdStatement", "Effect": "Allow", "Action": [ "s3:List*", "s3:Get*" ], "Resource": [ "arn:aws:s3:::confidential-data", "arn:aws:s3:::confidential-data/*" ] } ] }
IAMに馴染みの無い人(僕もその一人)からするとわかりにくいと思うので、要点だけあげると
iam:ChangePassword
というアクションだったら許可s3:ListAllMyBuckets
というアクションだったら許可arn:aws:s3:::confidential-data
というリソース、あるいはその配下のリソースに対して、s3:List
から始まるアクション、s3:Get
から始まるアクションを許可
という内容になります。これをOPAのRegoで書くと次のようになります!
注意:一部、公式のコメントを日本語に差し替えています
package aws # FirstStatement allow { input.action == "iam:ChangePassword" } # SecondStatement allow { input.action == "s3:ListAllMyBuckets" } # ThirdStatement # ここではヘルパールールを定義することで見やすくしています # actions_matchとresources_matchがtrueの場合にallowがtrueになります allow { actions_match resources_match } # actions_matchはinput.actionがactionsに含まれていればtrueになる actions_match { actions := ["s3:List.*","s3:Get.*"] # actionはactionsを順番にイテレートする、という意味があります action := actions[_] # check if input.action matches an action regex.globs_match(input.action, action) } # resources_matchはinput.resourceがresourcesに含まれていればtrueになる resources_match { resources := ["arn:aws:s3:::confidential-data","arn:aws:s3:::confidential-data/.*"] # resourceはresourcesを順番にイテレートする、という意味があります resource := resources[_] # check if input.resource matches a resource regex.globs_match(input.resource, resource) }
Regoに対する慣れは必要ですが、上から読んでいってそれなりに意味を読み取れるのではないかと思います。 ただし、トリッキーな部分が少なくとも2個あります。
action := actions[_]
のようにイテレーションを宣言してる部分regex.globs_match(input.action, action)
でビルトインな関数を使っている部分
actions_match
のルールをもう少し噛み砕いてみます。
まず、このルールですけど action := actions[_]
と代入してる場所を無くしてみましょう。
個人的には action
が単数形なのがわかりにくくしている気がします。
関係のある箇所だけ見ると下のようになります。
actions_match { actions := ["s3:List.*","s3:Get.*"] regex.globs_match(input.action, actions[_]) }
だいぶすっきりしましたね!このルールを言葉で言うと
インプットで渡ってきた action
が actions
内の正規表現( s3:List.*
, s3:Get.*
) とマッチすれば許可する
という意味になります。 regex.globs_match
は公式ドキュメントにも記載がありますが、最初から使えるビルトインな関数です。
true if the intersection of regex-style globs glob1 and glob2 matches a non-empty set of non-empty strings.
ドキュメントにも書いてあるように、 第1引数と第2引数を総当たりして、正規表現がマッチするものがあれば true
になるという関数です。
試してみたところ、引数は string
でも stringの配列
でも正常に動作する様です。
Playgroundで検証
OPAにはPlaygroundがあるので、下のPlaygroundを使って検証してみましょう!
次のようなInputを渡してみましょう。
{ "action": "s3:ListBucket", "resource": "arn:aws:s3:::confidential-data/12345678" }
arn:aws:s3:::confidential-data
配下のリソースに対して s3:List
から始まるアクションを行っているので許可されるはずです。
Outputをみてみましょう。
{ "result": { "actions_match": true, "allow": true, "resources_match": true } }
true
の評価結果が全部返ってくるので actions_match
も resources_match
も返ってきますが、本命の allow
も true
になっているのがわかりますね!
では s3:ListBucket
を下のように s3:CreateBucket
にしたらどうなるでしょう?
{ "action": "s3:CreateBucket", "resource": "arn:aws:s3:::confidential-data/12345678" }
結果は下の通り。
{ "result": { "resources_match": true } }
リソースのルールは許可されているけど、アクションが拒否されたので allow
は返ってきませんでした!
想定した動きになってそうですね。
では、ちょっと戻りますけど、最初に登場する2つの allow
ルールについても検証してみましょう。
パスワード変更を想定して次のようなInputを渡します。
{ "action": "iam:ChangePassword", "resource": "arn:aws:iam::123456789012:user/JohnDoe" }
結果は下の通り。
{ "result": { "allow": true } }
iam:ChangePassword
であればなんでも許可なので allow
が true
になってます。
最後に s3:ListAllMyBuckets
についても見てみましょう。
{ "action": "s3:ListAllMyBuckets", "resource": "arn:aws:s3:::*" }
結果は下の通り。
{ "result": { "actions_match": true, "allow": true } }
allow
は true
で返ってきますね!また、 s3:List
から始まるアクションなので actions_match
も true
で返ってきているのがわかります。
おわりに
OPAではこのようにAWSのIAMに似たPolicy管理もしていくことができそうですね。ただし、シンプルなパターンを見ているだけなので、複雑なパターンでも検証してみたいところです。 今後のネタとしてその点についてはストックしておきます!
おまけ
regex.globs_match(glob1, glob2)
は第1引数( glob1
)と第2引数( glob2
)を総当たりして true
があれば true
になるみたいなので、Inputを下のようにすると
{ "action": ".*", "resource": ".*" }
正規表現としては なんでもOK
という意味になるので結果は下の通り。
{ "result": { "actions_match": true, "allow": true, "resources_match": true } }
この動きはどうなんだろう、と思いながらも使う側の問題なので制御自体はできそうです。
あるいは、ヘルパー関数は自分でも作れるので、例えば 第1引数の文字列が、第2引数の正規表現のいずれかにマッチする
かチェックする関数を作れば良さそうです。
この点についても今後深掘りしていけたらと思います!