-
Notifications
You must be signed in to change notification settings - Fork 344
docs(agents): add infra layer design spec #9363
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f40bfd5
225b95d
311fdd7
bd30ab8
b8f6ce2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| # Foundry Infrastructure Layers for `azd ai agent init --infra` | ||
|
|
||
| ## Summary | ||
|
|
||
| `azd ai agent init --infra` cannot currently generate Foundry IaC in an azd | ||
| project that already has an `infra/` directory. Both the existing project and | ||
| the generated Foundry templates expect to own the same deployment entry point. | ||
|
|
||
| This design preserves the project's existing infrastructure and adds Foundry | ||
| infrastructure as a separate provisioning layer under `infra/foundry`. | ||
| Foundry-only projects keep the existing simple `infra/` layout. | ||
|
|
||
| > `--infra` adds editable Foundry infrastructure without merging into or | ||
| > overwriting infrastructure the user already owns. | ||
|
|
||
| Source issue: [Azure/azure-dev#9126](https://github.com/Azure/azure-dev/issues/9126). | ||
|
|
||
| ## Problem | ||
|
|
||
| Developers commonly add a Foundry agent to an existing web application or API. | ||
| When that application already contains Bicep or Terraform, eject fails: | ||
|
|
||
| ```text | ||
| ERROR: `./infra/` already exists | ||
| ``` | ||
|
|
||
| Deleting `infra/` loses application infrastructure. File-level merging is not | ||
| safe because both trees normally contain `main.bicep` or Terraform entry-point | ||
| files. Resource-level merging would require azd to understand and rewrite | ||
| arbitrary user-authored IaC. | ||
|
|
||
| ## Goals | ||
|
|
||
| 1. Add editable Foundry IaC to projects with existing infrastructure. | ||
| 2. Preserve existing files and configuration. | ||
| 3. Support Bicep and Terraform eject. | ||
| 4. Avoid semantic IaC merging and silent overwrites. | ||
| 5. Keep Foundry-only projects backward-compatible. | ||
|
|
||
| ## Proposed Experience | ||
|
|
||
| The command surface is unchanged: | ||
|
|
||
| | Command | Result | | ||
| |---|---| | ||
| | `azd ai agent init --infra` | Eject Bicep | | ||
| | `azd ai agent init --infra=bicep` | Eject Bicep explicitly | | ||
| | `azd ai agent init --infra=terraform` | Eject Terraform | | ||
|
|
||
| ### Behavior by project shape | ||
|
|
||
| | Project shape | Behavior | | ||
| |---|---| | ||
| | New or Foundry-only project | Generate the existing root layout under `infra/` | | ||
| | Existing single-layer Bicep or Terraform project | Preserve it as a layer and add `foundry` at `infra/foundry` | | ||
| | Existing custom/fileless provider | Preserve it as a layer and add `foundry` | | ||
| | Existing layered project without `foundry` | Append a `foundry` layer; preserve existing layer bodies | | ||
| | Existing compatible `foundry` layer with empty target | Generate into its declared `path` and `module` | | ||
| | Existing `foundry` layer with a different provider | Refuse; do not convert providers implicitly | | ||
| | Existing `infra/foundry` directory without a layer | Add only non-conflicting files; refuse generated-file collisions | | ||
| | Existing files in the declared `foundry` target | Refuse without overwriting | | ||
| | Brownfield project with `endpoint:` | Refuse eject; existing resource remains externally owned | | ||
| | Terraform request with private `network:` | Refuse and recommend Bicep | | ||
|
|
||
| A folder and a layer are separate signals. An `infra/foundry` folder does not | ||
| authorize azd to overwrite its contents. An existing `foundry` layer is reused | ||
| only when its provider matches the requested format and generated files do not | ||
| already exist. | ||
|
|
||
| ### Example migration | ||
|
|
||
| Before: | ||
|
|
||
| ```yaml | ||
| infra: | ||
| provider: bicep | ||
| ``` | ||
|
|
||
| After `azd ai agent init --infra`: | ||
|
|
||
| ```yaml | ||
| infra: | ||
| layers: | ||
| - name: infra | ||
| path: infra | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The migration example only covers a project using the defaults, so it doesn't say what happens to Both schemas hard-forbid those keys once "allOf": [{
"if": { "required": ["layers"], "properties": { "layers": { "minItems": 1 } } },
"then": { "properties": { "path": false, "module": false } }
}]
So for a project like this: infra:
provider: bicep
path: iac
module: deploythe migration can't leave those at the root, it has to move them into the preserved layer. This example hardcodes The already-layered path says "preserve existing layer bodies", but the root-to-layers path never states the equivalent. Worth saying explicitly that |
||
| provider: bicep | ||
| - name: foundry | ||
| path: infra/foundry | ||
| provider: microsoft.foundry | ||
| ``` | ||
|
|
||
| The existing `infra/main.bicep` remains unchanged. Foundry files are generated | ||
| under `infra/foundry/`. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The motivating scenario is "add a Foundry agent to an existing web application or API," but the design stops at file layout and resource-group ownership. It never says how the existing That's the part the user actually cares about after eject. If the wiring is expected to be manual, it belongs in Out of scope. If azd is supposed to plumb it, it changes both the safety rules and the |
||
|
|
||
| The Foundry layer is independent by default. Eject does not add `dependsOn`; | ||
| projects can declare a dependency explicitly when their Foundry template | ||
| actually consumes another layer's outputs. | ||
|
Comment on lines
+95
to
+97
Comment on lines
+95
to
+97
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Independent by default" is doing more work here than it reads, because azd's layer dependency analyzer doesn't see this layer at all.
The paragraph covers the direction where the Foundry template consumes another layer. The reverse direction is the one the Problem section actually motivates: an existing app layer that wants the Foundry project endpoint. That layer is bicep so it does get scanned, but a
Worth stating outright that no dependency is inferred in either direction for a |
||
|
|
||
| ## Ownership and Lifecycle | ||
|
|
||
| Generated Bicep continues to use `microsoft.foundry`, with the layer's `path` | ||
| and `module` telling the provider where to load the ejected templates. | ||
| Generated Terraform uses the core `terraform` provider because Terraform state | ||
| and lifecycle are owned by azd core. | ||
|
|
||
| The Foundry layer owns an isolated resource group: | ||
|
|
||
| ```text | ||
| rg-${AZURE_ENV_NAME}-foundry | ||
| ``` | ||
|
Comment on lines
+108
to
+110
|
||
|
|
||
| It publishes `AZURE_FOUNDRY_RESOURCE_GROUP` rather than replacing the existing | ||
| application's `AZURE_RESOURCE_GROUP`. This keeps targeted `azd down foundry` | ||
|
Comment on lines
+112
to
+113
|
||
| from deleting sibling-layer resources. | ||
|
|
||
| ## Safety Rules | ||
|
|
||
| The command validates the full plan before updating the project: | ||
|
|
||
| - Exactly one Foundry provisioning service must exist. | ||
| - Brownfield `endpoint:` projects cannot eject greenfield IaC. | ||
| - Terraform cannot eject private networking until it reaches Bicep parity. | ||
| - Layer names and paths must not conflict. | ||
| - Paths must stay inside the project and cannot escape through symlinks. | ||
| - Module names cannot contain traversal, separators, or file extensions. | ||
| - Existing generated destinations are never overwritten. | ||
| - Files are generated in a temporary directory, installed atomically, and | ||
| removed if the `azure.yaml` update fails. | ||
|
|
||
| ## Key Decisions | ||
|
|
||
| ### 1. Layers instead of file merging | ||
|
|
||
| We preserve existing IaC as one layer and generate Foundry IaC as another. | ||
| File-level merging was rejected because deployment entry-point conflicts are | ||
| semantic, not only textual. | ||
|
|
||
| ### 2. Isolated resource-group ownership | ||
|
|
||
| The Foundry layer receives its own resource group instead of sharing | ||
| `AZURE_RESOURCE_GROUP`. This avoids Terraform import conflicts and destructive | ||
| cross-layer teardown. | ||
|
|
||
| ### 3. Fail closed on rerun | ||
|
|
||
| If generated files already exist, eject refuses rather than regenerating or | ||
| using `--force`. Once ejected, IaC is user-owned and may contain intentional | ||
| customizations. | ||
|
|
||
| ### 4. Preserve the simple project shape | ||
|
|
||
| Foundry-only projects continue using root `infra/`. Layers are introduced only | ||
| when composition requires them. | ||
|
|
||
| ## Scope | ||
|
|
||
| ### In scope | ||
|
|
||
| - Bicep and Terraform Foundry layers | ||
| - Migration from a root provider to `infra.layers` | ||
| - Appending to existing layered projects | ||
| - Path, module, provider, and file-conflict validation | ||
| - Isolated Foundry resource-group ownership | ||
|
|
||
| ### Out of scope | ||
|
|
||
| - Semantic Bicep/Terraform merging | ||
| - Overwriting or regenerating user-owned IaC | ||
| - Terraform private networking | ||
| - Brownfield IaC generation | ||
| - Merging a unified sample `azure.yaml` into an existing project | ||
| ([#8884](https://github.com/Azure/azure-dev/issues/8884)) | ||
|
|
||
| ## Rollout and Validation | ||
|
|
||
| Existing projects are unchanged until users explicitly run `--infra`. The | ||
| stable and alpha `azure.yaml` schemas add per-layer `provider` support. | ||
|
|
||
| Automated coverage includes: | ||
|
|
||
| - Root Bicep/Terraform migration | ||
| - Existing layered and custom-provider projects | ||
| - Existing `foundry` layer and folder conflicts | ||
| - Bicep/Terraform generation and complete non-interactive parameters | ||
| - Path traversal, symlink, module, brownfield, and networking failures | ||
| - Atomic install and rollback behavior | ||
| - Bicep compilation and agents/projects template parity | ||
|
|
||
| Before release, manually validate: | ||
|
|
||
| 1. Existing Bicep app -> eject -> provision -> `azd down foundry`. | ||
| 2. Existing Terraform app -> eject -> provision -> `azd down foundry`. | ||
| 3. Existing mixed-layer project -> eject -> targeted preview/provision. | ||
| 4. Foundry-only project -> root eject regression. | ||
| 5. Rerun after editing generated files -> non-destructive refusal. | ||
|
|
||
| ## Open Questions | ||
|
|
||
| 1. **Should rerun support regeneration?** Proposed: no, until there is an | ||
| explicit backup/diff workflow. | ||
| 2. **Should resource-group naming become an init flag?** Proposed: no; users | ||
| can edit generated parameters without expanding the init surface. | ||
| 3. **Should all agent projects use layers?** Proposed: no; introduce layers | ||
| only when composing with existing infrastructure. | ||
|
|
||
| ## References | ||
|
|
||
| - [Issue #9126](https://github.com/Azure/azure-dev/issues/9126) | ||
| - [Bicep-less/eject RFC #8065](https://github.com/Azure/azure-dev/issues/8065) | ||
| - [Layered provisioning PR #5492](https://github.com/Azure/azure-dev/pull/5492) | ||
| - [Unified manifest adoption boundary #8884](https://github.com/Azure/azure-dev/issues/8884) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
filelessis what's breaking cspell-lint on this PR. It isn't in.vscode/cspell.global.yaml, there's no override coveringdocs/specs/agent-init-infra-layers/, and the word doesn't appear anywhere else in the repo.The repo pattern for one-off terms is a file-scoped override in
.vscode/cspell.misc.yaml:Alternatively reword the row, if there's already an established term for a provider that ships no local templates.