Azure API Management
Microsoftのエンタープライズ向けAPI管理プラットフォーム。ハイブリッド・マルチクラウド対応、開発者ポータル、詳細な分析機能を提供。
概要
Azure API Managementは、Microsoftが提供するエンタープライズレベルの包括的API管理プラットフォームです。API の作成、公開、セキュリティ確保、監視、分析を統合的に行える完全マネージドサービスとして、ハイブリッド・マルチクラウド環境での API戦略を支援します。
2014年にリリースされ、Microsoft 365、Azure Active Directory、Power Platformとの深い統合により、企業のデジタル変革を支援する中核ツールとして進化してきました。開発者ポータル、詳細な分析機能、豊富なポリシーセットにより、API-first戦略の実現を可能にします。
主要な特徴
- エンタープライズ機能: 大規模組織向けの高度な管理・監視機能
- 統合プラットフォーム: Azure・Microsoft 365・Power Platformとの深い統合
- 開発者ポータル: カスタマイズ可能な開発者向けセルフサービスポータル
- ハイブリッド対応: オンプレミス・マルチクラウド・エッジ環境での一元管理
- 豊富な分析: 詳細なAPIアナリティクスとビジネスインサイト
主要機能
コア機能
- API Gateway: 高性能なAPIプロキシとルーティング
- 開発者ポータル: APIドキュメント、テストコンソール、サブスクリプション管理
- API分析: リアルタイム分析とカスタムレポート
- バージョン管理: APIバージョニングとライフサイクル管理
- セキュリティ: OAuth 2.0、JWT、IP制限、APIキー認証
エンタープライズ機能
- VNET統合: Azure Virtual Networkとの統合
- 自己ホスト型ゲートウェイ: オンプレミス・マルチクラウド展開
- 複数リージョン: グローバル展開とディザスタリカバリ
- カスタムドメイン: SSL証明書とカスタムドメイン管理
- ワークスペース: チーム別の分離された管理環境
統合機能
- Azure Active Directory: シームレスなユーザー認証・認可
- Logic Apps: ワークフロー統合
- Power Platform: ローコード・ノーコード開発統合
- Azure Monitor: 包括的な監視・アラート
インストール・セットアップ
Azure CLI による作成
Azure CLI インストール・設定
# Azure CLI インストール(Linux)
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
# macOS
brew install azure-cli
# Windows (PowerShell)
Invoke-WebRequest -Uri https://aka.ms/installazurecliwindows -OutFile .\AzureCLI.msi; Start-Process msiexec.exe -Wait -ArgumentList '/I AzureCLI.msi /quiet'
# ログイン
az login
# アカウント確認
az account show
# サブスクリプション設定
az account set --subscription "Your-Subscription-ID"
API Management インスタンス作成
# リソースグループ作成
az group create \
--name "rg-apim-prod" \
--location "East US"
# API Management インスタンス作成
az apim create \
--name "contoso-api-management" \
--resource-group "rg-apim-prod" \
--location "East US" \
--publisher-email "[email protected]" \
--publisher-name "Contoso Ltd" \
--sku-name "Developer" \
--sku-capacity 1
# 作成確認
az apim show \
--name "contoso-api-management" \
--resource-group "rg-apim-prod"
ARM テンプレートによる作成
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"apiManagementServiceName": {
"type": "string",
"defaultValue": "contoso-apim",
"metadata": {
"description": "Name of the API Management service."
}
},
"publisherEmail": {
"type": "string",
"metadata": {
"description": "The email address of the owner of the service"
}
},
"publisherName": {
"type": "string",
"metadata": {
"description": "The name of the owner of the service"
}
},
"sku": {
"type": "string",
"allowedValues": [
"Developer",
"Standard",
"Premium"
],
"defaultValue": "Developer",
"metadata": {
"description": "The pricing tier of this API Management service"
}
},
"skuCount": {
"type": "int",
"defaultValue": 1,
"metadata": {
"description": "The instance size of this API Management service."
}
}
},
"resources": [
{
"type": "Microsoft.ApiManagement/service",
"apiVersion": "2021-08-01",
"name": "[parameters('apiManagementServiceName')]",
"location": "[resourceGroup().location]",
"sku": {
"name": "[parameters('sku')]",
"capacity": "[parameters('skuCount')]"
},
"properties": {
"publisherEmail": "[parameters('publisherEmail')]",
"publisherName": "[parameters('publisherName')]",
"customProperties": {
"Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls10": "false",
"Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls11": "false",
"Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls10": "false",
"Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls11": "false",
"Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Ssl30": "false"
}
}
}
],
"outputs": {
"apiManagementServiceName": {
"type": "string",
"value": "[parameters('apiManagementServiceName')]"
},
"apiManagementServiceUrl": {
"type": "string",
"value": "[reference(resourceId('Microsoft.ApiManagement/service', parameters('apiManagementServiceName'))).gatewayUrl]"
}
}
}
Terraform による作成
# main.tf
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~>3.0"
}
}
}
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "main" {
name = "rg-apim-${var.environment}"
location = var.location
}
resource "azurerm_api_management" "main" {
name = "apim-${var.project}-${var.environment}"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
publisher_name = var.publisher_name
publisher_email = var.publisher_email
sku_name = var.sku_name
identity {
type = "SystemAssigned"
}
security {
enable_backend_ssl30 = false
enable_backend_tls10 = false
enable_backend_tls11 = false
enable_frontend_ssl30 = false
enable_frontend_tls10 = false
enable_frontend_tls11 = false
tls_ecdhe_ecdsa_with_aes256_cbc_sha_ciphers_enabled = false
tls_ecdhe_ecdsa_with_aes128_cbc_sha_ciphers_enabled = false
tls_ecdhe_rsa_with_aes256_cbc_sha_ciphers_enabled = false
tls_ecdhe_rsa_with_aes128_cbc_sha_ciphers_enabled = false
tls_rsa_with_aes128_gcm_sha256_ciphers_enabled = false
tls_rsa_with_aes256_cbc_sha256_ciphers_enabled = false
tls_rsa_with_aes128_cbc_sha256_ciphers_enabled = false
tls_rsa_with_aes256_cbc_sha_ciphers_enabled = false
tls_rsa_with_aes128_cbc_sha_ciphers_enabled = false
}
tags = {
Environment = var.environment
Project = var.project
}
}
# カスタムドメイン設定
resource "azurerm_api_management_custom_domain" "main" {
api_management_id = azurerm_api_management.main.id
gateway {
host_name = "api.${var.domain_name}"
key_vault_id = azurerm_key_vault_certificate.ssl.versionless_id
negotiate_client_certificate = false
ssl_keyvault_identity_client_id = azurerm_api_management.main.identity[0].principal_id
}
developer_portal {
host_name = "developer.${var.domain_name}"
key_vault_id = azurerm_key_vault_certificate.ssl_developer.versionless_id
negotiate_client_certificate = false
ssl_keyvault_identity_client_id = azurerm_api_management.main.identity[0].principal_id
}
}
PowerShell による作成
# PowerShell による API Management 作成
# Azure PowerShell モジュールインストール
Install-Module -Name Az -AllowClobber -Scope CurrentUser
# Azure ログイン
Connect-AzAccount
# 変数設定
$resourceGroupName = "rg-apim-prod"
$apiManagementName = "contoso-apim"
$location = "East US"
$publisherEmail = "[email protected]"
$publisherName = "Contoso Ltd"
# リソースグループ作成
New-AzResourceGroup -Name $resourceGroupName -Location $location
# API Management インスタンス作成
$apimContext = New-AzApiManagement -ResourceGroupName $resourceGroupName `
-Name $apiManagementName `
-Location $location `
-Organization $publisherName `
-AdminEmail $publisherEmail `
-Sku "Developer"
Write-Host "API Management created successfully"
Write-Host "Gateway URL: $($apimContext.GatewayUrl)"
Write-Host "Management URL: $($apimContext.ManagementApiUrl)"
Write-Host "Portal URL: $($apimContext.PortalUrl)"
基本的な使い方
API の追加とバックエンド設定
Azure CLI による API 作成
# API 作成
az apim api create \
--resource-group "rg-apim-prod" \
--service-name "contoso-api-management" \
--api-id "users-api" \
--path "/users" \
--display-name "Users API" \
--description "User management API" \
--service-url "https://backend.contoso.com/api" \
--protocols "https"
# 操作追加
az apim api operation create \
--resource-group "rg-apim-prod" \
--service-name "contoso-api-management" \
--api-id "users-api" \
--operation-id "get-users" \
--display-name "Get Users" \
--method "GET" \
--url-template "/users"
az apim api operation create \
--resource-group "rg-apim-prod" \
--service-name "contoso-api-management" \
--api-id "users-api" \
--operation-id "get-user" \
--display-name "Get User" \
--method "GET" \
--url-template "/users/{id}"
# 製品との関連付け
az apim product api add \
--resource-group "rg-apim-prod" \
--service-name "contoso-api-management" \
--product-id "starter" \
--api-id "users-api"
OpenAPI 仕様からの API インポート
# OpenAPI JSON/YAML からインポート
az apim api import \
--resource-group "rg-apim-prod" \
--service-name "contoso-api-management" \
--api-id "petstore-api" \
--path "/petstore" \
--specification-format "OpenApi" \
--specification-url "https://petstore.swagger.io/v2/swagger.json" \
--display-name "Pet Store API"
# ローカルファイルからインポート
az apim api import \
--resource-group "rg-apim-prod" \
--service-name "contoso-api-management" \
--api-id "custom-api" \
--path "/custom" \
--specification-format "OpenApi" \
--specification-path "./openapi.yaml" \
--display-name "Custom API"
ポリシーの設定
レート制限ポリシー
<!-- rate-limit-policy.xml -->
<policies>
<inbound>
<base />
<!-- IP アドレス別レート制限 -->
<rate-limit-by-key calls="100"
renewal-period="60"
counter-key="@(context.Request.IpAddress)" />
<!-- サブスクリプション別レート制限 -->
<rate-limit-by-key calls="1000"
renewal-period="3600"
counter-key="@(context.Subscription?.Key ?? "anonymous")" />
<!-- カスタムヘッダーベースレート制限 -->
<rate-limit-by-key calls="50"
renewal-period="60"
counter-key="@(context.Request.Headers.GetValueOrDefault("X-Client-ID", "default"))" />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<!-- レート制限情報をヘッダーに追加 -->
<set-header name="X-RateLimit-Limit" exists-action="override">
<value>100</value>
</set-header>
<set-header name="X-RateLimit-Remaining" exists-action="override">
<value>@{
string key = context.Request.IpAddress;
var counter = context.Cache.LookupByKey(key + ":counter");
return counter != null ? (100 - (int)counter).ToString() : "100";
}</value>
</set-header>
</outbound>
<on-error>
<base />
</on-error>
</policies>
認証・認可ポリシー
<!-- authentication-policy.xml -->
<policies>
<inbound>
<base />
<!-- JWT 検証 -->
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized">
<openid-config url="https://login.microsoftonline.com/{tenant-id}/v2.0/.well-known/openid_configuration" />
<required-claims>
<claim name="aud">
<value>api://your-api-id</value>
</claim>
<claim name="roles" match="any">
<value>user</value>
<value>admin</value>
</claim>
</required-claims>
</validate-jwt>
<!-- ロールベースアクセス制御 -->
<choose>
<when condition="@(context.Request.Method == "DELETE")">
<validate-jwt header-name="Authorization" failed-validation-httpcode="403" failed-validation-error-message="Insufficient permissions">
<openid-config url="https://login.microsoftonline.com/{tenant-id}/v2.0/.well-known/openid_configuration" />
<required-claims>
<claim name="roles" match="any">
<value>admin</value>
</claim>
</required-claims>
</validate-jwt>
</when>
</choose>
<!-- APIキー検証 -->
<check-header name="X-API-Key" failed-check-httpcode="401" failed-check-error-message="API Key required" ignore-case="false" />
<!-- IP制限 -->
<ip-filter action="allow">
<address-range from="192.168.1.0" to="192.168.1.255" />
<address>203.0.113.5</address>
</ip-filter>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<!-- ユーザー情報をヘッダーに追加 -->
<set-header name="X-User-ID" exists-action="override">
<value>@{
Jwt jwt;
if (context.Request.Headers.GetValueOrDefault("Authorization","").TryParseJwt(out jwt))
{
return jwt.Claims.GetValueOrDefault("sub", "");
}
return "";
}</value>
</set-header>
</outbound>
<on-error>
<base />
</on-error>
</policies>
リクエスト・レスポンス変換
<!-- transformation-policy.xml -->
<policies>
<inbound>
<base />
<!-- リクエストボディ変換 -->
<set-body>
@{
JObject inBody = context.Request.Body.As<JObject>(preserveContent: true);
// 新しいフィールド追加
inBody["timestamp"] = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
inBody["source"] = "api-management";
// センシティブ情報の除去
if (inBody["password"] != null)
{
inBody.Remove("password");
}
return inBody.ToString();
}
</set-body>
<!-- ヘッダー設定 -->
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-header name="X-Forwarded-For" exists-action="override">
<value>@(context.Request.IpAddress)</value>
</set-header>
<!-- クエリパラメータ変換 -->
<set-query-parameter name="version" exists-action="override">
<value>v2</value>
</set-query-parameter>
<!-- 条件付きルーティング -->
<choose>
<when condition="@(context.Request.Headers.GetValueOrDefault("X-Version","") == "v1")">
<set-backend-service base-url="https://api-v1.backend.com" />
</when>
<otherwise>
<set-backend-service base-url="https://api-v2.backend.com" />
</otherwise>
</choose>
</inbound>
<backend>
<base />
<!-- タイムアウト設定 -->
<timeout value="30" />
<!-- リトライ設定 -->
<retry condition="@(context.Response.StatusCode >= 500)" count="3" interval="2">
<forward-request buffer-request-body="true" />
</retry>
</backend>
<outbound>
<base />
<!-- レスポンスボディ変換 -->
<set-body>
@{
JObject outBody = context.Response.Body.As<JObject>(preserveContent: true);
// メタデータ追加
JObject metadata = new JObject();
metadata["requestId"] = context.RequestId;
metadata["timestamp"] = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
metadata["version"] = "v2.0";
outBody["_metadata"] = metadata;
return outBody.ToString();
}
</set-body>
<!-- CORS ヘッダー -->
<cors>
<allowed-origins>
<origin>https://contoso.com</origin>
<origin>https://app.contoso.com</origin>
</allowed-origins>
<allowed-methods>
<method>GET</method>
<method>POST</method>
<method>PUT</method>
<method>DELETE</method>
</allowed-methods>
<allowed-headers>
<header>*</header>
</allowed-headers>
<expose-headers>
<header>X-RateLimit-Limit</header>
<header>X-RateLimit-Remaining</header>
</expose-headers>
</cors>
<!-- キャッシュ制御 -->
<cache-store duration="300" />
</outbound>
<on-error>
<base />
<!-- エラーレスポンス整形 -->
<set-body>
@{
var error = new JObject();
error["error"] = new JObject();
error["error"]["code"] = context.LastError?.Source;
error["error"]["message"] = context.LastError?.Message;
error["error"]["timestamp"] = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
error["error"]["requestId"] = context.RequestId;
return error.ToString();
}
</set-body>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
</on-error>
</policies>
設定例
Azure Active Directory 統合
<!-- aad-integration-policy.xml -->
<policies>
<inbound>
<base />
<!-- Azure AD B2C JWT 検証 -->
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Token validation failed">
<openid-config url="https://{tenant-name}.b2clogin.com/{tenant-name}.onmicrosoft.com/{policy-name}/v2.0/.well-known/openid_configuration" />
<required-claims>
<claim name="aud">
<value>{application-id}</value>
</claim>
<claim name="iss">
<value>https://{tenant-name}.b2clogin.com/{tenant-id}/v2.0/</value>
</claim>
</required-claims>
</validate-jwt>
<!-- Microsoft Graph API 呼び出し用トークン取得 -->
<authentication-managed-identity resource="https://graph.microsoft.com" output-token-variable-name="graph-token" />
<!-- ユーザー情報取得 -->
<send-request mode="new" response-variable-name="user-info" timeout="10" ignore-error="false">
<set-url>https://graph.microsoft.com/v1.0/me</set-url>
<set-method>GET</set-method>
<set-header name="Authorization" exists-action="override">
<value>@("Bearer " + (string)context.Variables["graph-token"])</value>
</set-header>
</send-request>
<!-- ユーザー情報をヘッダーに設定 -->
<set-header name="X-User-Email" exists-action="override">
<value>@{
var response = ((IResponse)context.Variables["user-info"]);
if (response.StatusCode == 200)
{
var userInfo = response.Body.As<JObject>();
return userInfo["mail"]?.ToString() ?? "";
}
return "";
}</value>
</set-header>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
Logic Apps 統合
<!-- logic-apps-integration.xml -->
<policies>
<inbound>
<base />
<!-- Logic Apps ワークフロー呼び出し -->
<send-request mode="new" response-variable-name="workflow-response" timeout="30" ignore-error="true">
<set-url>https://prod-xx.eastus.logic.azure.com:443/workflows/{workflow-id}/triggers/manual/paths/invoke</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>
@{
var requestBody = new JObject();
requestBody["apiPath"] = context.Request.Url.Path;
requestBody["method"] = context.Request.Method;
requestBody["userId"] = context.Request.Headers.GetValueOrDefault("X-User-ID", "");
requestBody["timestamp"] = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
return requestBody.ToString();
}
</set-body>
</send-request>
<!-- ワークフロー結果の処理 -->
<choose>
<when condition="@(((IResponse)context.Variables["workflow-response"]).StatusCode != 200)">
<return-response>
<set-status code="503" reason="Workflow unavailable" />
<set-body>{"error": "Business logic service temporarily unavailable"}</set-body>
</return-response>
</when>
</choose>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<!-- ワークフロー結果をレスポンスに含める -->
<set-header name="X-Workflow-Status" exists-action="override">
<value>@{
var response = ((IResponse)context.Variables["workflow-response"]);
if (response.StatusCode == 200)
{
var result = response.Body.As<JObject>();
return result["status"]?.ToString() ?? "completed";
}
return "error";
}</value>
</set-header>
</outbound>
<on-error>
<base />
</on-error>
</policies>
マルチリージョン構成
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"primaryRegion": {
"type": "string",
"defaultValue": "East US"
},
"secondaryRegion": {
"type": "string",
"defaultValue": "West Europe"
}
},
"resources": [
{
"type": "Microsoft.ApiManagement/service",
"apiVersion": "2021-08-01",
"name": "global-apim",
"location": "[parameters('primaryRegion')]",
"sku": {
"name": "Premium",
"capacity": 2
},
"properties": {
"publisherEmail": "[email protected]",
"publisherName": "Contoso Global",
"additionalLocations": [
{
"location": "[parameters('secondaryRegion')]",
"sku": {
"name": "Premium",
"capacity": 1
},
"virtualNetworkConfiguration": {
"subnetResourceId": "/subscriptions/{subscription-id}/resourceGroups/{rg-name}/providers/Microsoft.Network/virtualNetworks/{vnet-name}/subnets/{subnet-name}"
}
}
],
"virtualNetworkConfiguration": {
"subnetResourceId": "/subscriptions/{subscription-id}/resourceGroups/{rg-name}/providers/Microsoft.Network/virtualNetworks/{vnet-name}/subnets/{subnet-name}"
},
"customProperties": {
"Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls10": "false",
"Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls11": "false"
}
}
}
]
}
認証・セキュリティ
OAuth 2.0 認証サーバー設定
# OAuth 認証サーバー追加
az apim authserver create \
--resource-group "rg-apim-prod" \
--service-name "contoso-api-management" \
--authsid "oauth-server" \
--display-name "OAuth Authorization Server" \
--description "OAuth 2.0 Authorization Server" \
--authorization-endpoint "https://login.contoso.com/oauth/authorize" \
--token-endpoint "https://login.contoso.com/oauth/token" \
--client-registration-endpoint "https://login.contoso.com/oauth/register" \
--client-id "your-client-id" \
--client-secret "your-client-secret" \
--authorization-methods "GET,POST" \
--grant-types "authorizationCode,implicit" \
--bearer-token-sending-methods "authorizationHeader,query"
証明書認証設定
<!-- certificate-authentication.xml -->
<policies>
<inbound>
<base />
<!-- クライアント証明書検証 -->
<choose>
<when condition="@(context.Request.Certificate == null)">
<return-response>
<set-status code="401" reason="Client certificate required" />
<set-body>{"error": "Client certificate authentication required"}</set-body>
</return-response>
</when>
</choose>
<!-- 証明書の詳細検証 -->
<choose>
<when condition="@(context.Request.Certificate.Thumbprint != "expected-thumbprint")">
<return-response>
<set-status code="403" reason="Invalid certificate" />
<set-body>{"error": "Invalid client certificate"}</set-body>
</return-response>
</when>
</choose>
<!-- 証明書の有効期限確認 -->
<choose>
<when condition="@(context.Request.Certificate.NotAfter < DateTime.Now)">
<return-response>
<set-status code="403" reason="Certificate expired" />
<set-body>{"error": "Client certificate has expired"}</set-body>
</return-response>
</when>
</choose>
<!-- 証明書情報をヘッダーに追加 -->
<set-header name="X-Client-Certificate-Subject" exists-action="override">
<value>@(context.Request.Certificate.Subject)</value>
</set-header>
<set-header name="X-Client-Certificate-Issuer" exists-action="override">
<value>@(context.Request.Certificate.Issuer)</value>
</set-header>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
Key Vault 統合
{
"type": "Microsoft.ApiManagement/service/namedValues",
"apiVersion": "2021-08-01",
"name": "apim-service/database-connection-string",
"properties": {
"displayName": "DatabaseConnectionString",
"keyVault": {
"secretIdentifier": "https://contoso-keyvault.vault.azure.net/secrets/db-connection-string/",
"identityClientId": null
}
}
}
IP 制限とネットワークセキュリティ
<!-- ip-security-policy.xml -->
<policies>
<inbound>
<base />
<!-- IP アドレス制限 -->
<ip-filter action="allow">
<!-- 本社オフィス -->
<address-range from="203.0.113.0" to="203.0.113.255" />
<!-- データセンター -->
<address-range from="198.51.100.0" to="198.51.100.127" />
<!-- 特定パートナー -->
<address>192.0.2.50</address>
</ip-filter>
<!-- 地理的制限 -->
<choose>
<when condition="@(context.Request.Headers.GetValueOrDefault("CF-IPCountry", "") == "CN")">
<return-response>
<set-status code="403" reason="Access denied" />
<set-body>{"error": "Access from this region is not permitted"}</set-body>
</return-response>
</when>
</choose>
<!-- User-Agent ブラックリスト -->
<choose>
<when condition="@(context.Request.Headers.GetValueOrDefault("User-Agent", "").Contains("bot"))">
<return-response>
<set-status code="403" reason="Bot access denied" />
<set-body>{"error": "Automated access is not permitted"}</set-body>
</return-response>
</when>
</choose>
<!-- DDoS 保護(IP あたりの同時リクエスト制限)-->
<rate-limit-by-key calls="10"
renewal-period="1"
counter-key="@(context.Request.IpAddress)"
increment-condition="@(true)" />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
レート制限・トラフィック管理
製品ベースのレート制限
# 基本プラン製品作成
az apim product create \
--resource-group "rg-apim-prod" \
--service-name "contoso-api-management" \
--product-id "basic-plan" \
--product-name "Basic Plan" \
--description "Basic tier with limited quota" \
--subscription-required true \
--approval-required false \
--state "published"
# プレミアムプラン製品作成
az apim product create \
--resource-group "rg-apim-prod" \
--service-name "contoso-api-management" \
--product-id "premium-plan" \
--product-name "Premium Plan" \
--description "Premium tier with high quota" \
--subscription-required true \
--approval-required true \
--state "published"
製品レベルポリシー
<!-- product-rate-limit.xml -->
<policies>
<inbound>
<!-- 基本プラン:1時間あたり1000リクエスト -->
<choose>
<when condition="@(context.Product.Id == "basic-plan")">
<quota calls="1000" renewal-period="3600" />
<rate-limit calls="50" renewal-period="60" />
</when>
<!-- プレミアムプラン:1時間あたり10000リクエスト -->
<when condition="@(context.Product.Id == "premium-plan")">
<quota calls="10000" renewal-period="3600" />
<rate-limit calls="500" renewal-period="60" />
</when>
<!-- エンタープライズプラン:無制限 -->
<when condition="@(context.Product.Id == "enterprise-plan")">
<!-- 制限なし -->
</when>
<otherwise>
<!-- 未認証ユーザー:厳しい制限 -->
<rate-limit calls="10" renewal-period="60" />
<quota calls="100" renewal-period="3600" />
</otherwise>
</choose>
<!-- 使用量情報をログに記録 -->
<log-to-eventhub logger-id="event-hub-logger">
@{
var logEntry = new JObject();
logEntry["subscriptionId"] = context.Subscription?.Id ?? "anonymous";
logEntry["productId"] = context.Product?.Id ?? "none";
logEntry["apiId"] = context.Api.Id;
logEntry["operationId"] = context.Operation.Id;
logEntry["timestamp"] = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
logEntry["ipAddress"] = context.Request.IpAddress;
return logEntry.ToString();
}
</log-to-eventhub>
</inbound>
<backend>
<base />
</backend>
<outbound>
<!-- 使用量情報をヘッダーに追加 -->
<set-header name="X-Quota-Remaining" exists-action="override">
<value>@(context.Subscription?.QuotaCallsRemaining.ToString() ?? "N/A")</value>
</set-header>
<set-header name="X-Rate-Limit-Remaining" exists-action="override">
<value>@(context.Subscription?.CallsRemaining.ToString() ?? "N/A")</value>
</set-header>
</outbound>
<on-error>
<base />
</on-error>
</policies>
動的レート制限
<!-- dynamic-rate-limit.xml -->
<policies>
<inbound>
<base />
<!-- Azure Storage からユーザー制限設定取得 -->
<send-request mode="new" response-variable-name="user-limits" timeout="5" ignore-error="true">
<set-url>@{
string subscriptionId = context.Subscription?.Id ?? "default";
return $"https://contosolimits.blob.core.windows.net/limits/{subscriptionId}.json";
}</set-url>
<set-method>GET</set-method>
<authentication-managed-identity resource="https://storage.azure.com/" />
</send-request>
<!-- 取得した制限を適用 -->
<choose>
<when condition="@(((IResponse)context.Variables["user-limits"]).StatusCode == 200)">
<set-variable name="user-limit-config" value="@{
var response = ((IResponse)context.Variables["user-limits"]);
return response.Body.As<JObject>();
}" />
<!-- 動的レート制限適用 -->
<rate-limit-by-key calls="@{
var config = (JObject)context.Variables["user-limit-config"];
return (int)(config["rateLimit"]?["calls"] ?? 100);
}"
renewal-period="@{
var config = (JObject)context.Variables["user-limit-config"];
return (int)(config["rateLimit"]?["period"] ?? 60);
}"
counter-key="@(context.Subscription?.Id ?? context.Request.IpAddress)" />
</when>
<otherwise>
<!-- デフォルト制限 -->
<rate-limit-by-key calls="100" renewal-period="60" counter-key="@(context.Subscription?.Id ?? context.Request.IpAddress)" />
</otherwise>
</choose>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
モニタリング・ログ
Application Insights 統合
{
"type": "Microsoft.ApiManagement/service/loggers",
"apiVersion": "2021-08-01",
"name": "contoso-api-management/app-insights-logger",
"properties": {
"loggerType": "applicationInsights",
"description": "Application Insights logger",
"credentials": {
"instrumentationKey": "{instrumentation-key}"
}
}
}
詳細ログポリシー
<!-- detailed-logging.xml -->
<policies>
<inbound>
<base />
<!-- リクエスト開始ログ -->
<log-to-eventhub logger-id="event-hub-logger">
@{
var logEntry = new JObject();
logEntry["eventType"] = "request_start";
logEntry["requestId"] = context.RequestId;
logEntry["timestamp"] = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
logEntry["method"] = context.Request.Method;
logEntry["url"] = context.Request.Url.ToString();
logEntry["ipAddress"] = context.Request.IpAddress;
logEntry["userAgent"] = context.Request.Headers.GetValueOrDefault("User-Agent", "");
logEntry["subscriptionId"] = context.Subscription?.Id;
logEntry["productId"] = context.Product?.Id;
logEntry["userId"] = context.User?.Id;
// ヘッダー情報(機密情報除く)
var headers = new JObject();
foreach (var header in context.Request.Headers)
{
if (!header.Key.ToLower().Contains("authorization") &&
!header.Key.ToLower().Contains("key"))
{
headers[header.Key] = string.Join(",", header.Value);
}
}
logEntry["headers"] = headers;
return logEntry.ToString();
}
</log-to-eventhub>
<!-- パフォーマンストラッキング開始 -->
<set-variable name="start-time" value="@(DateTime.UtcNow)" />
</inbound>
<backend>
<base />
<!-- バックエンド呼び出し時間計測 -->
<set-variable name="backend-start-time" value="@(DateTime.UtcNow)" />
</backend>
<outbound>
<base />
<!-- レスポンス完了ログ -->
<log-to-eventhub logger-id="event-hub-logger">
@{
var startTime = (DateTime)context.Variables["start-time"];
var backendStartTime = (DateTime)context.Variables["backend-start-time"];
var endTime = DateTime.UtcNow;
var logEntry = new JObject();
logEntry["eventType"] = "request_complete";
logEntry["requestId"] = context.RequestId;
logEntry["timestamp"] = endTime.ToString("yyyy-MM-ddTHH:mm:ssZ");
logEntry["statusCode"] = context.Response.StatusCode;
logEntry["totalDuration"] = (endTime - startTime).TotalMilliseconds;
logEntry["backendDuration"] = (endTime - backendStartTime).TotalMilliseconds;
logEntry["requestSize"] = context.Request.Body?.Length ?? 0;
logEntry["responseSize"] = context.Response.Body?.Length ?? 0;
// エラー情報
if (context.LastError != null)
{
logEntry["error"] = new JObject();
logEntry["error"]["message"] = context.LastError.Message;
logEntry["error"]["source"] = context.LastError.Source;
}
return logEntry.ToString();
}
</log-to-eventhub>
<!-- メトリクス情報をヘッダーに追加 -->
<set-header name="X-Request-Duration" exists-action="override">
<value>@{
var startTime = (DateTime)context.Variables["start-time"];
return ((DateTime.UtcNow - startTime).TotalMilliseconds).ToString();
}</value>
</set-header>
</outbound>
<on-error>
<base />
<!-- エラーログ -->
<log-to-eventhub logger-id="event-hub-logger">
@{
var logEntry = new JObject();
logEntry["eventType"] = "request_error";
logEntry["requestId"] = context.RequestId;
logEntry["timestamp"] = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
logEntry["error"] = new JObject();
logEntry["error"]["message"] = context.LastError?.Message ?? "Unknown error";
logEntry["error"]["source"] = context.LastError?.Source ?? "";
logEntry["error"]["reason"] = context.LastError?.Reason ?? "";
return logEntry.ToString();
}
</log-to-eventhub>
</on-error>
</policies>
カスタムメトリクス送信
<!-- custom-metrics.xml -->
<policies>
<inbound>
<base />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<!-- Azure Monitor カスタムメトリクス送信 -->
<send-request mode="new" response-variable-name="metrics-response" timeout="5" ignore-error="true">
<set-url>https://api.apim.contoso.com/metrics</set-url>
<set-method>POST</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<authentication-managed-identity resource="https://api.apim.contoso.com" />
<set-body>
@{
var metrics = new JObject();
metrics["timestamp"] = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
metrics["apiId"] = context.Api.Id;
metrics["operationId"] = context.Operation.Id;
metrics["statusCode"] = context.Response.StatusCode;
metrics["responseTime"] = ((DateTime)context.Variables["start-time"] - DateTime.UtcNow).TotalMilliseconds;
metrics["subscriptionId"] = context.Subscription?.Id;
metrics["region"] = context.Deployment.Region;
// ビジネスメトリクス
if (context.Api.Id == "orders-api" && context.Operation.Id == "create-order")
{
metrics["businessEvent"] = "order_created";
metrics["orderValue"] = context.Request.Body.As<JObject>()?["total"]?.ToObject<decimal>() ?? 0;
}
return metrics.ToString();
}
</set-body>
</send-request>
</outbound>
<on-error>
<base />
</on-error>
</policies>
高度な機能
自己ホスト型ゲートウェイ
# self-hosted-gateway.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: apim-gateway
namespace: apim-system
spec:
replicas: 3
selector:
matchLabels:
app: apim-gateway
template:
metadata:
labels:
app: apim-gateway
spec:
containers:
- name: apim-gateway
image: mcr.microsoft.com/azure-api-management/gateway:latest
ports:
- name: http
containerPort: 8080
- name: https
containerPort: 8081
env:
- name: config.service.endpoint
value: "https://contoso-apim.configuration.azure-api.net/subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/Microsoft.ApiManagement/service/contoso-apim"
- name: config.service.auth
valueFrom:
secretKeyRef:
name: apim-gateway-token
key: value
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /status-0123456789abcdef
port: http
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /status-0123456789abcdef
port: http
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: apim-gateway-service
namespace: apim-system
spec:
type: LoadBalancer
ports:
- name: http
port: 80
targetPort: 8080
- name: https
port: 443
targetPort: 8081
selector:
app: apim-gateway
GraphQL API 統合
<!-- graphql-policy.xml -->
<policies>
<inbound>
<base />
<!-- GraphQL クエリ検証 -->
<validate-graphql-request max-size="102400" max-depth="10">
<authorize>
<rule>
<operations>query</operations>
<claims>
<claim name="scope" match="any">
<value>read</value>
<value>admin</value>
</claim>
</claims>
</rule>
<rule>
<operations>mutation</operations>
<claims>
<claim name="scope" match="any">
<value>write</value>
<value>admin</value>
</claim>
</claims>
</rule>
</authorize>
</validate-graphql-request>
<!-- クエリ複雑度チェック -->
<choose>
<when condition="@{
var body = context.Request.Body.As<string>(preserveContent: true);
var query = JsonConvert.DeserializeObject<JObject>(body);
var queryString = query["query"]?.ToString() ?? "";
// ネストレベルをカウント
int nestLevel = 0;
int maxNest = 0;
foreach (char c in queryString)
{
if (c == '{') nestLevel++;
if (c == '}') nestLevel--;
maxNest = Math.Max(maxNest, nestLevel);
}
return maxNest > 5; // 5段階以上のネストを禁止
}">
<return-response>
<set-status code="400" reason="Query too complex" />
<set-body>{"error": "Query complexity exceeds limit"}</set-body>
</return-response>
</when>
</choose>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
Power Platform 統合
<!-- power-platform-integration.xml -->
<policies>
<inbound>
<base />
<!-- Power Apps からのリクエスト識別 -->
<choose>
<when condition="@(context.Request.Headers.GetValueOrDefault("User-Agent", "").Contains("PowerApps"))">
<set-header name="X-Source" exists-action="override">
<value>PowerApps</value>
</set-header>
<!-- Power Apps 専用レート制限 -->
<rate-limit-by-key calls="200"
renewal-period="60"
counter-key="@("powerapps-" + (context.Request.Headers.GetValueOrDefault("X-PowerApps-App-Id", context.Request.IpAddress)))" />
</when>
</choose>
<!-- Power BI 向けデータ変換 -->
<choose>
<when condition="@(context.Request.Headers.GetValueOrDefault("X-Target", "") == "PowerBI")">
<set-query-parameter name="format" exists-action="override">
<value>powerbi</value>
</set-query-parameter>
</when>
</choose>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<!-- Power BI 向けレスポンス整形 -->
<choose>
<when condition="@(context.Request.QueryString.GetValueOrDefault("format") == "powerbi")">
<set-body>
@{
var response = context.Response.Body.As<JObject>(preserveContent: true);
// Power BI に適したフォーマットに変換
var powerBiResponse = new JObject();
powerBiResponse["value"] = response["data"];
powerBiResponse["@odata.context"] = context.Request.Url.ToString();
return powerBiResponse.ToString();
}
</set-body>
<set-header name="Content-Type" exists-action="override">
<value>application/json;odata.metadata=minimal</value>
</set-header>
</when>
</choose>
</outbound>
<on-error>
<base />
</on-error>
</policies>
パフォーマンス最適化
キャッシング戦略
<!-- advanced-caching.xml -->
<policies>
<inbound>
<base />
<!-- カスタムキャッシュキー生成 -->
<set-variable name="cache-key" value="@{
string baseKey = context.Request.Url.Path;
string queryParams = context.Request.Url.QueryString;
string userContext = context.User?.Id ?? "anonymous";
string version = context.Request.Headers.GetValueOrDefault("API-Version", "v1");
return $"{baseKey}:{queryParams}:{userContext}:{version}";
}" />
<!-- キャッシュからレスポンス取得試行 -->
<cache-lookup vary-by-developer="false"
vary-by-developer-groups="false"
downstream-caching-type="none"
cache-preference="internal"
caching-key="@((string)context.Variables["cache-key"])">
<vary-by-header>Accept</vary-by-header>
<vary-by-header>Accept-Encoding</vary-by-header>
<vary-by-query-parameter>page</vary-by-query-parameter>
<vary-by-query-parameter>size</vary-by-query-parameter>
</cache-lookup>
<!-- 条件付きキャッシュ -->
<choose>
<when condition="@(context.Request.Method == "GET" && context.Request.Url.Path.Contains("/reference/"))">
<!-- 参照データは長時間キャッシュ -->
<set-variable name="cache-duration" value="3600" />
</when>
<when condition="@(context.Request.Method == "GET" && context.Request.Url.Path.Contains("/user/"))">
<!-- ユーザーデータは短時間キャッシュ -->
<set-variable name="cache-duration" value="300" />
</when>
<otherwise>
<!-- その他は中程度 -->
<set-variable name="cache-duration" value="600" />
</otherwise>
</choose>
</inbound>
<backend>
<base />
<!-- 外部キャッシュ(Redis)からの取得 -->
<send-request mode="new" response-variable-name="redis-response" timeout="2" ignore-error="true">
<set-url>@($"https://contoso-redis.redis.cache.windows.net:6380/cache/{(string)context.Variables["cache-key"]}")</set-url>
<set-method>GET</set-method>
<authentication-managed-identity resource="https://redis.cache.windows.net/" />
</send-request>
<!-- Redis キャッシュヒット時はバックエンド呼び出しをスキップ -->
<choose>
<when condition="@(((IResponse)context.Variables["redis-response"]).StatusCode == 200)">
<return-response>
<set-status code="200" reason="OK" />
<set-header name="X-Cache" exists-action="override">
<value>HIT-REDIS</value>
</set-header>
<set-body>@(((IResponse)context.Variables["redis-response"]).Body.As<string>())</set-body>
</return-response>
</when>
</choose>
</backend>
<outbound>
<base />
<!-- レスポンスをキャッシュに保存 -->
<choose>
<when condition="@(context.Response.StatusCode == 200)">
<!-- 内部キャッシュに保存 -->
<cache-store duration="@((int)context.Variables["cache-duration"])"
cache-preference="internal"
caching-key="@((string)context.Variables["cache-key"])" />
<!-- Redis にも保存 -->
<send-request mode="new" response-variable-name="redis-store-response" timeout="2" ignore-error="true">
<set-url>@($"https://contoso-redis.redis.cache.windows.net:6380/cache/{(string)context.Variables["cache-key"]}")</set-url>
<set-method>PUT</set-method>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<authentication-managed-identity resource="https://redis.cache.windows.net/" />
<set-body>@(context.Response.Body.As<string>(preserveContent: true))</set-body>
</send-request>
<set-header name="X-Cache" exists-action="override">
<value>MISS</value>
</set-header>
</when>
</choose>
<!-- キャッシュ制御ヘッダー設定 -->
<set-header name="Cache-Control" exists-action="override">
<value>@($"public, max-age={(int)context.Variables["cache-duration"]}")</value>
</set-header>
<set-header name="ETag" exists-action="override">
<value>@{
var body = context.Response.Body.As<string>(preserveContent: true);
using (var sha1 = System.Security.Cryptography.SHA1.Create())
{
var hash = sha1.ComputeHash(System.Text.Encoding.UTF8.GetBytes(body));
return Convert.ToBase64String(hash);
}
}</value>
</set-header>
</outbound>
<on-error>
<base />
</on-error>
</policies>
バックエンド負荷分散
<!-- load-balancing.xml -->
<policies>
<inbound>
<base />
<!-- バックエンドプール定義 -->
<set-variable name="backend-pool" value="@{
var backends = new List<string> {
"https://api1.contoso.com",
"https://api2.contoso.com",
"https://api3.contoso.com"
};
return backends;
}" />
<!-- ヘルスチェック結果取得 -->
<send-request mode="new" response-variable-name="health-check" timeout="1" ignore-error="true">
<set-url>https://contoso-health.blob.core.windows.net/health/backends.json</set-url>
<set-method>GET</set-method>
<authentication-managed-identity resource="https://storage.azure.com/" />
</send-request>
<!-- 健全なバックエンドのみに負荷分散 -->
<set-variable name="healthy-backends" value="@{
var allBackends = (List<string>)context.Variables["backend-pool"];
var healthyBackends = new List<string>();
try {
var healthResponse = ((IResponse)context.Variables["health-check"]);
if (healthResponse.StatusCode == 200) {
var healthData = healthResponse.Body.As<JObject>();
foreach (var backend in allBackends) {
var backendName = backend.Replace("https://", "").Replace(".contoso.com", "");
if (healthData[backendName]?.ToString() == "healthy") {
healthyBackends.Add(backend);
}
}
}
} catch {
// ヘルスチェック失敗時は全バックエンドを使用
healthyBackends = allBackends;
}
return healthyBackends.Count > 0 ? healthyBackends : allBackends;
}" />
<!-- ラウンドロビン選択 -->
<set-variable name="selected-backend" value="@{
var backends = (List<string>)context.Variables["healthy-backends"];
if (backends.Count == 0) return "https://api1.contoso.com"; // フォールバック
// リクエストIDベースのハッシュで選択(一貫性保証)
var hash = context.RequestId.GetHashCode();
var index = Math.Abs(hash) % backends.Count;
return backends[index];
}" />
<!-- 選択されたバックエンドに設定 -->
<set-backend-service base-url="@((string)context.Variables["selected-backend"])" />
<!-- バックエンド情報をヘッダーに追加 -->
<set-header name="X-Backend-Server" exists-action="override">
<value>@((string)context.Variables["selected-backend"])</value>
</set-header>
</inbound>
<backend>
<base />
<!-- バックエンド障害時のフォールバック -->
<choose>
<when condition="@(context.Response.StatusCode >= 500)">
<!-- 別のバックエンドにリトライ -->
<set-variable name="retry-backend" value="@{
var backends = (List<string>)context.Variables["healthy-backends"];
var currentBackend = (string)context.Variables["selected-backend"];
// 現在使用中以外のバックエンドを選択
var availableBackends = backends.Where(b => b != currentBackend).ToList();
if (availableBackends.Count > 0) {
return availableBackends[0];
}
return currentBackend;
}" />
<set-backend-service base-url="@((string)context.Variables["retry-backend"])" />
<forward-request buffer-request-body="true" />
</when>
</choose>
</backend>
<outbound>
<base />
<!-- レスポンス時間計測 -->
<set-header name="X-Response-Time" exists-action="override">
<value>@{
var startTime = context.Variables.ContainsKey("start-time") ?
(DateTime)context.Variables["start-time"] : DateTime.UtcNow;
return (DateTime.UtcNow - startTime).TotalMilliseconds.ToString();
}</value>
</set-header>
</outbound>
<on-error>
<base />
</on-error>
</policies>
トラブルシューティング
診断ツールとログ分析
Azure CLI による診断
# API Management インスタンス状態確認
az apim show \
--name "contoso-api-management" \
--resource-group "rg-apim-prod" \
--query "{name:name,status:provisioningState,gatewayUrl:gatewayUrl}"
# API 一覧取得
az apim api list \
--service-name "contoso-api-management" \
--resource-group "rg-apim-prod" \
--query "[].{name:displayName,path:path,id:id}"
# 製品一覧とサブスクリプション確認
az apim product list \
--service-name "contoso-api-management" \
--resource-group "rg-apim-prod"
# ネットワーク接続確認
az apim check-name-availability \
--service-name "contoso-api-management"
Application Insights クエリ
// API Management リクエスト分析
requests
| where timestamp > ago(1h)
| where cloud_RoleName contains "apim"
| summarize
RequestCount = count(),
AvgDuration = avg(duration),
P95Duration = percentile(duration, 95),
ErrorRate = countif(success == false) * 100.0 / count()
by bin(timestamp, 5m), operation_Name
| order by timestamp desc
// エラー率分析
requests
| where timestamp > ago(24h)
| where cloud_RoleName contains "apim"
| where success == false
| summarize ErrorCount = count() by resultCode, operation_Name
| order by ErrorCount desc
// レート制限違反
traces
| where timestamp > ago(1h)
| where message contains "rate limit"
| summarize count() by bin(timestamp, 5m), severityLevel
| order by timestamp desc
// パフォーマンス分析
requests
| where timestamp > ago(1h)
| where cloud_RoleName contains "apim"
| where duration > 5000 // 5秒以上のリクエスト
| project timestamp, operation_Name, duration, resultCode
| order by duration desc
よくある問題と解決法
CORS 問題の解決
<!-- cors-troubleshooting.xml -->
<policies>
<inbound>
<base />
<!-- 詳細な CORS ログ -->
<choose>
<when condition="@(context.Request.Method == "OPTIONS")">
<log-to-eventhub logger-id="debug-logger">
@{
var corsInfo = new JObject();
corsInfo["eventType"] = "cors_preflight";
corsInfo["origin"] = context.Request.Headers.GetValueOrDefault("Origin", "");
corsInfo["method"] = context.Request.Headers.GetValueOrDefault("Access-Control-Request-Method", "");
corsInfo["headers"] = context.Request.Headers.GetValueOrDefault("Access-Control-Request-Headers", "");
return corsInfo.ToString();
}
</log-to-eventhub>
</when>
</choose>
<!-- 動的 CORS 設定 -->
<cors>
<allowed-origins>
<origin>@{
string origin = context.Request.Headers.GetValueOrDefault("Origin", "");
string[] allowedDomains = {"https://contoso.com", "https://app.contoso.com", "http://localhost:3000"};
if (allowedDomains.Contains(origin)) {
return origin;
}
return "https://contoso.com"; // デフォルト
}</origin>
</allowed-origins>
<allowed-methods preflight-result-max-age="300">
<method>GET</method>
<method>POST</method>
<method>PUT</method>
<method>DELETE</method>
<method>OPTIONS</method>
</allowed-methods>
<allowed-headers>
<header>*</header>
</allowed-headers>
<expose-headers>
<header>X-RateLimit-Limit</header>
<header>X-RateLimit-Remaining</header>
<header>X-Request-ID</header>
</expose-headers>
</cors>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<!-- CORS ヘッダーの確認ログ -->
<log-to-eventhub logger-id="debug-logger">
@{
var corsResponse = new JObject();
corsResponse["eventType"] = "cors_response";
corsResponse["accessControlAllowOrigin"] = context.Response.Headers.GetValueOrDefault("Access-Control-Allow-Origin", "NOT_SET");
corsResponse["accessControlAllowMethods"] = context.Response.Headers.GetValueOrDefault("Access-Control-Allow-Methods", "NOT_SET");
return corsResponse.ToString();
}
</log-to-eventhub>
</outbound>
<on-error>
<base />
</on-error>
</policies>
タイムアウト問題の診断
<!-- timeout-diagnosis.xml -->
<policies>
<inbound>
<base />
<set-variable name="request-start-time" value="@(DateTime.UtcNow)" />
</inbound>
<backend>
<base />
<!-- タイムアウト設定の診断 -->
<choose>
<when condition="@(context.Api.Id == "slow-api")">
<!-- 遅いAPIには長めのタイムアウト -->
<timeout value="120" />
</when>
<otherwise>
<!-- 通常のタイムアウト -->
<timeout value="30" />
</otherwise>
</choose>
<!-- バックエンド呼び出し前の時間記録 -->
<set-variable name="backend-start-time" value="@(DateTime.UtcNow)" />
<!-- リトライ付きフォワード -->
<retry condition="@(context.Response.StatusCode == 408 || context.Response.StatusCode >= 500)"
count="3"
interval="2"
delta="1"
max-interval="10">
<forward-request buffer-request-body="true" timeout="30" />
</retry>
</backend>
<outbound>
<base />
<!-- タイミング情報をヘッダーに追加 -->
<set-header name="X-Total-Time" exists-action="override">
<value>@{
var startTime = (DateTime)context.Variables["request-start-time"];
return (DateTime.UtcNow - startTime).TotalMilliseconds.ToString();
}</value>
</set-header>
<set-header name="X-Backend-Time" exists-action="override">
<value>@{
var backendStartTime = (DateTime)context.Variables["backend-start-time"];
return (DateTime.UtcNow - backendStartTime).TotalMilliseconds.ToString();
}</value>
</set-header>
<!-- パフォーマンス警告 -->
<choose>
<when condition="@{
var startTime = (DateTime)context.Variables["request-start-time"];
return (DateTime.UtcNow - startTime).TotalMilliseconds > 10000;
}">
<log-to-eventhub logger-id="alert-logger">
@{
var alert = new JObject();
alert["alertType"] = "slow_request";
alert["duration"] = (DateTime.UtcNow - (DateTime)context.Variables["request-start-time"]).TotalMilliseconds;
alert["apiId"] = context.Api.Id;
alert["operationId"] = context.Operation.Id;
alert["requestId"] = context.RequestId;
return alert.ToString();
}
</log-to-eventhub>
</when>
</choose>
</outbound>
<on-error>
<base />
<!-- タイムアウトエラーの詳細ログ -->
<choose>
<when condition="@(context.LastError?.Source == "timeout")">
<log-to-eventhub logger-id="error-logger">
@{
var timeoutError = new JObject();
timeoutError["errorType"] = "timeout";
timeoutError["requestDuration"] = (DateTime.UtcNow - (DateTime)context.Variables["request-start-time"]).TotalMilliseconds;
timeoutError["apiId"] = context.Api.Id;
timeoutError["backendUrl"] = context.Request.Url.ToString();
return timeoutError.ToString();
}
</log-to-eventhub>
</when>
</choose>
</on-error>
</policies>
デバッグ用設定
<!-- debug-policy.xml -->
<policies>
<inbound>
<base />
<!-- デバッグモードの確認 -->
<choose>
<when condition="@(context.Request.Headers.GetValueOrDefault("X-Debug-Mode", "") == "true")">
<set-variable name="debug-mode" value="true" />
<!-- リクエスト詳細をログ出力 -->
<log-to-eventhub logger-id="debug-logger">
@{
var debugInfo = new JObject();
debugInfo["debugType"] = "request_details";
debugInfo["method"] = context.Request.Method;
debugInfo["url"] = context.Request.Url.ToString();
debugInfo["headers"] = new JObject();
foreach (var header in context.Request.Headers)
{
debugInfo["headers"][header.Key] = string.Join(",", header.Value);
}
if (context.Request.Body != null)
{
debugInfo["body"] = context.Request.Body.As<string>(preserveContent: true);
}
return debugInfo.ToString();
}
</log-to-eventhub>
</when>
</choose>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<!-- デバッグモード時の詳細レスポンス -->
<choose>
<when condition="@((bool?)context.Variables["debug-mode"] == true)">
<set-header name="X-Debug-Request-ID" exists-action="override">
<value>@(context.RequestId)</value>
</set-header>
<set-header name="X-Debug-API-ID" exists-action="override">
<value>@(context.Api.Id)</value>
</set-header>
<set-header name="X-Debug-Operation-ID" exists-action="override">
<value>@(context.Operation.Id)</value>
</set-header>
<set-header name="X-Debug-Subscription-ID" exists-action="override">
<value>@(context.Subscription?.Id ?? "N/A")</value>
</set-header>
<!-- レスポンス詳細をログ出力 -->
<log-to-eventhub logger-id="debug-logger">
@{
var debugResponse = new JObject();
debugResponse["debugType"] = "response_details";
debugResponse["statusCode"] = context.Response.StatusCode;
debugResponse["headers"] = new JObject();
foreach (var header in context.Response.Headers)
{
debugResponse["headers"][header.Key] = string.Join(",", header.Value);
}
if (context.Response.Body != null)
{
debugResponse["body"] = context.Response.Body.As<string>(preserveContent: true);
}
return debugResponse.ToString();
}
</log-to-eventhub>
</when>
</choose>
</outbound>
<on-error>
<base />
</on-error>
</policies>
参考リンク
公式ドキュメント
- Azure API Management Documentation
- API Management REST API Reference
- Azure API Management Policies
- API Management PowerShell Reference
ベストプラクティス
- API Management Best Practices
- Azure Well-Architected Framework - API Management
- API Design Guidelines
学習リソース
ツールとSDK
- Azure CLI API Management Extension
- Azure PowerShell API Management Module
- Azure Resource Manager Templates
- Terraform Azure Provider