Resilire Engineer Blog

サプライチェーンリスク管理クラウド Resilire (レジリア) のエンジニアが発信するソフトウェア技術ブログです

Resilire Tech Blog

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 の季節なのもあって、各社からも新しいケース紹介の記事が出ていました。他にもたくさんの記事があることを承知していますが、ここでは一部をご紹介させてください。

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月頃のこちらの投稿です。

Slackメッセージ: どっかでフロントエンドの開発基盤のADR残したほうが後世のためになるよな~と思いつつ、何もできておらぬ。。。ADR、DesignDocより軽量だし簡潔だから結構好き

程なくして ADR の導入を意思決定する ADRリポジトリに Pull Request されてマージされていました。後世のために、という狙いのとおりジョインしたメンバーがコントリビュートするための最初の手がかりとして当時の ADR が役に立っています。

ADR を運用する強い意志が感じられる Slack

自然と ADR を書くという習慣が見てとれます。

Slackメッセージ: ADR も書いた方がいいなぁ

ADR の有用性を実感し、過去にさかのぼって適用されている様子も見つけました。

Slackメッセージ: 確かにADR書く文化の前に合意した内容だったのですが、漏れてるので後からコードを見た人もわかるように)書いておくのが良い気がします。

:adr: という emoji と共に新しい ADR に対するわくわくが表明されています。

Slackメッセージ: "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 の流れを汲み turborepopnpm 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 のプロダクトはフロントエンド的にもチャレンジポイントが多いので、ぜひ多くの方に知っていただきたいところです。

  1. 高品質で堅牢なサービス開発が事業価値に直結する
  2. スケーラブルなシステムを支えるスケーラブルな UI の水平展開が求められる
  3. 堅牢+モダンな技術基盤でプロダクトの価値提供に集中できる

今回ご紹介したように技術スタックの方向性は一定見えてきたものの、開発すべき機能や提供すべき価値は今まさにこれから始まる 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 は、セキュリティアップデートプログラムを使用してプルリクエストを発行することにより、脆弱性のある依存関係を修正できます。

https://docs.github.com/ja/code-security/dependabot/dependabot-security-updates/about-dependabot-security-updates

とてもありがたいのですが、一つ困ったことがあって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)
生成手順

ローカルで実行

  1. gRPCなど自動生成ファイルを生成
  2. 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が実行されても問題なさそうか確認しておきましょう。

  1. 自動生成ファイルをすべて削除
  2. go mod tidyを実行
  3. 自動生成ファイルを再度生成
  4. TestやLintを実行してエラーがでないか確認

以上で、dependabot.go配置による自動パッケージアッブデート問題は解消します。

おわりに

Resilireでは仲間を募集しています。

サーバーサイドだけでなく、フロントエンドやSREの採用も積極的に行っています。話を聴いてみたい!だけでも良いので、ご興味ある方はぜひご連絡ください!

https://recruit.resilire.jp/for-engineers

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関数が便利です。 image.png 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の採用も積極的に行っています。話を聴いてみたい!だけでも良いので、ご興味ある方はぜひご連絡ください!

https://recruit.resilire.jp/for-engineers