diff --git a/src/frontend/config/sidebar/integrations.topics.ts b/src/frontend/config/sidebar/integrations.topics.ts index 391630f18..e991df050 100644 --- a/src/frontend/config/sidebar/integrations.topics.ts +++ b/src/frontend/config/sidebar/integrations.topics.ts @@ -405,6 +405,44 @@ export const integrationTopics: StarlightSidebarTopicsUserConfig = { }, ], }, + { + label: 'Azure API Management', + collapsed: true, + items: [ + { + label: 'Get started', + slug: 'integrations/cloud/azure/azure-api-management/azure-api-management-get-started', + }, + { + label: 'APIs and operations', + slug: 'integrations/cloud/azure/azure-api-management/azure-api-management-apis', + }, + { + label: 'OpenAPI import', + slug: 'integrations/cloud/azure/azure-api-management/azure-api-management-openapi', + }, + { + label: 'Backends and pools', + slug: 'integrations/cloud/azure/azure-api-management/azure-api-management-backends', + }, + { + label: 'Policies and named values', + slug: 'integrations/cloud/azure/azure-api-management/azure-api-management-policies', + }, + { + label: 'Products and subscriptions', + slug: 'integrations/cloud/azure/azure-api-management/azure-api-management-products', + }, + { + label: 'Service configuration', + slug: 'integrations/cloud/azure/azure-api-management/azure-api-management-service', + }, + { + label: 'Networking', + slug: 'integrations/cloud/azure/azure-api-management/azure-api-management-networking', + }, + ], + }, { label: 'Azure App Configuration', collapsed: true, diff --git a/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-apis.mdx b/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-apis.mdx new file mode 100644 index 000000000..a455474e7 --- /dev/null +++ b/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-apis.mdx @@ -0,0 +1,224 @@ +--- +title: Azure API Management APIs and operations +description: Publish Aspire workloads and other backends as Azure API Management APIs, define explicit operations, and add OpenAI-compatible APIs in your AppHost. +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +An API in Azure API Management (APIM) is a set of operations published at a path on the gateway. The Aspire Azure API Management hosting integration models each API as an `AzureApiManagementApiResource` child of the service. This page assumes the `apim` resource and the `catalog` workload from [Get started with Azure API Management](/integrations/cloud/azure/azure-api-management/azure-api-management-get-started/#add-a-service-and-route-a-workload). + +## Route an Aspire workload + +Call `AddApi` with a compute resource to publish that workload through the gateway. The compute resource must expose an HTTP or HTTPS endpoint and have an Azure compute environment, such as an Azure Container Apps environment. + + + + +```typescript title="apphost.mts" +await apim.addApi("catalog-api", catalog, "catalog", { + displayName: "Catalog API", + subscriptionRequired: true, + apiName: "catalog-v1", +}); +``` + + + + +```csharp title="AppHost.cs" +apim.AddApi( + "catalog-api", + catalog, + path: "catalog", + displayName: "Catalog API", + subscriptionRequired: true, + apiName: "catalog-v1"); +``` + + + + +For each compute-targeted API, Aspire deploys: + +- An API that accepts HTTPS requests at the path that you specify. +- A reusable APIM backend whose URL is the workload's deployed endpoint, with TLS certificate-chain and hostname validation enabled. +- Catch-all operations that forward requests to that backend. For more information, see [Generated proxy operations](#generated-proxy-operations). +- An API policy that inherits the parent policy and sets the backend service. + +The `AddApi` method has the following parameters: + +| Parameter | Required | Description | +| --------- | -------- | ----------- | +| `name` | Yes | The Aspire resource name. It's also the default physical API name. | +| `target` | Compute-targeted only | The compute resource to route to. | +| `path` | Yes | The public path beneath the gateway host name. Surrounding slashes are trimmed. | +| `displayName` | No | The API display name. Defaults to the resource name. | +| `subscriptionRequired` | No | Whether callers must send an APIM subscription key. Defaults to `true`. | +| `apiName` | No | The physical API identifier in APIM. Defaults to the resource name. | + +Aspire validates these values when you add the API: + +- The physical API name is at most 256 characters and can't contain `*`, `#`, `&`, `+`, `:`, `<`, `>`, or `?`. +- The path is non-empty after trimming and has at most 400 characters. +- The display name has 1 to 300 characters. +- The physical API name and the path are each unique within the service, ignoring case. + +### Choose the workload endpoint + +APIM selects the first external HTTP or HTTPS endpoint of the workload, or the first HTTP or HTTPS endpoint when none is external. If the workload exposes no HTTP or HTTPS endpoint, publishing fails with the error `Resource 'catalog' does not have an HTTP or HTTPS endpoint.` + +A workload that exposes only internal endpoints is reachable only when the APIM service has virtual network connectivity. For more information, see [Virtual networking](/integrations/cloud/azure/azure-api-management/azure-api-management-networking/#route-to-a-private-container-apps-environment). + +## Route to a backend you configure + +Use the overload without a compute target when the API should forward to a backend that isn't an Aspire compute resource, such as an external service. Then attach a backend or a backend pool with `WithBackend`. + + + + +```typescript title="apphost.mts" +import { refExpr } from './.aspire/modules/aspire.mjs'; + +const backend = await apim.addBackend("legacy-backend", refExpr`https://legacy.example.com`); + +const legacyApi = await apim.addApiWithoutTarget("legacy-api", "legacy"); +await legacyApi.withApiBackend(backend); +``` + + + + +```csharp title="AppHost.cs" +var backend = apim.AddBackend( + "legacy-backend", + ReferenceExpression.Create($"https://legacy.example.com")); + +apim.AddApi("legacy-api", path: "legacy") + .WithBackend(backend); +``` + + + + +In TypeScript, the overload without a target is `addApiWithoutTarget`. An API without a compute target must have exactly one backend or pool when you deploy. Aspire throws an `InvalidOperationException` if the API has none, if it already has one, if it targets a compute resource, or if the backend belongs to a different APIM service. + +For backend options, backend pools, and managed identity authentication, see [Backends and backend pools](/integrations/cloud/azure/azure-api-management/azure-api-management-backends/). + +## Generated proxy operations + +APIM needs operations to dispatch requests. When an API has no OpenAPI import, Aspire generates a root operation and a catch-all operation for each of the eight supported HTTP methods: + +| Operation name | URL template | Matches | +| -------------- | ------------ | ------- | +| `proxy-{method}-root` | `/` | The API path itself, such as `/catalog`. | +| `proxy-{method}` | `/{*path}` | Any descendant path, such as `/catalog/products/1`. | + +The methods are `DELETE`, `GET`, `HEAD`, `OPTIONS`, `PATCH`, `POST`, `PUT`, and `TRACE`. APIM doesn't reliably dispatch operations that use the wildcard method, so Aspire creates a concrete operation for each method. The wildcard route doesn't match the API path itself, which is why Aspire also generates the root operations. + +When you import an OpenAPI document, the imported operations replace the generated proxy operations. For more information, see [OpenAPI import](/integrations/cloud/azure/azure-api-management/azure-api-management-openapi/). + +## Add explicit operations + +Call `AddOperation` to model an operation explicitly, for example to attach a policy to a specific route. Explicit operations are provisioned in addition to the generated proxy operations or the imported operations. + + + + +```typescript title="apphost.mts" +const catalogApi = await apim.addApi("catalog-api", catalog, "catalog"); + +await catalogApi.addOperation("get-product", "GET", "/products/{id}", { + displayName: "Get product", +}); +await catalogApi.addOperation("create-product", "POST", "/products"); +``` + + + + +```csharp title="AppHost.cs" +var catalogApi = apim.AddApi("catalog-api", catalog, path: "catalog"); + +catalogApi.AddOperation( + "get-product", + method: "GET", + urlTemplate: "/products/{id}", + displayName: "Get product"); + +catalogApi.AddOperation("create-product", "POST", "/products"); +``` + + + + +Each placeholder in the URL template, such as `{id}`, becomes a required string template parameter. The `AddOperation` method has the following parameters: + +| Parameter | Required | Description | +| --------- | -------- | ----------- | +| `name` | Yes | The Aspire resource name. It's also the default physical operation name. | +| `method` | Yes | A concrete HTTP method. Aspire converts it to uppercase. The wildcard `*` isn't allowed. | +| `urlTemplate` | Yes | The URL template relative to the API path. Maximum 1,000 characters. | +| `displayName` | No | The operation display name. Defaults to the resource name. | +| `operationName` | No | The physical operation identifier in APIM. Defaults to the resource name. Maximum 80 characters. | + +Aspire enforces these rules: + +- The physical operation name is unique within the API. +- The HTTP method and URL template are unique within the API. Templates that differ only in parameter names, such as `/products/{id}` and `/products/{productId}`, count as the same route. +- The names `proxy`, `proxy-{method}`, and `proxy-{method}-root` are reserved for generated operations. +- The routes `/` and `/{*path}` are reserved for generated proxy operations. You can use them only when the API has an OpenAPI import, regardless of whether you call `WithOpenApiDocument` before or after `AddOperation`. + +Operations support operation-level policies. For more information, see [Policies and named values](/integrations/cloud/azure/azure-api-management/azure-api-management-policies/). + +## Add an OpenAI-compatible API + +Call `AddOpenAIApi` to publish an API that forwards OpenAI-style requests to a backend pool. The method creates an API with the generated proxy operations and an explicit `POST /chat/completions` operation named `chat-completions`. + + + + +```typescript title="apphost.mts" +const foundry = await builder.addFoundry("foundry"); +const chat = await foundry.addDeployment("chat", { + name: "gpt-5-mini", + version: "2025-08-07", + format: "OpenAI", +}); + +const backend = await apim.addFoundryBackend("foundry-backend", chat); + +const openAIApi = await apim.addOpenAIApi("openai-api", "openai", { + subscriptionRequired: false, +}); +await openAIApi.withApiBackend(backend); +``` + + + + +```csharp title="AppHost.cs" +using Aspire.Hosting.Foundry; + +var foundry = builder.AddFoundry("foundry"); +var chat = foundry.AddDeployment("chat", FoundryModel.OpenAI.Gpt5Mini); + +var backend = apim.AddFoundryBackend("foundry-backend", chat); + +apim.AddOpenAIApi( + "openai-api", + path: "openai", + subscriptionRequired: false) + .WithBackend(backend); +``` + + + + +Requests beneath the API path are appended to the URI of the selected backend. You can attach a single backend, as in the preceding example, or a backend pool that load balances several deployments. Create the backends with `AddAzureOpenAIBackend` or `AddFoundryBackend`. For a pool example, see [Load balance OpenAI deployments](/integrations/cloud/azure/azure-api-management/azure-api-management-backends/#load-balance-openai-deployments). + +## See also + +- [Get started with Azure API Management](/integrations/cloud/azure/azure-api-management/azure-api-management-get-started/) +- [Import OpenAPI definitions](/integrations/cloud/azure/azure-api-management/azure-api-management-openapi/) +- [Backends and backend pools](/integrations/cloud/azure/azure-api-management/azure-api-management-backends/) +- [API Management key concepts](https://learn.microsoft.com/azure/api-management/api-management-key-concepts) diff --git a/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-backends.mdx b/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-backends.mdx new file mode 100644 index 000000000..d4b60902e --- /dev/null +++ b/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-backends.mdx @@ -0,0 +1,375 @@ +--- +title: Azure API Management backends and pools +description: Add HTTP, SOAP, Azure OpenAI, Foundry, and Blob Storage backends to Azure API Management, and group them into weighted or priority-based pools. +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +A backend in Azure API Management (APIM) is a reusable definition of a service that the gateway forwards requests to. A backend pool groups several backends so that APIM load balances across them. The Aspire Azure API Management hosting integration models these as `AzureApiManagementBackendResource` and `AzureApiManagementBackendPoolResource` children of the service, and an API selects one of them with `WithBackend`. + +You don't need to add a backend when you route an Aspire workload with `AddApi`, because that overload creates the backend for you. Use the APIs on this page when an API forwards to something else. + +This page assumes the `apim` resource from [Get started with Azure API Management](/integrations/cloud/azure/azure-api-management/azure-api-management-get-started/#add-a-service-and-route-a-workload). + +## Add a backend + +Call `AddBackend` with a deferred URI. In C#, create the URI with `ReferenceExpression.Create`. In TypeScript, use the `refExpr` tagged template. + + + + +```typescript title="apphost.mts" +import { refExpr } from './.aspire/modules/aspire.mjs'; + +const backend = await apim.addBackend("orders-backend", refExpr`https://orders.example.com`); + +const ordersApi = await apim.addApiWithoutTarget("orders-api", "orders"); +await ordersApi.withApiBackend(backend); +``` + + + + +```csharp title="AppHost.cs" +var backend = apim.AddBackend( + "orders-backend", + ReferenceExpression.Create($"https://orders.example.com")); + +apim.AddApi("orders-api", path: "orders") + .WithBackend(backend); +``` + + + + +### Backend options + +Pass an options object to `AddBackend` to configure the protocol, TLS validation, managed identity authentication, and a circuit breaker. + + + + +```typescript title="apphost.mts" +import { AzureApiManagementBackendProtocol, refExpr } from './.aspire/modules/aspire.mjs'; + +const backend = await apim.addBackend( + "custom-backend", + refExpr`https://backend.example.com`, + { + options: { + protocol: AzureApiManagementBackendProtocol.Soap, + managedIdentityResource: "api://backend", + validateCertificateName: false, + circuitBreaker: { + name: "serverErrors", + failureCount: 3, + failureIntervalSeconds: 30, + tripDurationSeconds: 60, + statusCodeRanges: [{ minimum: 500, maximum: 599 }], + }, + }, + }); +``` + + + + +```csharp title="AppHost.cs" +using Aspire.Hosting.Azure; + +var backend = apim.AddBackend( + "custom-backend", + ReferenceExpression.Create($"https://backend.example.com"), + new AzureApiManagementBackendOptions + { + Protocol = AzureApiManagementBackendProtocol.Soap, + ManagedIdentityResource = "api://backend", + ValidateCertificateName = false, + CircuitBreaker = new AzureApiManagementCircuitBreakerOptions + { + Name = "serverErrors", + FailureCount = 3, + FailureIntervalSeconds = 30, + TripDurationSeconds = 60, + StatusCodeRanges = [new(500, 599)], + }, + }); +``` + + + + +The backend options have these properties: + +| Option | Default | Description | +| ------ | ------- | ----------- | +| `Title` | The resource name | The backend display title. It has 1 to 300 characters. | +| `Protocol` | `Http` | The protocol to the backend: `Http` for HTTP or HTTPS, or `Soap`. | +| `ValidateCertificateChain` | `true` | Whether APIM validates the backend certificate chain. | +| `ValidateCertificateName` | `true` | Whether APIM validates the backend certificate host name. | +| `ManagedIdentityResource` | None | The Microsoft Entra resource URI that APIM requests a token for, such as `https://storage.azure.com/`. Leave it unset for anonymous or policy-defined authentication. | +| `CircuitBreaker` | None | The circuit breaker rule. | + +The `AddBackend` method also accepts a `backendName` that sets the physical backend identifier in APIM. It defaults to the resource name, has at most 80 characters, can't contain `*`, `#`, `&`, `+`, `:`, `<`, `>`, or `?`, and must be unique across all backends and backend pools of the service. + +TLS certificate-chain and host name validation are on by default. Turn them off only for a specific backend that can't present a valid certificate. + +### Authenticate with managed identity + +When you set `ManagedIdentityResource`, the generated API policy includes an `authentication-managed-identity` statement for that resource URI. It selects APIM's user-assigned managed identity explicitly by client ID, because APIM otherwise defaults to a system-assigned identity. + +Generic backends don't infer Azure role assignments, because the required role depends on the target service. Grant the APIM identity access to the target yourself. In C#, the identity is available as `apim.Resource.Identity`, and its principal ID is available as `apim.Resource.PrincipalId`. For more information about the identity, see [Service configuration](/integrations/cloud/azure/azure-api-management/azure-api-management-service/#managed-identity). + +### Configure a circuit breaker + +A circuit breaker stops APIM from sending requests to a backend that's failing. Circuit breakers aren't supported on the Consumption tier. + +| Option | Default | Description | +| ------ | ------- | ----------- | +| `Name` | `default` | The rule name. | +| `FailureCount` | `1` | The number of failures within the interval that opens the circuit. Minimum 1. | +| `FailureIntervalSeconds` | `10` | The interval, in seconds, during which failures are counted. Minimum 1. | +| `StatusCodeRanges` | None | The inclusive HTTP status-code ranges that count as failures. Each minimum is 200 to 599, each maximum is at least its minimum and at most 599, and a rule has at most 10 ranges. | +| `TripDurationSeconds` | `10` | How long, in seconds, the circuit stays open. Minimum 1. | +| `AcceptRetryAfter` | `false` | Whether APIM honors the backend's `Retry-After` response header. | + +## Add a backend for an Azure service + +The specialized methods create a backend for a modeled Azure resource. Each one sets the URL, the managed identity resource URI, and the role assignment that the APIM identity needs. + +| Method | Target | Managed identity resource | Role granted to the APIM identity | Circuit breaker | +| ------ | ------ | ------------------------- | --------------------------------- | --------------- | +| `AddAzureOpenAIBackend` | An Azure OpenAI deployment | `https://cognitiveservices.azure.com` | Cognitive Services OpenAI User on the account | Opens on HTTP 429 and honors `Retry-After` | +| `AddFoundryBackend` | A Foundry deployment that uses the OpenAI format | `https://cognitiveservices.azure.com` | Cognitive Services User on the account | Opens on HTTP 429 and honors `Retry-After` | +| `AddBlobStorageBackend` | An Azure Blob Storage resource | `https://storage.azure.com/` | Storage Blob Data Reader on the storage account | None | + +The Azure OpenAI and Foundry backends use the deployment URL `{account endpoint}openai/deployments/{deployment name}`. They aren't supported on the Consumption tier, and `AddFoundryBackend` throws an `InvalidOperationException` for a deployment that doesn't use the OpenAI format. + + + + +```typescript title="apphost.mts" +const openai = await builder.addAzureOpenAI("openai"); +const chat = await openai.addDeployment("chat", "gpt-4o", "2024-08-06"); + +const openAIBackend = await apim.addAzureOpenAIBackend("openai-backend", chat); + +const storage = await builder.addAzureStorage("storage"); +const blobs = await storage.addBlobs("blobs"); + +const blobBackend = await apim.addBlobStorageBackend("blob-backend", blobs); +``` + + + + +```csharp title="AppHost.cs" +var openai = builder.AddAzureOpenAI("openai"); +var chat = openai.AddDeployment("chat", "gpt-4o", "2024-08-06"); + +var openAIBackend = apim.AddAzureOpenAIBackend("openai-backend", chat); + +var storage = builder.AddAzureStorage("storage"); +var blobs = storage.AddBlobs("blobs"); + +var blobBackend = apim.AddBlobStorageBackend("blob-backend", blobs); +``` + + + + +The principal that runs `aspire deploy` needs `Microsoft.Authorization/roleAssignments/write` on each target account, for example through the User Access Administrator or Owner role. For more information about role assignments, see [Azure role assignments](/integrations/cloud/azure/role-assignments/). + +## Add a backend pool + +A backend pool is a backend that load balances across its members. Create a pool with `AddBackendPool`, then add members with `WithBackend` in C# or `addBackendPoolMember` in TypeScript. Attach the pool to an API with `WithBackend` in C# or `withApiBackendPool` in TypeScript. + + + + +```typescript title="apphost.mts" +import { refExpr } from './.aspire/modules/aspire.mjs'; + +const primary = await apim.addBackend("primary", refExpr`https://primary.example.com`); +const secondary = await apim.addBackend("secondary", refExpr`https://secondary.example.com`); + +const pool = await apim.addBackendPool("backend-pool"); +await pool.addBackendPoolMember(primary, { priority: 1, weight: 3 }); +await pool.addBackendPoolMember(secondary, { priority: 1, weight: 1 }); + +const pooledApi = await apim.addApiWithoutTarget("pooled-api", "pooled"); +await pooledApi.withApiBackendPool(pool); +``` + + + + +```csharp title="AppHost.cs" +var primary = apim.AddBackend( + "primary", + ReferenceExpression.Create($"https://primary.example.com")); +var secondary = apim.AddBackend( + "secondary", + ReferenceExpression.Create($"https://secondary.example.com")); + +var pool = apim.AddBackendPool("backend-pool") + .WithBackend(primary, priority: 1, weight: 3) + .WithBackend(secondary, priority: 1, weight: 1); + +apim.AddApi("pooled-api", path: "pooled") + .WithBackend(pool); +``` + + + + +Members at the same priority share traffic according to their weights, so the example sends about three of every four requests to `primary`. A lower priority number is preferred. APIM uses the members at the next priority when every member in the preferred group has an open circuit. + +Aspire enforces these rules for pools: + +- Priority and weight are each 0 to 100, and both default to 1. +- A pool has at most 30 members, and a backend can be added to a pool only once. +- The pool and its members belong to the same APIM service. +- All members use the same `ManagedIdentityResource` value. The API that uses the pool authenticates with that audience. +- A pool contains at least one backend when you deploy. +- The physical pool name follows the same rules as a backend name, and the `displayName` has 1 to 300 characters. + +An API accepts either a single backend or a pool. The Azure OpenAI and Foundry backends include a circuit breaker by default. For generic backends, set `CircuitBreaker` if you want APIM to take a failing member out of rotation. + +### Load balance OpenAI deployments + +The following example balances an OpenAI-compatible API across two Foundry accounts. Each backend uses the managed identity of APIM, and each has a circuit breaker for HTTP 429 responses. + + + + +```typescript title="apphost.mts" +const primaryFoundry = await builder.addFoundry("foundry-primary"); +const primaryChat = await primaryFoundry.addDeployment("chat-primary", { + name: "gpt-5-mini", + version: "2025-08-07", + format: "OpenAI", +}); + +const secondaryFoundry = await builder.addFoundry("foundry-secondary"); +const secondaryChat = await secondaryFoundry.addDeployment("chat-secondary", { + name: "gpt-5-mini", + version: "2025-08-07", + format: "OpenAI", +}); + +const primaryBackend = await apim.addFoundryBackend("primary-backend", primaryChat); +const secondaryBackend = await apim.addFoundryBackend("secondary-backend", secondaryChat); + +const pool = await apim.addBackendPool("openai-pool"); +await pool.addBackendPoolMember(primaryBackend, { priority: 1, weight: 3 }); +await pool.addBackendPoolMember(secondaryBackend, { priority: 1, weight: 1 }); + +const openAIApi = await apim.addOpenAIApi("openai-api", "openai", { + subscriptionRequired: false, +}); +await openAIApi.withApiBackendPool(pool); +``` + + + + +```csharp title="AppHost.cs" +using Aspire.Hosting.Foundry; + +var primaryFoundry = builder.AddFoundry("foundry-primary"); +var primaryChat = primaryFoundry.AddDeployment( + "chat-primary", + FoundryModel.OpenAI.Gpt5Mini); + +var secondaryFoundry = builder.AddFoundry("foundry-secondary"); +var secondaryChat = secondaryFoundry.AddDeployment( + "chat-secondary", + FoundryModel.OpenAI.Gpt5Mini); + +var primaryBackend = apim.AddFoundryBackend("primary-backend", primaryChat); +var secondaryBackend = apim.AddFoundryBackend("secondary-backend", secondaryChat); + +var pool = apim.AddBackendPool("openai-pool") + .WithBackend(primaryBackend, priority: 1, weight: 3) + .WithBackend(secondaryBackend, priority: 1, weight: 1); + +apim.AddOpenAIApi( + "openai-api", + path: "openai", + subscriptionRequired: false) + .WithBackend(pool); +``` + + + + +A request through the gateway maps to the selected deployment. For example, a request such as: + +```text +POST https://.azure-api.net/openai/chat/completions?api-version= +``` + +is forwarded to the selected account at: + +```text +POST https:///openai/deployments//chat/completions?api-version= +``` + +### Load balance Blob Storage accounts + +The same pool model works with other services. This example shares traffic between two Blob Storage accounts and adds the `x-ms-version` header that the Blob Storage REST API expects: + + + + +```typescript title="apphost.mts" +const primaryStorage = await builder.addAzureStorage("storage-primary"); +const primaryBlobs = await primaryStorage.addBlobs("blobs-primary"); +const secondaryStorage = await builder.addAzureStorage("storage-secondary"); +const secondaryBlobs = await secondaryStorage.addBlobs("blobs-secondary"); + +const primaryBackend = await apim.addBlobStorageBackend("blob-primary", primaryBlobs); +const secondaryBackend = await apim.addBlobStorageBackend("blob-secondary", secondaryBlobs); + +const pool = await apim.addBackendPool("blob-pool"); +await pool.addBackendPoolMember(primaryBackend, { weight: 3 }); +await pool.addBackendPoolMember(secondaryBackend); + +const blobApi = await apim.addApiWithoutTarget("blob-api", "blobs"); +await blobApi.withApiBackendPool(pool); +await blobApi.withInboundPolicy( + '2023-11-03'); +``` + + + + +```csharp title="AppHost.cs" +var primaryBlobs = builder.AddAzureStorage("storage-primary").AddBlobs("blobs-primary"); +var secondaryBlobs = builder.AddAzureStorage("storage-secondary").AddBlobs("blobs-secondary"); + +var primaryBackend = apim.AddBlobStorageBackend("blob-primary", primaryBlobs); +var secondaryBackend = apim.AddBlobStorageBackend("blob-secondary", secondaryBlobs); + +var pool = apim.AddBackendPool("blob-pool") + .WithBackend(primaryBackend, weight: 3) + .WithBackend(secondaryBackend); + +apim.AddApi("blob-api", path: "blobs") + .WithBackend(pool) + .WithInboundPolicy( + """2023-11-03"""); +``` + + + + +## See also + +- [APIs and operations](/integrations/cloud/azure/azure-api-management/azure-api-management-apis/) +- [Policies and named values](/integrations/cloud/azure/azure-api-management/azure-api-management-policies/) +- [Azure AI Foundry integration](/integrations/cloud/azure/azure-ai-foundry/azure-ai-foundry-get-started/) +- [Azure OpenAI integration](/integrations/cloud/azure/azure-openai/azure-openai-get-started/) +- [Azure Storage Blobs integration](/integrations/cloud/azure/azure-storage-blobs/azure-storage-blobs-get-started/) +- [Backends in Azure API Management](https://learn.microsoft.com/azure/api-management/backends) diff --git a/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-get-started.mdx b/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-get-started.mdx new file mode 100644 index 000000000..f14ee69ea --- /dev/null +++ b/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-get-started.mdx @@ -0,0 +1,291 @@ +--- +title: Get started with Azure API Management +description: Add the Aspire Azure API Management hosting integration, route a workload through an APIM gateway, and provision the service with aspire deploy. +--- + +import { Image } from 'astro:assets'; +import { CardGrid, LinkCard, Tabs, TabItem } from '@astrojs/starlight/components'; +import InstallPackage from '@components/InstallPackage.astro'; +import azureIcon from '@assets/icons/azure-icon.png'; + +Azure logo + +[Azure API Management](https://learn.microsoft.com/azure/api-management/) (APIM) is a managed gateway for publishing, securing, and observing APIs. The Aspire Azure API Management hosting integration models an APIM service and the resources beneath it, such as APIs, backends, policies, and products, without hand-authored Bicep. + +:::danger[Experimental] +The `Aspire.Hosting.Azure.ApiManagement` package ships as a prerelease package, and its APIs might change in future releases. The APIM types themselves don't carry a compiler diagnostic. The virtual network APIs that you combine with APIM, such as `AddAzureVirtualNetwork`, `AddPrivateEndpoint`, and `WithInternalLoadBalancer`, are experimental and report [ASPIREAZURE003](/diagnostics/aspireazure003/) in C#. +::: + +APIM is a deploy-only resource. Aspire omits it during `aspire run` and provisions it when you run `aspire deploy`, along with the APIs, backends, policies, and other resources that you declare beneath it. You don't need an execution-mode guard in your AppHost. + +## Hosting integration + +The Aspire Azure API Management hosting integration models the service as `AzureApiManagementResource`. Each part of the service is a child resource of that type: + +| Resource type | Created by | Represents | +| ------------- | ---------- | ---------- | +| `AzureApiManagementApiResource` | `AddApi`, `AddOpenAIApi` | An API published at a path on the gateway. | +| `AzureApiManagementOperationResource` | `AddOperation` | An explicitly modeled operation of an API. | +| `AzureApiManagementBackendResource` | `AddBackend`, `AddAzureOpenAIBackend`, `AddFoundryBackend`, `AddBlobStorageBackend` | A reusable backend service. | +| `AzureApiManagementBackendPoolResource` | `AddBackendPool` | A weighted or priority-based group of backends. | +| `AzureApiManagementPolicyFragmentResource` | `AddPolicyFragment` | A reusable policy fragment. | +| `AzureApiManagementNamedValueResource` | `AddNamedValue`, `AddSecretNamedValue`, `AddKeyVaultNamedValue` | A named value that policies can reference. | +| `AzureApiManagementProductResource` | `AddProduct` | A product that groups APIs. | +| `AzureApiManagementSubscriptionResource` | `AddSubscription` | A product-scoped subscription. | + +To access these types and APIs, add the [📦 Aspire.Hosting.Azure.ApiManagement](https://www.nuget.org/packages/Aspire.Hosting.Azure.ApiManagement) NuGet package to your AppHost project. + + + +### Add a service and route a workload + +Call `AddAzureApiManagement` to add the service, then call `AddApi` with a compute resource to publish that workload through the gateway: + + + + +```typescript title="apphost.mts" +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const environment = await builder.addAzureContainerAppEnvironment("env"); + +const catalog = await builder.addProject("catalog", "../Catalog/Catalog.csproj") + .withComputeEnvironment(environment) + .withExternalHttpEndpoints(); + +const apim = await builder.addAzureApiManagement("apim", { + publisherEmail: "api-owners@example.com", + publisherName: "Contoso APIs", +}); + +await apim.addApi("catalog-api", catalog, "catalog", { + subscriptionRequired: false, +}); + +await builder.build().run(); +``` + + + + +```csharp title="AppHost.cs" +var builder = DistributedApplication.CreateBuilder(args); + +var environment = builder.AddAzureContainerAppEnvironment("env"); + +var catalog = builder.AddProject("catalog") + .WithComputeEnvironment(environment) + .WithExternalHttpEndpoints(); + +var apim = builder.AddAzureApiManagement("apim", new() +{ + PublisherEmail = "api-owners@example.com", + PublisherName = "Contoso APIs", +}); + +apim.AddApi( + "catalog-api", + catalog, + path: "catalog", + subscriptionRequired: false); + +builder.Build().Run(); +``` + + + + +The preceding code: + +- Adds an Azure Container Apps environment. APIM routes to the workload's deployed endpoint, so the workload needs an Azure compute environment when you publish or deploy. +- Adds a `catalog` project that exposes external HTTP endpoints, which the APIM gateway can reach. +- Adds an APIM service with the required publisher email and name. The service uses the Developer tier unless you set another [SKU](#choose-a-pricing-tier). +- Publishes `catalog` at the `catalog` path of the gateway. + +The compute-targeted `AddApi` overload creates an API, a reusable APIM backend, and catch-all operations that forward requests to the workload's deployed endpoint. APIs require an APIM subscription key by default. The example sets `subscriptionRequired` to `false` so that you can call the gateway without a key. Keep the default for APIs that you don't intend to expose anonymously, and see [Products and subscriptions](/integrations/cloud/azure/azure-api-management/azure-api-management-products/) for how to issue keys. + +:::caution +When you add an Azure resource, Aspire implicitly calls `AddAzureProvisioning`. Your AppHost needs the Azure subscription, location, and resource group settings that Aspire uses to provision resources. For more information, see [Local provisioning: Configuration](/integrations/cloud/azure/local-provisioning/#configuration). +::: + +### Deploy the gateway + +APIM is omitted when you run `aspire run`, because a cloud-hosted APIM instance can't reach a backend that runs on localhost, and Azure compute environments don't materialize public endpoints in run mode. Your other resources continue to run locally as usual. + +To provision the service and exercise its routing, deploy the AppHost: + +```bash title="Aspire CLI — Deploy the AppHost" +aspire deploy +``` + +To generate the Bicep without deploying, use `aspire publish`. For more information, see [Deploy to Azure](/deployment/azure/) and the [`aspire deploy` command](/reference/cli/commands/aspire-deploy/). + +After deployment, call the gateway at `https://.azure-api.net/catalog`, where `` is the generated APIM service name. The `gatewayUrl` deployment output contains the gateway base URL. + +### Choose a pricing tier + +Set `Sku` and `Capacity` on the options object that you pass to `AddAzureApiManagement`. Aspire validates the combination when you add the resource and throws an `ArgumentException` for an invalid capacity. + + + + +```typescript title="apphost.mts" +import { AzureApiManagementSku, createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const apim = await builder.addAzureApiManagement("apim", { + publisherEmail: "api-owners@example.com", + publisherName: "Contoso APIs", + sku: AzureApiManagementSku.StandardV2, + capacity: 2, +}); + +await builder.build().run(); +``` + + + + +```csharp title="AppHost.cs" +using Aspire.Hosting.Azure; + +var builder = DistributedApplication.CreateBuilder(args); + +var apim = builder.AddAzureApiManagement("apim", new() +{ + PublisherEmail = "api-owners@example.com", + PublisherName = "Contoso APIs", + Sku = AzureApiManagementSku.StandardV2, + Capacity = 2, +}); + +builder.Build().Run(); +``` + + + + +The options have these properties: + +| Option | Required | Default | Description | +| ------ | -------- | ------- | ----------- | +| `PublisherEmail` | Yes | None | The publisher email address that APIM shows. Maximum 100 characters. | +| `PublisherName` | Yes | None | The publisher name that APIM shows. Maximum 100 characters. | +| `Sku` | No | `Developer` | The pricing tier. | +| `Capacity` | No | `0` for Consumption, otherwise `1` | The number of capacity units. | + +The integration supports the following tiers. Capacity is the range of scale units that Aspire accepts for the tier. + +| `Sku` value | Capacity | Notes | +| ----------- | -------- | ----- | +| `Consumption` | 0 | Serverless. Doesn't support backend circuit breakers, Azure OpenAI and Foundry backends, or private endpoints. Supports one custom domain for the gateway endpoint only. | +| `Developer` | 1 | Non-production. Supports classic virtual network injection and multiple gateway custom domains. | +| `Basic` | 1 to 2 | Classic Basic tier. | +| `BasicV2` | 1 to 10 | Doesn't support private endpoints. | +| `Standard` | 1 to 4 | Classic Standard tier. | +| `StandardV2` | 1 to 10 | Supports outbound virtual network integration. | +| `Premium` | 1 to 12 | Supports classic virtual network injection and multiple gateway custom domains. | +| `PremiumV2` | 1 to 30 | Supports outbound virtual network integration, virtual network injection, and multiple gateway custom domains. | + +Tier capabilities differ substantially for networking, scale, availability zones, and multi-region deployment. For the full comparison, see [API Management tier features](https://learn.microsoft.com/azure/api-management/api-management-features). For networking requirements by tier, see [Virtual networking](/integrations/cloud/azure/azure-api-management/azure-api-management-networking/). + +### Service outputs + +The deployed service exposes the following outputs. They're available as `BicepOutputReference` properties on the C# `AzureApiManagementResource` and aren't projected to TypeScript AppHosts. + +| Property | Value | +| -------- | ----- | +| `NameOutputReference` | The physical APIM service name (`name` output). | +| `Id` | The Azure resource ID of the service (`id` output). | +| `GatewayUrl` | The default gateway URL (`gatewayUrl` output). | +| `PrincipalId` | The principal ID of the user-assigned managed identity that APIM uses. | + +The `Identity` property returns the user-assigned identity resource. For more information about how APIM uses it, see [Service configuration](/integrations/cloud/azure/azure-api-management/azure-api-management-service/#managed-identity). + +### Customize provisioning + +Use `ConfigureInfrastructure` to customize the generated resources through the types in `Azure.Provisioning.ApiManagement`, such as `ApiManagementService`, `ApiManagementApi`, and `ApiManagementBackend`. For more information, see [Customize Azure resources](/integrations/cloud/azure/customize-resources/). + +:::note +`ConfigureInfrastructure` works with .NET `Azure.Provisioning` types, so it's available only to C# AppHosts. TypeScript AppHosts use the APIs that the integration exports. +::: + +### Security considerations + +The integration creates a reverse proxy with configurable public ingress, and it accepts arbitrary APIM policy XML. Consider the following defaults and requirements: + +- APIs require an APIM subscription key unless you set `subscriptionRequired` to `false`. +- Backend TLS certificate-chain and hostname validation are enabled by default. Disable them only for a specific backend that needs it. +- Specialized Azure backends use APIM's user-assigned managed identity and least-privilege service roles. For more information, see [Backends and backend pools](/integrations/cloud/azure/azure-api-management/azure-api-management-backends/). +- Creating role assignments requires `Microsoft.Authorization/roleAssignments/write` on each target scope. The principal that runs `aspire deploy` needs that permission, for example through the User Access Administrator or Owner role. +- Key Vault-backed named values and certificates use the same user-assigned identity, so Aspire grants it only the Key Vault roles that it needs. + +### Current limitations + +The integration doesn't currently model the following capabilities: + +- Automatic disabling of public network access. +- Backend client certificates and credential collections. +- Consumer-safe references to subscription keys. +- Structured gateway URL references. +- Multi-region, availability zone, and autoscale configuration. +- API versions and revisions. + +## Next steps + + + + + + + + + + + +## See also + +- [Azure API Management documentation](https://learn.microsoft.com/azure/api-management/) +- [Azure integrations overview](/integrations/cloud/azure/overview/) +- [Customize Azure resources](/integrations/cloud/azure/customize-resources/) +- [Azure role assignments](/integrations/cloud/azure/role-assignments/) +- [📦 Aspire.Hosting.Azure.ApiManagement](https://www.nuget.org/packages/Aspire.Hosting.Azure.ApiManagement) diff --git a/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-networking.mdx b/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-networking.mdx new file mode 100644 index 000000000..ff3212458 --- /dev/null +++ b/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-networking.mdx @@ -0,0 +1,331 @@ +--- +title: Azure API Management virtual networking +description: Connect Azure API Management to a virtual network with classic injection, v2 integration, Premium v2 injection, private endpoints, and private backends. +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +Azure API Management (APIM) can reach private backends and accept private traffic when you connect it to an Azure virtual network. The Aspire Azure API Management hosting integration supports the APIM networking models through the [Azure Virtual Network integration](/integrations/cloud/azure/azure-virtual-network/). This page assumes the `apim` resource from [Get started with Azure API Management](/integrations/cloud/azure/azure-api-management/azure-api-management-get-started/#add-a-service-and-route-a-workload). + +:::danger[Experimental] +The Azure Virtual Network integration is experimental. In C#, the virtual network, subnet, private endpoint, and internal load balancer APIs report [ASPIREAZURE003](/diagnostics/aspireazure003/). Suppress the diagnostic to use them, and expect the APIs to change. +::: + +## Choose a networking model + +Each model applies to specific pricing tiers. Pick the one that matches the traffic that you need to secure. + +| Model | Method | Tiers | Effect | +| ----- | ------ | ----- | ------ | +| Classic virtual network injection | `WithClassicVirtualNetwork` | Developer, Premium | Places the service in an undelegated subnet, with an external or internal gateway. | +| Outbound virtual network integration | `WithVirtualNetworkIntegration` | Standard v2, Premium v2 | Keeps the gateway public and lets APIM reach backends in the virtual network. | +| Premium v2 virtual network injection | `WithVirtualNetworkInjection` | Premium v2 | Provides private inbound and outbound access. | +| Inbound private endpoint | `AddPrivateEndpoint` | All tiers except Consumption and Basic v2 | Adds a private endpoint for the gateway without changing public network access. | + +The following rules apply to every model: + +- Each APIM service needs a dedicated subnet. A subnet that one APIM service uses can't be used by another. +- A service has one virtual network configuration. Repeating the same call is allowed, but a different configuration throws an `InvalidOperationException`. +- An existing service can't change its virtual network configuration. Private endpoints work with existing services because they don't modify the service. +- Aspire models one delegation for each subnet. A subnet that has a conflicting delegation causes an `InvalidOperationException`. + +## Inject a classic service into a subnet + +Call `WithClassicVirtualNetwork` on a Developer or Premium service. The subnet must not be delegated to another Azure service, and Azure requires a network security group (NSG) on it. + + + + +```typescript title="apphost.mts" +import { AzureApiManagementVirtualNetworkMode, SecurityRuleProtocol } from './.aspire/modules/aspire.mjs'; + +const vnet = await builder.addAzureVirtualNetwork("vnet"); +const apimSubnet = await vnet.addSubnet("apim-subnet", "10.0.0.0/24"); +await apimSubnet.allowInbound({ port: "3443", from: "ApiManagement", protocol: SecurityRuleProtocol.Tcp }); +await apimSubnet.allowInbound({ port: "6390", from: "AzureLoadBalancer", protocol: SecurityRuleProtocol.Tcp }); +await apimSubnet.allowInbound({ port: "443", from: "Internet", protocol: SecurityRuleProtocol.Tcp }); + +await apim.withClassicVirtualNetwork(apimSubnet, AzureApiManagementVirtualNetworkMode.External); +``` + + + + +```csharp title="AppHost.cs" +#pragma warning disable ASPIREAZURE003 + +using Aspire.Hosting.Azure; +using Azure.Provisioning.Network; + +var vnet = builder.AddAzureVirtualNetwork("vnet"); +var apimSubnet = vnet.AddSubnet("apim-subnet", "10.0.0.0/24") + .AllowInbound(port: "3443", from: "ApiManagement", protocol: SecurityRuleProtocol.Tcp) + .AllowInbound(port: "6390", from: AzureServiceTags.AzureLoadBalancer, protocol: SecurityRuleProtocol.Tcp) + .AllowInbound(port: "443", from: AzureServiceTags.Internet, protocol: SecurityRuleProtocol.Tcp); + +apim.WithClassicVirtualNetwork(apimSubnet, AzureApiManagementVirtualNetworkMode.External); +``` + + + + +The `mode` argument selects how the gateway is exposed: + +| Mode | Behavior | +| ---- | -------- | +| `External` | Exposes APIM through a public load balancer and lets it reach private backends. | +| `Internal` | Exposes APIM only through an internal load balancer. You manage DNS for APIM's private virtual IP address. | + +The NSG rules in the example allow TCP 3443 from the `ApiManagement` service tag for the management endpoint, TCP 6390 from `AzureLoadBalancer` for health probes, and TCP 443 from `Internet` while the gateway remains public. For more information about the requirements, see [Azure API Management virtual network concepts](https://learn.microsoft.com/azure/api-management/virtual-network-concepts). + +## Integrate a v2 service for outbound access + +Call `WithVirtualNetworkIntegration` on a Standard v2 or Premium v2 service. The gateway stays publicly accessible, and APIM can reach resources in the virtual network. + + + + +```typescript title="apphost.mts" +import { AzureApiManagementSku, createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const vnet = await builder.addAzureVirtualNetwork("vnet"); +const subnet = await vnet.addSubnet("apim-subnet", "10.0.0.0/24"); + +const apim = await builder.addAzureApiManagement("apim", { + publisherEmail: "api-owners@example.com", + publisherName: "Contoso APIs", + sku: AzureApiManagementSku.StandardV2, +}); +await apim.withVirtualNetworkIntegration(subnet); + +await builder.build().run(); +``` + + + + +```csharp title="AppHost.cs" +#pragma warning disable ASPIREAZURE003 + +using Aspire.Hosting.Azure; + +var builder = DistributedApplication.CreateBuilder(args); + +var vnet = builder.AddAzureVirtualNetwork("vnet"); +var subnet = vnet.AddSubnet("apim-subnet", "10.0.0.0/24"); + +var apim = builder.AddAzureApiManagement("apim", new() +{ + PublisherEmail = "api-owners@example.com", + PublisherName = "Contoso APIs", + Sku = AzureApiManagementSku.StandardV2, +}); +apim.WithVirtualNetworkIntegration(subnet); + +builder.Build().Run(); +``` + + + + +Aspire delegates the subnet to `Microsoft.Web/serverFarms`. The subnet must be a `/27` or larger. + +## Inject a Premium v2 service + +Call `WithVirtualNetworkInjection` on a Premium v2 service for private inbound and outbound access. Aspire delegates the subnet to `Microsoft.Web/hostingEnvironments`, and the subnet must be a `/27` or larger. + + + + +```typescript title="apphost.mts" +import { AzureApiManagementSku, createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const vnet = await builder.addAzureVirtualNetwork("vnet"); +const subnet = await vnet.addSubnet("apim-subnet", "10.0.0.0/24"); + +const apim = await builder.addAzureApiManagement("apim", { + publisherEmail: "api-owners@example.com", + publisherName: "Contoso APIs", + sku: AzureApiManagementSku.PremiumV2, +}); +await apim.withVirtualNetworkInjection(subnet); + +await builder.build().run(); +``` + + + + +```csharp title="AppHost.cs" +#pragma warning disable ASPIREAZURE003 + +using Aspire.Hosting.Azure; + +var builder = DistributedApplication.CreateBuilder(args); + +var vnet = builder.AddAzureVirtualNetwork("vnet"); +var subnet = vnet.AddSubnet("apim-subnet", "10.0.0.0/24"); + +var apim = builder.AddAzureApiManagement("apim", new() +{ + PublisherEmail = "api-owners@example.com", + PublisherName = "Contoso APIs", + Sku = AzureApiManagementSku.PremiumV2, +}); +apim.WithVirtualNetworkInjection(subnet); + +builder.Build().Run(); +``` + + + + +You must select Premium v2 injection when the service is created. A service can't later change from injection to integration. Configure private DNS for the injected gateway host name as described in [Inject a Premium v2 API Management instance into a virtual network](https://learn.microsoft.com/azure/api-management/inject-vnet-v2). + +### NSG rule for Azure Key Vault + +Both v2 helpers require the subnet to allow outbound TCP 443 to the `AzureKeyVault` service tag. Aspire handles this for you: + +- When the subnet has no NSG, Aspire creates one. +- Aspire adds an outbound allow rule named `allow-apim-key-vault`. On an existing NSG, it uses a free priority that's ahead of any outbound TCP 443 deny rule. It conservatively treats service tags and address expressions as potentially overlapping. +- If no earlier priority is available, configuration fails instead of adding a rule that a deny rule would shadow. +- Later changes to the modeled NSG must keep that allow rule and its ordering, or the deployment fails. + +## Route to a private Container Apps environment + +An APIM service that's injected into a virtual network can reach a Container Apps environment that has no public endpoint. Place the environment and APIM in separate subnets of the same virtual network. Configure the environment with an internal load balancer and APIM with classic external injection. + + + + +```typescript title="apphost.mts" +import { AzureApiManagementVirtualNetworkMode, SecurityRuleProtocol, createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const vnet = await builder.addAzureVirtualNetwork("vnet"); +const containerAppsSubnet = await vnet.addSubnet("container-apps-subnet", "10.0.0.0/23"); +const apimSubnet = await vnet.addSubnet("apim-subnet", "10.0.2.0/24"); +await apimSubnet.allowInbound({ port: "3443", from: "ApiManagement", protocol: SecurityRuleProtocol.Tcp }); +await apimSubnet.allowInbound({ port: "6390", from: "AzureLoadBalancer", protocol: SecurityRuleProtocol.Tcp }); +await apimSubnet.allowInbound({ port: "443", from: "Internet", protocol: SecurityRuleProtocol.Tcp }); + +const environment = await builder.addAzureContainerAppEnvironment("env"); +await environment.withDelegatedSubnet(containerAppsSubnet); +await environment.withInternalLoadBalancer(vnet); + +const catalog = await builder.addProject("catalog", "../Catalog/Catalog.csproj") + .withComputeEnvironment(environment) + .withExternalHttpEndpoints(); + +const apim = await builder.addAzureApiManagement("apim", { + publisherEmail: "api-owners@example.com", + publisherName: "Contoso APIs", +}); +await apim.withClassicVirtualNetwork(apimSubnet, AzureApiManagementVirtualNetworkMode.External); +await apim.addApi("catalog-api", catalog, "catalog"); + +await builder.build().run(); +``` + + + + +```csharp title="AppHost.cs" +#pragma warning disable ASPIREAZURE003 + +using Aspire.Hosting.Azure; +using Azure.Provisioning.Network; + +var builder = DistributedApplication.CreateBuilder(args); + +var vnet = builder.AddAzureVirtualNetwork("vnet"); +var containerAppsSubnet = vnet.AddSubnet("container-apps-subnet", "10.0.0.0/23"); +var apimSubnet = vnet.AddSubnet("apim-subnet", "10.0.2.0/24") + .AllowInbound(port: "3443", from: "ApiManagement", protocol: SecurityRuleProtocol.Tcp) + .AllowInbound(port: "6390", from: AzureServiceTags.AzureLoadBalancer, protocol: SecurityRuleProtocol.Tcp) + .AllowInbound(port: "443", from: AzureServiceTags.Internet, protocol: SecurityRuleProtocol.Tcp); + +var environment = builder.AddAzureContainerAppEnvironment("env") + .WithDelegatedSubnet(containerAppsSubnet) + .WithInternalLoadBalancer(vnet); + +var catalog = builder.AddProject("catalog") + .WithComputeEnvironment(environment) + .WithExternalHttpEndpoints(); + +var apim = builder.AddAzureApiManagement("apim", new() +{ + PublisherEmail = "api-owners@example.com", + PublisherName = "Contoso APIs", +}); +apim.WithClassicVirtualNetwork(apimSubnet, AzureApiManagementVirtualNetworkMode.External); + +apim.AddApi("catalog-api", catalog, path: "catalog"); + +builder.Build().Run(); +``` + + + + +`WithInternalLoadBalancer` gives the Container Apps environment a private virtual IP address and creates a private DNS zone, a wildcard A record, and a virtual network link for the environment's default domain. Although `WithExternalHttpEndpoints` enables ingress from outside the environment, the environment itself has no public endpoint. APIM reaches the app over the virtual network and acts as the public gateway. + +Keep the following in mind: + +- The workload exposes an internal endpoint of the environment, so the APIM service must have virtual network connectivity to the same virtual network as the environment. Otherwise, Aspire throws an `InvalidOperationException` when you publish. +- You can't import the OpenAPI document of such a workload with `WithOpenApiEndpoint`, because the APIM control plane can't reach the internal environment. Use `WithOpenApiDocument` instead. For more information, see [Import OpenAPI](/integrations/cloud/azure/azure-api-management/azure-api-management-openapi/). +- Container Apps Express environments don't support an internal load balancer. + +For more information about delegating a subnet to a compute environment, see [Delegate subnets to compute environments](/integrations/cloud/azure/azure-virtual-network/#delegate-subnets-to-compute-environments). + +## Add a private endpoint + +Call `AddPrivateEndpoint` on a subnet and pass the APIM resource to create an inbound private endpoint for the gateway. + + + + +```typescript title="apphost.mts" +const vnet = await builder.addAzureVirtualNetwork("vnet"); +const privateEndpointSubnet = await vnet.addSubnet("private-endpoint-subnet", "10.0.0.0/24"); + +await privateEndpointSubnet.addPrivateEndpoint(apim); +``` + + + + +```csharp title="AppHost.cs" +#pragma warning disable ASPIREAZURE003 + +var vnet = builder.AddAzureVirtualNetwork("vnet"); +var privateEndpointSubnet = vnet.AddSubnet("private-endpoint-subnet", "10.0.0.0/24"); + +privateEndpointSubnet.AddPrivateEndpoint(apim); +``` + + + + +Aspire provisions the private endpoint for the `Gateway` group and the `privatelink.azure-api.net` private DNS zone. It works for new and existing APIM services. + +:::caution +Adding a private endpoint doesn't make the gateway private-only. New services keep their default public access, and existing services keep their configured access settings. This differs from other Azure resources, where Aspire disables public network access when you add a private endpoint. To restrict gateway access to private endpoints, [disable public network access](https://learn.microsoft.com/azure/api-management/private-endpoint#optionally-disable-public-network-access) separately after you configure and approve the private endpoint. +::: + +Private endpoints can't be combined with classic injection or Premium v2 injection, and Aspire throws an `InvalidOperationException` when you publish that combination. The Consumption and Basic v2 tiers don't support private endpoints. For more information about private endpoints in Aspire, see [Add private endpoints](/integrations/cloud/azure/azure-virtual-network/#add-private-endpoints). + +## See also + +- [Azure Virtual Network integration](/integrations/cloud/azure/azure-virtual-network/) +- [Get started with Azure API Management](/integrations/cloud/azure/azure-api-management/azure-api-management-get-started/) +- [Configure Azure Container Apps environments](/integrations/cloud/azure/configure-container-apps/) +- [Azure security best practices](/deployment/azure/azure-security-best-practices/) +- [Azure API Management virtual network concepts](https://learn.microsoft.com/azure/api-management/virtual-network-concepts) +- [Integrate a v2 API Management instance with a virtual network](https://learn.microsoft.com/azure/api-management/integrate-vnet-outbound) +- [Connect privately to API Management with a private endpoint](https://learn.microsoft.com/azure/api-management/private-endpoint) diff --git a/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-openapi.mdx b/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-openapi.mdx new file mode 100644 index 000000000..da99d7f19 --- /dev/null +++ b/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-openapi.mdx @@ -0,0 +1,140 @@ +--- +title: Import OpenAPI into Azure API Management +description: Import operations into an Azure API Management API from an OpenAPI file or from a deployed Aspire workload's OpenAPI endpoint in your AppHost. +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +Azure API Management (APIM) can import operations, parameters, schemas, and descriptions from an OpenAPI document instead of using catch-all proxy operations. The Aspire Azure API Management hosting integration supports two sources for the document: a file that you keep with your AppHost, and an endpoint that a deployed Aspire workload exposes. An API can use only one source. + +This page assumes the `apim` resource and the `catalog` workload from [Get started with Azure API Management](/integrations/cloud/azure/azure-api-management/azure-api-management-get-started/#add-a-service-and-route-a-workload). + +## Import from a file + +Call `WithOpenApiDocument` to import a JSON or YAML document. The path is relative to the AppHost directory unless it's absolute. + + + + +```typescript title="apphost.mts" +const catalogApi = await apim.addApi("catalog-api", catalog, "catalog"); +await catalogApi.withOpenApiDocument("../Catalog/openapi.json"); +``` + + + + +```csharp title="AppHost.cs" +apim.AddApi("catalog-api", catalog, path: "catalog") + .WithOpenApiDocument("../Catalog/openapi.json"); +``` + + + + +Aspire infers the document format from the file extension: + +| Extension | Format | `AzureApiManagementOpenApiFormat` value | +| --------- | ------ | --------------------------------------- | +| `.json` | OpenAPI 3.x JSON | `OpenApiJson` | +| `.yaml`, `.yml` | OpenAPI YAML | `OpenApi` | + +For any other extension, and for Swagger 2.0 documents, pass the format explicitly. Swagger 2.0 documents use `SwaggerJson`: + + + + +```typescript title="apphost.mts" +import { AzureApiManagementOpenApiFormat } from './.aspire/modules/aspire.mjs'; + +const legacyApi = await apim.addApi("legacy-api", catalog, "legacy"); +await legacyApi.withOpenApiDocument("../Catalog/swagger.json", { + format: AzureApiManagementOpenApiFormat.SwaggerJson, +}); +``` + + + + +```csharp title="AppHost.cs" +using Aspire.Hosting.Azure; + +apim.AddApi("legacy-api", catalog, path: "legacy") + .WithOpenApiDocument( + "../Catalog/swagger.json", + AzureApiManagementOpenApiFormat.SwaggerJson); +``` + + + + +Aspire reads the file when it generates infrastructure for publish or deploy, not during `aspire run`. The file doesn't need to exist for local development. When you publish or deploy, the file must exist and contain non-whitespace content, or Aspire throws an `InvalidOperationException`. + +`WithOpenApiDocument` works for every API, including APIs that use a backend that you configure. Use a file when the workload's endpoint isn't reachable from the APIM control plane, such as a workload in an internal Container Apps environment, or when you generate the document before deployment. + +## Import from a deployed workload + +Call `WithOpenApiEndpoint` to have APIM retrieve the OpenAPI document from the workload after it's deployed. + + + + +```typescript title="apphost.mts" +const catalogApi = await apim.addApi("catalog-api", catalog, "catalog", { + subscriptionRequired: false, +}); +await catalogApi.withOpenApiEndpoint({ documentPath: "/openapi/v1.json" }); +``` + + + + +```csharp title="AppHost.cs" +apim.AddApi("catalog-api", catalog, path: "catalog", subscriptionRequired: false) + .WithOpenApiEndpoint("/openapi/v1.json"); +``` + + + + +The method has the following parameters: + +| Parameter | Default | Description | +| --------- | ------- | ----------- | +| `documentPath` | `/openapi/v1.json` | The absolute path of the OpenAPI endpoint. It must start with `/`. | +| `endpointName` | An external HTTP or HTTPS endpoint | The name of the workload endpoint to use. | +| `format` | Inferred from the path extension | The document format. | + +ASP.NET Core serves its generated document at `/openapi/v1.json` when you call `MapOpenApi`, which is why that path is the default. + +Aspire resolves the workload's deployed external endpoint, appends the document path, and passes the resulting link to APIM. APIM retrieves the document through its control plane during deployment, so the endpoint must be publicly reachable at that time. Aspire orders the APIM deployment after the workload's deployment so that the document is available. + +`WithOpenApiEndpoint` requires a compute-targeted API. Aspire throws an `InvalidOperationException` in these cases: + +- The API has no target compute resource. +- The workload has no suitable external HTTP or HTTPS endpoint. +- The workload deploys to an internal Container Apps environment. +- The workload's compute publisher isn't an Azure provisioning resource that Aspire can order before the APIM deployment. + +In each case, use `WithOpenApiDocument` instead. + +## How imports change the API + +When an OpenAPI source is configured: + +- The imported operations replace the generated catch-all proxy operations. +- Operations that you add with `AddOperation` are still provisioned in addition to the imported operations. +- Explicit operations can use the `/` and `/{*path}` routes that are otherwise reserved for generated proxy operations. +- For an endpoint import, the backend URL comes from the `servers` entry that APIM resolves from the document, so a non-root server path is preserved. If the document has no server URL, Aspire falls back to the workload's endpoint. + +An API can have only one OpenAPI source. Repeating the same configuration is allowed. Configuring a different source, path, endpoint name, or format throws an `InvalidOperationException` instead of replacing the existing source. + +:::caution +An OpenAPI import defines the operations that APIM accepts. Requests that don't match an imported operation return an error from the gateway, so keep the document in sync with the workload. +::: + +## See also + +- [APIs and operations](/integrations/cloud/azure/azure-api-management/azure-api-management-apis/) +- [Virtual networking](/integrations/cloud/azure/azure-api-management/azure-api-management-networking/) +- [Import an OpenAPI specification in Azure API Management](https://learn.microsoft.com/azure/api-management/import-api-from-oas) diff --git a/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-policies.mdx b/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-policies.mdx new file mode 100644 index 000000000..d8639d6e8 --- /dev/null +++ b/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-policies.mdx @@ -0,0 +1,274 @@ +--- +title: API Management policies and named values +description: Add inbound policies, complete policy documents, reusable fragments, and named values to Azure API Management in your Aspire AppHost. +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +Policies change how Azure API Management (APIM) processes requests and responses, for example to limit rates, set headers, or rewrite URLs. The Aspire Azure API Management hosting integration lets you add policy statements at the service, API, and operation scope, share statements through policy fragments, and reference configuration values through named values. + +This page assumes the `apim` and `catalog` resources from [Get started with Azure API Management](/integrations/cloud/azure/azure-api-management/azure-api-management-get-started/#add-a-service-and-route-a-workload). + +:::caution +Policies are arbitrary XML that APIM runs in the gateway request pipeline. Treat policy XML like code, and review it as you would any change that affects every request. +::: + +## Add inbound policy statements + +Call `WithInboundPolicy` to append one or more policy statements to the inbound section at a scope. The method exists on the service, on APIs, and on operations. + + + + +```typescript title="apphost.mts" +await apim.withInboundPolicy( + 'Aspire'); + +const catalogApi = await apim.addApi("catalog-api", catalog, "catalog"); +await catalogApi.withInboundPolicy(''); + +const getProduct = await catalogApi.addOperation("get-product", "GET", "/products/{id}"); +await getProduct.withInboundPolicy( + 'get-product'); +``` + + + + +```csharp title="AppHost.cs" +apim.WithInboundPolicy(""" + + Aspire + + """); + +var catalogApi = apim.AddApi("catalog-api", catalog, path: "catalog") + .WithInboundPolicy(""" + + """); + +catalogApi.AddOperation("get-product", "GET", "/products/{id}") + .WithInboundPolicy(""" + + get-product + + """); +``` + + + + +Each call appends statements in call order. The value must be one or more well-formed policy elements without a `policies` envelope. Aspire throws an `ArgumentException` if the XML is malformed or contains a complete `policies` document. + +Aspire composes the policy document for each scope: + +| Scope | Generated policy | +| ----- | ---------------- | +| Service | An `inbound` section with your statements, a `forward-request` backend section, and empty `outbound` and `on-error` sections. The service policy is the root of the hierarchy, so it has no `base` element. | +| API | An `inbound` section with `base`, the `authentication-managed-identity` statement when the backend uses managed identity, `set-backend-service`, and then your statements. The other sections contain `base`. | +| Operation | An `inbound` section with `base` and your statements. The other sections contain `base`. | + +## Replace the complete policy + +Call `WithPolicy` when you need full control of a policy document. The value must be a complete document with a `policies` root element. APIM replaces the whole policy at that scope. + + + + +```typescript title="apphost.mts" +import { refExpr } from './.aspire/modules/aspire.mjs'; + +const backend = await apim.addBackend("orders-backend", refExpr`https://orders.example.com`); + +const ordersApi = await apim.addApiWithoutTarget("orders-api", "orders"); +await ordersApi.withApiBackend(backend); +await ordersApi.withPolicy(` + + + + + + + + +`); +``` + + + + +```csharp title="AppHost.cs" +var backend = apim.AddBackend( + "orders-backend", + ReferenceExpression.Create($"https://orders.example.com")); + +apim.AddApi("orders-api", path: "orders") + .WithBackend(backend) + .WithPolicy(""" + + + + + + + + + + + """); +``` + + + + +Keep the following in mind when you replace a policy: + +- Replacing an API policy also replaces the backend routing that Aspire generates. Your document must set the backend if the API should keep forwarding requests. The backend ID of a standalone backend is its physical name, which defaults to the resource name. +- For an API that targets a compute resource, the generated backend ID is the API resource name with hyphens replaced by underscores, followed by `Backend`, such as `catalog_apiBackend`. +- `WithPolicy` and `WithInboundPolicy` can't be combined at the same scope. Aspire throws an `InvalidOperationException` instead of silently discarding one of them. +- The XML must be well-formed and must have a `policies` root element. + +## Share statements with policy fragments + +A policy fragment is a reusable set of policy statements. Define it once with `AddPolicyFragment`, then include it at the service, API, or operation scope with `WithInboundPolicyFragment`. + + + + +```typescript title="apphost.mts" +const standardHeaders = await apim.addPolicyFragment( + "standard-headers", + ` + Aspire +`, + { description: "Headers shared by every API." }); + +await apim.withInboundPolicyFragment(standardHeaders); + +const catalogApi = await apim.addApi("catalog-api", catalog, "catalog"); +await catalogApi.withInboundPolicyFragment(standardHeaders); +``` + + + + +```csharp title="AppHost.cs" +var standardHeaders = apim.AddPolicyFragment( + "standard-headers", + """ + + Aspire + + """, + description: "Headers shared by every API."); + +apim.WithInboundPolicyFragment(standardHeaders); + +var catalogApi = apim.AddApi("catalog-api", catalog, path: "catalog"); +catalogApi.WithInboundPolicyFragment(standardHeaders); +``` + + + + +Aspire wraps the statements in a `fragment` element and adds an `include-fragment` statement to the inbound policy at the scope where you include it. APIM deploys the policies after the fragments that they reference. + +The `AddPolicyFragment` method has the following parameters: + +| Parameter | Required | Description | +| --------- | -------- | ----------- | +| `name` | Yes | The Aspire resource name. It's also the default fragment identifier. | +| `policyXml` | Yes | One or more well-formed policy elements without a `fragment` envelope. | +| `description` | No | A description with at most 1,000 characters. | +| `fragmentName` | No | The physical fragment identifier in APIM. Defaults to the resource name. | + +The fragment identifier has at most 80 characters. It can contain letters, digits, underscores, and hyphens, but it can't start or end with a hyphen. Each identifier is unique within the service, and a policy can include only fragments from its own service. + +## Add named values + +Named values are configuration entries that policies reference with the `{{name}}` syntax. Aspire supports three kinds: + +- A literal value, for non-secret configuration. +- A secret parameter, for a value that you supply through [external parameters](/fundamentals/external-parameters/). +- A Key Vault secret, which APIM reads from Azure Key Vault at run time. + + + + +```typescript title="apphost.mts" +await apim.addNamedValue("gateway-name", "Aspire"); + +const apiKey = await builder.addParameter("upstream-api-key", { secret: true }); +await apim.addSecretNamedValue("upstream-api-key-value", apiKey, { + displayName: "UpstreamApiKey", +}); + +const vault = await builder.addAzureKeyVault("vault"); +await apim.addKeyVaultNamedValue("shared-secret", vault, "shared-secret", { + displayName: "SharedSecret", +}); + +await apim.withInboundPolicy( + '{{gateway-name}}'); +``` + + + + +```csharp title="AppHost.cs" +apim.AddNamedValue("gateway-name", "Aspire"); + +var apiKey = builder.AddParameter("upstream-api-key", secret: true); +apim.AddSecretNamedValue( + "upstream-api-key-value", + apiKey, + displayName: "UpstreamApiKey"); + +var vault = builder.AddAzureKeyVault("vault"); +apim.AddKeyVaultNamedValue( + "shared-secret", + vault.GetSecret("shared-secret"), + displayName: "SharedSecret"); + +apim.WithInboundPolicy(""" + + {{gateway-name}} + + """); +``` + + + + +In TypeScript, `addKeyVaultNamedValue` takes the Key Vault resource and the secret name as separate arguments. In C#, you pass the Key Vault secret reference that `GetSecret` returns. + +The named value methods share these parameters: + +| Parameter | Description | +| --------- | ----------- | +| `name` | The Aspire resource name. It's also the default physical named-value name. | +| `value` | A string, a secret parameter, or a Key Vault secret. | +| `displayName` | The name that policies use in `{{name}}`. Defaults to the resource name. It has 1 to 256 letters, digits, hyphens, periods, or underscores. | +| `namedValueName` | The physical named-value identifier in APIM. Defaults to the resource name. | +| `tags` | Tags that organize the named value. | + +Aspire enforces these rules: + +- A literal value isn't empty or whitespace and has at most 4,096 characters. +- `AddSecretNamedValue` requires a parameter that's marked as secret. Aspire throws an `ArgumentException` for a parameter that isn't. +- The physical name and the display name are each unique within the service, ignoring case. + +### Key Vault-backed named values + +APIM reads a Key Vault-backed value through the user-assigned managed identity of the service. Aspire grants that identity the Key Vault Secrets User role on the vault through the standard Azure role-assignment pipeline, and the APIM deployment waits for the role assignment. The secret reference is versionless, so APIM picks up rotated values. For a Key Vault resource that isn't an Azure provisioning resource, Aspire keeps an inline role assignment instead. + +For an existing APIM service, supply an identity that's already attached to the service. For more information, see [Service configuration](/integrations/cloud/azure/azure-api-management/azure-api-management-service/#use-an-existing-service). + +## See also + +- [APIs and operations](/integrations/cloud/azure/azure-api-management/azure-api-management-apis/) +- [Backends and backend pools](/integrations/cloud/azure/azure-api-management/azure-api-management-backends/) +- [Azure Key Vault integration](/integrations/cloud/azure/azure-key-vault/azure-key-vault-get-started/) +- [External parameters](/fundamentals/external-parameters/) +- [Azure API Management policies](https://learn.microsoft.com/azure/api-management/api-management-howto-policies) +- [Named values in Azure API Management](https://learn.microsoft.com/azure/api-management/api-management-howto-properties) diff --git a/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-products.mdx b/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-products.mdx new file mode 100644 index 000000000..2776a33ae --- /dev/null +++ b/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-products.mdx @@ -0,0 +1,146 @@ +--- +title: API Management products and subscriptions +description: Group Azure API Management APIs into products, configure approval and subscription limits, and provision subscriptions from your Aspire AppHost. +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +A product in Azure API Management (APIM) groups one or more APIs and controls how consumers get access to them. A subscription gives a consumer the keys that call the APIs in a product. The Aspire Azure API Management hosting integration models these as `AzureApiManagementProductResource` and `AzureApiManagementSubscriptionResource` children of the service. + +This page assumes the `apim` and `catalog` resources from [Get started with Azure API Management](/integrations/cloud/azure/azure-api-management/azure-api-management-get-started/#add-a-service-and-route-a-workload). + +## Add a product and a subscription + +Call `AddProduct` to create a product, `WithApi` to add APIs to it, and `AddSubscription` to provision a subscription that's scoped to the product. + + + + +```typescript title="apphost.mts" +const catalogApi = await apim.addApi("catalog-api", catalog, "catalog"); + +const product = await apim.addProduct("catalog-product", "Catalog", { + options: { + description: "Catalog APIs", + subscriptionRequired: true, + approvalRequired: false, + }, +}); +await product.withApi(catalogApi); +await product.addSubscription("catalog-client", "Catalog client"); +``` + + + + +```csharp title="AppHost.cs" +var catalogApi = apim.AddApi("catalog-api", catalog, path: "catalog"); + +apim.AddProduct("catalog-product", "Catalog", new() + { + Description = "Catalog APIs", + SubscriptionRequired = true, + ApprovalRequired = false, + }) + .WithApi(catalogApi) + .AddSubscription("catalog-client", "Catalog client"); +``` + + + + +A product can contain only APIs from the same APIM service. Aspire provisions product membership as an API link on the product, which references the full resource ID of the API. + +## Product options + +The `AddProduct` method has the following parameters: + +| Parameter | Required | Description | +| --------- | -------- | ----------- | +| `name` | Yes | The Aspire resource name. It's also the default physical product name. | +| `displayName` | Yes | The product display name. It has at most 300 characters. | +| `options` | No | The product options. | +| `productName` | No | The physical product identifier in APIM. It defaults to the resource name, has at most 256 characters, can't contain `*`, `#`, `&`, `+`, `:`, `<`, `>`, or `?`, and is unique within the service. | + +The product options have these properties: + +| Option | Default | Description | +| ------ | ------- | ----------- | +| `Description` | None | The product description. It has at most 1,000 characters. | +| `Terms` | None | The terms of use that subscribers see. | +| `SubscriptionRequired` | `true` | Whether callers need a subscription to use the APIs in the product. | +| `ApprovalRequired` | `false` | Whether subscription requests need administrator approval. It requires `SubscriptionRequired`. | +| `SubscriptionsLimit` | None | The maximum number of subscriptions a user can create for the product. It must be greater than zero and requires `SubscriptionRequired`. When unset, the number is unlimited. | +| `State` | `Published` | `Published` makes the product visible to subscribers. `NotPublished` hides it. | + +Aspire throws an `ArgumentException` when you combine `ApprovalRequired` or `SubscriptionsLimit` with `SubscriptionRequired` set to `false`. + +The following example creates a product for partners that requires approval, limits subscriptions, and stays unpublished until you publish it: + + + + +```typescript title="apphost.mts" +import { AzureApiManagementProductState } from './.aspire/modules/aspire.mjs'; + +await apim.addProduct("partner-product", "Partner", { + options: { + description: "APIs for partners.", + terms: "Use of these APIs is subject to the partner agreement.", + approvalRequired: true, + subscriptionsLimit: 5, + state: AzureApiManagementProductState.NotPublished, + }, +}); +``` + + + + +```csharp title="AppHost.cs" +using Aspire.Hosting.Azure; + +apim.AddProduct("partner-product", "Partner", new() +{ + Description = "APIs for partners.", + Terms = "Use of these APIs is subject to the partner agreement.", + ApprovalRequired = true, + SubscriptionsLimit = 5, + State = AzureApiManagementProductState.NotPublished, +}); +``` + + + + +## Subscription options + +The `AddSubscription` method has the following parameters: + +| Parameter | Required | Description | +| --------- | -------- | ----------- | +| `name` | Yes | The Aspire resource name. It's also the default physical subscription name. | +| `displayName` | Yes | The subscription display name. It has at most 100 characters. | +| `options` | No | The subscription options. | +| `subscriptionName` | No | The physical subscription identifier in APIM. It defaults to the resource name, has at most 256 characters, and is unique across all products of the service. | + +The subscription options have these properties: + +| Option | Default | Description | +| ------ | ------- | ----------- | +| `AllowTracing` | `false` | Whether tracing is allowed for the subscription. | +| `State` | `Active` | The subscription state: `Active`, `Suspended`, `Submitted`, `Rejected`, `Expired`, or `Cancelled`. | + +## Get the subscription keys + +APIM generates the primary and secondary keys for a subscription during deployment. Aspire intentionally doesn't expose them as deployment outputs, because they're secrets. Retrieve the keys from the Azure portal or through the APIM management API. + +By default, callers send the key in the `Ocp-Apim-Subscription-Key` request header or in the `subscription-key` query string parameter. + +An API that you add with `subscriptionRequired` set to `true`, which is the default, rejects requests without a valid key. The API-level and product-level subscription settings are separate properties in APIM. For how APIM combines them, see [Subscriptions in Azure API Management](https://learn.microsoft.com/azure/api-management/api-management-subscriptions). + +## See also + +- [APIs and operations](/integrations/cloud/azure/azure-api-management/azure-api-management-apis/) +- [Get started with Azure API Management](/integrations/cloud/azure/azure-api-management/azure-api-management-get-started/) +- [Products in Azure API Management](https://learn.microsoft.com/azure/api-management/api-management-howto-add-products) diff --git a/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-service.mdx b/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-service.mdx new file mode 100644 index 000000000..95f43d19d --- /dev/null +++ b/src/frontend/src/content/docs/integrations/cloud/azure/azure-api-management/azure-api-management-service.mdx @@ -0,0 +1,269 @@ +--- +title: Configure the API Management service +description: Configure the managed identity, Application Insights diagnostics, custom domains, and existing-service adoption for Azure API Management in Aspire. +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +This page covers settings that apply to the Azure API Management (APIM) service as a whole: the managed identity that it uses, Application Insights diagnostics, custom domains, and adopting an existing service. It assumes the `apim` and `catalog` resources from [Get started with Azure API Management](/integrations/cloud/azure/azure-api-management/azure-api-management-get-started/#add-a-service-and-route-a-workload). + +## Managed identity + +APIM uses a user-assigned managed identity to authenticate to backends and to read secrets and certificates from Azure Key Vault. For a new service, Aspire creates the identity, attaches it to the service, and grants it the roles that your configuration requires through the standard Azure role-assignment pipeline: + +| Configuration | Role granted to the identity | +| ------------- | ---------------------------- | +| `AddAzureOpenAIBackend` | Cognitive Services OpenAI User on the account | +| `AddFoundryBackend` | Cognitive Services User on the account | +| `AddBlobStorageBackend` | Storage Blob Data Reader on the storage account | +| `AddKeyVaultNamedValue` | Key Vault Secrets User on the vault | +| `WithCustomDomain` | Key Vault Certificate User on the vault | + +Role-assignment modules deploy at the target resource's scope, including a different resource group or subscription. The principal that runs `aspire deploy` needs `Microsoft.Authorization/roleAssignments/write` on each target scope. + +To use an identity that you manage, add it as a user-assigned identity resource and pass it to `WithAzureUserAssignedIdentity`. A new service attaches the supplied identity automatically instead of creating its own. The inferred role assignments follow the selected identity, so you can call the method before or after you add backends and named values. + + + + +```typescript title="apphost.mts" +const identity = await builder.addAzureUserAssignedIdentity("shared-identity"); + +await apim.withAzureUserAssignedIdentity(identity); +``` + + + + +```csharp title="AppHost.cs" +using Aspire.Hosting.Azure; + +var identity = builder.AddAzureUserAssignedIdentity("shared-identity"); + +apim.WithAzureUserAssignedIdentity(identity); +``` + + + + +In C#, the identity that APIM uses is available as `apim.Resource.Identity`. For more information about identities, see [Azure user-assigned managed identity](/integrations/cloud/azure/user-assigned-identity/). + +## Send diagnostics to Application Insights + +Call `WithApplicationInsights` on the service to log every API to an Azure Application Insights resource, or on an API to configure its diagnostics separately. + + + + +```typescript title="apphost.mts" +import { AzureApiManagementDiagnosticVerbosity } from './.aspire/modules/aspire.mjs'; + +const insights = await builder.addAzureApplicationInsights("insights"); + +await apim.withApplicationInsights(insights, { samplingPercentage: 25 }); + +const catalogApi = await apim.addApi("catalog-api", catalog, "catalog"); +await catalogApi.withApplicationInsights(insights, { + samplingPercentage: 100, + verbosity: AzureApiManagementDiagnosticVerbosity.Error, +}); +``` + + + + +```csharp title="AppHost.cs" +using Aspire.Hosting.Azure; + +var insights = builder.AddAzureApplicationInsights("insights"); + +apim.WithApplicationInsights(insights, new() +{ + SamplingPercentage = 25, +}); + +var catalogApi = apim.AddApi("catalog-api", catalog, path: "catalog"); +catalogApi.WithApplicationInsights(insights, new() +{ + SamplingPercentage = 100, + Verbosity = AzureApiManagementDiagnosticVerbosity.Error, +}); +``` + + + + +Aspire creates an APIM logger for each Application Insights resource that you use, and a diagnostic named `applicationinsights` at each scope that you configure. The diagnostic uses W3C correlation, always logs errors, emits metrics, and doesn't capture request or response bodies. + +The diagnostic options have these properties: + +| Option | Default | Description | +| ------ | ------- | ----------- | +| `SamplingPercentage` | `100` | The percentage of requests to sample, from 0 to 100. | +| `Verbosity` | `Information` | `Error`, `Information`, or `Verbose`. | +| `LogClientIp` | `false` | Whether telemetry includes the client IP address. | + +You can configure diagnostics once for the service and once for each API. A second call at the same scope throws an `InvalidOperationException`. For more information about the Application Insights resource, see [Azure Application Insights integration](/integrations/cloud/azure/azure-application-insights/). + +## Add a custom domain + +Call `WithCustomDomain` to bind a host name to a PFX certificate that's stored as a secret in Azure Key Vault. + + + + +```typescript title="apphost.mts" +const certificateVault = await builder.addAzureKeyVault("certificates") + .publishAsExisting("contoso-certificates"); + +await apim.withCustomDomain("api.contoso.com", certificateVault, "gateway-certificate", { + defaultSslBinding: true, +}); +``` + + + + +```csharp title="AppHost.cs" +using Aspire.Hosting.Azure; + +var certificateVault = builder.AddAzureKeyVault("certificates") + .PublishAsExisting("contoso-certificates", resourceGroup: null); + +apim.WithCustomDomain( + "api.contoso.com", + certificateVault.GetSecret("gateway-certificate"), + AzureApiManagementHostnameType.Proxy, + defaultSslBinding: true); +``` + + + + +In TypeScript, `withCustomDomain` takes the Key Vault resource and the certificate secret name as separate arguments. In C#, you pass the Key Vault secret reference that `GetSecret` returns. + +The method has the following parameters: + +| Parameter | Default | Description | +| --------- | ------- | ----------- | +| `hostname` | None | A fully qualified, multi-label DNS name, such as `api.contoso.com`. Single-label host names and IP addresses are rejected. | +| `certificate` | None | The Key Vault secret that contains the PFX certificate. | +| `type` | `Proxy` | The endpoint: `Proxy` for the gateway, `ConfigurationApi`, `DeveloperPortal`, `Portal`, `Management`, or `Scm`. | +| `defaultSslBinding` | `false` | Whether the certificate is the default SNI fallback. Valid only for gateway host names. | +| `negotiateClientCertificate` | `false` | Whether the endpoint negotiates client certificates. | + +The certificate URI is versionless, so APIM refreshes a renewed certificate automatically. Aspire grants the APIM identity the Key Vault Certificate User role and configures the host name to use that identity. Key vaults that require APIM's system-assigned identity through the trusted-services firewall exception aren't supported. Aspire doesn't create DNS records for the host name. + +Aspire validates the combination of host names and tiers: + +- Only one host name can be the default SSL binding. +- The Consumption tier supports one custom domain, for the gateway endpoint only. +- The v2 tiers (`BasicV2`, `StandardV2`, and `PremiumV2`) don't support custom domains for the `Management` and `Scm` endpoints. +- Only the Developer, Premium, and Premium v2 tiers support more than one gateway custom domain. +- Each non-gateway endpoint type supports one host name, and a host name can't be repeated for the same endpoint type. + +## Use an existing service + +Call `PublishAsExisting` to adopt an APIM service that already exists. Aspire treats the service itself as read-only and manages the resources that you declare beneath it: APIs, operations, backends, pools, policies, fragments, products, subscriptions, named values, and diagnostics. + + + + +```typescript title="apphost.mts" +import { AzureApiManagementSku, createBuilder, refExpr } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const apim = await builder.addAzureApiManagement("apim", { + publisherEmail: "api-owners@example.com", + publisherName: "Contoso APIs", + sku: AzureApiManagementSku.Premium, +}).publishAsExisting("shared-apim", { + resourceGroup: "shared-infrastructure", +}); + +const backend = await apim.addBackend("catalog-backend", refExpr`https://catalog.example.com`); + +const catalogApi = await apim.addApiWithoutTarget("catalog-api", "catalog"); +await catalogApi.withApiBackend(backend); + +await builder.build().run(); +``` + + + + +```csharp title="AppHost.cs" +using Aspire.Hosting.Azure; + +var builder = DistributedApplication.CreateBuilder(args); + +var apim = builder.AddAzureApiManagement("apim", new() +{ + PublisherEmail = "api-owners@example.com", + PublisherName = "Contoso APIs", + Sku = AzureApiManagementSku.Premium, +}).PublishAsExisting("shared-apim", resourceGroup: "shared-infrastructure"); + +var backend = apim.AddBackend( + "catalog-backend", + ReferenceExpression.Create($"https://catalog.example.com")); + +apim.AddApi("catalog-api", path: "catalog") + .WithBackend(backend); + +builder.Build().Run(); +``` + + + + +Adopting a service has these requirements and limits: + +- Set `Sku` to the adopted service's actual pricing tier, even when it's Developer. Aspire doesn't query or change the service's tier. It uses the value to validate children that depend on the tier, such as circuit breakers and OpenAI backends. The Developer default applies only to new services, and a missing `Sku` throws an `InvalidOperationException` when you publish. +- Aspire rejects service-level virtual network configuration and custom domains for an existing service, and it doesn't change the service's managed identities. +- Private endpoints work with existing services without modifying them. For more information, see [Virtual networking](/integrations/cloud/azure/azure-api-management/azure-api-management-networking/#add-a-private-endpoint). +- Diagnostics aren't supported when you adopt a service from another resource group or subscription. For a service in the deployment resource group, the service and the Application Insights resource must both be in that group. + +### Supply an existing identity + +APIM uses a user-assigned identity for managed-identity backends and Key Vault-backed named values. Aspire can't attach an identity to an existing service, so you must supply one that's already attached. Configure it as an existing resource and pass it to `WithAzureUserAssignedIdentity`: + + + + +```typescript title="apphost.mts" +const identity = await builder.addAzureUserAssignedIdentity("existing-apim-identity") + .publishAsExisting("shared-apim-identity", { + resourceGroup: "shared-infrastructure", + }); + +await apim.withAzureUserAssignedIdentity(identity); +``` + + + + +```csharp title="AppHost.cs" +using Aspire.Hosting.Azure; + +var identity = builder.AddAzureUserAssignedIdentity("existing-apim-identity") + .PublishAsExisting("shared-apim-identity", resourceGroup: "shared-infrastructure"); + +apim.WithAzureUserAssignedIdentity(identity); +``` + + + + +If an existing service uses a managed-identity backend or a Key Vault-backed named value and the identity isn't configured as an existing resource, publishing throws an `InvalidOperationException`. Aspire doesn't query Azure to verify that the identity is attached, so attaching it remains your responsibility. An existing service that uses only anonymous backends and literal named values doesn't need an existing identity. + +## See also + +- [Get started with Azure API Management](/integrations/cloud/azure/azure-api-management/azure-api-management-get-started/) +- [Policies and named values](/integrations/cloud/azure/azure-api-management/azure-api-management-policies/) +- [Azure user-assigned managed identity](/integrations/cloud/azure/user-assigned-identity/) +- [Azure role assignments](/integrations/cloud/azure/role-assignments/) +- [Azure Application Insights integration](/integrations/cloud/azure/azure-application-insights/) +- [Azure Key Vault integration](/integrations/cloud/azure/azure-key-vault/azure-key-vault-get-started/) +- [Configure a custom domain name for Azure API Management](https://learn.microsoft.com/azure/api-management/configure-custom-domain) diff --git a/src/frontend/src/content/docs/integrations/cloud/azure/azure-virtual-network.mdx b/src/frontend/src/content/docs/integrations/cloud/azure/azure-virtual-network.mdx index 19e6fe077..b7b7574b6 100644 --- a/src/frontend/src/content/docs/integrations/cloud/azure/azure-virtual-network.mdx +++ b/src/frontend/src/content/docs/integrations/cloud/azure/azure-virtual-network.mdx @@ -504,6 +504,10 @@ storage.ConfigureInfrastructure(infra => [Azure SQL Server](/integrations/cloud/azure/azure-sql-database/azure-sql-database-get-started/) uses a deployment script to grant your application's managed identity access to the SQL database. When you add a private endpoint for a SQL Server resource, the deployment script needs to run inside the virtual network via Azure Container Instances (ACI). Aspire automatically creates a delegated subnet and a storage account for the deployment script container. For details on customizing this behavior, see [Admin deployment script](/integrations/cloud/azure/azure-sql-database/azure-sql-database-host/#admin-deployment-script). +##### Azure API Management + +[Azure API Management](/integrations/cloud/azure/azure-api-management/azure-api-management-get-started/) is an exception to the automatic lockdown. Adding a private endpoint for an APIM resource creates the `Gateway` private endpoint and the `privatelink.azure-api.net` private DNS zone, but it doesn't change the public network access setting of the service. The Consumption and Basic v2 tiers don't support private endpoints, and you can't combine a private endpoint with classic virtual network injection or Premium v2 injection. For details, see [Azure API Management networking](/integrations/cloud/azure/azure-api-management/azure-api-management-networking/#add-a-private-endpoint). + ##### Azure Container Registry [Azure Container Registry](/integrations/cloud/azure/azure-container-registry/azure-container-registry-host/) requires the **Premium** SKU to support private endpoints. When you add a private endpoint for an Azure Container Registry resource, Aspire automatically upgrades the SKU to Premium.