🐙

あまねくGitHubイベントのSlack通知をOPA/Regoで制御する

2022/03/12に公開約6,300字

TL;DR

  • GitHubは公式の通知機能やコメント内容に応じた通知ツールがすでにあるが、より多くの通知ユースケースがありそう
  • OPA/Regoを使うと通知ルールと実装をうまく分離できる
  • 全てのGitHubイベントの通知ができるツールをPoCとして実装してみた

https://github.com/m-mizutani/ghnotify

GitHub通知の活用

GitHub上での出来事を通知する方法は、公式のEmail通知やSlack連携だけでなく、様々なツールがこれを実現しています。特に(以前お世話になっていた)tokiteはIssueやPull Request(PR)内の文字列を検査して、特定のキーワードを含むコメントなどがあった場合にSlackへ通知してくれます。具体的なユースケースとしては、

  • 直接IDによってメンションされていないが、自分の名前がでてきた話題を見つける
  • 自分が興味のあるトピックに関連するキーワード(自分の場合だと、例えば「セキュリティ」「個人情報」「認証」など)が出てきた社内のIssueを見つける

などが挙げられます。同様のツールとしてhubhookというRust製のツールも先日公開されていました。これらはとても便利である一方、基本的にはコメントに特化されています。GitHubではこの他にも様々なイベントの通知がWebhookで提供されており、コメント以外にも以下のようなイベントで通知を受けることで、開発を効率化したりGitHub運用の透明性を上げることができそうです。

  • GitHub Actions の結果(特定のリポジトリや特定のstepが成功・失敗した場合に通知)
  • 新しい Deploy key の発行
  • 特定のbranchへのpush
  • 特定のラベルの付与・削除
  • リポジトリの作成やアーカイブ
  • チームの変更
  • GitHub Appのインストールやアンインストール

通知ルールを作る課題

GitHubが提供している通知を活用できると良さそうですが、実現にあたって「組織や個人ごとに通知を受けたい内容が異なる」という課題があります。前述したとおり、通知を受けたいのも「特定の」リソースに対する「特定の」挙動であったりします。これは組織、あるいは個人のポリシーによって決まるため、ある組織で有用な内容を通知するツールを作成しても別組織ではノイズだらけになります。組織や個人ごとに異なる実装を用意するというのも論理的には可能ですが、あまり筋がよく無さそう[1]です。

組織や個人にあった通知をするために tokite や hubhook ではコメントの本文やissueの属性を指定するルールが記述できます。これは

  • issueやPRに対象を絞り、
  • 通知したいかどうかを判断する属性(本文、ラベル、タイトルなど)やチェックする方法を絞る

ことによって、ルール化していると言えます。ルールの体系を作り、それを実装するというのはそれなりに高い労力が必要なため、一般的に使われるであろう項目に注力することで、投資対効果を高めています。しかし当然ながらチェックする方法が合わなかったり、自分が望む属性値が対象に入っていないということは大いにありえます。実装のコストが極端に低いのであれば、全てのイベントに対応するほうが望ましいでしょう。

ポリシーと実装の分離

この課題を解決するひとつの手段としてOpen Policy Agent(OPA)およびRegoが利用できます。OPA/Regoの詳しい説明はこちらを見ていただくのが良さそうですが、ものすごくざっくり説明すると「構造データ(JSONなど)を入力すると、新しい構造データを出力する」という機能だけを提供する言語がRegoで、それの実行エンジンがOPAになります。通常のユースケースとしては「リクエスト内容を入力すると、その許可・不許可を出力する」「クラウドの設定情報を入力すると組織のポリシーに準拠しているかどうかを出力する」というような使われ方になりますが、OPA/Regoは汎用性が高くこれらの用途に限らず利用できます。

今回のケースでは「GitHubのイベントを入力すると、通知するべき内容を出力する」という機能をOPAに任せることで、実装とルール(ポリシー)の分離が可能になります。図にするとこういう感じです。

OPAのポリシー判定は主に 1) 読み込んだRegoファイルをGoのランタイムで処理する、2) OPAサーバへ問い合わせる、という方法で利用できるため、組織の使い方に合わせて選択することができます。

OPAではどのような形式の構造データでも入力として扱うことができます。そのためGitHubのようにイベントごとに複数のスキーマがあってもスキーマ毎に個別処理する実装をする必要はなく、「とりあえず入力として投げ込む」ということができます。Regoで記述されたポリシーで「このフィールドがあったら・なかったら通知する」というような記述もできるため、通知を受ける実装はスキーマについての関心を放棄[2]できます。

関心を放棄できるのは通知についても同様です。OPAから返されるのは「このような文章や項目が記載された通知を送れ」という指示だけになります。そのため通知部分もスキーマなどをほぼ気にせず[3]、メッセージを整形してAPIを叩くだけの動作となります。

これによって、

  • GitHubの通知イベントの種類に依存した実装が必要ないため、追加の実装コストなしに全てのイベントに対応できる
  • 実装とポリシーを分離しているため、通知ルールを変更することで実装に影響を及ぼさないことが保証できる[4]

ということが実現できるようになりました。

ghnotify

ということで作ってみたのが以下のツールです(再掲)。

https://github.com/m-mizutani/ghnotify

具体的な使い方についてはREADMEに詳しく記載しましたが、例えば以下のようなルールを書くと、

notify[msg] {
    input.name == "issue_comment"
    contains(input.event.comment.body, "mizutani")
    msg := {
        "channel": "#notify-mizutani",
        "text": "Hello, mizutani",
        "body": input.event.comment.body,
    }
}

監視対象となっているリポジトリのIssueのコメントに mizutani というワードが含まれていた場合、以下のような通知がSlackで飛んできます。

使い方として、以下のような構成ができるようにしました。

  • ケース1) ghnotifyをWebサービスとして動かしてWebhookを受け取る
    • ケース1.1) 別途動いているOPAサーバに問い合わせをする
    • ケース1.2) ghnotifyのWebサービスに同梱されたポリシーファイルを読み込む
  • ケース2) GitHub Actions上でイベントを補足する

ケース1はGitHub AppもしくはWebhookの設定と、ghnotifyをWebサービスとしてデプロイが必要なため手間がかかりますが、Organization全体の通知を受け取ることができるため、多くのリポジトリを監視したい場合などに便利です。一方ケース2は設定は簡単なものの、1つのリポジトリしか見ることができません。筆者は以下のような構成で動かしています。

現状は個人リポジトリの監視のみですが、今後は社内の環境にも導入していけると良いなと考えています。

ポリシー例

具体的に他にどのようなポリシーを書けるのか?というのを簡単にご紹介したいと思います。

特定のラベルがPRに付与された

notify[msg] {
    input.name == "pull_requests"
    input.event.action == "labeled"
    input.event.label.name == "breaking-change"

    msg := {
        "channel": "#alert",
        "text": "A new breaking change PR",
    }
}

特定のリポジトリのGitHub Actionsが失敗した

notify[msg] {
    input.name == "workflow_run"
    input.event.action == "completed"
    input.event.repository.full_name == "m-mizutani/nanika"
    input.event.workflow.name == "deploy"
    input.event.workflow_run.conclusion != "success"

    msg := {
        "text": "nanika deployment failed",
    }
}

新しいDeploy Keyが発行された

notify[msg] {
    input.name == "deploy_key"
    input.event.action == "created"
    msg := {
        "channel": "#alert",
        "text": "A new deploy key created",

        # fieldsに任意のkey-valueを指定すると通知に表示される
        "fields": [
            {
                "name": "title",
                "value": input.event.key.title,
            },
            {
                "name": "read_only",
                "value": input.event.key.read_only,
            },
        ],
    }
}

リポジトリの作成、削除、公開

notify[msg] {
    input.name == "repository"
    input.event.action == "created"

    msg := {
        "channel": "#alert",
        "text": "repository created",
    }
}

notify[msg] {
    input.name == "repository"
    input.event.action == "deleted"

    msg := {
        "channel": "#alert",
        "text": "repository created",
    }
}

notify[msg] {
    input.name == "repository"
    input.event.action == "publicized"

    msg := {
        "channel": "#alert",
        "text": "repository publicized",
    }
}

まとめ

OPAやRegoは「ポリシー記述言語」ということから情報セキュリティの分野での活用が主な用途ですが、開発の思想である「実装と判断のロジックを分離する(Policy Decoupling)
」の側面から考えると、日常で使うツールにも活用することができます。個人的に、このような実装とポリシー分離の発想を活かせるツールやシステムというのは少なくないのではと考えており、今後も応用を考えて行きたいと思っています。

脚注
  1. 例えば実装毎にメンテナンスをしないといけない、通知などのロジックと判定のためのロジックが混ざる、というような運用上の課題が考えられます ↩︎

  2. とはいえ今回の実装でも正しい入力であるというバリデーションはしているので、全く見ていないわけではないです ↩︎

  3. こちらも共通して通知に使うようなフィールド(リポジトリ名、issueタイトル、ユーザ名)などは取得しているので厳密にはスキーマを触れていますが、イベントの種別に依存することのないような実装にしました ↩︎

  4. ルール同士が干渉して通知に影響を及ぼす、というのは考えられるのですが、これはOPAに標準で備わっているテスト機能で回帰的にテストすることで影響を防ぎやすくなると考えられます ↩︎

Discussion

ログインするとコメントできます