本体テナントに影響を与えずに開発用 GitHub Enterprise(EMU)環境を構築する — Entra ID B2B + SCIM 実例(2026年4月版)

はじめに

NTTビジネスソリューションズ / NTT西日本の樋口です。
本記事は2026年4月時点の情報に基づきます。

大きな組織の開発現場で、こんなことを感じたことはありませんか。筆者の正直なホンネを3つ並べてみます。

「社内の標準認証基盤(OA の Entra ID)で、開発ツールにもログインさせたい。組織がデカくなるほど巨大なID基盤での調整には時間がかかってしまう。『これだと自分で別にアカウント作った方が早いのではないか?』と、一瞬よぎってしまう」

「とはいえ、セキュリティもガバナンスも守らなければならない。不用意にアカウントを増やしてしまうと管理は煩雑になってしまい、単純にセキュリティリスクも増大する」

「でも、開発環境や AI 環境をちゃんと整えなければ、結局のところ社員は個人アカウントで生成 AI に質問して、その答えを人力で社内システムに打ち込むような運用になってしまいかねない」

本記事は、こうしたもどかしさを抱える大きな組織の開発チームに向けて、Microsoft Entra ID の B2B ゲスト招待と SCIM 自動プロビジョニングを組み合わせ、GitHub Enterprise Cloud(以下、GHE)の EMU(Enterprise Managed Users)環境を構築する手順を紹介します。

この記事を通じてできるようになること

  • 本体テナントに影響を与えずに、開発チーム専用の GHE 環境を構築できる
  • Graph API を使って、B2B ゲスト招待とグループ管理を再現性のある形で運用できる
  • SCIM オンデマンド同期で、メンバー変更を GHE に即時反映できる

大きな組織では ID 管理ポリシーや監査要件の関係で、本体テナントに外部メンバーを直接追加したり、開発専用のアプリケーションを気軽に立てたりすることが難しいケースが多くあります。その現実的な解として、開発専用のテナントを別に用意し、B2B ゲスト招待で本体側および他社メンバーを招いたうえで、SCIM プロビジョニングで GHE に自動同期する構成を採ります。

ポータル画面での手作業ではなく、Microsoft Graph API を中心にした構築・運用を軸にしています。「GUI 操作中心では再現性を担保しにくい」「Infrastructure as Code(インフラの構成をコードで管理する考え方)の思想で ID 管理もやりたい」という方には、参考になる部分があるかと思います。

正直なところ、SCIM 連携は公式ドキュメント通りにはいかない場面が多く、実際に API を叩いてみて初めてわかることがかなりありました。本記事では、筆者が実環境で得たログや API レスポンスをもとに、設計判断と実装の両面からお伝えします。

対象読者

以下に2つ以上当てはまるなら、本記事が役立つ可能性が高いです。

  • 大きな組織で開発チームを運営しており、本体テナントに外部 ID や開発用アプリを追加しづらい制約を抱えている
  • 本体テナント管理者との調整に時間を取られ、開発スピードが落ちることに課題感がある
  • 協力会社・メンバーを含むチームで GitHub Enterprise をセキュアに使いたい
  • Graph API を使用した経験があり、ポータル操作より API ベースの管理に関心がある
  • Microsoft Entra ID の基本的な概念(テナント、ユーザー、グループ)は理解している

本記事のスクリーンショット表記について

本記事に掲載するスクリーンショットは、社外公開用に一部の情報を架空の値へ置き換えています。

  • AAAA****-**** BBBB****-**** 等のマスク表記は、オブジェクトID / アプリケーションID / アクティビティIDなどを置き換えたダミー値です
  • Contoso株式会社 @contoso.com contoso.onmicrosoft.com はサンプルの組織名・ドメインです
  • ProjectA ProjectB ProjectC はサンプルプロジェクト名、個人名 Taro Yamada (山田 太郎) などもサンプル値です

実運用環境のログや API 出力と表記が食い違う箇所は、すべて意図的なマスク処理によるものです。

目次

1. 背景・目的

大きな組織の開発チームが抱える「ID 管理のジレンマ」

大きな組織で開発チームを運営していると、こんな課題に直面します。

  • 他社(子会社・協力会社)のメンバーに GitHub を使わせたいが、本体テナントに外部の ID を作りたくない
  • かといって、メンバーごとに別アカウントを発行するとライセンスコストと管理負荷が増大する
  • 開発環境だけを管理する専用のテナントを立てたいが、構築方法の情報が少ない

筆者のチームでも、まさにこの状況でした。AI 開発プロジェクトで GitHub Copilot を含む GitHub Enterprise の機能をフル活用したい。しかし、本体テナントに協力会社の方のアカウントを次々作るわけにはいきません。

この記事で解決すること

本記事では、以下のアプローチで上記の課題を解決します。

  • 開発専用テナントを開発チーム用に用意する(本体テナントに影響を与えない)
  • B2B ゲスト招待で他社・協力会社のメンバーを招く(既存の業務アカウントをそのまま利用)
  • SCIM 自動プロビジョニングで Entra ID のグループ変更を GHE(GitHub Enterprise Cloud)に自動反映する

いわば「本体テナントに影響を与えずに、開発チーム専用の GitHub 環境を最小構成で構築する」ための手順書です。

ポイント: なぜ「開発専用テナント」を立てるのか?

  • 本体テナントの保護: 外部 ID を本体テナントに混在させない(セキュリティ境界の分離)
  • ライセンス境界の明確化: 開発チーム向けに追加で発生する P1 相当ライセンスのコストを本体テナントと分離して管理できる
  • 責任境界の明確化: 開発チームが自律的にテナント運用できる(本体テナント管理者への依頼を最小化)
  • コンプライアンス分離: 開発環境のポリシーと本番環境のポリシーを独立して設計できる

2. 全体像と選定理由

構成図

構成図内の ProjectA-Proper ProjectA-Collab ProjectB-Proper ProjectB-Collab はサンプルのグループ名表記です。

構成の全体像は上図の通りです。登場する要素を整理します。

要素 役割
開発専用テナント(Entra ID) ユーザーとグループの管理拠点。SCIM のソース
B2B ゲスト招待 他社・協力会社のメンバーを専用テナントに招く仕組み。ホームテナントの ID をそのまま利用できる
SCIM プロビジョニング Entra ID のユーザー・グループ情報を GHE に自動同期する仕組み
GitHub Enterprise(EMU) Enterprise Managed Users。Entra ID が IdP(Identity Provider)となり、ユーザーのライフサイクルを一元管理

なぜこの組み合わせか

ポイント: GHE EMU(Enterprise Managed Users)では、ユーザーとグループの管理は IdP(Entra ID)から SCIM で同期する前提です。Managed User は GHE 上で個別編集できず、属性変更はすべて Entra ID 経由となります。

この前提のもと、開発チームへの GitHub 提供方法は主に2つ考えられます。

方式 メリット デメリット
A. 本体テナントから直接 SCIM 管理が一元化 本体テナントに外部 ID が混在。テナント管理者の承認が必要
B. 専用テナント + B2B + SCIM(本記事) 本体テナントに影響を与えない。自動同期。既存 ID を再利用 専用テナントの運用コスト。ライセンス費用

意外かもしれませんが、方式 B は「大がかりな構成」ではありません。数十名規模の開発チームであれば、Entra ID の無料枠 + P1 相当のライセンス数本で運用可能です。

スコープの絞り方

本記事では条件付きアクセス(Conditional Access)や属性マッピングの詳細設計、多要素認証(MFA)ポリシーの統一といったトピックはあえて扱いません。これらは組織ごとの事情が大きく、独立して議論した方が見通しが良いためです。本記事では、専用テナント + B2B + SCIM の最小構成を回せる状態までにスコープを絞ります。

3. 前提条件

3.1 本記事のスコープ

本記事は SCIM プロビジョニングと B2B 招待の設定・運用にフォーカスしています。 以下の構築手順は記事では扱わないため、未構築の項目は Microsoft / GitHub 公式ドキュメントを参照してください。

前提となる構築項目 公式ドキュメント
Entra ID テナントの新規作成 クイックスタート - アクセスして新しいテナントを作成する
GitHub EMU アプリのギャラリー追加 + SAML(Security Assertion Markup Language)/ OIDC(OpenID Connect)による SSO(Single Sign-On)設定 GitHub Enterprise Managed User の SSO 設定
EMU の SCIM プロビジョニング初期設定 GitHub Enterprise Managed User の自動ユーザープロビジョニング
GHE Enterprise アカウント(EMU 型)の契約 GitHub の一元管理された EMU について

3.2 必要なライセンスと権限

  • Microsoft Entra ID P1 相当以上のライセンス
    • 本記事で紹介する Entra ID の自動プロビジョニング機能(特にグループ連動の自動同期)を使う場合に必要
    • 該当するライセンス: Entra ID P1 / P2 単体、または Microsoft 365 E3 / E5 / F1 / F3 / Business Premium など Entra ID P1 以上を含むプラン
    • 機能対応の詳細は公式の Microsoft Entra ライセンス を参照
    • Entra ID P1 単体購入の場合は月額 899 円/ユーザー(税抜・年間契約、2026年4月時点)
    • Entra ID の自動プロビジョニング機能を使わず、GHE 側の API や Web UI で個別にユーザーを追加/削除する運用なら必須ではない
  • B2B ゲスト MAU(Monthly Active Users:月間アクティブユーザー数)無料枠: 最初の 50,000 MAU まで無料。以降は従量課金(Microsoft が将来変更する可能性あり)
  • 開発専用テナントの 全体管理者 権限
  • GHE Enterprise の Enterprise Owner 権限
  • 本体テナントの テナント管理者 との事前調整(次節参照)

注意: Entra ID の自動プロビジョニング機能(特にグループ連動の SCIM 同期)を使うには、Entra ID P1 相当以上のライセンスが必要です。公式ライセンス一覧 によれば、Microsoft 365 E3 / E5 / F1 / F3 / Business Premium にはこれが含まれます。既にいずれかを導入済みなら追加費用は発生しませんが、単体購入の場合はコストが発生するため、導入前に既存のライセンス構成を確認しておくと判断しやすいです。

以下の Graph API 権限も必要です。

操作 必要な権限
B2B ゲスト招待 User.Invite.All または グローバル管理者
グループメンバー管理 GroupMember.ReadWrite.All
SCIM プロビジョニング管理 クラウドアプリケーション管理者 以上
GHE 側の SCIM 設定 Enterprise Owner

3.3 本体テナント側の事前調整

本体テナントから開発専用テナントへ B2B 招待を成立させるには、 本体テナント管理者に以下の確認・設定が必要です。

他社・協力会社など他社テナントから招待する場合も、各他社テナント側で同様の許可が必要です。 社内調整を記事の初期タスクに含めておくと手戻りが減ります。

4. Entra ID 側の構成(構築済み環境の確認)

ここからは、実際に構築済みの環境を確認しながら各コンポーネントを説明します。

エンタープライズアプリケーション

Entra ID ポータルで「GitHub」と検索し、EMU 環境で SCIM プロビジョニングに使う GitHub Enterprise Managed User アプリを表示します。

画像内の AAAA****-**** はオブジェクトID、BBBB****-**** はアプリケーションIDを置き換えたマスク表記です。組織名は Contoso株式会社 に置換しています。

EMU 環境での SCIM プロビジョニングは、この GitHub Enterprise Managed User アプリが担います。以降の設定はすべてこのアプリに対して行います。

GitHub Enterprise Managed User アプリの概要

画像内の BBBB****-**** はアプリケーションID、AAAA****-**** はオブジェクトIDのマスク表記です。

このアプリが Entra ID と GHE を結ぶ要です。SSO(シングルサインオン)と SCIM プロビジョニングの両方をこのアプリ1つで担います。

SCIM プロビジョニングの状態

画像内の AAAA****-**** はサービスプリンシパルオブジェクトID、CCCC****-**** はジョブIDの一部、DDDD****-**** はアクティビティIDのマスク表記です。

筆者の環境では、この時点で複数ユーザー・複数グループが同期されています。プロビジョニングモードは「自動」に設定しており、Entra ID 側の変更が定期的に GHE に反映されます。

注意: 既定のプロビジョニング間隔は最短でも 40 分です。メンバー追加直後に「GHE に反映されていない」と慌てる前に、この間隔を把握しておいてください。急ぎの場合は、後述のオンデマンド同期で即時反映が可能です。

プロビジョニングログの確認

画像内の aaaaaaaa-bbbb-cccc-dddd-...12345678-abcd-... 11112222-3333-... 等のGUID表記は、SCIM 同期時に使われた実際のID値を置き換えたマスク表記です。

プロビジョニングログでは、各同期サイクルで何が行われたかを確認できます。CreateUpdateDelete のアクション別に成否が記録されており、エラーが発生した場合の原因調査に重要です。

設定時に迷った判断: 自動 vs 手動プロビジョニング

初期構築時、プロビジョニングモードを「自動」にするか「手動」にするかで少し迷いました。手動の方が同期タイミングを完全にコントロールできる反面、メンバー変更のたびに毎回実行するのは現実的ではありません。

結局、基本は自動(40 分サイクル)に任せて、急ぎの反映だけオンデマンド同期で即時反映する運用に落ち着きました。この方針なら、日常運用では同期を意識せずに済み、ライセンスの棚卸しなど特定タイミングだけ手動介入すればよくなります。

5. GHE 側の受け入れ設定

本記事では SAML SSO を用いた EMU 構成を前提としています(OIDC 構成でも SCIM の基本的な仕組みは同様ですが、設定項目が異なるため、本記事では SAML に絞って解説します)。

GHE(EMU)側で必要な設定は、大きく2点です。

SAML SSO の有効化

EMU の Enterprise 設定で SAML SSO を有効化し、Entra ID を IdP として指定します。設定項目は以下の通りです。

  • Sign on URL: Entra ID アプリの SAML サインオン URL
  • Issuer: Entra ID のアプリケーション ID URI
  • Public certificate: Entra ID からダウンロードした証明書

SCIM トークンの発行

GHE の Enterprise 設定から SCIM 用の Personal Access Token(PAT、classic 形式)を発行し、Entra ID 側のプロビジョニング設定に入力します。

このトークンは Enterprise Owner 権限を持つ EMU 管理者アカウント(いわゆる setup user@<enterprise-slug>_admin 形式)で発行する必要があります。Organization Owner 権限では権限が不足します。発行時のスコープは、EMU の SCIM プロビジョニング用途では scim:enterprise を含める必要があります。

補足: GHE 側の設定手順は GitHub 公式ドキュメントに詳しい説明があります。本記事では Entra ID 側の Graph API 操作にフォーカスするため、GHE 側は概要にとどめます。

設定時に迷った判断: SCIM トークンの有効期限

SCIM 用 PAT(Personal Access Token)の有効期限をどう設定するかは、悩みどころです。無期限にすればトークン更新の手間はなくなりますが、漏洩時の影響が大きくなります。逆に短期間にすると、更新作業が運用の負担になります。

筆者のチームでは、90日ごとに更新する運用を採用しています。Entra ID のプロビジョニング設定にトークンを再登録する作業は 1 分程度で済むため、継続的な運用コストとしては十分に許容できる範囲でした。この辺りは組織のセキュリティポリシーによるので、社内ガイドラインとすり合わせて決めるのが無難です。

6. B2B でのゲスト招待(Graph API 実例)

ここからが本記事の本題です。協力会社のメンバーを Graph API で B2B ゲスト招待する実際の手順を紹介します。

Graph API の認証について

本記事の Graph API 操作は、Azure CLI の az account get-access-token --resource-type ms-graph で取得したアクセストークンを Authorization: Bearer <token> ヘッダに付けて呼び出す方式を前提にしています。

Microsoft Graph Explorer や Postman を使う方法もありますが、スクリプト化と監査性の観点から、筆者のチームでは CLI ベースで統一しています。

認証方式の詳細は Microsoft Graph の認証と認可の基本概念 を参照してください。

具体的にはシェル上で以下のように使っています。

# アクセストークンを取得して環境変数に格納
TOKEN=$(az account get-access-token --resource-type ms-graph --query accessToken -o tsv)

# Graph API を呼び出す例
curl -s -X POST "https://graph.microsoft.com/v1.0/invitations" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "invitedUserEmailAddress": "ichiro.sato@partner.example.com",
    "inviteRedirectUrl": "https://myapplications.microsoft.com/",
    "sendInvitationMessage": false,
    "invitedUserType": "Guest"
  }'

az login 済みの状態でこの az account get-access-token を実行すれば、ログイン中のユーザー権限でトークンが取れます。CI パイプラインで使う場合は、サービスプリンシパル + az login --service-principal の組み合わせに差し替えてください。

なぜ Graph API を使うのか

Entra ID ポータルからもゲスト招待は可能ですが、Graph API を使う理由は明確です。

  • 再現性: スクリプト化すれば、同じ手順を何度でも正確に再現できる
  • バッチ処理: 複数名を一括で招待できる(ポータルでは1人ずつ)
  • 監査性: リクエストとレスポンスをログとして残せる
  • 自動化: CI/CD パイプラインやスケジュール実行に組み込める

ゲスト招待: POST /invitations

協力会社のメンバーを招待する Graph API リクエストの例です。

POST https://graph.microsoft.com/v1.0/invitations
Content-Type: application/json

{
  "invitedUserEmailAddress": "ichiro.sato@partner.example.com",
  "inviteRedirectUrl": "https://myapplications.microsoft.com/",
  "sendInvitationMessage": false,
  "invitedUserType": "Guest"
}

ポイント: sendInvitationMessagefalse にしています。招待メールを送らずにゲストユーザーを作成するパターンです。チーム内では、チャットで直接「このURLからサインインしてね」と伝える方が効率的なことが多いです。

レスポンス例

{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#invitations/$entity",
  "id": "9c50ecdd-xxxx-xxxx-xxxx-8332d9b7d2d0",
  "invitedUserEmailAddress": "ichiro.sato@partner.example.com",
  "invitedUserType": "Guest",
  "sendInvitationMessage": false,
  "status": "PendingAcceptance",
  "invitedUser": {
    "id": "089891dc-xxxx-xxxx-xxxx-05f85883a15f",
    "userPrincipalName": "ichiro.sato_partner.example.com#EXT#@contoso.onmicrosoft.com"
  }
}

注目すべきは invitedUser.id です。このユーザー ID は後続のグループ追加(POST /groups/{id}/members/$ref)で使います。また、userPrincipalName元のメールアドレス_ドメイン#EXT#@テナントドメイン という独特のフォーマットになる点も把握しておいてください。

招待後のユーザー状態

画像内のユーザー名 ichiro.sato kenji.watanabe jiro.suzuki や、ドメイン contoso.onmicros... はサンプル表記です(実環境の B2B ゲストユーザー情報を置き換えたもの)。

招待直後のユーザーは PendingAcceptance(承諾待ち)の状態です。ただし、SCIM プロビジョニングの観点では、招待承諾前でもグループに追加してプロビジョニング対象にすることが可能です。これは運用上かなり便利で、「招待 → グループ追加 → SCIM 同期」を一連の流れで実行できます。

グループへのメンバー追加: POST /groups/{id}/members/$ref

招待したゲストユーザーを SCIM 同期対象のグループに追加します。

POST https://graph.microsoft.com/v1.0/groups/{group-id}/members/$ref
Content-Type: application/json

{
  "@odata.id": "https://graph.microsoft.com/v1.0/directoryObjects/089891dc-xxxx-xxxx-xxxx-05f85883a15f"
}

成功すると 204 No Content が返ります。レスポンスボディは空です。

実際に確認してみると、この API は冪等(べきとう)ではありません。既にグループに所属しているユーザーを再追加しようとすると 400 Bad Request になります。スクリプトで一括処理する場合は、事前にメンバー一覧を取得して存在チェックを入れるか、エラーハンドリングで対処してください。

招待 + グループ追加を一括で回すスクリプト例

ここまでの招待・グループ追加の操作を、シェルスクリプトにまとめるとこんな感じになります。筆者のチームではこれをベースに運用ツール化しています。

#!/usr/bin/env bash
set -euo pipefail

TOKEN=$(az account get-access-token --resource-type ms-graph --query accessToken -o tsv)
GROUP_ID="<SCIM同期対象のグループID>"
EMAILS=(
  "ichiro.sato@partner.example.com"
  "kenji.watanabe@partner.example.com"
  "jiro.suzuki@fabrikam.example.com"
)

for EMAIL in "${EMAILS[@]}"; do
  # 1) B2B 招待
  USER_ID=$(curl -s -X POST "https://graph.microsoft.com/v1.0/invitations" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d "{\"invitedUserEmailAddress\":\"$EMAIL\",\"inviteRedirectUrl\":\"https://myapplications.microsoft.com/\",\"sendInvitationMessage\":false,\"invitedUserType\":\"Guest\"}" \
    | jq -r '.invitedUser.id')

  # 2) グループへ追加
  curl -s -o /dev/null -w "[%{http_code}] " -X POST \
    "https://graph.microsoft.com/v1.0/groups/$GROUP_ID/members/\$ref" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d "{\"@odata.id\":\"https://graph.microsoft.com/v1.0/directoryObjects/$USER_ID\"}"
  echo "$EMAIL -> $USER_ID"
done

ポイントは以下です。

  • set -euo pipefail で途中失敗時に止まるようにする(サイレント失敗防止)
  • 招待レスポンスから invitedUser.id を抽出してグループ追加に渡す
  • グループ追加の HTTP ステータス(204 が成功)をログに残しておくと、一部失敗時の切り分けが早い

エラーハンドリング(400 Bad Request で既にメンバーの場合のスキップ等)を足せば、日常運用に耐えるレベルになります。

グループからのメンバー除外: DELETE /groups/{id}/members/{userId}/$ref

メンバーの異動や離任時には、グループから除外します。

DELETE https://graph.microsoft.com/v1.0/groups/{group-id}/members/{user-id}/$ref

こちらも成功すると 204 No Content が返ります。

ポイント: グループからの除外と、テナントからのゲストユーザー削除は別の操作です。筆者のチームでは、グループ除外のみ行い、テナントからの削除は行わない運用にしています。

  • 同じメンバーが将来別のプロジェクトグループに参加する可能性がある
  • テナントに残っているゲストユーザーは、アクティブでなければ B2B の MAU 課金対象にならない

7. グループメンバー変更と SCIM オンデマンド同期(実ログ付き)

ここでは、メンバーの入れ替え(追加3 名・除外3 名)を行い、SCIM オンデマンド同期で GHE に即時反映する一連の流れを、実際の API ログとともに紹介します。

7-1. メンバーの追加と除外

作業の流れ

今回の作業は以下のステップで行いました。

Step 操作 Graph API
1 新メンバー3 名を B2B 招待 POST /invitations
2 招待したユーザーを対象グループに追加 POST /groups/{id}/members/$ref
3 追加結果を確認 GET /groups/{id}/members
4 異動メンバー3 名をグループから除外 DELETE /groups/{id}/members/{userId}/$ref
5 SCIM オンデマンド同期を実行 POST /servicePrincipals/{id}/synchronization/jobs/{jobId}/provisionOnDemand

Step 1-2: 招待とグループ追加

3 名のメンバーを招待し、それぞれ対象のグループに追加しました。

ichiro.sato@partner.example.com    → ProjectA-Collab グループ
kenji.watanabe@partner.example.com → ProjectA-Collab グループ
jiro.suzuki@fabrikam.example.com   → ProjectA-Proper グループ

招待の API レスポンスは前章で紹介した通りです。3 名とも status: 201(Created)で正常に招待が完了し、続くグループ追加も status: 204(No Content)で成功しました。

Step 3: 追加結果の確認

グループのメンバー一覧を取得して、追加が正しく反映されたことを確認します。

GET https://graph.microsoft.com/v1.0/groups/{group-id}/members?$select=id,displayName,userPrincipalName

ProjectA-Collab グループの確認結果

{
  "value": [
    {
      "id": "eee55555-6666-7777-8888-xxxxxxxxxxxx",
      "displayName": "ichiro.sato",
      "userPrincipalName": "ichiro.sato_partner.example.com#EXT#@contoso.onmicrosoft.com"
    },
    {
      "id": "fff66666-7777-8888-9999-xxxxxxxxxxxx",
      "displayName": "kenji.watanabe",
      "userPrincipalName": "kenji.watanabe_partner.example.com#EXT#@contoso.onmicrosoft.com"
    }
  ]
}

ここで気づくのは、B2B 招待直後のユーザーは displayName がメールアドレスのローカルパート(ichiro.sato など)になっている点です。ホームテナント側で設定された表示名は、招待承諾後に反映されます。SCIM 同期では、この displayName がそのまま GHE 側のプロフィール名として使われるため、運用上は招待承諾を待ってから SCIM 同期を実行する方が望ましい場合もあります。

Step 4: グループからの除外

異動メンバー3 名をそれぞれのグループから除外しました。

Kazuma Suzuki  → ProjectB-Collab から除外
Kousuke Sakai  → ProjectA-Collab から除外
Yoshimichi Yokota → ProjectA-Collab から除外

※上記は架空名に置換しています。

除外はすべて status: 204 で成功。除外前には GET /groups/{id}/members/{userId} で対象ユーザーの所属を確認してから実行しています。「削除する対象が本当に正しいか」を API で事前検証する手順は、本番運用では省略しないでください。

7-2. オンデマンド同期の実行

Step 5: SCIM オンデマンド同期

通常の SCIM プロビジョニングは 40 分間隔の自動実行ですが、メンバー変更の即時反映が必要な場合はオンデマンドプロビジョニングを使います。

servicePrincipal の特定

まず、SCIM プロビジョニングが設定されているエンタープライズアプリの servicePrincipal を特定します。

GET https://graph.microsoft.com/v1.0/servicePrincipals?$filter=displayName eq 'GitHub Enterprise Managed User'&$select=id,displayName,appId
{
  "value": [
    {
      "id": "7c0a9e47-xxxx-xxxx-xxxx-899ba255d7df",
      "displayName": "GitHub Enterprise Managed User",
      "appId": "62dd8251-xxxx-xxxx-xxxx-abb393966451"
    }
  ]
}

SCIM プロビジョニングの対象は id7c0a9e47-...GitHub Enterprise Managed User です。この id を次のオンデマンドプロビジョニング API のパスに使います。

jobId と ruleId の取得方法

provisionOnDemand のリクエストには、プロビジョニングジョブの jobId と同期ルールの ruleId が必要です。これらは以下の API で取得できます。

GET https://graph.microsoft.com/v1.0/servicePrincipals/{sp-id}/synchronization/jobs

レスポンスに含まれる id がジョブ ID、schema.synchronizationRules[].idruleId です。筆者の環境では、GitHub Enterprise Managed User アプリのプロビジョニング設定ジョブがひとつだけなので、先頭の jobs[0].id をそのまま使っています。

オンデマンドプロビジョニングの実行

POST https://graph.microsoft.com/v1.0/servicePrincipals/{sp-id}/synchronization/jobs/{job-id}/provisionOnDemand
Content-Type: application/json

{
  "parameters": [
    {
      "ruleId": "{provisioning-rule-id}",
      "subjects": [
        {
          "objectId": "{group-id}",
          "objectTypeName": "Group"
        }
      ]
    }
  ]
}

このAPIでグループ単位のオンデマンド同期を実行できます。

補足: Graph API の provisionOnDemand はグループ単位でも実行可能ですが、Azure ポータルの「オンデマンドプロビジョニング」UI では既定でユーザー単位の操作になります。API のほうが柔軟性が高く、本記事のようなバッチ変更時にはこちらを使うほうが実用的です。

オンデマンド同期の結果

画像内のグループ名 GG-GH-ProjectA-Proper はサンプル表記、12340000-aaaa-bbbb-cccc-... はグループオブジェクトIDをマスクした値です。

オンデマンド同期を実行すると、各メンバーの同期ステータスが返ってきます——初めてこれを見たとき、筆者は RedundantExport という見慣れないステータスに「何かミスったか?」とヒヤッとしました。結論から言うと、これはエラーではありません。

画像内のグループ名 GG-GH-ProjectA-Proper はサンプル表記です。

RedundantExport は「エクスポート先(GHE 側)に既に同じ状態のオブジェクトが存在するため、書き込みをスキップした」という意味です。つまり GHE 側が既に最新状態であることを示しており、同期処理としては正常です。ログを眺めているとドキッとしますが、動作としては期待通りなので安心してください。

SCIM 同期対象外のユーザーを指定した場合

画像内のユーザー名 Taro Contoso、メールアドレス taro.contoso@contoso.onmicrosoft.com はサンプル/マスク表記です。

プロビジョニング対象のグループに所属していないユーザーを指定してオンデマンド同期を実行すると、OutOfScope(スコープ外)となります。エラーにはなりませんが、同期も行われません。

7-3. GHE 側での反映確認

SCIM オンデマンド同期が完了したら、GHE 側で結果を確認します。

IdP グループ一覧

画像内のグループ名 GG-GH-ProjectA/B/C-Proper/Collab はサンプル表記です(実環境の組織別グループ名を置き換えたもの)。組織名 Contoso株式会社 も同様にサンプルです。

GHE の Enterprise 設定 > Identity provider groups に、Entra ID 側のグループが同期されていることを確認できます。

グループメンバーの反映

画像内のグループ名 GG-GH-ProjectA-Collab、ユーザー名/ハンドル ichiro.sato kenji.watanabe、SCIM Group ID 12340000-... はすべてサンプル/マスク表記です。

画像内のグループ名 GG-GH-ProjectA-Proper、ユーザー名/ハンドル Taro Yamada jiro.suzuki Hanako Tanaka John Smith、SCIM Group ID 12340000-... はすべてサンプル/マスク表記です。

Entra ID 側でのグループメンバー変更(追加・除外)が、GHE 側にも反映されています。追加したメンバーは GHE の IdP グループに表示され、除外したメンバーは GHE 側からも削除されています。

Entra ID 側グループの最終状態(参考)

Entra ID 側のグループメンバーも確認しておきます。

画像内のグループ名 GG-GH-ProjectA-Proper、ユーザー名 Taro Yamada jiro.suzuki Hanako Tanaka John Smith、メールアドレス @contoso.com、オブジェクトID aaa11111-... 等はすべてサンプル/マスク表記です。

画像内のグループ名 GG-GH-ProjectA-Collab、ユーザー名 ichiro.sato kenji.watanabe、メールアドレス @contoso.com、オブジェクトID eee55555-... fff66666-... はすべてサンプル/マスク表記です。

この章のまとめ

  • Graph API で招待 → グループ追加 → オンデマンド同期の一連のフローを、実 API ログと GHE 側の反映結果で確認しました
  • 同期結果に出てくる RedundantExport OutOfScope は正常なステータスです。エラーではないことを把握しておくと、ログ読みの混乱が減ります
  • 急ぎの反映以外は、既定の 40 分サイクルに任せる運用が API レート制限の観点でも安全です

8. 運用のコツ

実際に運用してみて気づいたポイントをいくつか共有します。

Graph API スクリプトの管理

筆者のチームでは、メンバーの招待・グループ追加・除外の操作をシェルスクリプトにまとめています。手作業を避けることで、「誰が」「いつ」「何を変更したか」がログとして残ります。Infrastructure as Code の考え方を ID 管理にも適用している形です。

SCIM 同期のタイミング

基本的には40 分間隔の自動同期に任せ、急ぎの場合だけオンデマンド同期を使う運用がおすすめです。オンデマンド同期は便利ですが、API 呼び出しにはレート制限(単位時間あたりの呼び出し回数上限)があるため、頻繁に実行するとスロットリング(一時的な要求拒否)が発生する可能性があります。

SCIM 同期エラーへの対処フロー

SCIM 同期が失敗する典型的なパターンとして、属性の必須チェック漏れ(displayName 未設定など)や、GHE 側で既に同名ユーザーが存在するケースに遭遇します。筆者の環境では、エラー発生時の確認順序を以下のように決めています。

  1. プロビジョニングログで該当ユーザー/グループのエラー内容を特定
  2. Entra ID 側で対象ユーザーの属性を確認(特に UPN(User Principal Name)・メール・表示名)
  3. GHE 側の管理画面で既存の重複がないかを確認
  4. 属性を修正して手動でオンデマンド同期を再実行

段階的に切り分けることで、「再同期しても直らない」という事態をかなり減らせます。

メンバー離任時のチェックリスト

メンバーが離任する際、筆者のチームでは以下の順序でクリーンアップします。

  1. Entra ID のグループから除外(本記事 6 章の Graph API 操作)
  2. 必要に応じて B2B ゲストユーザー自体を削除(長期間再参加の見込みがない場合)
  3. GHE 側で SCIM 同期の結果として除外されていることを確認
  4. もし GHE 上に残っている場合は、プロビジョニングログで原因を特定

「グループから外したつもりが GHE に残っていた」は意外と起こるため、同期後の確認は省略しない方が安全です。

定期的な棚卸し

筆者のチームでは、月次で以下の棚卸しを行っています。

  • Entra ID 側のグループメンバー一覧と、GHE 側の IdP グループメンバー一覧の突合
  • 長期間ログインのないゲストユーザーの棚卸し(B2B MAU の抑制にもつながります)
  • プロビジョニングログの定期確認(直近 30 日のエラー傾向を眺めて、運用改善ポイントがないか検討)

ID 管理は「作って終わり」ではなく、運用で育てるものだと感じています。

9. まとめ

本記事では、Entra ID の B2B ゲスト招待と SCIM 自動プロビジョニングを組み合わせて、本体テナントを開発用途に直接使えない大きな組織の開発チーム向けに GHE EMU 環境を構築する方法を紹介しました。

ポイントを整理します。

  • 専用テナント + B2B + SCIM の組み合わせで、本体テナントに影響を与えずに GitHub 環境を構築できる
  • Graph API を使うことで、再現性と監査性のある ID 管理が実現できる
  • SCIM のオンデマンドプロビジョニングを活用すれば、メンバー変更を即時反映できる
  • 数十名規模の開発チームであれば、Entra ID P1 相当 + EMU の最小構成で運用可能(筆者の環境での実績に基づく)

正直なところ、SCIM 連携は「設定すれば終わり」ではなく、運用の中で API レスポンスの読み方やエラーパターンを把握していく必要があります。本記事の Graph API 実例やログが、これから同じ構成を組む方の参考になれば幸いです。

執筆者

樋口竣一(NTTビジネスソリューションズ / NTT西日本) NTTグループ内をわりと転々としております。

参考資料・出典

本記事を執筆するにあたり、以下のサイトを参考にしました。

商標

  • 「Microsoft」「Microsoft Entra ID」「Microsoft Azure」「Microsoft Graph」は、米国 Microsoft Corporation の米国およびその他の国における登録商標または商標です。
  • 「GitHub」「GitHub Enterprise」「GitHub Copilot」は、GitHub, Inc. の商標または登録商標です。
  • 記載の会社名・製品名はそれぞれの会社の商標もしくは登録商標です。

© NTT WEST, Inc.