Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/terraform-offline.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Terraform offline acceptance

on:
pull_request:
paths:
- 'samples/terraform/**'
- '.github/workflows/terraform-offline.yml'
push:
paths:
- 'samples/terraform/**'
- '.github/workflows/terraform-offline.yml'
workflow_dispatch:

permissions:
contents: read

jobs:
offline:
runs-on: windows-2022
timeout-minutes: 20
env:
ARM_SKIP_PROVIDER_REGISTRATION: 'true'
TF_IN_AUTOMATION: 'true'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3
with:
terraform_version: 1.11.4
terraform_wrapper: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: '3.12.10'
- name: Credential-free checks
shell: pwsh
run: .\samples\terraform\scripts\Test-Offline.ps1
4 changes: 2 additions & 2 deletions samples/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Samples

A curated catalog of samples that build on the [AI Gateway tier in Azure API Management](https://aka.ms/aigateway/docs). This folder catalogs samples that live in their own repositories; it does not host sample code.
A curated catalog of samples that build on the [AI Gateway tier in Azure API Management](https://aka.ms/aigateway/docs). Samples are linked from their own repositories, with a local Terraform infrastructure sample below.

| Sample | Language | Description |
| ------ | -------- | ----------- |
| Foundry AI Gateway E2E | [Python](https://aka.ms/foundry-aigateway-python) / [.NET (C#)](https://aka.ms/foundry-aigateway-dotnet) | A Daily Digest Agent deployed as a Foundry Hosted Agent whose model and GitHub MCP traffic is routed through AI Gateway. |
| IaC only | [Bicep](https://github.com/Azure-Samples/simple-foundry-hosted-agent-python-aigateway/tree/main/infra/ai-gateway) | Infrastructure as code for provisioning infrastructure using Bicep, scripts, or azd. Direct links to the building blocks: [AI Gateway](https://github.com/Azure-Samples/simple-foundry-hosted-agent-python-aigateway/tree/main/infra/ai-gateway), [Foundry Models](https://github.com/Azure-Samples/simple-foundry-hosted-agent-python-aigateway/tree/main/infra/foundry-models), and [Foundry Hosted Agent](https://github.com/Azure-Samples/simple-foundry-hosted-agent-python-aigateway/tree/main/infra/foundry-agents). |
| IaC only | [Bicep](https://github.com/Azure-Samples/simple-foundry-hosted-agent-python-aigateway/tree/main/infra/ai-gateway) / [Terraform](terraform/README.md) | Infrastructure as code for AI Gateway, Foundry Models, and Foundry Hosted Agent infrastructure. Bicep building blocks: [AI Gateway](https://github.com/Azure-Samples/simple-foundry-hosted-agent-python-aigateway/tree/main/infra/ai-gateway), [Foundry Models](https://github.com/Azure-Samples/simple-foundry-hosted-agent-python-aigateway/tree/main/infra/foundry-models), and [Foundry Hosted Agent](https://github.com/Azure-Samples/simple-foundry-hosted-agent-python-aigateway/tree/main/infra/foundry-agents). The Terraform sample provisions all three building blocks together and includes an optional [private-connectivity PoC profile](terraform/docs/private-connectivity.md) (gateway public access remains enabled). |
15 changes: 15 additions & 0 deletions samples/terraform/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.terraform/
*.tfstate
*.tfstate.*
*.tfplan
*.tfvars
*.tfvars.json
!profiles/private-connectivity.tfvars.json
crash.log
crash.*.log
override.tf
override.tf.json
*_override.tf
*_override.tf.json
__pycache__/
acceptance-results/
23 changes: 23 additions & 0 deletions samples/terraform/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

202 changes: 202 additions & 0 deletions samples/terraform/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# AI Gateway and Foundry with Terraform

Manage an Azure AI Gateway that routes calls to Foundry models. This is standalone
Terraform for the [Foundry AI Gateway Python sample](https://github.com/Azure-Samples/simple-foundry-hosted-agent-python-aigateway), not an `azd` app.

> **Preview PoC, not production.** The AI Gateway preview has **no SLA**. Everything
> here is billable (model calls, Premium ACR, Storage, monitoring). Read the
> [limitations](docs/reference.md#limitations) before applying.

> **Existing deployments only.** This sample retains the legacy
> `Microsoft.ApiManagement/service` resource with SKU `AIGateway`. New gateways
> cannot be created with that SKU, and existing ones must migrate before
> **January 15, 2027**. This sample does not perform that migration.
> See [resource type and lifecycle](docs/reference.md#resource-type-and-lifecycle).

## Choose your networking path

The sample includes **both public and private connectivity**. The public path is
the Terraform default; the private profile adds private endpoints and routing.

| Path | Private endpoints | Gateway public access | Model public access |
| --- | --- | --- | --- |
| [Private connectivity](#private-connectivity-path) | Gateway and model account | **Enabled** | **Disabled** after staging |
| [Public endpoints](#public-endpoint-path) | None | Enabled | Enabled |

**Pending: private-only gateway ingress (product-group investigation).** The
observed failure when disabling public access is being investigated with the
product group. No fix has been verified for this public-preview sample yet.
The private profile is intended to provide a private route while the gateway's
public endpoint remains **Enabled**. Verify both endpoints, DNS and inference
after every deployment; applying this profile alone is not proof that the route
works. Do not treat it as private-only. See the [status and observed behavior](docs/reference.md#known-blocker-private-only-gateway-ingress).

**Already using the private profile?** Follow [existing private deployments](docs/private-connectivity.md#existing-private-deployments).
Keep the profile on every plan, including drift checks. Do not follow the public
path or repeat the temporary-public staging step for an already-private backend.

## Before you start

**Check these first:**

- [ ] Terraform 1.11+ (below 2.0) and Azure CLI installed
- [ ] Existing legacy AIGateway deployment and subscription access to Foundry hosted agents in your regions
- [ ] Permission to create resources and role assignments (for example, Contributor plus Role Based Access Control Administrator)
- [ ] If enabling the test VM: permission to manage its **custom role definition**, not just role assignments; see [test-client permissions](docs/private-connectivity.md#test-client-permissions)
- [ ] Providers registered: `Microsoft.ApiManagement`, `Microsoft.Web`, `Microsoft.CognitiveServices`, `Microsoft.Storage`, `Microsoft.ContainerRegistry`, `Microsoft.OperationalInsights`, `Microsoft.Insights`, `Microsoft.Monitor` (this sample does not register them for you)
- [ ] Quota for the model and SKU in your region

**Decide before you start:**

- **Public endpoints OK?** If your policy prohibits public gateway or model endpoints, even temporarily, do not run this sample unchanged.
- **Need private hosted-agent execution?** Set it when the Foundry account is created. This sample does not configure it, and you cannot add it to an existing account later.
- **Need private-only gateway ingress?** Pending product-group investigation; the current profile does not meet this requirement. See the [status and observed behavior](docs/reference.md#known-blocker-private-only-gateway-ingress).

## What gets deployed

| Component | Purpose |
| --- | --- |
| **AI Gateway** | Front door: checks the runtime API key, applies token limits, routes to a model alias, records telemetry. It does not run models. |
| **Foundry model account** | Runs the models. The gateway authenticates with its managed identity, not an API key. |
| **Foundry hosted-agent account and project** | Prepared for a *later* agent app: Storage, Premium Container Registry, roles and connections. **No agent, image or running container is deployed.** |
| **Private network (private path)** | Gateway and model private endpoints, DNS, and gateway outbound VNet integration. See [private connectivity](docs/private-connectivity.md). |
| **Optional private test VM** | Probe client for the private path. It is not required if you already have a suitable private client. |

Both paths manage all three core components. Hosted-agent infrastructure is
included even if you only test model calls.

All application resources use **one shared resource group**:
`rg-<environment_name>-<resource_token>-gateway`. The existing gateway group name
and Terraform address are retained; the AIGateway SKU, private-network design and
agent monitoring are unchanged. Gateway OTLP enablement can create **Azure-managed
monitoring helper groups**, with one gateway publishing role on the linked DCR.
This explicit exception, its costs and the required two-stage setup are covered
in [gateway monitoring](docs/monitoring.md).

**Upgrading from the former three-group layout is a migration, not a harmless
refactor.** Terraform can propose backend replacements and deletion of the old
groups. Do not apply until the exact migration/recreation and any data loss have
been approved. See [single-group consolidation](docs/reference.md#single-group-consolidation).

**Authentication in both paths** (independent of public or private routing):

```text
Client --(runtime API key)--> AI Gateway --(managed identity)--> Foundry model account
```

A **model alias** (for example, `gpt-mini`) is the name clients send. It does not have to match the underlying model name (for example, `gpt-4.1-mini`).

## Shared setup

Run from `samples/terraform` for either networking path. Use the existing
deployment's input file and Terraform state/backend. Do not start with an empty
state for resources already managed by this sample.

**1. Sign in:**

```powershell
az login
```

**2. Review the existing `terraform.tfvars`:**

| Setting | What to set |
| --- | --- |
| `subscription_id` | Where resources are created |
| `environment_name` | Existing deployment label; changing it changes resource names |
| `location`, `gateway_location` | Foundry and gateway regions (independent of each other) |
| `publisher_email` | Gateway publisher contact |
| `model_deployments` | Model versions, capacity and token limits. Keys (`gpt-mini`, `gpt-full`) become client-facing aliases. |
| `storage_public_network_access` | `Disabled` only if your policy requires it |

This file is git-ignored. Do not overwrite it with
[terraform.tfvars.example](terraform.tfvars.example), which documents the input
shape only. Optional settings are in the [input reference](docs/reference.md#inputs).

**3. Initialize and check:**

```powershell
az account set --subscription "<subscription-id>"
terraform init
terraform fmt -check -recursive
terraform validate
```

**4. Bootstrap or upgrade gateway monitoring:** follow
[gateway monitoring](docs/monitoring.md) to enable the existing Insights
destination and verify its actual endpoints before the exporter/role plan.
Preserve your networking profile and review each saved plan. This adds
service-managed resources and is not a cost-free or fully reversible toggle.

Then continue with **one** of the networking paths below.

## Private connectivity path

Use [profiles/private-connectivity.tfvars.json](profiles/private-connectivity.tfvars.json)
for private client-to-gateway and gateway-to-model traffic:

```text
Private client -> gateway private endpoint -> gateway outbound VNet integration -> model private endpoint
```

Follow the [private-connectivity guide](docs/private-connectivity.md) for the
staged plan/apply, private probe, and VM lifecycle. The gateway public endpoint
stays enabled; model public access is disabled in the final profile.

**Do not run the public-path plan below for this configuration.** Omitting the
private profile can plan removal of private networking or re-enable model public
access, depending on your other inputs.

## Public endpoint path

Use this path only for a deployment intentionally configured without private
networking. It is not a prerequisite for the private path.

**1. Plan and review:**

```powershell
terraform plan "-out=deployment.tfplan"
```

Stop if planning fails. Review the plan for public access, resource creation and
cost. Do not apply unexpected changes.

**2. Apply the reviewed plan:**

```powershell
terraform apply "deployment.tfplan"
```

**3. Test every alias:**

```powershell
.\scripts\Test-ModelAliases.ps1
```

Each alias should print a short reply, for example `gpt-mini: Hello!`.
This check uses your machine and public endpoints. It does **not** prove private routing.
The example calls every configured alias and assumes chat-completions models,
such as the two models in `terraform.tfvars.example`. Both test scripts reject
missing or incompatible API metadata before making model calls; they do not
silently skip aliases. See [probe compatibility](docs/reference.md#probe-compatibility)
for supported request parameters and updating older outputs. For other model
types, use their supported API and request body instead.

**Done when:** every alias returns a response and `terraform plan -detailed-exitcode` exits `0`.

## Additional runtime checks

For a reproducible private-path rehearsal, use the
[end-to-end runbook](docs/end-to-end.md): credential-free CI, a guarded backend
verifier and a separate authenticated portal review including fresh metrics.
The portal evidence validator checks completeness; it does not automate the UI.

Complete the [customer acceptance guide](docs/acceptance.md) for either path.
It includes commands, expected results, and failure boundaries for missing and
invalid keys, the six service RBAC assignments plus the DCR publishing role, Foundry connections, telemetry,
and the final drift check. A successful model call alone is not full acceptance.

## Going further

- [Reference](docs/reference.md): file guide, all inputs, outputs, networking, state and security, offline tests, limitations
- [Cost and cleanup](docs/reference.md#cost-and-cleanup)
Loading