はじめに
Microsoft365のアプリを登録する機会がごくたまに発生するんですが、
そもそもアプリ登録とは?やアプリの登録とエンタープライズアプリケーションって何が違うんだっけ?と迷子になっているため理解を深めるために投稿します。
この記事では、アプリの登録の役割とエンタープライズアプリケーションとの違いを整理しつつ、実際にMirosoft365のサービス正常性をTeamsチャネルに通知する仕組みを作っています。
この記事でわかること
- アプリの登録とは何か?
- アプリの登録とエンタープライズアプリケーションの違い
- アプリの登録〜APIアクセス許可〜管理者の同意までの手順
- Pythonでサービス正常性を取得し、Teamsへ通知するサンプルコード
先に結論
- アプリの登録=アプリの「設計図」(アプリケーションオブジェクト)を作る場所
- エンタープライズアプリケーション = そのアプリがテナント内で実際に動くための「実体」(サービスプリンシパル)を管理する場所
- 自分で作るスクリプトやツールからGraph APIを呼ぶなら、まず「アプリの登録」から作る
本記事での実行環境
| 項目 | 内容 |
|---|---|
| OS | macOS |
| Python | 3.12(Homebrewでインストール) |
| ライブラリ | msal / requests / python-dotenv |
| 権限 | Entra IDでアプリ登録と管理者の同意ができる管理者アカウント |
| 通知先 | Teamsチャネル(Workflowsアプリのwebhook) |
1. Microsoft 365のアプリケーション登録とは
アプリの登録とは、自作のアプリやスクリプトをEntra ID(旧Azure AD)に「このアプリは信頼できる存在です」と登録する作業です。
Graph APIを呼ぶには「どのアプリが」「どの権限で」アクセスするのかをEntra IDが把握している必要があり、その身分証を発行する手続きがアプリの登録です。
アプリを登録すると、主に次の情報が得られます。
| 項目 | 役割 |
|---|---|
| アプリケーション(クライアント)ID | アプリを一意に識別するID |
| ディレクトリ(テナント)ID | アプリが所属するテナントのID |
| 資格情報(クライアントシークレット/証明書) | アプリが本人であることを証明するためのパスワード相当のもの |
| APIのアクセス許可 | アプリが何にアクセスしてよいか(例:ServiceHealth.Read.All) |
この3点セット(テナントID・クライアントID・シークレット)を使ってアクセストークンを取得し、Graph APIを呼び出す、というのが基本の流れになります。
2. アプリの登録とエンタープライズアプリケーションの違い
Entra管理センターでアプリを登録すると、アプリの登録にもエンタープライズアプリケーションにそれぞれオブジェクトが作られます。
| アプリの登録 | エンタープライズアプリケーション | |
|---|---|---|
| 実体 | アプリケーションオブジェクト | サービスプリンシパル |
| たとえると | アプリの「設計図」 | テナントに置かれた「実体(インスタンス)」 |
| 存在する場所 | アプリを作ったテナント(ホームテナント)に1つだけ | アプリを使う各テナントに1つずつ |
| 主に設定すること | リダイレクトURI、資格情報、要求するAPIのアクセス許可など | ユーザー割り当て、SSO設定、同意済みの権限の確認、サインインログなど |
| 代表的な例 | 自作スクリプト、社内ツール | 自作アプリの実体+外部SaaS(Zoom、Salesforceなど) |
ポイントは次の2つです。
-
アプリの登録をすると、自テナントのエンタープライズアプリケーションにも自動で同じ名前のアプリが現れる
→ 設計図(アプリケーションオブジェクト)と実体(サービスプリンシパル)がセットで作られるためです。 -
外部SaaSを追加した場合は、エンタープライズアプリケーションにだけ現れる
→ 設計図は提供元ベンダーのテナントにあり、自テナントには実体だけが作られるためです。
- 自分でアプリを作る → 「アプリの登録」から作成する。アプリの設計図と実体が必要になる。
- 既存のアプリ(Zoomなど)を「誰が使えるか」「どうサインインするか」を管理する → 「エンタープライズアプリケーション」で設定する。設計図は提供元ベンダーに登録されている。
3. 実際にアプリケーション登録を実施してみる
ここからは実践編です。サービス正常性の状態をTeamsに通知するアプリを作っていきます。
全体の構成
[Pythonスクリプト]
│ ① クライアントID+シークレットでトークン取得
▼
[Entra ID] ──トークン──▶ [Pythonスクリプト]
│ ② トークンを付けてGraph APIを呼び出し
▼
[Microsoft Graph:サービス正常性API]
│ ③ 異常のあるサービスだけ抽出
▼
[Teams Workflows(webhook)] ──▶ Teamsチャネルに投稿
手順1:アプリを登録する
- Microsoft Entra管理センター にサインイン
- 「Entra ID」→「アプリの登録」→「新規登録」を選択
- 次のとおり入力して「登録」をクリック
| 項目 | 設定値 |
|---|---|
| 名前 |
service-health-teams(わかりやすい名前でOK) |
| サポートされているアカウントの種類 | この組織ディレクトリのみに含まれるアカウント(シングルテナント) |
| リダイレクトURI | 空欄(ユーザーのサインインを伴わないため不要) |
※上記の概要ページに表示される アプリケーション(クライアント)ID と ディレクトリ(テナント)ID を控えておく
手順2:クライアントシークレットを発行する
- 登録したアプリの「証明書とシークレット」→「新しいクライアントシークレット」
- 説明と有効期限を入力して「追加」
- 表示された 「値」 を控える
⚠️ シークレットの値は作成直後の一度しか表示されません。画面を離れる前に必ず控えましょう。
また、有効期限が切れると通知も止まるので、期限はカレンダー等に登録しておくと安心です。
💡 そもそもクライアントシークレットとは?
クライアントシークレットは、アプリが「自分は登録されたあのアプリ本人です」と証明するためのパスワードです。
人がサインインするときは「ユーザー名+パスワード」を使いますが、アプリの場合は 「クライアントID(ユーザー名に相当)+クライアントシークレット(パスワードに相当)」 を使ってEntra IDに認証し、アクセストークンを受け取ります。今回のように人がサインインせずにスクリプトが自動で動く場合(クライアント資格情報フロー)、このシークレットがアプリの身元を証明する唯一の手段になります。
そのため、シークレットが漏れると、第三者がそのアプリになりすましてGraph APIを呼び出せてしまいます。取り扱いには次の点に気をつけましょう。
- コードに直書きせず、
.envや Azure Key Vault などで管理する- GitHubなどに誤ってプッシュしない(
.gitignoreに追加する)- 有効期限を設け、期限が来る前に更新する
なお、アプリの資格情報にはシークレットの代わりに 証明書 も使えます。本番運用では、よりセキュアな証明書の利用が推奨されています。今回は手軽に試すためシークレットを使います。
手順3:APIのアクセス許可を追加して、管理者の同意を与える
- 「APIのアクセス許可」→「アクセス許可の追加」→「Microsoft Graph」
- 「アプリケーションの許可」 を選択
-
ServiceHealth.Read.Allを検索してチェックし、「アクセス許可の追加」 - 「(テナント名)に管理者の同意を与えます」をクリック
- 状態が「✅ 付与されました」になっていることを確認
ここでのポイントは 「委任されたアクセス許可」ではなく「アプリケーションの許可」を選ぶことです。
| 種類 | 動き方 | 今回の用途 |
|---|---|---|
| 委任されたアクセス許可 | サインインしたユーザーの代わりにアクセスする | × 人がサインインしない |
| アプリケーションの許可 | アプリ自身の権限でアクセスする | ○ スクリプトが無人で定期実行する |
手順4:Teams側でwebhookを用意する
Teamsの通知先として、Workflows(Power Automate) のwebhookを使います。
- 通知したいTeamsチャネルの「…」→「ワークフロー」
- テンプレート「Webhook要求を受信するとチャネルに投稿する」を選択
- 投稿先のチームとチャネルを確認して作成
- 表示された webhookのURL を控える
手順5:Pythonで通知スクリプトを作る
環境構築
mkdir -p ~/projects/service-health-teams && cd ~/projects/service-health-teams
python3.12 -m venv .venv
source .venv/bin/activate
pip install msal requests python-dotenv
.env(認証情報はコードに直書きしない)
TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx
TEAMS_WEBHOOK_URL=https://xxxxx.logic.azure.com/workflows/...
.envは必ず.gitignoreに追加しておきましょう。
main.py
import os
import msal
import requests
from dotenv import load_dotenv
load_dotenv()
TENANT_ID = os.environ["TENANT_ID"]
CLIENT_ID = os.environ["CLIENT_ID"]
CLIENT_SECRET = os.environ["CLIENT_SECRET"]
TEAMS_WEBHOOK_URL = os.environ["TEAMS_WEBHOOK_URL"]
HEALTH_URL = "https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/healthOverviews"
# 通知しないステータス(正常・復旧済み・誤検知など)
IGNORE_STATUSES = {
"serviceOperational",
"serviceRestored",
"falsePositive",
"postIncidentReviewPublished",
}
# Graph APIのステータス値 → Teamsに表示する日本語
STATUS_LABELS = {
"investigating": "調査中です",
"serviceDegradation": "一部で問題が発生しています(性能低下)",
"serviceInterruption": "サービスが中断しています",
"restoringService": "サービスを復旧中です",
"extendedRecovery": "復旧に時間がかかっています",
"verifyingService": "復旧を確認中です",
"investigationSuspended": "調査を中断しました",
}
def get_access_token() -> str:
"""クライアントID+シークレットでアクセストークンを取得する"""
app = msal.ConfidentialClientApplication(
CLIENT_ID,
authority=f"https://login.microsoftonline.com/{TENANT_ID}",
client_credential=CLIENT_SECRET,
)
result = app.acquire_token_for_client(
scopes=["https://graph.microsoft.com/.default"]
)
if "access_token" not in result:
raise RuntimeError(f"トークン取得に失敗しました: {result.get('error_description')}")
return result["access_token"]
def get_unhealthy_services(token: str) -> list[dict]:
"""サービス正常性を取得し、現在問題が発生しているサービスだけ返す"""
res = requests.get(
HEALTH_URL,
headers={"Authorization": f"Bearer {token}"},
timeout=30,
)
res.raise_for_status()
services = res.json()["value"]
return [s for s in services if s["status"] not in IGNORE_STATUSES]
def build_teams_payload(services: list[dict]) -> dict:
"""Teamsに投稿するアダプティブカードを組み立てる"""
# 「- **サービス名**: 状態」の箇条書きを作る(未知のステータスは英語のまま表示)
lines = [
f"- **{s['service']}**: {STATUS_LABELS.get(s['status'], s['status'])}"
for s in services
]
return {
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{
"type": "TextBlock",
"text": "サービス正常性通知",
"weight": "Bolder",
},
{
"type": "TextBlock",
"text": "⚠️ 以下のサービスで問題が発生しています:",
"wrap": True,
},
{
"type": "TextBlock",
"text": "\n".join(lines),
"wrap": True,
},
],
},
}
],
}
def main() -> None:
token = get_access_token()
services = get_unhealthy_services(token)
if not services:
print("すべてのサービスが正常です。通知はスキップします。")
return
res = requests.post(TEAMS_WEBHOOK_URL, json=build_teams_payload(services), timeout=30)
res.raise_for_status()
print(f"{len(services)}件をTeamsに通知しました。")
if __name__ == "__main__":
main()
実行してみる
python main.py
異常のあるサービスがあれば、次のようなカードがTeamsチャネルに投稿されます。
すべて正常な場合は、コンソールに「すべてのサービスが正常です。」と表示され、通知はスキップされます。
つまずきポイント
| 症状 | 原因と対処 |
|---|---|
403 Forbidden が返る |
管理者の同意が未付与、または「委任されたアクセス許可」で追加している → 手順3を再確認 |
トークン取得で invalid_client
|
シークレットの「値」ではなく「シークレットID」を設定している、または期限切れ |
| Teamsに投稿されない | webhook URLの誤り、またはカードのJSON形式の誤り → ワークフローの実行履歴を確認 |
まとめ
- アプリの登録はアプリの「設計図」、エンタープライズアプリケーションはテナント内の「実体」
- 自作ツールからGraph APIを使うなら「アプリの登録」から作る
- 無人で動くスクリプトには 「アプリケーションの許可」+管理者の同意 が必要
- テナントID・クライアントID・シークレットの3点セットでトークンを取得し、Graph APIを呼び出す
- 今回は手動実行でしたが、定期実行の仕組みや、障害の詳細(インシデント情報)まで通知する方法に挑戦したいと思います。



