Azure API Management

Microsoftのエンタープライズ向けAPI管理プラットフォーム。ハイブリッド・マルチクラウド対応、開発者ポータル、詳細な分析機能を提供。

API管理Azureエンタープライズ開発者ポータルOAuthAzure ADハイブリッドクラウドマルチクラウド分析

概要

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>

参考リンク

公式ドキュメント

ベストプラクティス

学習リソース

ツールとSDK

コミュニティ