BetterStack の監視 IP 更新を GitHub Actions + Repository Dispatch で自動化してみる
IP 制限環境で BetterStack を使うときの「許可リスト更新忘れ」を GitHub Actions でなくす取り組み
はじめに
外形監視に BetterStack (BetterUptime) を使っているのですが、BetterStack の監視元 IP アドレスは頻繁に変わります。 このため IP 制限をかけているアプリでは、許可リストをその都度更新しなければなりません。
厄介なのが、対象リポジトリが複数の GitHub Organization に分散していることです。さらに Terraform で管理しているリポジトリもあれば、Kubernetes マニフェストで管理しているリポジトリもあります。 手動で全リポジトリに反映するのは面倒ですし、漏れのリスクもあります。
もともと IP 変更を検知して Slack に通知する仕組みはあったのですが、通知を受けてから手動で各リポジトリを更新するのでは結局手間がかかります。 そこで、IP 変更を検知したら各リポジトリに自動で PR を作成する仕組みを構築しました。
やりたかったこと
- BetterStack の IP 変更を自動で検知する
- 変更があれば、対象リポジトリに自動で PR を作成する
- チームメンバーは approve + apply するだけで済む状態にする
なぜ Repository Dispatch にしたか
方式はいくつか検討しました(外部 Webhook、Reusable Workflow、Git Submodule など)。 ただ、今回は Organization を横断してイベントを飛ばす必要があったため、選択肢は限られました。
Repository Dispatch は GitHub API 経由でリモートリポジトリのワークフローをトリガーできる仕組みで、以下の理由で採用しました。
- Organization 横断でイベントを送信できる
- ペイロードに任意のデータを載せられるので、受信側は自分の構成に合わせたロジックを書ける
- GitHub 内で完結するため、外部インフラが不要
構成
送信側リポジトリのファイル構成は以下の通りです。
checker.sh # IP取得・差分検出・PR作成
dispatch.sh # 各リポジトリへの dispatch 送信
repositories.json # 送信先リポジトリの一覧
ips.txt # 現在の IP リスト(自動更新)
.github/workflows/check.yml # メインワークフロー(cron 実行)
受信側の各リポジトリには、ワークフローテンプレートを1つ配置するだけです。
全体のアーキテクチャ
betteruptime.com/ips.txt
│
▼
┌──────────────────────────────────────────┐
│ 送信側リポジトリ │
│ │
│ 1. IP 取得 & 差分検出 │
│ 2. ips.txt 更新 → PR 作成 │
│ 3. Slack 通知 │
│ 4. Repository Dispatch 送信 │
└──────┬───────────────┬───────────────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ org-a/ │ │ org-b/ │ ...追加可能
│ repo-a-tf │ │ repo-b-k8s │
│ │ │ │
│ Terraform │ │ Kubernetes │
│ マーカー置換 │ │ マーカー置換 │
│ → PR 作成 │ │ → PR 作成 │
└──────────────┘ └──────────────┘
送信側は1つのリポジトリです。cron で毎日実行され、以下の流れで処理が進みます。
checker.shが BetterStack から最新の IP リストを取得し、差分があれば送信側リポジトリ自体に PR を作成- IP が変わったことを Slack に通知(変更内容と対象リポジトリの一覧を送信)
- GitHub App の Installation Token を Organization ごとに生成
dispatch.shが各受信リポジトリに Repository Dispatch イベントを送信
送信先は repositories.json で管理しています。新しいリポジトリを追加するときは、ここに1行追加するだけです。
{
"repositories": [
{ "owner": "org-a", "repo": "repo-a-terraform" },
{ "owner": "org-b", "repo": "repo-b-k8s-manifest" },
{ "owner": "org-c", "repo": "repo-c-terraform" }
]
}Repository Dispatch のペイロードの仕組み
repository_dispatch とは
Repository Dispatch は GitHub API のエンドポイントで、POST /repos/{owner}/{repo}/dispatches にリクエストを送ると、対象リポジトリの GitHub Actions ワークフローをトリガーできます。
リクエストボディには以下の2つを指定します。
event_type: イベント名の文字列。受信側がどの dispatch イベントに反応するかをフィルタリングするために使いますclient_payload: 任意の JSON オブジェクト。ここに好きなデータを載せられます
送信側: ペイロードの組み立て
実際に送信される JSON はこのような形です。
{
"event_type": "betterstack-ips-updated",
"client_payload": {
"ips": ["5.161.90.230/32", "2a02:16a8:dc41::/48"],
"ipv4": ["5.161.90.230/32"],
"ipv6": ["2a02:16a8:dc41::/48"],
"added": ["203.0.113.0/24"],
"removed": ["198.51.100.0/24"],
"source_url": "https://betteruptime.com/ips.txt"
}
}client_payload の中身は自由に設計できます。今回は受信側の構成に応じて使い分けられるよう、IPv4/IPv6 を分けたフィールドと、まとめたフィールドの両方を含めています。
送信側: curl で API を叩く
組み立てたペイロードを curl で GitHub API に POST します。
HTTP_STATUS=$(curl -s -o "$RESPONSE_FILE" -w "%{http_code}" \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $TOKEN" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/$OWNER/$REPO_NAME/dispatches" \
-d "$PAYLOAD")成功すると HTTP 204 (No Content) が返ります。API を叩いた時点で受信側のワークフローが非同期にトリガーされる、fire-and-forget 方式です。
受信側: ペイロード取得
受信側のワークフローでは、repository_dispatch イベントをトリガーとして定義します。types で特定の event_type だけにフィルタリングできます。
on:
repository_dispatch:
types: [betterstack-ips-updated]ペイロードのデータには github.event.client_payload からアクセスします。送信側が client_payload に入れた JSON がそのまま使えます。
# Terraform の場合: IPv4 と IPv6 を別々に取得
env:
IPV4_IPS: ${{ toJson(github.event.client_payload.ipv4) }}
IPV6_IPS: ${{ toJson(github.event.client_payload.ipv6) }}# Kubernetes の場合: 全 IP をまとめて取得
env:
ALL_IPS: ${{ toJson(github.event.client_payload.ips) }}認証: Organization ごとのトークンをどう用意するか
Repository Dispatch の API (POST /repos/{owner}/{repo}/dispatches) を呼ぶには、対象リポジトリへの contents: write 権限を持つトークンが必要です。
GITHUB_TOKEN は自リポジトリにしか権限がなく、さらに送信先が複数の Organization にまたがるため、Organization ごとに外部トークンを用意する必要があります。
この認証基盤として、継続利用を見越して個人に紐づかない GitHub App を採用しました。
今回の GitHub App はボットではなく、トークンを発行するための認証基盤として使っています。App に Contents: Read & Write を設定して各 Organization にインストールしておくと、Organization ごとに短命な Installation Token を発行でき、このトークンで dispatch API を叩きます。
補足: 今回は GitHub Enterprise を利用していたため、Enterprise の Settings から Internal(Enterprise 内限定)の GitHub App として作成しました。Enterprise でない場合は PAT が現実的な選択肢になります。
Installation Token が発行されるまで
actions/create-github-app-token@v2 に APP_ID、APP_PRIVATE_KEY、owner を渡すと、内部で次の処理が行われます。
APP_ID + APP_PRIVATE_KEY
│
▼
① JWT 生成
(秘密鍵で署名した「自分が誰の App か」の証明。これ単体では API 操作不可)
│
▼
② JWT → GitHub API「org-a の Installation ID は?」→ 取得
(Installation ID = App を org-a にインストールした際に振られる一意の ID)
│
▼
③ JWT + Installation ID → GitHub API「org-a 用のトークンをください」
│
▼
Installation Token(1時間有効、Contents: Read & Write 権限付き)
Installation Token に付く権限は、App 作成時に宣言した権限(Contents: Read & Write)が根拠です。各 Organization の管理者がその権限でインストールを承認しているため、Token 発行時に GitHub がその権限を付与します。JWT 自体には権限はなく、「自分がどの App か」を証明するためだけのものです。
ワークフローでの実装
APP_ID と APP_PRIVATE_KEY を送信側リポジトリの Secrets に保存しておき、ワークフロー内で Organization ごとにトークンを生成します。
- name: Generate token for org-a
id: token-org-a
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
owner: org-a
- name: Generate token for org-b
id: token-org-b
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
owner: org-b生成したトークンを環境変数として dispatch.sh に渡します。
- name: Dispatch to target repositories
run: ./dispatch.sh ...
env:
DISPATCH_TOKEN_ORG_A: ${{ steps.token-org-a.outputs.token }}
DISPATCH_TOKEN_ORG_B: ${{ steps.token-org-b.outputs.token }}
DISPATCH_TOKEN_ORG_C: ${{ steps.token-org-c.outputs.token }}dispatch.sh はリポジトリごとに対応する環境変数からトークンを取得し、dispatch API を叩きます。
受信側: マーカーコメント方式で IP を置換する
受信側のリポジトリでは、対象ファイルに BEGIN / END マーカーコメントを埋め込み、その間だけを自動置換します。
Terraform の場合:
cidr_blocks = [
# BEGIN BETTERSTACK_IPV4
"5.161.90.230/32",
"54.94.132.0/26",
# END BETTERSTACK_IPV4
]Kubernetes YAML の場合:
# BEGIN BETTERSTACK_IPS
- 5.161.90.230/32
- 2a02:16a8:dc41::/48
# END BETTERSTACK_IPS受信側のワークフローでは、マーカーの間だけを新しい IP リストに置換します。マーカーの外側は一切触りません。
この方式の利点は以下の通りです。
- ファイル形式に依存しない: HCL でも YAML でも、マーカーコメントが書ければ使える
- 差分が最小限: IP 部分だけが変わり、それ以外のコードには影響しない
- セットアップが簡単: 受信側はマーカーを1回埋め込み、ワークフローテンプレートを配置するだけ
置換後に差分があれば PR を作成します。
まとめ
BetterStack の IP 変更を検知して、複数の Organization にまたがるリポジトリに自動で PR を作る仕組みを GitHub Actions で構築しました。
- Repository Dispatch を使うことで Organization を横断したイベント駆動の自動化を実現
- マーカーコメント方式 で Terraform / Kubernetes など異なるファイル形式に汎用的に対応
- GitHub App でトークンの属人化と有効期限管理の問題を解消
Repository Dispatch はペイロードに任意のデータを載せられるため、今回のような「1つのイベントをきっかけに複数リポジトリを更新する」パターンに適しています。同様のクロスリポジトリ自動化が必要な場面では、検討してみてはいかがでしょうか。