Resilire 開発チームのシード期から始める Architecture Decision Record (ADR) 文化
Resilire Tech Blog は zenn に移行しました
私たちのテックブログをご覧いただきありがとうございます! 2024年から掲載先を zenn に移行しました。是非フォローして最新記事をご覧ください。
これからも Resilire Tech Blog をよろしくお願いします。
アーキテクチャの意思決定を記録するプラクティス
Resilire に技術アドバイザーとして参画している @ahomu でございます。今日はリアーキテクチャの開発で師走を大々満喫中の皆さまに代わって、ADR (Architecture Decision Records) 文化と運用の実態をご紹介します。
ADR (Architecture Decision Record) とは?
ADR はその名のとおりアーキテクチャに関する意思決定の記録を残すためのドキュメントです。意思決定の内容や背景と経緯、根拠などが記されます。その場において認識の共有に役立つのはもちろん、あとから設計を見直したり議論したりするときにも意思決定の記録が役立ちます。
原典?として挙げられるのは Michael Nygard 氏のブログ記事でしょうか。日本国内でケーススタディが展開されるようになったのは近年の印象ですが、この記事の日付みると2011年であり実はそれなりに以前からあるプラクティスだったと言えます。私自身、不勉強で近年まで知らなかったのと、実際に運用されている光景は Resilire が初見でした。
Architectural Decision を踏まえた ADR の周辺解説はインフィニットループさんの記事 ADR – アーキテクチャ上の設計判断を記録しよう で特に詳説されていましたのでこちらもご覧いただくと良いです。(おすすめ)
他社さまの ADR ケーススタディ
ちょうど Advent Calendar の季節なのもあって、各社からも新しいケース紹介の記事が出ていました。他にもたくさんの記事があることを承知していますが、ここでは一部をご紹介させてください。
- スタディサプリさん
- 〜その意思決定を刻め〜「アーキテクチャ・デシジョン・レコード(ADR)」を利用した設計の記録
- ADR の名前が拡がったのは特にこちらの記事がきっかけだったように思います
- ZOZOさん
- バイセルさん
- ROUTE06さん
- チーム開発における技術選定の進め方
- チーム内で ADR の運用をしていく上で ROUTE06 さんの事例は特に参考にさせていただいたそうです
- ニフティさん
- 一休さん
Resilie 開発チームの ADR 運用
Reslire ではリポジトリルートから docs/adr
の位置に下記のようなマークダウンドキュメント群が整備されています。既に結構な分量が蓄積しているので読み応えがあります。
内容はファイル名から感じ取ってください。
adr ├── README.md ├── backend │ ├── 001-backend-go-framework.md │ ├── 002-grpc-systemcontrol-handling.md │ ├── 003-write-and-read-db-instances.md │ ├── 004-grpc-design-spec.md │ └── ... ├── development │ ├── 001-adr-adoption.md │ ├── 002-open-api-tag-rules.md │ ├── 003-tools-for-editing-open-api-schema.md │ ├── 004-release-flow.md │ └── ... ├── frontend │ ├── 001-generouted-adoption.md │ ├── 002-react-query-adoption.md │ ├── 003-react-router-dom-adoption.md │ ├── 004-react-adoption.md │ └── ... ├── spec │ ├── 001-handling-timezone.md │ ├── 002-uri-naming-conventions.md │ ├── 003-unification-of-list-sorting.md │ ├── 004-adopting-idaas.md │ └── ... └── template.md
development
カテゴリーはリリースフローや、コードがマージされるまでの取り決め、spec
カテゴリーは仕様上の取り決めなど、ソフトウェアのアーキテクチャに限らない様々なチームの意思決定が記録されています。ADR のスコープをどこまで拡げるかは先に紹介した各社の事例でもまちまちの印象ですが、Resilire ではこのように運用しています。
記述フォーマット
本稿執筆時点では次のようなフォーマットがテンプレートファイルとして用意されています。基本的には Documenting architecture decisions - Michael Nygard に沿ったフォーマットですね。
# {TITLE} ## Status <!-- Format <Date> <Status: 承認 or 廃止> --> {DATE} <承認 or 廃止> ## Background <!-- この決定を必要とした課題とその背景経緯を書く --> ## Decision <!-- 決定事項を書く --> <!-- 検討の過程で他の選択肢があった場合、採用しなかった理由を書く --> ## Impact <!-- この決定から生まれる副作用(期待する良い効果、悪い効果)を書く --> ## Compliance <!-- この決定を守るためのチェック方法を書く -->
Resilie 開発チームに根付いた ADR 文化
Resilire における ADR の原点を見つけるべく Slack の奥地で見つけてきたのが2023年3月頃のこちらの投稿です。
程なくして ADR の導入を意思決定する ADR がリポジトリに Pull Request されてマージされていました。後世のために、という狙いのとおりジョインしたメンバーがコントリビュートするための最初の手がかりとして当時の ADR が役に立っています。
ADR を運用する強い意志が感じられる Slack
自然と ADR を書くという習慣が見てとれます。
ADR の有用性を実感し、過去にさかのぼって適用されている様子も見つけました。
:adr:
という emoji と共に新しい ADR に対するわくわくが表明されています。
実際に書いたり読んだりした感想
わたしも Resilire を見習って別の場所で ADR に取り組んでいますが、アーキテクチャの検討段階で前提条件や選択肢の棚卸しをすること自体が思考の整理になるのでとても良い習慣だと感じました。
Resilire のコードベースを理解する上でも各種の ADR に最初に目を通すことで全体像や方針を掴むことができて、コードリーディングがスムーズになりました。以前に新しくジョインされた方と話したときも ADR のおかげでスムーズに開発に入れたというコメントもありましたし情報資産としてうまくワークしているようです。
あなたも Resilire で ADR を書きませんか
ということでソフトウェアエンジニア採用絶賛強化中ですので、興味を持った方は下記からぜひどうぞ!
Product & Technology の紹介資料もどうぞ。
最後までお読みいただきありがとうございました。:)
ResilireのWebフロントエンドのコードベースと設計をチラ見せ紹介するよ!
Resilire Tech Blog は zenn に移行しました
私たちのテックブログをご覧いただきありがとうございます! 2024年から掲載先を zenn に移行しました。是非フォローして最新記事をご覧ください。
これからも Resilire Tech Blog をよろしくお願いします。
サプライチェーンマネジメント SaaS のフロントエンドテックリード採用中です!
Resilire に技術アドバイザーとして参画している @ahomu でございます。見出しのとおり採用のための参考情報を兼ねた記事です。
今回は Resilire の Web フロントエンド環境がどのようになっているかを紹介がてら現状をブログにまとめてみました。私が手がけたわけではないので、ここまで基盤を整えてきた方にインタビューをしながらお送りしています 💁
プロダクトの概要
サプライチェーンというとフロントエンド周りだと npm のサプライチェーン攻撃を思い浮かべる各位もおられると思いますが、Resilire がスコープとしているのは物理的なグローバルサプライチェーン、原料調達、製造、輸送の最前線です。
現在の主要機能はサプライチェーンにおけるサプライヤー情報の一元管理と可視化、リスク情報の自動検知と同報送信による状況把握の円滑化です。
現在のステータス
これまでは PoC の延長線上にあるファーストプロダクトによってサプライチェーンマネジメント SaaS が提供すべき価値を検証してきました。 検証の中でシステムとして実現すべき像が明確になり今後の価値提供のために最適な設計、実装が一定見えてきたため、それらを反映すべくゼロベースのシステム刷新が行われたところです。
設計コンセプトと技術スタック
サプライチェーン自体がグローバルなものであることからグローバル展開を前提とした開発が求められ、サプライチェーンのリスクに備えるための機能を提供していることから高品質かつ堅牢なシステムの実現を目指しています。
フロントエンドとデザインの観点では、長大なサプライチェーンやリアルタイムに更新されるリスク情報などさまざまな大規模データを直感的に理解するためのインタラクティブな UI もポイントです。データやユースケースに合わせた使い勝手の良い UI を提供する必要があり、エンタープライズユースをターゲットにしているからこそユーザーに対する緻密な配慮が求められます。
Single Page Application —— Vite + React
Resilire はユーザーがログインして利用する toB SaaS であり、本体 Web アプリケーション自体に公開ページは含まれないので OGP の配信や Web Vitals の最適化を放念できます。 それを踏まえ以下のような観点で Vite を利用してビルドする純粋なシングルページアプリケーション構成が選択されています。
- 静的ファイルの配信なので Web の各種インフラコストが軽くなる
- 静的ファイルをビルドして配置するだけなのでデプロイがシンプルになる
- SSR + SPA が抱えるサーバー・クライアントの境界に依る複雑性を避けられる
- Web アプリケーションサーバー起因の潜在的な障害の可能性を回避できる
Next.js のような重厚なフレームワークの採用は見送っており、今の時点では自分たちでコントロール可能な要素技術の組み合わせによるコードベースを志向しています。 開発中リポジトリから抜粋した dependencies をご覧いただくと意思決定の傾向を読み取って頂けると思います。
{ "dependencies": { "@auth0/auth0-react": " ", "@generouted/react-router": " ", "@hookform/resolvers": " ", "@sentry/react": " ", "@sentry/vite-plugin": " ", "@tanstack/react-query": " ", "i18next": " ", "query-string": " ", "react": " ", "react-dom": " ", "react-hook-form": " ", "react-i18next": " ", "react-router-dom": " ", "ts-pattern": " ", "zod": " ", "zod-i18n-map": " " }, "devDependencies": { "@faker-js/faker": " ", "@storybook/react": " ", "cypress": " ", "eslint": " ", "jsdom": " ", "msw": " ", "orval": " ", "typescript": " ", "vite": " ", "vitest": " " } }
Monorepo —— turborepo + pnpm
サプライチェーンマネジメント関連のあらゆる機能の展開、価値提供を進めていく中でさまざまなアプリケーションやコンポーネントを水平展開することが予想されています。コードーベースの規模が拡大しても一定の見通しが担保されている状態が望ましいでしょう。
それを実現するためにディレクトリ構造は t3-oss/create-t3-turbo または turbo/examples/basic の流れを汲み turborepo と pnpm workspace を利用した monorepo 構成になっています。
- モジュール毎に package 化することによって、アプリケーション内をシンプルに保てる
- インターナルパッケージによる依存管理によってアプリケーション間の再利用性の向上を期待できる
- apps と packages 以下を見れば、構成部品の所在がおおよそ推測できるので開発・レビュー効率の向上が期待できる
上に挙げたメリットが早速発揮されている例として、8 月の下旬に新しくジョインしたメンバーも短い時間でアプリケーションの構造をキャッチアップして機能開発に取りかかることができています。
. ├── apps │ ├── storybook # Storybook │ └── vite # アプリケーション └── packages ├── auth # 認証関連 ├── config # tsconfig / eslint ├── icon # アイコン ├── locales # 多言語化向けの辞書 ├── map # マップ機能のUIコンポーネント ├── scm # ツリー機能のUIコンポーネント ├── test-utils # テストユーティリティ └── ui # 共通UIコンポーネント
今後の展望
個人ブログ記事 でも紹介しましたが Resilire のプロダクトはフロントエンド的にもチャレンジポイントが多いので、ぜひ多くの方に知っていただきたいところです。
- 高品質で堅牢なサービス開発が事業価値に直結する
- スケーラブルなシステムを支えるスケーラブルな UI の水平展開が求められる
- 堅牢+モダンな技術基盤でプロダクトの価値提供に集中できる
今回ご紹介したように技術スタックの方向性は一定見えてきたものの、開発すべき機能や提供すべき価値は今まさにこれから始まる 0 → 1 に近いフェーズです。 まずは Resilire に興味をもってくださった方のご参考になれば幸いです!
おすすめ記事
Resilire の物づくりをもっと知りたい方はこちらの記事もおすすめです 💁
Golangで、自動生成ファイルがある場合のパッケージ自動更新
Resilire Tech Blog は zenn に移行しました
私たちのテックブログをご覧いただきありがとうございます! 2024年から掲載先を zenn に移行しました。是非フォローして最新記事をご覧ください。
これからも Resilire Tech Blog をよろしくお願いします。
はじめに
こんにちは、バックエンドエンジニアのmynkitです。
Resilireでは、バックエンドにGolangを採用しています。Golangでパッケージを自動更新させたい場合、dependabotやRenovateが選択肢になるかと思います。
ただgRPCなどの自動生成ファイルがあるときはいろいろと厄介で、結論からいうとパッケージをimportするだけのgoファイルを作る必要があります。
今回は自動生成ファイルがある場合に試行錯誤したことと、最終的に作成したdependabot.goの生成方法を共有します。
dependabotと、自動生成ファイルがあるときの課題
dependabotはパッケージアップデートを自動で行ってくれるGithub純正のBotです。
Dependabot は、セキュリティアップデートプログラムを使用してプルリクエストを発行することにより、脆弱性のある依存関係を修正できます。
とてもありがたいのですが、一つ困ったことがあってdependabotが走ってプルリクが出された際、go mod tidy
が自動で走ります。これによってgo.modに必要なパッケージの追加と不要なパッケージの削除をしてくれるのですが、自動生成ファイルがあるときには悪さをすることがあります。
具体的にいうと、自動生成ファイルのみが呼び出しているパッケージがgo.modから削除されます。そのため自動生成ファイル部分のコードが動かなくなり、Lintなどが落ちます。
対応方法
Renovateの検討(うまくいかなかった)
ではgo mod tidy
を自動で走らせなければいいのでは?という気持ちになります。
Renovateでは、dependabotよりもカスタム性のある自動パッケージアップデートができます。また指定しない限りはgo mod tidy
が走らないので今回のケースでも良い選択肢に見えます。
そこで実際に試してみたのですが、自動テスト実行時に
go: updates to go.mod needed; to update it: go mod tidy
と怒られてしまいました。。
Renovateではrenovate.jsonに
"postUpdateOptions": [ "gomodTidy" ],
を追記することでgo mod tidy
を実行させることが可能ですが、Renovateのgo mod tidy
はgo.sumからパッケージのsumが抜け落ちることがあるようです。(そしてこの問題は対応しない方針のようです)
https://github.com/renovatebot/renovate/issues/3017
そのため、今回の場合Renovateを使う選択肢はなさそうです。
dependabot.goの追加(うまくいった)
そもそも今回の問題が報告されていないのか調べてみると、dependabotにissueがありました。
https://github.com/dependabot/dependabot-core/issues/3300
この手法は、自動生成ファイルがつくられるディレクトリにdependabot.goを用意し、自動生成ファイルに用いるパッケージをすべてimportしておくというものです。
dependabot.goの生成
すべての生成ファイルを目で確認していくのは大変なので、(なぜかPythonでつくりましたが)dependabot.go生成コードを置いておきます。
Install
import部分を認識させるため、ASTを利用します。ASTのjsonをdumpしてくれるgoastというパッケージがあるので、インストールしておきます。
go install github.com/m-mizutani/goast/cmd/goast@latest
次にPython3をインストールしておきます。必要な外部パッケージは特にありません。
generate_dependabotgo.pyの作成
# generate_dependabotgo.py import subprocess import json import glob import os def get_package_from_dic(dic): if 'Kind' in dic.keys() and dic['Kind']=='ImportSpec': return dic['Node']['Path']['Value'] else: return None def get_imported_packages(go_file): cmd = f"goast dump '{go_file}'" ast_result = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True).stdout dics = [json.loads(l) for l in ast_result.replace('\n}\n{\n', '}\t{').replace('\n', '').replace('\t', '\n').splitlines()] return set([get_package_from_dic(d) for d in dics if get_package_from_dic(d)]) def gen_dependabotgofile(dir_path): all_packages = [] if dir_path[-1] == '/': dir_path = dir_path[:-1] basename = os.path.basename(dir_path) go_paths = glob.glob(f'{dir_path}/*.go', recursive=True) if len(go_paths) == 0: Exception(f'dir_path: {dir_path} が適切ではありません') for go_path in go_paths: all_packages.extend(get_imported_packages(go_path)) import_txt = '' import_txt += f'package {basename}\n' import_txt += '\n' import_txt += 'import (\n' for package in sorted(set(all_packages)): import_txt += f'\t_ {package}\n' import_txt += ')\n' print(import_txt) with open(f'{dir_path}/dependabot.go', mode='w') as f: f.write(import_txt) def goformat(dir_path): subprocess.run(f'goimports -w "{dir_path}/dependabot.go"', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) auto_generated_gofile_dirs = [ # 自動生成ファイルの作られるディレクトリ達 ] for dir_path in auto_generated_gofile_dirs: print(dir_path) gen_dependabotgofile(dir_path) goformat(dir_path)
生成手順
ローカルで実行
- gRPCなど自動生成ファイルを生成
- generate_dependabotgo.pyの
auto_generated_gofile_dirs
を書き換えて、python generate_dependabotgo.py
を実行
※ちなみにこの方法だとdependabot.goに標準パッケージのimportも含まれるので、気持ち悪い方は適宜削除してください。
出力されたdependabot.goはこんな感じになると思います。dependabot.goがgitの追跡対象になるように.gitignoreの設定も忘れずに。
package account import ( _ "bytes" _ "context" _ "errors" _ "fmt" _ "net" _ "net/mail" _ "net/url" _ "reflect" _ "regexp" _ "sort" _ "strings" _ "sync" _ "time" _ "unicode/utf8" _ "google.golang.org/grpc" _ "google.golang.org/grpc/codes" _ "google.golang.org/grpc/status" _ "google.golang.org/protobuf/reflect/protoreflect" _ "google.golang.org/protobuf/runtime/protoimpl" _ "google.golang.org/protobuf/types/known/anypb" )
dependabot.goがちゃんと機能しているか確認
最後に、dependabotによってgo mod tidy
が実行されても問題なさそうか確認しておきましょう。
- 自動生成ファイルをすべて削除
go mod tidy
を実行- 自動生成ファイルを再度生成
- TestやLintを実行してエラーがでないか確認
以上で、dependabot.go配置による自動パッケージアッブデート問題は解消します。
おわりに
Resilireでは仲間を募集しています。
サーバーサイドだけでなく、フロントエンドやSREの採用も積極的に行っています。話を聴いてみたい!だけでも良いので、ご興味ある方はぜひご連絡ください!
Golangのfor rangeでのポインタ問題をLinterで検知する
Resilire Tech Blog は zenn に移行しました
私たちのテックブログをご覧いただきありがとうございます! 2024年から掲載先を zenn に移行しました。是非フォローして最新記事をご覧ください。
これからも Resilire Tech Blog をよろしくお願いします。
はじめに
こんにちは、バックエンドエンジニアのmynkitです。
Resilireでは、バックエンドにGolangを採用しています。Golangはとても書きやすく個人的にも好きな言語ですが、稀にひっかかる部分があります。
今回はfor range内でのポインタの挙動で気をつけないといけないことと、これをLinterで検知する方法を紹介させていただきます。
for range内でのポインタの挙動
まず以下のコードを実行すると、結果はどうなるでしょうか。
// main.go package main import "fmt" type User struct { id int name string } func main() { users := []User{{1, "John"}, {2, "Melissa"}, {3, "Robert"}} var names []*string for _, v := range users { names = append(names, &v.name) } for _, v := range names { fmt.Println(*v) } }
結果はこうなります。すべて最後のUserの値で置き換えられてしまっています。
Robert Robert Robert
これはfor _, v := range
文の中では、毎回同じアドレスが使われていることに起因します。
対策方法
これを防ぐためには以下のように[i]
でアクセスすることで対応できます。
// main_fix.go package main import "fmt" type User struct { id int name string } func main() { users := []User{{1, "John"}, {2, "Melissa"}, {3, "Robert"}} var names []*string for i := range users { names = append(names, &(users[i]).name) } for i := range names { fmt.Println(*names[i]) } }
出力結果
John Melissa Robert
Linterによる検知
今回はmain.go
のような書き方になってしまっているファイルをLinterで発見する仕組みを考えます。
技術選定
GolangでLinterといえば、golangci-lintが思いつくと思います。
カスタムルールを作成する方法も紹介されていますが、.so
を書き出す必要があったりして、既存のプロジェクトに組み込むのは拡張性などの面で少しハードルがあります。
https://golangci-lint.run/contributing/new-linters/#how-to-add-a-private-linter-to-golangci-lint
保守性と拡張性を考えた時、goastというものが使えそうです
https://github.com/m-mizutani/goast
goast概要
以下の開発者の記事によると、
Goのコードを読み込み、コードの抽象表現であるAST(Abstract Syntax Tree、構文抽象木)をRegoで記述されたポリシーによって評価します。ASTに関する説明はこちらの資料などがわかりやすいかと思います。 parser パッケージを使ってGoソースコードのASTを取得し、これをRegoのポリシーで評価します。評価はファイル全体のASTを一度だけ渡す、あるいはASTのノード毎に評価するモードを用意しています。
とのことです。
https://zenn.dev/mizutani/articles/go-static-analysis-with-rego
そのため、Regoでカスタムルールを書いていくことになります。
Linter実装
先に結論
プロジェクトディレクトリに.goast
ディレクトリを作っておきます。ここの中に.regoファイルを書いておきます。
これでfor rangeのvalueのアドレスを、append関数の中で利用しようとした場合にエラーを吐き出します。
# do_not_append_for_range_value_memory_address.rego package goast fail[res] { input.Kind == "RangeStmt" input.Node.Value != null [appendPath, appendValue] := walk(input.Node.Body.List) appendValue[_].Fun.Name == "append" # search for append func only [path, value] := walk(appendValue) value.Op == 17 # operator: & [pathDetail, valueDetail] := walk(value) valueDetail.Name == input.Node.Value.Name res := { "msg": "do not append for range value memory address", "pos": value.OpPos, "sev": "ERROR", } }
次にgoastコマンドを実行するため、インストールしておきます
go install github.com/m-mizutani/goast/cmd/goast@latest
localからコマンドラインでLinterを実行する場合は以下のコマンドを実行します。
goast eval -p .goast --ignore-auto-generated **/*.go
試しに冒頭のmain.go
に対して、コマンド実行すると、以下の出力が得られます。
$ goast eval -p .goast/do_not_append_for_range_value_memory_address.rego main.go [main.go:14] - do not append for range value memory address: "&v" names = append(names, &v.name) ~~~~~~~~ Detected 1 violations
Github Actionsを利用したい場合は、goast-actionを用意していただいているため簡単に設定できます。
# ci.yml name: goast on: pull_request: jobs: eval: runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v2 - name: run goast Lint uses: m-mizutani/goast-action@main with: policy: ./.goast format: text source: ./ ignore_auto_generated: true
Regoファイルの作成方法
基本ルール
上記のRegoファイルの作成方法を解説していきます。
まず、goastのRegoファイルにいくつかルールがあるようです。
- package が goast でなければならない
- 入力: input には Path、Kind のようなメタ情報と、実際のASTである Node が渡される
- 出力:違反があった場合、 fail という変数に以下のフィールドをもつ構造体を入れる
- msg: 違反内容のメッセージ(文字列)
- pos: ファイル内の位置を示す整数値
- sev: 深刻度。INFO, WARNING, もしくは ERROR
そのため基本フォーマットは以下のようになります
package goast fail[res] { # ここにルールを記述 res := { "msg": "エラーメッセージ", "pos": エラーの位置.~~Pos, "sev": "ERROR", } }
次にルールを書いていきたいですが、その前にいまのコードの構造がどうなっているのかASTを出力して確認したほうが良いです。
ASTの出力
goast dump main.go >> ast.json
出力されたast.json
から、該当のfor文にあたるものを探します
{ "Path": "main.go", "FileName": "main.go", "DirName": ".", "Node": { "For": 171, "Key": { "NamePos": 175, "Name": "_", "Obj": null }, "Value": { "NamePos": 178, "Name": "v", "Obj": null }, "TokPos": 180, "Tok": 47, "Range": 183, "X": { "NamePos": 189, "Name": "users", "Obj": null }, "Body": { "Lbrace": 195, "List": [ { "Lhs": [ { "NamePos": 199, "Name": "names", "Obj": null } ], "TokPos": 205, "Tok": 42, "Rhs": [ { "Fun": { "NamePos": 207, "Name": "append", "Obj": null }, "Lparen": 213, "Args": [ { "NamePos": 214, "Name": "names", "Obj": null }, { "OpPos": 221, "Op": 17, "X": { "X": { "NamePos": 222, "Name": "v", "Obj": null }, "Sel": { "NamePos": 224, "Name": "name", "Obj": null } } } ], "Ellipsis": 0, "Rparen": 228 } ] } ], "Rbrace": 231 } }, "Kind": "RangeStmt" }
"Op": 17
が、演算子(Op)「&」(17)を表しています。
Regoファイルの作成
最後にRegoファイルの書き方です。
基本的にはASTのjsonを探索していって、すべての条件がマッチしたものだけが最後のres
にたどり着いてエラー文を吐きます。
たとえば以下のルールでは、for rangeのvalueの使用自体を禁止できます。
# do_not_use_for_range_value.rego package goast fail[res] { input.Kind == "RangeStmt" input.Node.Value != null res := { "msg": "do not use for range value", "pos": input.Node.Value.NamePos, "sev": "ERROR", } }
$ goast eval -p .goast/do_not_use_for_range_value.rego main.go [main.go:13] - do not use for range value for _, v := range users { ~~~~~~~~~~~~~~~~~~ [main.go:16] - do not use for range value for _, v := range names { ~~~~~~~~~~~~~~~~~~ Detected 2 violations
複雑な探索をしたいときは、walk関数が便利です。
path
はあるkeyまでのkeyの配列、value
はそのkey以下のjsonです。
例えば、以下のように書くと
fail[res] { input.Kind == "RangeStmt" input.Node.Value != null [path, value] := walk(input) value.Op == 17 }
pathとvalueの値はそれぞれ
path: ["Node","Body","List",0,"Rhs",0,"Args",1] value: {"Op":17,"OpPos":221,"X":{"Sel":{"Name":"name","NamePos":224,"Obj":null},"X":{"Name":"v","NamePos":222,"Obj":null}}}
となります。
※ちなみにdebug時にpathやvalueを確認したい場合は、json.marshal関数で文字列に変換すればresのmsgに含ませたりすることで書き出せます。
これを利用すれば、for rangeの中で&vの使用を禁止するルールがかけます。
# do_not_use_for_range_value_memory_address.rego package goast fail[res] { input.Kind == "RangeStmt" input.Node.Value != null [path, value] := walk(input) value.Op == 17 # operator: & [pathDetail, valueDetail] := walk(value) valueDetail.Name == input.Node.Value.Name res := { "msg": "do not use for range value memory address", "pos": value.OpPos, "sev": "ERROR", } }
append関数の中での&vの使用を禁止したければ、appendがあるリストと同階層から探索すればよさそうなので、以下のようになります。
# do_not_append_for_range_value_memory_address.rego package goast fail[res] { input.Kind == "RangeStmt" input.Node.Value != null [appendPath, appendValue] := walk(input) appendValue[_].Fun.Name == "append" # search for append func only [path, value] := walk(appendValue) value.Op == 17 # operator: & [pathDetail, valueDetail] := walk(value) valueDetail.Name == input.Node.Value.Name res := { "msg": "do not append for range value memory address", "pos": value.OpPos, "sev": "ERROR", } }
おわりに
Resilireでは仲間を募集しています。
サーバーサイドだけでなく、フロントエンドやSREの採用も積極的に行っています。話を聴いてみたい!だけでも良いので、ご興味ある方はぜひご連絡ください!