diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..dfdb8b771
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+*.sh text eol=lf
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 000000000..2e3730df7
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,49 @@
+# See for instructions on this file https://help.github.com/articles/about-codeowners/
+
+#### Team-owned sample areas ############################################################################
+# Each contributing team owns their sample paths for review routing. See CONTRIBUTING.md for setup guide.
+/samples/python/hosted-agents/ @microsoft-foundry/hosted-agents
+/samples/csharp/hosted-agents/ @microsoft-foundry/hosted-agents
+
+#### files referenced in docs (DO NOT EDIT, except for Docs team!!!) ##########################################
+/infrastructure/infrastructure-setup-bicep/01-connections/connection-key-vault.bicep @microsoft-foundry/AI-Platform-Docs
+/infrastructure/infrastructure-setup-bicep/05-custom-policy-definitions/deny-disallowed-connections.json @microsoft-foundry/AI-Platform-Docs
+/infrastructure/infrastructure-setup-terraform/00-basic-azurerm/code/main.tf @microsoft-foundry/AI-Platform-Docs
+/infrastructure/infrastructure-setup-terraform/00-basic-azurerm/code/providers.tf @microsoft-foundry/AI-Platform-Docs
+/infrastructure/infrastructure-setup-terraform/00-basic-azurerm/code/variables.tf @microsoft-foundry/AI-Platform-Docs
+/infrastructure/infrastructure-setup-terraform/00-basic/code/main.tf @microsoft-foundry/AI-Platform-Docs
+/infrastructure/infrastructure-setup-terraform/00-basic/code/providers.tf @microsoft-foundry/AI-Platform-Docs
+/infrastructure/infrastructure-setup-terraform/00-basic/code/variables.tf @microsoft-foundry/AI-Platform-Docs
+/samples-classic/REST/quickstart/quickstart.sh @microsoft-foundry/AI-Platform-Docs
+/samples-classic/csharp/quickstart/Samples/.env.example @microsoft-foundry/AI-Platform-Docs
+/samples-classic/csharp/quickstart/Samples/AgentFileSearch.cs @microsoft-foundry/AI-Platform-Docs
+/samples-classic/csharp/quickstart/Samples/AgentService.cs @microsoft-foundry/AI-Platform-Docs
+/samples-classic/csharp/quickstart/Samples/SimpleInference.cs @microsoft-foundry/AI-Platform-Docs
+/samples-classic/java/quickstart/src/main/java/com/azure/ai/foundry/samples/AgentSample.java @microsoft-foundry/AI-Platform-Docs
+/samples-classic/java/quickstart/src/main/java/com/azure/ai/foundry/samples/ChatCompletionSample.java @microsoft-foundry/AI-Platform-Docs
+/samples-classic/java/quickstart/src/main/java/com/azure/ai/foundry/samples/FileSearchAgentSample.java @microsoft-foundry/AI-Platform-Docs
+/samples-classic/python/quickstart/create_project.py @microsoft-foundry/AI-Platform-Docs
+/samples-classic/python/quickstart/quickstart.py @microsoft-foundry/AI-Platform-Docs
+/samples-classic/typescript/quickstart/.env.template @microsoft-foundry/AI-Platform-Docs
+/samples-classic/typescript/quickstart/src/quickstart.ts @microsoft-foundry/AI-Platform-Docs
+/samples/REST/quickstart/quickstart-chat-with-agent.sh @microsoft-foundry/AI-Platform-Docs
+/samples/REST/quickstart/quickstart-create-agent.sh @microsoft-foundry/AI-Platform-Docs
+/samples/REST/quickstart/quickstart-responses.sh @microsoft-foundry/AI-Platform-Docs
+/samples/csharp/enterprise-agent-tutorial/1-idea-to-prototype/Evaluate/Program.cs @microsoft-foundry/AI-Platform-Docs
+/samples/csharp/enterprise-agent-tutorial/1-idea-to-prototype/ModernWorkplaceAssistant/Program.cs @microsoft-foundry/AI-Platform-Docs
+/samples/csharp/quickstart/chat-with-agent/quickstart-chat-with-agent.cs @microsoft-foundry/AI-Platform-Docs
+/samples/csharp/quickstart/create-agent/quickstart-create-agent.cs @microsoft-foundry/AI-Platform-Docs
+/samples/csharp/quickstart/responses/quickstart-responses.cs @microsoft-foundry/AI-Platform-Docs
+/samples/java/quickstart/chat-with-agent/src/main/java/com/azure/ai/agents/ChatWithAgent.java @microsoft-foundry/AI-Platform-Docs
+/samples/java/quickstart/create-agent/src/main/java/com/azure/ai/agents/CreateAgent.java @microsoft-foundry/AI-Platform-Docs
+/samples/python/enterprise-agent-tutorial/1-idea-to-prototype/evaluate.py @microsoft-foundry/AI-Platform-Docs
+/samples/python/enterprise-agent-tutorial/1-idea-to-prototype/main.py @microsoft-foundry/AI-Platform-Docs
+/samples/python/foundry-models/model-router/model-router-chat-completions.py @microsoft-foundry/AI-Platform-Docs
+/samples/python/foundry-models/model-router/model-router-foundry-responses.py @microsoft-foundry/AI-Platform-Docs
+/samples/python/quickstart/chat-with-agent/quickstart-chat-with-agent.py @microsoft-foundry/AI-Platform-Docs
+/samples/python/quickstart/create-agent/quickstart-create-agent.py @microsoft-foundry/AI-Platform-Docs
+/samples/python/quickstart/responses/quickstart-responses.py @microsoft-foundry/AI-Platform-Docs
+/samples/typescript/quickstart/chat-with-agent/src/quickstart-chat-with-agent.ts @microsoft-foundry/AI-Platform-Docs
+/samples/typescript/quickstart/create-agent/src/quickstart-create-agent.ts @microsoft-foundry/AI-Platform-Docs
+/samples/typescript/quickstart/responses/src/quickstart-responses.ts @microsoft-foundry/AI-Platform-Docs
+
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 000000000..4e306b3cd
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,12 @@
+## Files owned by the AI Platform Docs team
+
+If a file is listed in the CODEOWNERS file with @azure-ai-foundry/ai-platform-docs as the owner, it is owned by the AI Platform Docs team. For these files:
+
+- Do not change the filename or move the file.
+- Do not remove any comments which contain or (for any text in between the tags)
+- Do not remove any cell in a notebook if it contains metadata with "name:" in it.
+
+In a code review, if any of the above rules are broken, please add the following text to your review:
+πSTOP! This PR contains changes that may break documentation. Please post a message on [ai-platform-docs](https://teams.microsoft.com/l/team/19%3AHhf4F_YfPn3kYGdmWvePNwlbF5-RR8wciQEUwwrcggw1%40thread.tacv2/conversations?groupId=fdaf4412-8993-4ea6-a7d4-aeaded7fc854&tenantId=72f988bf-86f1-41af-91ab-2d7cd011db47) to request help.
+
+Only files owned by the AI Platform Docs team are subject to these rules.
diff --git a/.github/scripts/commit-filesize-diff-summary.py b/.github/scripts/commit-filesize-diff-summary.py
new file mode 100644
index 000000000..075bc93fa
--- /dev/null
+++ b/.github/scripts/commit-filesize-diff-summary.py
@@ -0,0 +1,211 @@
+#!/usr/bin/env python3
+import argparse
+import subprocess
+import sys
+from dataclasses import dataclass
+from itertools import chain
+from pathlib import Path
+from typing import Dict, Iterable, List, Literal, Optional, Tuple
+
+
+@dataclass
+class GitDiffTreeRecord:
+ """Represents a line of output from 'git diff-tree'"""
+
+ src_mode: str
+ src_hash: str
+ dst_mode: str
+ dst_hash: str
+ src_path: Path
+ dst_path: Optional[Path]
+ status: Literal["A", "C", "D", "M", "R", "T", "U", "X"]
+ score: Optional[int]
+
+
+@dataclass
+class GitChange:
+ diff_record: GitDiffTreeRecord
+ bytes_changed: int
+
+
+def parse_git_diff_tree_output(output: str) -> List[GitDiffTreeRecord]:
+ """Parses the output of `git diff-tree` as described in the "Raw Output" section
+ of the man page
+ """
+
+ def make_record(line: str) -> GitDiffTreeRecord:
+ src_mode, dst_mode, src_hash, dst_hash, rest = line[1:].split(" ", maxsplit=4)
+ status_score_and_paths = rest.split("\t")
+ return GitDiffTreeRecord(
+ src_mode=src_mode,
+ src_hash=src_hash,
+ dst_mode=dst_mode,
+ dst_hash=dst_hash,
+ status=status_score_and_paths[0][0],
+ score=int(status_score_and_paths[0][1:]) if len(status_score_and_paths[0]) > 1 else None,
+ src_path=Path(status_score_and_paths[1]),
+ dst_path=Path(status_score_and_paths[2]) if len(status_score_and_paths) >= 3 else None,
+ )
+
+ return [make_record(line) for line in output.splitlines(keepends=False)]
+
+
+def get_blob_sizes(hashes: Iterable[str]) -> Dict[str, Optional[int]]:
+ """Fetches the sizes, in bytes, of git blobs
+
+ :param hashes: A iterable of git blob hashes
+ :type hashes: Iterable[str]
+
+ :return: A dictionary that mapping blob hashes to their size if the blob exists,
+ or None otherwise
+ :rtype: Dict[str, Optional[int]]
+ """
+ input = "\n".join(set(hashes))
+ cat_file_output = subprocess.run(
+ ["git", "cat-file", "--batch-check"],
+ input=input,
+ check=True,
+ text=True,
+ capture_output=True,
+ ).stdout
+
+ def make_object_size_tuple(line: str) -> Tuple[str, Optional[int]]:
+ hash, *_, size = line.split()
+ return (hash, int(size) if size != "missing" else None)
+
+ return dict(make_object_size_tuple(line) for line in cat_file_output.splitlines(keepends=False))
+
+
+def get_file_size_differences(commit_range: str) -> Dict[Path, GitChange]:
+ """Computes the size difference, in bytes, of files changed between two commits
+
+ :param commit_range: A git commit range (e.g. HEAD~3..HEAD)
+ :type commit_range: str
+
+ :return: A dictionary mapping paths (relative to repository root) to size
+ differences.
+ :rtype: dict[Path, GitChange]
+ """
+ changed_records = parse_git_diff_tree_output(
+ subprocess.run(
+ ["git", "diff-tree", "-r", commit_range],
+ capture_output=True,
+ text=True,
+ check=True,
+ ).stdout
+ )
+
+ sizes = get_blob_sizes(chain.from_iterable((idx.src_hash, idx.dst_hash) for idx in changed_records))
+
+ assert {"A", "D", "M"}.issuperset(idx.status for idx in changed_records)
+
+ def as_int(maybe_num: Optional[int]) -> int:
+ return maybe_num or 0
+
+ return {
+ x.src_path: GitChange(
+ diff_record=x,
+ bytes_changed=as_int(sizes[x.dst_hash]) - as_int(sizes[x.src_hash]),
+ )
+ for x in changed_records
+ }
+
+
+def main(
+ commit_range: str,
+ quiet: bool = False,
+ limit: Optional[int] = None,
+ show_n_largest_files: int = 30,
+) -> Literal[0, 1]:
+ size_differences = get_file_size_differences(commit_range)
+ cumulative_size_difference = sum(x.bytes_changed for x in size_differences.values())
+ exceeds_limit = limit is not None and cumulative_size_difference > limit
+
+ def bytes_diff(num: int) -> str:
+ return ("+" if num >= 0 else "") + human_friendly_bytes(num)
+
+ if not quiet:
+ print(f"Total file size difference for commit range '{commit_range}': ")
+ print(f"\t{bytes_diff(cumulative_size_difference)}", end="")
+ print(f" (Exceeds set limit of {bytes_diff(limit)})" if exceeds_limit else "")
+
+ largest_n_sizes = sorted(size_differences.items(), key=lambda x: x[1].bytes_changed, reverse=True)[
+ :show_n_largest_files
+ ]
+
+ if largest_n_sizes:
+ print("")
+ print(f"Largest {len(largest_n_sizes)} filesize differences:")
+
+ for path, val in largest_n_sizes:
+ print(f"\t{bytes_diff(val.bytes_changed)}\t{path}")
+
+ return 1 if exceeds_limit else 0
+
+
+def num_bytes(arg: str) -> int:
+ """Converts a string to a number of bytes"""
+ error = argparse.ArgumentTypeError(f"'{arg}' cannot be parsed into a number of bytes")
+ try:
+ return int(arg)
+ except ValueError:
+ pass
+
+ if len(arg) < 3:
+ raise error
+
+ num, suffix = (arg[:-2], arg[-2:])
+ shift_values = {
+ "KB": 1,
+ "MB": 2,
+ "GB": 3,
+ "TB": 4,
+ "PB": 5,
+ "EB": 6,
+ "ZB": 7,
+ "YB": 8,
+ }
+
+ shift = shift_values.get(suffix)
+
+ if shift is None:
+ raise error
+ try:
+ return int(num) << (shift * 10)
+ except ValueError as e:
+ raise error from e
+
+
+def human_friendly_bytes(num: int) -> str:
+ """Prints a number of bytes as a human friendly string"""
+ for prefix in ["", "K", "M", "G", "T", "P", "E", "Z"]:
+ if abs(num) < 1024.0:
+ return f"{num:.1f}{prefix}B"
+ num /= 1024.0
+ return f"{num:.1f}YB"
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(
+ description="A program that summarizes the file size " + "differences between two git commits."
+ )
+ parser.add_argument(
+ "commit_range",
+ default="HEAD^..HEAD",
+ type=str,
+ help="Commit range to commit the diff for (e.g. HEAD~3..HEAD)",
+ )
+ parser.add_argument("--quiet", action="store_true", help="Silence all output")
+ parser.add_argument(
+ "--limit",
+ type=num_bytes,
+ help="Exit non-zero if total changes exceeds this value. "
+ + "Can be a raw number of bytes (e.g. 65536) or a suffixed value (e.g 2MB)",
+ )
+ parser.add_argument(
+ "--show-n-largest-files",
+ type=int,
+ help="Show this many of the largest files in diff",
+ default=30,
+ )
+ sys.exit(main(**vars(parser.parse_args())))
diff --git a/.github/workflows/ado-automation.yml b/.github/workflows/ado-automation.yml
new file mode 100644
index 000000000..52a5d9931
--- /dev/null
+++ b/.github/workflows/ado-automation.yml
@@ -0,0 +1,52 @@
+name: Create ADO user story from GitHub issue
+run-name: GitHub Issue #${{ github.event.issue.number }}
+
+on:
+ issues:
+ types: [opened]
+
+jobs:
+ create-ado-story:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Build payload and call Azure DevOps
+ env:
+ ADO_ORG: ${{ secrets.ADO_ORG }}
+ ADO_PROJECT: ${{ secrets.ADO_PROJECT }}
+ ADO_PAT: ${{ secrets.ADO_PAT }}
+ DRI_EMAIL: ${{ secrets.DRI_EMAIL }}
+ ADO_AREA_PATH: ${{ secrets.ADO_AREA_PATH }}
+ ADO_ITERATION_PATH: ${{ secrets.ADO_ITERATION_PATH }}
+ ADO_TAG: ${{ secrets.ADO_TAG }}
+ ISSUE_TITLE: ${{ github.event.issue.title }}
+ ISSUE_BODY: ${{ github.event.issue.body }}
+ ISSUE_URL: ${{ github.event.issue.html_url }}
+ run: |
+ DESCRIPTION="
"
+
+ jq -n \
+ --arg title "$ISSUE_TITLE" \
+ --arg desc "$DESCRIPTION" \
+ --arg area "$ADO_AREA_PATH" \
+ --arg iter "$ADO_ITERATION_PATH" \
+ --arg assn "$DRI_EMAIL" \
+ --arg tags "$ADO_TAG" \
+ '[
+ { "op":"add", "path":"/fields/System.Title", "value":$title },
+ { "op":"add", "path":"/fields/System.Description", "value":$desc },
+ { "op":"add", "path":"/fields/System.AreaPath", "value":$area },
+ { "op":"add", "path":"/fields/System.IterationPath", "value":$iter },
+ { "op":"add", "path":"/fields/System.AssignedTo", "value":$assn },
+ { "op":"add", "path":"/fields/System.Tags", "value":$tags }
+ ]' > /tmp/payload.json
+
+ AUTH=$(printf ":$ADO_PAT" | base64 | tr -d '\n')
+ WORK_ITEM_URL="https://dev.azure.com/${ADO_ORG}/${ADO_PROJECT}/_apis/wit/workitems/\$User%20Story?api-version=7.1-preview.3"
+
+ curl --fail-with-body -sS \
+ -H "Content-Type: application/json-patch+json" \
+ -H "Authorization: Basic $AUTH" \
+ -X POST \
+ --data-binary @/tmp/payload.json \
+ "$WORK_ITEM_URL"
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
new file mode 100644
index 000000000..466da19c6
--- /dev/null
+++ b/.github/workflows/pre-commit.yml
@@ -0,0 +1,23 @@
+name: Pre-Commit
+
+
+on:
+ # push:
+ # branches:
+ # - main
+ # pull_request:
+ # branches:
+ # - main
+ workflow_dispatch:
+
+jobs:
+ pre-commit:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-python@v4
+ with:
+ python-version: "3.9"
+ - run: pip install -r dev-requirements.txt
+ - name: Run Pre-Commit
+ run: pre-commit run --all-files
diff --git a/.github/workflows/pull-request-checks.yml b/.github/workflows/pull-request-checks.yml
new file mode 100644
index 000000000..79ba913c7
--- /dev/null
+++ b/.github/workflows/pull-request-checks.yml
@@ -0,0 +1,24 @@
+name: Pull Request Checks
+
+on:
+ # push:
+ # branches:
+ # - main
+ # pull_request:
+ # branches:
+ # - main
+ workflow_dispatch:
+
+jobs:
+ pull_request_size:
+ if: github.event_name == 'pull_request'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-python@v4
+ with:
+ python-version: "3.9"
+ - name: Check Pull Request Size
+ run: |
+ git fetch origin ${{ github.event.pull_request.base.ref }} --quiet # Need to manually fetch base branch in CI
+ python ./.github/scripts/commit-filesize-diff-summary.py --limit 1MB origin/${{ github.event.pull_request.base.ref }}..HEAD
diff --git a/.github/workflows/redirect-pull-requests.yml b/.github/workflows/redirect-pull-requests.yml
new file mode 100644
index 000000000..390cf9163
--- /dev/null
+++ b/.github/workflows/redirect-pull-requests.yml
@@ -0,0 +1,132 @@
+name: Redirect Pull Requests
+
+on:
+ pull_request_target:
+ types: [opened]
+
+permissions:
+ pull-requests: write
+
+jobs:
+ redirect:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check org membership and redirect
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const pr = context.payload.pull_request;
+ const author = pr.user.login;
+
+ // Allow PRs from trusted automation bots (e.g., repo sync)
+ const allowedBots = ['foundry-samples-repo-sync[bot]'];
+ if (allowedBots.includes(author)) {
+ console.log(`Skipping redirect for allowed bot: ${author}`);
+ return;
+ }
+
+ // Check if author is a Microsoft contributor using multiple signals.
+ // The GITHUB_TOKEN can only see *public* members of the 'microsoft' org
+ // (since this repo is in the 'microsoft-foundry' org), so we cascade
+ // through several checks to catch contributors with private membership.
+ let isInternal = false;
+ let matchedSignal = null;
+
+ // Signal 1: Check microsoft-foundry org membership (full visibility via GITHUB_TOKEN)
+ try {
+ const res = await github.rest.orgs.checkMembershipForUser({
+ org: 'microsoft-foundry',
+ username: author,
+ });
+ if (res.status === 204) {
+ isInternal = true;
+ matchedSignal = 'microsoft-foundry org member';
+ }
+ } catch {
+ // 404 or 302 means not a member
+ }
+
+ // Signal 2: Check if author is a collaborator on this repo
+ if (!isInternal) {
+ try {
+ const res = await github.rest.repos.checkCollaborator({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ username: author,
+ });
+ if (res.status === 204) {
+ isInternal = true;
+ matchedSignal = 'repo collaborator';
+ }
+ } catch {
+ // 404 means not a collaborator
+ }
+ }
+
+ // Signal 3: Check microsoft org membership (catches public members only)
+ if (!isInternal) {
+ try {
+ const res = await github.rest.orgs.checkMembershipForUser({
+ org: 'microsoft',
+ username: author,
+ });
+ if (res.status === 204) {
+ isInternal = true;
+ matchedSignal = 'microsoft org member (public)';
+ }
+ } catch {
+ // 404 or 302 means not a member (or private membership not visible)
+ }
+ }
+
+ console.log(`Author: ${author}, isInternal: ${isInternal}, signal: ${matchedSignal || 'none'}`);
+
+ let body;
+ if (isInternal) {
+ body = [
+ `π Thanks for your contribution, @${author}!`,
+ '',
+ 'This repository is read-only. As a Microsoft contributor, please submit your PR to the private staging repository instead:',
+ '',
+ 'π **[foundry-samples-pr](https://github.com/microsoft-foundry/foundry-samples-pr)**',
+ '',
+ 'See [CONTRIBUTING.md](https://github.com/microsoft-foundry/foundry-samples/blob/main/CONTRIBUTING.md) for full instructions.',
+ ].join('\n');
+ } else {
+ body = [
+ `π Thanks for your interest in contributing, @${author}!`,
+ '',
+ 'This repository does not accept pull requests directly. If you\'d like to report a bug, suggest an improvement, or propose a new sample, please **[open an issue](https://github.com/microsoft-foundry/foundry-samples/issues/new)** instead.',
+ '',
+ 'See [CONTRIBUTING.md](https://github.com/microsoft-foundry/foundry-samples/blob/main/CONTRIBUTING.md) for more details.',
+ ].join('\n');
+ }
+
+ // Skip if the bot already commented (idempotent on re-runs)
+ const comments = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number,
+ });
+ const alreadyCommented = comments.data.some(c =>
+ c.user.login === 'github-actions[bot]' &&
+ c.body.includes('This repository')
+ );
+ if (alreadyCommented) {
+ console.log('Bot already commented on this PR, skipping.');
+ return;
+ }
+
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number,
+ body,
+ });
+
+ await github.rest.pulls.update({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: pr.number,
+ state: 'closed',
+ });
diff --git a/.github/workflows/run-setup.yml b/.github/workflows/run-setup.yml
new file mode 100644
index 000000000..f045900a1
--- /dev/null
+++ b/.github/workflows/run-setup.yml
@@ -0,0 +1,135 @@
+name: Run Setup
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - infrastructure/infrastructure-setup-bicep/**
+ pull_request:
+ branches: [main]
+ paths:
+ - infrastructure/infrastructure-setup-bicep/**
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+jobs:
+ run-setup:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout source branch
+ uses: actions/checkout@v3
+ with:
+ # PR: checks out the PR branch, Push: checks out main, Dispatch: checks out default branch
+ ref: ${{ github.head_ref || github.ref_name }}
+ fetch-depth: 0
+
+ - name: Install Bicep
+ run: |
+ INSTALL_PATH="$RUNNER_TEMP/bicep"
+ BICEP_PATH="$RUNNER_TEMP/bicep/bicep"
+ mkdir -p "$INSTALL_PATH"
+ curl -sLo bicep https://github.com/Azure/bicep/releases/latest/download/bicep-linux-x64
+ chmod +x ./bicep
+ sudo mv ./bicep "$INSTALL_PATH"
+ echo "BICEP_PATH=$BICEP_PATH" >> $GITHUB_ENV
+ $BICEP_PATH --version
+
+ - name: Determine changed main.bicep files
+ id: changes
+ run: |
+ set -e
+ cd "$GITHUB_WORKSPACE"
+
+ EVENT="${{ github.event_name }}"
+ echo "Event: $EVENT"
+
+ if [ "$EVENT" = "pull_request" ]; then
+ BASE="${{ github.event.pull_request.base.sha }}"
+ HEAD="${{ github.event.pull_request.head.sha }}"
+ elif [ "$EVENT" = "push" ]; then
+ BASE="${{ github.event.before }}"
+ HEAD="${{ github.sha }}"
+ else
+ # workflow_dispatch: use last commit as best-effort
+ BASE="$(git rev-parse HEAD~1 || echo '')"
+ HEAD="$(git rev-parse HEAD)"
+ fi
+
+ echo "Diff range: ${BASE}..${HEAD}"
+
+ # Only rebuild when main.bicep changes
+ if [ -n "$BASE" ]; then
+ MODIFIED=$(git diff --name-only "$BASE" "$HEAD" \
+ | grep -E "^infrastructure/infrastructure-setup-bicep/.*/main\.bicep$" || true)
+ else
+ MODIFIED=$(git show --name-only --pretty="" -1 \
+ | grep -E "^infrastructure/infrastructure-setup-bicep/.*/main\.bicep$" || true)
+ fi
+
+ if [ -z "$MODIFIED" ]; then
+ echo "No relevant Bicep changes detected."
+ echo "files=" >> $GITHUB_OUTPUT
+ exit 0
+ fi
+
+ echo "Changed main.bicep files:"
+ echo "$MODIFIED"
+
+ # Output as newline-delimited list
+ {
+ echo "files<> $GITHUB_OUTPUT
+
+ - name: Build changed Bicep files -> azuredeploy.json
+ if: steps.changes.outputs.files != ''
+ run: |
+ set -e
+ cd "$GITHUB_WORKSPACE"
+
+ while IFS= read -r BICEP_FILE; do
+ OUTFILE="$(dirname "$BICEP_FILE")/azuredeploy.json"
+ echo "Building: $BICEP_FILE -> $OUTFILE"
+ $BICEP_PATH build "$BICEP_FILE" --outfile "$OUTFILE"
+ done <<< "${{ steps.changes.outputs.files }}"
+
+ - name: Commit + push changes back to branch (PR) or main (push)
+ if: always()
+ run: |
+ set -e
+ cd "$GITHUB_WORKSPACE"
+
+ git config --global user.email "foundry-samples@noreply.github.com"
+ git config --global user.name "foundry-samples automation"
+
+ git add -A
+
+ if git diff-index --quiet HEAD --; then
+ echo "No changes to commit."
+ exit 0
+ fi
+
+ git commit -m "Automatic fixes"
+
+ EVENT="${{ github.event_name }}"
+
+ # If PR is from a fork, pushing will be rejected. Detect and skip.
+ if [ "$EVENT" = "pull_request" ]; then
+ if [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
+ echo "PR is from a fork; cannot push changes back to fork branch. Skipping push."
+ exit 0
+ fi
+ BRANCH="${{ github.head_ref }}"
+ echo "Pushing fixes to PR branch: $BRANCH"
+ git push origin "HEAD:refs/heads/$BRANCH"
+ exit 0
+ fi
+
+ # push / workflow_dispatch
+ BRANCH="${{ github.ref_name }}"
+ echo "Pushing fixes to branch: $BRANCH"
+ git push origin "HEAD:refs/heads/$BRANCH"
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index ece0ddaab..79cfdf7cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,6 +38,9 @@ bld/
# Visual Studio 2015/2017 cache/options directory
.vs/
+
+# Language server cache files
+*.lscache
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
@@ -464,3 +467,11 @@ samples/microsoft/developer-journey-stage-1-idea-to-prototype.md
# oh-my-claudecode local state
.omc/
+
+# Generated cloud E2E artifacts
+logs_latest/
+logs_maf/
+logs_new/
+
+# Playwright MCP browser session snapshots
+.playwright-mcp/
diff --git a/.infra/pytest_plugins/changed_samples/build/lib/pytest_changed_samples/plugin.py b/.infra/pytest_plugins/changed_samples/build/lib/pytest_changed_samples/plugin.py
index 02900aa81..977fc3261 100644
--- a/.infra/pytest_plugins/changed_samples/build/lib/pytest_changed_samples/plugin.py
+++ b/.infra/pytest_plugins/changed_samples/build/lib/pytest_changed_samples/plugin.py
@@ -42,9 +42,7 @@ def pytest_configure(config: pytest.Config) -> None:
# Validate that mutually exclusive options haven't been provided
mutually_exclusive_options = (WORKING_TREE_CHANGES_OPTION, PR_CHANGES_OPTION)
if sum(bool(config.getoption(opt_var(o))) for o in mutually_exclusive_options) > 1:
- raise pytest.UsageError(
- f"{' and '.join(mutually_exclusive_options)} are mutually exclusive"
- )
+ raise pytest.UsageError(f"{' and '.join(mutually_exclusive_options)} are mutually exclusive")
@pytest.hookimpl(hookwrapper=True)
@@ -70,9 +68,7 @@ def pytest_collection(session: pytest.Session) -> None:
del config.stash[DIFF_PATH_TRIE_KEY]
-def pytest_ignore_collect(
- collection_path: Path, config: pytest.Config
-) -> Optional[bool]:
+def pytest_ignore_collect(collection_path: Path, config: pytest.Config) -> Optional[bool]:
"""Ignore paths that were not touched by the current git diff."""
if DIFF_PATH_TRIE_KEY not in config.stash:
# Occurs when calling `pytest --fixtures`
@@ -99,9 +95,7 @@ def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
session.exitstatus = pytest.ExitCode.OK
-def get_diff_paths_function(
- config: pytest.Config,
-) -> Optional[Callable[[], Iterable[Path]]]:
+def get_diff_paths_function(config: pytest.Config) -> Optional[Callable[[], Iterable[Path]]]:
"""Get the function that returns paths present in a diff specfied by cmdline arguments
:param pytest.Config config: The pytest config
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..e320f1aaa
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,35 @@
+# Contributing to Microsoft Foundry Samples
+
+This repository contains official Microsoft Foundry documentation samples. The contents are published automatically from a private staging repository and are not edited directly here.
+
+## Reporting Issues
+
+If you find a bug, have a question, or want to suggest an improvement to an existing sample, please [open an issue](https://github.com/microsoft-foundry/foundry-samples/issues/new) on this repository. We welcome feedback from everyone!
+
+## Contributing Changes (Microsoft Contributors)
+
+Sample contributions are currently limited to Microsoft Foundry teams.
+
+All changes β new samples, updates, and bug fixes β are submitted through the private staging repository [`foundry-samples-pr`](https://github.com/microsoft-foundry/foundry-samples-pr). Changes merged there are automatically synced to this public repository on a nightly basis.
+
+> [!NOTE]
+> The link above will return a **404** until you've completed step 1 below.
+
+### How to get started
+
+1. **Join the `microsoft-foundry` GitHub organization.** Navigate to the organization page on the Open Source Management Portal and click **Join**:
+
+
+
+2. **Access the staging repository.** Once you've joined the org, you'll be able to view [`foundry-samples-pr`](https://github.com/microsoft-foundry/foundry-samples-pr).
+
+3. **Follow the contributing guide there.** The `foundry-samples-pr` repository has its own [`CONTRIBUTING.md`](https://github.com/microsoft-foundry/foundry-samples-pr/blob/main/CONTRIBUTING.md) with full instructions for setting up write access, creating a branch, and submitting a pull request.
+
+## Contributor License Agreement
+
+This project requires a Contributor License Agreement (CLA). When you submit a pull request, a CLA bot will check whether you need to sign one and guide you through the process. You only need to do this once across all Microsoft repos. For details, visit .
+
+## Code of Conduct
+
+This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com).
+
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..3ca3252ff
--- /dev/null
+++ b/README.md
@@ -0,0 +1,14 @@
+# Microsoft Foundry Documentation Samples
+
+This repository acts as the top-level directory for official Microsoft Foundry documentation sample code and examples. It includes notebooks and sample code that contain end-to-end examples as well as smaller code snippets for common developer tasks.
+
+This repository is entirely open source, guidance on how to contribute and links to additional repositories are provided below.
+
+Use the samples in this repository to try out Microsoft Foundry scenarios on your local machine!
+
+## Contributing
+
+Found a bug or have a suggestion? [Open an issue](https://github.com/microsoft-foundry/foundry-samples/issues/new) β we welcome feedback from everyone!
+
+Sample contributions are submitted through a private staging repository. If you're a Microsoft employee or contractor, see the [contributing guidelines](CONTRIBUTING.md) for how to get started.
+
diff --git a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/README.md b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/README.md
new file mode 100644
index 000000000..9edae6c1a
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/README.md
@@ -0,0 +1,424 @@
+---
+description: This template demonstrates how to set up Azure AI Foundry with a basic agent configuration using VNet injection for network isolation.
+page_type: sample
+products:
+- azure
+- azure-resource-manager
+urlFragment: network-secured-basic-agent-vnet
+languages:
+- bicep
+- json
+---
+
+# Microsoft Foundry: Basic Agent Setup with E2E Network Isolation (without Tools behind VNET)
+
+> **NEW**
+> For support on deploying the right network isolation template, check out the [GitHub Copilot for Azure skill for private networking](https://github.com/microsoft/GitHub-Copilot-for-Azure/blob/main/plugin/skills/microsoft-foundry/resource/private-network/private-network.md) set-up!
+
+---
+
+## Overview
+
+This infrastructure-as-code (IaC) solution deploys a **basic agent** environment with **VNet injection** for network isolation. Unlike the [standard agent setup (template 15)](../15-private-network-standard-agent-setup), this template does **not** create or connect BYO (Bring Your Own) resources such as Azure AI Search, Azure Storage Account, or Azure Cosmos DB. Instead, it relies on platform-managed resources for agent storage needs.
+
+This template combines:
+- **Basic agent configuration** β No BYO resources, no connections to external search/storage/cosmos services
+- **VNet injection** β Custom virtual network with subnet delegation for agent workloads (`Microsoft.App/environments`)
+- **Private endpoints** β Network-secured access to the AI Services account only
+- **Capability host** β Basic agent capability host without BYO connections
+
+### What Gets Deployed
+
+| Resource | Purpose |
+|----------|---------|
+| **Virtual Network** | Network isolation with two subnets |
+| **Agent Subnet** | Delegated to `Microsoft.App/environments` for VNet-injected agent workloads |
+| **Private Endpoint Subnet** | Hosts private endpoint for the AI Services account |
+| **AI Services Account** | AI Foundry account with network injection, public access disabled |
+| **Private Endpoint** | Secure private connectivity to AI Services |
+| **Private DNS Zones** (x3) | DNS resolution for `privatelink.services.ai.azure.com`, `privatelink.openai.azure.com`, `privatelink.cognitiveservices.azure.com` |
+| **AI Foundry Project** | Project with system-assigned managed identity |
+| **Capability Host** | Basic agent capability host (platform-managed storage) |
+| **Model Deployment** | gpt-4.1 (configurable) |
+
+
+[](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fazure-ai-foundry%2Ffoundry-samples%2Frefs%2Fheads%2Fmain%2Finfrastructure%2Finfrastructure-setup-bicep%2F11-private-network-basic-vnet%2Fmain.json)
+
+---
+
+## When to Use This Template
+
+Use this template when you need:
+- **VNet injection for network isolation** β Agent workloads run inside your own virtual network with subnet delegation
+- **Basic agent setup without BYO resources** β Platform-managed storage for agent data (no customer-managed Storage, Cosmos DB, or AI Search)
+- **Private endpoint access to AI Services** β Secure, internal-only connectivity to the Foundry account
+- **System Assigned Managed Identity** β Simplified identity management with platform-managed credentials
+
+### Template Decision Guide
+
+Use the table below to choose the right infrastructure template for your scenario:
+
+| Template | Agent Type | Networking | Identity | Key Use Case |
+|----------|-----------|------------|----------|-------------|
+| [**11** (this template)](../11-private-network-basic-vnet/) | **Basic** (platform-managed) | BYO VNet injection | System Assigned MI | Basic agents with VNet isolation β no BYO resources needed |
+| [**15**](../15-private-network-standard-agent-setup/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | E2E network isolation with full agent capabilities |
+| [**19**](../19-private-network-agent-tools/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | Same as 15 **plus** tools behind VNet (MCP, OpenAPI, Functions, A2A) |
+| [**17**](../17-private-network-standard-user-assigned-identity-agent-setup/) | Standard (BYO resources) | BYO VNet + Private Endpoints | **User Assigned MI** | Same as 15 but with user-managed identity |
+| [**16**](../16-private-network-standard-agent-apim-setup-preview/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | Same as 15 **plus** private APIM integration (preview) |
+| [**18**](../18-managed-virtual-network-preview/) | Standard (BYO resources) | **Managed VNet** (Microsoft-managed) | System Assigned MI | Network isolation without managing your own VNet (preview) |
+| [**15a**](../15a-private-network-evaluation-only-setup/) | Evaluation only | BYO VNet + Private Endpoints | System Assigned MI | Minimal setup for evaluation β no Cosmos DB, AI Search, or capability host |
+| [**41**](../41-standard-agent-setup/) | Standard (BYO resources) | **Public** (no VNet) | System Assigned MI | Standard agents without network isolation |
+| [**40**](../40-basic-agent-setup/) | **Basic** (platform-managed) | **Public** (no VNet) | System Assigned MI | Simplest setup β no BYO resources, no private networking |
+
+---
+
+## Key Information
+
+**Region and Resource Placement Requirements**
+- **All Foundry workspace resources should be in the same region as the VNet**, including the Foundry Account and Project. The only exception is within the Foundry Account, you may choose to deploy your model to a different region, and any cross-region communication will be handled securely within our network infrastructure.
+ - **Note:** Your Virtual Network can be in a different resource group than your Foundry workspace resources
+
+---
+
+## Prerequisites
+
+1. **Active Azure subscription with appropriate permissions**
+ - **Foundry Account Owner**: Needed to create the Azure AI Foundry account and project.
+ - **Owner or Role Based Access Administrator**: Needed to assign RBAC on the Azure resources used by this template.
+ - **Foundry User**: Needed to create and use agents, projects, or evaluation workloads after deployment.
+
+2. **Register Resource Providers**
+
+ Make sure you have an active Azure subscription that allows registering resource providers. Subnet delegation requires the Microsoft.App provider to be registered in your subscription. If it's not already registered, run the commands below:
+
+ ```bash
+ az provider register --namespace 'Microsoft.CognitiveServices'
+ az provider register --namespace 'Microsoft.Network'
+ az provider register --namespace 'Microsoft.App'
+ ```
+
+3. Network administrator permissions (if operating in a restricted or enterprise environment)
+
+4. Sufficient quota for all resources required by this template in the target Azure region, including model deployment quota.
+
+5. Azure CLI installed and configured on your local workstation or deployment pipeline server
+
+---
+
+## Pre-Deployment Steps
+
+### Networking Requirements
+
+1. Review network requirements and plan Virtual Network address space (e.g., `192.168.0.0/16` or an alternative non-overlapping address space)
+
+2. Two subnets are needed:
+ - **Agent Subnet** (e.g., `192.168.0.0/24`): Hosts Agent client for Agent workloads, delegated to `Microsoft.App/environments`. The recommended size should be `/24` for this delegated subnet.
+ - **Private Endpoint Subnet** (e.g., `192.168.1.0/24`): Hosts private endpoints for the AI Services account
+ - Ensure that the address spaces for the used VNET does not overlap with any existing networks in your Azure environment or reserved IP ranges like the following: `169.254.0.0/16, 172.30.0.0/16, 172.31.0.0/16, 192.0.2.0/24, 0.0.0.0/8, 127.0.0.0/8, 100.100.0.0/17, 100.100.192.0/19, 100.100.224.0/19, 100.64.0.0/11`. This includes all address space(s) you have in your VNET if you have more than one, and peered VNETs.
+
+ > **Notes:**
+ - If you do not provide an existing virtual network, the template will create a new virtual network with the default address spaces and subnets described above. If you use an existing virtual network, make sure it already contains two subnets (Agent and Private Endpoint) before deploying the template.
+ - You must ensure the Foundry account was successfully created so that underlying caphost has also succeeded. Then proceed to deploying the project caphost bicep.
+ - You must ensure the subnet is exclusively delegated to __Microsoft.App/environments__ and cannot be used by any other Azure resources.
+
+### Limitations / Known Issues
+
+1. The delegated agent subnet must be exclusively used by a single Foundry account. It cannot be shared across accounts.
+2. The Foundry resource and the virtual network must be in the same Azure region.
+3. Private Class A IP address ranges (10.x.x.x) are only supported in the following regions: **Australia East, Brazil South, Canada East, East US, East US 2, France Central, Germany West Central, Italy North, Japan East, South Africa North, South Central US, South India, Spain Central, Sweden Central, UAE North, UK South, West US, West US 3.**. Use Class B (172.16.x.x) or C (192.168.x.x) ranges for other regions.
+4. This template uses platform-managed resources for agent storage. If you need customer-managed Storage, Cosmos DB, or AI Search, use [template 15](../15-private-network-standard-agent-setup/) instead.
+5. There is no upgrade path from BYO VNet (this template) to Managed Virtual Network (template 18). A Foundry resource redeployment is required.
+6. All projects within the same Foundry account share model deployments. Per-project model isolation is not supported.
+
+### Account Deletion Prerequisites and Cleanup Guidance
+
+Before deleting an **Account** resource, it is essential to first delete the associated **Account Capability Host**. Failure to do so may result in residual dependenciesβsuch as subnets and other provisioned resources (e.g., ACA applications)βremaining linked to the capability host. This can lead to errors such as **"Subnet already in use"** when attempting to reuse the same subnet in a different account deployment.
+
+**Cleanup Options**
+
+**1. Full Account Removal**: To completely remove an account, you must delete and purge the account. Simply deleting the account is not sufficient, you must purge so that deletion of the associated capability host is triggered. The service will automatically handle the removal of the capability host and any linked resources in the background. To purge the account, use the following [link](https://learn.microsoft.com/en-us/azure/ai-services/recover-purge-resources?tabs=azure-portal#purge-a-deleted-resource). Please allow approximately max of 20 minutes for all resources to be fully unlinked from the account.
+
+**2. Retain Account, Remove Capability Host**: If you intend to retain the account but remove the capability host, execute the script `deleteCaphost.sh` located in the [template 15 folder](../15-private-network-standard-agent-setup/). After deletion, allow approximately max of 20 minutes for all resources to be fully unlinked from the account. To recreate the capability host for the account, use the script `createCaphost.sh` located in the same folder.
+
+> **Important**: Before deleting the account capability host, ensure that the **project capability host** is deleted.
+
+### Template Customization
+
+#### Parameters
+
+| Parameter | Description | Default | Required |
+|-----------|-------------|---------|----------|
+| `location` | Azure region for deployment | `eastus` | Yes |
+| `aiServices` | Base name for the AI Services resource | `aiservices` | No |
+| `firstProjectName` | Name for the Foundry project | `project` | No |
+| `modelName` | Model to deploy | `gpt-4.1` | No |
+| `modelFormat` | Model provider | `OpenAI` | No |
+| `modelVersion` | Model version | `2025-04-14` | No |
+| `modelSkuName` | Model deployment SKU | `GlobalStandard` | No |
+| `modelCapacity` | Tokens per minute (TPM) capacity | `30` | No |
+| `vnetName` | Virtual Network name | `agent-vnet-test` | No |
+| `agentSubnetName` | Subnet name for agent workloads | `agent-subnet` | No |
+| `agentSubnetPrefix` | Address prefix for agent subnet | `192.168.0.0/24` | No |
+| `peSubnetName` | Subnet name for private endpoints | `pe-subnet` | No |
+| `peSubnetPrefix` | Address prefix for PE subnet | `192.168.1.0/24` | No |
+| `existingVnetResourceId` | Full ARM Resource ID of an existing VNet | `''` (creates new) | No |
+| `vnetAddressPrefix` | Address space for new VNet | `192.168.0.0/16` | No |
+| `dnsZonesSubscriptionId` | Subscription ID for existing DNS zones | `''` (current sub) | No |
+| `existingDnsZones` | Map of DNS zone names to resource groups | All empty (creates new) | No |
+| `projectCapHost` | Name of the project capability host | `caphostproj` | No |
+
+#### BYO Virtual Network Details
+
+**Use an Existing Virtual Network and Subnets**
+
+To use an existing VNet and subnets, set the `existingVnetResourceId` parameter to the full Azure Resource ID of the target VNet, and provide the names of the two required subnets. If the existing VNet is associated with private DNS zones, set the `existingDnsZones` parameter to the resource group name in which the zones are located. For example:
+- `param existingVnetResourceId = "/subscriptions//resourceGroups//providers/Microsoft.Network/virtualNetworks/"`
+- `param agentSubnetName string = 'agent-subnet'` (optional, default is `agent-subnet`)
+- `param agentSubnetPrefix string = '192.168.0.0/24'` (optional, default is `192.168.0.0/24`)
+- `param peSubnetName string = 'pe-subnet'` (optional, default is `pe-subnet`)
+- `param peSubnetPrefix string = '192.168.1.0/24'` (optional, default is `192.168.1.0/24`)
+- `param dnsZonesSubscriptionId string = ''` (optional, leave empty to use current subscription, or set to a subscription ID if DNS zones are in a different subscription)
+- `param existingDnsZones = {`
+ `'privatelink.services.ai.azure.com': 'privzoneRG'` (add resource group name where your private DNS zone is located)
+ `'privatelink.openai.azure.com': ''` (leave empty to create new private DNS zone)
+ `'privatelink.cognitiveservices.azure.com': ''` `}`
+
+> **Tip**: If subnet information is provided, make sure the subnets exist within the specified VNet to avoid deployment errors. If subnet information is not provided, the template will create subnets with the default address space.
+
+> **Cross-Subscription DNS Zones**: All DNS zones specified in `existingDnsZones` will be referenced from the subscription specified in `dnsZonesSubscriptionId`. Leave this parameter empty (default) to use the current deployment subscription, or set it to a subscription ID if your DNS zones are located in a different subscription.
+
+> **Important**: When `dnsZonesSubscriptionId` is set to a different subscription, ALL DNS zones in `existingDnsZones` must have resource groups specified (non-empty values). The template does not support creating new DNS zones in a different subscription. Empty resource groups are only allowed when creating zones in the current deployment subscription.
+
+---
+
+## Deployment Steps
+
+Choose your deployment method: Use the "Deploy to Azure" button from the top of this README for a guided experience in Azure Portal.
+
+### Option 1: Automatic deployment
+Click the Deploy to Azure button above to open the Azure portal and deploy the template directly.
+- Fill in the parameters as needed, including the existing VNet and subnets if applicable.
+
+### Option 2: Deploy via Azure CLI
+
+- **Create a New (or Use Existing) Resource Group**
+
+ ```bash
+ az group create --name --location
+ ```
+
+- Deploy the main.bicep file
+ - Edit the main.bicepparam file to use an existing Virtual Network & subnets if needed.
+
+ ```bash
+ az deployment group create \
+ --resource-group \
+ --template-file main.bicep \
+ --parameters main.bicepparam
+ ```
+
+ Or deploy with inline parameters:
+
+ ```bash
+ az deployment group create \
+ --resource-group \
+ --template-file main.bicep \
+ --parameters aiServices=myFoundry location=eastus
+ ```
+
+### Option 3: Use an Existing Virtual Network
+
+If you already have a VNet you want to use, provide its resource ID:
+
+```bash
+az deployment group create \
+ --resource-group \
+ --template-file main.bicep \
+ --parameters main.bicepparam \
+ --parameters existingVnetResourceId='/subscriptions//resourceGroups//providers/Microsoft.Network/virtualNetworks/'
+```
+
+The template will create the required subnets (agent subnet with delegation and PE subnet) in your existing VNet.
+
+---
+
+## Post-Deployment
+
+**NOTE:** To access your Foundry resource securely, please use either a VM, VPN, or ExpressRoute since public network access is disabled.
+
+### Cleanup
+
+To delete all resources created by this template:
+
+```bash
+az group delete --name --yes --no-wait
+```
+
+> **Important**: If you need to reuse the same subnet, follow the [Account Deletion Prerequisites and Cleanup Guidance](#account-deletion-prerequisites-and-cleanup-guidance) to properly purge the account and wait for the capability host to fully unlink (~20 minutes).
+
+---
+
+## Network Secured Basic Agent Architecture Deep Dive
+
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Secure Access (VPN Gateway / ExpressRoute / Azure Bastion) β
+ββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
+ β
+ ββββββββββββββββΌβββββββββββββββ
+ β Microsoft Foundry β
+ β (publicNetworkAccess: β
+ β DISABLED) β
+ β β
+ β ββββββββββββββββββββββββββ β
+ β β Foundry Project β β
+ β β (Agent Workspace) β β
+ β βββββββββββββ¬βββββββββββββ β
+ ββββββββββββββββΌβββββββββββββββ
+ β Subnet Delegation
+ ββββββββββββββββΌβββββββββββββββ
+ β BYO Virtual Network β
+ β (192.168.0.0/16) β
+ β β
+ β ββββββββββββββββββββββββ β
+ β β Agent Subnet β β
+ β β (192.168.0.0/24) β β βββ Delegated to
+ β β Microsoft.App/envs β β Microsoft.App/environments
+ β ββββββββββββββββββββββββ β
+ β β
+ β ββββββββββββββββββββββββ β
+ β β PE Subnet β β
+ β β (192.168.1.0/24) β β
+ β β β β
+ β β ββββββββββ β β
+ β β βFoundry β β β βββ Private endpoint
+ β β ββββββββββ β β (no public access)
+ β ββββββββββββββββββββββββ β
+ ββββββββββββββββββββββββββββββββ
+```
+
+### Core Components
+
+**Microsoft Foundry** resource
+- Central orchestration point
+- Manages service connections
+- Set networking and policy configurations
+
+**Foundry** project
+- Defines the workspace configuration
+- Service integration
+- Agents are created within a specific project, and each project acts as an isolated workspace. This means:
+ - All agents in the same project share access to platform-managed storage resources.
+ - Data is isolated between projects. Agents in one project cannot access resources from another. Projects are currently the unit of sharing and isolation in Foundry. See the what is AI foundry article for more information on Foundry projects.
+
+**Platform-Managed Resources**: In the basic agent setup, all agent state (thread storage, file storage, vector store) is managed by the platform. No customer-managed BYO resources are required.
+
+### Azure Resources Created
+
+**Microsoft Foundry (Cognitive Services)**
+- Type: `Microsoft.CognitiveServices/accounts`
+- API version: `2025-04-01-preview`
+- Kind: AIServices
+- SKU: S0
+- Identity: System-assigned
+- Features:
+ - Custom subdomain name
+ - Disabled public network access
+ - Network ACLs with Azure Services bypass
+ - Network injection with agent subnet delegation
+
+**AI Model Deployment**
+- Type: `Microsoft.CognitiveServices/accounts/deployments`
+- API version: `2025-04-01-preview`
+- SKU: Based on `modelSkuName` parameter, capacity set by `modelCapacity`
+- Model properties:
+ - Name: From `modelName` parameter
+ - Format: From `modelFormat` parameter
+ - Version: From `modelVersion` parameter
+
+### Network Security Design
+
+This implementation utilizes a BYO VNet (Bring Your Own Virtual Network) approach with subnet delegation. Within your virtual network, two subnets are created: one delegated for agent workloads and one for private endpoints.
+
+**Network Security**
+- Public network access disabled
+- Private endpoint for AI Services
+- Network ACLs with deny by default
+
+**Network Infrastructure**
+- A Virtual Network (192.168.0.0/16) is created (if existing isn't passed in)
+- Agent Subnet (192.168.0.0/24): Hosts Agent client, delegated to `Microsoft.App/environments`
+- Private Endpoint Subnet (192.168.1.0/24): Hosts private endpoint for AI Services
+
+**Private Endpoints**
+A private endpoint ensures secure, internal-only connectivity to the AI Services account.
+
+**Private DNS Zones**
+| Private Link Resource Type | Sub Resource | Private DNS Zone Name | Public DNS Zone Forwarders |
+|----------------------------|--------------|------------------------|-----------------------------|
+| **Microsoft Foundry** | account | `privatelink.cognitiveservices.azure.com`
`privatelink.openai.azure.com`
`privatelink.services.ai.azure.com` | `cognitiveservices.azure.com`
`openai.azure.com`
`services.ai.azure.com` |
+
+### Authentication & Authorization
+
+- **Managed Identity**
+ - Zero-trust security model
+ - No credential storage
+ - Platform-managed rotation
+
+ This template uses System Managed Identity.
+
+> **Note**: Unlike the standard agent setup (template 15), this basic template does not create RBAC role assignments for BYO resources (Storage, Cosmos DB, AI Search), since those resources are not used.
+
+---
+
+## Module Structure
+
+```text
+modules-network-secured/
+βββ ai-account-identity.bicep # AI Services account with network injection
+βββ add-project-capability-host.bicep # Basic capability host (no BYO connections)
+βββ network-agent-vnet.bicep # VNet router (new or existing)
+βββ vnet.bicep # New VNet creation
+βββ existing-vnet.bicep # Existing VNet integration
+βββ subnet.bicep # Subnet creation helper
+βββ private-endpoint-and-dns.bicep # PE and DNS for AI Services only
+```
+
+### Compared to Other Templates
+
+| Feature | 10 (Private Network Basic) | **11 (This Template)** | 15 (Standard Agent Setup) |
+|---------|---------------------------|------------------------|---------------------------|
+| Public Network Access | Disabled | Disabled | Disabled |
+| VNet Injection | No | **Yes** | Yes |
+| Agent Subnet Delegation | No | **Yes** (`Microsoft.App/environments`) | Yes |
+| Private Endpoints | AI Services only | **AI Services only** | AI Services + Search + Storage + Cosmos DB |
+| BYO Resources | No | **No** | Yes (Search, Storage, Cosmos DB) |
+| Capability Host | No | **Yes (basic)** | Yes (with BYO connections) |
+| Existing VNet Support | No | **Yes** | Yes |
+| RBAC Assignments | No | **No** | Yes (Storage, Cosmos, Search) |
+
+---
+
+## Maintenance
+
+### Regular Tasks
+
+1. Monitor network security
+2. Check service health
+3. Review DNS resolution
+4. Update configurations as needed
+
+### Troubleshooting
+
+1. Verify private endpoint connectivity
+2. Check DNS resolution for AI Services
+3. Validate subnet delegation is correct
+4. Review network security groups
+
+---
+
+## References
+
+- [Microsoft Foundry Networking Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link?tabs=azure-portal&pivots=fdp-project)
+- [Private Endpoint Documentation](https://learn.microsoft.com/en-us/azure/private-link/)
+- [Network Security Best Practices](https://learn.microsoft.com/en-us/azure/security/fundamentals/network-best-practices)
diff --git a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/azuredeploy.json b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/azuredeploy.json
new file mode 100644
index 000000000..b744d76c4
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/azuredeploy.json
@@ -0,0 +1,1329 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.43.1.21952",
+ "templateHash": "7766740854543235375"
+ }
+ },
+ "parameters": {
+ "location": {
+ "type": "string",
+ "defaultValue": "eastus",
+ "allowedValues": [
+ "westus",
+ "eastus",
+ "eastus2",
+ "japaneast",
+ "francecentral",
+ "spaincentral",
+ "uaenorth",
+ "southcentralus",
+ "italynorth",
+ "germanywestcentral",
+ "brazilsouth",
+ "southafricanorth",
+ "australiaeast",
+ "swedencentral",
+ "canadaeast",
+ "westeurope",
+ "westus3",
+ "uksouth",
+ "southindia",
+ "koreacentral",
+ "polandcentral",
+ "switzerlandnorth",
+ "norwayeast"
+ ],
+ "metadata": {
+ "description": "Location for all resources."
+ }
+ },
+ "aiServices": {
+ "type": "string",
+ "defaultValue": "aiservices",
+ "metadata": {
+ "description": "Name for your AI Services resource."
+ }
+ },
+ "modelName": {
+ "type": "string",
+ "defaultValue": "gpt-4.1",
+ "metadata": {
+ "description": "The name of the model you want to deploy"
+ }
+ },
+ "modelFormat": {
+ "type": "string",
+ "defaultValue": "OpenAI",
+ "metadata": {
+ "description": "The provider of your model"
+ }
+ },
+ "modelVersion": {
+ "type": "string",
+ "defaultValue": "2025-04-14",
+ "metadata": {
+ "description": "The version of your model"
+ }
+ },
+ "modelSkuName": {
+ "type": "string",
+ "defaultValue": "GlobalStandard",
+ "metadata": {
+ "description": "The sku of your model deployment"
+ }
+ },
+ "modelCapacity": {
+ "type": "int",
+ "defaultValue": 30,
+ "metadata": {
+ "description": "The tokens per minute (TPM) of your model deployment"
+ }
+ },
+ "deploymentTimestamp": {
+ "type": "string",
+ "defaultValue": "[utcNow('yyyyMMddHHmmss')]"
+ },
+ "firstProjectName": {
+ "type": "string",
+ "defaultValue": "project",
+ "metadata": {
+ "description": "Name for your project resource."
+ }
+ },
+ "projectDescription": {
+ "type": "string",
+ "defaultValue": "A project for the AI Foundry account with network secured basic Agent",
+ "metadata": {
+ "description": "This project will be a sub-resource of your account"
+ }
+ },
+ "displayName": {
+ "type": "string",
+ "defaultValue": "network secured basic agent project",
+ "metadata": {
+ "description": "The display name of the project"
+ }
+ },
+ "vnetName": {
+ "type": "string",
+ "defaultValue": "agent-vnet-test",
+ "metadata": {
+ "description": "Virtual Network name for the Agent to create new or existing virtual network"
+ }
+ },
+ "agentSubnetName": {
+ "type": "string",
+ "defaultValue": "agent-subnet",
+ "metadata": {
+ "description": "The name of Agents Subnet to create new or existing subnet for agents"
+ }
+ },
+ "peSubnetName": {
+ "type": "string",
+ "defaultValue": "pe-subnet",
+ "metadata": {
+ "description": "The name of Private Endpoint subnet to create new or existing subnet for private endpoints"
+ }
+ },
+ "existingVnetResourceId": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Existing Virtual Network name Resource ID"
+ }
+ },
+ "vnetAddressPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address space for the VNet (only used for new VNet)"
+ }
+ },
+ "agentSubnetPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address prefix for the agent subnet. The default value is 192.168.0.0/24 but you can choose any size /26 or any class like 10.0.0.0 or 172.168.0.0"
+ }
+ },
+ "peSubnetPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address prefix for the private endpoint subnet"
+ }
+ },
+ "dnsZonesSubscriptionId": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Subscription ID where existing private DNS zones are located. Leave empty to use current subscription."
+ }
+ },
+ "existingDnsZones": {
+ "type": "object",
+ "defaultValue": {
+ "privatelink.services.ai.azure.com": "",
+ "privatelink.openai.azure.com": "",
+ "privatelink.cognitiveservices.azure.com": ""
+ },
+ "metadata": {
+ "description": "Object mapping DNS zone names to their resource group, or empty string to indicate creation"
+ }
+ },
+ "projectCapHost": {
+ "type": "string",
+ "defaultValue": "caphostproj",
+ "metadata": {
+ "description": "The name of the project capability host to be created"
+ }
+ }
+ },
+ "variables": {
+ "uniqueSuffix": "[substring(uniqueString(format('{0}-{1}', resourceGroup().id, parameters('deploymentTimestamp'))), 0, 4)]",
+ "accountName": "[toLower(format('{0}{1}', parameters('aiServices'), variables('uniqueSuffix')))]",
+ "projectName": "[toLower(format('{0}{1}', parameters('firstProjectName'), variables('uniqueSuffix')))]",
+ "existingVnetPassedIn": "[not(equals(parameters('existingVnetResourceId'), ''))]",
+ "vnetParts": "[split(parameters('existingVnetResourceId'), '/')]",
+ "vnetSubscriptionId": "[if(variables('existingVnetPassedIn'), variables('vnetParts')[2], subscription().subscriptionId)]",
+ "vnetResourceGroupName": "[if(variables('existingVnetPassedIn'), variables('vnetParts')[4], resourceGroup().name)]",
+ "existingVnetName": "[if(variables('existingVnetPassedIn'), last(variables('vnetParts')), parameters('vnetName'))]",
+ "trimVnetName": "[trim(variables('existingVnetName'))]",
+ "resolvedDnsZonesSubscriptionId": "[if(empty(parameters('dnsZonesSubscriptionId')), subscription().subscriptionId, parameters('dnsZonesSubscriptionId'))]"
+ },
+ "resources": [
+ {
+ "type": "Microsoft.CognitiveServices/accounts/projects",
+ "apiVersion": "2025-04-01-preview",
+ "name": "[format('{0}/{1}', variables('accountName'), variables('projectName'))]",
+ "location": "[parameters('location')]",
+ "identity": {
+ "type": "SystemAssigned"
+ },
+ "properties": {
+ "description": "[parameters('projectDescription')]",
+ "displayName": "[parameters('displayName')]"
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix')))]",
+ "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "[format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "location": {
+ "value": "[parameters('location')]"
+ },
+ "vnetName": {
+ "value": "[variables('trimVnetName')]"
+ },
+ "useExistingVnet": {
+ "value": "[variables('existingVnetPassedIn')]"
+ },
+ "existingVnetResourceGroupName": {
+ "value": "[variables('vnetResourceGroupName')]"
+ },
+ "agentSubnetName": {
+ "value": "[parameters('agentSubnetName')]"
+ },
+ "peSubnetName": {
+ "value": "[parameters('peSubnetName')]"
+ },
+ "vnetAddressPrefix": {
+ "value": "[parameters('vnetAddressPrefix')]"
+ },
+ "agentSubnetPrefix": {
+ "value": "[parameters('agentSubnetPrefix')]"
+ },
+ "peSubnetPrefix": {
+ "value": "[parameters('peSubnetPrefix')]"
+ },
+ "existingVnetSubscriptionId": {
+ "value": "[variables('vnetSubscriptionId')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.43.1.21952",
+ "templateHash": "17355345610868234879"
+ }
+ },
+ "parameters": {
+ "location": {
+ "type": "string",
+ "metadata": {
+ "description": "Azure region for the deployment"
+ }
+ },
+ "vnetName": {
+ "type": "string",
+ "metadata": {
+ "description": "The name of the virtual network"
+ }
+ },
+ "useExistingVnet": {
+ "type": "bool",
+ "defaultValue": false,
+ "metadata": {
+ "description": "Indicates if an existing VNet should be used"
+ }
+ },
+ "existingVnetSubscriptionId": {
+ "type": "string",
+ "defaultValue": "[subscription().subscriptionId]",
+ "metadata": {
+ "description": "Subscription ID of the existing VNet (if different from current subscription)"
+ }
+ },
+ "existingVnetResourceGroupName": {
+ "type": "string",
+ "defaultValue": "[resourceGroup().name]",
+ "metadata": {
+ "description": "Resource Group name of the existing VNet (if different from current resource group)"
+ }
+ },
+ "agentSubnetName": {
+ "type": "string",
+ "defaultValue": "agent-subnet",
+ "metadata": {
+ "description": "The name of Agents Subnet"
+ }
+ },
+ "peSubnetName": {
+ "type": "string",
+ "defaultValue": "pe-subnet",
+ "metadata": {
+ "description": "The name of Private Endpoint subnet"
+ }
+ },
+ "vnetAddressPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address space for the VNet (only used for new VNet)"
+ }
+ },
+ "agentSubnetPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address prefix for the agent subnet"
+ }
+ },
+ "peSubnetPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address prefix for the private endpoint subnet"
+ }
+ }
+ },
+ "resources": [
+ {
+ "condition": "[not(parameters('useExistingVnet'))]",
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "vnet-deployment",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "location": {
+ "value": "[parameters('location')]"
+ },
+ "vnetName": {
+ "value": "[parameters('vnetName')]"
+ },
+ "agentSubnetName": {
+ "value": "[parameters('agentSubnetName')]"
+ },
+ "peSubnetName": {
+ "value": "[parameters('peSubnetName')]"
+ },
+ "vnetAddressPrefix": {
+ "value": "[parameters('vnetAddressPrefix')]"
+ },
+ "agentSubnetPrefix": {
+ "value": "[parameters('agentSubnetPrefix')]"
+ },
+ "peSubnetPrefix": {
+ "value": "[parameters('peSubnetPrefix')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.43.1.21952",
+ "templateHash": "10901913041029806810"
+ }
+ },
+ "parameters": {
+ "location": {
+ "type": "string",
+ "metadata": {
+ "description": "Azure region for the deployment"
+ }
+ },
+ "vnetName": {
+ "type": "string",
+ "defaultValue": "agents-vnet-test",
+ "metadata": {
+ "description": "The name of the virtual network"
+ }
+ },
+ "agentSubnetName": {
+ "type": "string",
+ "defaultValue": "agent-subnet",
+ "metadata": {
+ "description": "The name of Agents Subnet"
+ }
+ },
+ "peSubnetName": {
+ "type": "string",
+ "defaultValue": "pe-subnet",
+ "metadata": {
+ "description": "The name of Hub subnet"
+ }
+ },
+ "vnetAddressPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address space for the VNet"
+ }
+ },
+ "agentSubnetPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address prefix for the agent subnet"
+ }
+ },
+ "peSubnetPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address prefix for the private endpoint subnet"
+ }
+ }
+ },
+ "variables": {
+ "defaultVnetAddressPrefix": "192.168.0.0/16",
+ "vnetAddress": "[if(empty(parameters('vnetAddressPrefix')), variables('defaultVnetAddressPrefix'), parameters('vnetAddressPrefix'))]",
+ "agentSubnet": "[if(empty(parameters('agentSubnetPrefix')), cidrSubnet(variables('vnetAddress'), 24, 0), parameters('agentSubnetPrefix'))]",
+ "peSubnet": "[if(empty(parameters('peSubnetPrefix')), cidrSubnet(variables('vnetAddress'), 24, 1), parameters('peSubnetPrefix'))]"
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Network/virtualNetworks",
+ "apiVersion": "2024-05-01",
+ "name": "[parameters('vnetName')]",
+ "location": "[parameters('location')]",
+ "properties": {
+ "addressSpace": {
+ "addressPrefixes": [
+ "[variables('vnetAddress')]"
+ ]
+ },
+ "subnets": [
+ {
+ "name": "[parameters('agentSubnetName')]",
+ "properties": {
+ "addressPrefix": "[variables('agentSubnet')]",
+ "delegations": [
+ {
+ "name": "Microsoft.app/environments",
+ "properties": {
+ "serviceName": "Microsoft.App/environments"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "[parameters('peSubnetName')]",
+ "properties": {
+ "addressPrefix": "[variables('peSubnet')]"
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "outputs": {
+ "peSubnetName": {
+ "type": "string",
+ "value": "[parameters('peSubnetName')]"
+ },
+ "agentSubnetName": {
+ "type": "string",
+ "value": "[parameters('agentSubnetName')]"
+ },
+ "agentSubnetId": {
+ "type": "string",
+ "value": "[format('{0}/subnets/{1}', resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('agentSubnetName'))]"
+ },
+ "peSubnetId": {
+ "type": "string",
+ "value": "[format('{0}/subnets/{1}', resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('peSubnetName'))]"
+ },
+ "virtualNetworkName": {
+ "type": "string",
+ "value": "[parameters('vnetName')]"
+ },
+ "virtualNetworkId": {
+ "type": "string",
+ "value": "[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
+ },
+ "virtualNetworkResourceGroup": {
+ "type": "string",
+ "value": "[resourceGroup().name]"
+ },
+ "virtualNetworkSubscriptionId": {
+ "type": "string",
+ "value": "[subscription().subscriptionId]"
+ }
+ }
+ }
+ }
+ },
+ {
+ "condition": "[parameters('useExistingVnet')]",
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "existing-vnet-deployment",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "vnetName": {
+ "value": "[parameters('vnetName')]"
+ },
+ "vnetResourceGroupName": {
+ "value": "[parameters('existingVnetResourceGroupName')]"
+ },
+ "vnetSubscriptionId": {
+ "value": "[parameters('existingVnetSubscriptionId')]"
+ },
+ "agentSubnetName": {
+ "value": "[parameters('agentSubnetName')]"
+ },
+ "peSubnetName": {
+ "value": "[parameters('peSubnetName')]"
+ },
+ "agentSubnetPrefix": {
+ "value": "[parameters('agentSubnetPrefix')]"
+ },
+ "peSubnetPrefix": {
+ "value": "[parameters('peSubnetPrefix')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.43.1.21952",
+ "templateHash": "10509554109014084911"
+ }
+ },
+ "parameters": {
+ "vnetName": {
+ "type": "string",
+ "metadata": {
+ "description": "The name of the existing virtual network"
+ }
+ },
+ "vnetSubscriptionId": {
+ "type": "string",
+ "defaultValue": "[subscription().subscriptionId]",
+ "metadata": {
+ "description": "Subscription ID of virtual network (if different from current subscription)"
+ }
+ },
+ "vnetResourceGroupName": {
+ "type": "string",
+ "defaultValue": "[resourceGroup().name]",
+ "metadata": {
+ "description": "Resource Group name of the existing VNet (if different from current resource group)"
+ }
+ },
+ "agentSubnetName": {
+ "type": "string",
+ "defaultValue": "agent-subnet",
+ "metadata": {
+ "description": "The name of Agents Subnet"
+ }
+ },
+ "peSubnetName": {
+ "type": "string",
+ "defaultValue": "pe-subnet",
+ "metadata": {
+ "description": "The name of Private Endpoint subnet"
+ }
+ },
+ "agentSubnetPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address prefix for the agent subnet (only needed if creating new subnet)"
+ }
+ },
+ "peSubnetPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address prefix for the private endpoint subnet (only needed if creating new subnet)"
+ }
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "[format('agent-subnet-{0}', uniqueString(deployment().name, parameters('agentSubnetName')))]",
+ "resourceGroup": "[parameters('vnetResourceGroupName')]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "vnetName": {
+ "value": "[parameters('vnetName')]"
+ },
+ "subnetName": {
+ "value": "[parameters('agentSubnetName')]"
+ },
+ "addressPrefix": "[if(empty(parameters('agentSubnetPrefix')), createObject('value', cidrSubnet(reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), '2024-05-01').addressSpace.addressPrefixes[0], 24, 0)), createObject('value', parameters('agentSubnetPrefix')))]",
+ "delegations": {
+ "value": [
+ {
+ "name": "Microsoft.App/environments",
+ "properties": {
+ "serviceName": "Microsoft.App/environments"
+ }
+ }
+ ]
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.43.1.21952",
+ "templateHash": "13091507001124186389"
+ }
+ },
+ "parameters": {
+ "vnetName": {
+ "type": "string",
+ "metadata": {
+ "description": "Name of the virtual network"
+ }
+ },
+ "subnetName": {
+ "type": "string",
+ "metadata": {
+ "description": "Name of the subnet"
+ }
+ },
+ "addressPrefix": {
+ "type": "string",
+ "metadata": {
+ "description": "Address prefix for the subnet"
+ }
+ },
+ "delegations": {
+ "type": "array",
+ "defaultValue": [],
+ "metadata": {
+ "description": "Array of subnet delegations"
+ }
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Network/virtualNetworks/subnets",
+ "apiVersion": "2024-05-01",
+ "name": "[format('{0}/{1}', parameters('vnetName'), parameters('subnetName'))]",
+ "properties": {
+ "addressPrefix": "[parameters('addressPrefix')]",
+ "delegations": "[parameters('delegations')]"
+ }
+ }
+ ],
+ "outputs": {
+ "subnetId": {
+ "type": "string",
+ "value": "[resourceId('Microsoft.Network/virtualNetworks/subnets', split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[0], split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[1])]"
+ },
+ "subnetName": {
+ "type": "string",
+ "value": "[parameters('subnetName')]"
+ }
+ }
+ }
+ }
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "[format('pe-subnet-{0}', uniqueString(deployment().name, parameters('peSubnetName')))]",
+ "resourceGroup": "[parameters('vnetResourceGroupName')]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "vnetName": {
+ "value": "[parameters('vnetName')]"
+ },
+ "subnetName": {
+ "value": "[parameters('peSubnetName')]"
+ },
+ "addressPrefix": "[if(empty(parameters('peSubnetPrefix')), createObject('value', cidrSubnet(reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), '2024-05-01').addressSpace.addressPrefixes[0], 24, 1)), createObject('value', parameters('peSubnetPrefix')))]",
+ "delegations": {
+ "value": []
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.43.1.21952",
+ "templateHash": "13091507001124186389"
+ }
+ },
+ "parameters": {
+ "vnetName": {
+ "type": "string",
+ "metadata": {
+ "description": "Name of the virtual network"
+ }
+ },
+ "subnetName": {
+ "type": "string",
+ "metadata": {
+ "description": "Name of the subnet"
+ }
+ },
+ "addressPrefix": {
+ "type": "string",
+ "metadata": {
+ "description": "Address prefix for the subnet"
+ }
+ },
+ "delegations": {
+ "type": "array",
+ "defaultValue": [],
+ "metadata": {
+ "description": "Array of subnet delegations"
+ }
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Network/virtualNetworks/subnets",
+ "apiVersion": "2024-05-01",
+ "name": "[format('{0}/{1}', parameters('vnetName'), parameters('subnetName'))]",
+ "properties": {
+ "addressPrefix": "[parameters('addressPrefix')]",
+ "delegations": "[parameters('delegations')]"
+ }
+ }
+ ],
+ "outputs": {
+ "subnetId": {
+ "type": "string",
+ "value": "[resourceId('Microsoft.Network/virtualNetworks/subnets', split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[0], split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[1])]"
+ },
+ "subnetName": {
+ "type": "string",
+ "value": "[parameters('subnetName')]"
+ }
+ }
+ }
+ }
+ }
+ ],
+ "outputs": {
+ "peSubnetName": {
+ "type": "string",
+ "value": "[parameters('peSubnetName')]"
+ },
+ "agentSubnetName": {
+ "type": "string",
+ "value": "[parameters('agentSubnetName')]"
+ },
+ "agentSubnetId": {
+ "type": "string",
+ "value": "[format('{0}/subnets/{1}', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('agentSubnetName'))]"
+ },
+ "peSubnetId": {
+ "type": "string",
+ "value": "[format('{0}/subnets/{1}', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('peSubnetName'))]"
+ },
+ "virtualNetworkName": {
+ "type": "string",
+ "value": "[parameters('vnetName')]"
+ },
+ "virtualNetworkId": {
+ "type": "string",
+ "value": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
+ },
+ "virtualNetworkResourceGroup": {
+ "type": "string",
+ "value": "[parameters('vnetResourceGroupName')]"
+ },
+ "virtualNetworkSubscriptionId": {
+ "type": "string",
+ "value": "[parameters('vnetSubscriptionId')]"
+ }
+ }
+ }
+ }
+ }
+ ],
+ "outputs": {
+ "virtualNetworkName": {
+ "type": "string",
+ "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.virtualNetworkName.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.virtualNetworkName.value)]"
+ },
+ "virtualNetworkId": {
+ "type": "string",
+ "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.virtualNetworkId.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.virtualNetworkId.value)]"
+ },
+ "virtualNetworkSubscriptionId": {
+ "type": "string",
+ "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.virtualNetworkSubscriptionId.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.virtualNetworkSubscriptionId.value)]"
+ },
+ "virtualNetworkResourceGroup": {
+ "type": "string",
+ "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.virtualNetworkResourceGroup.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.virtualNetworkResourceGroup.value)]"
+ },
+ "agentSubnetName": {
+ "type": "string",
+ "value": "[parameters('agentSubnetName')]"
+ },
+ "peSubnetName": {
+ "type": "string",
+ "value": "[parameters('peSubnetName')]"
+ },
+ "agentSubnetId": {
+ "type": "string",
+ "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.agentSubnetId.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.agentSubnetId.value)]"
+ },
+ "peSubnetId": {
+ "type": "string",
+ "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.peSubnetId.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.peSubnetId.value)]"
+ }
+ }
+ }
+ }
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "[format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "accountName": {
+ "value": "[variables('accountName')]"
+ },
+ "location": {
+ "value": "[parameters('location')]"
+ },
+ "modelName": {
+ "value": "[parameters('modelName')]"
+ },
+ "modelFormat": {
+ "value": "[parameters('modelFormat')]"
+ },
+ "modelVersion": {
+ "value": "[parameters('modelVersion')]"
+ },
+ "modelSkuName": {
+ "value": "[parameters('modelSkuName')]"
+ },
+ "modelCapacity": {
+ "value": "[parameters('modelCapacity')]"
+ },
+ "agentSubnetId": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.agentSubnetId.value]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.43.1.21952",
+ "templateHash": "14040588815014793423"
+ }
+ },
+ "parameters": {
+ "accountName": {
+ "type": "string"
+ },
+ "location": {
+ "type": "string"
+ },
+ "modelName": {
+ "type": "string"
+ },
+ "modelFormat": {
+ "type": "string"
+ },
+ "modelVersion": {
+ "type": "string"
+ },
+ "modelSkuName": {
+ "type": "string"
+ },
+ "modelCapacity": {
+ "type": "int"
+ },
+ "agentSubnetId": {
+ "type": "string"
+ },
+ "networkInjection": {
+ "type": "string",
+ "defaultValue": "true"
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.CognitiveServices/accounts",
+ "apiVersion": "2025-04-01-preview",
+ "name": "[parameters('accountName')]",
+ "location": "[parameters('location')]",
+ "sku": {
+ "name": "S0"
+ },
+ "kind": "AIServices",
+ "identity": {
+ "type": "SystemAssigned"
+ },
+ "properties": {
+ "allowProjectManagement": true,
+ "customSubDomainName": "[parameters('accountName')]",
+ "networkAcls": {
+ "defaultAction": "Deny",
+ "virtualNetworkRules": [],
+ "ipRules": [],
+ "bypass": "AzureServices"
+ },
+ "publicNetworkAccess": "Disabled",
+ "networkInjections": "[if(equals(parameters('networkInjection'), 'true'), createArray(createObject('scenario', 'agent', 'subnetArmId', parameters('agentSubnetId'), 'useMicrosoftManagedNetwork', false())), null())]",
+ "disableLocalAuth": false
+ }
+ },
+ {
+ "type": "Microsoft.CognitiveServices/accounts/deployments",
+ "apiVersion": "2025-04-01-preview",
+ "name": "[format('{0}/{1}', parameters('accountName'), parameters('modelName'))]",
+ "sku": {
+ "capacity": "[parameters('modelCapacity')]",
+ "name": "[parameters('modelSkuName')]"
+ },
+ "properties": {
+ "model": {
+ "name": "[parameters('modelName')]",
+ "format": "[parameters('modelFormat')]",
+ "version": "[parameters('modelVersion')]"
+ }
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName'))]"
+ ]
+ }
+ ],
+ "outputs": {
+ "accountName": {
+ "type": "string",
+ "value": "[parameters('accountName')]"
+ },
+ "accountID": {
+ "type": "string",
+ "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName'))]"
+ },
+ "accountTarget": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName')), '2025-04-01-preview').endpoint]"
+ },
+ "accountPrincipalId": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName')), '2025-04-01-preview', 'full').identity.principalId]"
+ }
+ }
+ }
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "[format('{0}-private-endpoint', variables('uniqueSuffix'))]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "aiAccountName": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))), '2025-04-01').outputs.accountName.value]"
+ },
+ "vnetName": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.virtualNetworkName.value]"
+ },
+ "peSubnetName": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.peSubnetName.value]"
+ },
+ "suffix": {
+ "value": "[variables('uniqueSuffix')]"
+ },
+ "vnetResourceGroupName": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.virtualNetworkResourceGroup.value]"
+ },
+ "vnetSubscriptionId": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.virtualNetworkSubscriptionId.value]"
+ },
+ "existingDnsZones": {
+ "value": "[parameters('existingDnsZones')]"
+ },
+ "dnsZonesSubscriptionId": {
+ "value": "[variables('resolvedDnsZonesSubscriptionId')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.43.1.21952",
+ "templateHash": "9941350741815038708"
+ }
+ },
+ "parameters": {
+ "aiAccountName": {
+ "type": "string",
+ "metadata": {
+ "description": "Name of the AI Foundry account"
+ }
+ },
+ "vnetName": {
+ "type": "string",
+ "metadata": {
+ "description": "Name of the Vnet"
+ }
+ },
+ "peSubnetName": {
+ "type": "string",
+ "metadata": {
+ "description": "Name of the private endpoint subnet"
+ }
+ },
+ "suffix": {
+ "type": "string",
+ "metadata": {
+ "description": "Suffix for unique resource names"
+ }
+ },
+ "vnetResourceGroupName": {
+ "type": "string",
+ "defaultValue": "[resourceGroup().name]",
+ "metadata": {
+ "description": "Resource Group name for existing Virtual Network (if different from current resource group)"
+ }
+ },
+ "vnetSubscriptionId": {
+ "type": "string",
+ "defaultValue": "[subscription().subscriptionId]",
+ "metadata": {
+ "description": "Subscription ID for Virtual Network"
+ }
+ },
+ "existingDnsZones": {
+ "type": "object",
+ "defaultValue": {
+ "privatelink.services.ai.azure.com": "",
+ "privatelink.openai.azure.com": "",
+ "privatelink.cognitiveservices.azure.com": ""
+ },
+ "metadata": {
+ "description": "Map of DNS zone FQDNs to resource group names. If provided, reference existing DNS zones in this resource group instead of creating them."
+ }
+ },
+ "dnsZonesSubscriptionId": {
+ "type": "string",
+ "metadata": {
+ "description": "Subscription ID where existing private DNS zones are located. Should be resolved to current subscription if empty."
+ }
+ }
+ },
+ "variables": {
+ "aiServicesDnsZoneName": "privatelink.services.ai.azure.com",
+ "openAiDnsZoneName": "privatelink.openai.azure.com",
+ "cognitiveServicesDnsZoneName": "privatelink.cognitiveservices.azure.com",
+ "aiServicesDnsZoneRG": "[parameters('existingDnsZones')[variables('aiServicesDnsZoneName')]]",
+ "openAiDnsZoneRG": "[parameters('existingDnsZones')[variables('openAiDnsZoneName')]]",
+ "cognitiveServicesDnsZoneRG": "[parameters('existingDnsZones')[variables('cognitiveServicesDnsZoneName')]]",
+ "aiServicesDnsZoneId": "[if(empty(variables('aiServicesDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('aiServicesDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('dnsZonesSubscriptionId'), variables('aiServicesDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('aiServicesDnsZoneName')))]",
+ "openAiDnsZoneId": "[if(empty(variables('openAiDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('openAiDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('dnsZonesSubscriptionId'), variables('openAiDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('openAiDnsZoneName')))]",
+ "cognitiveServicesDnsZoneId": "[if(empty(variables('cognitiveServicesDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('cognitiveServicesDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('dnsZonesSubscriptionId'), variables('cognitiveServicesDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('cognitiveServicesDnsZoneName')))]"
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Network/privateEndpoints",
+ "apiVersion": "2024-05-01",
+ "name": "[format('{0}-private-endpoint', parameters('aiAccountName'))]",
+ "location": "[resourceGroup().location]",
+ "properties": {
+ "subnet": {
+ "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('peSubnetName'))]"
+ },
+ "privateLinkServiceConnections": [
+ {
+ "name": "[format('{0}-private-link-service-connection', parameters('aiAccountName'))]",
+ "properties": {
+ "privateLinkServiceId": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('aiAccountName'))]",
+ "groupIds": [
+ "account"
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "condition": "[empty(variables('aiServicesDnsZoneRG'))]",
+ "type": "Microsoft.Network/privateDnsZones",
+ "apiVersion": "2020-06-01",
+ "name": "[variables('aiServicesDnsZoneName')]",
+ "location": "global"
+ },
+ {
+ "condition": "[empty(variables('openAiDnsZoneRG'))]",
+ "type": "Microsoft.Network/privateDnsZones",
+ "apiVersion": "2020-06-01",
+ "name": "[variables('openAiDnsZoneName')]",
+ "location": "global"
+ },
+ {
+ "condition": "[empty(variables('cognitiveServicesDnsZoneRG'))]",
+ "type": "Microsoft.Network/privateDnsZones",
+ "apiVersion": "2020-06-01",
+ "name": "[variables('cognitiveServicesDnsZoneName')]",
+ "location": "global"
+ },
+ {
+ "condition": "[empty(variables('aiServicesDnsZoneRG'))]",
+ "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
+ "apiVersion": "2024-06-01",
+ "name": "[format('{0}/{1}', variables('aiServicesDnsZoneName'), format('aiServices-{0}-link', parameters('suffix')))]",
+ "location": "global",
+ "properties": {
+ "virtualNetwork": {
+ "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
+ },
+ "registrationEnabled": false
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Network/privateDnsZones', variables('aiServicesDnsZoneName'))]"
+ ]
+ },
+ {
+ "condition": "[empty(variables('openAiDnsZoneRG'))]",
+ "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
+ "apiVersion": "2024-06-01",
+ "name": "[format('{0}/{1}', variables('openAiDnsZoneName'), format('aiServicesOpenAI-{0}-link', parameters('suffix')))]",
+ "location": "global",
+ "properties": {
+ "virtualNetwork": {
+ "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
+ },
+ "registrationEnabled": false
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Network/privateDnsZones', variables('openAiDnsZoneName'))]"
+ ]
+ },
+ {
+ "condition": "[empty(variables('cognitiveServicesDnsZoneRG'))]",
+ "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
+ "apiVersion": "2024-06-01",
+ "name": "[format('{0}/{1}', variables('cognitiveServicesDnsZoneName'), format('aiServicesCognitiveServices-{0}-link', parameters('suffix')))]",
+ "location": "global",
+ "properties": {
+ "virtualNetwork": {
+ "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
+ },
+ "registrationEnabled": false
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Network/privateDnsZones', variables('cognitiveServicesDnsZoneName'))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups",
+ "apiVersion": "2024-05-01",
+ "name": "[format('{0}/{1}', format('{0}-private-endpoint', parameters('aiAccountName')), format('{0}-dns-group', parameters('aiAccountName')))]",
+ "properties": {
+ "privateDnsZoneConfigs": [
+ {
+ "name": "[format('{0}-dns-aiserv-config', parameters('aiAccountName'))]",
+ "properties": {
+ "privateDnsZoneId": "[variables('aiServicesDnsZoneId')]"
+ }
+ },
+ {
+ "name": "[format('{0}-dns-openai-config', parameters('aiAccountName'))]",
+ "properties": {
+ "privateDnsZoneId": "[variables('openAiDnsZoneId')]"
+ }
+ },
+ {
+ "name": "[format('{0}-dns-cogserv-config', parameters('aiAccountName'))]",
+ "properties": {
+ "privateDnsZoneId": "[variables('cognitiveServicesDnsZoneId')]"
+ }
+ }
+ ]
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('aiAccountName')))]",
+ "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('aiServicesDnsZoneName'), format('aiServices-{0}-link', parameters('suffix')))]",
+ "[resourceId('Microsoft.Network/privateDnsZones', variables('aiServicesDnsZoneName'))]",
+ "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('cognitiveServicesDnsZoneName'), format('aiServicesCognitiveServices-{0}-link', parameters('suffix')))]",
+ "[resourceId('Microsoft.Network/privateDnsZones', variables('cognitiveServicesDnsZoneName'))]",
+ "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('openAiDnsZoneName'), format('aiServicesOpenAI-{0}-link', parameters('suffix')))]",
+ "[resourceId('Microsoft.Network/privateDnsZones', variables('openAiDnsZoneName'))]"
+ ]
+ }
+ ]
+ }
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix')))]",
+ "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "[format('capabilityHost-configuration-{0}-deployment', variables('uniqueSuffix'))]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "accountName": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))), '2025-04-01').outputs.accountName.value]"
+ },
+ "projectName": {
+ "value": "[variables('projectName')]"
+ },
+ "projectCapHost": {
+ "value": "[parameters('projectCapHost')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.43.1.21952",
+ "templateHash": "4929793693928679874"
+ }
+ },
+ "parameters": {
+ "projectName": {
+ "type": "string"
+ },
+ "accountName": {
+ "type": "string"
+ },
+ "projectCapHost": {
+ "type": "string"
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.CognitiveServices/accounts/projects/capabilityHosts",
+ "apiVersion": "2025-04-01-preview",
+ "name": "[format('{0}/{1}/{2}', parameters('accountName'), parameters('projectName'), parameters('projectCapHost'))]",
+ "properties": {
+ "capabilityHostKind": "Agents"
+ }
+ }
+ ],
+ "outputs": {
+ "projectCapHost": {
+ "type": "string",
+ "value": "[parameters('projectCapHost')]"
+ }
+ }
+ }
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix')))]",
+ "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]",
+ "[resourceId('Microsoft.CognitiveServices/accounts/projects', variables('accountName'), variables('projectName'))]"
+ ]
+ }
+ ],
+ "outputs": {
+ "accountId": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))), '2025-04-01').outputs.accountID.value]"
+ },
+ "accountName": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))), '2025-04-01').outputs.accountName.value]"
+ },
+ "projectName": {
+ "type": "string",
+ "value": "[variables('projectName')]"
+ }
+ }
+}
\ No newline at end of file
diff --git a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/main.bicep b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/main.bicep
new file mode 100644
index 000000000..3ee3085c2
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/main.bicep
@@ -0,0 +1,235 @@
+/*
+Basic Agent Setup with VNet Injection - Network Secured
+-----------------------------------
+This template creates:
+ - A virtual network with agent and private endpoint subnets
+ - An AI Foundry account with network injection (VNet integration for agents)
+ - Private endpoint and DNS zones for the AI Services account
+ - An AI Foundry project with system-assigned managed identity
+ - A capability host for the project (basic agent, no BYO resources)
+ - A model deployment (gpt-4.1 by default)
+
+This is a "basic" agent setup β it does NOT create or connect BYO resources
+(Azure AI Search, Storage Account, Cosmos DB). The platform-managed resources
+are used instead.
+*/
+@description('Location for all resources.')
+@allowed([
+ 'westus'
+ 'eastus'
+ 'eastus2'
+ 'japaneast'
+ 'francecentral'
+ 'spaincentral'
+ 'uaenorth'
+ 'southcentralus'
+ 'italynorth'
+ 'germanywestcentral'
+ 'brazilsouth'
+ 'southafricanorth'
+ 'australiaeast'
+ 'swedencentral'
+ 'canadaeast'
+ 'westeurope'
+ 'westus3'
+ 'uksouth'
+ 'southindia'
+
+ //only class B and C
+ 'koreacentral'
+ 'polandcentral'
+ 'switzerlandnorth'
+ 'norwayeast'
+])
+param location string = 'eastus'
+
+@description('Name for your AI Services resource.')
+param aiServices string = 'aiservices'
+
+// Model deployment parameters
+@description('The name of the model you want to deploy')
+param modelName string = 'gpt-4.1'
+@description('The provider of your model')
+param modelFormat string = 'OpenAI'
+@description('The version of your model')
+param modelVersion string = '2025-04-14'
+@description('The sku of your model deployment')
+param modelSkuName string = 'GlobalStandard'
+@description('The tokens per minute (TPM) of your model deployment')
+param modelCapacity int = 30
+
+// Create a short, unique suffix, that will be unique to each resource group
+param deploymentTimestamp string = utcNow('yyyyMMddHHmmss')
+var uniqueSuffix = substring(uniqueString('${resourceGroup().id}-${deploymentTimestamp}'), 0, 4)
+var accountName = toLower('${aiServices}${uniqueSuffix}')
+
+@description('Name for your project resource.')
+param firstProjectName string = 'project'
+
+@description('This project will be a sub-resource of your account')
+param projectDescription string = 'A project for the AI Foundry account with network secured basic Agent'
+
+@description('The display name of the project')
+param displayName string = 'network secured basic agent project'
+
+// Virtual Network parameters
+@description('Virtual Network name for the Agent to create new or existing virtual network')
+param vnetName string = 'agent-vnet-test'
+
+@description('The name of Agents Subnet to create new or existing subnet for agents')
+param agentSubnetName string = 'agent-subnet'
+
+@description('The name of Private Endpoint subnet to create new or existing subnet for private endpoints')
+param peSubnetName string = 'pe-subnet'
+
+@description('Existing Virtual Network name Resource ID')
+param existingVnetResourceId string = ''
+
+@description('Address space for the VNet (only used for new VNet)')
+param vnetAddressPrefix string = ''
+
+@description('Address prefix for the agent subnet. The default value is 192.168.0.0/24 but you can choose any size /26 or any class like 10.0.0.0 or 172.168.0.0')
+param agentSubnetPrefix string = ''
+
+@description('Address prefix for the private endpoint subnet')
+param peSubnetPrefix string = ''
+
+// DNS zone parameters
+@description('Subscription ID where existing private DNS zones are located. Leave empty to use current subscription.')
+param dnsZonesSubscriptionId string = ''
+
+@description('Object mapping DNS zone names to their resource group, or empty string to indicate creation')
+param existingDnsZones object = {
+ 'privatelink.services.ai.azure.com': ''
+ 'privatelink.openai.azure.com': ''
+ 'privatelink.cognitiveservices.azure.com': ''
+}
+
+@description('The name of the project capability host to be created')
+param projectCapHost string = 'caphostproj'
+
+
+var projectName = toLower('${firstProjectName}${uniqueSuffix}')
+
+// Check if existing VNet has been passed in
+var existingVnetPassedIn = existingVnetResourceId != ''
+
+var vnetParts = split(existingVnetResourceId, '/')
+var vnetSubscriptionId = existingVnetPassedIn ? vnetParts[2] : subscription().subscriptionId
+var vnetResourceGroupName = existingVnetPassedIn ? vnetParts[4] : resourceGroup().name
+var existingVnetName = existingVnetPassedIn ? last(vnetParts) : vnetName
+var trimVnetName = trim(existingVnetName)
+
+// Resolve DNS zones subscription ID - use current subscription if not specified
+var resolvedDnsZonesSubscriptionId = empty(dnsZonesSubscriptionId) ? subscription().subscriptionId : dnsZonesSubscriptionId
+
+
+/*
+ Step 1: Create Virtual Network and Subnets
+ - Agent subnet delegated to Microsoft.App/environments for VNet injection
+ - Private endpoint subnet for secure access to AI Services
+*/
+module vnet 'modules-network-secured/network-agent-vnet.bicep' = {
+ name: 'vnet-${trimVnetName}-${uniqueSuffix}-deployment'
+ params: {
+ location: location
+ vnetName: trimVnetName
+ useExistingVnet: existingVnetPassedIn
+ existingVnetResourceGroupName: vnetResourceGroupName
+ agentSubnetName: agentSubnetName
+ peSubnetName: peSubnetName
+ vnetAddressPrefix: vnetAddressPrefix
+ agentSubnetPrefix: agentSubnetPrefix
+ peSubnetPrefix: peSubnetPrefix
+ existingVnetSubscriptionId: vnetSubscriptionId
+ }
+}
+
+/*
+ Step 2: Create the AI Services account with network injection and model deployment
+ - Network injection points the agent subnet for VNet integration
+ - Public network access is disabled
+ - Model deployment (gpt-4.1 by default)
+*/
+module aiAccount 'modules-network-secured/ai-account-identity.bicep' = {
+ name: '${accountName}-${uniqueSuffix}-deployment'
+ params: {
+ accountName: accountName
+ location: location
+ modelName: modelName
+ modelFormat: modelFormat
+ modelVersion: modelVersion
+ modelSkuName: modelSkuName
+ modelCapacity: modelCapacity
+ agentSubnetId: vnet.outputs.agentSubnetId
+ }
+}
+
+/*
+ Step 3: Private Endpoint and DNS Configuration for AI Services
+ - Creates private endpoint in the PE subnet
+ - Sets up private DNS zones for AI Services, OpenAI, and Cognitive Services
+ - Links DNS zones to the VNet for name resolution
+*/
+module privateEndpointAndDNS 'modules-network-secured/private-endpoint-and-dns.bicep' = {
+ name: '${uniqueSuffix}-private-endpoint'
+ params: {
+ aiAccountName: aiAccount.outputs.accountName
+ vnetName: vnet.outputs.virtualNetworkName
+ peSubnetName: vnet.outputs.peSubnetName
+ suffix: uniqueSuffix
+ vnetResourceGroupName: vnet.outputs.virtualNetworkResourceGroup
+ vnetSubscriptionId: vnet.outputs.virtualNetworkSubscriptionId
+ existingDnsZones: existingDnsZones
+ dnsZonesSubscriptionId: resolvedDnsZonesSubscriptionId
+ }
+}
+
+/*
+ Step 4: Create a Project
+ - Sub-resource of the AI Services account
+ - System-assigned managed identity
+ - No BYO resource connections (basic agent setup)
+*/
+resource account 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = {
+ name: accountName
+}
+
+resource project 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' = {
+ parent: account
+ name: projectName
+ location: location
+ identity: {
+ type: 'SystemAssigned'
+ }
+ properties: {
+ description: projectDescription
+ displayName: displayName
+ }
+ dependsOn: [
+ aiAccount
+ privateEndpointAndDNS
+ ]
+}
+
+/*
+ Step 5: Create the Capability Host for the project
+ - Basic agent capability host (no BYO connections)
+ - Platform-managed resources are used for thread storage, file storage, and vector store
+*/
+module addProjectCapabilityHost 'modules-network-secured/add-project-capability-host.bicep' = {
+ name: 'capabilityHost-configuration-${uniqueSuffix}-deployment'
+ params: {
+ accountName: aiAccount.outputs.accountName
+ projectName: projectName
+ projectCapHost: projectCapHost
+ }
+ dependsOn: [
+ project
+ privateEndpointAndDNS
+ ]
+}
+
+output accountId string = aiAccount.outputs.accountID
+output accountName string = aiAccount.outputs.accountName
+output projectName string = project.name
diff --git a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/main.bicepparam b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/main.bicepparam
new file mode 100644
index 000000000..2ee35c245
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/main.bicepparam
@@ -0,0 +1,28 @@
+using './main.bicep'
+
+param location = 'eastus'
+param aiServices = 'foundry'
+param modelName = 'gpt-4.1'
+param modelFormat = 'OpenAI'
+param modelVersion = '2025-04-14'
+param modelSkuName = 'GlobalStandard'
+param modelCapacity = 30
+param firstProjectName = 'project'
+param projectDescription = 'A project for the AI Foundry account with network secured basic Agent'
+param displayName = 'project'
+param peSubnetName = 'pe-subnet'
+
+// Virtual Network parameters
+// If you provide an existing VNet resource ID, the deployment will use it instead of creating a new one
+param existingVnetResourceId = ''
+param vnetName = 'agent-vnet-test'
+param agentSubnetName = 'agent-subnet'
+
+// DNS zone parameters
+// Leave empty to create new DNS zones, or provide resource group names to use existing ones
+param dnsZonesSubscriptionId = ''
+param existingDnsZones = {
+ 'privatelink.services.ai.azure.com': ''
+ 'privatelink.openai.azure.com': ''
+ 'privatelink.cognitiveservices.azure.com': ''
+}
diff --git a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/main.json b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/main.json
new file mode 100644
index 000000000..9652d3b63
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/main.json
@@ -0,0 +1,1329 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "3777570653041274440"
+ }
+ },
+ "parameters": {
+ "location": {
+ "type": "string",
+ "defaultValue": "eastus",
+ "allowedValues": [
+ "westus",
+ "eastus",
+ "eastus2",
+ "japaneast",
+ "francecentral",
+ "spaincentral",
+ "uaenorth",
+ "southcentralus",
+ "italynorth",
+ "germanywestcentral",
+ "brazilsouth",
+ "southafricanorth",
+ "australiaeast",
+ "swedencentral",
+ "canadaeast",
+ "westeurope",
+ "westus3",
+ "uksouth",
+ "southindia",
+ "koreacentral",
+ "polandcentral",
+ "switzerlandnorth",
+ "norwayeast"
+ ],
+ "metadata": {
+ "description": "Location for all resources."
+ }
+ },
+ "aiServices": {
+ "type": "string",
+ "defaultValue": "aiservices",
+ "metadata": {
+ "description": "Name for your AI Services resource."
+ }
+ },
+ "modelName": {
+ "type": "string",
+ "defaultValue": "gpt-4.1",
+ "metadata": {
+ "description": "The name of the model you want to deploy"
+ }
+ },
+ "modelFormat": {
+ "type": "string",
+ "defaultValue": "OpenAI",
+ "metadata": {
+ "description": "The provider of your model"
+ }
+ },
+ "modelVersion": {
+ "type": "string",
+ "defaultValue": "2025-04-14",
+ "metadata": {
+ "description": "The version of your model"
+ }
+ },
+ "modelSkuName": {
+ "type": "string",
+ "defaultValue": "GlobalStandard",
+ "metadata": {
+ "description": "The sku of your model deployment"
+ }
+ },
+ "modelCapacity": {
+ "type": "int",
+ "defaultValue": 30,
+ "metadata": {
+ "description": "The tokens per minute (TPM) of your model deployment"
+ }
+ },
+ "deploymentTimestamp": {
+ "type": "string",
+ "defaultValue": "[utcNow('yyyyMMddHHmmss')]"
+ },
+ "firstProjectName": {
+ "type": "string",
+ "defaultValue": "project",
+ "metadata": {
+ "description": "Name for your project resource."
+ }
+ },
+ "projectDescription": {
+ "type": "string",
+ "defaultValue": "A project for the AI Foundry account with network secured basic Agent",
+ "metadata": {
+ "description": "This project will be a sub-resource of your account"
+ }
+ },
+ "displayName": {
+ "type": "string",
+ "defaultValue": "network secured basic agent project",
+ "metadata": {
+ "description": "The display name of the project"
+ }
+ },
+ "vnetName": {
+ "type": "string",
+ "defaultValue": "agent-vnet-test",
+ "metadata": {
+ "description": "Virtual Network name for the Agent to create new or existing virtual network"
+ }
+ },
+ "agentSubnetName": {
+ "type": "string",
+ "defaultValue": "agent-subnet",
+ "metadata": {
+ "description": "The name of Agents Subnet to create new or existing subnet for agents"
+ }
+ },
+ "peSubnetName": {
+ "type": "string",
+ "defaultValue": "pe-subnet",
+ "metadata": {
+ "description": "The name of Private Endpoint subnet to create new or existing subnet for private endpoints"
+ }
+ },
+ "existingVnetResourceId": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Existing Virtual Network name Resource ID"
+ }
+ },
+ "vnetAddressPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address space for the VNet (only used for new VNet)"
+ }
+ },
+ "agentSubnetPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address prefix for the agent subnet. The default value is 192.168.0.0/24 but you can choose any size /26 or any class like 10.0.0.0 or 172.168.0.0"
+ }
+ },
+ "peSubnetPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address prefix for the private endpoint subnet"
+ }
+ },
+ "dnsZonesSubscriptionId": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Subscription ID where existing private DNS zones are located. Leave empty to use current subscription."
+ }
+ },
+ "existingDnsZones": {
+ "type": "object",
+ "defaultValue": {
+ "privatelink.services.ai.azure.com": "",
+ "privatelink.openai.azure.com": "",
+ "privatelink.cognitiveservices.azure.com": ""
+ },
+ "metadata": {
+ "description": "Object mapping DNS zone names to their resource group, or empty string to indicate creation"
+ }
+ },
+ "projectCapHost": {
+ "type": "string",
+ "defaultValue": "caphostproj",
+ "metadata": {
+ "description": "The name of the project capability host to be created"
+ }
+ }
+ },
+ "variables": {
+ "uniqueSuffix": "[substring(uniqueString(format('{0}-{1}', resourceGroup().id, parameters('deploymentTimestamp'))), 0, 4)]",
+ "accountName": "[toLower(format('{0}{1}', parameters('aiServices'), variables('uniqueSuffix')))]",
+ "projectName": "[toLower(format('{0}{1}', parameters('firstProjectName'), variables('uniqueSuffix')))]",
+ "existingVnetPassedIn": "[not(equals(parameters('existingVnetResourceId'), ''))]",
+ "vnetParts": "[split(parameters('existingVnetResourceId'), '/')]",
+ "vnetSubscriptionId": "[if(variables('existingVnetPassedIn'), variables('vnetParts')[2], subscription().subscriptionId)]",
+ "vnetResourceGroupName": "[if(variables('existingVnetPassedIn'), variables('vnetParts')[4], resourceGroup().name)]",
+ "existingVnetName": "[if(variables('existingVnetPassedIn'), last(variables('vnetParts')), parameters('vnetName'))]",
+ "trimVnetName": "[trim(variables('existingVnetName'))]",
+ "resolvedDnsZonesSubscriptionId": "[if(empty(parameters('dnsZonesSubscriptionId')), subscription().subscriptionId, parameters('dnsZonesSubscriptionId'))]"
+ },
+ "resources": [
+ {
+ "type": "Microsoft.CognitiveServices/accounts/projects",
+ "apiVersion": "2025-04-01-preview",
+ "name": "[format('{0}/{1}', variables('accountName'), variables('projectName'))]",
+ "location": "[parameters('location')]",
+ "identity": {
+ "type": "SystemAssigned"
+ },
+ "properties": {
+ "description": "[parameters('projectDescription')]",
+ "displayName": "[parameters('displayName')]"
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix')))]",
+ "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "[format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "location": {
+ "value": "[parameters('location')]"
+ },
+ "vnetName": {
+ "value": "[variables('trimVnetName')]"
+ },
+ "useExistingVnet": {
+ "value": "[variables('existingVnetPassedIn')]"
+ },
+ "existingVnetResourceGroupName": {
+ "value": "[variables('vnetResourceGroupName')]"
+ },
+ "agentSubnetName": {
+ "value": "[parameters('agentSubnetName')]"
+ },
+ "peSubnetName": {
+ "value": "[parameters('peSubnetName')]"
+ },
+ "vnetAddressPrefix": {
+ "value": "[parameters('vnetAddressPrefix')]"
+ },
+ "agentSubnetPrefix": {
+ "value": "[parameters('agentSubnetPrefix')]"
+ },
+ "peSubnetPrefix": {
+ "value": "[parameters('peSubnetPrefix')]"
+ },
+ "existingVnetSubscriptionId": {
+ "value": "[variables('vnetSubscriptionId')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "9522150535342725348"
+ }
+ },
+ "parameters": {
+ "location": {
+ "type": "string",
+ "metadata": {
+ "description": "Azure region for the deployment"
+ }
+ },
+ "vnetName": {
+ "type": "string",
+ "metadata": {
+ "description": "The name of the virtual network"
+ }
+ },
+ "useExistingVnet": {
+ "type": "bool",
+ "defaultValue": false,
+ "metadata": {
+ "description": "Indicates if an existing VNet should be used"
+ }
+ },
+ "existingVnetSubscriptionId": {
+ "type": "string",
+ "defaultValue": "[subscription().subscriptionId]",
+ "metadata": {
+ "description": "Subscription ID of the existing VNet (if different from current subscription)"
+ }
+ },
+ "existingVnetResourceGroupName": {
+ "type": "string",
+ "defaultValue": "[resourceGroup().name]",
+ "metadata": {
+ "description": "Resource Group name of the existing VNet (if different from current resource group)"
+ }
+ },
+ "agentSubnetName": {
+ "type": "string",
+ "defaultValue": "agent-subnet",
+ "metadata": {
+ "description": "The name of Agents Subnet"
+ }
+ },
+ "peSubnetName": {
+ "type": "string",
+ "defaultValue": "pe-subnet",
+ "metadata": {
+ "description": "The name of Private Endpoint subnet"
+ }
+ },
+ "vnetAddressPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address space for the VNet (only used for new VNet)"
+ }
+ },
+ "agentSubnetPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address prefix for the agent subnet"
+ }
+ },
+ "peSubnetPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address prefix for the private endpoint subnet"
+ }
+ }
+ },
+ "resources": [
+ {
+ "condition": "[not(parameters('useExistingVnet'))]",
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "vnet-deployment",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "location": {
+ "value": "[parameters('location')]"
+ },
+ "vnetName": {
+ "value": "[parameters('vnetName')]"
+ },
+ "agentSubnetName": {
+ "value": "[parameters('agentSubnetName')]"
+ },
+ "peSubnetName": {
+ "value": "[parameters('peSubnetName')]"
+ },
+ "vnetAddressPrefix": {
+ "value": "[parameters('vnetAddressPrefix')]"
+ },
+ "agentSubnetPrefix": {
+ "value": "[parameters('agentSubnetPrefix')]"
+ },
+ "peSubnetPrefix": {
+ "value": "[parameters('peSubnetPrefix')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "1725519900292599397"
+ }
+ },
+ "parameters": {
+ "location": {
+ "type": "string",
+ "metadata": {
+ "description": "Azure region for the deployment"
+ }
+ },
+ "vnetName": {
+ "type": "string",
+ "defaultValue": "agents-vnet-test",
+ "metadata": {
+ "description": "The name of the virtual network"
+ }
+ },
+ "agentSubnetName": {
+ "type": "string",
+ "defaultValue": "agent-subnet",
+ "metadata": {
+ "description": "The name of Agents Subnet"
+ }
+ },
+ "peSubnetName": {
+ "type": "string",
+ "defaultValue": "pe-subnet",
+ "metadata": {
+ "description": "The name of Hub subnet"
+ }
+ },
+ "vnetAddressPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address space for the VNet"
+ }
+ },
+ "agentSubnetPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address prefix for the agent subnet"
+ }
+ },
+ "peSubnetPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address prefix for the private endpoint subnet"
+ }
+ }
+ },
+ "variables": {
+ "defaultVnetAddressPrefix": "192.168.0.0/16",
+ "vnetAddress": "[if(empty(parameters('vnetAddressPrefix')), variables('defaultVnetAddressPrefix'), parameters('vnetAddressPrefix'))]",
+ "agentSubnet": "[if(empty(parameters('agentSubnetPrefix')), cidrSubnet(variables('vnetAddress'), 24, 0), parameters('agentSubnetPrefix'))]",
+ "peSubnet": "[if(empty(parameters('peSubnetPrefix')), cidrSubnet(variables('vnetAddress'), 24, 1), parameters('peSubnetPrefix'))]"
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Network/virtualNetworks",
+ "apiVersion": "2024-05-01",
+ "name": "[parameters('vnetName')]",
+ "location": "[parameters('location')]",
+ "properties": {
+ "addressSpace": {
+ "addressPrefixes": [
+ "[variables('vnetAddress')]"
+ ]
+ },
+ "subnets": [
+ {
+ "name": "[parameters('agentSubnetName')]",
+ "properties": {
+ "addressPrefix": "[variables('agentSubnet')]",
+ "delegations": [
+ {
+ "name": "Microsoft.app/environments",
+ "properties": {
+ "serviceName": "Microsoft.App/environments"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "[parameters('peSubnetName')]",
+ "properties": {
+ "addressPrefix": "[variables('peSubnet')]"
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "outputs": {
+ "peSubnetName": {
+ "type": "string",
+ "value": "[parameters('peSubnetName')]"
+ },
+ "agentSubnetName": {
+ "type": "string",
+ "value": "[parameters('agentSubnetName')]"
+ },
+ "agentSubnetId": {
+ "type": "string",
+ "value": "[format('{0}/subnets/{1}', resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('agentSubnetName'))]"
+ },
+ "peSubnetId": {
+ "type": "string",
+ "value": "[format('{0}/subnets/{1}', resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('peSubnetName'))]"
+ },
+ "virtualNetworkName": {
+ "type": "string",
+ "value": "[parameters('vnetName')]"
+ },
+ "virtualNetworkId": {
+ "type": "string",
+ "value": "[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
+ },
+ "virtualNetworkResourceGroup": {
+ "type": "string",
+ "value": "[resourceGroup().name]"
+ },
+ "virtualNetworkSubscriptionId": {
+ "type": "string",
+ "value": "[subscription().subscriptionId]"
+ }
+ }
+ }
+ }
+ },
+ {
+ "condition": "[parameters('useExistingVnet')]",
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "existing-vnet-deployment",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "vnetName": {
+ "value": "[parameters('vnetName')]"
+ },
+ "vnetResourceGroupName": {
+ "value": "[parameters('existingVnetResourceGroupName')]"
+ },
+ "vnetSubscriptionId": {
+ "value": "[parameters('existingVnetSubscriptionId')]"
+ },
+ "agentSubnetName": {
+ "value": "[parameters('agentSubnetName')]"
+ },
+ "peSubnetName": {
+ "value": "[parameters('peSubnetName')]"
+ },
+ "agentSubnetPrefix": {
+ "value": "[parameters('agentSubnetPrefix')]"
+ },
+ "peSubnetPrefix": {
+ "value": "[parameters('peSubnetPrefix')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "12834877200138369468"
+ }
+ },
+ "parameters": {
+ "vnetName": {
+ "type": "string",
+ "metadata": {
+ "description": "The name of the existing virtual network"
+ }
+ },
+ "vnetSubscriptionId": {
+ "type": "string",
+ "defaultValue": "[subscription().subscriptionId]",
+ "metadata": {
+ "description": "Subscription ID of virtual network (if different from current subscription)"
+ }
+ },
+ "vnetResourceGroupName": {
+ "type": "string",
+ "defaultValue": "[resourceGroup().name]",
+ "metadata": {
+ "description": "Resource Group name of the existing VNet (if different from current resource group)"
+ }
+ },
+ "agentSubnetName": {
+ "type": "string",
+ "defaultValue": "agent-subnet",
+ "metadata": {
+ "description": "The name of Agents Subnet"
+ }
+ },
+ "peSubnetName": {
+ "type": "string",
+ "defaultValue": "pe-subnet",
+ "metadata": {
+ "description": "The name of Private Endpoint subnet"
+ }
+ },
+ "agentSubnetPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address prefix for the agent subnet (only needed if creating new subnet)"
+ }
+ },
+ "peSubnetPrefix": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Address prefix for the private endpoint subnet (only needed if creating new subnet)"
+ }
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "[format('agent-subnet-{0}', uniqueString(deployment().name, parameters('agentSubnetName')))]",
+ "resourceGroup": "[parameters('vnetResourceGroupName')]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "vnetName": {
+ "value": "[parameters('vnetName')]"
+ },
+ "subnetName": {
+ "value": "[parameters('agentSubnetName')]"
+ },
+ "addressPrefix": "[if(empty(parameters('agentSubnetPrefix')), createObject('value', cidrSubnet(reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), '2024-05-01').addressSpace.addressPrefixes[0], 24, 0)), createObject('value', parameters('agentSubnetPrefix')))]",
+ "delegations": {
+ "value": [
+ {
+ "name": "Microsoft.App/environments",
+ "properties": {
+ "serviceName": "Microsoft.App/environments"
+ }
+ }
+ ]
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "16684585228229443328"
+ }
+ },
+ "parameters": {
+ "vnetName": {
+ "type": "string",
+ "metadata": {
+ "description": "Name of the virtual network"
+ }
+ },
+ "subnetName": {
+ "type": "string",
+ "metadata": {
+ "description": "Name of the subnet"
+ }
+ },
+ "addressPrefix": {
+ "type": "string",
+ "metadata": {
+ "description": "Address prefix for the subnet"
+ }
+ },
+ "delegations": {
+ "type": "array",
+ "defaultValue": [],
+ "metadata": {
+ "description": "Array of subnet delegations"
+ }
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Network/virtualNetworks/subnets",
+ "apiVersion": "2024-05-01",
+ "name": "[format('{0}/{1}', parameters('vnetName'), parameters('subnetName'))]",
+ "properties": {
+ "addressPrefix": "[parameters('addressPrefix')]",
+ "delegations": "[parameters('delegations')]"
+ }
+ }
+ ],
+ "outputs": {
+ "subnetId": {
+ "type": "string",
+ "value": "[resourceId('Microsoft.Network/virtualNetworks/subnets', split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[0], split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[1])]"
+ },
+ "subnetName": {
+ "type": "string",
+ "value": "[parameters('subnetName')]"
+ }
+ }
+ }
+ }
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "[format('pe-subnet-{0}', uniqueString(deployment().name, parameters('peSubnetName')))]",
+ "resourceGroup": "[parameters('vnetResourceGroupName')]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "vnetName": {
+ "value": "[parameters('vnetName')]"
+ },
+ "subnetName": {
+ "value": "[parameters('peSubnetName')]"
+ },
+ "addressPrefix": "[if(empty(parameters('peSubnetPrefix')), createObject('value', cidrSubnet(reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), '2024-05-01').addressSpace.addressPrefixes[0], 24, 1)), createObject('value', parameters('peSubnetPrefix')))]",
+ "delegations": {
+ "value": []
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "16684585228229443328"
+ }
+ },
+ "parameters": {
+ "vnetName": {
+ "type": "string",
+ "metadata": {
+ "description": "Name of the virtual network"
+ }
+ },
+ "subnetName": {
+ "type": "string",
+ "metadata": {
+ "description": "Name of the subnet"
+ }
+ },
+ "addressPrefix": {
+ "type": "string",
+ "metadata": {
+ "description": "Address prefix for the subnet"
+ }
+ },
+ "delegations": {
+ "type": "array",
+ "defaultValue": [],
+ "metadata": {
+ "description": "Array of subnet delegations"
+ }
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Network/virtualNetworks/subnets",
+ "apiVersion": "2024-05-01",
+ "name": "[format('{0}/{1}', parameters('vnetName'), parameters('subnetName'))]",
+ "properties": {
+ "addressPrefix": "[parameters('addressPrefix')]",
+ "delegations": "[parameters('delegations')]"
+ }
+ }
+ ],
+ "outputs": {
+ "subnetId": {
+ "type": "string",
+ "value": "[resourceId('Microsoft.Network/virtualNetworks/subnets', split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[0], split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[1])]"
+ },
+ "subnetName": {
+ "type": "string",
+ "value": "[parameters('subnetName')]"
+ }
+ }
+ }
+ }
+ }
+ ],
+ "outputs": {
+ "peSubnetName": {
+ "type": "string",
+ "value": "[parameters('peSubnetName')]"
+ },
+ "agentSubnetName": {
+ "type": "string",
+ "value": "[parameters('agentSubnetName')]"
+ },
+ "agentSubnetId": {
+ "type": "string",
+ "value": "[format('{0}/subnets/{1}', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('agentSubnetName'))]"
+ },
+ "peSubnetId": {
+ "type": "string",
+ "value": "[format('{0}/subnets/{1}', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('peSubnetName'))]"
+ },
+ "virtualNetworkName": {
+ "type": "string",
+ "value": "[parameters('vnetName')]"
+ },
+ "virtualNetworkId": {
+ "type": "string",
+ "value": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
+ },
+ "virtualNetworkResourceGroup": {
+ "type": "string",
+ "value": "[parameters('vnetResourceGroupName')]"
+ },
+ "virtualNetworkSubscriptionId": {
+ "type": "string",
+ "value": "[parameters('vnetSubscriptionId')]"
+ }
+ }
+ }
+ }
+ }
+ ],
+ "outputs": {
+ "virtualNetworkName": {
+ "type": "string",
+ "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.virtualNetworkName.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.virtualNetworkName.value)]"
+ },
+ "virtualNetworkId": {
+ "type": "string",
+ "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.virtualNetworkId.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.virtualNetworkId.value)]"
+ },
+ "virtualNetworkSubscriptionId": {
+ "type": "string",
+ "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.virtualNetworkSubscriptionId.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.virtualNetworkSubscriptionId.value)]"
+ },
+ "virtualNetworkResourceGroup": {
+ "type": "string",
+ "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.virtualNetworkResourceGroup.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.virtualNetworkResourceGroup.value)]"
+ },
+ "agentSubnetName": {
+ "type": "string",
+ "value": "[parameters('agentSubnetName')]"
+ },
+ "peSubnetName": {
+ "type": "string",
+ "value": "[parameters('peSubnetName')]"
+ },
+ "agentSubnetId": {
+ "type": "string",
+ "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.agentSubnetId.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.agentSubnetId.value)]"
+ },
+ "peSubnetId": {
+ "type": "string",
+ "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.peSubnetId.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.peSubnetId.value)]"
+ }
+ }
+ }
+ }
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "[format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "accountName": {
+ "value": "[variables('accountName')]"
+ },
+ "location": {
+ "value": "[parameters('location')]"
+ },
+ "modelName": {
+ "value": "[parameters('modelName')]"
+ },
+ "modelFormat": {
+ "value": "[parameters('modelFormat')]"
+ },
+ "modelVersion": {
+ "value": "[parameters('modelVersion')]"
+ },
+ "modelSkuName": {
+ "value": "[parameters('modelSkuName')]"
+ },
+ "modelCapacity": {
+ "value": "[parameters('modelCapacity')]"
+ },
+ "agentSubnetId": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.agentSubnetId.value]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "481468620289904255"
+ }
+ },
+ "parameters": {
+ "accountName": {
+ "type": "string"
+ },
+ "location": {
+ "type": "string"
+ },
+ "modelName": {
+ "type": "string"
+ },
+ "modelFormat": {
+ "type": "string"
+ },
+ "modelVersion": {
+ "type": "string"
+ },
+ "modelSkuName": {
+ "type": "string"
+ },
+ "modelCapacity": {
+ "type": "int"
+ },
+ "agentSubnetId": {
+ "type": "string"
+ },
+ "networkInjection": {
+ "type": "string",
+ "defaultValue": "true"
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.CognitiveServices/accounts",
+ "apiVersion": "2025-04-01-preview",
+ "name": "[parameters('accountName')]",
+ "location": "[parameters('location')]",
+ "sku": {
+ "name": "S0"
+ },
+ "kind": "AIServices",
+ "identity": {
+ "type": "SystemAssigned"
+ },
+ "properties": {
+ "allowProjectManagement": true,
+ "customSubDomainName": "[parameters('accountName')]",
+ "networkAcls": {
+ "defaultAction": "Deny",
+ "virtualNetworkRules": [],
+ "ipRules": [],
+ "bypass": "AzureServices"
+ },
+ "publicNetworkAccess": "Disabled",
+ "networkInjections": "[if(equals(parameters('networkInjection'), 'true'), createArray(createObject('scenario', 'agent', 'subnetArmId', parameters('agentSubnetId'), 'useMicrosoftManagedNetwork', false())), null())]",
+ "disableLocalAuth": false
+ }
+ },
+ {
+ "type": "Microsoft.CognitiveServices/accounts/deployments",
+ "apiVersion": "2025-04-01-preview",
+ "name": "[format('{0}/{1}', parameters('accountName'), parameters('modelName'))]",
+ "sku": {
+ "capacity": "[parameters('modelCapacity')]",
+ "name": "[parameters('modelSkuName')]"
+ },
+ "properties": {
+ "model": {
+ "name": "[parameters('modelName')]",
+ "format": "[parameters('modelFormat')]",
+ "version": "[parameters('modelVersion')]"
+ }
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName'))]"
+ ]
+ }
+ ],
+ "outputs": {
+ "accountName": {
+ "type": "string",
+ "value": "[parameters('accountName')]"
+ },
+ "accountID": {
+ "type": "string",
+ "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName'))]"
+ },
+ "accountTarget": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName')), '2025-04-01-preview').endpoint]"
+ },
+ "accountPrincipalId": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName')), '2025-04-01-preview', 'full').identity.principalId]"
+ }
+ }
+ }
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "[format('{0}-private-endpoint', variables('uniqueSuffix'))]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "aiAccountName": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))), '2025-04-01').outputs.accountName.value]"
+ },
+ "vnetName": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.virtualNetworkName.value]"
+ },
+ "peSubnetName": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.peSubnetName.value]"
+ },
+ "suffix": {
+ "value": "[variables('uniqueSuffix')]"
+ },
+ "vnetResourceGroupName": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.virtualNetworkResourceGroup.value]"
+ },
+ "vnetSubscriptionId": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.virtualNetworkSubscriptionId.value]"
+ },
+ "existingDnsZones": {
+ "value": "[parameters('existingDnsZones')]"
+ },
+ "dnsZonesSubscriptionId": {
+ "value": "[variables('resolvedDnsZonesSubscriptionId')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "11715965448884311497"
+ }
+ },
+ "parameters": {
+ "aiAccountName": {
+ "type": "string",
+ "metadata": {
+ "description": "Name of the AI Foundry account"
+ }
+ },
+ "vnetName": {
+ "type": "string",
+ "metadata": {
+ "description": "Name of the Vnet"
+ }
+ },
+ "peSubnetName": {
+ "type": "string",
+ "metadata": {
+ "description": "Name of the private endpoint subnet"
+ }
+ },
+ "suffix": {
+ "type": "string",
+ "metadata": {
+ "description": "Suffix for unique resource names"
+ }
+ },
+ "vnetResourceGroupName": {
+ "type": "string",
+ "defaultValue": "[resourceGroup().name]",
+ "metadata": {
+ "description": "Resource Group name for existing Virtual Network (if different from current resource group)"
+ }
+ },
+ "vnetSubscriptionId": {
+ "type": "string",
+ "defaultValue": "[subscription().subscriptionId]",
+ "metadata": {
+ "description": "Subscription ID for Virtual Network"
+ }
+ },
+ "existingDnsZones": {
+ "type": "object",
+ "defaultValue": {
+ "privatelink.services.ai.azure.com": "",
+ "privatelink.openai.azure.com": "",
+ "privatelink.cognitiveservices.azure.com": ""
+ },
+ "metadata": {
+ "description": "Map of DNS zone FQDNs to resource group names. If provided, reference existing DNS zones in this resource group instead of creating them."
+ }
+ },
+ "dnsZonesSubscriptionId": {
+ "type": "string",
+ "metadata": {
+ "description": "Subscription ID where existing private DNS zones are located. Should be resolved to current subscription if empty."
+ }
+ }
+ },
+ "variables": {
+ "aiServicesDnsZoneName": "privatelink.services.ai.azure.com",
+ "openAiDnsZoneName": "privatelink.openai.azure.com",
+ "cognitiveServicesDnsZoneName": "privatelink.cognitiveservices.azure.com",
+ "aiServicesDnsZoneRG": "[parameters('existingDnsZones')[variables('aiServicesDnsZoneName')]]",
+ "openAiDnsZoneRG": "[parameters('existingDnsZones')[variables('openAiDnsZoneName')]]",
+ "cognitiveServicesDnsZoneRG": "[parameters('existingDnsZones')[variables('cognitiveServicesDnsZoneName')]]",
+ "aiServicesDnsZoneId": "[if(empty(variables('aiServicesDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('aiServicesDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('dnsZonesSubscriptionId'), variables('aiServicesDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('aiServicesDnsZoneName')))]",
+ "openAiDnsZoneId": "[if(empty(variables('openAiDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('openAiDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('dnsZonesSubscriptionId'), variables('openAiDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('openAiDnsZoneName')))]",
+ "cognitiveServicesDnsZoneId": "[if(empty(variables('cognitiveServicesDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('cognitiveServicesDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('dnsZonesSubscriptionId'), variables('cognitiveServicesDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('cognitiveServicesDnsZoneName')))]"
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Network/privateEndpoints",
+ "apiVersion": "2024-05-01",
+ "name": "[format('{0}-private-endpoint', parameters('aiAccountName'))]",
+ "location": "[resourceGroup().location]",
+ "properties": {
+ "subnet": {
+ "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('peSubnetName'))]"
+ },
+ "privateLinkServiceConnections": [
+ {
+ "name": "[format('{0}-private-link-service-connection', parameters('aiAccountName'))]",
+ "properties": {
+ "privateLinkServiceId": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('aiAccountName'))]",
+ "groupIds": [
+ "account"
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "condition": "[empty(variables('aiServicesDnsZoneRG'))]",
+ "type": "Microsoft.Network/privateDnsZones",
+ "apiVersion": "2020-06-01",
+ "name": "[variables('aiServicesDnsZoneName')]",
+ "location": "global"
+ },
+ {
+ "condition": "[empty(variables('openAiDnsZoneRG'))]",
+ "type": "Microsoft.Network/privateDnsZones",
+ "apiVersion": "2020-06-01",
+ "name": "[variables('openAiDnsZoneName')]",
+ "location": "global"
+ },
+ {
+ "condition": "[empty(variables('cognitiveServicesDnsZoneRG'))]",
+ "type": "Microsoft.Network/privateDnsZones",
+ "apiVersion": "2020-06-01",
+ "name": "[variables('cognitiveServicesDnsZoneName')]",
+ "location": "global"
+ },
+ {
+ "condition": "[empty(variables('aiServicesDnsZoneRG'))]",
+ "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
+ "apiVersion": "2024-06-01",
+ "name": "[format('{0}/{1}', variables('aiServicesDnsZoneName'), format('aiServices-{0}-link', parameters('suffix')))]",
+ "location": "global",
+ "properties": {
+ "virtualNetwork": {
+ "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
+ },
+ "registrationEnabled": false
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Network/privateDnsZones', variables('aiServicesDnsZoneName'))]"
+ ]
+ },
+ {
+ "condition": "[empty(variables('openAiDnsZoneRG'))]",
+ "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
+ "apiVersion": "2024-06-01",
+ "name": "[format('{0}/{1}', variables('openAiDnsZoneName'), format('aiServicesOpenAI-{0}-link', parameters('suffix')))]",
+ "location": "global",
+ "properties": {
+ "virtualNetwork": {
+ "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
+ },
+ "registrationEnabled": false
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Network/privateDnsZones', variables('openAiDnsZoneName'))]"
+ ]
+ },
+ {
+ "condition": "[empty(variables('cognitiveServicesDnsZoneRG'))]",
+ "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
+ "apiVersion": "2024-06-01",
+ "name": "[format('{0}/{1}', variables('cognitiveServicesDnsZoneName'), format('aiServicesCognitiveServices-{0}-link', parameters('suffix')))]",
+ "location": "global",
+ "properties": {
+ "virtualNetwork": {
+ "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
+ },
+ "registrationEnabled": false
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Network/privateDnsZones', variables('cognitiveServicesDnsZoneName'))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups",
+ "apiVersion": "2024-05-01",
+ "name": "[format('{0}/{1}', format('{0}-private-endpoint', parameters('aiAccountName')), format('{0}-dns-group', parameters('aiAccountName')))]",
+ "properties": {
+ "privateDnsZoneConfigs": [
+ {
+ "name": "[format('{0}-dns-aiserv-config', parameters('aiAccountName'))]",
+ "properties": {
+ "privateDnsZoneId": "[variables('aiServicesDnsZoneId')]"
+ }
+ },
+ {
+ "name": "[format('{0}-dns-openai-config', parameters('aiAccountName'))]",
+ "properties": {
+ "privateDnsZoneId": "[variables('openAiDnsZoneId')]"
+ }
+ },
+ {
+ "name": "[format('{0}-dns-cogserv-config', parameters('aiAccountName'))]",
+ "properties": {
+ "privateDnsZoneId": "[variables('cognitiveServicesDnsZoneId')]"
+ }
+ }
+ ]
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('aiAccountName')))]",
+ "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('aiServicesDnsZoneName'), format('aiServices-{0}-link', parameters('suffix')))]",
+ "[resourceId('Microsoft.Network/privateDnsZones', variables('aiServicesDnsZoneName'))]",
+ "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('cognitiveServicesDnsZoneName'), format('aiServicesCognitiveServices-{0}-link', parameters('suffix')))]",
+ "[resourceId('Microsoft.Network/privateDnsZones', variables('cognitiveServicesDnsZoneName'))]",
+ "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('openAiDnsZoneName'), format('aiServicesOpenAI-{0}-link', parameters('suffix')))]",
+ "[resourceId('Microsoft.Network/privateDnsZones', variables('openAiDnsZoneName'))]"
+ ]
+ }
+ ]
+ }
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix')))]",
+ "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "[format('capabilityHost-configuration-{0}-deployment', variables('uniqueSuffix'))]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "accountName": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))), '2025-04-01').outputs.accountName.value]"
+ },
+ "projectName": {
+ "value": "[variables('projectName')]"
+ },
+ "projectCapHost": {
+ "value": "[parameters('projectCapHost')]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "17607216443982035670"
+ }
+ },
+ "parameters": {
+ "projectName": {
+ "type": "string"
+ },
+ "accountName": {
+ "type": "string"
+ },
+ "projectCapHost": {
+ "type": "string"
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.CognitiveServices/accounts/projects/capabilityHosts",
+ "apiVersion": "2025-04-01-preview",
+ "name": "[format('{0}/{1}/{2}', parameters('accountName'), parameters('projectName'), parameters('projectCapHost'))]",
+ "properties": {
+ "capabilityHostKind": "Agents"
+ }
+ }
+ ],
+ "outputs": {
+ "projectCapHost": {
+ "type": "string",
+ "value": "[parameters('projectCapHost')]"
+ }
+ }
+ }
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix')))]",
+ "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]",
+ "[resourceId('Microsoft.CognitiveServices/accounts/projects', variables('accountName'), variables('projectName'))]"
+ ]
+ }
+ ],
+ "outputs": {
+ "accountId": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))), '2025-04-01').outputs.accountID.value]"
+ },
+ "accountName": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))), '2025-04-01').outputs.accountName.value]"
+ },
+ "projectName": {
+ "type": "string",
+ "value": "[variables('projectName')]"
+ }
+ }
+}
\ No newline at end of file
diff --git a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/metadata.json b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/metadata.json
new file mode 100644
index 000000000..bc42e410d
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/metadata.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://aka.ms/azure-quickstart-templates-metadata-schema#",
+ "type": "QuickStart",
+ "itemDisplayName": "Network Secured Basic Agent Setup with VNet Injection",
+ "description": "This template demonstrates how to set up Foundry with a basic agent configuration using VNet injection for network isolation.",
+ "summary": "This template demonstrates how to set up Foundry with a basic agent using your own virtual network for network-secured agent workloads.",
+ "githubUsername": "meerakurup",
+ "dateUpdated": "2026-05-04",
+ "environments": [
+ "AzureCloud"
+ ]
+}
diff --git a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/add-project-capability-host.bicep b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/add-project-capability-host.bicep
new file mode 100644
index 000000000..155eeffb8
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/add-project-capability-host.bicep
@@ -0,0 +1,22 @@
+param projectName string
+param accountName string
+param projectCapHost string
+
+resource account 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = {
+ name: accountName
+}
+
+resource project 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' existing = {
+ name: projectName
+ parent: account
+}
+
+resource projectCapabilityHost 'Microsoft.CognitiveServices/accounts/projects/capabilityHosts@2025-04-01-preview' = {
+ name: projectCapHost
+ parent: project
+ properties: {
+ capabilityHostKind: 'Agents'
+ }
+}
+
+output projectCapHost string = projectCapabilityHost.name
diff --git a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/ai-account-identity.bicep b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/ai-account-identity.bicep
new file mode 100644
index 000000000..47f72a3be
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/ai-account-identity.bicep
@@ -0,0 +1,63 @@
+param accountName string
+param location string
+param modelName string
+param modelFormat string
+param modelVersion string
+param modelSkuName string
+param modelCapacity int
+param agentSubnetId string
+param networkInjection string = 'true'
+
+#disable-next-line BCP036
+resource account 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = {
+ name: accountName
+ location: location
+ sku: {
+ name: 'S0'
+ }
+ kind: 'AIServices'
+ identity: {
+ type: 'SystemAssigned'
+ }
+ properties: {
+ allowProjectManagement: true
+ customSubDomainName: accountName
+ networkAcls: {
+ defaultAction: 'Deny'
+ virtualNetworkRules: []
+ ipRules: []
+ bypass:'AzureServices'
+ }
+ publicNetworkAccess: 'Disabled'
+ networkInjections:((networkInjection == 'true') ? [
+ {
+ scenario: 'agent'
+ subnetArmId: agentSubnetId
+ useMicrosoftManagedNetwork: false
+ }
+ ] : null )
+ disableLocalAuth: false
+ }
+}
+
+#disable-next-line BCP081
+resource modelDeployment 'Microsoft.CognitiveServices/accounts/deployments@2025-04-01-preview'= {
+ parent: account
+ name: modelName
+ sku : {
+ capacity: modelCapacity
+ name: modelSkuName
+ }
+ properties: {
+ model:{
+ name: modelName
+ format: modelFormat
+ version: modelVersion
+ }
+ }
+}
+
+output accountName string = account.name
+output accountID string = account.id
+output accountTarget string = account.properties.endpoint
+output accountPrincipalId string = account.identity.principalId
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/existing-vnet.bicep b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/existing-vnet.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/existing-vnet.bicep
rename to infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/existing-vnet.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/network-agent-vnet.bicep b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/network-agent-vnet.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/network-agent-vnet.bicep
rename to infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/network-agent-vnet.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/private-endpoint-and-dns.bicep b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/private-endpoint-and-dns.bicep
new file mode 100644
index 000000000..babc0f76d
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/private-endpoint-and-dns.bicep
@@ -0,0 +1,189 @@
+/*
+Private Endpoint and DNS Configuration Module
+------------------------------------------
+This module configures private network access for the AI Services account using:
+
+1. Private Endpoints:
+ - Creates network interface in the specified subnet
+ - Establishes private connection to AI Services account
+ - Enables secure access without public internet exposure
+
+2. Private DNS Zones:
+ - Enables custom DNS resolution for private endpoints
+
+3. DNS Zone Links:
+ - Links private DNS zones to the VNet
+ - Enables name resolution for resources in the VNet
+ - Prevents DNS resolution conflicts
+
+Security Benefits:
+- Eliminates public internet exposure
+- Enables secure access from within VNet
+- Prevents data exfiltration through network
+*/
+
+// Resource names and identifiers
+@description('Name of the AI Foundry account')
+param aiAccountName string
+@description('Name of the Vnet')
+param vnetName string
+@description('Name of the private endpoint subnet')
+param peSubnetName string
+@description('Suffix for unique resource names')
+param suffix string
+
+@description('Resource Group name for existing Virtual Network (if different from current resource group)')
+param vnetResourceGroupName string = resourceGroup().name
+
+@description('Subscription ID for Virtual Network')
+param vnetSubscriptionId string = subscription().subscriptionId
+
+@description('Map of DNS zone FQDNs to resource group names. If provided, reference existing DNS zones in this resource group instead of creating them.')
+param existingDnsZones object = {
+ 'privatelink.services.ai.azure.com': ''
+ 'privatelink.openai.azure.com': ''
+ 'privatelink.cognitiveservices.azure.com': ''
+}
+
+@description('Subscription ID where existing private DNS zones are located. Should be resolved to current subscription if empty.')
+param dnsZonesSubscriptionId string
+
+// ---- Resource references ----
+resource aiAccount 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = {
+ name: aiAccountName
+ scope: resourceGroup()
+}
+
+// Reference existing network resources
+resource vnet 'Microsoft.Network/virtualNetworks@2024-05-01' existing = {
+ name: vnetName
+ scope: resourceGroup(vnetSubscriptionId, vnetResourceGroupName)
+}
+resource peSubnet 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' existing = {
+ parent: vnet
+ name: peSubnetName
+}
+
+/* -------------------------------------------- AI Foundry Account Private Endpoint -------------------------------------------- */
+
+// Private endpoint for AI Services account
+// - Creates network interface in customer hub subnet
+// - Establishes private connection to AI Services account
+resource aiAccountPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = {
+ name: '${aiAccountName}-private-endpoint'
+ location: resourceGroup().location
+ properties: {
+ subnet: { id: peSubnet.id } // Deploy in customer hub subnet
+ privateLinkServiceConnections: [
+ {
+ name: '${aiAccountName}-private-link-service-connection'
+ properties: {
+ privateLinkServiceId: aiAccount.id
+ groupIds: [ 'account' ] // Target AI Services account
+ }
+ }
+ ]
+ }
+}
+
+/* -------------------------------------------- Private DNS Zones -------------------------------------------- */
+
+// Format: 1) Private DNS Zone
+// 2) Link Private DNS Zone to VNet
+// 3) Create DNS Zone Group for Private Endpoint
+
+var aiServicesDnsZoneName = 'privatelink.services.ai.azure.com'
+var openAiDnsZoneName = 'privatelink.openai.azure.com'
+var cognitiveServicesDnsZoneName = 'privatelink.cognitiveservices.azure.com'
+
+// ---- DNS Zone Resource Group lookups ----
+var aiServicesDnsZoneRG = existingDnsZones[aiServicesDnsZoneName]
+var openAiDnsZoneRG = existingDnsZones[openAiDnsZoneName]
+var cognitiveServicesDnsZoneRG = existingDnsZones[cognitiveServicesDnsZoneName]
+
+// ---- DNS Zone Resources and References ----
+resource aiServicesPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (empty(aiServicesDnsZoneRG)) {
+ name: aiServicesDnsZoneName
+ location: 'global'
+}
+
+// Reference existing private DNS zone if provided
+resource existingAiServicesPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = if (!empty(aiServicesDnsZoneRG)) {
+ name: aiServicesDnsZoneName
+ scope: resourceGroup(dnsZonesSubscriptionId, aiServicesDnsZoneRG)
+}
+//creating condition if user pass existing dns zones or not
+var aiServicesDnsZoneId = empty(aiServicesDnsZoneRG) ? aiServicesPrivateDnsZone.id : existingAiServicesPrivateDnsZone.id
+
+resource openAiPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (empty(openAiDnsZoneRG)) {
+ name: openAiDnsZoneName
+ location: 'global'
+}
+
+// Reference existing private DNS zone if provided
+resource existingOpenAiPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = if (!empty(openAiDnsZoneRG)) {
+ name: openAiDnsZoneName
+ scope: resourceGroup(dnsZonesSubscriptionId, openAiDnsZoneRG)
+}
+//creating condition if user pass existing dns zones or not
+var openAiDnsZoneId = empty(openAiDnsZoneRG) ? openAiPrivateDnsZone.id : existingOpenAiPrivateDnsZone.id
+
+resource cognitiveServicesPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (empty(cognitiveServicesDnsZoneRG)) {
+ name: cognitiveServicesDnsZoneName
+ location: 'global'
+}
+
+// Reference existing private DNS zone if provided
+resource existingCognitiveServicesPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = if (!empty(cognitiveServicesDnsZoneRG)) {
+ name: cognitiveServicesDnsZoneName
+ scope: resourceGroup(dnsZonesSubscriptionId, cognitiveServicesDnsZoneRG)
+}
+//creating condition if user pass existing dns zones or not
+var cognitiveServicesDnsZoneId = empty(cognitiveServicesDnsZoneRG) ? cognitiveServicesPrivateDnsZone.id : existingCognitiveServicesPrivateDnsZone.id
+
+// ---- DNS VNet Links ----
+resource aiServicesLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = if (empty(aiServicesDnsZoneRG)) {
+ parent: aiServicesPrivateDnsZone
+ location: 'global'
+ name: 'aiServices-${suffix}-link'
+ properties: {
+ virtualNetwork: { id: vnet.id }
+ registrationEnabled: false
+ }
+}
+resource openAiLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = if (empty(openAiDnsZoneRG)) {
+ parent: openAiPrivateDnsZone
+ location: 'global'
+ name: 'aiServicesOpenAI-${suffix}-link'
+ properties: {
+ virtualNetwork: { id: vnet.id }
+ registrationEnabled: false
+ }
+}
+resource cognitiveServicesLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = if (empty(cognitiveServicesDnsZoneRG)) {
+ parent: cognitiveServicesPrivateDnsZone
+ location: 'global'
+ name: 'aiServicesCognitiveServices-${suffix}-link'
+ properties: {
+ virtualNetwork: { id: vnet.id }
+ registrationEnabled: false
+ }
+}
+
+// ---- DNS Zone Groups ----
+resource aiServicesDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = {
+ parent: aiAccountPrivateEndpoint
+ name: '${aiAccountName}-dns-group'
+ properties: {
+ privateDnsZoneConfigs: [
+ { name: '${aiAccountName}-dns-aiserv-config', properties: { privateDnsZoneId: aiServicesDnsZoneId } }
+ { name: '${aiAccountName}-dns-openai-config', properties: { privateDnsZoneId: openAiDnsZoneId } }
+ { name: '${aiAccountName}-dns-cogserv-config', properties: { privateDnsZoneId: cognitiveServicesDnsZoneId } }
+ ]
+ }
+ dependsOn: [
+ empty(aiServicesDnsZoneRG) ? aiServicesLink : null
+ empty(openAiDnsZoneRG) ? openAiLink : null
+ empty(cognitiveServicesDnsZoneRG) ? cognitiveServicesLink : null
+ ]
+}
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/subnet.bicep b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/subnet.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/subnet.bicep
rename to infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/subnet.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/vnet.bicep b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/vnet.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/vnet.bicep
rename to infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/vnet.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/README.md b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/README.md
index 9bffa6610..2fde8bc50 100644
--- a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/README.md
+++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/README.md
@@ -1,5 +1,5 @@
---
-description: This set of templates demonstrates how to set up Azure AI Agent Service with virtual network isolation with private network links to connect the agent to your secure data.
+description: This set of templates demonstrates how to set up Foundry Agent Service with virtual network isolation with private network links to connect the agent to your secure data.
page_type: sample
products:
- azure
@@ -10,30 +10,51 @@ languages:
- json
---
-# Azure AI Agent Service: Standard Agent Setup with E2E Network Isolation
+# Microsoft Foundry: Standard Agent Setup with E2E Network Isolation (without Tools behind VNET)
+
+> **NEW**
+> For support on deploying the right network isolation template, check out the [GitHub Copilot for Azure skill for private networking](https://github.com/microsoft/GitHub-Copilot-for-Azure/blob/main/plugin/skills/microsoft-foundry/resource/private-network/private-network.md) set-up!
> **IMPORTANT**
->
-> Private Class A subnet support is GA and available in the following regions. **Supported regions: Australia East, Brazil South, Canada East, East US, East US 2, France Central, Germany West Central, Italy North, Japan East, South Africa North, South Central US, South India, Spain Central, Sweden Central, UAE North, UK South, West Europe, West US, West US 3.**
->
-> Private Class B and C subnet support is already GA and available in all regions supported by Azure AI Foundry Agent Service. Deployment templates and setup steps are identical for Class A, B, and C subnets. For more on the supported regions of the Azure AI Foundry Agent service, see [Models supported by Azure AI Foundry Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/concepts/model-region-support?tabs=global-standard)
+> Please note this template does not support using Agent tools behind a VNET. Please refer to [template 19](../19-private-network-agent-tools/) and the [TESTING-GUIDE.md](../19-private-network-agent-tools/tests/TESTING-GUIDE.md) to ensure tool traffic also goes through your vnet.
---
## Overview
-This infrastructure-as-code (IaC) solution deploys a network-secured Azure AI agent environment with private networking and role-based access control (RBAC).
+This infrastructure-as-code (IaC) solution deploys a network-secured agent environment with private networking and role-based access control (RBAC).
-Standard setup supports private network isolation through utilizing **Bring Your Own Virtual Network (BYO VNet)** approach, also known as **custom VNet support with subnet delegation.**
+Standard setup supports private network isolation through utilizing **Bring Your Own Virtual Network (BYO VNet)** approach, also known as **custom VNet support with subnet delegation.** Please note this template does not support using Agent tools behind a VNET. Please use [template 19](../19-private-network-agent-tools/) for this.
This implementation gives you full control over the inbound and outbound communication paths for your agent. You can restrict access to only the resources explicitly required by your agent, such as storage accounts, databases, or APIs, while blocking all other traffic by default. This approach ensures that your agent operates within a tightly scoped network boundary, reducing the risk of data leakage or unauthorized access. By default, this setup simplifies security configuration while enforcing strong isolation guarantees, ensuring that each agent deployment remains secure, compliant, and aligned with enterprise networking policies.
---
-## Key Information
+## When to Use This Template
+
+Use this template when you need:
+- **Full end-to-end network isolation** β All resources behind private endpoints with no public internet access
+- **BYO VNet control** β You manage your own virtual network, subnets, and network security groups
+- **Standard agent setup with BYO resources** β Customer-managed Storage, Cosmos DB, and AI Search for data residency and compliance
+- **System Assigned Managed Identity** β Simplified identity management with platform-managed credentials
+
+### Template Decision Guide
+
+Use the table below to choose the right infrastructure template for your scenario:
-**Region and Resource Placement Requirements**
-- **All Foundry workspace resources should be in the same region as the VNet**, including CosmosDB, Storage Account, AI Search, Foundry Account, Project, Managed Identity. The only exception is within the Foundry Account, you may choose to deploy your model to a different region, and any cross-region communication will be handled securely within our network infrastructure.
- - **Note:** Your Virtual Network can be in a different resource group than your Foundry workspace resources
+| Template | Agent Type | Networking | Identity | Key Use Case |
+|----------|-----------|------------|----------|-------------|
+| [**15** (this template)](../15-private-network-standard-agent-setup/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | E2E network isolation with full agent capabilities |
+| [**19**](../19-private-network-agent-tools/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | Same as 15 **plus** tools behind VNet (MCP, OpenAPI, Functions, A2A) |
+| [**17**](../17-private-network-standard-user-assigned-identity-agent-setup/) | Standard (BYO resources) | BYO VNet + Private Endpoints | **User Assigned MI** | Same as 15 but with user-managed identity |
+| [**16**](../16-private-network-standard-agent-apim-setup-preview/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | Same as 15 **plus** private APIM integration (preview) |
+| [**18**](../18-managed-virtual-network-preview/) | Standard (BYO resources) | **Managed VNet** (Microsoft-managed) | System Assigned MI | Network isolation without managing your own VNet (preview) |
+| [**15a**](../15a-private-network-evaluation-only-setup/) | Evaluation only | BYO VNet + Private Endpoints | System Assigned MI | Minimal setup for evaluation β no Cosmos DB, AI Search, or capability host |
+| [**11**](../11-private-network-basic-vnet/) | **Basic** (platform-managed) | BYO VNet injection | System Assigned MI | Basic agents with VNet isolation β no BYO resources needed |
+| [**41**](../41-standard-agent-setup/) | Standard (BYO resources) | **Public** (no VNet) | System Assigned MI | Standard agents without network isolation |
+| [**40**](../40-basic-agent-setup/) | **Basic** (platform-managed) | **Public** (no VNet) | System Assigned MI | Simplest setup β no BYO resources, no private networking |
+---
+
+## Deploy to Azure
[](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fazure-ai-foundry%2Ffoundry-samples%2Frefs%2Fheads%2Fmain%2Finfrastructure%2Finfrastructure-setup-bicep%2F15-private-network-standard-agent-setup%2Fazuredeploy.json)
@@ -42,9 +63,9 @@ This implementation gives you full control over the inbound and outbound communi
## Prerequisites
1. **Active Azure subscription with appropriate permissions**
- - **Azure AI Account Owner**: Needed to create a cognitive services account and project
- - **Owner or Role Based Access Administrator**: Needed to assign RBAC to the required resources (Cosmos DB, Azure AI Search, Storage)
- - **Azure AI User**: Needed to create and edit agents
+ - **Foundry Account Owner**: Needed to create the Microsoft Foundry account and project.
+ - **Owner or Role Based Access Administrator**: Needed to assign RBAC on the Azure resources used by this template.
+ - **Foundry User**: Needed to create and use agents, projects, or evaluation workloads after deployment.
1. **Register Resource Providers**
@@ -62,8 +83,8 @@ This implementation gives you full control over the inbound and outbound communi
1. Network administrator permissions (if operating in a restricted or enterprise environment)
-1. Sufficient quota for all resources in your target Azure region
- * If no parameters are passed in, this template creates an Azure AI Foundry resource, Foundry project, Azure Cosmos DB for NoSQL, Azure AI Search, and Azure Storage account
+1. Sufficient quota for all resources required by this template in the target Azure region, including model deployment quota.
+ * If no parameters are passed in, this template creates an Microsoft Foundry resource, Foundry project, Azure Cosmos DB for NoSQL, Azure AI Search, and Azure Storage account
1. Azure CLI installed and configured on your local workstation or deployment pipeline server
---
@@ -81,10 +102,21 @@ This implementation gives you full control over the inbound and outbound communi
> **Notes:**
- If you do not provide an existing virtual network, the template will create a new virtual network with the default address spaces and subnets described above. If you use an existing virtual network, make sure it already contains two subnets (Agent and Private Endpoint) before deploying the template.
- - You must ensure the Foundry account was successfully created so that underlying caphost has also succeeded. Then proceed to deploying the project caphost bicep.
- - You must ensure the subnet is not already in use by another account. It must be an exclusive subnet for the Foundry account.
+ - The account-level capability host is now provisioned declaratively by `modules-network-secured/add-account-capability-host.bicep` as part of `main.bicep`. The standalone `createCapHost.sh` script is no longer required for first-time deployments; it remains in the folder only to support the cleanup-then-recreate flow described in the [Account Deletion Prerequisites and Cleanup Guidance](#account-deletion-prerequisites-and-cleanup-guidance).
- You must ensure the subnet is exclusively delegated to __Microsoft.App/environments__ and cannot be used by any other Azure resources.
-
+
+
+
+### Limitations / Known Issues
+
+1. The delegated agent subnet must be exclusively used by a single Foundry account. It cannot be shared across accounts.
+2. The Foundry resource and the virtual network must be in the same Azure region. BYO resources (Storage, Cosmos DB, AI Search) may be in different regions.
+3. For the virtual network IP range, you may use any Private Class A, B or C IP range. Private Class A IP address ranges (10.x.x.x) are only supported in the following regions: **Australia East, Brazil South, Canada East, East US, East US 2, France Central, Germany West Central, Italy North, Japan East, South Africa North, South Central US, South India, Spain Central, Sweden Central, UAE North, UK South, West US, West US 3.** Use Class B (172.16.x.x) or C (192.168.x.x) ranges for other regions. You may not use any other IP range that overlaps to the list above or uses public IP ranges.
+4. This template does **not** support tools (MCP servers, OpenAPI tools, Azure Functions, A2A) behind the VNet. Use [template 19](../19-private-network-agent-tools/) for that scenario.
+5. There is no upgrade path from BYO VNet (this template) to Managed Virtual Network (template 18). A Foundry resource redeployment is required.
+6. All projects within the same Foundry account share model deployments. Per-project model isolation is not supported.
+7. Cosmos DB is deployed as single-region. Multi-region replication must be configured manually post-deployment.
+8. When reusing an existing Foundry account (`existingAiFoundryAccountResourceId`), the template will not create a new model deployment if `skipModelDeployment` is set to `true`. The required model deployment(s) must already exist on the BYO account.
### Account Deletion Prerequisites and Cleanup Guidance
@@ -96,6 +128,8 @@ Before deleting an **Account** resource, it is essential to first delete the ass
**2. Retain Account, Remove Capability Host**: If you intend to retain the account but remove the capability host, execute the script `deleteCaphost.sh` located in this folder. After deletion, allow approximately max of 20 minutes for all resources to be fully unlinked from the account. To recreate the capability host for the account, use the script `createCaphost.sh` located in the same folder.
+> **Note**: The account-level capability host is created declaratively by `main.bicep` (via `modules-network-secured/add-account-capability-host.bicep`) on first deployment. The `createCapHost.sh` script is intended for this cleanup-then-recreate scenario only; it is not required for an initial deployment.
+
> **Important**: Before deleting the account capability host, ensure that the **project capability host** is deleted.
@@ -109,6 +143,34 @@ Note: If not provided, the following resources will be created automatically for
#### Parameters
+| Parameter | Description | Default | Required |
+|-----------|-------------|---------|----------|
+| `location` | Azure region for deployment | `eastus` | Yes |
+| `aiServices` | Base name for the AI Services resource | `aiservices` | No |
+| `firstProjectName` | Name for the Foundry project | `project` | No |
+| `modelName` | Model to deploy | `gpt-4.1` | No |
+| `modelFormat` | Model provider | `OpenAI` | No |
+| `modelVersion` | Model version | `2025-04-14` | No |
+| `modelSkuName` | Model deployment SKU | `GlobalStandard` | No |
+| `modelCapacity` | Tokens per minute (TPM) capacity | `30` | No |
+| `skipModelDeployment` | When `true`, skip creating a model deployment. Recommended when reusing an existing Foundry account that already has the required model deployments. | `false` | No |
+| `vnetName` | Virtual Network name. When `existingVnetResourceId` is set, the name is derived from that resource ID and this parameter is ignored. When creating a new VNet, leave empty to use the generated default. | `''` | No |
+| `agentSubnetName` | Subnet name for agent workloads | `agent-subnet` | No |
+| `agentSubnetPrefix` | Address prefix for agent subnet | `192.168.0.0/24` | No |
+| `peSubnetName` | Subnet name for private endpoints | `pe-subnet` | No |
+| `peSubnetPrefix` | Address prefix for PE subnet | `192.168.1.0/24` | No |
+| `existingVnetResourceId` | Full ARM Resource ID of an existing VNet | `''` (creates new) | No |
+| `reuseExistingSubnets` | When `true` and `existingVnetResourceId` is set, the template will reference your existing subnets without modifying them. Use this when your subnets are already configured by your platform team (NSG, route tables, private endpoint network policies) and tenant policies forbid changes. | `false` | No |
+| `vnetAddressPrefix` | Address space for new VNet | `192.168.0.0/16` | No |
+| `aiSearchResourceId` | ARM Resource ID of existing AI Search | `''` (creates new) | No |
+| `azureStorageAccountResourceId` | ARM Resource ID of existing Storage account | `''` (creates new) | No |
+| `azureCosmosDBAccountResourceId` | ARM Resource ID of existing Cosmos DB | `''` (creates new) | No |
+| `existingAiFoundryAccountResourceId` | Full ARM Resource ID of an existing Microsoft Foundry (Cognitive Services / AIServices) account to reuse. When set, the template will not create a new account. | `''` (creates new) | No |
+| `dnsZonesSubscriptionId` | Subscription ID for existing DNS zones. Accepts either a bare GUID (``) or a full ARM subscription path (`/subscriptions/`); the template normalizes the value internally. | `''` (current sub) | No |
+| `existingDnsZones` | Map of DNS zone names to resource groups | All empty (creates new) | No |
+
+#### BYO Resource Details
+
1. **Use Existing Virtual Network and Subnets**
To use an existing VNet and subnets, set the existingVnetResourceId parameter to the full Azure Resource ID of the target VNet and its address range, and provide the names of the two required subnets. If the existing VNet is associated with private DNS zones, set the existingDnsZones parameter to the resource group name in which the zones are located. For example:
@@ -126,7 +188,9 @@ To use an existing VNet and subnets, set the existingVnetResourceId parameter to
π‘ If subnets information is provided then make sure it exist within the specified VNet to avoid deployment errors. If subnet information is not provided, the template will create subnets with the default address space.
-π‘ **Cross-Subscription DNS Zones**: All DNS zones specified in `existingDnsZones` will be referenced from the subscription specified in `dnsZonesSubscriptionId`. Leave this parameter empty (default) to use the current deployment subscription, or set it to a subscription ID if your DNS zones are located in a different subscription.
+π‘ **Reuse pre-configured subnets**: If your subnets are already configured by your platform team (NSG, route tables, `privateEndpointNetworkPolicies` set per tenant policy), set `reuseExistingSubnets = true`. This tells the template to reference the subnets without re-applying their configuration, which prevents an inadvertent reset of subnet properties on redeploy.
+
+π‘ **Cross-Subscription DNS Zones**: All DNS zones specified in `existingDnsZones` will be referenced from the subscription specified in `dnsZonesSubscriptionId`. Leave this parameter empty (default) to use the current deployment subscription, or set it to a subscription ID if your DNS zones are located in a different subscription. The parameter accepts either a bare subscription GUID or a full ARM subscription path (`/subscriptions/`); the template normalizes the value internally.
β οΈ **Important**: When `dnsZonesSubscriptionId` is set to a different subscription, ALL DNS zones in `existingDnsZones` must have resource groups specified (non-empty values). The template does not support creating new DNS zones in a different subscription. Empty resource groups are only allowed when creating zones in the current deployment subscription.
@@ -136,25 +200,6 @@ To use an existing VNet and subnets, set the existingVnetResourceId parameter to
To use an existing Cosmos DB for NoSQL resource, set cosmosDBResourceId parameter to the full Azure Resource ID of the target Cosmos DB.
- param azureCosmosDBAccountResourceId string = /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/{cosmosDbAccountName}
-> **β οΈ Important: Cosmos DB Connection Requirements**
->
-> When creating the Cosmos DB connection (e.g., via REST API or ARM), ensure the following:
-> - The `authType` **must** be set to `AAD`. This is the only supported authentication type for the Cosmos DB connection used by the Agent Service.
-> - The `metadata` section **must** include the `ResourceId` property, set to the full Azure Resource ID of your Cosmos DB account. The Agent Service relies on this property to correctly identify and connect to your Cosmos DB resource. Omitting `ResourceId` from the metadata will cause the connection to fail.
->
-> Example connection properties:
-> ```json
-> {
-> "category": "CosmosDB",
-> "authType": "AAD",
-> "metadata": {
-> "ApiType": "Azure",
-> "ResourceId": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/{cosmosDbAccountName}",
-> "location": "{region}"
-> }
-> }
-> ```
-
3. **Use an existing Azure AI Search resource**
@@ -167,6 +212,18 @@ To use an existing Azure AI Search resource, set aiSearchServiceResourceId param
To use an existing Azure Storage account, set aiStorageAccountResourceId parameter to the full Azure resource Id of the target Azure Storage account resource.
- param aiStorageAccountResourceId string = /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{storageAccountName}
+
+5. **Use an existing Microsoft Foundry account**
+
+To reuse an existing Microsoft Foundry (Cognitive Services / AIServices kind) account instead of creating a new one, set `existingAiFoundryAccountResourceId` to the full Azure Resource ID of the target account. The template will reference the existing account, scope the account-level capability host to its resource group and subscription, and skip the deterministic-suffix account creation path (which would otherwise create a new account on every redeploy).
+
+- param existingAiFoundryAccountResourceId string = '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{accountName}'
+- param skipModelDeployment bool = true // recommended when the BYO account already has the required model deployment(s)
+
+π‘ **When to use this**: bring-your-own-account is intended for scenarios where the Foundry account is provisioned ahead of time by a platform team or landing zone, and the workload deployment must reuse it (for compliance, naming standards, or to avoid orphaned accounts on retry).
+
+β οΈ **Important**: When `existingAiFoundryAccountResourceId` is set, the required model deployment(s) must already exist on the BYO account if `skipModelDeployment = true`. The agent service depends on at least one model deployment matching `modelName` / `modelVersion` to function.
+
---
## Deploy the bicep template
@@ -191,15 +248,70 @@ Click the deploy to Azure button above to open the Azure portal and deploy the t
az deployment group create --resource-group --template-file main.bicep --parameters main.bicepparam
```
-> **Note:** To access your Foundry resource securely, use either a VM, VPN, or ExpressRoute.
+> **Note:** To access a private Foundry resource securely, use one of the following:
+> - A VM or jump box on the virtual network, optionally accessed through Azure Bastion
+> - Azure VPN Gateway
+> - Azure ExpressRoute
+
+### Cleanup
+
+To delete all resources created by this template:
+
+```bash
+az group delete --name --yes --no-wait
+```
+
+> **Important**: If you need to reuse the same subnet, follow the [Account Deletion Prerequisites and Cleanup Guidance](#account-deletion-prerequisites-and-cleanup-guidance) to properly purge the account and wait for the capability host to fully unlink (~20 minutes).
---
## Network Secured Agent Project Architecture Deep Dive
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Secure Access (VPN Gateway / ExpressRoute / Azure Bastion) β
+ββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
+ β
+ ββββββββββββββββΌβββββββββββββββ
+ β Microsoft Foundry β
+ β (publicNetworkAccess: β
+ β DISABLED) β
+ β β
+ β ββββββββββββββββββββββββββ β
+ β β Foundry Project β β
+ β β (Agent Workspace) β β
+ β βββββββββββββ¬βββββββββββββ β
+ ββββββββββββββββΌβββββββββββββββ
+ β Subnet Delegation
+ ββββββββββββββββΌβββββββββββββββ
+ β BYO Virtual Network β
+ β (192.168.0.0/16) β
+ β β
+ β ββββββββββββββββββββββββ β
+ β β Agent Subnet β β
+ β β (192.168.0.0/24) β β βββ Delegated to
+ β β Microsoft.App/envs β β Microsoft.App/environments
+ β ββββββββββββββββββββββββ β
+ β β
+ β ββββββββββββββββββββββββ β
+ β β PE Subnet β β
+ β β (192.168.1.0/24) β β
+ β β β β
+ β β ββββββββββ ββββββββββ β β
+ β β βStorage β βCosmos β β β βββ Private endpoints
+ β β ββββββββββ ββββββββββ β β (no public access)
+ β β ββββββββββ ββββββββββ β β
+ β β βSearch β βFoundry β β β
+ β β ββββββββββ ββββββββββ β β
+ β ββββββββββββββββββββββββ β
+ ββββββββββββββββββββββββββββββββ
+```
+
+> **Tip:** For detailed layer-by-layer deployment diagrams, see the `diagrams/` folder.
+
### Core Components
-**Azure AI Foundry** resource
+**Microsoft Foundry** resource
- Central orchestration point
- Manages service connections
- Set networking and policy configurations
@@ -216,11 +328,11 @@ Click the deploy to Azure button above to open the Azure portal and deploy the t
- BYO Search: All vector stores created by the agent leverage the customerβs Azure AI Search resource.
- BYO Thread Storage: All customer messages and conversation history will be stored in the customerβs own Azure Cosmos DB account.
-By bundling these BYO features (file storage, search, and thread storage), the standard setup guarantees that your deployment is secure by default. All data processed by Azure AI Foundry Agent Service is automatically stored at rest in your own Azure resources, helping you meet internal policies, compliance requirements, and enterprise security standards.
+By bundling these BYO features (file storage, search, and thread storage), the standard setup guarantees that your deployment is secure by default. All data processed by Microsoft Foundry Agent Service is automatically stored at rest in your own Azure resources, helping you meet internal policies, compliance requirements, and enterprise security standards.
### Azure Resources Created
-Azure AI Foundry (Cognitive Services)
+Microsoft Foundry (Cognitive Services)
- Type: Microsoft.CognitiveServices/accounts
- API version: 2025-04-01-preview
- Kind: AIServices
@@ -291,7 +403,7 @@ Network Security
**Private Endpoints**
Private endpoints ensure secure, internal-only connectivity. Private endpoints are created for the following:
-- Azure AI Foundry
+- Microsoft Foundry
- Azure AI Search
- Azure Storage
- Azure Cosmos DB
@@ -299,7 +411,7 @@ Private endpoints ensure secure, internal-only connectivity. Private endpoints a
**Private DNS Zones**
| Private Link Resource Type | Sub Resource | Private DNS Zone Name | Public DNS Zone Forwarders |
|----------------------------|--------------|------------------------|-----------------------------|
-| **Azure AI Foundry** | account | `privatelink.cognitiveservices.azure.com`
`privatelink.openai.azure.com`
`privatelink.services.ai.azure.com` | `cognitiveservices.azure.com`
`openai.azure.com`
`services.ai.azure.com` |
+| **Microsoft Foundry** | account | `privatelink.cognitiveservices.azure.com`
`privatelink.openai.azure.com`
`privatelink.services.ai.azure.com` | `cognitiveservices.azure.com`
`openai.azure.com`
`services.ai.azure.com` |
| **Azure AI Search** | searchService| `privatelink.search.windows.net` | `search.windows.net` |
| **Azure Cosmos DB** | Sql | `privatelink.documents.azure.com` | `documents.azure.com` |
| **Azure Storage** | blob | `privatelink.blob.core.windows.net` | `blob.core.windows.net` |
@@ -340,8 +452,9 @@ Private endpoints ensure secure, internal-only connectivity. Private endpoints a
```text
modules-network-secured/
+βββ add-account-capability-host.bicep # Declarative account-level capability host (replaces createCapHost.sh for first-time deployments)
βββ add-project-capability-host.bicep # Configuring the project's capability host
-βββ ai-account-identity.bicep # Azure AI Foundry deployment and configuration
+βββ ai-account-identity.bicep # Microsoft Foundry deployment and configuration (supports BYO existing account)
βββ ai-project-identity.bicep # Foundry project deployment and connection configuration
βββ ai-search-role-assignments.bicep # AI Search RBAC configuration
βββ azure-storage-account-role-assignments.bicep # Storage Account RBAC configuration
@@ -358,8 +471,6 @@ modules-network-secured/
βββ vnet.bicep # Deploying a new virtual network
```
-> **Note:** If you bring your own VNET for this template, ensure the subnet for Agents has the correct subnet delegation to `Microsoft.App/environments`. If you have not specified the delegated subnet, the template will complete this for you.
-
## Maintenance
### Regular Tasks
@@ -377,8 +488,7 @@ modules-network-secured/
4. Review network security groups
---
----
-# (Optional) Adding Multiple Projects to AI Foundry Deployment
+# (Optional) Adding Multiple Projects to Foundry Deployment
This guide explains how to add additional projects to your existing AI Foundry deployment with network security and capability hosts.
@@ -541,8 +651,8 @@ Each new project deployment creates:
## References
-- [Azure AI Foundry Networking Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link?tabs=azure-portal&pivots=fdp-project)
-- [Azure AI Foundry RBAC Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/rbac-azure-ai-foundry?pivots=fdp-project)
+- [Microsoft Foundry Networking Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link?tabs=azure-portal&pivots=fdp-project)
+- [Microsoft Foundry RBAC Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/rbac-azure-ai-foundry?pivots=fdp-project)
- [Private Endpoint Documentation](https://learn.microsoft.com/en-us/azure/private-link/)
- [RBAC Documentation](https://learn.microsoft.com/en-us/azure/role-based-access-control/)
- [Network Security Best Practices](https://learn.microsoft.com/en-us/azure/security/fundamentals/network-best-practices)
diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/azuredeploy.json b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/azuredeploy.json
index 8c09508fa..fac50d7df 100644
--- a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/azuredeploy.json
+++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/azuredeploy.json
@@ -4,8 +4,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.40.2.10011",
- "templateHash": "10213309447334439675"
+ "version": "0.43.8.12551",
+ "templateHash": "4437106568758821389"
}
},
"parameters": {
@@ -110,9 +110,9 @@
},
"vnetName": {
"type": "string",
- "defaultValue": "agent-vnet-test",
+ "defaultValue": "",
"metadata": {
- "description": "Virtual Network name for the Agent to create new or existing virtual network"
+ "description": "Virtual Network name. Required ONLY when creating a NEW VNet (existingVnetResourceId is empty). When existingVnetResourceId is set, this value is IGNORED, the name is derived from the resource ID. If you supply both they should match; otherwise the resource ID wins."
}
},
"agentSubnetName": {
@@ -157,6 +157,27 @@
"description": "Address prefix for the private endpoint subnet"
}
},
+ "reuseExistingSubnets": {
+ "type": "bool",
+ "defaultValue": false,
+ "metadata": {
+ "description": "When true and existingVnetResourceId is set, the template will NOT modify your existing subnets."
+ }
+ },
+ "existingAiFoundryAccountResourceId": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Optional. Full ARM resource ID of an existing AI Foundry (CognitiveServices/accounts kind=AIServices) account to reuse. When set, the template will NOT create a new account."
+ }
+ },
+ "skipModelDeployment": {
+ "type": "bool",
+ "defaultValue": false,
+ "metadata": {
+ "description": "Optional. When true, skip the model deployment. Recommended when reusing an existing account that already has the required model deployments."
+ }
+ },
"aiSearchResourceId": {
"type": "string",
"defaultValue": "",
@@ -224,10 +245,18 @@
"variables": {
"uniqueSuffix": "[substring(uniqueString(format('{0}-{1}', resourceGroup().id, parameters('deploymentTimestamp'))), 0, 4)]",
"accountName": "[toLower(format('{0}{1}', parameters('aiServices'), variables('uniqueSuffix')))]",
+ "useExistingAccount": "[not(empty(parameters('existingAiFoundryAccountResourceId')))]",
+ "existingAccountIdParts": "[split(parameters('existingAiFoundryAccountResourceId'), '/')]",
+ "existingAccountSubscriptionId": "[if(variables('useExistingAccount'), variables('existingAccountIdParts')[2], subscription().subscriptionId)]",
+ "existingAccountResourceGroupName": "[if(variables('useExistingAccount'), variables('existingAccountIdParts')[4], resourceGroup().name)]",
"projectName": "[toLower(format('{0}{1}', parameters('firstProjectName'), variables('uniqueSuffix')))]",
- "cosmosDBName": "[toLower(format('{0}{1}cosmosdb', parameters('aiServices'), variables('uniqueSuffix')))]",
+ "aiServicesSanitized": "[toLower(replace(parameters('aiServices'), '-', ''))]",
+ "storagePrefixMax": 18,
+ "storagePrefix": "[if(greater(length(variables('aiServicesSanitized')), variables('storagePrefixMax')), substring(variables('aiServicesSanitized'), 0, variables('storagePrefixMax')), variables('aiServicesSanitized'))]",
+ "azureStorageName": "[format('{0}{1}st', variables('storagePrefix'), variables('uniqueSuffix'))]",
+ "cosmosDBNameRaw": "[toLower(format('{0}{1}cosmosdb', parameters('aiServices'), variables('uniqueSuffix')))]",
+ "cosmosDBName": "[if(greater(length(variables('cosmosDBNameRaw')), 44), substring(variables('cosmosDBNameRaw'), 0, 44), variables('cosmosDBNameRaw'))]",
"aiSearchName": "[toLower(format('{0}{1}search', parameters('aiServices'), variables('uniqueSuffix')))]",
- "azureStorageName": "[toLower(format('{0}{1}storage', parameters('aiServices'), variables('uniqueSuffix')))]",
"storagePassedIn": "[not(equals(parameters('azureStorageAccountResourceId'), ''))]",
"searchPassedIn": "[not(equals(parameters('aiSearchResourceId'), ''))]",
"cosmosPassedIn": "[not(equals(parameters('azureCosmosDBAccountResourceId'), ''))]",
@@ -246,7 +275,8 @@
"vnetResourceGroupName": "[if(variables('existingVnetPassedIn'), variables('vnetParts')[4], resourceGroup().name)]",
"existingVnetName": "[if(variables('existingVnetPassedIn'), last(variables('vnetParts')), parameters('vnetName'))]",
"trimVnetName": "[trim(variables('existingVnetName'))]",
- "resolvedDnsZonesSubscriptionId": "[if(empty(parameters('dnsZonesSubscriptionId')), subscription().subscriptionId, parameters('dnsZonesSubscriptionId'))]"
+ "normalizedDnsZonesSubscriptionId": "[if(empty(parameters('dnsZonesSubscriptionId')), '', if(startsWith(toLower(parameters('dnsZonesSubscriptionId')), '/subscriptions/'), split(parameters('dnsZonesSubscriptionId'), '/')[2], parameters('dnsZonesSubscriptionId')))]",
+ "resolvedDnsZonesSubscriptionId": "[if(empty(variables('normalizedDnsZonesSubscriptionId')), subscription().subscriptionId, variables('normalizedDnsZonesSubscriptionId'))]"
},
"resources": [
{
@@ -288,6 +318,9 @@
},
"existingVnetSubscriptionId": {
"value": "[variables('vnetSubscriptionId')]"
+ },
+ "reuseExistingSubnets": {
+ "value": "[parameters('reuseExistingSubnets')]"
}
},
"template": {
@@ -296,8 +329,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.40.2.10011",
- "templateHash": "9522150535342725348"
+ "version": "0.43.8.12551",
+ "templateHash": "1854398928287753773"
}
},
"parameters": {
@@ -368,6 +401,13 @@
"metadata": {
"description": "Address prefix for the private endpoint subnet"
}
+ },
+ "reuseExistingSubnets": {
+ "type": "bool",
+ "defaultValue": false,
+ "metadata": {
+ "description": "When true and useExistingVnet=true, do NOT modify the existing subnets, reference them as-is."
+ }
}
},
"resources": [
@@ -410,8 +450,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.40.2.10011",
- "templateHash": "1725519900292599397"
+ "version": "0.43.8.12551",
+ "templateHash": "6615847355060514198"
}
},
"parameters": {
@@ -575,6 +615,9 @@
},
"peSubnetPrefix": {
"value": "[parameters('peSubnetPrefix')]"
+ },
+ "reuseExistingSubnets": {
+ "value": "[parameters('reuseExistingSubnets')]"
}
},
"template": {
@@ -583,8 +626,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.40.2.10011",
- "templateHash": "12834877200138369468"
+ "version": "0.43.8.12551",
+ "templateHash": "8011604849876491209"
}
},
"parameters": {
@@ -635,10 +678,18 @@
"metadata": {
"description": "Address prefix for the private endpoint subnet (only needed if creating new subnet)"
}
+ },
+ "reuseExistingSubnets": {
+ "type": "bool",
+ "defaultValue": false,
+ "metadata": {
+ "description": "When true, do NOT modify the existing subnets, reference them as-is. Recommended when the caller manages subnet config (NSG/RT/PE policies) outside this template."
+ }
}
},
"resources": [
{
+ "condition": "[not(parameters('reuseExistingSubnets'))]",
"type": "Microsoft.Resources/deployments",
"apiVersion": "2025-04-01",
"name": "[format('agent-subnet-{0}', uniqueString(deployment().name, parameters('agentSubnetName')))]",
@@ -673,8 +724,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.40.2.10011",
- "templateHash": "16684585228229443328"
+ "version": "0.43.8.12551",
+ "templateHash": "11349014942847443024"
}
},
"parameters": {
@@ -729,6 +780,7 @@
}
},
{
+ "condition": "[not(parameters('reuseExistingSubnets'))]",
"type": "Microsoft.Resources/deployments",
"apiVersion": "2025-04-01",
"name": "[format('pe-subnet-{0}', uniqueString(deployment().name, parameters('peSubnetName')))]",
@@ -756,8 +808,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.40.2.10011",
- "templateHash": "16684585228229443328"
+ "version": "0.43.8.12551",
+ "templateHash": "11349014942847443024"
}
},
"parameters": {
@@ -920,6 +972,12 @@
},
"agentSubnetId": {
"value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.agentSubnetId.value]"
+ },
+ "existingAccountResourceId": {
+ "value": "[parameters('existingAiFoundryAccountResourceId')]"
+ },
+ "skipModelDeployment": {
+ "value": "[parameters('skipModelDeployment')]"
}
},
"template": {
@@ -928,8 +986,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.40.2.10011",
- "templateHash": "481468620289904255"
+ "version": "0.43.8.12551",
+ "templateHash": "2832622634761787610"
}
},
"parameters": {
@@ -960,10 +1018,32 @@
"networkInjection": {
"type": "string",
"defaultValue": "true"
+ },
+ "existingAccountResourceId": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Optional. Full ARM resource ID of an existing AI Foundry (CognitiveServices/accounts kind=AIServices) account to reuse. When set, the template will NOT create a new account."
+ }
+ },
+ "skipModelDeployment": {
+ "type": "bool",
+ "defaultValue": false,
+ "metadata": {
+ "description": "Optional. When true, skip the model deployment. Recommended when reusing an existing account that already has the required model deployments."
+ }
}
},
+ "variables": {
+ "useExistingAccount": "[not(empty(parameters('existingAccountResourceId')))]",
+ "existingParts": "[split(parameters('existingAccountResourceId'), '/')]",
+ "existingAccountSub": "[if(variables('useExistingAccount'), variables('existingParts')[2], subscription().subscriptionId)]",
+ "existingAccountRg": "[if(variables('useExistingAccount'), variables('existingParts')[4], resourceGroup().name)]",
+ "existingAccountName": "[if(variables('useExistingAccount'), last(variables('existingParts')), parameters('accountName'))]"
+ },
"resources": [
{
+ "condition": "[not(variables('useExistingAccount'))]",
"type": "Microsoft.CognitiveServices/accounts",
"apiVersion": "2025-04-01-preview",
"name": "[parameters('accountName')]",
@@ -990,6 +1070,7 @@
}
},
{
+ "condition": "[and(not(variables('useExistingAccount')), not(parameters('skipModelDeployment')))]",
"type": "Microsoft.CognitiveServices/accounts/deployments",
"apiVersion": "2025-04-01-preview",
"name": "[format('{0}/{1}', parameters('accountName'), parameters('modelName'))]",
@@ -1012,19 +1093,19 @@
"outputs": {
"accountName": {
"type": "string",
- "value": "[parameters('accountName')]"
+ "value": "[if(variables('useExistingAccount'), variables('existingAccountName'), parameters('accountName'))]"
},
"accountID": {
"type": "string",
- "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName'))]"
+ "value": "[if(variables('useExistingAccount'), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('existingAccountSub'), variables('existingAccountRg')), 'Microsoft.CognitiveServices/accounts', variables('existingAccountName')), resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName')))]"
},
"accountTarget": {
"type": "string",
- "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName')), '2025-04-01-preview').endpoint]"
+ "value": "[if(variables('useExistingAccount'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('existingAccountSub'), variables('existingAccountRg')), 'Microsoft.CognitiveServices/accounts', variables('existingAccountName')), '2025-04-01-preview').endpoint, reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName')), '2025-04-01-preview').endpoint)]"
},
"accountPrincipalId": {
"type": "string",
- "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName')), '2025-04-01-preview', 'full').identity.principalId]"
+ "value": "[if(variables('useExistingAccount'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('existingAccountSub'), variables('existingAccountRg')), 'Microsoft.CognitiveServices/accounts', variables('existingAccountName')), '2025-04-01-preview', 'full').identity.principalId, reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName')), '2025-04-01-preview', 'full').identity.principalId)]"
}
}
}
@@ -1068,8 +1149,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.40.2.10011",
- "templateHash": "13265653374239491155"
+ "version": "0.43.8.12551",
+ "templateHash": "680740366057404341"
}
},
"parameters": {
@@ -1226,8 +1307,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.40.2.10011",
- "templateHash": "1228922674432785221"
+ "version": "0.43.8.12551",
+ "templateHash": "14267398792843698911"
}
},
"parameters": {
@@ -1509,8 +1590,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.40.2.10011",
- "templateHash": "6108847882161636110"
+ "version": "0.43.8.12551",
+ "templateHash": "16240696292444732987"
}
},
"parameters": {
@@ -2044,8 +2125,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.40.2.10011",
- "templateHash": "1007872875847158534"
+ "version": "0.43.8.12551",
+ "templateHash": "5316230502381000043"
}
},
"parameters": {
@@ -2219,8 +2300,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.40.2.10011",
- "templateHash": "12606978205101260380"
+ "version": "0.43.8.12551",
+ "templateHash": "16292565234555392040"
}
},
"parameters": {
@@ -2274,8 +2355,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.40.2.10011",
- "templateHash": "15991712531324316353"
+ "version": "0.43.8.12551",
+ "templateHash": "10985949897065090550"
}
},
"parameters": {
@@ -2332,8 +2413,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.40.2.10011",
- "templateHash": "7256419427588810161"
+ "version": "0.43.8.12551",
+ "templateHash": "17331576029113373855"
}
},
"parameters": {
@@ -2396,8 +2477,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.40.2.10011",
- "templateHash": "15867124606695536257"
+ "version": "0.43.8.12551",
+ "templateHash": "15580039133385622424"
}
},
"parameters": {
@@ -2446,6 +2527,81 @@
"[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]"
]
},
+ {
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "[format('account-caphost-{0}-deployment', variables('uniqueSuffix'))]",
+ "subscriptionId": "[variables('existingAccountSubscriptionId')]",
+ "resourceGroup": "[variables('existingAccountResourceGroupName')]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "accountName": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))), '2025-04-01').outputs.accountName.value]"
+ },
+ "agentSubnetResourceId": {
+ "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.agentSubnetId.value]"
+ }
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.43.8.12551",
+ "templateHash": "18033747207003499362"
+ }
+ },
+ "parameters": {
+ "accountName": {
+ "type": "string",
+ "metadata": {
+ "description": "Name of the AI Foundry (Cognitive Services) account"
+ }
+ },
+ "accountCapHost": {
+ "type": "string",
+ "defaultValue": "caphostacct",
+ "metadata": {
+ "description": "Name of the account-level capabilityHost"
+ }
+ },
+ "agentSubnetResourceId": {
+ "type": "string",
+ "metadata": {
+ "description": "ARM resource ID of the customer agent subnet (delegated to Microsoft.App/environments)"
+ }
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.CognitiveServices/accounts/capabilityHosts",
+ "apiVersion": "2025-04-01-preview",
+ "name": "[format('{0}/{1}', parameters('accountName'), parameters('accountCapHost'))]",
+ "properties": {
+ "capabilityHostKind": "Agents",
+ "customerSubnet": "[parameters('agentSubnetResourceId')]"
+ }
+ }
+ ],
+ "outputs": {
+ "accountCapHostName": {
+ "type": "string",
+ "value": "[parameters('accountCapHost')]"
+ }
+ }
+ }
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix')))]",
+ "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]",
+ "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]"
+ ]
+ },
{
"type": "Microsoft.Resources/deployments",
"apiVersion": "2025-04-01",
@@ -2481,8 +2637,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.40.2.10011",
- "templateHash": "687161311666023487"
+ "version": "0.43.8.12551",
+ "templateHash": "9752585897450875181"
}
},
"parameters": {
@@ -2538,6 +2694,7 @@
}
},
"dependsOn": [
+ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('existingAccountSubscriptionId'), variables('existingAccountResourceGroupName')), 'Microsoft.Resources/deployments', format('account-caphost-{0}-deployment', variables('uniqueSuffix')))]",
"[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix')))]",
"[resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix')))]",
"[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]",
@@ -2575,8 +2732,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.40.2.10011",
- "templateHash": "3625425571119261380"
+ "version": "0.43.8.12551",
+ "templateHash": "13556924300142468207"
}
},
"parameters": {
@@ -2654,8 +2811,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.40.2.10011",
- "templateHash": "14722474518981746838"
+ "version": "0.43.8.12551",
+ "templateHash": "9820545713493490823"
}
},
"parameters": {
diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/diagrams/deployment_flow.png b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/diagrams/deployment_flow.png
new file mode 100644
index 000000000..df9269567
Binary files /dev/null and b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/diagrams/deployment_flow.png differ
diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/diagrams/layer1_network_foundation.png b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/diagrams/layer1_network_foundation.png
new file mode 100644
index 000000000..18e1a36c6
Binary files /dev/null and b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/diagrams/layer1_network_foundation.png differ
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/diagrams/layer2_data_resources.png b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/diagrams/layer2_data_resources.png
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/diagrams/layer2_data_resources.png
rename to infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/diagrams/layer2_data_resources.png
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/diagrams/layer3_ai_services.png b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/diagrams/layer3_ai_services.png
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/diagrams/layer3_ai_services.png
rename to infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/diagrams/layer3_ai_services.png
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/diagrams/layer4_project_connections.png b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/diagrams/layer4_project_connections.png
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/diagrams/layer4_project_connections.png
rename to infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/diagrams/layer4_project_connections.png
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/diagrams/layer5_capability_host.png b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/diagrams/layer5_capability_host.png
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/diagrams/layer5_capability_host.png
rename to infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/diagrams/layer5_capability_host.png
diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.bicep b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.bicep
index 8c8a9a601..1c760fceb 100644
--- a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.bicep
+++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.bicep
@@ -62,8 +62,14 @@ param projectDescription string = 'A project for the AI Foundry account with net
param displayName string = 'network secured agent project'
// Existing Virtual Network parameters
-@description('Virtual Network name for the Agent to create new or existing virtual network')
-param vnetName string = 'agent-vnet-test'
+// vnetName precedence + UX clarity.
+// When existingVnetResourceId is set, vnetName is IGNORED and the actual name
+// is derived from the resource ID (`last(vnetParts)`). The original default
+// 'agent-vnet-test' was misleading: users who copied it as-is alongside
+// existingVnetResourceId thought they were targeting a specific VNet but the
+// resource ID won, hiding mistakes. Default is now empty.
+@description('Virtual Network name. Required ONLY when creating a NEW VNet (existingVnetResourceId is empty). When existingVnetResourceId is set, this value is IGNORED, the name is derived from the resource ID. If you supply both they should match; otherwise the resource ID wins.')
+param vnetName string = ''
@description('The name of Agents Subnet to create new or existing subnet for agents')
param agentSubnetName string = 'agent-subnet'
@@ -84,6 +90,29 @@ param agentSubnetPrefix string = ''
@description('Address prefix for the private endpoint subnet')
param peSubnetPrefix string = ''
+// Non-destructive subnet handling.
+// Set to true when bringing your own pre-configured subnets (NSG/RT/PE policies
+// already set by your platform team). Prevents the template from doing a PUT
+// that would reset privateEndpointNetworkPolicies and trip tenant policies.
+@description('When true and existingVnetResourceId is set, the template will NOT modify your existing subnets.')
+param reuseExistingSubnets bool = false
+
+// True BYO Foundry account.
+// When set, the template references the existing AI Foundry account instead of
+// creating a new one with a deterministic suffix (which orphans on re-runs).
+@description('Optional. Full ARM resource ID of an existing AI Foundry (CognitiveServices/accounts kind=AIServices) account to reuse. When set, the template will NOT create a new account.')
+param existingAiFoundryAccountResourceId string = ''
+
+@description('Optional. When true, skip the model deployment. Recommended when reusing an existing account that already has the required model deployments.')
+param skipModelDeployment bool = false
+
+// Re-derive BYO account context at main.bicep level so we can scope the
+// account-level capabilityHost module to the right RG/subscription.
+var useExistingAccount = !empty(existingAiFoundryAccountResourceId)
+var existingAccountIdParts = split(existingAiFoundryAccountResourceId, '/')
+var existingAccountSubscriptionId = useExistingAccount ? existingAccountIdParts[2] : subscription().subscriptionId
+var existingAccountResourceGroupName = useExistingAccount ? existingAccountIdParts[4] : resourceGroup().name
+
@description('The AI Search Service full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.')
param aiSearchResourceId string = ''
@description('The AI Storage Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.')
@@ -120,9 +149,20 @@ param dnsZoneNames array = [
var projectName = toLower('${firstProjectName}${uniqueSuffix}')
-var cosmosDBName = toLower('${aiServices}${uniqueSuffix}cosmosdb')
+// Sanitize aiServices for storage account name: lowercase, no hyphens, max 24 chars total.
+// Reserve last 6 chars for `${uniqueSuffix}st` so uniqueness is preserved when prefix is truncated.
+var aiServicesSanitized = toLower(replace(aiServices, '-', ''))
+var storagePrefixMax = 18 // 24 total - 4 (uniqueSuffix) - 2 ('st' marker)
+var storagePrefix = length(aiServicesSanitized) > storagePrefixMax
+ ? substring(aiServicesSanitized, 0, storagePrefixMax)
+ : aiServicesSanitized
+var azureStorageName = '${storagePrefix}${uniqueSuffix}st'
+
+// Cosmos DB allows hyphens but enforces 44-char max. Cap defensively.
+var cosmosDBNameRaw = toLower('${aiServices}${uniqueSuffix}cosmosdb')
+var cosmosDBName = length(cosmosDBNameRaw) > 44 ? substring(cosmosDBNameRaw, 0, 44) : cosmosDBNameRaw
+
var aiSearchName = toLower('${aiServices}${uniqueSuffix}search')
-var azureStorageName = toLower('${aiServices}${uniqueSuffix}storage')
// Check if existing resources have been passed in
var storagePassedIn = azureStorageAccountResourceId != ''
@@ -149,8 +189,16 @@ var vnetResourceGroupName = existingVnetPassedIn ? vnetParts[4] : resourceGroup(
var existingVnetName = existingVnetPassedIn ? last(vnetParts) : vnetName
var trimVnetName = trim(existingVnetName)
-// Resolve DNS zones subscription ID - use current subscription if not specified
-var resolvedDnsZonesSubscriptionId = empty(dnsZonesSubscriptionId) ? subscription().subscriptionId : dnsZonesSubscriptionId
+// Resolve DNS zones subscription ID - use current subscription if not specified.
+// Accept either form: bare GUID or "/subscriptions/".
+// The full ARM path form previously broke the existing-zone cross-sub references
+// silently (the subscriptionId field needs the bare GUID).
+var normalizedDnsZonesSubscriptionId = empty(dnsZonesSubscriptionId)
+ ? ''
+ : (startsWith(toLower(dnsZonesSubscriptionId), '/subscriptions/')
+ ? split(dnsZonesSubscriptionId, '/')[2]
+ : dnsZonesSubscriptionId)
+var resolvedDnsZonesSubscriptionId = empty(normalizedDnsZonesSubscriptionId) ? subscription().subscriptionId : normalizedDnsZonesSubscriptionId
@description('The name of the project capability host to be created')
param projectCapHost string = 'caphostproj'
@@ -169,6 +217,7 @@ module vnet 'modules-network-secured/network-agent-vnet.bicep' = {
agentSubnetPrefix: agentSubnetPrefix
peSubnetPrefix: peSubnetPrefix
existingVnetSubscriptionId: vnetSubscriptionId
+ reuseExistingSubnets: reuseExistingSubnets
}
}
@@ -187,6 +236,8 @@ module aiAccount 'modules-network-secured/ai-account-identity.bicep' = {
modelSkuName: modelSkuName
modelCapacity: modelCapacity
agentSubnetId: vnet.outputs.agentSubnetId
+ existingAccountResourceId: existingAiFoundryAccountResourceId
+ skipModelDeployment: skipModelDeployment
}
}
/*
@@ -365,6 +416,21 @@ module aiSearchRoleAssignments 'modules-network-secured/ai-search-role-assignmen
]
}
+// Account-level capabilityHost (bootstraps before project caphost).
+// The current sample relies on createCapHost.sh being run manually; making it
+// declarative keeps the flow idempotent and works for both new and BYO accounts.
+module addAccountCapabilityHost 'modules-network-secured/add-account-capability-host.bicep' = {
+ name: 'account-caphost-${uniqueSuffix}-deployment'
+ scope: resourceGroup(existingAccountSubscriptionId, existingAccountResourceGroupName)
+ params: {
+ accountName: aiAccount.outputs.accountName
+ agentSubnetResourceId: vnet.outputs.agentSubnetId
+ }
+ dependsOn: [
+ privateEndpointAndDNS
+ ]
+}
+
// This module creates the capability host for the project and account
module addProjectCapabilityHost 'modules-network-secured/add-project-capability-host.bicep' = {
name: 'capabilityHost-configuration-${uniqueSuffix}-deployment'
@@ -377,6 +443,7 @@ module addProjectCapabilityHost 'modules-network-secured/add-project-capability-
projectCapHost: projectCapHost
}
dependsOn: [
+ addAccountCapabilityHost // account caphost must exist first
aiSearch // Ensure AI Search exists
storage // Ensure Storage exists
cosmosDB
diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/add-account-capability-host.bicep b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/add-account-capability-host.bicep
new file mode 100644
index 000000000..d4e502eb6
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/add-account-capability-host.bicep
@@ -0,0 +1,40 @@
+// Account-level capabilityHost.
+//
+// The current template ONLY creates the project-level capabilityHost in Bicep
+// and relies on the `createCapHost.sh` script being run manually before
+// deployment to bootstrap the account-level capabilityHost. When the account
+// is BYO (already-existing Foundry account that never had the script run),
+// the project capability host creation fails with:
+// "Foundry Account capabilityHost Not Found, please retry again after
+// creating capabilityHost for the Foundry Account."
+//
+// This module bootstraps the account-level capabilityHost in Bicep so the
+// flow is fully declarative and idempotent regardless of whether the account
+// is freshly created or pre-existing.
+
+@description('Name of the AI Foundry (Cognitive Services) account')
+param accountName string
+
+@description('Name of the account-level capabilityHost')
+param accountCapHost string = 'caphostacct'
+
+@description('ARM resource ID of the customer agent subnet (delegated to Microsoft.App/environments)')
+param agentSubnetResourceId string
+
+resource account 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = {
+ name: accountName
+}
+
+resource accountCapabilityHost 'Microsoft.CognitiveServices/accounts/capabilityHosts@2025-04-01-preview' = {
+ name: accountCapHost
+ parent: account
+ properties: {
+ // Bicep type defs reject this property; ARM API requires it.
+ #disable-next-line BCP037
+ capabilityHostKind: 'Agents'
+ #disable-next-line BCP037
+ customerSubnet: agentSubnetResourceId
+ }
+}
+
+output accountCapHostName string = accountCapabilityHost.name
diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/add-project-capability-host.bicep b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/add-project-capability-host.bicep
index dd2ac3297..e5a001a33 100644
--- a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/add-project-capability-host.bicep
+++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/add-project-capability-host.bicep
@@ -23,6 +23,11 @@ resource projectCapabilityHost 'Microsoft.CognitiveServices/accounts/projects/ca
name: projectCapHost
parent: project
properties: {
+ // Bicep type definitions for capabilityHosts are stale and reject
+ // `capabilityHostKind`, but the ARM API REQUIRES it (without it the
+ // capability host is created with no kind and downstream agents fail).
+ // Suppressing the false-positive BCP037 since runtime validation passes.
+ #disable-next-line BCP037
capabilityHostKind: 'Agents'
vectorStoreConnections: vectorStoreConnections
storageConnections: storageConnections
diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/ai-account-identity.bicep b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/ai-account-identity.bicep
index 47f72a3be..e3576f931 100644
--- a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/ai-account-identity.bicep
+++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/ai-account-identity.bicep
@@ -8,8 +8,24 @@ param modelCapacity int
param agentSubnetId string
param networkInjection string = 'true'
+// True BYO Foundry account.
+// When existingAccountResourceId is set, reference the existing AI Foundry
+// (Cognitive Services AIServices kind) account instead of creating a new one
+// with a deterministic suffix (which orphans on re-runs and collides on conflict).
+@description('Optional. Full ARM resource ID of an existing AI Foundry (CognitiveServices/accounts kind=AIServices) account to reuse. When set, the template will NOT create a new account.')
+param existingAccountResourceId string = ''
+
+@description('Optional. When true, skip the model deployment. Recommended when reusing an existing account that already has the required model deployments.')
+param skipModelDeployment bool = false
+
+var useExistingAccount = !empty(existingAccountResourceId)
+var existingParts = split(existingAccountResourceId, '/')
+var existingAccountSub = useExistingAccount ? existingParts[2] : subscription().subscriptionId
+var existingAccountRg = useExistingAccount ? existingParts[4] : resourceGroup().name
+var existingAccountName = useExistingAccount ? last(existingParts) : accountName
+
#disable-next-line BCP036
-resource account 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = {
+resource account 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = if (!useExistingAccount) {
name: accountName
location: location
sku: {
@@ -40,8 +56,14 @@ resource account 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = {
}
}
+// Reference to existing account (cross-RG / cross-sub aware)
+resource existingAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = {
+ name: existingAccountName
+ scope: resourceGroup(existingAccountSub, existingAccountRg)
+}
+
#disable-next-line BCP081
-resource modelDeployment 'Microsoft.CognitiveServices/accounts/deployments@2025-04-01-preview'= {
+resource modelDeployment 'Microsoft.CognitiveServices/accounts/deployments@2025-04-01-preview' = if (!useExistingAccount && !skipModelDeployment) {
parent: account
name: modelName
sku : {
@@ -57,7 +79,8 @@ resource modelDeployment 'Microsoft.CognitiveServices/accounts/deployments@2025-
}
}
-output accountName string = account.name
-output accountID string = account.id
-output accountTarget string = account.properties.endpoint
-output accountPrincipalId string = account.identity.principalId
+// Outputs use ARM short-circuit ternary so only the chosen branch is evaluated.
+output accountName string = useExistingAccount ? existingAccount.name : account.name
+output accountID string = useExistingAccount ? existingAccount.id : account.id
+output accountTarget string = useExistingAccount ? existingAccount.properties.endpoint : account.properties.endpoint
+output accountPrincipalId string = useExistingAccount ? existingAccount.identity.principalId : account.identity.principalId
diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/existing-vnet.bicep b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/existing-vnet.bicep
index b371d61e5..c25317e05 100644
--- a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/existing-vnet.bicep
+++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/existing-vnet.bicep
@@ -35,6 +35,18 @@ param agentSubnetPrefix string = ''
@description('Address prefix for the private endpoint subnet (only needed if creating new subnet)')
param peSubnetPrefix string = ''
+// Non-destructive subnet handling.
+// When the caller already has correctly-configured subnets (delegations, NSGs,
+// route tables, privateEndpointNetworkPolicies), the original template would
+// PUT a slim subnet body (only addressPrefix + delegations) and ARM would
+// silently RESET privateEndpointNetworkPolicies (and clobber NSG/RT references)
+// to defaults. In tenants that enforce a policy on those properties, this
+// fails with RequestDisallowedByPolicy.
+// When reuseExistingSubnets=true we skip the subnet PUT entirely and just
+// reference the existing subnet IDs in the outputs.
+@description('When true, do NOT modify the existing subnets, reference them as-is. Recommended when the caller manages subnet config (NSG/RT/PE policies) outside this template.')
+param reuseExistingSubnets bool = false
+
// Get the address space (array of CIDR strings)
var vnetAddressSpace = existingVNet.properties.addressSpace.addressPrefixes[0]
@@ -48,7 +60,7 @@ resource existingVNet 'Microsoft.Network/virtualNetworks@2024-05-01' existing =
}
// Create the agent subnet if requested
-module agentSubnet 'subnet.bicep' = {
+module agentSubnet 'subnet.bicep' = if (!reuseExistingSubnets) {
name: 'agent-subnet-${uniqueString(deployment().name, agentSubnetName)}'
scope: resourceGroup(vnetResourceGroupName)
params: {
@@ -67,7 +79,7 @@ module agentSubnet 'subnet.bicep' = {
}
// Create the private endpoint subnet if requested
-module peSubnet 'subnet.bicep' = {
+module peSubnet 'subnet.bicep' = if (!reuseExistingSubnets) {
name: 'pe-subnet-${uniqueString(deployment().name, peSubnetName)}'
scope: resourceGroup(vnetResourceGroupName)
params: {
diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/network-agent-vnet.bicep b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/network-agent-vnet.bicep
index bad8a4f27..87aaf5407 100644
--- a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/network-agent-vnet.bicep
+++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/network-agent-vnet.bicep
@@ -28,6 +28,10 @@ param agentSubnetPrefix string = ''
@description('Address prefix for the private endpoint subnet')
param peSubnetPrefix string = ''
+// Non-destructive subnet handling. See existing-vnet.bicep.
+@description('When true and useExistingVnet=true, do NOT modify the existing subnets, reference them as-is.')
+param reuseExistingSubnets bool = false
+
// Create new VNet if needed
module newVNet 'vnet.bicep' = if (!useExistingVnet) {
name: 'vnet-deployment'
@@ -53,6 +57,7 @@ module existingVNet 'existing-vnet.bicep' = if (useExistingVnet) {
peSubnetName: peSubnetName
agentSubnetPrefix: agentSubnetPrefix
peSubnetPrefix: peSubnetPrefix
+ reuseExistingSubnets: reuseExistingSubnets
}
}
diff --git a/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/README.md b/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/README.md
index cffad0739..d175061bc 100644
--- a/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/README.md
+++ b/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/README.md
@@ -1,5 +1,5 @@
---
-description: This set of templates demonstrates how to set up a network-secured Azure AI Foundry environment for evaluation scenarios without Cosmos DB, AI Search, or project capability host.
+description: This set of templates demonstrates how to set up a network-secured Microsoft Foundry environment for evaluation scenarios without Cosmos DB, AI Search, or project capability host.
page_type: sample
products:
- azure
@@ -10,7 +10,11 @@ languages:
- json
---
-# Azure AI Foundry: Evaluation-Only Setup with Private Network Isolation
+# Microsoft Foundry: Evaluation-Only Setup with Network Isolation
+
+
+> **NEW**
+> For support on deploying the right network isolation template, check out the [GitHub Copilot for Azure skill for private networking](https://github.com/microsoft/GitHub-Copilot-for-Azure/blob/main/plugin/skills/microsoft-foundry/resource/private-network/private-network.md) set-up!
> **IMPORTANT**
>
@@ -18,7 +22,7 @@ languages:
---
## Overview
-This infrastructure-as-code (IaC) solution deploys a **minimal** network-secured Azure AI Foundry environment with private networking and role-based access control (RBAC), intended for evaluation and testing purposes.
+This infrastructure-as-code (IaC) solution deploys a **minimal** network-secured Microsoft Foundry environment with private networking and role-based access control (RBAC), intended for evaluation and testing purposes.
Unlike the full standard agent setup, this template:
- **Does NOT** create an Azure Cosmos DB account (no thread/conversation storage)
@@ -35,23 +39,51 @@ What it **does** deploy:
---
-## Key Information
+## When to Use This Template
+
+Use this template when you need:
+- **Evaluation and testing only** β Run model evaluations without deploying full agent infrastructure
+- **Minimal private networking** β Private endpoints for AI Services and Storage only (no Cosmos DB or AI Search)
+- **Lower cost** β Fewer resources deployed compared to the full standard agent setup
+- **Quick iteration** β Faster deployment time with a smaller resource footprint
+
+### Template Decision Guide
+
+Use the table below to choose the right infrastructure template for your scenario:
+
+| Template | Agent Type | Networking | Identity | Key Use Case |
+|----------|-----------|------------|----------|-------------|
+| [**15a** (this template)](../15a-private-network-evaluation-only-setup/) | Evaluation only | BYO VNet + Private Endpoints | System Assigned MI | Minimal setup for evaluation β no Cosmos DB, AI Search, or capability host |
+| [**15**](../15-private-network-standard-agent-setup/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | E2E network isolation with full agent capabilities |
+| [**19**](../19-private-network-agent-tools/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | Same as 15 **plus** tools behind VNet (MCP, OpenAPI, Functions, A2A) |
+| [**17**](../17-private-network-standard-user-assigned-identity-agent-setup/) | Standard (BYO resources) | BYO VNet + Private Endpoints | **User Assigned MI** | Same as 15 but with user-managed identity |
+| [**16**](../16-private-network-standard-agent-apim-setup-preview/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | Same as 15 **plus** private APIM integration (preview) |
+| [**18**](../18-managed-virtual-network-preview/) | Standard (BYO resources) | **Managed VNet** (Microsoft-managed) | System Assigned MI | Network isolation without managing your own VNet (preview) |
+| [**11**](../11-private-network-basic-vnet/) | **Basic** (platform-managed) | BYO VNet injection | System Assigned MI | Basic agents with VNet isolation β no BYO resources needed |
+| [**41**](../41-standard-agent-setup/) | Standard (BYO resources) | **Public** (no VNet) | System Assigned MI | Standard agents without network isolation |
+| [**40**](../40-basic-agent-setup/) | **Basic** (platform-managed) | **Public** (no VNet) | System Assigned MI | Simplest setup β no BYO resources, no private networking |
+
+---
+
+## Deploy to Azure
-**Region and Resource Placement Requirements**
-- **All Foundry workspace resources should be in the same region as the VNet**, including the Storage Account, Foundry Account, Project, and Managed Identity. The only exception is within the Foundry Account, you may choose to deploy your model to a different region.
- - **Note:** Your Virtual Network can be in a different resource group than your Foundry workspace resources.
+
+[](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fazure-ai-foundry%2Ffoundry-samples%2Frefs%2Fheads%2Fmain%2Finfrastructure%2Finfrastructure-setup-bicep%2F15a-private-network-evaluation-only-setup%2Fazuredeploy.json)
---
## Prerequisites
1. **Active Azure subscription with appropriate permissions**
- - **Azure AI Account Owner**: Needed to create a cognitive services account and project
- - **Owner or Role Based Access Administrator**: Needed to assign RBAC to the storage account
- - **Azure AI User**: Needed to create and use evaluation workloads
+ - **Foundry Account Owner**: Needed to create the Microsoft Foundry account and project.
+ - **Owner or Role Based Access Administrator**: Needed to assign RBAC on the Azure resources used by this template.
+ - **Foundry User**: Needed to create and use agents, projects, or evaluation workloads after deployment.
+ In this template, the required RBAC assignments apply only to the storage account.
1. **Register Resource Providers**
+ Make sure you have an active Azure subscription that allows registering resource providers. For example, subnet delegation requires the Microsoft.App provider to be registered in your subscription. If it's not already registered, run the commands below:
+
```bash
az provider register --namespace 'Microsoft.KeyVault'
az provider register --namespace 'Microsoft.CognitiveServices'
@@ -63,8 +95,8 @@ What it **does** deploy:
1. Network administrator permissions (if operating in a restricted or enterprise environment)
-1. Sufficient quota for all resources in your target Azure region
- * If no parameters are passed in, this template creates an Azure AI Foundry resource, Foundry project, and Azure Storage account
+1. Sufficient quota for all resources required by this template in the target Azure region, including model deployment quota.
+ * If no parameters are passed in, this template creates an Microsoft Foundry resource, Foundry project, and Azure Storage account
1. Azure CLI installed and configured on your local workstation or deployment pipeline server
@@ -73,43 +105,80 @@ What it **does** deploy:
## Pre-Deployment Steps
### Networking Requirements
-1. Review network requirements and plan Virtual Network address space (e.g., 192.168.0.0/16)
+1. Review network requirements and plan Virtual Network address space (e.g., 192.168.0.0/16 or an alternative non-overlapping address space)
2. Two subnets are needed:
- - **Agent Subnet** (e.g., 192.168.0.0/24): Hosts Agent client for workloads, delegated to Microsoft.App/environments
+ - **Agent Subnet** (e.g., 192.168.0.0/24): Hosts Agent client for workloads, delegated to Microsoft.App/environments. The recommended size should be /24 for this delegated subnet.
- **Private endpoint Subnet** (e.g., 192.168.1.0/24): Hosts private endpoints
- - Ensure that the address spaces do not overlap with any existing networks
+ - Ensure that the address spaces for the used VNET does not overlap with any existing networks in your Azure environment or reserved IP ranges like the following: 169.254.0.0/16,172.30.0.0/16,172.31.0.0/16,192.0.2.0/24,0.0.0.0/8,127.0.0.0/8,100.100.0.0/17,100.100.192.0/19,100.100.224.0/19,100.64.0.0/11.
+ This includes all address space(s) you have in your VNET if you have more than one, and peered VNETs.
> **Notes:**
- If you do not provide an existing virtual network, the template will create a new virtual network with the default address spaces and subnets described above.
- You must ensure the subnet is not already in use by another account.
- - You must ensure the subnet is exclusively delegated to __Microsoft.App/environments__.
+ - You must ensure the subnet is exclusively delegated to __Microsoft.App/environments__ and cannot be used by any other Azure resources.
+ - Your Foundry resource and the virtual network created for delegation must be in the same region.
----
+### Limitations / Known Issues
-## Template Customization
+1. The delegated agent subnet must be exclusively used by a single Foundry account. It cannot be shared across accounts.
+2. The Foundry resource and the virtual network must be in the same Azure region.
+3. For the virtual network IP range, you may use any Private Class A, B or C IP range. Private Class A IP address ranges (10.x.x.x) are only supported in the following regions: **Australia East, Brazil South, Canada East, East US, East US 2, France Central, Germany West Central, Italy North, Japan East, South Africa North, South Central US, South India, Spain Central, Sweden Central, UAE North, UK South, West US, West US 3.** Use Class B (172.16.x.x) or C (192.168.x.x) ranges for other regions. You may not use any other IP range that overlaps to the list above or uses public IP ranges.
+4. This template does **not** deploy Cosmos DB, AI Search, or a project capability host. Stateful agents are not supported. Use [template 15](../15-private-network-standard-agent-setup/) for full agent capabilities.
+5. There is no upgrade path from this evaluation template to the full standard agent setup. A redeployment with template 15 is required.
+
+### Template Customization
Note: If not provided, the following resources will be created automatically for you:
- VNet and two subnets
- Azure Storage
-### Parameters
+#### Parameters
+
+| Parameter | Description | Default | Required |
+|-----------|-------------|---------|----------|
+| `location` | Azure region for deployment | `eastus` | Yes |
+| `aiServices` | Base name for the AI Services resource | `aiservices` | No |
+| `firstProjectName` | Name for the Foundry project | `project` | No |
+| `modelName` | Model to deploy | `gpt-4.1` | No |
+| `modelFormat` | Model provider | `OpenAI` | No |
+| `modelVersion` | Model version | `2025-04-14` | No |
+| `modelSkuName` | Model deployment SKU | `GlobalStandard` | No |
+| `modelCapacity` | Tokens per minute (TPM) capacity | `30` | No |
+| `vnetName` | Virtual Network name | `agent-vnet-test` | No |
+| `agentSubnetName` | Subnet name for agent workloads | `agent-subnet` | No |
+| `agentSubnetPrefix` | Address prefix for agent subnet | `192.168.0.0/24` | No |
+| `peSubnetName` | Subnet name for private endpoints | `pe-subnet` | No |
+| `peSubnetPrefix` | Address prefix for PE subnet | `192.168.1.0/24` | No |
+| `existingVnetResourceId` | Full ARM Resource ID of an existing VNet | `''` (creates new) | No |
+| `vnetAddressPrefix` | Address space for new VNet | `192.168.0.0/16` | No |
+| `azureStorageAccountResourceId` | ARM Resource ID of existing Storage account | `''` (creates new) | No |
+| `dnsZonesSubscriptionId` | Subscription ID for existing DNS zones | `''` (current sub) | No |
+| `existingDnsZones` | Map of DNS zone names to resource groups | All empty (creates new) | No |
+
+#### BYO Resource Details
1. **Use Existing Virtual Network and Subnets**
To use an existing VNet and subnets, set the `existingVnetResourceId` parameter to the full Azure Resource ID of the target VNet:
-```
-param existingVnetResourceId = "/subscriptions//resourceGroups//providers/Microsoft.Network/virtualNetworks/"
-param agentSubnetName string = 'agent-subnet'
-param peSubnetName string = 'pe-subnet'
-```
+- param existingVnetResourceId = "/subscriptions//resourceGroups//providers/Microsoft.Network/virtualNetworks/"
+- param agentSubnetName string = 'agent-subnet' //optional, default is 'agent-subnet'
+- param agentSubnetPrefix string = '192.168.0.0/24' //optional, default is '192.168.0.0/24'
+- param peSubnetName string = 'pe-subnet' //optional, default is 'pe-subnet'
+- param peSubnetPrefix string = '192.168.1.0/24' //optional, default is '192.168.1.0/24'
+- param dnsZonesSubscriptionId string = '' //optional, leave empty to use current subscription
+- param existingDnsZones = {
+
+ 'privatelink.services.ai.azure.com': 'privzoneRG' //add resource group name where your private DNS zone is located
+
+ 'privatelink.openai.azure.com': '' //Leave empty to create new private dns zone... }
+
+π‘ If subnets information is provided then make sure it exist within the specified VNet to avoid deployment errors. If subnet information is not provided, the template will create subnets with the default address space.
2. **Use an existing Azure Storage account**
-To use an existing Azure Storage account:
-```
-param azureStorageAccountResourceId string = /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{storageAccountName}
-```
+To use an existing Azure Storage account, set the azureStorageAccountResourceId parameter to the full Azure resource Id:
+- param azureStorageAccountResourceId string = /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{storageAccountName}
---
@@ -127,42 +196,135 @@ param azureStorageAccountResourceId string = /subscriptions/{subscriptionId}/res
az deployment group create --resource-group --template-file main.bicep --parameters main.bicepparam
```
-> **Note:** To access your Foundry resource securely, use either a VM, VPN, or ExpressRoute.
+> **Note:** To access a private Foundry resource securely, use one of the following:
+> - A VM or jump box on the virtual network, optionally accessed through Azure Bastion
+> - Azure VPN Gateway
+> - Azure ExpressRoute
+
+### Cleanup
+
+To delete all resources created by this template:
+
+```bash
+az group delete --name --yes --no-wait
+```
---
-## Architecture
+## Architecture Deep Dive
+
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Secure Access (VPN Gateway / ExpressRoute / Azure Bastion) β
+ββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
+ β
+ ββββββββββββββββΌβββββββββββββββ
+ β Microsoft Foundry β
+ β (publicNetworkAccess: β
+ β DISABLED) β
+ β β
+ β ββββββββββββββββββββββββββ β
+ β β Foundry Project β β
+ β β (Evaluation Only) β β
+ β βββββββββββββ¬βββββββββββββ β
+ ββββββββββββββββΌβββββββββββββββ
+ β Subnet Delegation
+ ββββββββββββββββΌβββββββββββββββ
+ β BYO Virtual Network β
+ β (192.168.0.0/16) β
+ β β
+ β ββββββββββββββββββββββββ β
+ β β Agent Subnet β β
+ β β (192.168.0.0/24) β β βββ Delegated to
+ β β Microsoft.App/envs β β Microsoft.App/environments
+ β ββββββββββββββββββββββββ β
+ β β
+ β ββββββββββββββββββββββββ β
+ β β PE Subnet β β
+ β β (192.168.1.0/24) β β
+ β β β β
+ β β ββββββββββ ββββββββββ β β
+ β β βStorage β βFoundry β β β βββ Private endpoints
+ β β ββββββββββ ββββββββββ β β (no public access)
+ β ββββββββββββββββββββββββ β
+ ββββββββββββββββββββββββββββββββ
+```
### Azure Resources Created
-| Resource | Type | Description |
-|----------|------|-------------|
-| Azure AI Foundry | `Microsoft.CognitiveServices/accounts` | AI Services account with disabled public access |
-| AI Model Deployment | `Microsoft.CognitiveServices/accounts/deployments` | Model deployment (e.g., gpt-4.1) |
-| Foundry Project | `Microsoft.CognitiveServices/accounts/projects` | Project with system-assigned managed identity |
-| Storage Account | `Microsoft.Storage/storageAccounts` | StorageV2 with disabled public access |
-| Virtual Network | `Microsoft.Network/virtualNetworks` | VNet with agent and PE subnets |
-| Private Endpoints | `Microsoft.Network/privateEndpoints` | For AI Services and Storage |
+Microsoft Foundry (Cognitive Services)
+- Type: Microsoft.CognitiveServices/accounts
+- API version: 2025-04-01-preview
+- Kind: AIServices
+- SKU: S0
+- Identity: System-assigned
+- Features:
+ - Custom subdomain name
+ - Disabled public network access
+ - Network ACLs with Azure Services bypass
+
+AI Model Deployment
+- Type: Microsoft.CognitiveServices/accounts/deployments
+- API version: 2025-04-01-preview
+- SKU: Based on modelSkuName parameter, capacity set by modelCapacity
+- Model properties:
+ - Name: From modelName parameter
+ - Format: From modelFormat parameter
+ - Version: From modelVersion parameter
+
+Foundry Project
+- Type: Microsoft.CognitiveServices/accounts/projects
+- Identity: System-assigned managed identity
+- Connections: Storage account only (no Cosmos DB or AI Search connections)
+
+Storage Account
+- Type: Microsoft.Storage/storageAccounts
+- Kind: StorageV2
+- SKU: ZRS or GRS (region dependent; use Standard_GRS if ZRS not available)
+- Features:
+ - Blob service
+ - Minimum TLS Version: 1.2
+ - Block public blob access
+ - Disabled public network access
+ - Force Azure AD authentication (SharedKey access disabled)
### Network Security Design
-**Private Endpoints** are created for:
-- Azure AI Foundry (account)
+Network Security
+- Public network access disabled
+- Private endpoints for AI Services and Storage
+- Network ACLs with deny by default
+
+**Network Infrastructure**
+- A Virtual Network (192.168.0.0/16) is created (if existing isn't passed in)
+- Agent Subnet (192.168.0.0/24): Hosts Agent client
+- Private endpoint Subnet (192.168.1.0/24): Hosts private endpoints
+
+**Private Endpoints**
+Private endpoints ensure secure, internal-only connectivity. Private endpoints are created for:
+- Microsoft Foundry (account)
- Azure Storage (blob)
-**Private DNS Zones**:
-| Private Link Resource Type | Sub Resource | Private DNS Zone Name |
-|----------------------------|--------------|------------------------|
-| **Azure AI Foundry** | account | `privatelink.cognitiveservices.azure.com`
`privatelink.openai.azure.com`
`privatelink.services.ai.azure.com` |
-| **Azure Storage** | blob | `privatelink.blob.core.windows.net` |
+**Private DNS Zones**
+| Private Link Resource Type | Sub Resource | Private DNS Zone Name | Public DNS Zone Forwarders |
+|----------------------------|--------------|------------------------|-----------------------------|
+| **Microsoft Foundry** | account | `privatelink.cognitiveservices.azure.com`
`privatelink.openai.azure.com`
`privatelink.services.ai.azure.com` | `cognitiveservices.azure.com`
`openai.azure.com`
`services.ai.azure.com` |
+| **Azure Storage** | blob | `privatelink.blob.core.windows.net` | `blob.core.windows.net` |
+
+### Authentication & Authorization
-### Role Assignments
+- **Managed Identity**
+ - Zero-trust security model
+ - No credential storage
+ - Platform-managed rotation
+ - This template uses System Managed Identity
-- **AI Services Account**
- - Azure AI User (`53ca6127-db72-4b80-b1b0-d745d6d5456d`) β grants the project MI data-plane access
-- **Azure Storage Account**
- - Storage Blob Data Contributor (`ba92f5b4-2d11-453d-a403-e96b0029c9fe`)
- - Storage Blob Data Owner (`b7e6dc6d-f1e8-4753-8033-0f276bb0955b`) β scoped to project containers
+- **Role Assignments**
+ - **AI Services Account**
+ - Foundry User (`53ca6127-db72-4b80-b1b0-d745d6d5456d`) β grants the project MI data-plane access
+ - **Azure Storage Account**
+ - Storage Blob Data Contributor (`ba92f5b4-2d11-453d-a403-e96b0029c9fe`)
+ - Storage Blob Data Owner (`b7e6dc6d-f1e8-4753-8033-0f276bb0955b`) β scoped to project containers
---
@@ -170,8 +332,8 @@ param azureStorageAccountResourceId string = /subscriptions/{subscriptionId}/res
```text
modules-network-secured/
-βββ ai-account-identity.bicep # Azure AI Foundry deployment and configuration
-βββ ai-account-role-assignment.bicep # Azure AI User role assignment on the account
+βββ ai-account-identity.bicep # Microsoft Foundry deployment and configuration
+βββ ai-account-role-assignment.bicep # Foundry User role assignment on the account
βββ ai-project-identity.bicep # Foundry project deployment with storage connection
βββ azure-storage-account-role-assignment.bicep # Storage Account RBAC configuration
βββ blob-storage-container-role-assignments.bicep # Blob Storage Container RBAC configuration
@@ -185,24 +347,14 @@ modules-network-secured/
βββ vnet.bicep # Deploying a new virtual network
```
----
-
-## Comparison with Standard Agent Setup
-
-| Feature | This Template (Evaluation-Only) | Standard Agent Setup (15) |
-|---------|-------------------------------|--------------------------|
-| AI Services + Model | β
| β
|
-| Project | β
| β
|
-| Storage Account | β
| β
|
-| VNet + Private Endpoints | β
(AI + Storage) | β
(AI + Storage + Search + Cosmos) |
-| Cosmos DB | β | β
|
-| AI Search | β | β
|
-| Project Capability Host | β | β
|
-| Stateful Agents | β | β
|
+## Maintenance
----
+### Regular Tasks
-## Maintenance
+1. Review role assignments
+2. Monitor network security
+3. Check service health
+4. Update configurations as needed
### Troubleshooting
@@ -210,3 +362,11 @@ modules-network-secured/
2. Check DNS resolution
3. Validate role assignments
4. Review network security groups
+
+## References
+
+- [Configure private link for Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link?tabs=azure-portal&pivots=fdp-project)
+- [Microsoft Foundry RBAC roles](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/rbac-azure-ai-foundry?pivots=fdp-project)
+- [Private Endpoint documentation](https://learn.microsoft.com/en-us/azure/private-link/)
+- [Azure RBAC documentation](https://learn.microsoft.com/en-us/azure/role-based-access-control/)
+- [Network security best practices](https://learn.microsoft.com/en-us/azure/security/fundamentals/network-best-practices)
diff --git a/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/azuredeploy.json b/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/azuredeploy.json
index 58cc95a6a..c161879c6 100644
--- a/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/azuredeploy.json
+++ b/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/azuredeploy.json
@@ -4,8 +4,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.42.1.51946",
- "templateHash": "3823237790915182165"
+ "version": "0.43.8.12551",
+ "templateHash": "16354343630624836697"
}
},
"parameters": {
@@ -261,8 +261,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.42.1.51946",
- "templateHash": "17284036880023313732"
+ "version": "0.43.8.12551",
+ "templateHash": "11296469007124090352"
}
},
"parameters": {
@@ -375,8 +375,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.42.1.51946",
- "templateHash": "7044568158736020946"
+ "version": "0.43.8.12551",
+ "templateHash": "6615847355060514198"
}
},
"parameters": {
@@ -548,8 +548,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.42.1.51946",
- "templateHash": "9399102347973763543"
+ "version": "0.43.8.12551",
+ "templateHash": "8341621792914014201"
}
},
"parameters": {
@@ -638,8 +638,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.42.1.51946",
- "templateHash": "8293031405760656222"
+ "version": "0.43.8.12551",
+ "templateHash": "11349014942847443024"
}
},
"parameters": {
@@ -721,8 +721,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.42.1.51946",
- "templateHash": "8293031405760656222"
+ "version": "0.43.8.12551",
+ "templateHash": "11349014942847443024"
}
},
"parameters": {
@@ -893,8 +893,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.42.1.51946",
- "templateHash": "1167841404925031150"
+ "version": "0.43.8.12551",
+ "templateHash": "1400943577065869287"
}
},
"parameters": {
@@ -1027,8 +1027,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.42.1.51946",
- "templateHash": "11208930521589341070"
+ "version": "0.43.8.12551",
+ "templateHash": "14681023185116728141"
}
},
"parameters": {
@@ -1120,8 +1120,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.42.1.51946",
- "templateHash": "4263282633179188093"
+ "version": "0.43.8.12551",
+ "templateHash": "12551648450543487356"
}
},
"parameters": {
@@ -1257,8 +1257,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.42.1.51946",
- "templateHash": "14317702821708481391"
+ "version": "0.43.8.12551",
+ "templateHash": "4199539764441797485"
}
},
"parameters": {
@@ -1596,8 +1596,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.42.1.51946",
- "templateHash": "15731446090362490111"
+ "version": "0.43.8.12551",
+ "templateHash": "17079120784923696162"
}
},
"parameters": {
@@ -1709,8 +1709,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.42.1.51946",
- "templateHash": "5729541472937730744"
+ "version": "0.43.8.12551",
+ "templateHash": "16292565234555392040"
}
},
"parameters": {
@@ -1764,8 +1764,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.42.1.51946",
- "templateHash": "6805612394357697185"
+ "version": "0.43.8.12551",
+ "templateHash": "10985949897065090550"
}
},
"parameters": {
@@ -1820,8 +1820,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.42.1.51946",
- "templateHash": "2328134986536572193"
+ "version": "0.43.8.12551",
+ "templateHash": "4519973268515204861"
}
},
"parameters": {
@@ -1887,8 +1887,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.42.1.51946",
- "templateHash": "14861833638743424490"
+ "version": "0.43.8.12551",
+ "templateHash": "13556924300142468207"
}
},
"parameters": {
diff --git a/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/main.bicep b/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/main.bicep
index 7a4816fed..4c88201d4 100644
--- a/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/main.bicep
+++ b/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/main.bicep
@@ -269,7 +269,7 @@ module storageAccountRoleAssignment 'modules-network-secured/azure-storage-accou
}
/*
- Assigns the project SMI the Azure AI User role on the AI Services account
+ Assigns the project SMI the Foundry User role on the AI Services account
*/
module aiAccountRoleAssignment 'modules-network-secured/ai-account-role-assignment.bicep' = {
name: 'ai-account-ra-${uniqueSuffix}-deployment'
diff --git a/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/modules-network-secured/ai-account-role-assignment.bicep b/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/modules-network-secured/ai-account-role-assignment.bicep
index 7f9c999a2..fa73c8c5c 100644
--- a/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/modules-network-secured/ai-account-role-assignment.bicep
+++ b/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/modules-network-secured/ai-account-role-assignment.bicep
@@ -1,4 +1,4 @@
-// Assigns the Azure AI User role to the project managed identity on the AI Services account
+// Assigns the Foundry User role to the project managed identity on the AI Services account
@description('Name of the AI Services account')
param accountName string
@@ -11,7 +11,7 @@ resource account 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' exist
scope: resourceGroup()
}
-// Azure AI User: 53ca6127-db72-4b80-b1b0-d745d6d5456d
+// Foundry User: 53ca6127-db72-4b80-b1b0-d745d6d5456d
resource azureAIUserRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
name: '53ca6127-db72-4b80-b1b0-d745d6d5456d'
scope: resourceGroup()
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/README.md b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/README.md
similarity index 51%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/README.md
rename to infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/README.md
index 6777a09e9..155fe8b5f 100644
--- a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/README.md
+++ b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/README.md
@@ -1,5 +1,5 @@
---
-description: This set of templates demonstrates how to set up Azure AI Agent Service with virtual network isolation with private network links to connect the agent to your secure data.
+description: This set of templates demonstrates how to set up Azure AI Agent Service with virtual network isolation, private network links, and private Azure API Management (APIM) integration to connect the agent to your secure data.
page_type: sample
products:
- azure
@@ -10,46 +10,60 @@ languages:
- json
---
-# Azure AI Agent Service: Standard Agent Setup with E2E Network Isolation
-
-> **IMPORTANT**
->
-> Class A subnet support is GA and available in the following regions. **Supported regions: Australia East, Brazil South, Canada East, East US, East US 2, France Central, Germany West Central, Italy North, Japan East, South Africa North, South Central US, South India, Spain Central, Sweden Central, UAE North, UK South, West Europe, West US, West US 3.**
->
-> Class B and C subnet support is already GA and available in all regions supported by Azure AI Foundry Agent Service. Deployment templates and setup steps are identical for Class A, B, and C subnets. For more on the supported regions of the Azure AI Foundry Agent service, see [Models supported by Azure AI Foundry Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/concepts/model-region-support?tabs=global-standard)
-
-> **IMPORTANT**
->
-> To use your existing APIM resource with Azure AI Foundry in a network isolated environment to build Agents, please deploy this template. The feature is currently in preview with a code first experience and no Foundry UI support.
+# Microsoft Foundry: Standard Agent Setup with Private APIM and E2E Network Isolation
+> **NEW**
+> For support on deploying the right network isolation template, check out the [GitHub Copilot for Azure skill for private networking](https://github.com/microsoft/GitHub-Copilot-for-Azure/blob/main/plugin/skills/microsoft-foundry/resource/private-network/private-network.md) set-up!
---
## Overview
-This infrastructure-as-code (IaC) solution deploys a network-secured Azure AI agent environment with private networking and role-based access control (RBAC).
+This infrastructure-as-code (IaC) solution deploys a network-secured Azure AI agent environment with private networking, role-based access control (RBAC), and **private Azure API Management (APIM) integration**. This template extends the [standard agent setup (template 15)](../15-private-network-standard-agent-setup/) by adding support for connecting an **existing Azure API Management service** behind a private endpoint within your VNet. This allows you to expose APIs through APIM while keeping all traffic within your private network boundary.
Standard setup supports private network isolation through utilizing **Bring Your Own Virtual Network (BYO VNet)** approach, also known as **custom VNet support with subnet delegation.**
-This implementation gives you full control over the inbound and outbound communication paths for your agent. You can restrict access to only the resources explicitly required by your agent, such as storage accounts, databases, or APIs, while blocking all other traffic by default. This approach ensures that your agent operates within a tightly scoped network boundary, reducing the risk of data leakage or unauthorized access. By default, this setup simplifies security configuration while enforcing strong isolation guarantees, ensuring that each agent deployment remains secure, compliant, and aligned with enterprise networking policies.
+This implementation gives you full control over the inbound and outbound communication paths for your agent. You can restrict access to only the resources explicitly required by your agent, such as storage accounts, databases, or APIs (via private APIM), while blocking all other traffic by default. This approach ensures that your agent operates within a tightly scoped network boundary, reducing the risk of data leakage or unauthorized access. By default, this setup simplifies security configuration while enforcing strong isolation guarantees, ensuring that each agent deployment remains secure, compliant, and aligned with enterprise networking policies.
---
-## Key Information
+## When to Use This Template
+
+Use this template when you need:
+- **Full end-to-end network isolation** β All resources behind private endpoints with no public internet access
+- **BYO VNet control** β You manage your own virtual network, subnets, and network security groups
+- **Standard agent setup with BYO resources** β Customer-managed Storage, Cosmos DB, and AI Search for data residency and compliance
+- **Private APIM integration** β Connect your existing Azure API Management service to the agent environment
+- **System Assigned Managed Identity** β Simplified identity management with platform-managed credentials
+
+### Template Decision Guide
+
+Use the table below to choose the right infrastructure template for your scenario:
-**Region and Resource Placement Requirements**
-- **All Foundry workspace resources should be in the same region as the VNet**, including CosmosDB, Storage Account, AI Search, Foundry Account, Project, Managed Identity. The only exception is within the Foundry Account, you may choose to deploy your model to a different region, and any cross-region communication will be handled securely within our network infrastructure.
- - **Note:** Your Virtual Network can be in a different resource group than your Foundry workspace resources
+| Template | Agent Type | Networking | Identity | Key Use Case |
+|----------|-----------|------------|----------|-------------|
+| [**15**](../15-private-network-standard-agent-setup/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | E2E network isolation with full agent capabilities |
+| [**19**](../19-private-network-agent-tools/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | Same as 15 **plus** tools behind VNet (MCP, OpenAPI, Functions, A2A) |
+| [**17**](../17-private-network-standard-user-assigned-identity-agent-setup/) | Standard (BYO resources) | BYO VNet + Private Endpoints | **User Assigned MI** | Same as 15 but with user-managed identity |
+| [**16** (this template)](../16-private-network-standard-agent-apim-setup/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | Same as 15 **plus** private APIM integration |
+| [**18**](../18-managed-virtual-network/) | Standard (BYO resources) | **Managed VNet** (Microsoft-managed) | System Assigned MI | Network isolation without managing your own VNet|
+| [**15a**](../15a-private-network-evaluation-only-setup/) | Evaluation only | BYO VNet + Private Endpoints | System Assigned MI | Minimal setup for evaluation β no Cosmos DB, AI Search, or capability host |
+| [**11**](../11-private-network-basic-vnet/) | **Basic** (platform-managed) | BYO VNet injection | System Assigned MI | Basic agents with VNet isolation β no BYO resources needed |
+| [**41**](../41-standard-agent-setup/) | Standard (BYO resources) | **Public** (no VNet) | System Assigned MI | Standard agents without network isolation |
+| [**40**](../40-basic-agent-setup/) | **Basic** (platform-managed) | **Public** (no VNet) | System Assigned MI | Simplest setup β no BYO resources, no private networking |
+---
+
+## Deploy to Azure
-[](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fazure-ai-foundry%2Ffoundry-samples%2Frefs%2Fheads%2Fmain%2Finfrastructure%2Finfrastructure-setup-bicep%2F16-private-network-standard-agent-apim-setup-preview%2Fazuredeploy.json)
+[](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fazure-ai-foundry%2Ffoundry-samples%2Frefs%2Fheads%2Fmain%2Finfrastructure%2Finfrastructure-setup-bicep%2F16-private-network-standard-agent-apim-setup%2Fazuredeploy.json)
---
## Prerequisites
1. **Active Azure subscription with appropriate permissions**
- - **Azure AI Account Owner**: Needed to create a cognitive services account and project
- - **Owner or Role Based Access Administrator**: Needed to assign RBAC to the required resources (Cosmos DB, Azure AI Search, Storage)
- - **Azure AI User**: Needed to create and edit agents
+ - **Foundry Account Owner**: Needed to create the Microsoft Foundry account and project.
+ - **Owner or Role Based Access Administrator**: Needed to assign RBAC on the Azure resources used by this template.
+ - **Foundry User**: Needed to create and use agents, projects, or evaluation workloads after deployment.
1. **Register Resource Providers**
@@ -63,12 +77,13 @@ This implementation gives you full control over the inbound and outbound communi
az provider register --namespace 'Microsoft.Network'
az provider register --namespace 'Microsoft.App'
az provider register --namespace 'Microsoft.ContainerService'
+ az provider register --namespace 'Microsoft.ApiManagement'
```
1. Network administrator permissions (if operating in a restricted or enterprise environment)
-1. Sufficient quota for all resources in your target Azure region
- * If no parameters are passed in, this template creates an Azure AI Foundry resource, Foundry project, Azure Cosmos DB for NoSQL, Azure AI Search, and Azure Storage account
+1. Sufficient quota for all resources required by this template in the target Azure region, including model deployment quota.
+ * If no parameters are passed in, this template creates an Microsoft Foundry resource, Foundry project, Azure Cosmos DB for NoSQL, Azure AI Search, and Azure Storage account. If an existing APIM resource ID is provided, a private endpoint is also created for APIM.
1. Azure CLI installed and configured on your local workstation or deployment pipeline server
---
@@ -86,24 +101,29 @@ This implementation gives you full control over the inbound and outbound communi
> **Notes:**
- If you do not provide an existing virtual network, the template will create a new virtual network with the default address spaces and subnets described above. If you use an existing virtual network, make sure it already contains two subnets (Agent and Private Endpoint) before deploying the template.
- You must ensure the Foundry account was successfully created so that underlying caphost has also succeeded. Then proceed to deploying the project caphost bicep.
- - You must ensure the subnet is not already in use by another account. It must be an exclusive subnet for the Foundry account.
- You must ensure the subnet is exclusively delegated to __Microsoft.App/environments__ and cannot be used by any other Azure resources.
+### Limitations / Known Issues
+
+1. The delegated agent subnet must be exclusively used by a single Foundry account. It cannot be shared across accounts.
+2. The Foundry resource and the virtual network must be in the same Azure region. BYO resources (Storage, Cosmos DB, AI Search) may be in different regions.
+3. For the virtual network IP range, you may use any Private Class A, B or C IP range. Private Class A IP address ranges (10.x.x.x) are only supported in the following regions: **Australia East, Brazil South, Canada East, East US, East US 2, France Central, Germany West Central, Italy North, Japan East, South Africa North, South Central US, South India, Spain Central, Sweden Central, UAE North, UK South, West US, West US 3.** Use Class B (172.16.x.x) or C (192.168.x.x) ranges for other regions. You may not use any other IP range that overlaps to the list above or uses public IP ranges.
+4. This template does **not** support tools (MCP servers, OpenAPI tools, Azure Functions, A2A) behind the VNet. Use [template 19](../19-private-network-agent-tools/) for that scenario.
+5. There is no upgrade path from BYO VNet (this template) to Managed Virtual Network (template 18). A Foundry resource redeployment is required.
+6. All projects within the same Foundry account share model deployments. Per-project model isolation is not supported.
+7. Cosmos DB is deployed as single-region. Multi-region replication must be configured manually post-deployment.
+
### Account Deletion Prerequisites and Cleanup Guidance
-Before deleting an **Account** resource, it is essential to first delete the associated **Account Capability Host**.
-Failure to do so may result in residual dependenciesβsuch as subnets and other provisioned resources (e.g., ACA applications)βremaining linked to the capability host.
-This can lead to errors such as **"Subnet already in use"** when attempting to reuse the same subnet in a different account deployment.
+Before deleting an **Account** resource, it is essential to first delete the associated **Account Capability Host**. Failure to do so may result in residual dependenciesβsuch as subnets and other provisioned resources (e.g., ACA applications)βremaining linked to the capability host. This can lead to errors such as **"Subnet already in use"** when attempting to reuse the same subnet in a different account deployment.
**Cleanup Options**
-**1. Full Account Removal**:
-You may delete and purge the account.
-The service will automatically handle the deletion of the associated capability host and any linked resources in the background.
+**1. Full Account Removal**: To completely remove an account, you must delete and purge the account. Simply deleting the account is not sufficient, you must purge so that deletion of the associated capability host is triggered. The service will automatically handle the removal of the capability host and any linked resources in the background. To purge the account, use the following [link](https://learn.microsoft.com/en-us/azure/ai-services/recover-purge-resources?tabs=azure-portal#purge-a-deleted-resource). Please allow approximately max of 20 minutes for all resources to be fully unlinked from the account.
+
+**2. Retain Account, Remove Capability Host**: If you intend to retain the account but remove the capability host, execute the script `deleteCaphost.sh` located in this folder. After deletion, allow approximately max of 20 minutes for all resources to be fully unlinked from the account. To recreate the capability host for the account, use the script `createCaphost.sh` located in the same folder.
-**2. Retain Account, Remove Capability Host**:
-If you intend to retain the account but remove the capability host, you can use the script `deleteCaphost.sh` located in this folder.
> **Important**: Before deleting the account capability host, ensure that the **project capability host** is deleted.
@@ -117,10 +137,36 @@ Note: If not provided, the following resources will be created automatically for
- Azure AI Search
- Azure Storage
-**Optional Integration:** API Management services can be integrated by providing an existing API Management service resource ID.
+**Private APIM Integration (Optional):** This template supports connecting an **existing Azure API Management service** behind a private endpoint. APIM is not created by this template β you must provide the ARM Resource ID of an existing APIM instance via the `apiManagementResourceId` parameter. When provided, a private endpoint and DNS zone for APIM (`privatelink.azure-api.net`) are created within your VNet.
#### Parameters
+| Parameter | Description | Default | Required |
+|-----------|-------------|---------|----------|
+| `location` | Azure region for deployment | `eastus` | Yes |
+| `aiServices` | Base name for the AI Services resource | `aiservices` | No |
+| `firstProjectName` | Name for the Foundry project | `project` | No |
+| `modelName` | Model to deploy | `gpt-4.1` | No |
+| `modelFormat` | Model provider | `OpenAI` | No |
+| `modelVersion` | Model version | `2025-04-14` | No |
+| `modelSkuName` | Model deployment SKU | `GlobalStandard` | No |
+| `modelCapacity` | Tokens per minute (TPM) capacity | `30` | No |
+| `vnetName` | Virtual Network name | `agent-vnet-test` | No |
+| `agentSubnetName` | Subnet name for agent workloads | `agent-subnet` | No |
+| `agentSubnetPrefix` | Address prefix for agent subnet | `192.168.0.0/24` | No |
+| `peSubnetName` | Subnet name for private endpoints | `pe-subnet` | No |
+| `peSubnetPrefix` | Address prefix for PE subnet | `192.168.1.0/24` | No |
+| `existingVnetResourceId` | Full ARM Resource ID of an existing VNet | `''` (creates new) | No |
+| `vnetAddressPrefix` | Address space for new VNet | `192.168.0.0/16` | No |
+| `aiSearchResourceId` | ARM Resource ID of existing AI Search | `''` (creates new) | No |
+| `azureStorageAccountResourceId` | ARM Resource ID of existing Storage account | `''` (creates new) | No |
+| `azureCosmosDBAccountResourceId` | ARM Resource ID of existing Cosmos DB | `''` (creates new) | No |
+| `apiManagementResourceId` | ARM Resource ID of existing API Management service | `''` (no APIM) | No |
+| `dnsZonesSubscriptionId` | Subscription ID for existing DNS zones | `''` (current sub) | No |
+| `existingDnsZones` | Map of DNS zone names to resource groups | All empty (creates new) | No |
+
+#### BYO Resource Details
+
1. **Use Existing Virtual Network and Subnets**
To use an existing VNet and subnets, set the existingVnetResourceId parameter to the full Azure Resource ID of the target VNet and its address range, and provide the names of the two required subnets. If the existing VNet is associated with private DNS zones, set the existingDnsZones parameter to the resource group name in which the zones are located. For example:
@@ -129,6 +175,7 @@ To use an existing VNet and subnets, set the existingVnetResourceId parameter to
- param agentSubnetPrefix string = '192.168.0.0/24' //optional, default is '192.168.0.0/24'
- param peSubnetName string = 'pe-subnet' //optional, default is 'pe-subnet'
- param peSubnetPrefix string = '192.168.1.0/24' //optional, default is '192.168.1.0/24'
+- param dnsZonesSubscriptionId string = '' //optional, leave empty to use current subscription, or set to a subscription ID if DNS zones are in a different subscription
- param existingDnsZones = {
'privatelink.services.ai.azure.com': 'privzoneRG' //add resource group name where your private DNS zone is located
@@ -137,6 +184,10 @@ To use an existing VNet and subnets, set the existingVnetResourceId parameter to
π‘ If subnets information is provided then make sure it exist within the specified VNet to avoid deployment errors. If subnet information is not provided, the template will create subnets with the default address space.
+π‘ **Cross-Subscription DNS Zones**: All DNS zones specified in `existingDnsZones` will be referenced from the subscription specified in `dnsZonesSubscriptionId`. Leave this parameter empty (default) to use the current deployment subscription, or set it to a subscription ID if your DNS zones are located in a different subscription.
+
+β οΈ **Important**: When `dnsZonesSubscriptionId` is set to a different subscription, ALL DNS zones in `existingDnsZones` must have resource groups specified (non-empty values). The template does not support creating new DNS zones in a different subscription. Empty resource groups are only allowed when creating zones in the current deployment subscription.
+
2. **Use an existing Azure Cosmos DB for NoSQL**
@@ -197,21 +248,79 @@ Click the deploy to Azure button above to open the Azure portal and deploy the t
az group create --name --location
```
- Deploy the main.bicep file
- - Edit the main.bicepparams file to use an existing Virtual Network & subnets, Azure Cosmos DB, Azure Storage, and Azure AI Search.
+ - Edit the main.bicepparams file to use an existing Virtual Network & subnets, Azure Cosmos DB, Azure Storage, Azure AI Search, and optionally an existing APIM resource.
```bash
az deployment group create --resource-group --template-file main.bicep --parameters main.bicepparam
```
-> **Note:** To access your Foundry resource securely, use either a VM, VPN, or ExpressRoute.
+> **Note:** To access a private Foundry resource securely, use one of the following:
+> - A VM or jump box on the virtual network, optionally accessed through Azure Bastion
+> - Azure VPN Gateway
+> - Azure ExpressRoute
+
+### Cleanup
+
+To delete all resources created by this template:
+
+```bash
+az group delete --name --yes --no-wait
+```
+
+> **Important**: If you need to reuse the same subnet, follow the [Account Deletion Prerequisites and Cleanup Guidance](#account-deletion-prerequisites-and-cleanup-guidance) to properly purge the account and wait for the capability host to fully unlink (~20 minutes).
---
## Network Secured Agent Project Architecture Deep Dive
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Secure Access (VPN Gateway / ExpressRoute / Azure Bastion) β
+ββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
+ β
+ ββββββββββββββββΌβββββββββββββββ
+ β Microsoft Foundry β
+ β (publicNetworkAccess: β
+ β DISABLED) β
+ β β
+ β ββββββββββββββββββββββββββ β
+ β β Foundry Project β β
+ β β (Agent Workspace) β β
+ β βββββββββββββ¬βββββββββββββ β
+ ββββββββββββββββΌβββββββββββββββ
+ β Subnet Delegation
+ ββββββββββββββββΌβββββββββββββββ
+ β BYO Virtual Network β
+ β (192.168.0.0/16) β
+ β β
+ β ββββββββββββββββββββββββ β
+ β β Agent Subnet β β
+ β β (192.168.0.0/24) β β βββ Delegated to
+ β β Microsoft.App/envs β β Microsoft.App/environments
+ β ββββββββββββββββββββββββ β
+ β β
+ β ββββββββββββββββββββββββ β
+ β β PE Subnet β β
+ β β (192.168.1.0/24) β β
+ β β β β
+ β β ββββββββββ ββββββββββ β β
+ β β βStorage β βCosmos β β β βββ Private endpoints
+ β β ββββββββββ ββββββββββ β β (no public access)
+ β β ββββββββββ ββββββββββ β β
+ β β βSearch β βFoundry β β β
+ β β ββββββββββ ββββββββββ β β
+ β β ββββββββββ β β
+ β β βAPIM β (optional) β β
+ β β ββββββββββ β β
+ β ββββββββββββββββββββββββ β
+ ββββββββββββββββββββββββββββββββ
+```
+
+> **Tip:** For detailed layer-by-layer deployment diagrams, see the `diagrams/` folder.
+
### Core Components
-**Azure AI Foundry** resource
+**Microsoft Foundry** resource
- Central orchestration point
- Manages service connections
- Set networking and policy configurations
@@ -228,11 +337,11 @@ Click the deploy to Azure button above to open the Azure portal and deploy the t
- BYO Search: All vector stores created by the agent leverage the customerβs Azure AI Search resource.
- BYO Thread Storage: All customer messages and conversation history will be stored in the customerβs own Azure Cosmos DB account.
-By bundling these BYO features (file storage, search, and thread storage), the standard setup guarantees that your deployment is secure by default. All data processed by Azure AI Foundry Agent Service is automatically stored at rest in your own Azure resources, helping you meet internal policies, compliance requirements, and enterprise security standards.
+By bundling these BYO features (file storage, search, and thread storage), the standard setup guarantees that your deployment is secure by default. All data processed by Microsoft Foundry Agent Service is automatically stored at rest in your own Azure resources, helping you meet internal policies, compliance requirements, and enterprise security standards.
### Azure Resources Created
-Azure AI Foundry (Cognitive Services)
+Microsoft Foundry (Cognitive Services)
- Type: Microsoft.CognitiveServices/accounts
- API version: 2025-04-01-preview
- Kind: AIServices
@@ -288,6 +397,15 @@ Cosmos DB Account
- Disabled local auth
- Single region deployment
+Azure API Management (Optional β BYO)
+- Type: Microsoft.ApiManagement/service (existing resource, not created by this template)
+- Integration: Connected via private endpoint when `apiManagementResourceId` is provided
+- Private DNS Zone: `privatelink.azure-api.net`
+- Sub Resource: Gateway
+- Features:
+ - Private endpoint within the VNet for secure API access
+ - No public internet exposure when configured behind private endpoint
+
### Network Security Design
This implementation utilizes a BYO VNet (Bring Your Own Virtual Network) approach, also known as custom VNet support with subnet delegation. Within your existing virtual network, one delegated subnet will be created.
@@ -303,7 +421,7 @@ Network Security
**Private Endpoints**
Private endpoints ensure secure, internal-only connectivity. Private endpoints are created for the following:
-- Azure AI Foundry
+- Microsoft Foundry
- Azure AI Search
- Azure Storage
- Azure Cosmos DB
@@ -312,7 +430,7 @@ Private endpoints ensure secure, internal-only connectivity. Private endpoints a
**Private DNS Zones**
| Private Link Resource Type | Sub Resource | Private DNS Zone Name | Public DNS Zone Forwarders |
|----------------------------|--------------|------------------------|-----------------------------|
-| **Azure AI Foundry** | account | `privatelink.cognitiveservices.azure.com`
`privatelink.openai.azure.com`
`privatelink.services.ai.azure.com` | `cognitiveservices.azure.com`
`openai.azure.com`
`services.ai.azure.com` |
+| **Microsoft Foundry** | account | `privatelink.cognitiveservices.azure.com`
`privatelink.openai.azure.com`
`privatelink.services.ai.azure.com` | `cognitiveservices.azure.com`
`openai.azure.com`
`services.ai.azure.com` |
| **Azure AI Search** | searchService| `privatelink.search.windows.net` | `search.windows.net` |
| **Azure Cosmos DB** | Sql | `privatelink.documents.azure.com` | `documents.azure.com` |
| **Azure Storage** | blob | `privatelink.blob.core.windows.net` | `blob.core.windows.net` |
@@ -355,7 +473,7 @@ Private endpoints ensure secure, internal-only connectivity. Private endpoints a
```text
modules-network-secured/
βββ add-project-capability-host.bicep # Configuring the project's capability host
-βββ ai-account-identity.bicep # Azure AI Foundry deployment and configuration
+βββ ai-account-identity.bicep # Microsoft Foundry deployment and configuration
βββ ai-project-identity.bicep # Foundry project deployment and connection configuration
βββ ai-search-role-assignments.bicep # AI Search RBAC configuration
βββ azure-storage-account-role-assignments.bicep # Storage Account RBAC configuration
@@ -394,8 +512,8 @@ modules-network-secured/
## References
-- [Azure AI Foundry Networking Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link?tabs=azure-portal&pivots=fdp-project)
-- [Azure AI Foundry RBAC Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/rbac-azure-ai-foundry?pivots=fdp-project)
+- [Microsoft Foundry Networking Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link?tabs=azure-portal&pivots=fdp-project)
+- [Microsoft Foundry RBAC Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/rbac-azure-ai-foundry?pivots=fdp-project)
- [Private Endpoint Documentation](https://learn.microsoft.com/en-us/azure/private-link/)
- [RBAC Documentation](https://learn.microsoft.com/en-us/azure/role-based-access-control/)
- [Network Security Best Practices](https://learn.microsoft.com/en-us/azure/security/fundamentals/network-best-practices)
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/azuredeploy.json b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/azuredeploy.json
similarity index 98%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/azuredeploy.json
rename to infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/azuredeploy.json
index 3e10f19fa..ec4c0d0a3 100644
--- a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/azuredeploy.json
+++ b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/azuredeploy.json
@@ -4,8 +4,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "13929636312670904506"
+ "version": "0.43.1.21952",
+ "templateHash": "8413960230154535001"
}
},
"parameters": {
@@ -297,8 +297,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "8505298823279202405"
+ "version": "0.43.1.21952",
+ "templateHash": "17355345610868234879"
}
},
"parameters": {
@@ -411,8 +411,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "4954184648131521061"
+ "version": "0.43.1.21952",
+ "templateHash": "10901913041029806810"
}
},
"parameters": {
@@ -584,8 +584,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "3152324712046183852"
+ "version": "0.43.1.21952",
+ "templateHash": "10509554109014084911"
}
},
"parameters": {
@@ -674,8 +674,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "17043822047386586435"
+ "version": "0.43.1.21952",
+ "templateHash": "13091507001124186389"
}
},
"parameters": {
@@ -757,8 +757,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "17043822047386586435"
+ "version": "0.43.1.21952",
+ "templateHash": "13091507001124186389"
}
},
"parameters": {
@@ -929,8 +929,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "854097619778148359"
+ "version": "0.43.1.21952",
+ "templateHash": "14040588815014793423"
}
},
"parameters": {
@@ -1069,8 +1069,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "15994578372975581196"
+ "version": "0.43.1.21952",
+ "templateHash": "5338128208054238484"
}
},
"parameters": {
@@ -1248,8 +1248,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "2754228344238136934"
+ "version": "0.43.1.21952",
+ "templateHash": "18286092773595477616"
}
},
"parameters": {
@@ -1537,8 +1537,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "16093594075579008850"
+ "version": "0.43.1.21952",
+ "templateHash": "884548399218956240"
}
},
"parameters": {
@@ -2159,8 +2159,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "5095087340309076800"
+ "version": "0.43.1.21952",
+ "templateHash": "1726962209014429687"
}
},
"parameters": {
@@ -2334,8 +2334,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "6910483561575524105"
+ "version": "0.43.1.21952",
+ "templateHash": "4283212754622059328"
}
},
"parameters": {
@@ -2389,8 +2389,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "14683840003859985069"
+ "version": "0.43.1.21952",
+ "templateHash": "11244759636908765413"
}
},
"parameters": {
@@ -2405,7 +2405,7 @@
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
- "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('azureStorageName'))]",
+ "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('azureStorageName'))]",
"name": "[guid(parameters('projectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'), resourceId('Microsoft.Storage/storageAccounts', parameters('azureStorageName')))]",
"properties": {
"principalId": "[parameters('projectPrincipalId')]",
@@ -2447,8 +2447,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "2161753938341361575"
+ "version": "0.43.1.21952",
+ "templateHash": "8930392032077836844"
}
},
"parameters": {
@@ -2469,7 +2469,7 @@
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
- "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', parameters('cosmosDBName'))]",
+ "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName'))]",
"name": "[guid(parameters('projectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', '230815da-be43-4aae-9cb4-875f7bd000aa'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName')))]",
"properties": {
"principalId": "[parameters('projectPrincipalId')]",
@@ -2511,8 +2511,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "7968115481508840"
+ "version": "0.43.1.21952",
+ "templateHash": "15161371673245917725"
}
},
"parameters": {
@@ -2533,7 +2533,7 @@
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
- "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('aiSearchName'))]",
+ "scope": "[resourceId('Microsoft.Search/searchServices', parameters('aiSearchName'))]",
"name": "[guid(parameters('projectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7'), resourceId('Microsoft.Search/searchServices', parameters('aiSearchName')))]",
"properties": {
"principalId": "[parameters('projectPrincipalId')]",
@@ -2544,7 +2544,7 @@
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
- "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('aiSearchName'))]",
+ "scope": "[resourceId('Microsoft.Search/searchServices', parameters('aiSearchName'))]",
"name": "[guid(parameters('projectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0'), resourceId('Microsoft.Search/searchServices', parameters('aiSearchName')))]",
"properties": {
"principalId": "[parameters('projectPrincipalId')]",
@@ -2596,8 +2596,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "17458377866351620215"
+ "version": "0.43.1.21952",
+ "templateHash": "13412710540818916374"
}
},
"parameters": {
@@ -2690,8 +2690,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "13874725855824693255"
+ "version": "0.43.1.21952",
+ "templateHash": "13970727149060615708"
}
},
"parameters": {
@@ -2721,7 +2721,7 @@
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
- "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('storageName'))]",
+ "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageName'))]",
"name": "[guid(resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'), resourceId('Microsoft.Storage/storageAccounts', parameters('storageName')))]",
"properties": {
"principalId": "[parameters('aiProjectPrincipalId')]",
@@ -2769,8 +2769,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "17187611271934567223"
+ "version": "0.43.1.21952",
+ "templateHash": "15639047449398800481"
}
},
"parameters": {
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/azuredeploy.parameters.json b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/azuredeploy.parameters.json
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/azuredeploy.parameters.json
rename to infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/azuredeploy.parameters.json
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/deleteCapHost.sh b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/deleteCapHost.sh
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/deleteCapHost.sh
rename to infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/deleteCapHost.sh
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/main.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/main.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/main.bicep
rename to infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/main.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/main.bicepparam b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/main.bicepparam
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/main.bicepparam
rename to infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/main.bicepparam
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/metadata.json b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/metadata.json
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/metadata.json
rename to infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/metadata.json
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/add-project-capability-host.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/add-project-capability-host.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/add-project-capability-host.bicep
rename to infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/add-project-capability-host.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/ai-account-identity.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/ai-account-identity.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/ai-account-identity.bicep
rename to infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/ai-account-identity.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/ai-project-identity.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/ai-project-identity.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/ai-project-identity.bicep
rename to infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/ai-project-identity.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/ai-search-role-assignments.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/ai-search-role-assignments.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/ai-search-role-assignments.bicep
rename to infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/ai-search-role-assignments.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/azure-storage-account-role-assignment.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/azure-storage-account-role-assignment.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/azure-storage-account-role-assignment.bicep
rename to infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/azure-storage-account-role-assignment.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/blob-storage-container-role-assignments.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/blob-storage-container-role-assignments.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/blob-storage-container-role-assignments.bicep
rename to infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/blob-storage-container-role-assignments.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/cosmos-container-role-assignments.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/cosmos-container-role-assignments.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/cosmos-container-role-assignments.bicep
rename to infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/cosmos-container-role-assignments.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/cosmosdb-account-role-assignment.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/cosmosdb-account-role-assignment.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/cosmosdb-account-role-assignment.bicep
rename to infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/cosmosdb-account-role-assignment.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/existing-vnet.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/existing-vnet.bicep
similarity index 76%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/existing-vnet.bicep
rename to infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/existing-vnet.bicep
index b464dedba..b371d61e5 100644
--- a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/existing-vnet.bicep
+++ b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/existing-vnet.bicep
@@ -13,6 +13,7 @@ This module works with existing virtual networks and required subnets.
- Private endpoint subnet for secure connectivity
*/
+
@description('The name of the existing virtual network')
param vnetName string
@@ -28,24 +29,17 @@ param agentSubnetName string = 'agent-subnet'
@description('The name of Private Endpoint subnet')
param peSubnetName string = 'pe-subnet'
-@description('The name of MCP subnet for user-deployed Container Apps')
-param mcpSubnetName string = 'mcp-subnet'
-
@description('Address prefix for the agent subnet (only needed if creating new subnet)')
param agentSubnetPrefix string = ''
@description('Address prefix for the private endpoint subnet (only needed if creating new subnet)')
param peSubnetPrefix string = ''
-@description('Address prefix for the MCP subnet (only needed if creating new subnet)')
-param mcpSubnetPrefix string = ''
-
// Get the address space (array of CIDR strings)
var vnetAddressSpace = existingVNet.properties.addressSpace.addressPrefixes[0]
var agentSubnetSpaces = empty(agentSubnetPrefix) ? cidrSubnet(vnetAddressSpace, 24, 0) : agentSubnetPrefix
var peSubnetSpaces = empty(peSubnetPrefix) ? cidrSubnet(vnetAddressSpace, 24, 1) : peSubnetPrefix
-var mcpSubnetSpaces = empty(mcpSubnetPrefix) ? cidrSubnet(vnetAddressSpace, 24, 2) : mcpSubnetPrefix
// Reference the existing virtual network
resource existingVNet 'Microsoft.Network/virtualNetworks@2024-05-01' existing = {
@@ -84,32 +78,11 @@ module peSubnet 'subnet.bicep' = {
}
}
-// Create the MCP subnet for user-deployed Container Apps
-module mcpSubnet 'subnet.bicep' = {
- name: 'mcp-subnet-${uniqueString(deployment().name, mcpSubnetName)}'
- scope: resourceGroup(vnetResourceGroupName)
- params: {
- vnetName: vnetName
- subnetName: mcpSubnetName
- addressPrefix: mcpSubnetSpaces
- delegations: [
- {
- name: 'Microsoft.App/environments'
- properties: {
- serviceName: 'Microsoft.App/environments'
- }
- }
- ]
- }
-}
-
// Output variables
output peSubnetName string = peSubnetName
output agentSubnetName string = agentSubnetName
-output mcpSubnetName string = mcpSubnetName
output agentSubnetId string = '${existingVNet.id}/subnets/${agentSubnetName}'
output peSubnetId string = '${existingVNet.id}/subnets/${peSubnetName}'
-output mcpSubnetId string = '${existingVNet.id}/subnets/${mcpSubnetName}'
output virtualNetworkName string = existingVNet.name
output virtualNetworkId string = existingVNet.id
output virtualNetworkResourceGroup string = vnetResourceGroupName
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/format-project-workspace-id.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/format-project-workspace-id.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/format-project-workspace-id.bicep
rename to infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/format-project-workspace-id.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/network-agent-vnet.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/network-agent-vnet.bicep
new file mode 100644
index 000000000..bad8a4f27
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/network-agent-vnet.bicep
@@ -0,0 +1,67 @@
+@description('Azure region for the deployment')
+param location string
+
+@description('The name of the virtual network')
+param vnetName string
+
+@description('Indicates if an existing VNet should be used')
+param useExistingVnet bool = false
+
+@description('Subscription ID of the existing VNet (if different from current subscription)')
+param existingVnetSubscriptionId string = subscription().subscriptionId
+
+@description('Resource Group name of the existing VNet (if different from current resource group)')
+param existingVnetResourceGroupName string = resourceGroup().name
+
+@description('The name of Agents Subnet')
+param agentSubnetName string = 'agent-subnet'
+
+@description('The name of Private Endpoint subnet')
+param peSubnetName string = 'pe-subnet'
+
+@description('Address space for the VNet (only used for new VNet)')
+param vnetAddressPrefix string = ''
+
+@description('Address prefix for the agent subnet')
+param agentSubnetPrefix string = ''
+
+@description('Address prefix for the private endpoint subnet')
+param peSubnetPrefix string = ''
+
+// Create new VNet if needed
+module newVNet 'vnet.bicep' = if (!useExistingVnet) {
+ name: 'vnet-deployment'
+ params: {
+ location: location
+ vnetName: vnetName
+ agentSubnetName: agentSubnetName
+ peSubnetName: peSubnetName
+ vnetAddressPrefix: vnetAddressPrefix
+ agentSubnetPrefix: agentSubnetPrefix
+ peSubnetPrefix: peSubnetPrefix
+ }
+}
+
+// Use existing VNet if requested
+module existingVNet 'existing-vnet.bicep' = if (useExistingVnet) {
+ name: 'existing-vnet-deployment'
+ params: {
+ vnetName: vnetName
+ vnetResourceGroupName: existingVnetResourceGroupName
+ vnetSubscriptionId: existingVnetSubscriptionId
+ agentSubnetName: agentSubnetName
+ peSubnetName: peSubnetName
+ agentSubnetPrefix: agentSubnetPrefix
+ peSubnetPrefix: peSubnetPrefix
+ }
+}
+
+// Provide unified outputs regardless of which module was used
+output virtualNetworkName string = useExistingVnet ? existingVNet.outputs.virtualNetworkName : newVNet.outputs.virtualNetworkName
+output virtualNetworkId string = useExistingVnet ? existingVNet.outputs.virtualNetworkId : newVNet.outputs.virtualNetworkId
+output virtualNetworkSubscriptionId string = useExistingVnet ? existingVNet.outputs.virtualNetworkSubscriptionId : newVNet.outputs.virtualNetworkSubscriptionId
+output virtualNetworkResourceGroup string = useExistingVnet ? existingVNet.outputs.virtualNetworkResourceGroup : newVNet.outputs.virtualNetworkResourceGroup
+output agentSubnetName string = agentSubnetName
+output peSubnetName string = peSubnetName
+output agentSubnetId string = useExistingVnet ? existingVNet.outputs.agentSubnetId : newVNet.outputs.agentSubnetId
+output peSubnetId string = useExistingVnet ? existingVNet.outputs.peSubnetId : newVNet.outputs.peSubnetId
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/private-endpoint-and-dns.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/private-endpoint-and-dns.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/private-endpoint-and-dns.bicep
rename to infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/private-endpoint-and-dns.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/standard-dependent-resources.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/standard-dependent-resources.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/standard-dependent-resources.bicep
rename to infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/standard-dependent-resources.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/subnet.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/subnet.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/subnet.bicep
rename to infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/subnet.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/validate-existing-resources.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/validate-existing-resources.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview/modules-network-secured/validate-existing-resources.bicep
rename to infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/validate-existing-resources.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/vnet.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/vnet.bicep
new file mode 100644
index 000000000..d5b8db277
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/vnet.bicep
@@ -0,0 +1,83 @@
+/*
+Virtual Network Module
+This module deploys the core network infrastructure with security controls:
+
+1. Address Space:
+ - VNet CIDR: 172.16.0.0/16 OR 192.168.0.0/16
+ - Agents Subnet: 172.16.0.0/24 OR 192.168.0.0/24
+ - Private Endpoint Subnet: 172.16.101.0/24 OR 192.168.1.0/24
+
+2. Security Features:
+ - Network isolation
+ - Subnet delegation
+ - Private endpoint subnet
+*/
+
+@description('Azure region for the deployment')
+param location string
+
+@description('The name of the virtual network')
+param vnetName string = 'agents-vnet-test'
+
+@description('The name of Agents Subnet')
+param agentSubnetName string = 'agent-subnet'
+
+@description('The name of Hub subnet')
+param peSubnetName string = 'pe-subnet'
+
+
+@description('Address space for the VNet')
+param vnetAddressPrefix string = ''
+
+@description('Address prefix for the agent subnet')
+param agentSubnetPrefix string = ''
+
+@description('Address prefix for the private endpoint subnet')
+param peSubnetPrefix string = ''
+var defaultVnetAddressPrefix = '192.168.0.0/16'
+var vnetAddress = empty(vnetAddressPrefix) ? defaultVnetAddressPrefix : vnetAddressPrefix
+var agentSubnet = empty(agentSubnetPrefix) ? cidrSubnet(vnetAddress, 24, 0) : agentSubnetPrefix
+var peSubnet = empty(peSubnetPrefix) ? cidrSubnet(vnetAddress, 24, 1) : peSubnetPrefix
+
+resource virtualNetwork 'Microsoft.Network/virtualNetworks@2024-05-01' = {
+ name: vnetName
+ location: location
+ properties: {
+ addressSpace: {
+ addressPrefixes: [
+ vnetAddress
+ ]
+ }
+ subnets: [
+ {
+ name: agentSubnetName
+ properties: {
+ addressPrefix: agentSubnet
+ delegations: [
+ {
+ name: 'Microsoft.app/environments'
+ properties: {
+ serviceName: 'Microsoft.App/environments'
+ }
+ }
+ ]
+ }
+ }
+ {
+ name: peSubnetName
+ properties: {
+ addressPrefix: peSubnet
+ }
+ }
+ ]
+ }
+}
+// Output variables
+output peSubnetName string = peSubnetName
+output agentSubnetName string = agentSubnetName
+output agentSubnetId string = '${virtualNetwork.id}/subnets/${agentSubnetName}'
+output peSubnetId string = '${virtualNetwork.id}/subnets/${peSubnetName}'
+output virtualNetworkName string = virtualNetwork.name
+output virtualNetworkId string = virtualNetwork.id
+output virtualNetworkResourceGroup string = resourceGroup().name
+output virtualNetworkSubscriptionId string = subscription().subscriptionId
diff --git a/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/README.md b/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/README.md
index e098f8941..b8c9bcf93 100644
--- a/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/README.md
+++ b/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/README.md
@@ -1,5 +1,5 @@
---
-description: This set of templates demonstrates how to set up Azure AI Agent Service with virtual network isolation using User Managed Identity authetication for the AI Service with private network links to connect the agent to your secure data.
+description: This set of templates demonstrates how to set up Foundry Agent Service with virtual network isolation using User Assigned Managed Identity authentication for the AI Service with private network links to connect the agent to your secure data.
page_type: sample
products:
- azure
@@ -10,45 +10,57 @@ languages:
- json
---
-# Azure AI Agent Service: Standard Agent Setup with E2E Network Isolation and UAI
-
-> **IMPORTANT**
->
-> Private Class A subnet support is GA and available in the following regions. **Supported regions: Australia East, Brazil South, Canada East, East US, East US 2, France Central, Germany West Central, Italy North, Japan East, South Africa North, South Central US, South India, Spain Central, Sweden Central, UAE North, UK South, West Europe, West US, West US 3.**
->
-> Private Class B and C subnet support is already GA and available in all regions supported by Azure AI Foundry Agent Service. Deployment templates and setup steps are identical for Class A, B, and C subnets. For more on the supported regions of the Azure AI Foundry Agent service, see [Models supported by Azure AI Foundry Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/concepts/model-region-support?tabs=global-standard)
+# Foundry Agent Service: Standard Agent Setup with E2E Network Isolation and User Assigned Identity
+> **NEW**
+> For support on deploying the right network isolation template, check out the [GitHub Copilot for Azure skill for private networking](https://github.com/microsoft/GitHub-Copilot-for-Azure/blob/main/plugin/skills/microsoft-foundry/resource/private-network/private-network.md) set-up!
---
## Overview
-This infrastructure-as-code (IaC) solution deploys a network-secured Azure AI agent environment with private networking and role-based access control (RBAC).
-
-Standard setup supports private network isolation through utilizing **Bring Your Own Virtual Network (BYO VNet)** approach, also known as **custom VNet support with subnet delegation.**
+This infrastructure-as-code (IaC) solution deploys a network-secured agent environment with private networking and role-based access control (RBAC) and **User-assigned identity**. Standard setup supports private network isolation through utilizing **Bring Your Own Virtual Network (BYO VNet)** approach, also known as **custom VNet support with subnet delegation.**
This implementation gives you full control over the inbound and outbound communication paths for your agent. You can restrict access to only the resources explicitly required by your agent, such as storage accounts, databases, or APIs, while blocking all other traffic by default. This approach ensures that your agent operates within a tightly scoped network boundary, reducing the risk of data leakage or unauthorized access. By default, this setup simplifies security configuration while enforcing strong isolation guarantees, ensuring that each agent deployment remains secure, compliant, and aligned with enterprise networking policies.
---
-## Key Information
+## When to Use This Template
+
+Use this template when you need:
+- **Full end-to-end network isolation** β All resources behind private endpoints with no public internet access
+- **BYO VNet control** β You manage your own virtual network, subnets, and network security groups
+- **Standard agent setup with BYO resources** β Customer-managed Storage, Cosmos DB, and AI Search for data residency and compliance
+- **User Assigned Managed Identity** β Customer-managed identity for greater control over credential lifecycle and cross-resource sharing
+
+### Template Decision Guide
+
+Use the table below to choose the right infrastructure template for your scenario:
-**Limited Region Support for Class A Subnet IPs**
-- Class A is supported in a subset of regions supported by the Azure AI Foundry Agent service.
+| Template | Agent Type | Networking | Identity | Key Use Case |
+|----------|-----------|------------|----------|-------------|
+| [**15**](../15-private-network-standard-agent-setup/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | E2E network isolation with full agent capabilities |
+| [**19**](../19-private-network-agent-tools/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | Same as 15 **plus** tools behind VNet (MCP, OpenAPI, Functions, A2A) |
+| [**17** (this template)](../17-private-network-standard-user-assigned-identity-agent-setup/) | Standard (BYO resources) | BYO VNet + Private Endpoints | **User Assigned MI** | Same as 15 but with user-managed identity |
+| [**16**](../16-private-network-standard-agent-apim-setup-preview/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | Same as 15 **plus** private APIM integration (preview) |
+| [**18**](../18-managed-virtual-network-preview/) | Standard (BYO resources) | **Managed VNet** (Microsoft-managed) | System Assigned MI | Network isolation without managing your own VNet (preview) |
+| [**15a**](../15a-private-network-evaluation-only-setup/) | Evaluation only | BYO VNet + Private Endpoints | System Assigned MI | Minimal setup for evaluation β no Cosmos DB, AI Search, or capability host |
+| [**11**](../11-private-network-basic-vnet/) | **Basic** (platform-managed) | BYO VNet injection | System Assigned MI | Basic agents with VNet isolation β no BYO resources needed |
+| [**41**](../41-standard-agent-setup/) | Standard (BYO resources) | **Public** (no VNet) | System Assigned MI | Standard agents without network isolation |
+| [**40**](../40-basic-agent-setup/) | **Basic** (platform-managed) | **Public** (no VNet) | System Assigned MI | Simplest setup β no BYO resources, no private networking |
-**Region and Resource Placement Requirements**
-- **All Foundry workspace resources should be in the same region as the VNet**, including CosmosDB, Storage Account, AI Search, Foundry Account, Project, Managed Identity. The only exception is within the Foundry Account, you may choose to deploy your model to a different region, and any cross-region communication will be handled securely within our network infrastructure.
- - **Note:** Your Virtual Network can be in a different resource group than your Foundry workspace resources
+---
+## Deploy to Azure
-[](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fazure-ai-foundry%2Ffoundry-samples%2Frefs%2Fheads%2Fmain%2Finfrastructure%2Finfrastructure-setup-bicep%2F15-private-network-standard-agent-setup%2Fazuredeploy.json)
+[](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fazure-ai-foundry%2Ffoundry-samples%2Frefs%2Fheads%2Fmain%2Finfrastructure%2Finfrastructure-setup-bicep%2F17-private-network-standard-user-assigned-identity-agent-setup%2Fazuredeploy.json)
---
## Prerequisites
1. **Active Azure subscription with appropriate permissions**
- - **Azure AI Account Owner**: Needed to create a cognitive services account and project
- - **Owner or Role Based Access Administrator**: Needed to assign RBAC to the required resources (Cosmos DB, Azure AI Search, Storage)
- - **Azure AI User**: Needed to create and edit agents
+ - **Foundry Account Owner**: Needed to create the Microsoft Foundry account and project.
+ - **Owner or Role Based Access Administrator**: Needed to assign RBAC on the Azure resources used by this template.
+ - **Foundry User**: Needed to create and use agents, projects, or evaluation workloads after deployment.
1. **Register Resource Providers**
@@ -66,8 +78,8 @@ This implementation gives you full control over the inbound and outbound communi
1. Network administrator permissions (if operating in a restricted or enterprise environment)
-1. Sufficient quota for all resources in your target Azure region
- * If no parameters are passed in, this template creates an Azure AI Foundry resource, Foundry project, Azure Cosmos DB for NoSQL, Azure AI Search, and Azure Storage account
+1. Sufficient quota for all resources required by this template in the target Azure region, including model deployment quota.
+ * If no parameters are passed in, this template creates an Microsoft Foundry resource, Foundry project, Azure Cosmos DB for NoSQL, Azure AI Search, and Azure Storage account
1. Azure CLI installed and configured on your local workstation or deployment pipeline server
---
@@ -80,15 +92,25 @@ This implementation gives you full control over the inbound and outbound communi
2. Two subnets are needed as well:
- **Agent Subnet** (e.g., 192.168.0.0/24): Hosts Agent client for Agent workloads, delegated to Microsoft.App/environments. The recommended size should be /24 for this delegated subnet.
- **Private endpoint Subnet** (e.g. 192.168.1.0/24): Hosts private endpoints
- - Ensure that the address spaces for the used VNET does not overlap with any existing networks in your Azure environment or reserved IP ranges like the following: 169.254.0.0/16,172.30.0.0/16,172.31.0.0/16,192.0.2.0/24,0.0.0.0/8,127.0.0.0/8,100.100.0.0/17,100.100.192.0/19,100.100.224.0/19,10.0.0.0/8.
+ - Ensure that the address spaces for the used VNET does not overlap with any existing networks in your Azure environment or reserved IP ranges like the following: 169.254.0.0/16,172.30.0.0/16,172.31.0.0/16,192.0.2.0/24,0.0.0.0/8,127.0.0.0/8,100.100.0.0/17,100.100.192.0/19,100.100.224.0/19,100.64.0.0/11.
This includes all address space(s) you have in your VNET if you have more than one, and peered VNETs.
> **Notes:**
- If you do not provide an existing virtual network, the template will create a new virtual network with the default address spaces and subnets described above. If you use an existing virtual network, make sure it already contains two subnets (Agent and Private Endpoint) before deploying the template.
- You must ensure the Foundry account was successfully created so that underlying caphost has also succeeded. Then proceed to deploying the project caphost bicep.
- - You must ensure the subnet is not already in use by another account. It must be an exclusive subnet for the Foundry account.
- You must ensure the subnet is exclusively delegated to __Microsoft.App/environments__ and cannot be used by any other Azure resources.
-
+
+
+
+### Limitations / Known Issues
+
+1. The delegated agent subnet must be exclusively used by a single Foundry account. It cannot be shared across accounts.
+2. The Foundry resource and the virtual network must be in the same Azure region. BYO resources (Storage, Cosmos DB, AI Search) may be in different regions.
+3. For the virtual network IP range, you may use any Private Class A, B or C IP range. Private Class A IP address ranges (10.x.x.x) are only supported in the following regions: **Australia East, Brazil South, Canada East, East US, East US 2, France Central, Germany West Central, Italy North, Japan East, South Africa North, South Central US, South India, Spain Central, Sweden Central, UAE North, UK South, West US, West US 3.** Use Class B (172.16.x.x) or C (192.168.x.x) ranges for other regions. You may not use any other IP range that overlaps to the list above or uses public IP ranges.
+4. This template does **not** support tools (MCP servers, OpenAPI tools, Azure Functions, A2A) behind the VNet. Use [template 19](../19-private-network-agent-tools/) for that scenario.
+5. There is no upgrade path from BYO VNet (this template) to Managed Virtual Network (template 18). A Foundry resource redeployment is required.
+6. All projects within the same Foundry account share model deployments. Per-project model isolation is not supported.
+7. Cosmos DB is deployed as single-region. Multi-region replication must be configured manually post-deployment.
### Account Deletion Prerequisites and Cleanup Guidance
@@ -116,6 +138,31 @@ Note: If not provided, the following resources will be created automatically for
#### Parameters
+| Parameter | Description | Default | Required |
+|-----------|-------------|---------|----------|
+| `location` | Azure region for deployment | `eastus` | Yes |
+| `aiServices` | Base name for the AI Services resource | `aiservices` | No |
+| `firstProjectName` | Name for the Foundry project | `project` | No |
+| `modelName` | Model to deploy | `gpt-4.1` | No |
+| `modelFormat` | Model provider | `OpenAI` | No |
+| `modelVersion` | Model version | `2025-04-14` | No |
+| `modelSkuName` | Model deployment SKU | `GlobalStandard` | No |
+| `modelCapacity` | Tokens per minute (TPM) capacity | `30` | No |
+| `vnetName` | Virtual Network name | `agent-vnet-test` | No |
+| `agentSubnetName` | Subnet name for agent workloads | `agent-subnet` | No |
+| `agentSubnetPrefix` | Address prefix for agent subnet | `192.168.0.0/24` | No |
+| `peSubnetName` | Subnet name for private endpoints | `pe-subnet` | No |
+| `peSubnetPrefix` | Address prefix for PE subnet | `192.168.1.0/24` | No |
+| `existingVnetResourceId` | Full ARM Resource ID of an existing VNet | `''` (creates new) | No |
+| `vnetAddressPrefix` | Address space for new VNet | `192.168.0.0/16` | No |
+| `aiSearchResourceId` | ARM Resource ID of existing AI Search | `''` (creates new) | No |
+| `azureStorageAccountResourceId` | ARM Resource ID of existing Storage account | `''` (creates new) | No |
+| `azureCosmosDBAccountResourceId` | ARM Resource ID of existing Cosmos DB | `''` (creates new) | No |
+| `dnsZonesSubscriptionId` | Subscription ID for existing DNS zones | `''` (current sub) | No |
+| `existingDnsZones` | Map of DNS zone names to resource groups | All empty (creates new) | No |
+
+#### BYO Resource Details
+
1. **Use Existing Virtual Network and Subnets**
To use an existing VNet and subnets, set the existingVnetResourceId parameter to the full Azure Resource ID of the target VNet and its address range, and provide the names of the two required subnets. If the existing VNet is associated with private DNS zones, set the existingDnsZones parameter to the resource group name in which the zones are located. For example:
@@ -124,6 +171,7 @@ To use an existing VNet and subnets, set the existingVnetResourceId parameter to
- param agentSubnetPrefix string = '192.168.0.0/24' //optional, default is '192.168.0.0/24'
- param peSubnetName string = 'pe-subnet' //optional, default is 'pe-subnet'
- param peSubnetPrefix string = '192.168.1.0/24' //optional, default is '192.168.1.0/24'
+- param dnsZonesSubscriptionId string = '' //optional, leave empty to use current subscription, or set to a subscription ID if DNS zones are in a different subscription
- param existingDnsZones = {
'privatelink.services.ai.azure.com': 'privzoneRG' //add resource group name where your private DNS zone is located
@@ -132,31 +180,16 @@ To use an existing VNet and subnets, set the existingVnetResourceId parameter to
π‘ If subnets information is provided then make sure it exist within the specified VNet to avoid deployment errors. If subnet information is not provided, the template will create subnets with the default address space.
+π‘ **Cross-Subscription DNS Zones**: All DNS zones specified in `existingDnsZones` will be referenced from the subscription specified in `dnsZonesSubscriptionId`. Leave this parameter empty (default) to use the current deployment subscription, or set it to a subscription ID if your DNS zones are located in a different subscription.
+
+β οΈ **Important**: When `dnsZonesSubscriptionId` is set to a different subscription, ALL DNS zones in `existingDnsZones` must have resource groups specified (non-empty values). The template does not support creating new DNS zones in a different subscription. Empty resource groups are only allowed when creating zones in the current deployment subscription.
+
2. **Use an existing Azure Cosmos DB for NoSQL**
To use an existing Cosmos DB for NoSQL resource, set cosmosDBResourceId parameter to the full Azure Resource ID of the target Cosmos DB.
- param azureCosmosDBAccountResourceId string = /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/{cosmosDbAccountName}
-> **β οΈ Important: Cosmos DB Connection Requirements**
->
-> When creating the Cosmos DB connection (e.g., via REST API or ARM), ensure the following:
-> - The `authType` **must** be set to `AAD`. This is the only supported authentication type for the Cosmos DB connection used by the Agent Service.
-> - The `metadata` section **must** include the `ResourceId` property, set to the full Azure Resource ID of your Cosmos DB account. The Agent Service relies on this property to correctly identify and connect to your Cosmos DB resource. Omitting `ResourceId` from the metadata will cause the connection to fail.
->
-> Example connection properties:
-> ```json
-> {
-> "category": "CosmosDB",
-> "authType": "AAD",
-> "metadata": {
-> "ApiType": "Azure",
-> "ResourceId": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/{cosmosDbAccountName}",
-> "location": "{region}"
-> }
-> }
-> ```
-
3. **Use an existing Azure AI Search resource**
@@ -193,15 +226,70 @@ Click the deploy to Azure button above to open the Azure portal and deploy the t
az deployment group create --resource-group --template-file main.bicep --parameters main.bicepparam
```
-> **Note:** To access your Foundry resource securely, use either a VM, VPN, or ExpressRoute.
+> **Note:** To access a private Foundry resource securely, use one of the following:
+> - A VM or jump box on the virtual network, optionally accessed through Azure Bastion
+> - Azure VPN Gateway
+> - Azure ExpressRoute
+
+### Cleanup
+
+To delete all resources created by this template:
+
+```bash
+az group delete --name --yes --no-wait
+```
+
+> **Important**: If you need to reuse the same subnet, follow the [Account Deletion Prerequisites and Cleanup Guidance](#account-deletion-prerequisites-and-cleanup-guidance) to properly purge the account and wait for the capability host to fully unlink (~20 minutes).
---
## Network Secured Agent Project Architecture Deep Dive
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Secure Access (VPN Gateway / ExpressRoute / Azure Bastion) β
+ββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
+ β
+ ββββββββββββββββΌβββββββββββββββ
+ β Microsoft Foundry β
+ β (publicNetworkAccess: β
+ β DISABLED) β
+ β β
+ β ββββββββββββββββββββββββββ β
+ β β Foundry Project β β
+ β β (Agent Workspace) β β
+ β βββββββββββββ¬βββββββββββββ β
+ ββββββββββββββββΌβββββββββββββββ
+ β Subnet Delegation
+ ββββββββββββββββΌβββββββββββββββ
+ β BYO Virtual Network β
+ β (192.168.0.0/16) β
+ β β
+ β ββββββββββββββββββββββββ β
+ β β Agent Subnet β β
+ β β (192.168.0.0/24) β β βββ Delegated to
+ β β Microsoft.App/envs β β Microsoft.App/environments
+ β ββββββββββββββββββββββββ β
+ β β
+ β ββββββββββββββββββββββββ β
+ β β PE Subnet β β
+ β β (192.168.1.0/24) β β
+ β β β β
+ β β ββββββββββ ββββββββββ β β
+ β β βStorage β βCosmos β β β βββ Private endpoints
+ β β ββββββββββ ββββββββββ β β (no public access)
+ β β ββββββββββ ββββββββββ β β
+ β β βSearch β βFoundry β β β
+ β β ββββββββββ ββββββββββ β β
+ β ββββββββββββββββββββββββ β
+ ββββββββββββββββββββββββββββββββ
+```
+
+> **Tip:** For detailed layer-by-layer deployment diagrams, see the `diagrams/` folder.
+
### Core Components
-**Azure AI Foundry** resource
+**Microsoft Foundry** resource
- Central orchestration point
- Manages service connections
- Set networking and policy configurations
@@ -218,11 +306,11 @@ Click the deploy to Azure button above to open the Azure portal and deploy the t
- BYO Search: All vector stores created by the agent leverage the customerβs Azure AI Search resource.
- BYO Thread Storage: All customer messages and conversation history will be stored in the customerβs own Azure Cosmos DB account.
-By bundling these BYO features (file storage, search, and thread storage), the standard setup guarantees that your deployment is secure by default. All data processed by Azure AI Foundry Agent Service is automatically stored at rest in your own Azure resources, helping you meet internal policies, compliance requirements, and enterprise security standards.
+By bundling these BYO features (file storage, search, and thread storage), the standard setup guarantees that your deployment is secure by default. All data processed by Microsoft Foundry Agent Service is automatically stored at rest in your own Azure resources, helping you meet internal policies, compliance requirements, and enterprise security standards.
### Azure Resources Created
-Azure AI Foundry (Cognitive Services)
+Microsoft Foundry (Cognitive Services)
- Type: Microsoft.CognitiveServices/accounts
- API version: 2025-04-01-preview
- Kind: AIServices
@@ -293,7 +381,7 @@ Network Security
**Private Endpoints**
Private endpoints ensure secure, internal-only connectivity. Private endpoints are created for the following:
-- Azure AI Foundry
+- Microsoft Foundry
- Azure AI Search
- Azure Storage
- Azure Cosmos DB
@@ -301,7 +389,7 @@ Private endpoints ensure secure, internal-only connectivity. Private endpoints a
**Private DNS Zones**
| Private Link Resource Type | Sub Resource | Private DNS Zone Name | Public DNS Zone Forwarders |
|----------------------------|--------------|------------------------|-----------------------------|
-| **Azure AI Foundry** | account | `privatelink.cognitiveservices.azure.com`
`privatelink.openai.azure.com`
`privatelink.services.ai.azure.com` | `cognitiveservices.azure.com`
`openai.azure.com`
`services.ai.azure.com` |
+| **Microsoft Foundry** | account | `privatelink.cognitiveservices.azure.com`
`privatelink.openai.azure.com`
`privatelink.services.ai.azure.com` | `cognitiveservices.azure.com`
`openai.azure.com`
`services.ai.azure.com` |
| **Azure AI Search** | searchService| `privatelink.search.windows.net` | `search.windows.net` |
| **Azure Cosmos DB** | Sql | `privatelink.documents.azure.com` | `documents.azure.com` |
| **Azure Storage** | blob | `privatelink.blob.core.windows.net` | `blob.core.windows.net` |
@@ -313,7 +401,7 @@ Private endpoints ensure secure, internal-only connectivity. Private endpoints a
- No credential storage
- Platform-managed rotation
- This template uses System Managed Identity, but User Assigned Managed Identity is also supported.
+ This template uses User Assigned Managed Identity for greater control over credential lifecycle and cross-resource sharing. System Assigned Managed Identity is also supported via [template 15](../15-private-network-standard-agent-setup/).
- **Role Assignments**
- **Azure AI Search**
@@ -343,7 +431,7 @@ Private endpoints ensure secure, internal-only connectivity. Private endpoints a
```text
modules-network-secured/
βββ add-project-capability-host.bicep # Configuring the project's capability host
-βββ ai-account-identity.bicep # Azure AI Foundry deployment and configuration
+βββ ai-account-identity.bicep # Microsoft Foundry deployment and configuration
βββ ai-project-identity.bicep # Foundry project deployment and connection configuration
βββ ai-search-role-assignments.bicep # AI Search RBAC configuration
βββ azure-storage-account-role-assignments.bicep # Storage Account RBAC configuration
@@ -380,172 +468,11 @@ modules-network-secured/
4. Review network security groups
---
----
-# (Optional) Adding Multiple Projects to AI Foundry Deployment
-
-This guide explains how to add additional projects to your existing AI Foundry deployment with network security and capability hosts.
-
-## Overview
-
-After deploying your initial AI Foundry setup using `main.bicep`, you can add additional projects using the modular approach provided in this repository. Each new project will:
-
-- β
**Reuse existing shared infrastructure** (AI Services account, Storage, Cosmos DB, AI Search, VNet)
-- β
**Create independent projects** with unique identities and connections
-- β
**Set up proper role assignments** and capability hosts for each project
-- β
**Maintain network security** configurations from your original deployment
-- β
**Deploy independently** without affecting existing projects
-
-## Files Added
-
-### Core Deployment Files
-
-| File | Purpose |
-|------|---------|
-| `add-project.bicep` | Main Bicep template for adding new projects |
-| `add-project.bicepparam` | Parameters file template for new projects |
-| `modules-network-secured/ai-project-identity-unique.bicep` | Modified project module with unique connection names |
-| `modules-network-secured/blob-storage-container-role-assignments-unique.bicep` | Modified storage role assignment module |
-
-### Helper Files
-
-| File | Purpose |
-|------|---------|
-| `get-existing-resources.ps1` | PowerShell script to discover existing resource names |
-
-## Prerequisites
-
-1. β
**Existing AI Foundry deployment** completed using `main.bicep`
-2. β
**Azure CLI** installed and logged in
-3. β
**Proper permissions** on the resource group and existing resources
-4. β
**Resource names** from your existing deployment
-
-## Step-by-Step Guide
-
-### Step 1: Discover Existing Resource Names
-
-Run the PowerShell script to automatically discover your existing resource names:
-
-```powershell
-# Navigate to your repository folder
-cd "path\to\your\AgentRepro\folder"
-
-# Run the discovery script
-.\get-existing-resources.ps1 -ResourceGroupName "your-resource-group-name"
-
-# Optional: Include subscription ID if needed
-.\get-existing-resources.ps1 -ResourceGroupName "your-resource-group-name" -SubscriptionId "your-subscription-id"
-```
-
-**Example output:**
-```
-=== Summary for add-project.bicepparam ===
-param existingAccountName = 'aiservicesytlz'
-param existingAiSearchName = 'aiservicesytlzsearch'
-param existingStorageName = 'aiservicesytlzstorage'
-param existingCosmosDBName = 'aiservicesytlzcosmosdb'
-param accountResourceGroupName = 'agenticvnet'
-param aiSearchResourceGroupName = 'agenticvnet'
-param storageResourceGroupName = 'agenticvnet'
-param cosmosDBResourceGroupName = 'agenticvnet'
-```
-
-### Step 2: Configure Parameters File
-
-Copy the output from Step 1 and update your `add-project.bicepparam` file:
-
-### Step 3: Deploy the New Project
-
-Deploy using Azure CLI:
-
-```powershell
-az deployment group create `
- --resource-group "your-resource-group" `
- --template-file "add-project.bicep" `
- --parameters "add-project.bicepparam"
-```
-
-## Adding Multiple Projects
-
-To add additional projects, repeat the process with different parameter values:
-
-### For a Third Project:
-
-1. **Update project-specific parameters:**
- ```bicep
- param projectName = 'thirdproject' // Must be unique
- param displayName = 'Third Project'
- param projectCapHost = 'caphostthird' // Must be unique
- ```
-
-3. **Deploy using the new parameters file:**
- ```powershell
- az deployment group create `
- --resource-group "your-resource-group" `
- --template-file "add-project.bicep" `
- --parameters "add-project.bicepparam"
- ```
-
-## What Gets Created
-
-Each new project deployment creates:
-
-| Resource | Description |
-|----------|-------------|
-| **AI Foundry Project** | New project under your existing AI Services account |
-| **Managed Identity** | Project-specific system-assigned identity |
-| **Unique Connections** | Project-specific connections to shared resources |
-| **Capability Host** | Configured for Agents with proper connections |
-| **RBAC Assignments** | Proper permissions on shared resources |
-
-### Role Assignments Created:
-
-- β
**Storage Blob Data Contributor** on Storage Account
-- β
**Storage Blob Data Owner** on project-specific containers
-- β
**Cosmos DB Operator** on Cosmos DB Account
-- β
**Cosmos Built-In Data Contributor** on project-specific containers
-- β
**Search Index Data Contributor** on AI Search Service
-- β
**Search Service Contributor** on AI Search Service
-
-## Configuration Reference
-
-### Required Parameters (Must Customize for Each Project)
-
-| Parameter | Description | Example |
-|-----------|-------------|---------|
-| `projectName` | Unique name for the project | `'secondproject'` |
-| `displayName` | Display name in Azure portal | `'Second Project'` |
-| `projectCapHost` | Unique capability host name | `'caphostsecond'` |
-| `projectDescription` | Description of the project | `'My second AI project'` |
-
-### Existing Resource Parameters (From Script)
-
-| Parameter | Description | Source |
-|-----------|-------------|---------|
-| `existingAccountName` | AI Services account name | Output from `get-existing-resources.ps1` |
-| `existingAiSearchName` | AI Search service name | Output from `get-existing-resources.ps1` |
-| `existingStorageName` | Storage account name | Output from `get-existing-resources.ps1` |
-| `existingCosmosDBName` | Cosmos DB account name | Output from `get-existing-resources.ps1` |
-| `*ResourceGroupName` | Resource group names | Usually same as deployment RG |
-| `*SubscriptionId` | Subscription IDs | Usually same subscription |
-
-
-## Security Considerations
-
-- β
**Least Privilege**: Each project gets only the permissions it needs
-- β
**Isolated Containers**: Projects get separate storage containers
-- β
**Network Security**: Inherits network security from original deployment
-- β
**Unique Identities**: Each project has its own managed identity
-
-## Limitations
-
-- π All projects share the same model deployments
-- π Projects must be in the same region as the original deployment
-- π Network configuration is inherited from original deployment
## References
-- [Azure AI Foundry Networking Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link?tabs=azure-portal&pivots=fdp-project)
-- [Azure AI Foundry RBAC Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/rbac-azure-ai-foundry?pivots=fdp-project)
+- [Microsoft Foundry Networking Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link?tabs=azure-portal&pivots=fdp-project)
+- [Microsoft Foundry RBAC Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/rbac-azure-ai-foundry?pivots=fdp-project)
- [Private Endpoint Documentation](https://learn.microsoft.com/en-us/azure/private-link/)
- [RBAC Documentation](https://learn.microsoft.com/en-us/azure/role-based-access-control/)
- [Network Security Best Practices](https://learn.microsoft.com/en-us/azure/security/fundamentals/network-best-practices)
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/managed-network.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/managed-network.bicep
deleted file mode 100644
index 1edeaf49b..000000000
--- a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/managed-network.bicep
+++ /dev/null
@@ -1,112 +0,0 @@
-@description('The name of the AI Services account')
-param accountName string
-
-@description('The isolation mode for the managed network')
-@allowed([
- 'AllowOnlyApprovedOutbound'
- 'AllowInternetOutbound'
-])
-param isolationMode string = 'AllowOnlyApprovedOutbound'
-
-@description('The name of the storage account to create outbound rules for')
-param storageName string
-
-@description('The resource group name where the storage account is located')
-param storageResourceGroupName string
-
-@description('The subscription ID where the storage account is located')
-param storageSubscriptionId string
-
-// @description('The name of the AI Search service to create outbound rules for')
-// param aiSearchName string
-
-// @description('The resource group name where the AI Search service is located')
-// param aiSearchResourceGroupName string
-
-// @description('The subscription ID where the AI Search service is located')
-// param aiSearchSubscriptionId string
-
-// @description('The name of the Cosmos DB account to create outbound rules for')
-// param cosmosDBName string
-
-// @description('The resource group name where the Cosmos DB account is located')
-// param cosmosDBResourceGroupName string
-
-// @description('The subscription ID where the Cosmos DB account is located')
-// param cosmosDBSubscriptionId string
-
-// Reference the existing AI Services account in the same resource group
-resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = {
- name: accountName
-}
-
-// Create the managed network settings first
-#disable-next-line BCP081
-resource managedNetwork 'Microsoft.CognitiveServices/accounts/managednetworks@2025-10-01-preview' = {
- parent: aiAccount
- name: 'default'
- properties: {
- managedNetwork: {
- IsolationMode: isolationMode
- managedNetworkKind: 'V2'
- provisionNetworkNow: true
- //firewallSku: 'Standard' // Uncomment to enable firewall only when in AllowOnlyApprovedOutbound mode
- }
- }
-}
-
-// Create outbound rule for Storage Account
-#disable-next-line BCP081
-resource storageOutboundRule 'Microsoft.CognitiveServices/accounts/managednetworks/outboundRules@2025-10-01-preview' = {
- parent: managedNetwork
- name: 'storage-outbound-rule'
- properties: {
- type: 'PrivateEndpoint'
- destination: {
- serviceResourceId: '/subscriptions/${storageSubscriptionId}/resourceGroups/${storageResourceGroupName}/providers/Microsoft.Storage/storageAccounts/${storageName}'
- subresourceTarget: 'blob'
- sparkEnabled: false
- sparkStatus: 'Inactive'
- }
- category: 'UserDefined'
- }
-}
-
-// // Create outbound rule for AI Search
-// #disable-next-line BCP081
-// resource aiSearchOutboundRule 'Microsoft.CognitiveServices/accounts/managednetworks/outboundRules@2025-10-01-preview' = {
-// parent: managedNetwork
-// name: 'aisearch-outbound-rule'
-// properties: {
-// type: 'PrivateEndpoint'
-// destination: {
-// serviceResourceId: '/subscriptions/${aiSearchSubscriptionId}/resourceGroups/${aiSearchResourceGroupName}/providers/Microsoft.Search/searchServices/${aiSearchName}'
-// subresourceTarget: 'searchService'
-// sparkEnabled: false
-// sparkStatus: 'Inactive'
-// }
-// category: 'UserDefined'
-// }
-// }
-
-// // Create outbound rule for Cosmos DB
-// #disable-next-line BCP081
-// resource cosmosDBOutboundRule 'Microsoft.CognitiveServices/accounts/managednetworks/outboundRules@2025-10-01-preview' = {
-// parent: managedNetwork
-// name: 'cosmosdb-outbound-rule'
-// properties: {
-// type: 'PrivateEndpoint'
-// destination: {
-// serviceResourceId: '/subscriptions/${cosmosDBSubscriptionId}/resourceGroups/${cosmosDBResourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${cosmosDBName}'
-// subresourceTarget: 'Sql'
-// sparkEnabled: false
-// sparkStatus: 'Inactive'
-// }
-// category: 'UserDefined'
-// }
-// }
-
-output managedNetworkSettingsName string = managedNetwork.name
-output storageOutboundRuleName string = storageOutboundRule.name
-// output aiSearchOutboundRuleName string = aiSearchOutboundRule.name
-// output cosmosDBOutboundRuleName string = cosmosDBOutboundRule.name
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/sample-mvnet/sample_mvnet.json b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/sample-mvnet/sample_mvnet.json
deleted file mode 100644
index cae0f8103..000000000
--- a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/sample-mvnet/sample_mvnet.json
+++ /dev/null
@@ -1,75 +0,0 @@
-/*****
-This is the sample to create a Cognitive Services account with a managed network using Azure Resource Manager (ARM) templates.
-There is no e2e secured set-up with this template. Please deploy the main.bicep for the full e2e managed network secured set-up.
-
-CLI command to run the sample:
-
-az deployment group create
---resource-group rg-name
---template-file "file_path"
---parameters accounts_foundry_mvnet="my-mvnet"
-
-********/
-
-{
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "parameters": {
- "accounts_foundry_mvnet": {
- "defaultValue": "my-mvnet",
- "type": "String"
- }
- },
- "variables": {},
- "resources": [
- {
- "type": "Microsoft.CognitiveServices/accounts",
- "apiVersion": "2025-10-01-preview",
- "name": "[parameters('accounts_foundry_mvnet')]",
- "location": "uaenorth",
- "sku": {
- "name": "S0"
- },
- "kind": "AIServices",
- "identity": {
- "type": "SystemAssigned"
- },
- "properties": {
- "apiProperties": {},
- "customSubDomainName": "[parameters('accounts_foundry_mvnet')]",
- "networkAcls": {
- "defaultAction": "Allow",
- "virtualNetworkRules": [],
- "ipRules": []
- },
- "allowProjectManagement": true,
- "defaultProject": "firstProject",
- "associatedProjects": [
- "firstProject"
- ],
- "networkInjections": [
- {
- "scenario": "agent",
- "subnetArmId": "",
- "useMicrosoftManagedNetwork": true
- }
- ],
- "publicNetworkAccess": "Enabled"
- }
- },
- {
- "type": "Microsoft.CognitiveServices/accounts/managednetworks",
- "apiVersion": "2025-10-01-preview",
- "name": "[concat(parameters('accounts_foundry_mvnet'),'/default')]",
- "dependsOn": [
- "[resourceId('Microsoft.CognitiveServices/accounts', parameters('accounts_foundry_mvnet'))]"
- ],
- "properties": {
- "managedNetwork": {
- "IsolationMode": "AllowOnlyApprovedOutbound",
- "managedNetworkKind": "V2"
- }
- }
- }
- ]
-}
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/update-outbound-rules-cli/batch-outbound-rules.json b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/update-outbound-rules-cli/batch-outbound-rules.json
deleted file mode 100644
index f3ad55419..000000000
--- a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/update-outbound-rules-cli/batch-outbound-rules.json
+++ /dev/null
@@ -1,41 +0,0 @@
-{
- "id": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{accountName}/managedNetworks/default",
- "name": "default",
- "type": "Microsoft.CognitiveServices/accounts/managedNetworks/outboundRules",
- "properties": {
- "IsolationMode": "AllowOnlyApprovedOutbound",
- "outboundRules": {
- "storage-rule": {
- "type": "PrivateEndpoint",
- "destination": {
- "serviceResourceId": "/subscriptions/{storageSubscriptionId}/resourceGroups/{storageResourceGroupName}/providers/Microsoft.Storage/storageAccounts/{storageName}",
- "subresourceTarget": "blob",
- "sparkEnabled": false,
- "sparkStatus": "Inactive"
- },
- "category": "UserDefined"
- },
- "aisearch-rule": {
- "type": "PrivateEndpoint",
- "destination": {
- "serviceResourceId": "/subscriptions/{aiSearchSubscriptionId}/resourceGroups/{aiSearchResourceGroupName}/providers/Microsoft.Search/searchServices/{aiSearchName}",
- "subresourceTarget": "searchService",
- "sparkEnabled": false,
- "sparkStatus": "Inactive"
- },
- "category": "UserDefined"
- },
- "cosmosdb-rule": {
- "type": "PrivateEndpoint",
- "destination": {
- "serviceResourceId": "/subscriptions/{cosmosDBSubscriptionId}/resourceGroups/{cosmosDBResourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/{cosmosDBName}",
- "subresourceTarget": "Sql",
- "sparkEnabled": false,
- "sparkStatus": "Inactive"
- },
- "category": "UserDefined"
- }
- },
- "managedNetworkKind": "V2"
- }
-}
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/update-outbound-rules-cli/outbound-rule-cdb.json b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/update-outbound-rules-cli/outbound-rule-cdb.json
deleted file mode 100644
index af6cd5653..000000000
--- a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/update-outbound-rules-cli/outbound-rule-cdb.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "id": "/subscriptions/{sub-id}/resourceGroups/{rg-name}/providers/Microsoft.CognitiveServices/accounts/{foundry-resource-name}/managednetworks/default/outboundRules/test-rule-cdb",
- "name": "test-rule-cdb",
- "type": "Microsoft.CognitiveServices/accounts/managednetworks/outboundRules",
- "properties": {
- "type": "PrivateEndpoint",
- "destination": {
- "serviceResourceId": "/subscriptions/{sub-id}/resourceGroups/{rg-name}/providers/Microsoft.DocumentDB/databaseAccounts/{cdb-name}",
- "subresourceTarget": "Sql"
- },
- "category": "UserDefined"
- }
-}
\ No newline at end of file
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/update-outbound-rules-cli/outbound-rule-search.json b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/update-outbound-rules-cli/outbound-rule-search.json
deleted file mode 100644
index 8ca296087..000000000
--- a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/update-outbound-rules-cli/outbound-rule-search.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "id": "/subscriptions/{sub-id}/resourceGroups/{rg-name}/providers/Microsoft.CognitiveServices/accounts/{foundry-account-name}/managednetworks/default/outboundRules/test-rule-search",
- "name": "test-rule-search",
- "type": "Microsoft.CognitiveServices/accounts/managednetworks/outboundRules",
- "properties": {
- "type": "PrivateEndpoint",
- "destination": {
- "serviceResourceId": "/subscriptions/{sub-id}/resourceGroups/{rg-name}/providers/Microsoft.Search/searchServices/{search-service-name}",
- "subresourceTarget": "searchService"
- },
- "category": "UserDefined"
- }
-}
\ No newline at end of file
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/update-outbound-rules-cli/outbound-rule-storage.json b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/update-outbound-rules-cli/outbound-rule-storage.json
deleted file mode 100644
index 264be546c..000000000
--- a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/update-outbound-rules-cli/outbound-rule-storage.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "id": "/subscriptions/{sub-id}/resourceGroups/{rg-name}/providers/Microsoft.CognitiveServices/accounts/{foundry-account-name}/managednetworks/default/outboundRules/test-rule-str",
- "name": "test-rule-str",
- "type": "Microsoft.CognitiveServices/accounts/managednetworks/outboundRules",
- "properties": {
- "type": "PrivateEndpoint",
- "destination": {
- "serviceResourceId": "/subscriptions/{sub-id}/resourceGroups/{rg-name}/providers/Microsoft.Storage/storageAccounts/{storage-account}",
- "subresourceTarget": "blob"
- },
- "category": "UserDefined"
- }
-}
\ No newline at end of file
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/README.md b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/README.md
similarity index 51%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/README.md
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/README.md
index 6f216c2ba..0afe3046b 100644
--- a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/README.md
+++ b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/README.md
@@ -1,5 +1,5 @@
---
-description: This set of templates demonstrates how to set up Azure AI Agent Service with managed virtual network isolation with private network links to connect the agent to your secure data.
+description: This set of templates demonstrates how to set up Microsoft Foundry Agent Service with managed virtual network isolation with private network links to connect the agent to your secure data.
page_type: sample
products:
- azure
@@ -10,31 +10,57 @@ languages:
- json
---
-# Azure AI Agent Service: Standard Agent Setup with Managed Virtual Network
+# Microsoft Foundry: Standard Agent Setup with Managed Virtual Network
+
+> **NEW**
+> For support on deploying the right network isolation template, check out the [GitHub Copilot for Azure skill for private networking](https://github.com/microsoft/GitHub-Copilot-for-Azure/blob/main/plugin/skills/microsoft-foundry/resource/private-network/private-network.md) set-up!
---
## Overview
-This infrastructure-as-code (IaC) solution deploys a network-secured Azure AI agent environment with private networking and role-based access control (RBAC).
+This infrastructure-as-code (IaC) solution deploys a network-secured agent environment with private networking and role-based access control (RBAC).
-Standard setup supports private network isolation through utilizing **Managed Virtual Network** approach. Managed Virtual Network support is a public preview feature and requires a feature flag to allowlst your subscription. Preview feature is titled "AI.ManagedVnetPreview" in the Azure Portal
+Standard setup supports private network isolation through utilizing **Managed Virtual Network** approach. With Managed Virtual Network, Microsoft manages the virtual network on your behalf, simplifying network configuration while maintaining enterprise-grade isolation.
+
+---
+
+## When to Use This Template
+
+Use this template when you need:
+- **Network isolation without managing your own VNet** β Microsoft manages the virtual network infrastructure for you
+- **Standard agent setup with BYO resources** β Customer-managed Storage, Cosmos DB, and AI Search for data residency and compliance
+- **Simplified networking** β No subnet planning, delegation, or NSG management required
+- **System Assigned Managed Identity** β Simplified identity management with platform-managed credentials
+
+### Template Decision Guide
+
+Use the table below to choose the right infrastructure template for your scenario:
+
+| Template | Agent Type | Networking | Identity | Key Use Case |
+|----------|-----------|------------|----------|-------------|
+| [**15**](../15-private-network-standard-agent-setup/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | E2E network isolation with full agent capabilities |
+| [**19**](../19-private-network-agent-tools/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | Same as 15 **plus** tools behind VNet (MCP, OpenAPI, Functions, A2A) |
+| [**17**](../17-private-network-standard-user-assigned-identity-agent-setup/) | Standard (BYO resources) | BYO VNet + Private Endpoints | **User Assigned MI** | Same as 15 but with user-managed identity |
+| [**16**](../16-private-network-standard-agent-apim-setup/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | Same as 15 **plus** private APIM integration |
+| [**18** (this template)](../18-managed-virtual-network/) | Standard (BYO resources) | **Managed VNet** (Microsoft-managed) | System Assigned MI | Network isolation without managing your own VNet |
+| [**15a**](../15a-private-network-evaluation-only-setup/) | Evaluation only | BYO VNet + Private Endpoints | System Assigned MI | Minimal setup for evaluation β no Cosmos DB, AI Search, or capability host |
+| [**11**](../11-private-network-basic-vnet/) | **Basic** (platform-managed) | BYO VNet injection | System Assigned MI | Basic agents with VNet isolation β no BYO resources needed |
+| [**41**](../41-standard-agent-setup/) | Standard (BYO resources) | **Public** (no VNet) | System Assigned MI | Standard agents without network isolation |
+| [**40**](../40-basic-agent-setup/) | **Basic** (platform-managed) | **Public** (no VNet) | System Assigned MI | Simplest setup β no BYO resources, no private networking |
---
## Deploy to Azure
-[](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fazure-ai-foundry%2Ffoundry-samples%2Frefs%2Fheads%2Fmain%2Finfrastructure%2Finfrastructure-setup-bicep%2F18-managed-virtual-network-preview%2Fazuredeploy.json)
+[](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fazure-ai-foundry%2Ffoundry-samples%2Frefs%2Fheads%2Fmain%2Finfrastructure%2Finfrastructure-setup-bicep%2F18-managed-virtual-network%2Fazuredeploy.json)
---
## Prerequisites
-1. **Register your subscription with the Preview Features in Azure Portal for Managed VNET**
- - Register your subscription for the preview feature "AI.ManagedVnetPreview" in the Azure Portal under "Preview Features". .
-
1. **Active Azure subscription with appropriate permissions**
- - **Azure AI Account Owner**: Needed to create a Foundry account and project
- - **Owner or Role Based Access Administrator**: Needed to assign RBAC to the required resources (Cosmos DB, Azure AI Search, Storage)
- - **Azure AI User**: Needed to create and edit agents
+ - **Foundry Account Owner**: Needed to create the Microsoft Foundry account and project.
+ - **Owner or Role Based Access Administrator**: Needed to assign RBAC on the Azure resources used by this template.
+ - **Foundry User**: Needed to create and use agents, projects, or evaluation workloads after deployment.
1. Azure CLI installed and configured on your local workstation or deployment pipeline server. Azure CLI support is required to run the 'az rest' commands to update your managed virtual network.
@@ -53,23 +79,40 @@ Standard setup supports private network isolation through utilizing **Managed Vi
1. Network administrator permissions (if operating in a restricted or enterprise environment)
-1. Sufficient quota for all resources in your target Azure region
- * If no parameters are passed in, this template creates an Azure AI Foundry resource, Foundry project, Azure Cosmos DB for NoSQL, Azure AI Search, and Azure Storage account
+1. Sufficient quota for all resources required by this template in the target Azure region, including model deployment quota.
+ * If no parameters are passed in, this template creates a Microsoft Foundry resource, Foundry project, Azure Cosmos DB for NoSQL, Azure AI Search, and Azure Storage account
---
## Pre-Deployment Steps
-### Limitations
-1. A managed network Foundry resource is only deployable via the `main.bicep` template in the folder `18-managed-virtual-network-preview` in foundry-samples.
-1. If you create FQDN outbound rules when the managed virtual network is in Allow Only Approved Outbound mode, a managed Azure Firewall is created which comes with associated Firewall costs. The FQDN outbound rules only support ports 80 and 443.
-1. Managed virtual network isolation cannot be disabled after enabling. There is no upgrade path from custom virtual network set-up to managed virtual network. A Foundry resource redeployment is required. Deleting your Foundry resource deletes the managed virtual network.
-1. Outbound rules from the managed network must be created through Azure CLI. For the end-to-end secured Agent service set-up with a managed virtual network, the template creates the managed private endpoint to the associated Storage account. Private endpoints are not created to CosmosDB or AI Search. Please use instructions in the `outbound rules CLI ` for information on how to create the managed private endpoints.
-1. Support for managed virtual network is only in the following regions: **East US, East US2, Japan East, France Central, UAE North, Brazil South, Spain Central, Germany West Central, Italy North, South Central US, West Central US, Australia East, Sweden Central, Canada East, South Africa North, West Europe, West US, West US 3, South India, and UK South.**
-1. If you require private access to on-premises resources for your Foundry resource, please use the to [Application Gatway](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/access-on-premises-resources?view=foundry-classic&viewFallbackFrom=foundry) to configure on-premises access. The same set-up with a private endpoint to Application Gateway and setting up backend pools is supported. Both L4 and L7 traffic are now supported with the Application Gateway in GA.
-1. Supports only Standard BYO resources Agents v1 and the Foundry classic experience. Basic Agents do not require network isolation. Support in the new Agents v2 and the new Foundry UI is coming soon.
-1. End-to-end network isolation for Agent MCP tools with managed virtual network is currently not supported. Please use public MCP tools with managed network isolation Foundry.
-1. For any feedback, please directly email meerakurup@microsoft.com
+### Limitations / Known Issues
+
+1. You can deploy a managed network Foundry resource in three ways.
+ 1. Bicep template in this folder
+ 2. Terraform template in this repository: [`infrastructure-setup-terraform/18-managed-virtual-network`](../../../infrastructure-setup-terraform/18-managed-virtual-network/)
+ 3. `az rest` and Azure CLI commands `az cognitiveservices` documented in the [`azure-cli/`](azure-cli/) folder in this directory
+1. There is no Azure Portal UI support to create the managed network yet. Support is coming soon.
+1. Once your Foundry resource is created, ensure you have assigned the Foundry resource's managed identity the built-in role of `Azure AI Enterprise Network Connection Approver` (role ID: `b556d68e-0be0-4f35-a333-ad7ee1ce17ea`) to ensure the required private endpoint to the Foundry resource is created and approved.
+1. You can't disable managed virtual network isolation after enabling it. There's no upgrade path from custom virtual network set-up to managed virtual network. A Foundry resource redeployment is required. Deleting your Foundry resource deletes the managed virtual network.
+1. Support for managed virtual network is only in the following regions: **East US, East US2, Japan East, France Central, UAE North, Brazil South, Spain Central, Germany West Central, Italy North, South Central US, Australia East, Sweden Central, Canada East, South Africa North, West US, West US 3, South India, and UK South.** Additional region support to follow soon.
+1. If you require private access to on-premises resources for your Foundry resource, use Application Gateway to configure on-premises access. The same set-up with a private endpoint to Application Gateway and setting up backend pools is supported. Both L4 and L7 traffic are now supported with the Application Gateway in GA.
+1. If you create FQDN outbound rules when the managed virtual network is in **Allow Only Approved Outbound** mode, a managed Azure Firewall is created which comes with associated Firewall costs. The FQDN outbound rules only support ports 80 and 443.
+1. You can't bring your own Azure Firewall to the managed virtual network. A managed firewall is automatically created for your Foundry account when you use **Allow Only Approved Outbound** mode.
+1. You can't reuse the same managed firewall for multiple Foundry accounts. Each Foundry account creates its own managed firewall when you use **Allow Only Approved Outbound** mode.
+1. To ensure your second created project inherits the networking settings of your first project and first Foundry resource, follow the steps in the [network secured Agent README](../15-private-network-standard-agent-setup/README.md) under __# (Optional) Adding Multiple Projects to AI Foundry Deployment__. This is required for new projects added to a Foundry resource seucred with managed network as well.
+
+### Account Deletion Prerequisites and Cleanup Guidance
+
+Before deleting an **Account** resource, it is essential to first delete the associated **Account Capability Host**. Failure to do so may result in residual dependenciesβsuch as subnets and other provisioned resourcesβremaining linked to the capability host.
+
+**Cleanup Options**
+
+**1. Full Account Removal**: To completely remove an account, you must delete and purge the account. Simply deleting the account is not sufficientβyou must purge so that deletion of the associated capability host is triggered. The service will automatically handle the removal of the capability host and any linked resources in the background. To purge the account, use the following [link](https://learn.microsoft.com/en-us/azure/ai-services/recover-purge-resources?tabs=azure-portal#purge-a-deleted-resource). Please allow approximately max of 20 minutes for all resources to be fully unlinked from the account.
+
+**2. Retain Account, Remove Capability Host**: If you intend to retain the account but remove the capability host, execute the script `deleteCaphost.sh` located in this folder. After deletion, allow approximately max of 20 minutes for all resources to be fully unlinked from the account. To recreate the capability host for the account, use the script `createCaphost.sh` located in the same folder.
+
+> **Important**: Before deleting the account capability host, ensure that the **project capability host** is deleted.
### Template Customization
@@ -83,6 +126,28 @@ Note: If not provided, the following resources will be created automatically for
#### Parameters
+| Parameter | Description | Default | Required |
+|-----------|-------------|---------|----------|
+| `location` | Azure region for deployment | `eastus` | Yes |
+| `aiServices` | Base name for the AI Services resource | `aiservices` | No |
+| `firstProjectName` | Name for the Foundry project | `project` | No |
+| `modelName` | Model to deploy | `gpt-4.1` | No |
+| `modelFormat` | Model provider | `OpenAI` | No |
+| `modelVersion` | Model version | `2025-04-14` | No |
+| `modelSkuName` | Model deployment SKU | `GlobalStandard` | No |
+| `modelCapacity` | Tokens per minute (TPM) capacity | `30` | No |
+| `vnetName` | Virtual Network name | `agent-vnet-test` | No |
+| `peSubnetName` | Subnet name for private endpoints | `pe-subnet` | No |
+| `peSubnetPrefix` | Address prefix for PE subnet | `192.168.1.0/24` | No |
+| `existingVnetResourceId` | Full ARM Resource ID of an existing VNet | `''` (creates new) | No |
+| `vnetAddressPrefix` | Address space for new VNet | `192.168.0.0/16` | No |
+| `aiSearchResourceId` | ARM Resource ID of existing AI Search | `''` (creates new) | No |
+| `azureStorageAccountResourceId` | ARM Resource ID of existing Storage account | `''` (creates new) | No |
+| `azureCosmosDBAccountResourceId` | ARM Resource ID of existing Cosmos DB | `''` (creates new) | No |
+| `existingDnsZones` | Map of DNS zone names to resource groups | All empty (creates new) | No |
+
+#### BYO Resource Details
+
1. **Use Existing Virtual Network and Subnets**
To use an existing VNet and subnet, set the existingVnetResourceId parameter to the full Azure Resource ID of the target VNet and its address range, and provide the names of the required subnet. If the existing VNet is associated with private DNS zones, set the existingDnsZones parameter to the resource group name in which the zones are located. For example:
@@ -158,15 +223,28 @@ Click the deploy to Azure button above to open the Azure portal and deploy the t
az deployment group create --resource-group --template-file main.bicep --parameters main.bicepparam
```
-> **Note:** To access your Foundry resource securely, use either a VM, VPN, or ExpressRoute.
+> **Note:** To access a private Foundry resource securely, use one of the following:
+> - A VM or jump box on the virtual network, optionally accessed through Azure Bastion
+> - Azure VPN Gateway
+> - Azure ExpressRoute
----
+### Cleanup
+
+To delete all resources created by this template:
+
+```bash
+az group delete --name --yes --no-wait
+```
-## Network Secured Agent Project Architecture Deep Dive
+> **Important**: Follow the [Account Deletion Prerequisites and Cleanup Guidance](#account-deletion-prerequisites-and-cleanup-guidance) to properly purge the account and wait for the capability host to fully unlink (~20 minutes).
+
+---
+
+## Managed Network Secured Agent Project Architecture Deep Dive
### Core Components
-**Azure AI Foundry** resource
+**Microsoft Foundry** resource
- Central orchestration point
- Manages service connections
- Set networking and policy configurations
@@ -183,11 +261,11 @@ Click the deploy to Azure button above to open the Azure portal and deploy the t
- BYO Search: All vector stores created by the agent leverage the customerβs Azure AI Search resource.
- BYO Thread Storage: All customer messages and conversation history will be stored in the customerβs own Azure Cosmos DB account.
-By bundling these BYO features (file storage, search, and thread storage), the standard setup guarantees that your deployment is secure by default. All data processed by Azure AI Foundry Agent Service is automatically stored at rest in your own Azure resources, helping you meet internal policies, compliance requirements, and enterprise security standards.
+By bundling these BYO features (file storage, search, and thread storage), the standard setup guarantees that your deployment is secure by default. All data processed by Microsoft Foundry Agent Service is automatically stored at rest in your own Azure resources, helping you meet internal policies, compliance requirements, and enterprise security standards.
### Azure Resources Created
-Azure AI Foundry (Cognitive Services)
+Microsoft Foundry (Cognitive Services)
- Type: Microsoft.CognitiveServices/accounts
- API version: 2025-04-01-preview
- Kind: AIServices
@@ -256,7 +334,7 @@ Network Security
**Private Endpoints**
Private endpoints ensure secure, internal-only connectivity. Private endpoints are created for the following:
-- Azure AI Foundry
+- Microsoft Foundry
- Azure AI Search
- Azure Storage
- Azure Cosmos DB
@@ -264,7 +342,7 @@ Private endpoints ensure secure, internal-only connectivity. Private endpoints a
**Private DNS Zones**
| Private Link Resource Type | Sub Resource | Private DNS Zone Name | Public DNS Zone Forwarders |
|----------------------------|--------------|------------------------|-----------------------------|
-| **Azure AI Foundry** | account | `privatelink.cognitiveservices.azure.com`
`privatelink.openai.azure.com`
`privatelink.services.ai.azure.com` | `cognitiveservices.azure.com`
`openai.azure.com`
`services.ai.azure.com` |
+| **Microsoft Foundry** | account | `privatelink.cognitiveservices.azure.com`
`privatelink.openai.azure.com`
`privatelink.services.ai.azure.com` | `cognitiveservices.azure.com`
`openai.azure.com`
`services.ai.azure.com` |
| **Azure AI Search** | searchService| `privatelink.search.windows.net` | `search.windows.net` |
| **Azure Cosmos DB** | Sql | `privatelink.documents.azure.com` | `documents.azure.com` |
| **Azure Storage** | blob | `privatelink.blob.core.windows.net` | `blob.core.windows.net` |
@@ -293,9 +371,13 @@ Private endpoints ensure secure, internal-only connectivity. Private endpoints a
- Storage Blob Data Owner
- **Cosmos DB for NoSQL**
- Cosmos DB Operator (`230815da-be43-4aae-9cb4-875f7bd000aa`)
- - Cosmos DB Built-in Data Contributor(`00000000-0000-0000-0000-000000000002`)
- - **Azure AI Foundry Resource**
- - Contributor (`b24988ac-6180-42a0-ab88-20f7382dd24c`) Required role for the Foundry account to accept all private endpoints created in the managed VNET. This role will be updated to the Azure AI Enterprise Network Connection Approver role which has a smaller scope.
+ - Cosmos DB Built-in Data Contributor (`00000000-0000-0000-0000-000000000002`)
+ - Three containers will automatically be provisioned during the create capability host process:
+ - Cosmos DB for NoSQL container: `<${projectWorkspaceId}>-thread-message-store`
+ - Cosmos DB for NoSQL container: `<${projectWorkspaceId}>-system-thread-message-store`
+ - Cosmos DB for NoSQL container: `<${projectWorkspaceId}>-agent-entity-store`
+ - **Microsoft Foundry Resource**
+ - Azure AI Enterprise Network Connection Approver (`b556d68e-0be0-4f35-a333-ad7ee1ce17ea`) Required role for the Foundry account to accept all private endpoints created in the managed VNET.
---
## Module Structure
@@ -303,7 +385,7 @@ Private endpoints ensure secure, internal-only connectivity. Private endpoints a
```text
modules-network-secured/
βββ add-project-capability-host.bicep # Configuring the project's capability host
-βββ ai-account-identity.bicep # Azure AI Foundry deployment and configuration
+βββ ai-account-identity.bicep # Microsoft Foundry deployment and configuration
βββ ai-project-identity.bicep # Foundry project deployment and connection configuration
βββ ai-search-role-assignments.bicep # AI Search RBAC configuration
βββ azure-storage-account-role-assignment.bicep # Storage Account RBAC configuration
@@ -319,8 +401,34 @@ modules-network-secured/
βββ subnet.bicep # Setting the subnet for Agent network injection
βββ validate-existing-resources.bicep # Validate existing CosmosDB, Storage, and Search to template deployment
βββ vnet.bicep # Deploying a new virtual network
+
+azure-cli/
+βββ azure-cli.md # End-to-end CLI walkthrough for deploying managed VNet via Azure CLI
+βββ outbound-rules-az-rest.md # az rest commands for creating outbound rules (Private Endpoint, FQDN, Service Tag)
```
+---
+
+## Deploy via Azure CLI
+
+As an alternative to the Bicep template, you can deploy a managed virtual network entirely using Azure CLI commands. The [`azure-cli/`](azure-cli/) folder contains step-by-step documentation for this approach.
+
+### Files
+
+| File | Description |
+|------|-------------|
+| [`azure-cli.md`](azure-cli/azure-cli.md) | Complete end-to-end walkthrough covering account creation with network injections, RBAC role assignment, managed network creation, outbound rule configuration, and deployment verification β all using `az cognitiveservices` CLI commands (requires Azure CLI 2.86.0+). |
+| [`outbound-rules-az-rest.md`](azure-cli/outbound-rules-az-rest.md) | `az rest` commands for creating individual and batch outbound rules (Private Endpoint rules for Storage, Cosmos DB, and AI Search). Use these when you need direct REST API access or are on an older CLI version. |
+
+### When to use the CLI approach
+
+- You want to add a managed network to an **existing** Foundry resource that was created without one
+- You prefer imperative CLI commands over declarative Bicep/Terraform templates
+- You need to manage outbound rules independently from the initial infrastructure deployment
+- You want to quickly test or prototype managed network configurations
+
+> **Note:** The `az cognitiveservices account managed-network` commands require **Azure CLI 2.86.0 or later**. The commands are currently in preview. For older CLI versions, use the `az rest` equivalents documented in [`outbound-rules-az-rest.md`](azure-cli/outbound-rules-az-rest.md).
+
## Maintenance
### Regular Tasks
@@ -341,9 +449,9 @@ modules-network-secured/
## References
-- [Azure AI Foundry Networking Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link?tabs=azure-portal&pivots=fdp-project)
-- [Azure AI Foundry RBAC Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/rbac-azure-ai-foundry?pivots=fdp-project)
+- [Configure managed virtual network for Microsoft Foundry](https://learn.microsoft.com/en-us/azure/foundry/how-to/managed-virtual-network)
+- [Microsoft Foundry Networking Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link?tabs=azure-portal&pivots=fdp-project)
+- [Microsoft Foundry RBAC Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/rbac-azure-ai-foundry?pivots=fdp-project)
- [Private Endpoint Documentation](https://learn.microsoft.com/en-us/azure/private-link/)
- [RBAC Documentation](https://learn.microsoft.com/en-us/azure/role-based-access-control/)
- [Network Security Best Practices](https://learn.microsoft.com/en-us/azure/security/fundamentals/network-best-practices)
-- Managed virtual network (public documentation coming soon)
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/azure-cli/azure-cli.md b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/azure-cli/azure-cli.md
new file mode 100644
index 000000000..ec026409d
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/azure-cli/azure-cli.md
@@ -0,0 +1,317 @@
+# Managed Network and Outbound Rule CLI Commands
+
+This document summarizes the new Azure CLI commands for configuring managed networks and outbound rules on Azure Cognitive Services accounts used with Microsoft Foundry.
+
+These commands help you configure network isolation and control outbound traffic from a managed network.
+
+## Command Groups
+
+```azurecli
+az cognitiveservices account managed-network
+az cognitiveservices account managed-network outbound-rule
+```
+
+## Managed Network Commands
+
+Use these commands to create, update, show, and provision the managed network for an Azure Cognitive Services account. You can run these commands after the Foundry resource was created.
+
+**Note:** Ensure you have assigned the Foundry account's managed identity the Azure AI Enterprise Network Connection Approver. This ensures the private endpoint to the Foundry resource from the managed VNET gets created. Command would be like this:
+
+`az role assignment create --assignee-object-id --assignee-principal-type ServicePrincipal --role "Azure AI Enterprise Network Connection Approver" --scope /subscriptions//resourceGroups/`
+
+| Command | Description |
+| --- | --- |
+| `az cognitiveservices account managed-network create` | Creates a managed network. Supports `allow_internet_outbound` or `allow_only_approved_outbound` isolation mode and an optional firewall SKU. |
+| `az cognitiveservices account managed-network update` | Updates managed network settings for an existing account. |
+| `az cognitiveservices account managed-network show` | Shows the current managed network settings for an account. |
+| `az cognitiveservices account managed-network provision-network` | Provisions the managed network after it is configured. |
+
+### Create a managed network with internet outbound access
+
+Use `allow_internet_outbound` when the managed network should allow outbound internet access.
+
+```azurecli
+az cognitiveservices account managed-network create \
+ --resource-group myResourceGroup \
+ --name myAccount \
+ --managed-network allow_internet_outbound
+```
+
+### Create a managed network with approved outbound only
+
+Use `allow_only_approved_outbound` when outbound traffic should be restricted to approved outbound rules. The optional `--firewall-sku` parameter configures the managed network firewall SKU.
+
+```azurecli
+az cognitiveservices account managed-network create \
+ --resource-group myResourceGroup \
+ --name myAccount \
+ --managed-network allow_only_approved_outbound \
+ --firewall-sku Standard
+```
+
+### Show managed network settings
+
+Use `show` to verify the current managed network configuration for an account.
+
+```azurecli
+az cognitiveservices account managed-network show \
+ --resource-group myResourceGroup \
+ --name myAccount
+```
+
+### Provision the managed network
+
+Use `provision-network` after configuring the managed network to apply and provision the network settings.
+
+```azurecli
+az cognitiveservices account managed-network provision-network \
+ --resource-group myResourceGroup \
+ --name myAccount
+```
+
+## Outbound Rule Commands
+
+Use outbound rules to define approved destinations when the managed network uses approved outbound access.
+
+| Command | Description |
+| --- | --- |
+| `az cognitiveservices account managed-network outbound-rule list` | Lists all outbound rules for the managed network. |
+| `az cognitiveservices account managed-network outbound-rule show` | Shows details for a specific outbound rule. |
+| `az cognitiveservices account managed-network outbound-rule remove` | Deletes an outbound rule from the managed network. |
+| `az cognitiveservices account managed-network outbound-rule set` | Creates or updates one outbound rule. Supports FQDN, private endpoint, and service tag rules. |
+| `az cognitiveservices account managed-network outbound-rule bulk-set` | Creates or updates multiple outbound rules from a YAML or JSON file. |
+
+## Outbound Rule Types
+
+The `set` command supports these outbound rule types:
+
+| Type | Description | Example destination |
+| --- | --- | --- |
+| `fqdn` | Allows outbound traffic to a fully qualified domain name. | `"*.openai.azure.com"` |
+| `privateendpoint` | Allows outbound traffic through a private endpoint rule. | Private endpoint configuration JSON |
+| `servicetag` | Allows outbound traffic to an Azure service tag, protocol, and port range. | `'{"serviceTag":"Storage","protocol":"TCP","portRanges":"443"}'` |
+
+### Create or update an FQDN outbound rule
+
+Use an FQDN rule to allow traffic to a domain name or wildcard domain.
+
+```azurecli
+az cognitiveservices account managed-network outbound-rule set \
+ --resource-group myResourceGroup \
+ --name myAccount \
+ --rule my-fqdn-rule \
+ --type fqdn \
+ --destination "*.openai.azure.com"
+```
+
+### Create or update a service tag outbound rule
+
+Use a service tag rule to allow traffic to an Azure service tag over a specific protocol and port range.
+
+```azurecli
+az cognitiveservices account managed-network outbound-rule set \
+ --resource-group myResourceGroup \
+ --name myAccount \
+ --rule my-servicetag-rule \
+ --type servicetag \
+ --destination '{"serviceTag":"Storage","protocol":"TCP","portRanges":"443"}'
+```
+
+### List outbound rules
+
+Use `list` to view all configured outbound rules for the managed network.
+
+```azurecli
+az cognitiveservices account managed-network outbound-rule list \
+ --resource-group myResourceGroup \
+ --name myAccount
+```
+
+### Show an outbound rule
+
+Use `show` to inspect one outbound rule by name.
+
+```azurecli
+az cognitiveservices account managed-network outbound-rule show \
+ --resource-group myResourceGroup \
+ --name myAccount \
+ --rule my-fqdn-rule
+```
+
+### Bulk create or update outbound rules
+
+Use `bulk-set` to create or update multiple outbound rules from a YAML or JSON file.
+
+```azurecli
+az cognitiveservices account managed-network outbound-rule bulk-set \
+ --resource-group myResourceGroup \
+ --name myAccount \
+ --file rules.yaml
+```
+
+### Remove an outbound rule
+
+Use `remove` to delete an outbound rule from the managed network.
+
+```azurecli
+az cognitiveservices account managed-network outbound-rule remove \
+ --resource-group myResourceGroup \
+ --name myAccount \
+ --rule my-fqdn-rule
+```
+
+## End-to-End Deployment: CLI Walkthrough
+
+This section walks through the full sequence of commands to deploy a Foundry resource with a managed virtual network using the CLI. Each step must be completed in order.
+
+> **Important:** The `az cognitiveservices account managed-network` CLI commands require a CLI extension. Please update to version 2.86.0. If the commands are not recognized, use the equivalent `az rest` commands shown below.
+
+### Prerequisites
+
+- Azure CLI installed and authenticated (`az login`)
+- A resource group in a [supported region](#supported-regions)
+
+### Step 1: Create the AI Services account with network injections
+
+The account must be created with `customSubDomainName`, `allowProjectManagement`, and `networkInjections` set **at creation time**. These properties cannot be added after the account is created.
+
+> **Important:** you must use `az rest` commands for account creation with network injections as the azure CLI does not yet support creating a Foundry resource with network injection
+
+```azurecli
+az rest --method PUT \
+ --url "https://management.azure.com/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts/?api-version=2025-10-01-preview" \
+ --body '{
+ "location": "",
+ "kind": "AIServices",
+ "sku": { "name": "S0" },
+ "identity": { "type": "SystemAssigned" },
+ "properties": {
+ "allowProjectManagement": true,
+ "customSubDomainName": "",
+ "networkInjections": [
+ {
+ "scenario": "agent",
+ "subnetArmId": "",
+ "useMicrosoftManagedNetwork": true
+ }
+ ],
+ "disableLocalAuth": false
+ }
+ }' \
+ --headers "Content-Type=application/json"
+```
+
+Wait for `provisioningState` to reach `Succeeded` before proceeding:
+
+```azurecli
+az rest --method GET \
+ --url "https://management.azure.com/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts/?api-version=2025-10-01-preview" \
+ --query "properties.provisioningState" -o tsv
+```
+
+### Step 2: Get the managed identity principal ID
+
+Retrieve the system-assigned managed identity principal ID from the account:
+
+```azurecli
+az cognitiveservices account show \
+ --resource-group \
+ --name \
+ --query identity.principalId -o tsv
+```
+
+### Step 3: Assign the Network Connection Approver role
+
+Assign the **Azure AI Enterprise Network Connection Approver** role to the account's managed identity. This allows managed network private endpoints to be auto-approved.
+
+```azurecli
+az role assignment create \
+ --assignee-object-id \
+ --assignee-principal-type ServicePrincipal \
+ --role "Azure AI Enterprise Network Connection Approver" \
+ --scope /subscriptions//resourceGroups/
+```
+
+> **Note:** If your target resources (Storage, Cosmos DB, AI Search) are in a different resource group, scope the role assignment to that resource group or to the subscription.
+
+### Step 4: Create the managed network
+
+Create the managed network child resource on the account. This establishes the network isolation mode and provisions the network infrastructure.
+
+Using the CLI (when available):
+
+```azurecli
+az cognitiveservices account managed-network create \
+ --resource-group \
+ --name \
+ --managed-network allow_only_approved_outbound \
+ --firewall-sku Standard
+```
+
+### Step 5: Add outbound rules
+
+After the managed network is active, add outbound rules for approved destinations. See the [Outbound Rule Commands](#outbound-rule-commands) section above for the full set of CLI commands.
+
+**FQDN rule example:**
+
+```azurecli
+az cognitiveservices account managed-network outbound-rule set \
+ --resource-group \
+ --name \
+ --rule my-fqdn-rule \
+ --type fqdn \
+ --destination "google.com"
+```
+
+**Service tag rule example:**
+
+```azurecli
+az cognitiveservices account managed-network outbound-rule set \
+ --resource-group \
+ --name \
+ --rule my-servicetag-rule \
+ --type servicetag \
+ --destination '{"serviceTag":"Storage","protocol":"TCP","portRanges":"443"}'
+```
+
+**Private endpoint rule example** (see [outbound-rules-az-rest.md](outbound-rules-az-rest.md) for `az rest` examples):
+
+```azurecli
+az cognitiveservices account managed-network outbound-rule set \
+ --resource-group \
+ --name \
+ --rule my-pe-rule \
+ --type privateendpoint \
+ --destination '{"serviceResourceId":"/subscriptions//resourceGroups//providers/Microsoft.Storage/storageAccounts/","subresourceTarget":"blob"}'
+```
+
+### Step 6: Verify the deployment
+
+Confirm the managed network is active:
+
+```azurecli
+az cognitiveservices account managed-network show \
+ --resource-group \
+ --name
+```
+
+List all outbound rules and their status:
+
+```azurecli
+az cognitiveservices account managed-network outbound-rule list \
+ --resource-group \
+ --name
+```
+
+Show a specific outbound rule:
+
+```azurecli
+az cognitiveservices account managed-network outbound-rule show \
+ --resource-group \
+ --name \
+ --rule my-fqdn-rule
+```
+
+### Supported regions
+
+The managed virtual network feature is available in the following regions: East US, East US 2, Japan East, France Central, UAE North, Brazil South, Spain Central, Germany West Central, Italy North, South Central US, Australia East, Sweden Central, Canada East, South Africa North, West US, West US 3, South India, UK South.
\ No newline at end of file
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/update-outbound-rules-cli/outbound-rule-cli.md b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/azure-cli/outbound-rules-az-rest.md
similarity index 71%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/update-outbound-rules-cli/outbound-rule-cli.md
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/azure-cli/outbound-rules-az-rest.md
index 2080ce654..e39fd44dc 100644
--- a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/update-outbound-rules-cli/outbound-rule-cli.md
+++ b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/azure-cli/outbound-rules-az-rest.md
@@ -1,10 +1,13 @@
-## Outbound Rules CLI
+## Outbound Rules az rest CLI
+
+Please follow this documentation to run `az rest` commands to create outbound rules. Please follow the other document in this folder for `az cognitiveservices` commands to create the managed network and create outbound rules.
### Outbound Rule for Storage account
Below is the CLI command to create an outbound rule from the managed VNET to your storage account. In the sample template we create the managed VNET PE for storage, but you will need two more for your CosmosDB resource and your Search resource.
+```bash
az rest --method PUT --url 'https://management.azure.com/subscriptions/{sub-id}/resourceGroups/{rg-name}/providers/Microsoft.CognitiveServices/accounts/{foundry-account}/managedNetworks/default/outboundRules/test-rule?api-version=2025-10-01-preview' \
--body '{
"id": "/subscriptions/{sub-id}/resourceGroups/{rg-name}/providers/Microsoft.CognitiveServices/accounts/{foundry-account}/managedNetworks/default/outboundRules/test-rule-str",
@@ -19,11 +22,13 @@ az rest --method PUT --url 'https://management.azure.com/subscriptions/{sub-id}/
"category": "UserDefined"
}
}'
+```
### Outbound Rule for CDB account
Below is the CLI command to create an outbound rule from the managed VNET to your CDB account.
+```bash
az rest --method PUT --url 'https://management.azure.com/subscriptions/{sub-id}/resourceGroups/{rg-name}/providers/Microsoft.CognitiveServices/accounts/{foundry-account}/managedNetworks/default/outboundRules/test-rule?api-version=2025-10-01-preview' \
--body '{
"id": "/subscriptions/{sub-id}/resourceGroups/{rg-name}/providers/Microsoft.CognitiveServices/accounts/{foundry-account}/managedNetworks/default/outboundRules/test-rule-cdb",
@@ -38,11 +43,13 @@ az rest --method PUT --url 'https://management.azure.com/subscriptions/{sub-id}/
"category": "UserDefined"
}
}'
+```
### Outbound Rule for Search account
Below is the CLI command to create an outbound rule from the managed VNET to your Search account.
+```bash
az rest --method PUT --url 'https://management.azure.com/subscriptions/{sub-id}/resourceGroups/{rg-name}/providers/Microsoft.CognitiveServices/accounts/{foundry-account}/managedNetworks/default/outboundRules/test-rule?api-version=2025-10-01-preview' \
--body '{
"id": "/subscriptions/{sub-id}/resourceGroups/{rg-name}/providers/Microsoft.CognitiveServices/accounts/{foundry-account}/managedNetworks/default/outboundRules/test-rule-search",
@@ -57,6 +64,7 @@ az rest --method PUT --url 'https://management.azure.com/subscriptions/{sub-id}/
"category": "UserDefined"
}
}'
+```
# Batch Outbound Rules CLI
@@ -101,3 +109,48 @@ az rest --method POST --uri $uri --body $body
- Run this after the main Bicep deployment completes
- The managed network must already exist before running this command
+### batch-outbound-rules.json
+
+```json
+{
+ "id": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/{accountName}/managedNetworks/default",
+ "name": "default",
+ "type": "Microsoft.CognitiveServices/accounts/managedNetworks/outboundRules",
+ "properties": {
+ "IsolationMode": "AllowOnlyApprovedOutbound",
+ "outboundRules": {
+ "storage-rule": {
+ "type": "PrivateEndpoint",
+ "destination": {
+ "serviceResourceId": "/subscriptions/{storageSubscriptionId}/resourceGroups/{storageResourceGroupName}/providers/Microsoft.Storage/storageAccounts/{storageName}",
+ "subresourceTarget": "blob",
+ "sparkEnabled": false,
+ "sparkStatus": "Inactive"
+ },
+ "category": "UserDefined"
+ },
+ "aisearch-rule": {
+ "type": "PrivateEndpoint",
+ "destination": {
+ "serviceResourceId": "/subscriptions/{aiSearchSubscriptionId}/resourceGroups/{aiSearchResourceGroupName}/providers/Microsoft.Search/searchServices/{aiSearchName}",
+ "subresourceTarget": "searchService",
+ "sparkEnabled": false,
+ "sparkStatus": "Inactive"
+ },
+ "category": "UserDefined"
+ },
+ "cosmosdb-rule": {
+ "type": "PrivateEndpoint",
+ "destination": {
+ "serviceResourceId": "/subscriptions/{cosmosDBSubscriptionId}/resourceGroups/{cosmosDBResourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/{cosmosDBName}",
+ "subresourceTarget": "Sql",
+ "sparkEnabled": false,
+ "sparkStatus": "Inactive"
+ },
+ "category": "UserDefined"
+ }
+ },
+ "managedNetworkKind": "V2"
+ }
+}
+```
\ No newline at end of file
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/azuredeploy.json b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/azuredeploy.json
similarity index 96%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/azuredeploy.json
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/azuredeploy.json
index 3904cced9..83f80f7ff 100644
--- a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/azuredeploy.json
+++ b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/azuredeploy.json
@@ -4,8 +4,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "15609984867232984221"
+ "version": "0.42.1.51946",
+ "templateHash": "10726822887583020348"
}
},
"parameters": {
@@ -250,8 +250,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "17445593936032020020"
+ "version": "0.42.1.51946",
+ "templateHash": "5439878820994143115"
}
},
"parameters": {
@@ -344,8 +344,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "81356089139070276"
+ "version": "0.42.1.51946",
+ "templateHash": "16620010525527418603"
}
},
"parameters": {
@@ -474,8 +474,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "1788095536881131913"
+ "version": "0.42.1.51946",
+ "templateHash": "11496589636440152813"
}
},
"parameters": {
@@ -547,8 +547,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "8025358656626191442"
+ "version": "0.42.1.51946",
+ "templateHash": "8762424559716292746"
}
},
"parameters": {
@@ -685,8 +685,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "8798681219838722072"
+ "version": "0.42.1.51946",
+ "templateHash": "9631735419288057649"
}
},
"parameters": {
@@ -732,9 +732,9 @@
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
- "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName')), 'b24988ac-6180-42a0-ab88-20f7382dd24c', resourceGroup().id)]",
+ "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName')), 'b556d68e-0be0-4f35-a333-ad7ee1ce17ea', resourceGroup().id)]",
"properties": {
- "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]",
+ "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b556d68e-0be0-4f35-a333-ad7ee1ce17ea')]",
"principalId": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName')), '2025-10-01-preview', 'full').identity.principalId]",
"principalType": "ServicePrincipal"
},
@@ -799,8 +799,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "15994578372975581196"
+ "version": "0.42.1.51946",
+ "templateHash": "15549652612714258938"
}
},
"parameters": {
@@ -978,8 +978,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "2754228344238136934"
+ "version": "0.42.1.51946",
+ "templateHash": "16516106179017566598"
}
},
"parameters": {
@@ -1208,15 +1208,6 @@
},
"isolationMode": {
"value": "[parameters('isolationMode')]"
- },
- "storageName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.azureStorageName.value]"
- },
- "storageResourceGroupName": {
- "value": "[variables('azureStorageResourceGroupName')]"
- },
- "storageSubscriptionId": {
- "value": "[variables('azureStorageSubscriptionId')]"
}
},
"template": {
@@ -1225,8 +1216,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "2247331089577562037"
+ "version": "0.42.1.51946",
+ "templateHash": "9263659451530831183"
}
},
"parameters": {
@@ -1246,24 +1237,6 @@
"metadata": {
"description": "The isolation mode for the managed network"
}
- },
- "storageName": {
- "type": "string",
- "metadata": {
- "description": "The name of the storage account to create outbound rules for"
- }
- },
- "storageResourceGroupName": {
- "type": "string",
- "metadata": {
- "description": "The resource group name where the storage account is located"
- }
- },
- "storageSubscriptionId": {
- "type": "string",
- "metadata": {
- "description": "The subscription ID where the storage account is located"
- }
}
},
"resources": [
@@ -1278,34 +1251,12 @@
"provisionNetworkNow": true
}
}
- },
- {
- "type": "Microsoft.CognitiveServices/accounts/managedNetworks/outboundRules",
- "apiVersion": "2025-10-01-preview",
- "name": "[format('{0}/{1}/{2}', parameters('accountName'), 'default', 'storage-outbound-rule')]",
- "properties": {
- "type": "PrivateEndpoint",
- "destination": {
- "serviceResourceId": "[format('/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Storage/storageAccounts/{2}', parameters('storageSubscriptionId'), parameters('storageResourceGroupName'), parameters('storageName'))]",
- "subresourceTarget": "blob",
- "sparkEnabled": false,
- "sparkStatus": "Inactive"
- },
- "category": "UserDefined"
- },
- "dependsOn": [
- "[resourceId('Microsoft.CognitiveServices/accounts/managedNetworks', parameters('accountName'), 'default')]"
- ]
}
],
"outputs": {
"managedNetworkSettingsName": {
"type": "string",
"value": "default"
- },
- "storageOutboundRuleName": {
- "type": "string",
- "value": "storage-outbound-rule"
}
}
}
@@ -1389,8 +1340,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "16093594075579008850"
+ "version": "0.42.1.51946",
+ "templateHash": "7099263758639578851"
}
},
"parameters": {
@@ -2012,8 +1963,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "5095087340309076800"
+ "version": "0.42.1.51946",
+ "templateHash": "18174839353195527759"
}
},
"parameters": {
@@ -2094,6 +2045,7 @@
}
},
"dependsOn": [
+ "[resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('accountName'), parameters('projectName'))]",
"[resourceId('Microsoft.CognitiveServices/accounts/projects/connections', parameters('accountName'), parameters('projectName'), parameters('cosmosDBName'))]"
]
},
@@ -2112,6 +2064,7 @@
}
},
"dependsOn": [
+ "[resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('accountName'), parameters('projectName'))]",
"[resourceId('Microsoft.CognitiveServices/accounts/projects/connections', parameters('accountName'), parameters('projectName'), parameters('azureStorageName'))]"
]
},
@@ -2187,8 +2140,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "6910483561575524105"
+ "version": "0.42.1.51946",
+ "templateHash": "5729541472937730744"
}
},
"parameters": {
@@ -2242,8 +2195,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "14683840003859985069"
+ "version": "0.42.1.51946",
+ "templateHash": "6805612394357697185"
}
},
"parameters": {
@@ -2258,7 +2211,7 @@
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
- "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('azureStorageName'))]",
+ "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('azureStorageName'))]",
"name": "[guid(parameters('projectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'), resourceId('Microsoft.Storage/storageAccounts', parameters('azureStorageName')))]",
"properties": {
"principalId": "[parameters('projectPrincipalId')]",
@@ -2300,8 +2253,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "2161753938341361575"
+ "version": "0.42.1.51946",
+ "templateHash": "3731695714569887273"
}
},
"parameters": {
@@ -2322,7 +2275,7 @@
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
- "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', parameters('cosmosDBName'))]",
+ "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName'))]",
"name": "[guid(parameters('projectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', '230815da-be43-4aae-9cb4-875f7bd000aa'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName')))]",
"properties": {
"principalId": "[parameters('projectPrincipalId')]",
@@ -2364,8 +2317,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "7968115481508840"
+ "version": "0.42.1.51946",
+ "templateHash": "167668741936896543"
}
},
"parameters": {
@@ -2386,7 +2339,7 @@
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
- "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('aiSearchName'))]",
+ "scope": "[resourceId('Microsoft.Search/searchServices', parameters('aiSearchName'))]",
"name": "[guid(parameters('projectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7'), resourceId('Microsoft.Search/searchServices', parameters('aiSearchName')))]",
"properties": {
"principalId": "[parameters('projectPrincipalId')]",
@@ -2397,7 +2350,7 @@
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
- "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('aiSearchName'))]",
+ "scope": "[resourceId('Microsoft.Search/searchServices', parameters('aiSearchName'))]",
"name": "[guid(parameters('projectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0'), resourceId('Microsoft.Search/searchServices', parameters('aiSearchName')))]",
"properties": {
"principalId": "[parameters('projectPrincipalId')]",
@@ -2449,8 +2402,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "17458377866351620215"
+ "version": "0.42.1.51946",
+ "templateHash": "10627022964818938294"
}
},
"parameters": {
@@ -2543,8 +2496,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "13874725855824693255"
+ "version": "0.42.1.51946",
+ "templateHash": "14861833638743424490"
}
},
"parameters": {
@@ -2574,7 +2527,7 @@
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
- "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('storageName'))]",
+ "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageName'))]",
"name": "[guid(resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'), resourceId('Microsoft.Storage/storageAccounts', parameters('storageName')))]",
"properties": {
"principalId": "[parameters('aiProjectPrincipalId')]",
@@ -2622,8 +2575,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "16291470712974205281"
+ "version": "0.42.1.51946",
+ "templateHash": "9661397591572983652"
}
},
"parameters": {
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/azuredeploy.parameters.json b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/azuredeploy.parameters.json
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/azuredeploy.parameters.json
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/azuredeploy.parameters.json
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/main.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/main.bicep
similarity index 97%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/main.bicep
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/main.bicep
index 80b941e7c..5386c9ba9 100644
--- a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/main.bicep
+++ b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/main.bicep
@@ -245,15 +245,6 @@ module managedNetwork 'modules-network-secured/managed-network.bicep' = {
params: {
accountName: aiAccount.outputs.accountName
isolationMode: isolationMode
- storageName: aiDependencies.outputs.azureStorageName
- storageResourceGroupName: azureStorageResourceGroupName
- storageSubscriptionId: azureStorageSubscriptionId
- // aiSearchName: aiDependencies.outputs.aiSearchName
- // aiSearchResourceGroupName: aiSearchServiceResourceGroupName
- // aiSearchSubscriptionId: aiSearchServiceSubscriptionId
- // cosmosDBName: aiDependencies.outputs.cosmosDBName
- // cosmosDBResourceGroupName: cosmosDBResourceGroupName
- // cosmosDBSubscriptionId: cosmosDBSubscriptionId
}
dependsOn: [
aiSearch // Ensure AI Search exists
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/main.bicepparam b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/main.bicepparam
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/main.bicepparam
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/main.bicepparam
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/main.parameters.json b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/main.parameters.json
new file mode 100644
index 000000000..54369ac21
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/main.parameters.json
@@ -0,0 +1,76 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
+ "contentVersion": "1.0.0.0",
+ "parameters": {
+ "location": {
+ "value": "eastus2"
+ },
+ "isolationMode": {
+ "value": "AllowOnlyApprovedOutbound"
+ },
+ "aiServices": {
+ "value": "aiservices"
+ },
+ "deploymentTimestamp": {
+ "value": "20260501222500"
+ },
+ "firstProjectName": {
+ "value": "project"
+ },
+ "projectDescription": {
+ "value": "A project for the AI Foundry account with managed network secured deployed Agent"
+ },
+ "displayName": {
+ "value": "project"
+ },
+ "peSubnetName": {
+ "value": "pe-subnet"
+ },
+ "existingVnetResourceId": {
+ "value": "/subscriptions/4c9044a9-7faa-4233-9745-94436400ee92/resourceGroups/mvnet-5-1-eus2/providers/Microsoft.Network/virtualNetworks/my-mvnet-5-1"
+ },
+ "vnetName": {
+ "value": ""
+ },
+ "aiSearchResourceId": {
+ "value": "/subscriptions/4c9044a9-7faa-4233-9745-94436400ee92/resourceGroups/rg-mvnet-eus2-4-1/providers/Microsoft.Search/searchServices/foundryzc3jsearch"
+ },
+ "azureStorageAccountResourceId": {
+ "value": "/subscriptions/4c9044a9-7faa-4233-9745-94436400ee92/resourceGroups/mvnet-5-1-eus2/providers/Microsoft.Storage/storageAccounts/aiservicesrw5rstorage"
+ },
+ "azureCosmosDBAccountResourceId": {
+ "value": "/subscriptions/4c9044a9-7faa-4233-9745-94436400ee92/resourceGroups/mvnet-5-1-eus2/providers/Microsoft.DocumentDB/databaseAccounts/aiservicesrw5rcosmosdb"
+ },
+ "apiManagementResourceId": {
+ "value": ""
+ },
+ "existingDnsZones": {
+ "value": {
+ "privatelink.services.ai.azure.com": "",
+ "privatelink.openai.azure.com": "",
+ "privatelink.cognitiveservices.azure.com": "",
+ "privatelink.search.windows.net": "",
+ "privatelink.blob.core.windows.net": "",
+ "privatelink.documents.azure.com": "",
+ "privatelink.azure-api.net": ""
+ }
+ },
+ "dnsZoneNames": {
+ "value": [
+ "privatelink.services.ai.azure.com",
+ "privatelink.openai.azure.com",
+ "privatelink.cognitiveservices.azure.com",
+ "privatelink.search.windows.net",
+ "privatelink.blob.core.windows.net",
+ "privatelink.documents.azure.com",
+ "privatelink.azure-api.net"
+ ]
+ },
+ "vnetAddressPrefix": {
+ "value": ""
+ },
+ "peSubnetPrefix": {
+ "value": ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/metadata.json b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/metadata.json
similarity index 62%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/metadata.json
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/metadata.json
index e51f264f4..8e04d75fe 100644
--- a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/metadata.json
+++ b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/metadata.json
@@ -2,8 +2,8 @@
"$schema": "https://aka.ms/azure-quickstart-templates-metadata-schema#",
"type": "QuickStart",
"itemDisplayName": "Managed Network Secured Standard Agent Setup",
- "description": "This set of templates demonstrates how to set up Azure AI Agent Service with the managed network secured standard setup environment.",
- "summary": "This set of templates demonstrates how to use Azure AI Agent Service with a managed virtual network.",
+ "description": "This set of templates demonstrates how to set up Foundry with a managed network secured standard setup environment.",
+ "summary": "This set of templates demonstrates how to use Foundry with a managed virtual network.",
"githubUsername": "meerakurup",
"dateUpdated": "2025-12-17",
"environments": [
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/add-project-capability-host.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/add-project-capability-host.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/add-project-capability-host.bicep
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/add-project-capability-host.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/ai-account-identity.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/ai-account-identity.bicep
similarity index 89%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/ai-account-identity.bicep
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/ai-account-identity.bicep
index 43330a81e..b940a50b2 100644
--- a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/ai-account-identity.bicep
+++ b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/ai-account-identity.bicep
@@ -39,12 +39,12 @@ resource account 'Microsoft.CognitiveServices/accounts@2025-10-01-preview' = {
}
}
-// Role assignment for the AI Services account managed identity, Azure AI Network connection approver role / Contributor (Change back when available)
+// Role assignment for the AI Services account managed identity, Azure AI Network connection approver role
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- name: guid(account.id, 'b24988ac-6180-42a0-ab88-20f7382dd24c', resourceGroup().id)
+ name: guid(account.id, 'b556d68e-0be0-4f35-a333-ad7ee1ce17ea', resourceGroup().id)
scope: resourceGroup()
properties: {
- roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b556d68e-0be0-4f35-a333-ad7ee1ce17ea')
principalId: account.identity.principalId
principalType: 'ServicePrincipal'
}
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/ai-project-identity.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/ai-project-identity.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/ai-project-identity.bicep
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/ai-project-identity.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/ai-search-role-assignments.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/ai-search-role-assignments.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/ai-search-role-assignments.bicep
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/ai-search-role-assignments.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/azure-storage-account-role-assignment.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/azure-storage-account-role-assignment.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/azure-storage-account-role-assignment.bicep
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/azure-storage-account-role-assignment.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/blob-storage-container-role-assignments.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/blob-storage-container-role-assignments.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/blob-storage-container-role-assignments.bicep
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/blob-storage-container-role-assignments.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/cosmos-container-role-assignments.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/cosmos-container-role-assignments.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/cosmos-container-role-assignments.bicep
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/cosmos-container-role-assignments.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/cosmosdb-account-role-assignment.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/cosmosdb-account-role-assignment.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/cosmosdb-account-role-assignment.bicep
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/cosmosdb-account-role-assignment.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/existing-vnet.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/existing-vnet.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/existing-vnet.bicep
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/existing-vnet.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/format-project-workspace-id.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/format-project-workspace-id.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/format-project-workspace-id.bicep
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/format-project-workspace-id.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/managed-network.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/managed-network.bicep
new file mode 100644
index 000000000..285948878
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/managed-network.bicep
@@ -0,0 +1,31 @@
+@description('The name of the AI Services account')
+param accountName string
+
+@description('The isolation mode for the managed network')
+@allowed([
+ 'AllowOnlyApprovedOutbound'
+ 'AllowInternetOutbound'
+])
+param isolationMode string = 'AllowOnlyApprovedOutbound'
+
+// Reference the existing AI Services account in the same resource group
+resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = {
+ name: accountName
+}
+
+// Create the managed network settings first
+#disable-next-line BCP081
+resource managedNetwork 'Microsoft.CognitiveServices/accounts/managednetworks@2025-10-01-preview' = {
+ parent: aiAccount
+ name: 'default'
+ properties: {
+ managedNetwork: {
+ IsolationMode: isolationMode
+ managedNetworkKind: 'V2'
+ provisionNetworkNow: true
+ //firewallSku: 'Standard' // Uncomment to enable firewall only when in AllowOnlyApprovedOutbound mode
+ }
+ }
+}
+
+output managedNetworkSettingsName string = managedNetwork.name
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/network-agent-vnet.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/network-agent-vnet.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/network-agent-vnet.bicep
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/network-agent-vnet.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/private-endpoint-and-dns.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/private-endpoint-and-dns.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/private-endpoint-and-dns.bicep
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/private-endpoint-and-dns.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/standard-dependent-resources.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/standard-dependent-resources.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/standard-dependent-resources.bicep
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/standard-dependent-resources.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/subnet.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/subnet.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/subnet.bicep
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/subnet.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/validate-existing-resources.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/validate-existing-resources.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/validate-existing-resources.bicep
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/validate-existing-resources.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/vnet.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/vnet.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/18-managed-virtual-network-preview/modules-network-secured/vnet.bicep
rename to infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/vnet.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/README.md b/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/README.md
deleted file mode 100644
index f67457cc9..000000000
--- a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/README.md
+++ /dev/null
@@ -1,222 +0,0 @@
-# Hybrid Private Resources Agent Setup
-
-This template deploys an Azure AI Foundry account with backend resources (AI Search, Cosmos DB, Storage) on **private endpoints**. By default, the Foundry resource itself also has **public network access disabled**, but this can be switched to public access if needed (see [Switching Between Private and Public Access](#switching-between-private-and-public-access)).
-
-## Architecture (Default β Private Foundry)
-
-```
-βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-β Secure Access (VPN Gateway / ExpressRoute / Azure Bastion) β
-ββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
- β
- ββββββββββββββββΌβββββββββββββββ
- β AI Services Account β
- β (publicNetworkAccess: β
- β DISABLED) β βββ Private by default
- β β
- β ββββββββββββββββββββββββββ β
- β β Data Proxy / Agent β β
- β β ToolServer β β
- β βββββββββββββ¬βββββββββββββ β
- ββββββββββββββββΌβββββββββββββββ
- β networkInjections
- ββββββββββββββββΌβββββββββββββββ
- β Private VNet β
- β β
- β βββββββββββ βββββββββββ β
- β βAI Searchβ βCosmos DBβ β βββ Private endpoints
- β βββββββββββ βββββββββββ β (no public access)
- β β
- β βββββββββββ βββββββββββ β
- β β Storage β β MCP β β
- β βββββββββββ β Servers β β
- β βββββββββββ β
- βββββββββββββββββββββββββββββββ
-```
-
-## Key Features
-
-| Feature | This Template (19) β Private (default) | This Template (19) β Public | Fully Private (15) |
-|---------|----------------------------------------|-----------------------------|-----------------------|
-| AI Services public access | β Disabled | β
Enabled | β Disabled |
-| Portal access | Via VPN/ExpressRoute/Bastion | β
Works directly | Via VPN/ExpressRoute/Bastion |
-| Backend resources | π Private | π Private | π Private |
-| Data Proxy | β
Configured | β
Configured | β
Configured |
-| Secure connection required | β
Yes | β No | β
Yes |
-
-## Switching Between Private and Public Access
-
-The Foundry resource has **public network access disabled by default**. You can switch between the two modes by modifying the Bicep template.
-
-### To enable public access
-
-In [modules-network-secured/ai-account-identity.bicep](modules-network-secured/ai-account-identity.bicep), change:
-
-```bicep
-// Change from:
-publicNetworkAccess: 'Disabled'
-// To:
-publicNetworkAccess: 'Enabled'
-
-// Also change:
-defaultAction: 'Deny'
-// To:
-defaultAction: 'Allow'
-```
-
-This makes the Foundry resource accessible from the internet (e.g., for portal-based development without VPN).
-
-### To disable public access (default)
-
-Revert the changes above, setting `publicNetworkAccess: 'Disabled'` and `defaultAction: 'Deny'`.
-
-## Connecting to a Private Foundry Resource
-
-When public network access is disabled (the default), you need a secure connection to reach the Foundry resource. Azure provides three methods:
-
-1. **Azure VPN Gateway** β Connect from your local network to the Azure VNet over an encrypted tunnel.
-2. **Azure ExpressRoute** β Use a private, dedicated connection from your on-premises infrastructure to Azure.
-3. **Azure Bastion** β Use a jump box VM on the VNet, accessed securely through the Azure portal.
-
-For detailed setup instructions, see: [Securely connect to Azure AI Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link?view=foundry#securely-connect-to-foundry).
-
-## When to Use This Template
-
-Use this template when you want:
-- **Private backend resources** β Keep AI Search, Cosmos DB, and Storage behind private endpoints
-- **MCP server integration** β Deploy MCP servers on the VNet that agents can access via Data Proxy
-- **OpenAPI tool integration** β Deploy OpenAPI-spec HTTP services on the VNet for agent tool access
-- **A2A (Agent-to-Agent)** β Connect agents to remote agents behind the VNet via the A2A protocol
-- **Azure Functions** β Deploy an Azure Function behind a VNET for agent tool access.
-- **Private Foundry (default)** β Full network isolation with secure access via VPN/ExpressRoute/Bastion
-- **Optional public Foundry access** β Switch to public for portal-based development if allowed by your security policy
-
-## When NOT to Use This Template
-
-Use [template 15](../15-private-network-standard-agent-setup/) instead when you need:
-- **Fully managed private networking** β Including managed VNet with Microsoft-managed private endpoints
-- **Compliance requirements** β Regulations that require a different private networking topology
-
-## Deployment
-
-### Prerequisites
-
-1. Azure CLI installed and authenticated
-2. Owner or Contributor role on the subscription
-3. Sufficient quota for model deployment (gpt-4o-mini)
-
-### Deploy
-
-```bash
-# Create resource group
-az group create --name "rg-hybrid-agent-test" --location "westus2"
-
-# Deploy the template
-az deployment group create \
- --resource-group "rg-hybrid-agent-test" \
- --template-file main.bicep \
- --parameters location="westus2"
-```
-
-### Verify Deployment
-
-```bash
-# Check deployment status
-az deployment group show \
- --resource-group "rg-hybrid-agent-test" \
- --name "main" \
- --query "properties.provisioningState"
-
-# List private endpoints (should see AI Search, Storage, Cosmos DB)
-az network private-endpoint list \
- --resource-group "rg-hybrid-agent-test" \
- --output table
-```
-
-## Testing Agents with Private Resources
-
-### Option 1: Portal Testing
-
-If the Foundry resource has **public network access enabled**, you can test directly in the portal:
-
-1. Navigate to [Azure AI Foundry portal](https://ai.azure.com)
-2. Select your project
-3. Create an agent with AI Search tool
-4. Test that the agent can query the private AI Search index
-
-If the Foundry resource has **public network access disabled** (default), you need to connect via VPN Gateway, ExpressRoute, or Azure Bastion before accessing the portal. See [Connecting to a Private Foundry Resource](#connecting-to-a-private-foundry-resource).
-
-### Option 2: SDK Testing
-
-See [tests/TESTING-GUIDE.md](tests/TESTING-GUIDE.md) for detailed SDK testing instructions.
-
-## MCP Server Deployment
-
-To deploy MCP servers on the private VNet:
-
-```bash
-# Create Container Apps environment on mcp-subnet
-az containerapp env create \
- --resource-group "rg-hybrid-agent-test" \
- --name "mcp-env" \
- --location "westus2" \
- --infrastructure-subnet-resource-id "" \
- --internal-only true
-
-# Deploy MCP server
-az containerapp create \
- --resource-group "rg-hybrid-agent-test" \
- --name "my-mcp-server" \
- --environment "mcp-env" \
- --image "" \
- --target-port 8080 \
- --ingress external \
- --min-replicas 1
-```
-
-Then configure private DNS zone for Container Apps (see TESTING-GUIDE.md Step 6.3).
-
-## Parameters
-
-> **β οΈ Important: Cosmos DB Connection Requirements**
->
-> If you are creating the Cosmos DB connection manually (e.g., via REST API or ARM), ensure the following:
-> - The `authType` **must** be set to `AAD`. This is the only supported authentication type for the Cosmos DB connection used by the Agent Service.
-> - The `metadata` section **must** include the `ResourceId` property, set to the full Azure Resource ID of your Cosmos DB account. The Agent Service relies on this property to correctly identify and connect to your Cosmos DB resource. Omitting `ResourceId` from the metadata will cause the connection to fail.
->
-> Example connection properties:
-> ```json
-> {
-> "category": "CosmosDB",
-> "authType": "AAD",
-> "metadata": {
-> "ApiType": "Azure",
-> "ResourceId": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/{cosmosDbAccountName}",
-> "location": "{region}"
-> }
-> }
-> ```
-
-| Parameter | Description | Default |
-|-----------|-------------|---------|
-| `location` | Azure region | `eastus2` |
-| `aiServices` | Base name for AI Services | `aiservices` |
-| `modelName` | Model to deploy | `gpt-4o-mini` |
-| `modelCapacity` | TPM capacity | `30` |
-| `vnetName` | VNet name | `agent-vnet-test` |
-| `agentSubnetName` | Subnet for AI Foundry (reserved) | `agent-subnet` |
-| `peSubnetName` | Subnet for private endpoints | `pe-subnet` |
-| `mcpSubnetName` | Subnet for MCP servers | `mcp-subnet` |
-
-## Cleanup
-
-```bash
-# Delete all resources
-az group delete --name "rg-hybrid-agent-test" --yes --no-wait
-```
-
-## Related Templates
-
-- [15-private-network-standard-agent-setup](../15-private-network-standard-agent-setup/) - Fully private setup (no public access)
-- [40-basic-agent-setup](../40-basic-agent-setup/) - Basic agent setup without private networking
-- [41-standard-agent-setup](../41-standard-agent-setup/) - Standard agent setup without private networking
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/main.bicepparam b/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/main.bicepparam
deleted file mode 100644
index a5125398b..000000000
--- a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/main.bicepparam
+++ /dev/null
@@ -1,66 +0,0 @@
-using './main.bicep'
-
-param location = 'norwayeast'
-param aiServices = 'djetchev'
-param modelName = 'gpt-4o-mini'
-param modelFormat = 'OpenAI'
-param modelVersion = '2024-07-18'
-param modelSkuName = 'GlobalStandard'
-param modelCapacity = 1
-param firstProjectName = 'project'
-param projectDescription = 'A project for the AI Foundry account with network secured deployed Agent'
-param displayName = 'project'
-param peSubnetName = 'pe-subnet'
-
-// Resource IDs for existing resources
-// If you provide these, the deployment will use the existing resources instead of creating new ones
-param existingVnetResourceId = ''
-param vnetName = 'agent-vnet-test'
-param agentSubnetName = 'agent-subnet'
-param aiSearchResourceId = ''
-param azureStorageAccountResourceId = ''
-param azureCosmosDBAccountResourceId = ''
-// Pass the DNS zone map here
-// Leave empty to create new DNS zone, add the resource group of existing DNS zone to use it
-param existingDnsZones = {
- 'privatelink.services.ai.azure.com': ''
- 'privatelink.openai.azure.com': ''
- 'privatelink.cognitiveservices.azure.com': ''
- 'privatelink.search.windows.net': ''
- 'privatelink.blob.core.windows.net': ''
- 'privatelink.documents.azure.com': ''
-}
-
-//DNSZones names for validating if they exist
-param dnsZoneNames = [
- 'privatelink.services.ai.azure.com'
- 'privatelink.openai.azure.com'
- 'privatelink.cognitiveservices.azure.com'
- 'privatelink.search.windows.net'
- 'privatelink.blob.core.windows.net'
- 'privatelink.documents.azure.com'
-]
-
-// Network configuration (behavior depends on `existingVnetResourceId`)
-//
-// - NEW VNet (existingVnetResourceId is empty):
-// The values below are used to CREATE the VNet and the two subnets.
-// Provide explicit, non-overlapping CIDR ranges when creating a new VNet.
-//
-// - EXISTING VNet (existingVnetResourceId is provided):
-// The module will reference the existing VNet. Subnet handling depends on the
-// values you provide:
-// * If `agentSubnetPrefix` or `peSubnetPrefix` are empty, the module may
-// auto-derive subnet CIDRs from the existing VNet's address space
-// (using cidrSubnet). This can produce /24 (or configured) subnets
-// starting at index 0, 1, etc.
-// * If you provide explicit subnet prefixes, the module will attempt to
-// create or update subnets with those prefixes in the existing VNet.
-//
-// Important operational notes and risks (when existingVnetResourceId is provided):
-// - Avoid CIDR overlaps with any existing subnets in the target VNet. Overlap
-// leads to `NetcfgSubnetRangesOverlap` and failed deployments.
-// - For highest safety when using an existing VNet, supply the existing `agentSubnetPrefix` and `peSubnetPrefix`.
-param vnetAddressPrefix = ''
-param agentSubnetPrefix = ''
-param peSubnetPrefix = ''
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/main.json b/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/main.json
deleted file mode 100644
index d7dd22475..000000000
--- a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/main.json
+++ /dev/null
@@ -1,2772 +0,0 @@
-{
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "metadata": {
- "_generator": {
- "name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "789904159633670276"
- }
- },
- "parameters": {
- "location": {
- "type": "string",
- "defaultValue": "eastus2",
- "allowedValues": [
- "westus",
- "eastus",
- "eastus2",
- "japaneast",
- "francecentral",
- "spaincentral",
- "uaenorth",
- "southcentralus",
- "italynorth",
- "germanywestcentral",
- "brazilsouth",
- "southafricanorth",
- "australiaeast",
- "swedencentral",
- "canadaeast",
- "westeurope",
- "westus3",
- "uksouth",
- "southindia",
- "koreacentral",
- "polandcentral",
- "switzerlandnorth",
- "norwayeast"
- ],
- "metadata": {
- "description": "Location for all resources."
- }
- },
- "aiServices": {
- "type": "string",
- "defaultValue": "aiservices",
- "metadata": {
- "description": "Name for your AI Services resource."
- }
- },
- "modelName": {
- "type": "string",
- "defaultValue": "gpt-4o",
- "metadata": {
- "description": "The name of the model you want to deploy"
- }
- },
- "modelFormat": {
- "type": "string",
- "defaultValue": "OpenAI",
- "metadata": {
- "description": "The provider of your model"
- }
- },
- "modelVersion": {
- "type": "string",
- "defaultValue": "2024-11-20",
- "metadata": {
- "description": "The version of your model"
- }
- },
- "modelSkuName": {
- "type": "string",
- "defaultValue": "GlobalStandard",
- "metadata": {
- "description": "The sku of your model deployment"
- }
- },
- "modelCapacity": {
- "type": "int",
- "defaultValue": 30,
- "metadata": {
- "description": "The tokens per minute (TPM) of your model deployment"
- }
- },
- "deploymentTimestamp": {
- "type": "string",
- "defaultValue": "[utcNow('yyyyMMddHHmmss')]"
- },
- "firstProjectName": {
- "type": "string",
- "defaultValue": "project",
- "metadata": {
- "description": "Name for your project resource."
- }
- },
- "projectDescription": {
- "type": "string",
- "defaultValue": "A project for the AI Foundry account with network secured deployed Agent",
- "metadata": {
- "description": "This project will be a sub-resource of your account"
- }
- },
- "displayName": {
- "type": "string",
- "defaultValue": "network secured agent project",
- "metadata": {
- "description": "The display name of the project"
- }
- },
- "vnetName": {
- "type": "string",
- "defaultValue": "agent-vnet-test",
- "metadata": {
- "description": "Virtual Network name for the Agent to create new or existing virtual network"
- }
- },
- "agentSubnetName": {
- "type": "string",
- "defaultValue": "agent-subnet",
- "metadata": {
- "description": "The name of Agents Subnet to create new or existing subnet for agents"
- }
- },
- "peSubnetName": {
- "type": "string",
- "defaultValue": "pe-subnet",
- "metadata": {
- "description": "The name of Private Endpoint subnet to create new or existing subnet for private endpoints"
- }
- },
- "existingVnetResourceId": {
- "type": "string",
- "defaultValue": "",
- "metadata": {
- "description": "Existing Virtual Network name Resource ID"
- }
- },
- "vnetAddressPrefix": {
- "type": "string",
- "defaultValue": "",
- "metadata": {
- "description": "Address space for the VNet (only used for new VNet)"
- }
- },
- "agentSubnetPrefix": {
- "type": "string",
- "defaultValue": "",
- "metadata": {
- "description": "Address prefix for the agent subnet. The default value is 192.168.0.0/24 but you can choose any size /26 or any class like 10.0.0.0 or 172.168.0.0"
- }
- },
- "peSubnetPrefix": {
- "type": "string",
- "defaultValue": "",
- "metadata": {
- "description": "Address prefix for the private endpoint subnet"
- }
- },
- "aiSearchResourceId": {
- "type": "string",
- "defaultValue": "",
- "metadata": {
- "description": "The AI Search Service full ARM Resource ID. This is an optional field, and if not provided, the resource will be created."
- }
- },
- "azureStorageAccountResourceId": {
- "type": "string",
- "defaultValue": "",
- "metadata": {
- "description": "The AI Storage Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created."
- }
- },
- "azureCosmosDBAccountResourceId": {
- "type": "string",
- "defaultValue": "",
- "metadata": {
- "description": "The Cosmos DB Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created."
- }
- },
- "fabricWorkspaceResourceId": {
- "type": "string",
- "defaultValue": "",
- "metadata": {
- "description": "The Microsoft Fabric Workspace full ARM Resource ID. This is an optional field for Fabric private link connectivity."
- }
- },
- "existingDnsZones": {
- "type": "object",
- "defaultValue": {
- "privatelink.services.ai.azure.com": "",
- "privatelink.openai.azure.com": "",
- "privatelink.cognitiveservices.azure.com": "",
- "privatelink.search.windows.net": "",
- "privatelink.blob.core.windows.net": "",
- "privatelink.documents.azure.com": "",
- "privatelink.analysis.windows.net": ""
- },
- "metadata": {
- "description": "Object mapping DNS zone names to their resource group, or empty string to indicate creation"
- }
- },
- "dnsZoneNames": {
- "type": "array",
- "defaultValue": [
- "privatelink.services.ai.azure.com",
- "privatelink.openai.azure.com",
- "privatelink.cognitiveservices.azure.com",
- "privatelink.search.windows.net",
- "privatelink.blob.core.windows.net",
- "privatelink.documents.azure.com",
- "privatelink.analysis.windows.net"
- ],
- "metadata": {
- "description": "Zone Names for Validation of existing Private Dns Zones"
- }
- },
- "projectCapHost": {
- "type": "string",
- "defaultValue": "caphostproj",
- "metadata": {
- "description": "The name of the project capability host to be created"
- }
- }
- },
- "variables": {
- "uniqueSuffix": "[substring(uniqueString(format('{0}-{1}', resourceGroup().id, parameters('deploymentTimestamp'))), 0, 4)]",
- "accountName": "[toLower(format('{0}{1}', parameters('aiServices'), variables('uniqueSuffix')))]",
- "projectName": "[toLower(format('{0}{1}', parameters('firstProjectName'), variables('uniqueSuffix')))]",
- "cosmosDBName": "[toLower(format('{0}{1}cosmosdb', parameters('aiServices'), variables('uniqueSuffix')))]",
- "aiSearchName": "[toLower(format('{0}{1}search', parameters('aiServices'), variables('uniqueSuffix')))]",
- "azureStorageName": "[toLower(format('{0}{1}storage', parameters('aiServices'), variables('uniqueSuffix')))]",
- "storagePassedIn": "[not(equals(parameters('azureStorageAccountResourceId'), ''))]",
- "searchPassedIn": "[not(equals(parameters('aiSearchResourceId'), ''))]",
- "cosmosPassedIn": "[not(equals(parameters('azureCosmosDBAccountResourceId'), ''))]",
- "existingVnetPassedIn": "[not(equals(parameters('existingVnetResourceId'), ''))]",
- "acsParts": "[split(parameters('aiSearchResourceId'), '/')]",
- "aiSearchServiceSubscriptionId": "[if(variables('searchPassedIn'), variables('acsParts')[2], subscription().subscriptionId)]",
- "aiSearchServiceResourceGroupName": "[if(variables('searchPassedIn'), variables('acsParts')[4], resourceGroup().name)]",
- "cosmosParts": "[split(parameters('azureCosmosDBAccountResourceId'), '/')]",
- "cosmosDBSubscriptionId": "[if(variables('cosmosPassedIn'), variables('cosmosParts')[2], subscription().subscriptionId)]",
- "cosmosDBResourceGroupName": "[if(variables('cosmosPassedIn'), variables('cosmosParts')[4], resourceGroup().name)]",
- "storageParts": "[split(parameters('azureStorageAccountResourceId'), '/')]",
- "azureStorageSubscriptionId": "[if(variables('storagePassedIn'), variables('storageParts')[2], subscription().subscriptionId)]",
- "azureStorageResourceGroupName": "[if(variables('storagePassedIn'), variables('storageParts')[4], resourceGroup().name)]",
- "vnetParts": "[split(parameters('existingVnetResourceId'), '/')]",
- "vnetSubscriptionId": "[if(variables('existingVnetPassedIn'), variables('vnetParts')[2], subscription().subscriptionId)]",
- "vnetResourceGroupName": "[if(variables('existingVnetPassedIn'), variables('vnetParts')[4], resourceGroup().name)]",
- "existingVnetName": "[if(variables('existingVnetPassedIn'), last(variables('vnetParts')), parameters('vnetName'))]",
- "trimVnetName": "[trim(variables('existingVnetName'))]"
- },
- "resources": [
- {
- "type": "Microsoft.Resources/deployments",
- "apiVersion": "2025-04-01",
- "name": "[format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))]",
- "properties": {
- "expressionEvaluationOptions": {
- "scope": "inner"
- },
- "mode": "Incremental",
- "parameters": {
- "location": {
- "value": "[parameters('location')]"
- },
- "vnetName": {
- "value": "[variables('trimVnetName')]"
- },
- "useExistingVnet": {
- "value": "[variables('existingVnetPassedIn')]"
- },
- "existingVnetResourceGroupName": {
- "value": "[variables('vnetResourceGroupName')]"
- },
- "agentSubnetName": {
- "value": "[parameters('agentSubnetName')]"
- },
- "peSubnetName": {
- "value": "[parameters('peSubnetName')]"
- },
- "vnetAddressPrefix": {
- "value": "[parameters('vnetAddressPrefix')]"
- },
- "agentSubnetPrefix": {
- "value": "[parameters('agentSubnetPrefix')]"
- },
- "peSubnetPrefix": {
- "value": "[parameters('peSubnetPrefix')]"
- },
- "existingVnetSubscriptionId": {
- "value": "[variables('vnetSubscriptionId')]"
- }
- },
- "template": {
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "metadata": {
- "_generator": {
- "name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "8505298823279202405"
- }
- },
- "parameters": {
- "location": {
- "type": "string",
- "metadata": {
- "description": "Azure region for the deployment"
- }
- },
- "vnetName": {
- "type": "string",
- "metadata": {
- "description": "The name of the virtual network"
- }
- },
- "useExistingVnet": {
- "type": "bool",
- "defaultValue": false,
- "metadata": {
- "description": "Indicates if an existing VNet should be used"
- }
- },
- "existingVnetSubscriptionId": {
- "type": "string",
- "defaultValue": "[subscription().subscriptionId]",
- "metadata": {
- "description": "Subscription ID of the existing VNet (if different from current subscription)"
- }
- },
- "existingVnetResourceGroupName": {
- "type": "string",
- "defaultValue": "[resourceGroup().name]",
- "metadata": {
- "description": "Resource Group name of the existing VNet (if different from current resource group)"
- }
- },
- "agentSubnetName": {
- "type": "string",
- "defaultValue": "agent-subnet",
- "metadata": {
- "description": "The name of Agents Subnet"
- }
- },
- "peSubnetName": {
- "type": "string",
- "defaultValue": "pe-subnet",
- "metadata": {
- "description": "The name of Private Endpoint subnet"
- }
- },
- "vnetAddressPrefix": {
- "type": "string",
- "defaultValue": "",
- "metadata": {
- "description": "Address space for the VNet (only used for new VNet)"
- }
- },
- "agentSubnetPrefix": {
- "type": "string",
- "defaultValue": "",
- "metadata": {
- "description": "Address prefix for the agent subnet"
- }
- },
- "peSubnetPrefix": {
- "type": "string",
- "defaultValue": "",
- "metadata": {
- "description": "Address prefix for the private endpoint subnet"
- }
- }
- },
- "resources": [
- {
- "condition": "[not(parameters('useExistingVnet'))]",
- "type": "Microsoft.Resources/deployments",
- "apiVersion": "2025-04-01",
- "name": "vnet-deployment",
- "properties": {
- "expressionEvaluationOptions": {
- "scope": "inner"
- },
- "mode": "Incremental",
- "parameters": {
- "location": {
- "value": "[parameters('location')]"
- },
- "vnetName": {
- "value": "[parameters('vnetName')]"
- },
- "agentSubnetName": {
- "value": "[parameters('agentSubnetName')]"
- },
- "peSubnetName": {
- "value": "[parameters('peSubnetName')]"
- },
- "vnetAddressPrefix": {
- "value": "[parameters('vnetAddressPrefix')]"
- },
- "agentSubnetPrefix": {
- "value": "[parameters('agentSubnetPrefix')]"
- },
- "peSubnetPrefix": {
- "value": "[parameters('peSubnetPrefix')]"
- }
- },
- "template": {
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "metadata": {
- "_generator": {
- "name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "4954184648131521061"
- }
- },
- "parameters": {
- "location": {
- "type": "string",
- "metadata": {
- "description": "Azure region for the deployment"
- }
- },
- "vnetName": {
- "type": "string",
- "defaultValue": "agents-vnet-test",
- "metadata": {
- "description": "The name of the virtual network"
- }
- },
- "agentSubnetName": {
- "type": "string",
- "defaultValue": "agent-subnet",
- "metadata": {
- "description": "The name of Agents Subnet"
- }
- },
- "peSubnetName": {
- "type": "string",
- "defaultValue": "pe-subnet",
- "metadata": {
- "description": "The name of Hub subnet"
- }
- },
- "vnetAddressPrefix": {
- "type": "string",
- "defaultValue": "",
- "metadata": {
- "description": "Address space for the VNet"
- }
- },
- "agentSubnetPrefix": {
- "type": "string",
- "defaultValue": "",
- "metadata": {
- "description": "Address prefix for the agent subnet"
- }
- },
- "peSubnetPrefix": {
- "type": "string",
- "defaultValue": "",
- "metadata": {
- "description": "Address prefix for the private endpoint subnet"
- }
- }
- },
- "variables": {
- "defaultVnetAddressPrefix": "192.168.0.0/16",
- "vnetAddress": "[if(empty(parameters('vnetAddressPrefix')), variables('defaultVnetAddressPrefix'), parameters('vnetAddressPrefix'))]",
- "agentSubnet": "[if(empty(parameters('agentSubnetPrefix')), cidrSubnet(variables('vnetAddress'), 24, 0), parameters('agentSubnetPrefix'))]",
- "peSubnet": "[if(empty(parameters('peSubnetPrefix')), cidrSubnet(variables('vnetAddress'), 24, 1), parameters('peSubnetPrefix'))]"
- },
- "resources": [
- {
- "type": "Microsoft.Network/virtualNetworks",
- "apiVersion": "2024-05-01",
- "name": "[parameters('vnetName')]",
- "location": "[parameters('location')]",
- "properties": {
- "addressSpace": {
- "addressPrefixes": [
- "[variables('vnetAddress')]"
- ]
- },
- "subnets": [
- {
- "name": "[parameters('agentSubnetName')]",
- "properties": {
- "addressPrefix": "[variables('agentSubnet')]",
- "delegations": [
- {
- "name": "Microsoft.app/environments",
- "properties": {
- "serviceName": "Microsoft.App/environments"
- }
- }
- ]
- }
- },
- {
- "name": "[parameters('peSubnetName')]",
- "properties": {
- "addressPrefix": "[variables('peSubnet')]"
- }
- }
- ]
- }
- }
- ],
- "outputs": {
- "peSubnetName": {
- "type": "string",
- "value": "[parameters('peSubnetName')]"
- },
- "agentSubnetName": {
- "type": "string",
- "value": "[parameters('agentSubnetName')]"
- },
- "agentSubnetId": {
- "type": "string",
- "value": "[format('{0}/subnets/{1}', resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('agentSubnetName'))]"
- },
- "peSubnetId": {
- "type": "string",
- "value": "[format('{0}/subnets/{1}', resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('peSubnetName'))]"
- },
- "virtualNetworkName": {
- "type": "string",
- "value": "[parameters('vnetName')]"
- },
- "virtualNetworkId": {
- "type": "string",
- "value": "[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
- },
- "virtualNetworkResourceGroup": {
- "type": "string",
- "value": "[resourceGroup().name]"
- },
- "virtualNetworkSubscriptionId": {
- "type": "string",
- "value": "[subscription().subscriptionId]"
- }
- }
- }
- }
- },
- {
- "condition": "[parameters('useExistingVnet')]",
- "type": "Microsoft.Resources/deployments",
- "apiVersion": "2025-04-01",
- "name": "existing-vnet-deployment",
- "properties": {
- "expressionEvaluationOptions": {
- "scope": "inner"
- },
- "mode": "Incremental",
- "parameters": {
- "vnetName": {
- "value": "[parameters('vnetName')]"
- },
- "vnetResourceGroupName": {
- "value": "[parameters('existingVnetResourceGroupName')]"
- },
- "vnetSubscriptionId": {
- "value": "[parameters('existingVnetSubscriptionId')]"
- },
- "agentSubnetName": {
- "value": "[parameters('agentSubnetName')]"
- },
- "peSubnetName": {
- "value": "[parameters('peSubnetName')]"
- },
- "agentSubnetPrefix": {
- "value": "[parameters('agentSubnetPrefix')]"
- },
- "peSubnetPrefix": {
- "value": "[parameters('peSubnetPrefix')]"
- }
- },
- "template": {
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "metadata": {
- "_generator": {
- "name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "3152324712046183852"
- }
- },
- "parameters": {
- "vnetName": {
- "type": "string",
- "metadata": {
- "description": "The name of the existing virtual network"
- }
- },
- "vnetSubscriptionId": {
- "type": "string",
- "defaultValue": "[subscription().subscriptionId]",
- "metadata": {
- "description": "Subscription ID of virtual network (if different from current subscription)"
- }
- },
- "vnetResourceGroupName": {
- "type": "string",
- "defaultValue": "[resourceGroup().name]",
- "metadata": {
- "description": "Resource Group name of the existing VNet (if different from current resource group)"
- }
- },
- "agentSubnetName": {
- "type": "string",
- "defaultValue": "agent-subnet",
- "metadata": {
- "description": "The name of Agents Subnet"
- }
- },
- "peSubnetName": {
- "type": "string",
- "defaultValue": "pe-subnet",
- "metadata": {
- "description": "The name of Private Endpoint subnet"
- }
- },
- "agentSubnetPrefix": {
- "type": "string",
- "defaultValue": "",
- "metadata": {
- "description": "Address prefix for the agent subnet (only needed if creating new subnet)"
- }
- },
- "peSubnetPrefix": {
- "type": "string",
- "defaultValue": "",
- "metadata": {
- "description": "Address prefix for the private endpoint subnet (only needed if creating new subnet)"
- }
- }
- },
- "resources": [
- {
- "type": "Microsoft.Resources/deployments",
- "apiVersion": "2025-04-01",
- "name": "[format('agent-subnet-{0}', uniqueString(deployment().name, parameters('agentSubnetName')))]",
- "resourceGroup": "[parameters('vnetResourceGroupName')]",
- "properties": {
- "expressionEvaluationOptions": {
- "scope": "inner"
- },
- "mode": "Incremental",
- "parameters": {
- "vnetName": {
- "value": "[parameters('vnetName')]"
- },
- "subnetName": {
- "value": "[parameters('agentSubnetName')]"
- },
- "addressPrefix": "[if(empty(parameters('agentSubnetPrefix')), createObject('value', cidrSubnet(reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), '2024-05-01').addressSpace.addressPrefixes[0], 24, 0)), createObject('value', parameters('agentSubnetPrefix')))]",
- "delegations": {
- "value": [
- {
- "name": "Microsoft.App/environments",
- "properties": {
- "serviceName": "Microsoft.App/environments"
- }
- }
- ]
- }
- },
- "template": {
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "metadata": {
- "_generator": {
- "name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "17043822047386586435"
- }
- },
- "parameters": {
- "vnetName": {
- "type": "string",
- "metadata": {
- "description": "Name of the virtual network"
- }
- },
- "subnetName": {
- "type": "string",
- "metadata": {
- "description": "Name of the subnet"
- }
- },
- "addressPrefix": {
- "type": "string",
- "metadata": {
- "description": "Address prefix for the subnet"
- }
- },
- "delegations": {
- "type": "array",
- "defaultValue": [],
- "metadata": {
- "description": "Array of subnet delegations"
- }
- }
- },
- "resources": [
- {
- "type": "Microsoft.Network/virtualNetworks/subnets",
- "apiVersion": "2024-05-01",
- "name": "[format('{0}/{1}', parameters('vnetName'), parameters('subnetName'))]",
- "properties": {
- "addressPrefix": "[parameters('addressPrefix')]",
- "delegations": "[parameters('delegations')]"
- }
- }
- ],
- "outputs": {
- "subnetId": {
- "type": "string",
- "value": "[resourceId('Microsoft.Network/virtualNetworks/subnets', split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[0], split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[1])]"
- },
- "subnetName": {
- "type": "string",
- "value": "[parameters('subnetName')]"
- }
- }
- }
- }
- },
- {
- "type": "Microsoft.Resources/deployments",
- "apiVersion": "2025-04-01",
- "name": "[format('pe-subnet-{0}', uniqueString(deployment().name, parameters('peSubnetName')))]",
- "resourceGroup": "[parameters('vnetResourceGroupName')]",
- "properties": {
- "expressionEvaluationOptions": {
- "scope": "inner"
- },
- "mode": "Incremental",
- "parameters": {
- "vnetName": {
- "value": "[parameters('vnetName')]"
- },
- "subnetName": {
- "value": "[parameters('peSubnetName')]"
- },
- "addressPrefix": "[if(empty(parameters('peSubnetPrefix')), createObject('value', cidrSubnet(reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), '2024-05-01').addressSpace.addressPrefixes[0], 24, 1)), createObject('value', parameters('peSubnetPrefix')))]",
- "delegations": {
- "value": []
- }
- },
- "template": {
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "metadata": {
- "_generator": {
- "name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "17043822047386586435"
- }
- },
- "parameters": {
- "vnetName": {
- "type": "string",
- "metadata": {
- "description": "Name of the virtual network"
- }
- },
- "subnetName": {
- "type": "string",
- "metadata": {
- "description": "Name of the subnet"
- }
- },
- "addressPrefix": {
- "type": "string",
- "metadata": {
- "description": "Address prefix for the subnet"
- }
- },
- "delegations": {
- "type": "array",
- "defaultValue": [],
- "metadata": {
- "description": "Array of subnet delegations"
- }
- }
- },
- "resources": [
- {
- "type": "Microsoft.Network/virtualNetworks/subnets",
- "apiVersion": "2024-05-01",
- "name": "[format('{0}/{1}', parameters('vnetName'), parameters('subnetName'))]",
- "properties": {
- "addressPrefix": "[parameters('addressPrefix')]",
- "delegations": "[parameters('delegations')]"
- }
- }
- ],
- "outputs": {
- "subnetId": {
- "type": "string",
- "value": "[resourceId('Microsoft.Network/virtualNetworks/subnets', split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[0], split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[1])]"
- },
- "subnetName": {
- "type": "string",
- "value": "[parameters('subnetName')]"
- }
- }
- }
- }
- }
- ],
- "outputs": {
- "peSubnetName": {
- "type": "string",
- "value": "[parameters('peSubnetName')]"
- },
- "agentSubnetName": {
- "type": "string",
- "value": "[parameters('agentSubnetName')]"
- },
- "agentSubnetId": {
- "type": "string",
- "value": "[format('{0}/subnets/{1}', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('agentSubnetName'))]"
- },
- "peSubnetId": {
- "type": "string",
- "value": "[format('{0}/subnets/{1}', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('peSubnetName'))]"
- },
- "virtualNetworkName": {
- "type": "string",
- "value": "[parameters('vnetName')]"
- },
- "virtualNetworkId": {
- "type": "string",
- "value": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
- },
- "virtualNetworkResourceGroup": {
- "type": "string",
- "value": "[parameters('vnetResourceGroupName')]"
- },
- "virtualNetworkSubscriptionId": {
- "type": "string",
- "value": "[parameters('vnetSubscriptionId')]"
- }
- }
- }
- }
- }
- ],
- "outputs": {
- "virtualNetworkName": {
- "type": "string",
- "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.virtualNetworkName.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.virtualNetworkName.value)]"
- },
- "virtualNetworkId": {
- "type": "string",
- "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.virtualNetworkId.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.virtualNetworkId.value)]"
- },
- "virtualNetworkSubscriptionId": {
- "type": "string",
- "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.virtualNetworkSubscriptionId.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.virtualNetworkSubscriptionId.value)]"
- },
- "virtualNetworkResourceGroup": {
- "type": "string",
- "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.virtualNetworkResourceGroup.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.virtualNetworkResourceGroup.value)]"
- },
- "agentSubnetName": {
- "type": "string",
- "value": "[parameters('agentSubnetName')]"
- },
- "peSubnetName": {
- "type": "string",
- "value": "[parameters('peSubnetName')]"
- },
- "agentSubnetId": {
- "type": "string",
- "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.agentSubnetId.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.agentSubnetId.value)]"
- },
- "peSubnetId": {
- "type": "string",
- "value": "[if(parameters('useExistingVnet'), reference(resourceId('Microsoft.Resources/deployments', 'existing-vnet-deployment'), '2025-04-01').outputs.peSubnetId.value, reference(resourceId('Microsoft.Resources/deployments', 'vnet-deployment'), '2025-04-01').outputs.peSubnetId.value)]"
- }
- }
- }
- }
- },
- {
- "type": "Microsoft.Resources/deployments",
- "apiVersion": "2025-04-01",
- "name": "[format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))]",
- "properties": {
- "expressionEvaluationOptions": {
- "scope": "inner"
- },
- "mode": "Incremental",
- "parameters": {
- "accountName": {
- "value": "[variables('accountName')]"
- },
- "location": {
- "value": "[parameters('location')]"
- },
- "modelName": {
- "value": "[parameters('modelName')]"
- },
- "modelFormat": {
- "value": "[parameters('modelFormat')]"
- },
- "modelVersion": {
- "value": "[parameters('modelVersion')]"
- },
- "modelSkuName": {
- "value": "[parameters('modelSkuName')]"
- },
- "modelCapacity": {
- "value": "[parameters('modelCapacity')]"
- },
- "agentSubnetId": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.agentSubnetId.value]"
- }
- },
- "template": {
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "metadata": {
- "_generator": {
- "name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "854097619778148359"
- }
- },
- "parameters": {
- "accountName": {
- "type": "string"
- },
- "location": {
- "type": "string"
- },
- "modelName": {
- "type": "string"
- },
- "modelFormat": {
- "type": "string"
- },
- "modelVersion": {
- "type": "string"
- },
- "modelSkuName": {
- "type": "string"
- },
- "modelCapacity": {
- "type": "int"
- },
- "agentSubnetId": {
- "type": "string"
- },
- "networkInjection": {
- "type": "string",
- "defaultValue": "true"
- }
- },
- "resources": [
- {
- "type": "Microsoft.CognitiveServices/accounts",
- "apiVersion": "2025-04-01-preview",
- "name": "[parameters('accountName')]",
- "location": "[parameters('location')]",
- "sku": {
- "name": "S0"
- },
- "kind": "AIServices",
- "identity": {
- "type": "SystemAssigned"
- },
- "properties": {
- "allowProjectManagement": true,
- "customSubDomainName": "[parameters('accountName')]",
- "networkAcls": {
- "defaultAction": "Deny",
- "virtualNetworkRules": [],
- "ipRules": [],
- "bypass": "AzureServices"
- },
- "publicNetworkAccess": "Disabled",
- "networkInjections": "[if(equals(parameters('networkInjection'), 'true'), createArray(createObject('scenario', 'agent', 'subnetArmId', parameters('agentSubnetId'), 'useMicrosoftManagedNetwork', false())), null())]",
- "disableLocalAuth": false
- }
- },
- {
- "type": "Microsoft.CognitiveServices/accounts/deployments",
- "apiVersion": "2025-04-01-preview",
- "name": "[format('{0}/{1}', parameters('accountName'), parameters('modelName'))]",
- "sku": {
- "capacity": "[parameters('modelCapacity')]",
- "name": "[parameters('modelSkuName')]"
- },
- "properties": {
- "model": {
- "name": "[parameters('modelName')]",
- "format": "[parameters('modelFormat')]",
- "version": "[parameters('modelVersion')]"
- }
- },
- "dependsOn": [
- "[resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName'))]"
- ]
- }
- ],
- "outputs": {
- "accountName": {
- "type": "string",
- "value": "[parameters('accountName')]"
- },
- "accountID": {
- "type": "string",
- "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName'))]"
- },
- "accountTarget": {
- "type": "string",
- "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName')), '2025-04-01-preview').endpoint]"
- },
- "accountPrincipalId": {
- "type": "string",
- "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('accountName')), '2025-04-01-preview', 'full').identity.principalId]"
- }
- }
- }
- },
- "dependsOn": [
- "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]"
- ]
- },
- {
- "type": "Microsoft.Resources/deployments",
- "apiVersion": "2025-04-01",
- "name": "[format('validate-existing-resources-{0}-deployment', variables('uniqueSuffix'))]",
- "properties": {
- "expressionEvaluationOptions": {
- "scope": "inner"
- },
- "mode": "Incremental",
- "parameters": {
- "aiSearchResourceId": {
- "value": "[parameters('aiSearchResourceId')]"
- },
- "azureStorageAccountResourceId": {
- "value": "[parameters('azureStorageAccountResourceId')]"
- },
- "azureCosmosDBAccountResourceId": {
- "value": "[parameters('azureCosmosDBAccountResourceId')]"
- },
- "existingDnsZones": {
- "value": "[parameters('existingDnsZones')]"
- },
- "dnsZoneNames": {
- "value": "[parameters('dnsZoneNames')]"
- }
- },
- "template": {
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "metadata": {
- "_generator": {
- "name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "7641310640078958122"
- }
- },
- "parameters": {
- "aiSearchResourceId": {
- "type": "string",
- "metadata": {
- "description": "Resource ID of the AI Search Service."
- }
- },
- "azureStorageAccountResourceId": {
- "type": "string",
- "metadata": {
- "description": "Resource ID of the Azure Storage Account."
- }
- },
- "azureCosmosDBAccountResourceId": {
- "type": "string",
- "metadata": {
- "description": "ResourceId of Cosmos DB Account"
- }
- },
- "existingDnsZones": {
- "type": "object",
- "metadata": {
- "description": "Object mapping DNS zone names to their resource group, or empty string to indicate creation"
- }
- },
- "dnsZoneNames": {
- "type": "array",
- "metadata": {
- "description": "List of private DNS zone names to validate"
- }
- }
- },
- "variables": {
- "storagePassedIn": "[not(equals(parameters('azureStorageAccountResourceId'), ''))]",
- "searchPassedIn": "[not(equals(parameters('aiSearchResourceId'), ''))]",
- "cosmosPassedIn": "[not(equals(parameters('azureCosmosDBAccountResourceId'), ''))]",
- "storageParts": "[split(parameters('azureStorageAccountResourceId'), '/')]",
- "azureStorageSubscriptionId": "[if(and(variables('storagePassedIn'), greater(length(variables('storageParts')), 2)), variables('storageParts')[2], subscription().subscriptionId)]",
- "azureStorageResourceGroupName": "[if(and(variables('storagePassedIn'), greater(length(variables('storageParts')), 4)), variables('storageParts')[4], resourceGroup().name)]",
- "acsParts": "[split(parameters('aiSearchResourceId'), '/')]",
- "aiSearchServiceSubscriptionId": "[if(and(variables('searchPassedIn'), greater(length(variables('acsParts')), 2)), variables('acsParts')[2], subscription().subscriptionId)]",
- "aiSearchServiceResourceGroupName": "[if(and(variables('searchPassedIn'), greater(length(variables('acsParts')), 4)), variables('acsParts')[4], resourceGroup().name)]",
- "cosmosParts": "[split(parameters('azureCosmosDBAccountResourceId'), '/')]",
- "cosmosDBSubscriptionId": "[if(and(variables('cosmosPassedIn'), greater(length(variables('cosmosParts')), 2)), variables('cosmosParts')[2], subscription().subscriptionId)]",
- "cosmosDBResourceGroupName": "[if(and(variables('cosmosPassedIn'), greater(length(variables('cosmosParts')), 4)), variables('cosmosParts')[4], resourceGroup().name)]",
- "dnsZoneTypes": [
- "Microsoft.Network/privateDnsZones"
- ]
- },
- "resources": [],
- "outputs": {
- "aiSearchExists": {
- "type": "bool",
- "value": "[and(variables('searchPassedIn'), equals(last(split(parameters('aiSearchResourceId'), '/')), variables('acsParts')[8]))]"
- },
- "cosmosDBExists": {
- "type": "bool",
- "value": "[and(variables('cosmosPassedIn'), equals(last(split(parameters('azureCosmosDBAccountResourceId'), '/')), variables('cosmosParts')[8]))]"
- },
- "azureStorageExists": {
- "type": "bool",
- "value": "[and(variables('storagePassedIn'), equals(last(split(parameters('azureStorageAccountResourceId'), '/')), variables('storageParts')[8]))]"
- },
- "aiSearchServiceSubscriptionId": {
- "type": "string",
- "value": "[variables('aiSearchServiceSubscriptionId')]"
- },
- "aiSearchServiceResourceGroupName": {
- "type": "string",
- "value": "[variables('aiSearchServiceResourceGroupName')]"
- },
- "cosmosDBSubscriptionId": {
- "type": "string",
- "value": "[variables('cosmosDBSubscriptionId')]"
- },
- "cosmosDBResourceGroupName": {
- "type": "string",
- "value": "[variables('cosmosDBResourceGroupName')]"
- },
- "azureStorageSubscriptionId": {
- "type": "string",
- "value": "[variables('azureStorageSubscriptionId')]"
- },
- "azureStorageResourceGroupName": {
- "type": "string",
- "value": "[variables('azureStorageResourceGroupName')]"
- },
- "dnsZoneExists": {
- "type": "array",
- "copy": {
- "count": "[length(parameters('dnsZoneNames'))]",
- "input": {
- "name": "[parameters('dnsZoneNames')[copyIndex()]]",
- "exists": "[not(empty(parameters('existingDnsZones')[parameters('dnsZoneNames')[copyIndex()]]))]"
- }
- }
- }
- }
- }
- }
- },
- {
- "type": "Microsoft.Resources/deployments",
- "apiVersion": "2025-04-01",
- "name": "[format('dependencies-{0}-deployment', variables('uniqueSuffix'))]",
- "properties": {
- "expressionEvaluationOptions": {
- "scope": "inner"
- },
- "mode": "Incremental",
- "parameters": {
- "location": {
- "value": "[parameters('location')]"
- },
- "azureStorageName": {
- "value": "[variables('azureStorageName')]"
- },
- "aiSearchName": {
- "value": "[variables('aiSearchName')]"
- },
- "cosmosDBName": {
- "value": "[variables('cosmosDBName')]"
- },
- "aiSearchResourceId": {
- "value": "[parameters('aiSearchResourceId')]"
- },
- "aiSearchExists": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('validate-existing-resources-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.aiSearchExists.value]"
- },
- "azureStorageAccountResourceId": {
- "value": "[parameters('azureStorageAccountResourceId')]"
- },
- "azureStorageExists": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('validate-existing-resources-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.azureStorageExists.value]"
- },
- "cosmosDBResourceId": {
- "value": "[parameters('azureCosmosDBAccountResourceId')]"
- },
- "cosmosDBExists": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('validate-existing-resources-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.cosmosDBExists.value]"
- }
- },
- "template": {
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "metadata": {
- "_generator": {
- "name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "2754228344238136934"
- }
- },
- "parameters": {
- "location": {
- "type": "string",
- "metadata": {
- "description": "Azure region of the deployment"
- }
- },
- "aiSearchName": {
- "type": "string",
- "metadata": {
- "description": "The name of the AI Search resource"
- }
- },
- "azureStorageName": {
- "type": "string",
- "metadata": {
- "description": "Name of the storage account"
- }
- },
- "cosmosDBName": {
- "type": "string",
- "metadata": {
- "description": "Name of the new Cosmos DB account"
- }
- },
- "aiSearchResourceId": {
- "type": "string",
- "metadata": {
- "description": "The AI Search Service full ARM Resource ID. This is an optional field, and if not provided, the resource will be created."
- }
- },
- "azureStorageAccountResourceId": {
- "type": "string",
- "metadata": {
- "description": "The AI Storage Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created."
- }
- },
- "cosmosDBResourceId": {
- "type": "string",
- "metadata": {
- "description": "The Cosmos DB Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created."
- }
- },
- "aiSearchExists": {
- "type": "bool"
- },
- "azureStorageExists": {
- "type": "bool"
- },
- "cosmosDBExists": {
- "type": "bool"
- },
- "noZRSRegions": {
- "type": "array",
- "defaultValue": [
- "southindia",
- "westus"
- ]
- },
- "sku": {
- "type": "object",
- "defaultValue": "[if(contains(parameters('noZRSRegions'), parameters('location')), createObject('name', 'Standard_GRS'), createObject('name', 'Standard_ZRS'))]"
- }
- },
- "variables": {
- "cosmosParts": "[split(parameters('cosmosDBResourceId'), '/')]",
- "canaryRegions": [
- "eastus2euap",
- "centraluseuap"
- ],
- "cosmosDbRegion": "[if(contains(variables('canaryRegions'), parameters('location')), 'westus', parameters('location'))]",
- "acsParts": "[split(parameters('aiSearchResourceId'), '/')]",
- "azureStorageParts": "[split(parameters('azureStorageAccountResourceId'), '/')]"
- },
- "resources": [
- {
- "condition": "[not(parameters('cosmosDBExists'))]",
- "type": "Microsoft.DocumentDB/databaseAccounts",
- "apiVersion": "2024-11-15",
- "name": "[parameters('cosmosDBName')]",
- "location": "[variables('cosmosDbRegion')]",
- "kind": "GlobalDocumentDB",
- "properties": {
- "consistencyPolicy": {
- "defaultConsistencyLevel": "Session"
- },
- "disableLocalAuth": true,
- "enableAutomaticFailover": false,
- "enableMultipleWriteLocations": false,
- "publicNetworkAccess": "Disabled",
- "enableFreeTier": false,
- "locations": [
- {
- "locationName": "[parameters('location')]",
- "failoverPriority": 0,
- "isZoneRedundant": false
- }
- ],
- "databaseAccountOfferType": "Standard"
- }
- },
- {
- "condition": "[not(parameters('aiSearchExists'))]",
- "type": "Microsoft.Search/searchServices",
- "apiVersion": "2024-06-01-preview",
- "name": "[parameters('aiSearchName')]",
- "location": "[parameters('location')]",
- "identity": {
- "type": "SystemAssigned"
- },
- "properties": {
- "disableLocalAuth": false,
- "authOptions": {
- "aadOrApiKey": {
- "aadAuthFailureMode": "http401WithBearerChallenge"
- }
- },
- "encryptionWithCmk": {
- "enforcement": "Unspecified"
- },
- "hostingMode": "default",
- "partitionCount": 1,
- "publicNetworkAccess": "disabled",
- "replicaCount": 1,
- "semanticSearch": "disabled",
- "networkRuleSet": {
- "bypass": "None",
- "ipRules": []
- }
- },
- "sku": {
- "name": "standard"
- }
- },
- {
- "condition": "[not(parameters('azureStorageExists'))]",
- "type": "Microsoft.Storage/storageAccounts",
- "apiVersion": "2023-05-01",
- "name": "[parameters('azureStorageName')]",
- "location": "[parameters('location')]",
- "kind": "StorageV2",
- "sku": "[parameters('sku')]",
- "properties": {
- "minimumTlsVersion": "TLS1_2",
- "allowBlobPublicAccess": false,
- "publicNetworkAccess": "Disabled",
- "networkAcls": {
- "bypass": "AzureServices",
- "defaultAction": "Deny",
- "virtualNetworkRules": []
- },
- "allowSharedKeyAccess": false
- }
- }
- ],
- "outputs": {
- "aiSearchName": {
- "type": "string",
- "value": "[if(parameters('aiSearchExists'), variables('acsParts')[8], parameters('aiSearchName'))]"
- },
- "aiSearchID": {
- "type": "string",
- "value": "[if(parameters('aiSearchExists'), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('acsParts')[2], variables('acsParts')[4]), 'Microsoft.Search/searchServices', variables('acsParts')[8]), resourceId('Microsoft.Search/searchServices', parameters('aiSearchName')))]"
- },
- "aiSearchServiceResourceGroupName": {
- "type": "string",
- "value": "[if(parameters('aiSearchExists'), variables('acsParts')[4], resourceGroup().name)]"
- },
- "aiSearchServiceSubscriptionId": {
- "type": "string",
- "value": "[if(parameters('aiSearchExists'), variables('acsParts')[2], subscription().subscriptionId)]"
- },
- "azureStorageName": {
- "type": "string",
- "value": "[if(parameters('azureStorageExists'), variables('azureStorageParts')[8], parameters('azureStorageName'))]"
- },
- "azureStorageId": {
- "type": "string",
- "value": "[if(parameters('azureStorageExists'), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('azureStorageParts')[2], variables('azureStorageParts')[4]), 'Microsoft.Storage/storageAccounts', variables('azureStorageParts')[8]), resourceId('Microsoft.Storage/storageAccounts', parameters('azureStorageName')))]"
- },
- "azureStorageResourceGroupName": {
- "type": "string",
- "value": "[if(parameters('azureStorageExists'), variables('azureStorageParts')[4], resourceGroup().name)]"
- },
- "azureStorageSubscriptionId": {
- "type": "string",
- "value": "[if(parameters('azureStorageExists'), variables('azureStorageParts')[2], subscription().subscriptionId)]"
- },
- "cosmosDBName": {
- "type": "string",
- "value": "[if(parameters('cosmosDBExists'), variables('cosmosParts')[8], parameters('cosmosDBName'))]"
- },
- "cosmosDBId": {
- "type": "string",
- "value": "[if(parameters('cosmosDBExists'), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('cosmosParts')[2], variables('cosmosParts')[4]), 'Microsoft.DocumentDB/databaseAccounts', variables('cosmosParts')[8]), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName')))]"
- },
- "cosmosDBResourceGroupName": {
- "type": "string",
- "value": "[if(parameters('cosmosDBExists'), variables('cosmosParts')[4], resourceGroup().name)]"
- },
- "cosmosDBSubscriptionId": {
- "type": "string",
- "value": "[if(parameters('cosmosDBExists'), variables('cosmosParts')[2], subscription().subscriptionId)]"
- }
- }
- }
- },
- "dependsOn": [
- "[resourceId('Microsoft.Resources/deployments', format('validate-existing-resources-{0}-deployment', variables('uniqueSuffix')))]"
- ]
- },
- {
- "type": "Microsoft.Resources/deployments",
- "apiVersion": "2025-04-01",
- "name": "[format('{0}-private-endpoint', variables('uniqueSuffix'))]",
- "properties": {
- "expressionEvaluationOptions": {
- "scope": "inner"
- },
- "mode": "Incremental",
- "parameters": {
- "aiAccountName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))), '2025-04-01').outputs.accountName.value]"
- },
- "aiSearchName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.aiSearchName.value]"
- },
- "storageName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.azureStorageName.value]"
- },
- "cosmosDBName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.cosmosDBName.value]"
- },
- "fabricWorkspaceResourceId": {
- "value": "[parameters('fabricWorkspaceResourceId')]"
- },
- "vnetName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.virtualNetworkName.value]"
- },
- "peSubnetName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.peSubnetName.value]"
- },
- "suffix": {
- "value": "[variables('uniqueSuffix')]"
- },
- "vnetResourceGroupName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.virtualNetworkResourceGroup.value]"
- },
- "vnetSubscriptionId": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.virtualNetworkSubscriptionId.value]"
- },
- "cosmosDBSubscriptionId": {
- "value": "[variables('cosmosDBSubscriptionId')]"
- },
- "cosmosDBResourceGroupName": {
- "value": "[variables('cosmosDBResourceGroupName')]"
- },
- "aiSearchSubscriptionId": {
- "value": "[variables('aiSearchServiceSubscriptionId')]"
- },
- "aiSearchResourceGroupName": {
- "value": "[variables('aiSearchServiceResourceGroupName')]"
- },
- "storageAccountResourceGroupName": {
- "value": "[variables('azureStorageResourceGroupName')]"
- },
- "storageAccountSubscriptionId": {
- "value": "[variables('azureStorageSubscriptionId')]"
- },
- "existingDnsZones": {
- "value": "[parameters('existingDnsZones')]"
- }
- },
- "template": {
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "metadata": {
- "_generator": {
- "name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "10536644141407027053"
- }
- },
- "parameters": {
- "aiAccountName": {
- "type": "string",
- "metadata": {
- "description": "Name of the AI Foundry account"
- }
- },
- "aiSearchName": {
- "type": "string",
- "metadata": {
- "description": "Name of the AI Search service"
- }
- },
- "storageName": {
- "type": "string",
- "metadata": {
- "description": "Name of the storage account"
- }
- },
- "cosmosDBName": {
- "type": "string",
- "metadata": {
- "description": "Name of the Cosmos DB account"
- }
- },
- "fabricWorkspaceResourceId": {
- "type": "string",
- "defaultValue": "",
- "metadata": {
- "description": "The Microsoft Fabric Workspace full ARM Resource ID. Optional - leave empty to skip Fabric private endpoint."
- }
- },
- "vnetName": {
- "type": "string",
- "metadata": {
- "description": "Name of the Vnet"
- }
- },
- "peSubnetName": {
- "type": "string",
- "metadata": {
- "description": "Name of the Customer subnet"
- }
- },
- "suffix": {
- "type": "string",
- "metadata": {
- "description": "Suffix for unique resource names"
- }
- },
- "vnetResourceGroupName": {
- "type": "string",
- "defaultValue": "[resourceGroup().name]",
- "metadata": {
- "description": "Resource Group name for existing Virtual Network (if different from current resource group)"
- }
- },
- "vnetSubscriptionId": {
- "type": "string",
- "defaultValue": "[subscription().subscriptionId]",
- "metadata": {
- "description": "Subscription ID for Virtual Network"
- }
- },
- "storageAccountResourceGroupName": {
- "type": "string",
- "defaultValue": "[resourceGroup().name]",
- "metadata": {
- "description": "Resource Group name for Storage Account"
- }
- },
- "storageAccountSubscriptionId": {
- "type": "string",
- "defaultValue": "[subscription().subscriptionId]",
- "metadata": {
- "description": "Subscription ID for Storage account"
- }
- },
- "aiSearchSubscriptionId": {
- "type": "string",
- "defaultValue": "[subscription().subscriptionId]",
- "metadata": {
- "description": "Subscription ID for AI Search service"
- }
- },
- "aiSearchResourceGroupName": {
- "type": "string",
- "defaultValue": "[resourceGroup().name]",
- "metadata": {
- "description": "Resource Group name for AI Search service"
- }
- },
- "cosmosDBSubscriptionId": {
- "type": "string",
- "defaultValue": "[subscription().subscriptionId]",
- "metadata": {
- "description": "Subscription ID for Cosmos DB account"
- }
- },
- "cosmosDBResourceGroupName": {
- "type": "string",
- "defaultValue": "[resourceGroup().name]",
- "metadata": {
- "description": "Resource group name for Cosmos DB account"
- }
- },
- "existingDnsZones": {
- "type": "object",
- "defaultValue": {
- "privatelink.services.ai.azure.com": "",
- "privatelink.openai.azure.com": "",
- "privatelink.cognitiveservices.azure.com": "",
- "privatelink.search.windows.net": "",
- "[format('privatelink.blob.{0}', environment().suffixes.storage)]": "",
- "privatelink.documents.azure.com": "",
- "privatelink.fabric.microsoft.com": ""
- },
- "metadata": {
- "description": "Map of DNS zone FQDNs to resource group names. If provided, reference existing DNS zones in this resource group instead of creating them."
- }
- }
- },
- "variables": {
- "fabricPassedIn": "[not(equals(parameters('fabricWorkspaceResourceId'), ''))]",
- "fabricParts": "[split(parameters('fabricWorkspaceResourceId'), '/')]",
- "fabricWorkspaceName": "[if(variables('fabricPassedIn'), last(variables('fabricParts')), '')]",
- "aiServicesDnsZoneName": "privatelink.services.ai.azure.com",
- "openAiDnsZoneName": "privatelink.openai.azure.com",
- "cognitiveServicesDnsZoneName": "privatelink.cognitiveservices.azure.com",
- "aiSearchDnsZoneName": "privatelink.search.windows.net",
- "storageDnsZoneName": "[format('privatelink.blob.{0}', environment().suffixes.storage)]",
- "cosmosDBDnsZoneName": "privatelink.documents.azure.com",
- "fabricDnsZoneName": "privatelink.fabric.microsoft.com",
- "aiServicesDnsZoneRG": "[parameters('existingDnsZones')[variables('aiServicesDnsZoneName')]]",
- "openAiDnsZoneRG": "[parameters('existingDnsZones')[variables('openAiDnsZoneName')]]",
- "cognitiveServicesDnsZoneRG": "[parameters('existingDnsZones')[variables('cognitiveServicesDnsZoneName')]]",
- "aiSearchDnsZoneRG": "[parameters('existingDnsZones')[variables('aiSearchDnsZoneName')]]",
- "storageDnsZoneRG": "[parameters('existingDnsZones')[variables('storageDnsZoneName')]]",
- "cosmosDBDnsZoneRG": "[parameters('existingDnsZones')[variables('cosmosDBDnsZoneName')]]",
- "fabricDnsZoneRG": "[coalesce(tryGet(parameters('existingDnsZones'), 'fabricDnsZoneName'), '')]",
- "aiServicesDnsZoneId": "[if(empty(variables('aiServicesDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('aiServicesDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('aiServicesDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('aiServicesDnsZoneName')))]",
- "openAiDnsZoneId": "[if(empty(variables('openAiDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('openAiDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('openAiDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('openAiDnsZoneName')))]",
- "cognitiveServicesDnsZoneId": "[if(empty(variables('cognitiveServicesDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('cognitiveServicesDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('cognitiveServicesDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('cognitiveServicesDnsZoneName')))]",
- "aiSearchDnsZoneId": "[if(empty(variables('aiSearchDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('aiSearchDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('aiSearchDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('aiSearchDnsZoneName')))]",
- "storageDnsZoneId": "[if(empty(variables('storageDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('storageDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('storageDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('storageDnsZoneName')))]",
- "cosmosDBDnsZoneId": "[if(empty(variables('cosmosDBDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('cosmosDBDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('cosmosDBDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('cosmosDBDnsZoneName')))]",
- "fabricDnsZoneId": "[if(variables('fabricPassedIn'), if(empty(variables('fabricDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('fabricDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('fabricDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('fabricDnsZoneName'))), '')]"
- },
- "resources": [
- {
- "type": "Microsoft.Network/privateEndpoints",
- "apiVersion": "2024-05-01",
- "name": "[format('{0}-private-endpoint', parameters('aiAccountName'))]",
- "location": "[resourceGroup().location]",
- "properties": {
- "subnet": {
- "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('peSubnetName'))]"
- },
- "privateLinkServiceConnections": [
- {
- "name": "[format('{0}-private-link-service-connection', parameters('aiAccountName'))]",
- "properties": {
- "privateLinkServiceId": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('aiAccountName'))]",
- "groupIds": [
- "account"
- ]
- }
- }
- ]
- }
- },
- {
- "type": "Microsoft.Network/privateEndpoints",
- "apiVersion": "2024-05-01",
- "name": "[format('{0}-private-endpoint', parameters('aiSearchName'))]",
- "location": "[resourceGroup().location]",
- "properties": {
- "subnet": {
- "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('peSubnetName'))]"
- },
- "privateLinkServiceConnections": [
- {
- "name": "[format('{0}-private-link-service-connection', parameters('aiSearchName'))]",
- "properties": {
- "privateLinkServiceId": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('aiSearchSubscriptionId'), parameters('aiSearchResourceGroupName')), 'Microsoft.Search/searchServices', parameters('aiSearchName'))]",
- "groupIds": [
- "searchService"
- ]
- }
- }
- ]
- }
- },
- {
- "type": "Microsoft.Network/privateEndpoints",
- "apiVersion": "2024-05-01",
- "name": "[format('{0}-private-endpoint', parameters('storageName'))]",
- "location": "[resourceGroup().location]",
- "properties": {
- "subnet": {
- "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('peSubnetName'))]"
- },
- "privateLinkServiceConnections": [
- {
- "name": "[format('{0}-private-link-service-connection', parameters('storageName'))]",
- "properties": {
- "privateLinkServiceId": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('storageAccountSubscriptionId'), parameters('storageAccountResourceGroupName')), 'Microsoft.Storage/storageAccounts', parameters('storageName'))]",
- "groupIds": [
- "blob"
- ]
- }
- }
- ]
- }
- },
- {
- "type": "Microsoft.Network/privateEndpoints",
- "apiVersion": "2024-05-01",
- "name": "[format('{0}-private-endpoint', parameters('cosmosDBName'))]",
- "location": "[resourceGroup().location]",
- "properties": {
- "subnet": {
- "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('peSubnetName'))]"
- },
- "privateLinkServiceConnections": [
- {
- "name": "[format('{0}-private-link-service-connection', parameters('cosmosDBName'))]",
- "properties": {
- "privateLinkServiceId": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('cosmosDBSubscriptionId'), parameters('cosmosDBResourceGroupName')), 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName'))]",
- "groupIds": [
- "Sql"
- ]
- }
- }
- ]
- }
- },
- {
- "condition": "[variables('fabricPassedIn')]",
- "type": "Microsoft.Network/privateEndpoints",
- "apiVersion": "2024-05-01",
- "name": "[format('{0}-fabric-private-endpoint', variables('fabricWorkspaceName'))]",
- "location": "[resourceGroup().location]",
- "properties": {
- "subnet": {
- "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('peSubnetName'))]"
- },
- "privateLinkServiceConnections": [
- {
- "name": "[format('{0}-private-link-service-connection', variables('fabricWorkspaceName'))]",
- "properties": {
- "privateLinkServiceId": "[parameters('fabricWorkspaceResourceId')]",
- "groupIds": [
- "Fabric"
- ]
- }
- }
- ]
- }
- },
- {
- "condition": "[empty(variables('aiServicesDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones",
- "apiVersion": "2020-06-01",
- "name": "[variables('aiServicesDnsZoneName')]",
- "location": "global"
- },
- {
- "condition": "[empty(variables('openAiDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones",
- "apiVersion": "2020-06-01",
- "name": "[variables('openAiDnsZoneName')]",
- "location": "global"
- },
- {
- "condition": "[empty(variables('cognitiveServicesDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones",
- "apiVersion": "2020-06-01",
- "name": "[variables('cognitiveServicesDnsZoneName')]",
- "location": "global"
- },
- {
- "condition": "[empty(variables('aiSearchDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones",
- "apiVersion": "2020-06-01",
- "name": "[variables('aiSearchDnsZoneName')]",
- "location": "global"
- },
- {
- "condition": "[empty(variables('storageDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones",
- "apiVersion": "2020-06-01",
- "name": "[variables('storageDnsZoneName')]",
- "location": "global"
- },
- {
- "condition": "[empty(variables('cosmosDBDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones",
- "apiVersion": "2020-06-01",
- "name": "[variables('cosmosDBDnsZoneName')]",
- "location": "global"
- },
- {
- "condition": "[and(variables('fabricPassedIn'), empty(variables('fabricDnsZoneRG')))]",
- "type": "Microsoft.Network/privateDnsZones",
- "apiVersion": "2020-06-01",
- "name": "[variables('fabricDnsZoneName')]",
- "location": "global"
- },
- {
- "condition": "[empty(variables('aiServicesDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
- "apiVersion": "2024-06-01",
- "name": "[format('{0}/{1}', variables('aiServicesDnsZoneName'), format('aiServices-{0}-link', parameters('suffix')))]",
- "location": "global",
- "properties": {
- "virtualNetwork": {
- "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
- },
- "registrationEnabled": false
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones', variables('aiServicesDnsZoneName'))]"
- ]
- },
- {
- "condition": "[empty(variables('openAiDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
- "apiVersion": "2024-06-01",
- "name": "[format('{0}/{1}', variables('openAiDnsZoneName'), format('aiServicesOpenAI-{0}-link', parameters('suffix')))]",
- "location": "global",
- "properties": {
- "virtualNetwork": {
- "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
- },
- "registrationEnabled": false
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones', variables('openAiDnsZoneName'))]"
- ]
- },
- {
- "condition": "[empty(variables('cognitiveServicesDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
- "apiVersion": "2024-06-01",
- "name": "[format('{0}/{1}', variables('cognitiveServicesDnsZoneName'), format('aiServicesCognitiveServices-{0}-link', parameters('suffix')))]",
- "location": "global",
- "properties": {
- "virtualNetwork": {
- "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
- },
- "registrationEnabled": false
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones', variables('cognitiveServicesDnsZoneName'))]"
- ]
- },
- {
- "condition": "[empty(variables('aiSearchDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
- "apiVersion": "2024-06-01",
- "name": "[format('{0}/{1}', variables('aiSearchDnsZoneName'), format('aiSearch-{0}-link', parameters('suffix')))]",
- "location": "global",
- "properties": {
- "virtualNetwork": {
- "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
- },
- "registrationEnabled": false
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones', variables('aiSearchDnsZoneName'))]"
- ]
- },
- {
- "condition": "[empty(variables('storageDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
- "apiVersion": "2024-06-01",
- "name": "[format('{0}/{1}', variables('storageDnsZoneName'), format('storage-{0}-link', parameters('suffix')))]",
- "location": "global",
- "properties": {
- "virtualNetwork": {
- "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
- },
- "registrationEnabled": false
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones', variables('storageDnsZoneName'))]"
- ]
- },
- {
- "condition": "[empty(variables('cosmosDBDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
- "apiVersion": "2024-06-01",
- "name": "[format('{0}/{1}', variables('cosmosDBDnsZoneName'), format('cosmosDB-{0}-link', parameters('suffix')))]",
- "location": "global",
- "properties": {
- "virtualNetwork": {
- "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
- },
- "registrationEnabled": false
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones', variables('cosmosDBDnsZoneName'))]"
- ]
- },
- {
- "condition": "[and(variables('fabricPassedIn'), empty(variables('fabricDnsZoneRG')))]",
- "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
- "apiVersion": "2024-06-01",
- "name": "[format('{0}/{1}', variables('fabricDnsZoneName'), format('fabric-{0}-link', parameters('suffix')))]",
- "location": "global",
- "properties": {
- "virtualNetwork": {
- "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
- },
- "registrationEnabled": false
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones', variables('fabricDnsZoneName'))]"
- ]
- },
- {
- "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups",
- "apiVersion": "2024-05-01",
- "name": "[format('{0}/{1}', format('{0}-private-endpoint', parameters('aiAccountName')), format('{0}-dns-group', parameters('aiAccountName')))]",
- "properties": {
- "privateDnsZoneConfigs": [
- {
- "name": "[format('{0}-dns-aiserv-config', parameters('aiAccountName'))]",
- "properties": {
- "privateDnsZoneId": "[variables('aiServicesDnsZoneId')]"
- }
- },
- {
- "name": "[format('{0}-dns-openai-config', parameters('aiAccountName'))]",
- "properties": {
- "privateDnsZoneId": "[variables('openAiDnsZoneId')]"
- }
- },
- {
- "name": "[format('{0}-dns-cogserv-config', parameters('aiAccountName'))]",
- "properties": {
- "privateDnsZoneId": "[variables('cognitiveServicesDnsZoneId')]"
- }
- }
- ]
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('aiAccountName')))]",
- "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('aiServicesDnsZoneName'), format('aiServices-{0}-link', parameters('suffix')))]",
- "[resourceId('Microsoft.Network/privateDnsZones', variables('aiServicesDnsZoneName'))]",
- "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('cognitiveServicesDnsZoneName'), format('aiServicesCognitiveServices-{0}-link', parameters('suffix')))]",
- "[resourceId('Microsoft.Network/privateDnsZones', variables('cognitiveServicesDnsZoneName'))]",
- "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('openAiDnsZoneName'), format('aiServicesOpenAI-{0}-link', parameters('suffix')))]",
- "[resourceId('Microsoft.Network/privateDnsZones', variables('openAiDnsZoneName'))]"
- ]
- },
- {
- "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups",
- "apiVersion": "2024-05-01",
- "name": "[format('{0}/{1}', format('{0}-private-endpoint', parameters('aiSearchName')), format('{0}-dns-group', parameters('aiSearchName')))]",
- "properties": {
- "privateDnsZoneConfigs": [
- {
- "name": "[format('{0}-dns-config', parameters('aiSearchName'))]",
- "properties": {
- "privateDnsZoneId": "[variables('aiSearchDnsZoneId')]"
- }
- }
- ]
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('aiSearchDnsZoneName'), format('aiSearch-{0}-link', parameters('suffix')))]",
- "[resourceId('Microsoft.Network/privateDnsZones', variables('aiSearchDnsZoneName'))]",
- "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('aiSearchName')))]"
- ]
- },
- {
- "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups",
- "apiVersion": "2024-05-01",
- "name": "[format('{0}/{1}', format('{0}-private-endpoint', parameters('storageName')), format('{0}-dns-group', parameters('storageName')))]",
- "properties": {
- "privateDnsZoneConfigs": [
- {
- "name": "[format('{0}-dns-config', parameters('storageName'))]",
- "properties": {
- "privateDnsZoneId": "[variables('storageDnsZoneId')]"
- }
- }
- ]
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('storageDnsZoneName'), format('storage-{0}-link', parameters('suffix')))]",
- "[resourceId('Microsoft.Network/privateDnsZones', variables('storageDnsZoneName'))]",
- "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('storageName')))]"
- ]
- },
- {
- "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups",
- "apiVersion": "2024-05-01",
- "name": "[format('{0}/{1}', format('{0}-private-endpoint', parameters('cosmosDBName')), format('{0}-dns-group', parameters('cosmosDBName')))]",
- "properties": {
- "privateDnsZoneConfigs": [
- {
- "name": "[format('{0}-dns-config', parameters('cosmosDBName'))]",
- "properties": {
- "privateDnsZoneId": "[variables('cosmosDBDnsZoneId')]"
- }
- }
- ]
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('cosmosDBDnsZoneName'), format('cosmosDB-{0}-link', parameters('suffix')))]",
- "[resourceId('Microsoft.Network/privateDnsZones', variables('cosmosDBDnsZoneName'))]",
- "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('cosmosDBName')))]"
- ]
- },
- {
- "condition": "[variables('fabricPassedIn')]",
- "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups",
- "apiVersion": "2024-05-01",
- "name": "[format('{0}/{1}', format('{0}-fabric-private-endpoint', variables('fabricWorkspaceName')), format('{0}-dns-group', variables('fabricWorkspaceName')))]",
- "properties": {
- "privateDnsZoneConfigs": [
- {
- "name": "[format('{0}-dns-config', variables('fabricWorkspaceName'))]",
- "properties": {
- "privateDnsZoneId": "[variables('fabricDnsZoneId')]"
- }
- }
- ]
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('fabricDnsZoneName'), format('fabric-{0}-link', parameters('suffix')))]",
- "[resourceId('Microsoft.Network/privateDnsZones', variables('fabricDnsZoneName'))]",
- "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-fabric-private-endpoint', variables('fabricWorkspaceName')))]"
- ]
- }
- ]
- }
- },
- "dependsOn": [
- "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix')))]",
- "[resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix')))]",
- "[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]"
- ]
- },
- {
- "type": "Microsoft.Resources/deployments",
- "apiVersion": "2025-04-01",
- "name": "[format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))]",
- "properties": {
- "expressionEvaluationOptions": {
- "scope": "inner"
- },
- "mode": "Incremental",
- "parameters": {
- "projectName": {
- "value": "[variables('projectName')]"
- },
- "projectDescription": {
- "value": "[parameters('projectDescription')]"
- },
- "displayName": {
- "value": "[parameters('displayName')]"
- },
- "location": {
- "value": "[parameters('location')]"
- },
- "aiSearchName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.aiSearchName.value]"
- },
- "aiSearchServiceResourceGroupName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.aiSearchServiceResourceGroupName.value]"
- },
- "aiSearchServiceSubscriptionId": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.aiSearchServiceSubscriptionId.value]"
- },
- "cosmosDBName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.cosmosDBName.value]"
- },
- "cosmosDBSubscriptionId": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.cosmosDBSubscriptionId.value]"
- },
- "cosmosDBResourceGroupName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.cosmosDBResourceGroupName.value]"
- },
- "azureStorageName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.azureStorageName.value]"
- },
- "azureStorageSubscriptionId": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.azureStorageSubscriptionId.value]"
- },
- "azureStorageResourceGroupName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.azureStorageResourceGroupName.value]"
- },
- "accountName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))), '2025-04-01').outputs.accountName.value]"
- }
- },
- "template": {
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "metadata": {
- "_generator": {
- "name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "5095087340309076800"
- }
- },
- "parameters": {
- "accountName": {
- "type": "string"
- },
- "location": {
- "type": "string"
- },
- "projectName": {
- "type": "string"
- },
- "projectDescription": {
- "type": "string"
- },
- "displayName": {
- "type": "string"
- },
- "aiSearchName": {
- "type": "string"
- },
- "aiSearchServiceResourceGroupName": {
- "type": "string"
- },
- "aiSearchServiceSubscriptionId": {
- "type": "string"
- },
- "cosmosDBName": {
- "type": "string"
- },
- "cosmosDBSubscriptionId": {
- "type": "string"
- },
- "cosmosDBResourceGroupName": {
- "type": "string"
- },
- "azureStorageName": {
- "type": "string"
- },
- "azureStorageSubscriptionId": {
- "type": "string"
- },
- "azureStorageResourceGroupName": {
- "type": "string"
- }
- },
- "resources": [
- {
- "type": "Microsoft.CognitiveServices/accounts/projects/connections",
- "apiVersion": "2025-04-01-preview",
- "name": "[format('{0}/{1}/{2}', parameters('accountName'), parameters('projectName'), parameters('cosmosDBName'))]",
- "properties": {
- "category": "CosmosDB",
- "target": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('cosmosDBSubscriptionId'), parameters('cosmosDBResourceGroupName')), 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName')), '2024-12-01-preview').documentEndpoint]",
- "authType": "AAD",
- "metadata": {
- "ApiType": "Azure",
- "ResourceId": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('cosmosDBSubscriptionId'), parameters('cosmosDBResourceGroupName')), 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName'))]",
- "location": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('cosmosDBSubscriptionId'), parameters('cosmosDBResourceGroupName')), 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName')), '2024-12-01-preview', 'full').location]"
- }
- },
- "dependsOn": [
- "[resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('accountName'), parameters('projectName'))]"
- ]
- },
- {
- "type": "Microsoft.CognitiveServices/accounts/projects/connections",
- "apiVersion": "2025-04-01-preview",
- "name": "[format('{0}/{1}/{2}', parameters('accountName'), parameters('projectName'), parameters('azureStorageName'))]",
- "properties": {
- "category": "AzureStorageAccount",
- "target": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('azureStorageSubscriptionId'), parameters('azureStorageResourceGroupName')), 'Microsoft.Storage/storageAccounts', parameters('azureStorageName')), '2023-05-01').primaryEndpoints.blob]",
- "authType": "AAD",
- "metadata": {
- "ApiType": "Azure",
- "ResourceId": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('azureStorageSubscriptionId'), parameters('azureStorageResourceGroupName')), 'Microsoft.Storage/storageAccounts', parameters('azureStorageName'))]",
- "location": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('azureStorageSubscriptionId'), parameters('azureStorageResourceGroupName')), 'Microsoft.Storage/storageAccounts', parameters('azureStorageName')), '2023-05-01', 'full').location]"
- }
- },
- "dependsOn": [
- "[resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('accountName'), parameters('projectName'))]"
- ]
- },
- {
- "type": "Microsoft.CognitiveServices/accounts/projects/connections",
- "apiVersion": "2025-04-01-preview",
- "name": "[format('{0}/{1}/{2}', parameters('accountName'), parameters('projectName'), parameters('aiSearchName'))]",
- "properties": {
- "category": "CognitiveSearch",
- "target": "[format('https://{0}.search.windows.net', parameters('aiSearchName'))]",
- "authType": "AAD",
- "metadata": {
- "ApiType": "Azure",
- "ResourceId": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('aiSearchServiceSubscriptionId'), parameters('aiSearchServiceResourceGroupName')), 'Microsoft.Search/searchServices', parameters('aiSearchName'))]",
- "location": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('aiSearchServiceSubscriptionId'), parameters('aiSearchServiceResourceGroupName')), 'Microsoft.Search/searchServices', parameters('aiSearchName')), '2024-06-01-preview', 'full').location]"
- }
- },
- "dependsOn": [
- "[resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('accountName'), parameters('projectName'))]"
- ]
- },
- {
- "type": "Microsoft.CognitiveServices/accounts/projects",
- "apiVersion": "2025-04-01-preview",
- "name": "[format('{0}/{1}', parameters('accountName'), parameters('projectName'))]",
- "location": "[parameters('location')]",
- "identity": {
- "type": "SystemAssigned"
- },
- "properties": {
- "description": "[parameters('projectDescription')]",
- "displayName": "[parameters('displayName')]"
- }
- }
- ],
- "outputs": {
- "projectName": {
- "type": "string",
- "value": "[parameters('projectName')]"
- },
- "projectId": {
- "type": "string",
- "value": "[resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('accountName'), parameters('projectName'))]"
- },
- "projectPrincipalId": {
- "type": "string",
- "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('accountName'), parameters('projectName')), '2025-04-01-preview', 'full').identity.principalId]"
- },
- "projectWorkspaceId": {
- "type": "string",
- "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('accountName'), parameters('projectName')), '2025-04-01-preview').internalId]"
- },
- "cosmosDBConnection": {
- "type": "string",
- "value": "[parameters('cosmosDBName')]"
- },
- "azureStorageConnection": {
- "type": "string",
- "value": "[parameters('azureStorageName')]"
- },
- "aiSearchConnection": {
- "type": "string",
- "value": "[parameters('aiSearchName')]"
- }
- }
- }
- },
- "dependsOn": [
- "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix')))]",
- "[resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix')))]",
- "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]"
- ]
- },
- {
- "type": "Microsoft.Resources/deployments",
- "apiVersion": "2025-04-01",
- "name": "[format('format-project-workspace-id-{0}-deployment', variables('uniqueSuffix'))]",
- "properties": {
- "expressionEvaluationOptions": {
- "scope": "inner"
- },
- "mode": "Incremental",
- "parameters": {
- "projectWorkspaceId": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.projectWorkspaceId.value]"
- }
- },
- "template": {
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "metadata": {
- "_generator": {
- "name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "6910483561575524105"
- }
- },
- "parameters": {
- "projectWorkspaceId": {
- "type": "string"
- }
- },
- "variables": {
- "part1": "[substring(parameters('projectWorkspaceId'), 0, 8)]",
- "part2": "[substring(parameters('projectWorkspaceId'), 8, 4)]",
- "part3": "[substring(parameters('projectWorkspaceId'), 12, 4)]",
- "part4": "[substring(parameters('projectWorkspaceId'), 16, 4)]",
- "part5": "[substring(parameters('projectWorkspaceId'), 20, 12)]",
- "formattedGuid": "[format('{0}-{1}-{2}-{3}-{4}', variables('part1'), variables('part2'), variables('part3'), variables('part4'), variables('part5'))]"
- },
- "resources": [],
- "outputs": {
- "projectWorkspaceIdGuid": {
- "type": "string",
- "value": "[variables('formattedGuid')]"
- }
- }
- }
- },
- "dependsOn": [
- "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]"
- ]
- },
- {
- "type": "Microsoft.Resources/deployments",
- "apiVersion": "2025-04-01",
- "name": "[format('storage-{0}-{1}-deployment', variables('azureStorageName'), variables('uniqueSuffix'))]",
- "subscriptionId": "[variables('azureStorageSubscriptionId')]",
- "resourceGroup": "[variables('azureStorageResourceGroupName')]",
- "properties": {
- "expressionEvaluationOptions": {
- "scope": "inner"
- },
- "mode": "Incremental",
- "parameters": {
- "azureStorageName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.azureStorageName.value]"
- },
- "projectPrincipalId": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.projectPrincipalId.value]"
- }
- },
- "template": {
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "metadata": {
- "_generator": {
- "name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "14683840003859985069"
- }
- },
- "parameters": {
- "azureStorageName": {
- "type": "string"
- },
- "projectPrincipalId": {
- "type": "string"
- }
- },
- "resources": [
- {
- "type": "Microsoft.Authorization/roleAssignments",
- "apiVersion": "2022-04-01",
- "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('azureStorageName'))]",
- "name": "[guid(parameters('projectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'), resourceId('Microsoft.Storage/storageAccounts', parameters('azureStorageName')))]",
- "properties": {
- "principalId": "[parameters('projectPrincipalId')]",
- "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]",
- "principalType": "ServicePrincipal"
- }
- }
- ]
- }
- },
- "dependsOn": [
- "[resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix')))]",
- "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]",
- "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]"
- ]
- },
- {
- "type": "Microsoft.Resources/deployments",
- "apiVersion": "2025-04-01",
- "name": "[format('cosmos-account-ra-{0}-deployment', variables('uniqueSuffix'))]",
- "subscriptionId": "[variables('cosmosDBSubscriptionId')]",
- "resourceGroup": "[variables('cosmosDBResourceGroupName')]",
- "properties": {
- "expressionEvaluationOptions": {
- "scope": "inner"
- },
- "mode": "Incremental",
- "parameters": {
- "cosmosDBName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.cosmosDBName.value]"
- },
- "projectPrincipalId": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.projectPrincipalId.value]"
- }
- },
- "template": {
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "metadata": {
- "_generator": {
- "name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "25128059954858801"
- }
- },
- "parameters": {
- "cosmosDBName": {
- "type": "string",
- "metadata": {
- "description": "Name of the Cosmos DB resource"
- }
- },
- "projectPrincipalId": {
- "type": "string",
- "metadata": {
- "description": "Principal ID of the AI project"
- }
- }
- },
- "resources": [
- {
- "type": "Microsoft.Authorization/roleAssignments",
- "apiVersion": "2022-04-01",
- "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', parameters('cosmosDBName'))]",
- "name": "[guid(parameters('projectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', '230815da-be43-4aae-9cb4-875f7bd000aa'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName')))]",
- "properties": {
- "principalId": "[parameters('projectPrincipalId')]",
- "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '230815da-be43-4aae-9cb4-875f7bd000aa')]",
- "principalType": "ServicePrincipal"
- }
- }
- ]
- }
- },
- "dependsOn": [
- "[resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix')))]",
- "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]",
- "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]"
- ]
- },
- {
- "type": "Microsoft.Resources/deployments",
- "apiVersion": "2025-04-01",
- "name": "[format('ai-search-ra-{0}-deployment', variables('uniqueSuffix'))]",
- "subscriptionId": "[variables('aiSearchServiceSubscriptionId')]",
- "resourceGroup": "[variables('aiSearchServiceResourceGroupName')]",
- "properties": {
- "expressionEvaluationOptions": {
- "scope": "inner"
- },
- "mode": "Incremental",
- "parameters": {
- "aiSearchName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.aiSearchName.value]"
- },
- "projectPrincipalId": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.projectPrincipalId.value]"
- }
- },
- "template": {
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "metadata": {
- "_generator": {
- "name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "7968115481508840"
- }
- },
- "parameters": {
- "aiSearchName": {
- "type": "string",
- "metadata": {
- "description": "Name of the AI Search resource"
- }
- },
- "projectPrincipalId": {
- "type": "string",
- "metadata": {
- "description": "Principal ID of the AI project"
- }
- }
- },
- "resources": [
- {
- "type": "Microsoft.Authorization/roleAssignments",
- "apiVersion": "2022-04-01",
- "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('aiSearchName'))]",
- "name": "[guid(parameters('projectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7'), resourceId('Microsoft.Search/searchServices', parameters('aiSearchName')))]",
- "properties": {
- "principalId": "[parameters('projectPrincipalId')]",
- "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7')]",
- "principalType": "ServicePrincipal"
- }
- },
- {
- "type": "Microsoft.Authorization/roleAssignments",
- "apiVersion": "2022-04-01",
- "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('aiSearchName'))]",
- "name": "[guid(parameters('projectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0'), resourceId('Microsoft.Search/searchServices', parameters('aiSearchName')))]",
- "properties": {
- "principalId": "[parameters('projectPrincipalId')]",
- "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0')]",
- "principalType": "ServicePrincipal"
- }
- }
- ]
- }
- },
- "dependsOn": [
- "[resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix')))]",
- "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]",
- "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]"
- ]
- },
- {
- "type": "Microsoft.Resources/deployments",
- "apiVersion": "2025-04-01",
- "name": "[format('capabilityHost-configuration-{0}-deployment', variables('uniqueSuffix'))]",
- "properties": {
- "expressionEvaluationOptions": {
- "scope": "inner"
- },
- "mode": "Incremental",
- "parameters": {
- "accountName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix'))), '2025-04-01').outputs.accountName.value]"
- },
- "projectName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.projectName.value]"
- },
- "cosmosDBConnection": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.cosmosDBConnection.value]"
- },
- "azureStorageConnection": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.azureStorageConnection.value]"
- },
- "aiSearchConnection": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.aiSearchConnection.value]"
- },
- "projectCapHost": {
- "value": "[parameters('projectCapHost')]"
- }
- },
- "template": {
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "metadata": {
- "_generator": {
- "name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "17458377866351620215"
- }
- },
- "parameters": {
- "cosmosDBConnection": {
- "type": "string"
- },
- "azureStorageConnection": {
- "type": "string"
- },
- "aiSearchConnection": {
- "type": "string"
- },
- "projectName": {
- "type": "string"
- },
- "accountName": {
- "type": "string"
- },
- "projectCapHost": {
- "type": "string"
- }
- },
- "variables": {
- "threadConnections": [
- "[format('{0}', parameters('cosmosDBConnection'))]"
- ],
- "storageConnections": [
- "[format('{0}', parameters('azureStorageConnection'))]"
- ],
- "vectorStoreConnections": [
- "[format('{0}', parameters('aiSearchConnection'))]"
- ]
- },
- "resources": [
- {
- "type": "Microsoft.CognitiveServices/accounts/projects/capabilityHosts",
- "apiVersion": "2025-04-01-preview",
- "name": "[format('{0}/{1}/{2}', parameters('accountName'), parameters('projectName'), parameters('projectCapHost'))]",
- "properties": {
- "capabilityHostKind": "Agents",
- "vectorStoreConnections": "[variables('vectorStoreConnections')]",
- "storageConnections": "[variables('storageConnections')]",
- "threadStorageConnections": "[variables('threadConnections')]"
- }
- }
- ],
- "outputs": {
- "projectCapHost": {
- "type": "string",
- "value": "[parameters('projectCapHost')]"
- }
- }
- }
- },
- "dependsOn": [
- "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix')))]",
- "[resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix')))]",
- "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]",
- "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('aiSearchServiceSubscriptionId'), variables('aiSearchServiceResourceGroupName')), 'Microsoft.Resources/deployments', format('ai-search-ra-{0}-deployment', variables('uniqueSuffix')))]",
- "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('cosmosDBSubscriptionId'), variables('cosmosDBResourceGroupName')), 'Microsoft.Resources/deployments', format('cosmos-account-ra-{0}-deployment', variables('uniqueSuffix')))]",
- "[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]",
- "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('azureStorageSubscriptionId'), variables('azureStorageResourceGroupName')), 'Microsoft.Resources/deployments', format('storage-{0}-{1}-deployment', variables('azureStorageName'), variables('uniqueSuffix')))]"
- ]
- },
- {
- "type": "Microsoft.Resources/deployments",
- "apiVersion": "2025-04-01",
- "name": "[format('storage-containers-ra-{0}-deployment', variables('uniqueSuffix'))]",
- "subscriptionId": "[variables('azureStorageSubscriptionId')]",
- "resourceGroup": "[variables('azureStorageResourceGroupName')]",
- "properties": {
- "expressionEvaluationOptions": {
- "scope": "inner"
- },
- "mode": "Incremental",
- "parameters": {
- "aiProjectPrincipalId": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.projectPrincipalId.value]"
- },
- "storageName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.azureStorageName.value]"
- },
- "workspaceId": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('format-project-workspace-id-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.projectWorkspaceIdGuid.value]"
- }
- },
- "template": {
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "metadata": {
- "_generator": {
- "name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "13874725855824693255"
- }
- },
- "parameters": {
- "storageName": {
- "type": "string",
- "metadata": {
- "description": "Name of the storage account"
- }
- },
- "aiProjectPrincipalId": {
- "type": "string",
- "metadata": {
- "description": "Principal ID of the AI Project"
- }
- },
- "workspaceId": {
- "type": "string",
- "metadata": {
- "description": "Workspace Id of the AI Project"
- }
- }
- },
- "variables": {
- "conditionStr": "[format('((!(ActionMatches{{''Microsoft.Storage/storageAccounts/blobServices/containers/blobs/tags/read''}}) AND !(ActionMatches{{''Microsoft.Storage/storageAccounts/blobServices/containers/blobs/filter/action''}}) AND !(ActionMatches{{''Microsoft.Storage/storageAccounts/blobServices/containers/blobs/tags/write''}}) ) OR (@Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringStartsWithIgnoreCase ''{0}'' AND @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringLikeIgnoreCase ''*-azureml-agent''))', parameters('workspaceId'))]"
- },
- "resources": [
- {
- "type": "Microsoft.Authorization/roleAssignments",
- "apiVersion": "2022-04-01",
- "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('storageName'))]",
- "name": "[guid(resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'), resourceId('Microsoft.Storage/storageAccounts', parameters('storageName')))]",
- "properties": {
- "principalId": "[parameters('aiProjectPrincipalId')]",
- "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')]",
- "principalType": "ServicePrincipal",
- "conditionVersion": "2.0",
- "condition": "[variables('conditionStr')]"
- }
- }
- ]
- }
- },
- "dependsOn": [
- "[resourceId('Microsoft.Resources/deployments', format('capabilityHost-configuration-{0}-deployment', variables('uniqueSuffix')))]",
- "[resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix')))]",
- "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]",
- "[resourceId('Microsoft.Resources/deployments', format('format-project-workspace-id-{0}-deployment', variables('uniqueSuffix')))]"
- ]
- },
- {
- "type": "Microsoft.Resources/deployments",
- "apiVersion": "2025-04-01",
- "name": "[format('cosmos-containers-ra-{0}-deployment', variables('uniqueSuffix'))]",
- "subscriptionId": "[variables('cosmosDBSubscriptionId')]",
- "resourceGroup": "[variables('cosmosDBResourceGroupName')]",
- "properties": {
- "expressionEvaluationOptions": {
- "scope": "inner"
- },
- "mode": "Incremental",
- "parameters": {
- "cosmosAccountName": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.cosmosDBName.value]"
- },
- "projectWorkspaceId": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('format-project-workspace-id-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.projectWorkspaceIdGuid.value]"
- },
- "projectPrincipalId": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix'))), '2025-04-01').outputs.projectPrincipalId.value]"
- }
- },
- "template": {
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "metadata": {
- "_generator": {
- "name": "bicep",
- "version": "0.39.26.7824",
- "templateHash": "16291470712974205281"
- }
- },
- "parameters": {
- "cosmosAccountName": {
- "type": "string",
- "metadata": {
- "description": "Name of the AI Search resource"
- }
- },
- "projectPrincipalId": {
- "type": "string",
- "metadata": {
- "description": "Project name"
- }
- },
- "projectWorkspaceId": {
- "type": "string"
- }
- },
- "variables": {
- "roleDefinitionId": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', parameters('cosmosAccountName'), '00000000-0000-0000-0000-000000000002')]",
- "accountScope": "[format('/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.DocumentDB/databaseAccounts/{2}', subscription().subscriptionId, resourceGroup().name, parameters('cosmosAccountName'))]"
- },
- "resources": [
- {
- "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments",
- "apiVersion": "2022-05-15",
- "name": "[format('{0}/{1}', parameters('cosmosAccountName'), guid(parameters('projectWorkspaceId'), parameters('cosmosAccountName'), variables('roleDefinitionId'), parameters('projectPrincipalId')))]",
- "properties": {
- "principalId": "[parameters('projectPrincipalId')]",
- "roleDefinitionId": "[variables('roleDefinitionId')]",
- "scope": "[variables('accountScope')]"
- }
- }
- ]
- }
- },
- "dependsOn": [
- "[resourceId('Microsoft.Resources/deployments', format('capabilityHost-configuration-{0}-deployment', variables('uniqueSuffix')))]",
- "[resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix')))]",
- "[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]",
- "[resourceId('Microsoft.Resources/deployments', format('format-project-workspace-id-{0}-deployment', variables('uniqueSuffix')))]",
- "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('azureStorageSubscriptionId'), variables('azureStorageResourceGroupName')), 'Microsoft.Resources/deployments', format('storage-containers-ra-{0}-deployment', variables('uniqueSuffix')))]"
- ]
- }
- ]
-}
\ No newline at end of file
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/validate-existing-resources.bicep b/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/validate-existing-resources.bicep
deleted file mode 100644
index f798e5e27..000000000
--- a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/validate-existing-resources.bicep
+++ /dev/null
@@ -1,91 +0,0 @@
-// @description('Resource ID of the AI Service Account. ')
-// param aiServiceAccountResourceId string
-
-@description('Resource ID of the AI Search Service.')
-param aiSearchResourceId string
-
-@description('Resource ID of the Azure Storage Account.')
-param azureStorageAccountResourceId string
-
-@description('ResourceId of Cosmos DB Account')
-param azureCosmosDBAccountResourceId string
-
-// Check if existing resources have been passed in
-var storagePassedIn = azureStorageAccountResourceId != ''
-var searchPassedIn = aiSearchResourceId != ''
-var cosmosPassedIn = azureCosmosDBAccountResourceId != ''
-
-var storageParts = split(azureStorageAccountResourceId, '/')
-var azureStorageSubscriptionId = storagePassedIn && length(storageParts) > 2 ? storageParts[2] : subscription().subscriptionId
-var azureStorageResourceGroupName = storagePassedIn && length(storageParts) > 4 ? storageParts[4] : resourceGroup().name
-
-var acsParts = split(aiSearchResourceId, '/')
-var aiSearchServiceSubscriptionId = searchPassedIn && length(acsParts) > 2 ? acsParts[2] : subscription().subscriptionId
-var aiSearchServiceResourceGroupName = searchPassedIn && length(acsParts) > 4 ? acsParts[4] : resourceGroup().name
-
-var cosmosParts = split(azureCosmosDBAccountResourceId, '/')
-var cosmosDBSubscriptionId = cosmosPassedIn && length(cosmosParts) > 2 ? cosmosParts[2] : subscription().subscriptionId
-var cosmosDBResourceGroupName = cosmosPassedIn && length(cosmosParts) > 4 ? cosmosParts[4] : resourceGroup().name
-
-// Validate AI Search
-resource aiSearch 'Microsoft.Search/searchServices@2024-06-01-preview' existing = if (searchPassedIn) {
- name: last(split(aiSearchResourceId, '/'))
- scope: resourceGroup(aiSearchServiceSubscriptionId, aiSearchServiceResourceGroupName)
-}
-
-// Validate Cosmos DB Account
-resource cosmosDBAccount 'Microsoft.DocumentDB/databaseAccounts@2024-12-01-preview' existing = if (cosmosPassedIn) {
- name: last(split(azureCosmosDBAccountResourceId, '/'))
- scope: resourceGroup(cosmosDBSubscriptionId,cosmosDBResourceGroupName)
-}
-
-// Validate Storage Account
-resource azureStorageAccount 'Microsoft.Storage/storageAccounts@2024-01-01' existing = if (storagePassedIn) {
- name: last(split(azureStorageAccountResourceId, '/'))
- scope: resourceGroup(azureStorageSubscriptionId,azureStorageResourceGroupName)
-}
-
-// output aiServiceExists bool = aiServicesPassedIn && (aiServiceAccount.name == aiServiceParts[8])
-output aiSearchExists bool = searchPassedIn && (aiSearch.name == acsParts[8])
-output cosmosDBExists bool = cosmosPassedIn && (cosmosDBAccount.name == cosmosParts[8])
-output azureStorageExists bool = storagePassedIn && (azureStorageAccount.name == storageParts[8])
-
-output aiSearchServiceSubscriptionId string = aiSearchServiceSubscriptionId
-output aiSearchServiceResourceGroupName string = aiSearchServiceResourceGroupName
-
-output cosmosDBSubscriptionId string = cosmosDBSubscriptionId
-output cosmosDBResourceGroupName string = cosmosDBResourceGroupName
-
-output azureStorageSubscriptionId string = azureStorageSubscriptionId
-output azureStorageResourceGroupName string = azureStorageResourceGroupName
-
-// Adding DNS Zone Check
-
-@description('Object mapping DNS zone names to their resource group, or empty string to indicate creation')
-param existingDnsZones object
-
-@description('List of private DNS zone names to validate')
-param dnsZoneNames array
-
-var dnsZoneTypes = [
- 'Microsoft.Network/privateDnsZones'
-]
-
-// Output whether each DNS zone exists
-output dnsZoneExists array = [
- for zoneName in dnsZoneNames: {
- name: zoneName
- exists: !empty(existingDnsZones[zoneName])
- }
-]
-
-/*
-// Helper function to check existence
-function resourceExists(resourceType: string, name: string, rg: string): bool {
- // Use the existing resource reference to check
- var res = existing resource dnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
- name: name
- scope: resourceGroup(rg)
- }
- return !empty(res.id)
-}*/
diff --git a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/README.md b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/README.md
new file mode 100644
index 000000000..d8ea34be4
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/README.md
@@ -0,0 +1,675 @@
+---
+description: This set of templates demonstrates how to set up Foundry Agent Service with virtual network isolation, private network links, and tools behind VNet.
+page_type: sample
+products:
+- azure
+- azure-resource-manager
+urlFragment: network-secured-agent-tools
+languages:
+- bicep
+- json
+---
+
+# Microsoft Foundry: Standard Agent Setup with E2E Network Isolation with Tools behind VNET
+
+> **NEW**
+> For support on deploying the right network isolation template, check out the [GitHub Copilot for Azure skill for private networking](https://github.com/microsoft/GitHub-Copilot-for-Azure/blob/main/plugin/skills/microsoft-foundry/resource/private-network/private-network.md) set-up!
+
+> **IMPORTANT**
+> When testing all the Agent tools behind a VNET, please use the [TESTING-GUIDE.md](tests/TESTING-GUIDE.md) file in this repository to ensure your tools are set-up correctly. Currently supported Agent tools behind a VNET include: private MCP, OpenAPI, A2A, Azure Functions, AI Search, Fabric Data Agent. More tools behind a VNET support is coming soon!
+
+---
+## Overview
+This infrastructure-as-code (IaC) solution deploys a network-secured agent environment with private networking, role-based access control (RBAC), and support for tools behind the VNet (MCP servers, OpenAPI tools, Azure Functions, A2A).
+
+Standard setup supports private network isolation through utilizing **Bring Your Own Virtual Network (BYO VNet)** approach, also known as **custom VNet support with subnet delegation.**
+
+This implementation gives you full control over the inbound and outbound communication paths for your agent. You can restrict access to only the resources explicitly required by your agent, such as storage accounts, databases, or APIs, while blocking all other traffic by default. This approach ensures that your agent operates within a tightly scoped network boundary, reducing the risk of data leakage or unauthorized access. By default, this setup simplifies security configuration while enforcing strong isolation guarantees, ensuring that each agent deployment remains secure, compliant, and aligned with enterprise networking policies.
+
+By default, the Foundry resource itself also has **public network access disabled**, but this can be switched to public access if needed (see [Switching Between Private and Public Access](#switching-between-private-and-public-access)).
+
+---
+
+## When to Use This Template
+
+Use this template when you need:
+- **Full end-to-end network isolation** β All resources behind private endpoints with no public internet access
+- **BYO VNet control** β You manage your own virtual network, subnets, and network security groups
+- **Standard agent setup with BYO resources** β Customer-managed Storage, Cosmos DB, and AI Search for data residency and compliance
+- **Tools behind VNet** β MCP servers, OpenAPI tools, Azure Functions, or A2A agents deployed on the private VNet
+- **System Assigned Managed Identity** β Simplified identity management with platform-managed credentials
+
+### Template Decision Guide
+
+Use the table below to choose the right infrastructure template for your scenario:
+
+| Template | Agent Type | Networking | Identity | Key Use Case |
+|----------|-----------|------------|----------|-------------|
+| [**15**](../15-private-network-standard-agent-setup/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | E2E network isolation with full agent capabilities |
+| [**19** (this template)](../19-private-network-agent-tools/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | Same as 15 **plus** tools behind VNet (MCP, OpenAPI, Functions, A2A) |
+| [**17**](../17-private-network-standard-user-assigned-identity-agent-setup/) | Standard (BYO resources) | BYO VNet + Private Endpoints | **User Assigned MI** | Same as 15 but with user-managed identity |
+| [**16**](../16-private-network-standard-agent-apim-setup-preview/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | Same as 15 **plus** private APIM integration (preview) |
+| [**18**](../18-managed-virtual-network-preview/) | Standard (BYO resources) | **Managed VNet** (Microsoft-managed) | System Assigned MI | Network isolation without managing your own VNet (preview) |
+| [**15a**](../15a-private-network-evaluation-only-setup/) | Evaluation only | BYO VNet + Private Endpoints | System Assigned MI | Minimal setup for evaluation β no Cosmos DB, AI Search, or capability host |
+| [**11**](../11-private-network-basic-vnet/) | **Basic** (platform-managed) | BYO VNet injection | System Assigned MI | Basic agents with VNet isolation β no BYO resources needed |
+| [**41**](../41-standard-agent-setup/) | Standard (BYO resources) | **Public** (no VNet) | System Assigned MI | Standard agents without network isolation |
+| [**40**](../40-basic-agent-setup/) | **Basic** (platform-managed) | **Public** (no VNet) | System Assigned MI | Simplest setup β no BYO resources, no private networking |
+
+### Key Features
+
+| Feature | This Template (19) β Private (default) | This Template (19) β Public | Fully Private (15) |
+|---------|----------------------------------------|-----------------------------|-----------------------|
+| AI Services public access | β Disabled | β
Enabled | β Disabled |
+| Portal access | Via VPN/ExpressRoute/Bastion | β
Works directly | Via VPN/ExpressRoute/Bastion |
+| Backend resources | π Private | π Private | π Private |
+| Data Proxy | β
Configured | β
Configured | β
Configured |
+| Tools behind VNet (MCP, OpenAPI, Functions, A2A) | β
Supported | β
Supported | β Not supported |
+| Secure connection required | β
Yes | β No | β
Yes |
+
+---
+
+## Deploy to Azure
+
+[](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fazure-ai-foundry%2Ffoundry-samples%2Frefs%2Fheads%2Fmain%2Finfrastructure%2Finfrastructure-setup-bicep%2F19-private-network-agent-tools%2Fazuredeploy.json/createUIDefinitionUri/https%3A%2F%2Fraw.githubusercontent.com%2Fazure-ai-foundry%2Ffoundry-samples%2Frefs%2Fheads%2Fmain%2Finfrastructure%2Finfrastructure-setup-bicep%2F19-private-network-agent-tools%2FcreateUiDefinition.json)
+
+> The "Deploy to Azure" button uses [`createUiDefinition.json`](./createUiDefinition.json) to render a guided wizard in the Azure Portal: real VNet/subnet pickers, resource pickers for AI Search / Cosmos / Storage, and per-field validation \u2014 instead of the default flat list of 27 text boxes.
+
+
+---
+
+## Prerequisites
+
+1. **Active Azure subscription with appropriate permissions**
+ - **Foundry Account Owner**: Needed to create the Microsoft Foundry account and project.
+ - **Owner or Role Based Access Administrator**: Needed to assign RBAC on the Azure resources used by this template.
+ - **Foundry User**: Needed to create and use agents, projects, or evaluation workloads after deployment.
+
+1. **Register Resource Providers**
+
+ Make sure you have an active Azure subscription that allows registering resource providers. For example, subnet delegation requires the Microsoft.App provider to be registered in your subscription. If it's not already registered, run the commands below:
+
+ ```bash
+ az provider register --namespace 'Microsoft.KeyVault'
+ az provider register --namespace 'Microsoft.CognitiveServices'
+ az provider register --namespace 'Microsoft.Storage'
+ az provider register --namespace 'Microsoft.Search'
+ az provider register --namespace 'Microsoft.Network'
+ az provider register --namespace 'Microsoft.App'
+ az provider register --namespace 'Microsoft.ContainerService'
+ ```
+
+1. Network administrator permissions (if operating in a restricted or enterprise environment)
+
+1. Sufficient quota for all resources required by this template in the target Azure region, including model deployment quota.
+ * If no parameters are passed in, this template creates an Microsoft Foundry resource, Foundry project, Azure Cosmos DB for NoSQL, Azure AI Search, and Azure Storage account
+1. Azure CLI installed and configured on your local workstation or deployment pipeline server
+
+---
+
+## Pre-Deployment Steps
+
+### Networking Requirements
+1. Review network requirements and plan Virtual Network address space (e.g., 192.168.0.0/16 or an alternative non-overlapping address space)
+
+2. Three subnets are needed:
+ - **Agent Subnet** (e.g., 192.168.0.0/24): Hosts Agent client for Agent workloads, delegated to Microsoft.App/environments. The recommended size should be /24 for this delegated subnet.
+ - **Private endpoint Subnet** (e.g., 192.168.1.0/24): Hosts private endpoints
+ - **MCP Subnet** (e.g., 192.168.2.0/24): Hosts MCP servers, OpenAPI tools, Azure Functions, and A2A agents on the VNet
+ - Ensure that the address spaces for the used VNET does not overlap with any existing networks in your Azure environment or reserved IP ranges like the following: 169.254.0.0/16,172.30.0.0/16,172.31.0.0/16,192.0.2.0/24,0.0.0.0/8,127.0.0.0/8,100.100.0.0/17,100.100.192.0/19,100.100.224.0/19,100.64.0.0/11.
+ This includes all address space(s) you have in your VNET if you have more than one, and peered VNETs.
+
+ > **Notes:**
+ - If you do not provide an existing virtual network, the template will create a new virtual network with the default address spaces and subnets described above. If you use an existing virtual network, make sure it already contains three subnets (Agent, Private Endpoint, and MCP) before deploying the template.
+ - You must ensure the Foundry account was successfully created so that underlying caphost has also succeeded. Then proceed to deploying the project caphost bicep.
+ - You must ensure the agent subnet is exclusively delegated to __Microsoft.App/environments__ and cannot be used by any other Azure resources.
+
+### Limitations / Known Issues
+
+1. The delegated agent subnet must be exclusively used by a single Foundry account. It cannot be shared across accounts.
+2. The Foundry resource and the virtual network must be in the same Azure region. BYO resources (Storage, Cosmos DB, AI Search) may be in different regions.
+3. For the virtual network IP range, you may use any Private Class A, B or C IP range. Private Class A IP address ranges (10.x.x.x) are only supported in the following regions: **Australia East, Brazil South, Canada East, East US, East US 2, France Central, Germany West Central, Italy North, Japan East, South Africa North, South Central US, South India, Spain Central, Sweden Central, UAE North, UK South, West US, West US 3.** Use Class B (172.16.x.x) or C (192.168.x.x) ranges for other regions. You may not use any other IP range that overlaps to the list above or uses public IP ranges.
+4. There is no upgrade path from BYO VNet (this template) to Managed Virtual Network (template 18). A Foundry resource redeployment is required.
+5. All projects within the same Foundry account share model deployments. Per-project model isolation is not supported.
+6. Cosmos DB is deployed as single-region. Multi-region replication must be configured manually post-deployment.
+
+### Switching Between Private and Public Access
+
+The Foundry resource has **public network access disabled by default**. You can switch between the two modes by modifying the Bicep template.
+
+#### To enable public access
+
+In [modules-network-secured/ai-account-identity.bicep](modules-network-secured/ai-account-identity.bicep), change:
+
+```bicep
+// Change from:
+publicNetworkAccess: 'Disabled'
+// To:
+publicNetworkAccess: 'Enabled'
+
+// Also change:
+defaultAction: 'Deny'
+// To:
+defaultAction: 'Allow'
+```
+
+This makes the Foundry resource accessible from the internet (e.g., for portal-based development without VPN).
+
+#### To disable public access (default)
+
+Revert the changes above, setting `publicNetworkAccess: 'Disabled'` and `defaultAction: 'Deny'`.
+
+### Account Deletion Prerequisites and Cleanup Guidance
+
+Before deleting an **Account** resource, it is essential to first delete the associated **Account Capability Host**. Failure to do so may result in residual dependenciesβsuch as subnets and other provisioned resources (e.g., ACA applications)βremaining linked to the capability host. This can lead to errors such as **"Subnet already in use"** when attempting to reuse the same subnet in a different account deployment.
+
+**Cleanup Options**
+
+**1. Full Account Removal**: To completely remove an account, you must delete and purge the account. Simply deleting the account is not sufficient, you must purge so that deletion of the associated capability host is triggered. The service will automatically handle the removal of the capability host and any linked resources in the background. To purge the account, use the following [link](https://learn.microsoft.com/en-us/azure/ai-services/recover-purge-resources?tabs=azure-portal#purge-a-deleted-resource). Please allow approximately max of 20 minutes for all resources to be fully unlinked from the account.
+
+**2. Retain Account, Remove Capability Host**: If you intend to retain the account but remove the capability host, execute the script `deleteCapHost.sh` located in this folder. After deletion, allow approximately max of 20 minutes for all resources to be fully unlinked from the account. To recreate the capability host for the account, use the script `createCapHost.sh` located in the same folder.
+
+> **Important**: Before deleting the account capability host, ensure that the **project capability host** is deleted.
+
+### Template Customization
+
+Note: If not provided, the following resources will be created automatically for you:
+- VNet and three subnets (Agent, PE, MCP)
+- Azure Cosmos DB for NoSQL
+- Azure AI Search
+- Azure Storage
+
+#### Parameters
+
+> **β οΈ Important: Cosmos DB Connection Requirements**
+>
+> If you are creating the Cosmos DB connection manually (e.g., via REST API or ARM), ensure the following:
+> - The `authType` **must** be set to `AAD`. This is the only supported authentication type for the Cosmos DB connection used by the Agent Service.
+> - The `metadata` section **must** include the `ResourceId` property, set to the full Azure Resource ID of your Cosmos DB account. The Agent Service relies on this property to correctly identify and connect to your Cosmos DB resource. Omitting `ResourceId` from the metadata will cause the connection to fail.
+>
+> Example connection properties:
+> ```json
+> {
+> "category": "CosmosDB",
+> "authType": "AAD",
+> "metadata": {
+> "ApiType": "Azure",
+> "ResourceId": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/{cosmosDbAccountName}",
+> "location": "{region}"
+> }
+> }
+> ```
+
+| Parameter | Description | Default | Required |
+|-----------|-------------|---------|----------|
+| `location` | Azure region for deployment | `eastus2` | Yes |
+| `aiServices` | Base name for the AI Services resource (2-40 lowercase chars) | `contoso` | No |
+| `firstProjectName` | Name for the Foundry project | `project` | No |
+| `modelName` | Model to deploy | `gpt-4o-mini` | No |
+| `modelFormat` | Model provider | `OpenAI` | No |
+| `modelVersion` | Model version | `2024-07-18` | No |
+| `modelSkuName` | Model deployment SKU | `GlobalStandard` | No |
+| `modelCapacity` | Tokens per minute (TPM) capacity | `30` | No |
+| `vnetName` | Virtual Network name (ignored when `existingVnetResourceId` is set) | `agent-vnet` | No |
+| `agentSubnetName` | Agent subnet name (ignored when `existingAgentSubnetResourceId` is set) | `agent-subnet` | No |
+| `agentSubnetPrefix` | Address prefix for agent subnet | `''` (auto) | No |
+| `peSubnetName` | Private endpoint subnet name (ignored when `existingPeSubnetResourceId` is set) | `pe-subnet` | No |
+| `peSubnetPrefix` | Address prefix for PE subnet | `''` (auto) | No |
+| `mcpSubnetName` | MCP subnet name (ignored when `existingMcpSubnetResourceId` is set) | `mcp-subnet` | No |
+| `mcpSubnetPrefix` | Address prefix for MCP subnet | `''` (auto) | No |
+| `existingVnetResourceId` | Full ARM Resource ID of an existing VNet | `''` (creates new) | No |
+| `existingAgentSubnetResourceId` | Full ARM ID of an existing agent subnet to reuse as-is | `''` (creates) | No |
+| `existingPeSubnetResourceId` | Full ARM ID of an existing PE subnet to reuse as-is | `''` (creates) | No |
+| `existingMcpSubnetResourceId` | Full ARM ID of an existing MCP subnet to reuse as-is | `''` (creates) | No |
+| `vnetAddressPrefix` | Address space for new VNet | `''` | No |
+| `existingAiSearchResourceId` | ARM Resource ID of existing AI Search | `''` (creates new) | No |
+| `existingAzureStorageAccountResourceId` | ARM Resource ID of existing Storage account | `''` (creates new) | No |
+| `existingAzureCosmosDBAccountResourceId` | ARM Resource ID of existing Cosmos DB | `''` (creates new) | No |
+| `existingFabricWorkspaceResourceId` | ARM Resource ID of existing Fabric workspace | `''` | No |
+| `existingDnsZones` | Map of `'': { subscriptionId, resourceGroup }` β see [Use existing Private DNS zones](#5-use-existing-private-dns-zones-cross-rg--cross-subscription) | All `{ subscriptionId: '', resourceGroup: '' }` (creates new) | No |
+
+> **Naming change (May 2026):** `aiSearchResourceId`, `azureStorageAccountResourceId`, `azureCosmosDBAccountResourceId`, and `fabricWorkspaceResourceId` were renamed to `existingAiSearchResourceId`, `existingAzureStorageAccountResourceId`, `existingAzureCosmosDBAccountResourceId`, and `existingFabricWorkspaceResourceId` for consistency with the `existing*ResourceId` pattern used by VNet and subnet params. Update existing parameter files accordingly.
+
+#### BYO Resource Details
+
+1. **Use Existing Virtual Network and Subnets**
+
+To use an existing VNet and subnets, set the `existingVnetResourceId` parameter to the full ARM ID of the target VNet. Provide the names (or, recommended, the full ARM IDs) of the three subnets you want to use.
+
+There are two levels of "existing" support:
+
+* **Existing VNet, let the template manage subnets** β set only `existingVnetResourceId` and the per-subnet `*Name` / `*Prefix` params. The template will look up the VNet and create/update the three subnets inside it.
+* **Existing VNet AND existing subnets (recommended for shared / production VNets)** β also set `existingAgentSubnetResourceId`, `existingPeSubnetResourceId`, and/or `existingMcpSubnetResourceId` to the full ARM IDs of subnets you already have. When set, those subnets are referenced as-is and **not created or modified** β preserving their existing NSGs, route tables, and delegations.
+
+Example:
+
+```bicep
+param existingVnetResourceId = '/subscriptions//resourceGroups//providers/Microsoft.Network/virtualNetworks/'
+param existingAgentSubnetResourceId = '/subscriptions//resourceGroups//providers/Microsoft.Network/virtualNetworks//subnets/agent-subnet'
+param existingPeSubnetResourceId = '/subscriptions//resourceGroups//providers/Microsoft.Network/virtualNetworks//subnets/pe-subnet'
+param existingMcpSubnetResourceId = '/subscriptions//resourceGroups//providers/Microsoft.Network/virtualNetworks//subnets/mcp-subnet'
+param existingDnsZones = {
+ 'privatelink.services.ai.azure.com': { subscriptionId: '', resourceGroup: 'shared-dns-rg' } // existing zone, same sub, different RG
+ 'privatelink.openai.azure.com': { subscriptionId: '', resourceGroup: '' } // create a new zone in this deploymentβs RG
+ // ... etc
+}
+```
+
+π‘ **When to use which**:
+* If the VNet is yours and the subnets are empty/unused β just set `existingVnetResourceId`.
+* If the VNet is shared with other workloads β always set the `existing*SubnetResourceId` params too. Otherwise, the template will issue subnet PUTs that can fail with `AnotherOperationInProgress` or, worse, succeed and overwrite settings managed by another team.
+
+π‘ If subnets information is provided then make sure it exist within the specified VNet to avoid deployment errors. If subnet information is not provided, the template will create subnets with the default address space.
+
+
+2. **Use an existing Azure Cosmos DB for NoSQL**
+
+To use an existing Cosmos DB for NoSQL resource, set `existingAzureCosmosDBAccountResourceId` to the full Azure Resource ID of the target Cosmos DB.
+- `param existingAzureCosmosDBAccountResourceId = '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/{cosmosDbAccountName}'`
+
+
+3. **Use an existing Azure AI Search resource**
+
+To use an existing Azure AI Search resource, set `existingAiSearchResourceId` to the full ARM ID of the target search service.
+ - `param existingAiSearchResourceId = '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Search/searchServices/{searchServiceName}'`
+
+
+4. **Use an existing Azure Storage account**
+
+To use an existing Azure Storage account, set `existingAzureStorageAccountResourceId` to the full ARM ID of the target storage account.
+- `param existingAzureStorageAccountResourceId = '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{storageAccountName}'`
+
+5. **Use existing Private DNS zones (cross-RG / cross-subscription)**
+
+The `existingDnsZones` parameter controls, **per zone**, whether the template creates a new private DNS zone in this deploymentβs resource group or references an existing one (optionally in another resource group and/or subscription).
+
+Each map value is an object with two optional properties:
+
+| Property | Meaning |
+|---|---|
+| `resourceGroup` | RG holding the existing zone. **Empty `''` β create a new zone in this deploymentβs RG.** Non-empty β reference the existing zone in that RG. |
+| `subscriptionId` | Subscription holding the existing zone. Empty `''` defaults to the current subscription. Only used when `resourceGroup` is non-empty. |
+
+Three usage modes:
+
+```bicep
+param existingDnsZones = {
+ // (a) Create a new zone in this deployment's RG (default behavior)
+ 'privatelink.blob.core.windows.net': { subscriptionId: '', resourceGroup: '' }
+
+ // (b) Reference an existing zone in another RG, SAME subscription
+ 'privatelink.openai.azure.com': { subscriptionId: '', resourceGroup: 'shared-dns-rg' }
+
+ // (c) Reference an existing zone in another RG and ANOTHER subscription
+ 'privatelink.search.windows.net': { subscriptionId: '11111111-2222-3333-4444-555555555555', resourceGroup: 'hub-dns-rg' }
+}
+```
+
+> β οΈ **You must pre-link the VNet to any referenced zone.** When the template references an existing zone (modes b and c), it intentionally **does not create the `virtualNetworkLinks` resource** β the deployment identity may not have write rights in the zoneβs RG/subscription. Ensure the zone is already linked to the VNet hosting the private endpoints, otherwise name resolution will fail even though the deployment succeeds.
+
+> β οΈ **Cross-subscription RBAC.** The deployment principal needs `Private DNS Zone Contributor` (or at least `reader` + permission to write zone groups) on each referenced zoneβs scope. For mode (c), grant this in the target subscription.
+
+> π‘ **Migrating from the old string format.** Earlier versions accepted `'': ''`. Replace each value with `{ subscriptionId: '', resourceGroup: '' }` (or `{ subscriptionId: '', resourceGroup: '' }` to create new).
+
+---
+
+## Deploy the bicep template
+
+Choose your deployment method: Use the "Deploy to Azure" button from the provided README for a guided experience in Azure Portal
+
+**Option 1: Automatic deployment**
+Click the deploy to Azure button above to open the Azure portal and deploy the template directly.
+- Fill in the parameters as needed, including the existing VNet and subnets if applicable.
+
+
+**Option 2: Manually deploy the bicep template**
+- **Create a New (or Use Existing) Resource Group**
+
+ ```bash
+ az group create --name --location
+ ```
+- Deploy the main.bicep file
+ - Edit the main.bicepparams file to use an existing Virtual Network & subnets, Azure Cosmos DB, Azure Storage, and Azure AI Search.
+
+ ```bash
+ az deployment group create --resource-group --template-file main.bicep --parameters main.bicepparam
+ ```
+
+> **Note:** To access a private Foundry resource securely, use one of the following:
+> - A VM or jump box on the virtual network, optionally accessed through Azure Bastion
+> - Azure VPN Gateway
+> - Azure ExpressRoute
+
+If public network access is enabled, you can also access the Foundry resource directly from the internet for portal-based development.
+
+### Verify Deployment
+
+```bash
+# Check deployment status
+az deployment group show \
+ --resource-group "rg-hybrid-agent-test" \
+ --name "main" \
+ --query "properties.provisioningState"
+
+# List private endpoints (should see AI Search, Storage, Cosmos DB)
+az network private-endpoint list \
+ --resource-group "rg-hybrid-agent-test" \
+ --output table
+```
+
+### Cleanup
+
+To delete all resources created by this template:
+
+```bash
+az group delete --name --yes --no-wait
+```
+
+> **Important**: If you need to reuse the same subnet, follow the [Account Deletion Prerequisites and Cleanup Guidance](#account-deletion-prerequisites-and-cleanup-guidance) to properly purge the account and wait for the capability host to fully unlink (~20 minutes).
+
+---
+
+## Network Secured Agent Project Architecture Deep Dive
+
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Secure Access (VPN Gateway / ExpressRoute / Azure Bastion) β
+ββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
+ β
+ ββββββββββββββββΌβββββββββββββββ
+ β Microsoft Foundry β
+ β (publicNetworkAccess: β
+ β DISABLED) β
+ β β
+ β ββββββββββββββββββββββββββ β
+ β β Foundry Project β β
+ β β (Agent Workspace) β β
+ β βββββββββββββ¬βββββββββββββ β
+ ββββββββββββββββΌβββββββββββββββ
+ β Subnet Delegation
+ ββββββββββββββββΌβββββββββββββββ
+ β BYO Virtual Network β
+ β (192.168.0.0/16) β
+ β β
+ β ββββββββββββββββββββββββ β
+ β β Agent Subnet β β
+ β β (192.168.0.0/24) β β βββ Delegated to
+ β β Microsoft.App/envs β β Microsoft.App/environments
+ β ββββββββββββββββββββββββ β
+ β β
+ β ββββββββββββββββββββββββ β
+ β β PE Subnet β β
+ β β (192.168.1.0/24) β β
+ β β β β
+ β β ββββββββββ ββββββββββ β β
+ β β βStorage β βCosmos β β β βββ Private endpoints
+ β β ββββββββββ ββββββββββ β β (no public access)
+ β β ββββββββββ ββββββββββ β β
+ β β βSearch β βFoundry β β β
+ β β ββββββββββ ββββββββββ β β
+ β ββββββββββββββββββββββββ β
+ β β
+ β ββββββββββββββββββββββββ β
+ β β MCP Subnet β β
+ β β (192.168.2.0/24) β β
+ β β β β
+ β β ββββββββββ ββββββββββ β β
+ β β β MCP β βOpenAPI β β β βββ Tools behind VNet
+ β β βServers β β Tools β β β
+ β β ββββββββββ ββββββββββ β β
+ β β ββββββββββ ββββββββββ β β
+ β β βAzure β β A2A β β β
+ β β βFuncs β βAgents β β β
+ β β ββββββββββ ββββββββββ β β
+ β ββββββββββββββββββββββββ β
+ ββββββββββββββββββββββββββββββββ
+```
+
+> **Tip:** For detailed layer-by-layer deployment diagrams, see the `diagrams/` folder.
+
+### Core Components
+
+**Microsoft Foundry** resource
+- Central orchestration point
+- Manages service connections
+- Set networking and policy configurations
+
+**Foundry** project
+- Defines the workspace configuration
+- Service integration
+- Agents are created within a specific project, and each project acts as an isolated workspace. This means:
+ - All agents in the same project share access to the same file storage, thread storage (conversation history), and search indexes.
+ - Data is isolated between projects. Agents in one project cannot access resources from another. Projects are currently the unit of sharing and isolation in Foundry. See the what is AI foundry article for more information on Foundry projects.
+
+**Bring Your Own (BYO) Azure Resources**: ensures all sensitive data remains under customer control. All agents created using our service are stateful, meaning they retain information across interactions. With this setup, agent states are automatically stored in customer-managed, single-tenant resources. The required Bring Your Own Resources include:
+- BYO File Storage: All files uploaded by developers (during agent configuration) or end-users (during interactions) are stored directly in the customer's Azure Storage account.
+- BYO Search: All vector stores created by the agent leverage the customer's Azure AI Search resource.
+- BYO Thread Storage: All customer messages and conversation history will be stored in the customer's own Azure Cosmos DB account.
+
+By bundling these BYO features (file storage, search, and thread storage), the standard setup guarantees that your deployment is secure by default. All data processed by Microsoft Foundry Agent Service is automatically stored at rest in your own Azure resources, helping you meet internal policies, compliance requirements, and enterprise security standards.
+
+### Azure Resources Created
+
+Microsoft Foundry (Cognitive Services)
+- Type: Microsoft.CognitiveServices/accounts
+- API version: 2025-04-01-preview
+- Kind: AIServices
+- SKU: S0
+- Identity: System-assigned
+- Features:
+ - Custom subdomain name
+ - Disabled public network access
+ - Network ACLs with Azure Services bypass
+
+AI Model Deployment
+- Type: Microsoft.CognitiveServices/accounts/deployments
+- API version: 2025-04-01-preview
+- SKU: Based on modelSkuName parameter, capacity set by modelCapacity
+- Model properties:
+ - Name: From modelName parameter
+ - Format: From modelFormat parameter
+ - Version: From modelVersion parameter
+
+Azure AI Search
+- Type: Microsoft.Search/searchServices
+- API version: 2024-06-01-preview
+- SKU: standard
+- Partition Count: 1
+- Replica Count: 1
+- Hosting Mode: default
+- Semantic Search: disabled
+- Features:
+ - Disabled public network access
+ - AAD auth with HTTP 401 challenge
+ - System-assigned managed identity
+
+Storage Account
+- Type: Microsoft.Storage/storageAccounts
+- API version: 2023-05-0
+- Kind: StorageV2
+- SKU: ZRS or GRS (region dependent; use Standard_GRS if ZRS not available)
+- Features:
+ - Blob service, Queue service (if Azure Function Tool supported)
+ - Minimum TLS Version: 1.2
+ - Block public blob access
+ - Disabled public network access
+ - Force Azure AD authentication (SharedKey access disabled)
+
+Cosmos DB Account
+- Type: Microsoft.DocumentDB/databaseAccounts
+- API version: 2024-11-15
+- Kind: GlobalDocumentDB (SQL API)
+- Consistency Level: Session
+- Database Account Offer Type: Standard
+- Features:
+ - Disabled public network access
+ - Disabled local auth
+ - Single region deployment
+
+### Network Security Design
+This implementation utilizes a BYO VNet (Bring Your Own Virtual Network) approach, also known as custom VNet support with subnet delegation. Within your existing virtual network, delegated subnets will be created.
+
+Network Security
+- Public network access disabled
+- Private endpoints for all services
+- Network ACLs with deny by default
+
+**Network Infrastructure**
+- A Virtual Network (192.168.0.0/16) is created (if existing isn't passed in)
+- Agent Subnet (192.168.0.0/24): Hosts Agent client
+- Private endpoint Subnet (192.168.1.0/24): Hosts private endpoints
+- MCP Subnet (192.168.2.0/24): Hosts MCP servers, OpenAPI tools, Azure Functions, and A2A agents
+
+**Private Endpoints**
+Private endpoints ensure secure, internal-only connectivity. Private endpoints are created for the following:
+- Microsoft Foundry
+- Azure AI Search
+- Azure Storage
+- Azure Cosmos DB
+
+**Private DNS Zones**
+| Private Link Resource Type | Sub Resource | Private DNS Zone Name | Public DNS Zone Forwarders |
+|----------------------------|--------------|------------------------|-----------------------------|
+| **Microsoft Foundry** | account | `privatelink.cognitiveservices.azure.com`
`privatelink.openai.azure.com`
`privatelink.services.ai.azure.com` | `cognitiveservices.azure.com`
`openai.azure.com`
`services.ai.azure.com` |
+| **Azure AI Search** | searchService| `privatelink.search.windows.net` | `search.windows.net` |
+| **Azure Cosmos DB** | Sql | `privatelink.documents.azure.com` | `documents.azure.com` |
+| **Azure Storage** | blob | `privatelink.blob.core.windows.net` | `blob.core.windows.net` |
+
+### Authentication & Authorization
+
+- **Managed Identity**
+ - Zero-trust security model
+ - No credential storage
+ - Platform-managed rotation
+
+ This template uses System Managed Identity, but User Assigned Managed Identity is also supported.
+
+- **Role Assignments**
+ - **Azure AI Search**
+ - Search Index Data Contributor (`8ebe5a00-799e-43f5-93ac-243d3dce84a7`)
+ - Search Service Contributor (`7ca78c08-252a-4471-8644-bb5ff32d4ba0`)
+ - **Azure Storage Account**
+ - Storage Blob Data Owner (`b7e6dc6d-f1e8-4753-8033-0f276bb0955b`)
+ - Storage Queue Data Contributor (`974c5e8b-45b9-4653-ba55-5f855dd0fb88`) (if Azure Function tool enabled)
+ - Two containers will automatically be provisioned during the project create capability host process:
+ - Azure Blob Storage Container: `-azureml-blobstore`
+ - Storage Blob Data Contributor
+ - Azure Blob Storage Container: `-agents-blobstore`
+ - Storage Blob Data Owner
+ - **Cosmos DB for NoSQL**
+ - Cosmos DB Operator (`230815da-be43-4aae-9cb4-875f7bd000aa`)
+ - Cosmos DB Built-in Data Contributor
+ - Three containers will automatically be provisioned during the create capability host process:
+ - Cosmos DB for NoSQL container: `<${projectWorkspaceId}>-thread-message-store`
+ - Cosmos DB for NoSQL container: `<${projectWorkspaceId}>-system-thread-message-store`
+ - Cosmos DB for NoSQL container: `<${projectWorkspaceId}>-agent-entity-store`
+
+---
+
+## Connecting to a Private Foundry Resource
+
+When public network access is disabled (the default), you need a secure connection to reach the Foundry resource. Azure provides three methods:
+
+1. **Azure VPN Gateway** β Connect from your local network to the Azure VNet over an encrypted tunnel.
+2. **Azure ExpressRoute** β Use a private, dedicated connection from your on-premises infrastructure to Azure.
+3. **Azure Bastion** β Use a jump box VM on the VNet, accessed securely through the Azure portal.
+
+For detailed setup instructions, see: [Securely connect to Azure AI Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link?view=foundry#securely-connect-to-foundry).
+
+---
+
+## Testing Agents with Private Resources
+
+### Option 1: Portal Testing
+
+If the Foundry resource has **public network access enabled**, you can test directly in the portal:
+
+1. Navigate to [Azure AI Foundry portal](https://ai.azure.com)
+2. Select your project
+3. Create an agent with AI Search tool
+4. Test that the agent can query the private AI Search index
+
+If the Foundry resource has **public network access disabled** (default), you need to connect via VPN Gateway, ExpressRoute, or Azure Bastion before accessing the portal. See [Connecting to a Private Foundry Resource](#connecting-to-a-private-foundry-resource).
+
+### Option 2: SDK Testing
+
+See [tests/TESTING-GUIDE.md](tests/TESTING-GUIDE.md) for detailed SDK testing instructions.
+
+---
+
+## MCP Server Deployment
+
+To deploy MCP servers on the private VNet:
+
+```bash
+# Create Container Apps environment on mcp-subnet
+az containerapp env create \
+ --resource-group "rg-hybrid-agent-test" \
+ --name "mcp-env" \
+ --location "westus2" \
+ --infrastructure-subnet-resource-id "" \
+ --internal-only true
+
+# Deploy MCP server
+az containerapp create \
+ --resource-group "rg-hybrid-agent-test" \
+ --name "my-mcp-server" \
+ --environment "mcp-env" \
+ --image "" \
+ --target-port 8080 \
+ --ingress external \
+ --min-replicas 1
+```
+
+Then configure private DNS zone for Container Apps (see TESTING-GUIDE.md Step 6.3).
+
+---
+
+## Module Structure
+
+```text
+modules-network-secured/
+βββ add-project-capability-host.bicep # Configuring the project's capability host
+βββ ai-account-identity.bicep # Microsoft Foundry deployment and configuration
+βββ ai-project-identity.bicep # Foundry project deployment and connection configuration
+βββ ai-project-identity-unique.bicep # Modified project module with unique connection names
+βββ ai-search-role-assignments.bicep # AI Search RBAC configuration
+βββ azure-storage-account-role-assignment.bicep # Storage Account RBAC configuration
+βββ blob-storage-container-role-assignments.bicep # Blob Storage Container RBAC configuration
+βββ blob-storage-container-role-assignments-unique.bicep # Modified storage role assignment module
+βββ cosmos-container-role-assignments.bicep # CosmosDB container Account RBAC configuration
+βββ cosmosdb-account-role-assignment.bicep # CosmosDB Account RBAC configuration
+βββ existing-vnet.bicep # Bring your existing virtual network to template deployment
+βββ format-project-workspace-id.bicep # Formatting the project workspace ID
+βββ network-agent-vnet.bicep # Logic for routing virtual network set-up if existing virtual network is selected
+βββ private-endpoint-and-dns.bicep # Creating virtual networks and DNS zones.
+βββ standard-dependent-resources.bicep # Deploying CosmosDB, Storage, and Search
+βββ subnet.bicep # Setting the subnet for Agent network injection
+βββ validate-existing-resources.bicep # Validate existing CosmosDB, Storage, and Search to template deployment
+βββ vnet.bicep # Deploying a new virtual network
+```
+
+## Maintenance
+
+### Regular Tasks
+
+1. Review role assignments
+2. Monitor network security
+3. Check service health
+4. Update configurations as needed
+
+### Troubleshooting
+
+1. Verify private endpoint connectivity
+2. Check DNS resolution
+3. Validate role assignments
+4. Review network security groups
+
+---
+
+## References
+
+- [Microsoft Foundry Networking Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link?tabs=azure-portal&pivots=fdp-project)
+- [Microsoft Foundry RBAC Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/rbac-azure-ai-foundry?pivots=fdp-project)
+- [Private Endpoint Documentation](https://learn.microsoft.com/en-us/azure/private-link/)
+- [RBAC Documentation](https://learn.microsoft.com/en-us/azure/role-based-access-control/)
+- [Network Security Best Practices](https://learn.microsoft.com/en-us/azure/security/fundamentals/network-best-practices)
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/a2a-server/Dockerfile b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/a2a-server/Dockerfile
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/a2a-server/Dockerfile
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/a2a-server/Dockerfile
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/a2a-server/main.py b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/a2a-server/main.py
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/a2a-server/main.py
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/a2a-server/main.py
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/a2a-server/requirements.txt b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/a2a-server/requirements.txt
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/a2a-server/requirements.txt
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/a2a-server/requirements.txt
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/add-project.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/add-project.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/add-project.bicep
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/add-project.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/add-project.bicepparam b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/add-project.bicepparam
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/add-project.bicepparam
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/add-project.bicepparam
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/azure-function-server/README.md b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/README.md
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/azure-function-server/README.md
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/README.md
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/azure-function-server/calculator_openapi.json b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/calculator_openapi.json
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/azure-function-server/calculator_openapi.json
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/calculator_openapi.json
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/azure-function-server/deploy-function.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/deploy-function.bicep
similarity index 96%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/azure-function-server/deploy-function.bicep
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/deploy-function.bicep
index af6a75f1c..82b8052e5 100644
--- a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/azure-function-server/deploy-function.bicep
+++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/deploy-function.bicep
@@ -165,14 +165,26 @@ resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
pythonVersion: '3.11'
linuxFxVersion: 'PYTHON|3.11'
appSettings: [
- { name: 'FUNCTIONS_WORKER_RUNTIME'; value: 'python' }
- { name: 'FUNCTIONS_EXTENSION_VERSION'; value: '~4' }
+ {
+ name: 'FUNCTIONS_WORKER_RUNTIME'
+ value: 'python'
+ }
+ {
+ name: 'FUNCTIONS_EXTENSION_VERSION'
+ value: '~4'
+ }
{
name: 'AzureWebJobsStorage'
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value}'
}
- { name: 'WEBSITE_CONTENTOVERVNET'; value: '1' }
- { name: 'WEBSITE_VNET_ROUTE_ALL'; value: '1' }
+ {
+ name: 'WEBSITE_CONTENTOVERVNET'
+ value: '1'
+ }
+ {
+ name: 'WEBSITE_VNET_ROUTE_ALL'
+ value: '1'
+ }
]
}
}
diff --git a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/deploy-function.json b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/deploy-function.json
new file mode 100644
index 000000000..68e59cbb3
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/deploy-function.json
@@ -0,0 +1,424 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.40.2.10011",
+ "templateHash": "16381039446523157932"
+ }
+ },
+ "parameters": {
+ "location": {
+ "type": "string",
+ "defaultValue": "[resourceGroup().location]",
+ "metadata": {
+ "description": "Azure region for all resources"
+ }
+ },
+ "vnetName": {
+ "type": "string",
+ "metadata": {
+ "description": "Name of the existing VNet"
+ }
+ },
+ "integrationSubnetName": {
+ "type": "string",
+ "defaultValue": "func-integration-subnet",
+ "metadata": {
+ "description": "Name of an existing or new subnet for the Function App VNet Integration"
+ }
+ },
+ "integrationSubnetPrefix": {
+ "type": "string",
+ "defaultValue": "192.168.5.0/24",
+ "metadata": {
+ "description": "Address prefix for the integration subnet (if creating new)"
+ }
+ },
+ "privateEndpointSubnetName": {
+ "type": "string",
+ "metadata": {
+ "description": "Name of the subnet where Private Endpoints will be created"
+ }
+ },
+ "baseName": {
+ "type": "string",
+ "defaultValue": "[format('functest{0}', uniqueString(resourceGroup().id))]",
+ "metadata": {
+ "description": "Base name for the Function App resources"
+ }
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Network/virtualNetworks/subnets",
+ "apiVersion": "2024-01-01",
+ "name": "[format('{0}/{1}', parameters('vnetName'), parameters('integrationSubnetName'))]",
+ "properties": {
+ "addressPrefix": "[parameters('integrationSubnetPrefix')]",
+ "delegations": [
+ {
+ "name": "Microsoft.Web.serverFarms",
+ "properties": {
+ "serviceName": "Microsoft.Web/serverFarms"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "type": "Microsoft.Storage/storageAccounts",
+ "apiVersion": "2023-05-01",
+ "name": "[format('{0}stor', take(parameters('baseName'), 20))]",
+ "location": "[parameters('location')]",
+ "sku": {
+ "name": "Standard_LRS"
+ },
+ "kind": "StorageV2",
+ "properties": {
+ "supportsHttpsTrafficOnly": true,
+ "allowBlobPublicAccess": false,
+ "publicNetworkAccess": "Enabled",
+ "networkAcls": {
+ "defaultAction": "Allow"
+ }
+ }
+ },
+ {
+ "type": "Microsoft.Network/privateEndpoints",
+ "apiVersion": "2024-01-01",
+ "name": "[format('{0}-blob-pe', parameters('baseName'))]",
+ "location": "[parameters('location')]",
+ "properties": {
+ "subnet": {
+ "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('privateEndpointSubnetName'))]"
+ },
+ "privateLinkServiceConnections": [
+ {
+ "name": "blob",
+ "properties": {
+ "privateLinkServiceId": "[resourceId('Microsoft.Storage/storageAccounts', format('{0}stor', take(parameters('baseName'), 20)))]",
+ "groupIds": [
+ "blob"
+ ]
+ }
+ }
+ ]
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Storage/storageAccounts', format('{0}stor', take(parameters('baseName'), 20)))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Network/privateEndpoints",
+ "apiVersion": "2024-01-01",
+ "name": "[format('{0}-queue-pe', parameters('baseName'))]",
+ "location": "[parameters('location')]",
+ "properties": {
+ "subnet": {
+ "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('privateEndpointSubnetName'))]"
+ },
+ "privateLinkServiceConnections": [
+ {
+ "name": "queue",
+ "properties": {
+ "privateLinkServiceId": "[resourceId('Microsoft.Storage/storageAccounts', format('{0}stor', take(parameters('baseName'), 20)))]",
+ "groupIds": [
+ "queue"
+ ]
+ }
+ }
+ ]
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Storage/storageAccounts', format('{0}stor', take(parameters('baseName'), 20)))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Network/privateEndpoints",
+ "apiVersion": "2024-01-01",
+ "name": "[format('{0}-file-pe', parameters('baseName'))]",
+ "location": "[parameters('location')]",
+ "properties": {
+ "subnet": {
+ "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('privateEndpointSubnetName'))]"
+ },
+ "privateLinkServiceConnections": [
+ {
+ "name": "file",
+ "properties": {
+ "privateLinkServiceId": "[resourceId('Microsoft.Storage/storageAccounts', format('{0}stor', take(parameters('baseName'), 20)))]",
+ "groupIds": [
+ "file"
+ ]
+ }
+ }
+ ]
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Storage/storageAccounts', format('{0}stor', take(parameters('baseName'), 20)))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Web/serverfarms",
+ "apiVersion": "2023-12-01",
+ "name": "[format('{0}-plan', parameters('baseName'))]",
+ "location": "[parameters('location')]",
+ "sku": {
+ "name": "EP1",
+ "tier": "ElasticPremium"
+ },
+ "kind": "elastic",
+ "properties": {
+ "reserved": true
+ }
+ },
+ {
+ "type": "Microsoft.Web/sites",
+ "apiVersion": "2023-12-01",
+ "name": "[format('{0}-func', parameters('baseName'))]",
+ "location": "[parameters('location')]",
+ "kind": "functionapp,linux",
+ "properties": {
+ "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', format('{0}-plan', parameters('baseName')))]",
+ "publicNetworkAccess": "Enabled",
+ "virtualNetworkSubnetId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('integrationSubnetName'))]",
+ "siteConfig": {
+ "pythonVersion": "3.11",
+ "linuxFxVersion": "PYTHON|3.11",
+ "appSettings": [
+ {
+ "name": "FUNCTIONS_WORKER_RUNTIME",
+ "value": "python"
+ },
+ {
+ "name": "FUNCTIONS_EXTENSION_VERSION",
+ "value": "~4"
+ },
+ {
+ "name": "AzureWebJobsStorage",
+ "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', format('{0}stor', take(parameters('baseName'), 20)), listKeys(resourceId('Microsoft.Storage/storageAccounts', format('{0}stor', take(parameters('baseName'), 20))), '2023-05-01').keys[0].value)]"
+ },
+ {
+ "name": "WEBSITE_CONTENTOVERVNET",
+ "value": "1"
+ },
+ {
+ "name": "WEBSITE_VNET_ROUTE_ALL",
+ "value": "1"
+ }
+ ]
+ }
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Web/serverfarms', format('{0}-plan', parameters('baseName')))]",
+ "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('integrationSubnetName'))]",
+ "[resourceId('Microsoft.Storage/storageAccounts', format('{0}stor', take(parameters('baseName'), 20)))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Network/privateEndpoints",
+ "apiVersion": "2024-01-01",
+ "name": "[format('{0}-func-pe', parameters('baseName'))]",
+ "location": "[parameters('location')]",
+ "properties": {
+ "subnet": {
+ "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('privateEndpointSubnetName'))]"
+ },
+ "privateLinkServiceConnections": [
+ {
+ "name": "sites",
+ "properties": {
+ "privateLinkServiceId": "[resourceId('Microsoft.Web/sites', format('{0}-func', parameters('baseName')))]",
+ "groupIds": [
+ "sites"
+ ]
+ }
+ }
+ ]
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Web/sites', format('{0}-func', parameters('baseName')))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Network/privateDnsZones",
+ "apiVersion": "2024-06-01",
+ "name": "privatelink.azurewebsites.net",
+ "location": "global"
+ },
+ {
+ "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
+ "apiVersion": "2024-06-01",
+ "name": "[format('{0}/{1}', 'privatelink.azurewebsites.net', format('{0}-link', parameters('vnetName')))]",
+ "location": "global",
+ "properties": {
+ "virtualNetwork": {
+ "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
+ },
+ "registrationEnabled": false
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.azurewebsites.net')]"
+ ]
+ },
+ {
+ "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups",
+ "apiVersion": "2024-01-01",
+ "name": "[format('{0}/{1}', format('{0}-func-pe', parameters('baseName')), 'default')]",
+ "properties": {
+ "privateDnsZoneConfigs": [
+ {
+ "name": "privatelink-azurewebsites-net",
+ "properties": {
+ "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.azurewebsites.net')]"
+ }
+ }
+ ]
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-func-pe', parameters('baseName')))]",
+ "[resourceId('Microsoft.Network/privateDnsZones', 'privatelink.azurewebsites.net')]"
+ ]
+ },
+ {
+ "type": "Microsoft.Network/privateDnsZones",
+ "apiVersion": "2024-06-01",
+ "name": "[format('privatelink.blob.{0}', environment().suffixes.storage)]",
+ "location": "global"
+ },
+ {
+ "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
+ "apiVersion": "2024-06-01",
+ "name": "[format('{0}/{1}', format('privatelink.blob.{0}', environment().suffixes.storage), format('{0}-blob-link', parameters('vnetName')))]",
+ "location": "global",
+ "properties": {
+ "virtualNetwork": {
+ "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
+ },
+ "registrationEnabled": false
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.blob.{0}', environment().suffixes.storage))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups",
+ "apiVersion": "2024-01-01",
+ "name": "[format('{0}/{1}', format('{0}-blob-pe', parameters('baseName')), 'default')]",
+ "properties": {
+ "privateDnsZoneConfigs": [
+ {
+ "name": "privatelink-blob",
+ "properties": {
+ "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.blob.{0}', environment().suffixes.storage))]"
+ }
+ }
+ ]
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.blob.{0}', environment().suffixes.storage))]",
+ "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-blob-pe', parameters('baseName')))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Network/privateDnsZones",
+ "apiVersion": "2024-06-01",
+ "name": "[format('privatelink.queue.{0}', environment().suffixes.storage)]",
+ "location": "global"
+ },
+ {
+ "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
+ "apiVersion": "2024-06-01",
+ "name": "[format('{0}/{1}', format('privatelink.queue.{0}', environment().suffixes.storage), format('{0}-queue-link', parameters('vnetName')))]",
+ "location": "global",
+ "properties": {
+ "virtualNetwork": {
+ "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
+ },
+ "registrationEnabled": false
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.queue.{0}', environment().suffixes.storage))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups",
+ "apiVersion": "2024-01-01",
+ "name": "[format('{0}/{1}', format('{0}-queue-pe', parameters('baseName')), 'default')]",
+ "properties": {
+ "privateDnsZoneConfigs": [
+ {
+ "name": "privatelink-queue",
+ "properties": {
+ "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.queue.{0}', environment().suffixes.storage))]"
+ }
+ }
+ ]
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.queue.{0}', environment().suffixes.storage))]",
+ "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-queue-pe', parameters('baseName')))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Network/privateDnsZones",
+ "apiVersion": "2024-06-01",
+ "name": "[format('privatelink.file.{0}', environment().suffixes.storage)]",
+ "location": "global"
+ },
+ {
+ "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
+ "apiVersion": "2024-06-01",
+ "name": "[format('{0}/{1}', format('privatelink.file.{0}', environment().suffixes.storage), format('{0}-file-link', parameters('vnetName')))]",
+ "location": "global",
+ "properties": {
+ "virtualNetwork": {
+ "id": "[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
+ },
+ "registrationEnabled": false
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.file.{0}', environment().suffixes.storage))]"
+ ]
+ },
+ {
+ "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups",
+ "apiVersion": "2024-01-01",
+ "name": "[format('{0}/{1}', format('{0}-file-pe', parameters('baseName')), 'default')]",
+ "properties": {
+ "privateDnsZoneConfigs": [
+ {
+ "name": "privatelink-file",
+ "properties": {
+ "privateDnsZoneId": "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.file.{0}', environment().suffixes.storage))]"
+ }
+ }
+ ]
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Network/privateDnsZones', format('privatelink.file.{0}', environment().suffixes.storage))]",
+ "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-file-pe', parameters('baseName')))]"
+ ]
+ }
+ ],
+ "outputs": {
+ "functionAppName": {
+ "type": "string",
+ "value": "[format('{0}-func', parameters('baseName'))]"
+ },
+ "functionAppHostname": {
+ "type": "string",
+ "value": "[reference(resourceId('Microsoft.Web/sites', format('{0}-func', parameters('baseName'))), '2023-12-01').defaultHostName]"
+ },
+ "functionPrivateEndpointId": {
+ "type": "string",
+ "value": "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-func-pe', parameters('baseName')))]"
+ },
+ "functionAppResourceId": {
+ "type": "string",
+ "value": "[resourceId('Microsoft.Web/sites', format('{0}-func', parameters('baseName')))]"
+ }
+ }
+}
\ No newline at end of file
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/azure-function-server/function_app.py b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/function_app.py
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/azure-function-server/function_app.py
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/function_app.py
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/azure-function-server/host.json b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/host.json
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/azure-function-server/host.json
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/host.json
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/azure-function-server/local.settings.json b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/local.settings.json
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/azure-function-server/local.settings.json
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/local.settings.json
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/azure-function-server/requirements.txt b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/requirements.txt
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/azure-function-server/requirements.txt
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/requirements.txt
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/azuredeploy.json b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azuredeploy.json
similarity index 80%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/azuredeploy.json
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azuredeploy.json
index 746a1b64a..12575125b 100644
--- a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/azuredeploy.json
+++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azuredeploy.json
@@ -4,8 +4,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.41.2.15936",
- "templateHash": "4618599826878163398"
+ "version": "0.43.8.12551",
+ "templateHash": "12812848625846814930"
}
},
"parameters": {
@@ -44,9 +44,11 @@
},
"aiServices": {
"type": "string",
- "defaultValue": "aiservices",
+ "defaultValue": "aifoundry",
+ "minLength": 2,
+ "maxLength": 40,
"metadata": {
- "description": "Name for your AI Services resource."
+ "description": "Name prefix for your AI Services (Cognitive Services) resource. Lowercase alphanumeric only; a 4-character random suffix will be appended."
}
},
"modelName": {
@@ -91,8 +93,10 @@
"firstProjectName": {
"type": "string",
"defaultValue": "project",
+ "minLength": 2,
+ "maxLength": 40,
"metadata": {
- "description": "Name for your project resource."
+ "description": "Name prefix for your Foundry project. A 4-character random suffix will be appended."
}
},
"projectDescription": {
@@ -111,30 +115,30 @@
},
"vnetName": {
"type": "string",
- "defaultValue": "agent-vnet-test",
+ "defaultValue": "agent-vnet",
"metadata": {
- "description": "Virtual Network name for the Agent to create new or existing virtual network"
+ "description": "Virtual Network name. When `existingVnetResourceId` is set, this is ignored β the name is derived from the resource ID."
}
},
"agentSubnetName": {
"type": "string",
"defaultValue": "agent-subnet",
"metadata": {
- "description": "The name of Agents Subnet to create new or existing subnet for agents"
+ "description": "Agent subnet name. Ignored when `existingAgentSubnetResourceId` is set."
}
},
"peSubnetName": {
"type": "string",
"defaultValue": "pe-subnet",
"metadata": {
- "description": "The name of Private Endpoint subnet to create new or existing subnet for private endpoints"
+ "description": "Private endpoint subnet name. Ignored when `existingPeSubnetResourceId` is set."
}
},
"mcpSubnetName": {
"type": "string",
"defaultValue": "mcp-subnet",
"metadata": {
- "description": "The name of MCP subnet for user-deployed Container Apps (e.g., MCP servers)"
+ "description": "MCP subnet name (hosts user-deployed Container Apps such as MCP servers). Ignored when `existingMcpSubnetResourceId` is set."
}
},
"existingVnetResourceId": {
@@ -172,62 +176,89 @@
"description": "Address prefix for the MCP subnet. The default value is 192.168.2.0/24."
}
},
- "aiSearchResourceId": {
+ "existingAgentSubnetResourceId": {
"type": "string",
"defaultValue": "",
"metadata": {
- "description": "The AI Search Service full ARM Resource ID. This is an optional field, and if not provided, the resource will be created."
+ "description": "Optional ARM Resource ID of an existing agent subnet. If provided, the subnet will be referenced as-is and will not be created/modified."
}
},
- "azureStorageAccountResourceId": {
+ "existingPeSubnetResourceId": {
"type": "string",
"defaultValue": "",
"metadata": {
- "description": "The AI Storage Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created."
+ "description": "Optional ARM Resource ID of an existing private endpoint subnet. If provided, the subnet will be referenced as-is and will not be created/modified."
}
},
- "azureCosmosDBAccountResourceId": {
+ "existingMcpSubnetResourceId": {
"type": "string",
"defaultValue": "",
"metadata": {
- "description": "The Cosmos DB Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created."
+ "description": "Optional ARM Resource ID of an existing MCP subnet. If provided, the subnet will be referenced as-is and will not be created/modified."
}
},
- "fabricWorkspaceResourceId": {
+ "existingAiSearchResourceId": {
"type": "string",
"defaultValue": "",
"metadata": {
- "description": "The Microsoft Fabric Workspace full ARM Resource ID. This is an optional field for Fabric private link connectivity."
+ "description": "The AI Search Service full ARM Resource ID. Optional β leave empty to create a new one."
+ }
+ },
+ "existingAzureStorageAccountResourceId": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "The AI Storage Account full ARM Resource ID. Optional β leave empty to create a new one."
+ }
+ },
+ "existingAzureCosmosDBAccountResourceId": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "The Cosmos DB Account full ARM Resource ID. Optional β leave empty to create a new one."
+ }
+ },
+ "existingFabricWorkspaceResourceId": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "The Microsoft Fabric Workspace full ARM Resource ID. Optional β enables Fabric private link connectivity."
}
},
"existingDnsZones": {
"type": "object",
"defaultValue": {
- "privatelink.services.ai.azure.com": "",
- "privatelink.openai.azure.com": "",
- "privatelink.cognitiveservices.azure.com": "",
- "privatelink.search.windows.net": "",
- "privatelink.blob.core.windows.net": "",
- "privatelink.documents.azure.com": "",
- "privatelink.analysis.windows.net": ""
+ "privatelink.services.ai.azure.com": {
+ "subscriptionId": "",
+ "resourceGroup": ""
+ },
+ "privatelink.openai.azure.com": {
+ "subscriptionId": "",
+ "resourceGroup": ""
+ },
+ "privatelink.cognitiveservices.azure.com": {
+ "subscriptionId": "",
+ "resourceGroup": ""
+ },
+ "privatelink.search.windows.net": {
+ "subscriptionId": "",
+ "resourceGroup": ""
+ },
+ "[format('privatelink.blob.{0}', environment().suffixes.storage)]": {
+ "subscriptionId": "",
+ "resourceGroup": ""
+ },
+ "privatelink.documents.azure.com": {
+ "subscriptionId": "",
+ "resourceGroup": ""
+ },
+ "privatelink.fabric.microsoft.com": {
+ "subscriptionId": "",
+ "resourceGroup": ""
+ }
},
"metadata": {
- "description": "Object mapping DNS zone names to their resource group, or empty string to indicate creation"
- }
- },
- "dnsZoneNames": {
- "type": "array",
- "defaultValue": [
- "privatelink.services.ai.azure.com",
- "privatelink.openai.azure.com",
- "privatelink.cognitiveservices.azure.com",
- "privatelink.search.windows.net",
- "privatelink.blob.core.windows.net",
- "privatelink.documents.azure.com",
- "privatelink.analysis.windows.net"
- ],
- "metadata": {
- "description": "Zone Names for Validation of existing Private Dns Zones"
+ "description": "Map of private DNS zone FQDNs to an object `{ subscriptionId, resourceGroup }` describing where the zone lives. Empty `resourceGroup` means \"create the zone in this deployment's resource group\". A non-empty `resourceGroup` references an existing zone in that RG; empty `subscriptionId` defaults to the current subscription, otherwise the zone is referenced cross-subscription. Note: when referencing an existing zone, the VNet link to that zone is NOT managed by this template β the caller must ensure the zone is already linked to the target VNet."
}
},
"projectCapHost": {
@@ -245,24 +276,33 @@
"cosmosDBName": "[toLower(format('{0}{1}cosmosdb', parameters('aiServices'), variables('uniqueSuffix')))]",
"aiSearchName": "[toLower(format('{0}{1}search', parameters('aiServices'), variables('uniqueSuffix')))]",
"azureStorageName": "[toLower(format('{0}{1}storage', parameters('aiServices'), variables('uniqueSuffix')))]",
- "storagePassedIn": "[not(equals(parameters('azureStorageAccountResourceId'), ''))]",
- "searchPassedIn": "[not(equals(parameters('aiSearchResourceId'), ''))]",
- "cosmosPassedIn": "[not(equals(parameters('azureCosmosDBAccountResourceId'), ''))]",
+ "storagePassedIn": "[not(equals(parameters('existingAzureStorageAccountResourceId'), ''))]",
+ "searchPassedIn": "[not(equals(parameters('existingAiSearchResourceId'), ''))]",
+ "cosmosPassedIn": "[not(equals(parameters('existingAzureCosmosDBAccountResourceId'), ''))]",
"existingVnetPassedIn": "[not(equals(parameters('existingVnetResourceId'), ''))]",
- "acsParts": "[split(parameters('aiSearchResourceId'), '/')]",
+ "agentSubnetExists": "[not(equals(parameters('existingAgentSubnetResourceId'), ''))]",
+ "peSubnetExists": "[not(equals(parameters('existingPeSubnetResourceId'), ''))]",
+ "mcpSubnetExists": "[not(equals(parameters('existingMcpSubnetResourceId'), ''))]",
+ "effectiveAgentSubnetName": "[if(variables('agentSubnetExists'), last(split(parameters('existingAgentSubnetResourceId'), '/')), parameters('agentSubnetName'))]",
+ "effectivePeSubnetName": "[if(variables('peSubnetExists'), last(split(parameters('existingPeSubnetResourceId'), '/')), parameters('peSubnetName'))]",
+ "effectiveMcpSubnetName": "[if(variables('mcpSubnetExists'), last(split(parameters('existingMcpSubnetResourceId'), '/')), parameters('mcpSubnetName'))]",
+ "acsParts": "[split(parameters('existingAiSearchResourceId'), '/')]",
"aiSearchServiceSubscriptionId": "[if(variables('searchPassedIn'), variables('acsParts')[2], subscription().subscriptionId)]",
"aiSearchServiceResourceGroupName": "[if(variables('searchPassedIn'), variables('acsParts')[4], resourceGroup().name)]",
- "cosmosParts": "[split(parameters('azureCosmosDBAccountResourceId'), '/')]",
+ "cosmosParts": "[split(parameters('existingAzureCosmosDBAccountResourceId'), '/')]",
"cosmosDBSubscriptionId": "[if(variables('cosmosPassedIn'), variables('cosmosParts')[2], subscription().subscriptionId)]",
"cosmosDBResourceGroupName": "[if(variables('cosmosPassedIn'), variables('cosmosParts')[4], resourceGroup().name)]",
- "storageParts": "[split(parameters('azureStorageAccountResourceId'), '/')]",
+ "storageParts": "[split(parameters('existingAzureStorageAccountResourceId'), '/')]",
"azureStorageSubscriptionId": "[if(variables('storagePassedIn'), variables('storageParts')[2], subscription().subscriptionId)]",
"azureStorageResourceGroupName": "[if(variables('storagePassedIn'), variables('storageParts')[4], resourceGroup().name)]",
"vnetParts": "[split(parameters('existingVnetResourceId'), '/')]",
"vnetSubscriptionId": "[if(variables('existingVnetPassedIn'), variables('vnetParts')[2], subscription().subscriptionId)]",
"vnetResourceGroupName": "[if(variables('existingVnetPassedIn'), variables('vnetParts')[4], resourceGroup().name)]",
"existingVnetName": "[if(variables('existingVnetPassedIn'), last(variables('vnetParts')), parameters('vnetName'))]",
- "trimVnetName": "[trim(variables('existingVnetName'))]"
+ "trimVnetName": "[trim(variables('existingVnetName'))]",
+ "aiSearchExists": "[not(equals(parameters('existingAiSearchResourceId'), ''))]",
+ "azureStorageExists": "[not(equals(parameters('existingAzureStorageAccountResourceId'), ''))]",
+ "cosmosDBExists": "[not(equals(parameters('existingAzureCosmosDBAccountResourceId'), ''))]"
},
"resources": [
{
@@ -288,13 +328,13 @@
"value": "[variables('vnetResourceGroupName')]"
},
"agentSubnetName": {
- "value": "[parameters('agentSubnetName')]"
+ "value": "[variables('effectiveAgentSubnetName')]"
},
"peSubnetName": {
- "value": "[parameters('peSubnetName')]"
+ "value": "[variables('effectivePeSubnetName')]"
},
"mcpSubnetName": {
- "value": "[parameters('mcpSubnetName')]"
+ "value": "[variables('effectiveMcpSubnetName')]"
},
"vnetAddressPrefix": {
"value": "[parameters('vnetAddressPrefix')]"
@@ -310,6 +350,15 @@
},
"existingVnetSubscriptionId": {
"value": "[variables('vnetSubscriptionId')]"
+ },
+ "agentSubnetExists": {
+ "value": "[variables('agentSubnetExists')]"
+ },
+ "peSubnetExists": {
+ "value": "[variables('peSubnetExists')]"
+ },
+ "mcpSubnetExists": {
+ "value": "[variables('mcpSubnetExists')]"
}
},
"template": {
@@ -318,8 +367,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.41.2.15936",
- "templateHash": "7580422666690415547"
+ "version": "0.43.8.12551",
+ "templateHash": "3367717386889798767"
}
},
"parameters": {
@@ -404,6 +453,27 @@
"metadata": {
"description": "Address prefix for the MCP subnet"
}
+ },
+ "agentSubnetExists": {
+ "type": "bool",
+ "defaultValue": false,
+ "metadata": {
+ "description": "Set to true to reference the agent subnet as existing (do not create or modify it)"
+ }
+ },
+ "peSubnetExists": {
+ "type": "bool",
+ "defaultValue": false,
+ "metadata": {
+ "description": "Set to true to reference the private endpoint subnet as existing (do not create or modify it)"
+ }
+ },
+ "mcpSubnetExists": {
+ "type": "bool",
+ "defaultValue": false,
+ "metadata": {
+ "description": "Set to true to reference the MCP subnet as existing (do not create or modify it)"
+ }
}
},
"resources": [
@@ -452,8 +522,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.41.2.15936",
- "templateHash": "14420765200836708228"
+ "version": "0.43.8.12551",
+ "templateHash": "3231703172659113421"
}
},
"parameters": {
@@ -660,6 +730,15 @@
},
"mcpSubnetPrefix": {
"value": "[parameters('mcpSubnetPrefix')]"
+ },
+ "agentSubnetExists": {
+ "value": "[parameters('agentSubnetExists')]"
+ },
+ "peSubnetExists": {
+ "value": "[parameters('peSubnetExists')]"
+ },
+ "mcpSubnetExists": {
+ "value": "[parameters('mcpSubnetExists')]"
}
},
"template": {
@@ -668,8 +747,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.41.2.15936",
- "templateHash": "15142627104032952494"
+ "version": "0.43.8.12551",
+ "templateHash": "8635420737989534099"
}
},
"parameters": {
@@ -734,6 +813,27 @@
"metadata": {
"description": "Address prefix for the MCP subnet (only needed if creating new subnet)"
}
+ },
+ "agentSubnetExists": {
+ "type": "bool",
+ "defaultValue": false,
+ "metadata": {
+ "description": "Set to true to reference the agent subnet as existing (do not create or modify it)"
+ }
+ },
+ "peSubnetExists": {
+ "type": "bool",
+ "defaultValue": false,
+ "metadata": {
+ "description": "Set to true to reference the private endpoint subnet as existing (do not create or modify it)"
+ }
+ },
+ "mcpSubnetExists": {
+ "type": "bool",
+ "defaultValue": false,
+ "metadata": {
+ "description": "Set to true to reference the MCP subnet as existing (do not create or modify it)"
+ }
}
},
"resources": [
@@ -755,6 +855,9 @@
"value": "[parameters('agentSubnetName')]"
},
"addressPrefix": "[if(empty(parameters('agentSubnetPrefix')), createObject('value', cidrSubnet(reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), '2024-05-01').addressSpace.addressPrefixes[0], 24, 0)), createObject('value', parameters('agentSubnetPrefix')))]",
+ "subnetExists": {
+ "value": "[parameters('agentSubnetExists')]"
+ },
"delegations": {
"value": [
{
@@ -772,8 +875,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.41.2.15936",
- "templateHash": "3458409006502413839"
+ "version": "0.43.8.12551",
+ "templateHash": "10805545016086603042"
}
},
"parameters": {
@@ -791,8 +894,9 @@
},
"addressPrefix": {
"type": "string",
+ "defaultValue": "",
"metadata": {
- "description": "Address prefix for the subnet"
+ "description": "Address prefix for the subnet (only required when creating a new subnet)"
}
},
"delegations": {
@@ -801,10 +905,18 @@
"metadata": {
"description": "Array of subnet delegations"
}
+ },
+ "subnetExists": {
+ "type": "bool",
+ "defaultValue": false,
+ "metadata": {
+ "description": "Set to true to reference an existing subnet instead of creating one"
+ }
}
},
"resources": [
{
+ "condition": "[not(parameters('subnetExists'))]",
"type": "Microsoft.Network/virtualNetworks/subnets",
"apiVersion": "2024-05-01",
"name": "[format('{0}/{1}', parameters('vnetName'), parameters('subnetName'))]",
@@ -817,7 +929,7 @@
"outputs": {
"subnetId": {
"type": "string",
- "value": "[resourceId('Microsoft.Network/virtualNetworks/subnets', split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[0], split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[1])]"
+ "value": "[if(parameters('subnetExists'), resourceId('Microsoft.Network/virtualNetworks/subnets', split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[0], split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[1]), resourceId('Microsoft.Network/virtualNetworks/subnets', split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[0], split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[1]))]"
},
"subnetName": {
"type": "string",
@@ -845,6 +957,9 @@
"value": "[parameters('peSubnetName')]"
},
"addressPrefix": "[if(empty(parameters('peSubnetPrefix')), createObject('value', cidrSubnet(reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), '2024-05-01').addressSpace.addressPrefixes[0], 24, 1)), createObject('value', parameters('peSubnetPrefix')))]",
+ "subnetExists": {
+ "value": "[parameters('peSubnetExists')]"
+ },
"delegations": {
"value": []
}
@@ -855,8 +970,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.41.2.15936",
- "templateHash": "3458409006502413839"
+ "version": "0.43.8.12551",
+ "templateHash": "10805545016086603042"
}
},
"parameters": {
@@ -874,8 +989,9 @@
},
"addressPrefix": {
"type": "string",
+ "defaultValue": "",
"metadata": {
- "description": "Address prefix for the subnet"
+ "description": "Address prefix for the subnet (only required when creating a new subnet)"
}
},
"delegations": {
@@ -884,10 +1000,18 @@
"metadata": {
"description": "Array of subnet delegations"
}
+ },
+ "subnetExists": {
+ "type": "bool",
+ "defaultValue": false,
+ "metadata": {
+ "description": "Set to true to reference an existing subnet instead of creating one"
+ }
}
},
"resources": [
{
+ "condition": "[not(parameters('subnetExists'))]",
"type": "Microsoft.Network/virtualNetworks/subnets",
"apiVersion": "2024-05-01",
"name": "[format('{0}/{1}', parameters('vnetName'), parameters('subnetName'))]",
@@ -900,7 +1024,7 @@
"outputs": {
"subnetId": {
"type": "string",
- "value": "[resourceId('Microsoft.Network/virtualNetworks/subnets', split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[0], split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[1])]"
+ "value": "[if(parameters('subnetExists'), resourceId('Microsoft.Network/virtualNetworks/subnets', split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[0], split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[1]), resourceId('Microsoft.Network/virtualNetworks/subnets', split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[0], split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[1]))]"
},
"subnetName": {
"type": "string",
@@ -908,7 +1032,10 @@
}
}
}
- }
+ },
+ "dependsOn": [
+ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Resources/deployments', format('agent-subnet-{0}', uniqueString(deployment().name, parameters('agentSubnetName'))))]"
+ ]
},
{
"type": "Microsoft.Resources/deployments",
@@ -928,6 +1055,9 @@
"value": "[parameters('mcpSubnetName')]"
},
"addressPrefix": "[if(empty(parameters('mcpSubnetPrefix')), createObject('value', cidrSubnet(reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), '2024-05-01').addressSpace.addressPrefixes[0], 24, 2)), createObject('value', parameters('mcpSubnetPrefix')))]",
+ "subnetExists": {
+ "value": "[parameters('mcpSubnetExists')]"
+ },
"delegations": {
"value": [
{
@@ -945,8 +1075,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.41.2.15936",
- "templateHash": "3458409006502413839"
+ "version": "0.43.8.12551",
+ "templateHash": "10805545016086603042"
}
},
"parameters": {
@@ -964,8 +1094,9 @@
},
"addressPrefix": {
"type": "string",
+ "defaultValue": "",
"metadata": {
- "description": "Address prefix for the subnet"
+ "description": "Address prefix for the subnet (only required when creating a new subnet)"
}
},
"delegations": {
@@ -974,10 +1105,18 @@
"metadata": {
"description": "Array of subnet delegations"
}
+ },
+ "subnetExists": {
+ "type": "bool",
+ "defaultValue": false,
+ "metadata": {
+ "description": "Set to true to reference an existing subnet instead of creating one"
+ }
}
},
"resources": [
{
+ "condition": "[not(parameters('subnetExists'))]",
"type": "Microsoft.Network/virtualNetworks/subnets",
"apiVersion": "2024-05-01",
"name": "[format('{0}/{1}', parameters('vnetName'), parameters('subnetName'))]",
@@ -990,7 +1129,7 @@
"outputs": {
"subnetId": {
"type": "string",
- "value": "[resourceId('Microsoft.Network/virtualNetworks/subnets', split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[0], split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[1])]"
+ "value": "[if(parameters('subnetExists'), resourceId('Microsoft.Network/virtualNetworks/subnets', split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[0], split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[1]), resourceId('Microsoft.Network/virtualNetworks/subnets', split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[0], split(format('{0}/{1}', parameters('vnetName'), parameters('subnetName')), '/')[1]))]"
},
"subnetName": {
"type": "string",
@@ -998,7 +1137,10 @@
}
}
}
- }
+ },
+ "dependsOn": [
+ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Resources/deployments', format('pe-subnet-{0}', uniqueString(deployment().name, parameters('peSubnetName'))))]"
+ ]
}
],
"outputs": {
@@ -1016,15 +1158,15 @@
},
"agentSubnetId": {
"type": "string",
- "value": "[format('{0}/subnets/{1}', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('agentSubnetName'))]"
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Resources/deployments', format('agent-subnet-{0}', uniqueString(deployment().name, parameters('agentSubnetName')))), '2025-04-01').outputs.subnetId.value]"
},
"peSubnetId": {
"type": "string",
- "value": "[format('{0}/subnets/{1}', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('peSubnetName'))]"
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Resources/deployments', format('pe-subnet-{0}', uniqueString(deployment().name, parameters('peSubnetName')))), '2025-04-01').outputs.subnetId.value]"
},
"mcpSubnetId": {
"type": "string",
- "value": "[format('{0}/subnets/{1}', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName')), parameters('mcpSubnetName'))]"
+ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('vnetResourceGroupName')), 'Microsoft.Resources/deployments', format('mcp-subnet-{0}', uniqueString(deployment().name, parameters('mcpSubnetName')))), '2025-04-01').outputs.subnetId.value]"
},
"virtualNetworkName": {
"type": "string",
@@ -1133,8 +1275,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.41.2.15936",
- "templateHash": "5075008770391657650"
+ "version": "0.43.8.12551",
+ "templateHash": "1400943577065869287"
}
},
"parameters": {
@@ -1238,143 +1380,6 @@
"[resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix')))]"
]
},
- {
- "type": "Microsoft.Resources/deployments",
- "apiVersion": "2025-04-01",
- "name": "[format('validate-existing-resources-{0}-deployment', variables('uniqueSuffix'))]",
- "properties": {
- "expressionEvaluationOptions": {
- "scope": "inner"
- },
- "mode": "Incremental",
- "parameters": {
- "aiSearchResourceId": {
- "value": "[parameters('aiSearchResourceId')]"
- },
- "azureStorageAccountResourceId": {
- "value": "[parameters('azureStorageAccountResourceId')]"
- },
- "azureCosmosDBAccountResourceId": {
- "value": "[parameters('azureCosmosDBAccountResourceId')]"
- },
- "existingDnsZones": {
- "value": "[parameters('existingDnsZones')]"
- },
- "dnsZoneNames": {
- "value": "[parameters('dnsZoneNames')]"
- }
- },
- "template": {
- "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
- "contentVersion": "1.0.0.0",
- "metadata": {
- "_generator": {
- "name": "bicep",
- "version": "0.41.2.15936",
- "templateHash": "16089088048916219322"
- }
- },
- "parameters": {
- "aiSearchResourceId": {
- "type": "string",
- "metadata": {
- "description": "Resource ID of the AI Search Service."
- }
- },
- "azureStorageAccountResourceId": {
- "type": "string",
- "metadata": {
- "description": "Resource ID of the Azure Storage Account."
- }
- },
- "azureCosmosDBAccountResourceId": {
- "type": "string",
- "metadata": {
- "description": "ResourceId of Cosmos DB Account"
- }
- },
- "existingDnsZones": {
- "type": "object",
- "metadata": {
- "description": "Object mapping DNS zone names to their resource group, or empty string to indicate creation"
- }
- },
- "dnsZoneNames": {
- "type": "array",
- "metadata": {
- "description": "List of private DNS zone names to validate"
- }
- }
- },
- "variables": {
- "storagePassedIn": "[not(equals(parameters('azureStorageAccountResourceId'), ''))]",
- "searchPassedIn": "[not(equals(parameters('aiSearchResourceId'), ''))]",
- "cosmosPassedIn": "[not(equals(parameters('azureCosmosDBAccountResourceId'), ''))]",
- "storageParts": "[split(parameters('azureStorageAccountResourceId'), '/')]",
- "azureStorageSubscriptionId": "[if(and(variables('storagePassedIn'), greater(length(variables('storageParts')), 2)), variables('storageParts')[2], subscription().subscriptionId)]",
- "azureStorageResourceGroupName": "[if(and(variables('storagePassedIn'), greater(length(variables('storageParts')), 4)), variables('storageParts')[4], resourceGroup().name)]",
- "acsParts": "[split(parameters('aiSearchResourceId'), '/')]",
- "aiSearchServiceSubscriptionId": "[if(and(variables('searchPassedIn'), greater(length(variables('acsParts')), 2)), variables('acsParts')[2], subscription().subscriptionId)]",
- "aiSearchServiceResourceGroupName": "[if(and(variables('searchPassedIn'), greater(length(variables('acsParts')), 4)), variables('acsParts')[4], resourceGroup().name)]",
- "cosmosParts": "[split(parameters('azureCosmosDBAccountResourceId'), '/')]",
- "cosmosDBSubscriptionId": "[if(and(variables('cosmosPassedIn'), greater(length(variables('cosmosParts')), 2)), variables('cosmosParts')[2], subscription().subscriptionId)]",
- "cosmosDBResourceGroupName": "[if(and(variables('cosmosPassedIn'), greater(length(variables('cosmosParts')), 4)), variables('cosmosParts')[4], resourceGroup().name)]",
- "dnsZoneTypes": [
- "Microsoft.Network/privateDnsZones"
- ]
- },
- "resources": [],
- "outputs": {
- "aiSearchExists": {
- "type": "bool",
- "value": "[and(variables('searchPassedIn'), equals(last(split(parameters('aiSearchResourceId'), '/')), variables('acsParts')[8]))]"
- },
- "cosmosDBExists": {
- "type": "bool",
- "value": "[and(variables('cosmosPassedIn'), equals(last(split(parameters('azureCosmosDBAccountResourceId'), '/')), variables('cosmosParts')[8]))]"
- },
- "azureStorageExists": {
- "type": "bool",
- "value": "[and(variables('storagePassedIn'), equals(last(split(parameters('azureStorageAccountResourceId'), '/')), variables('storageParts')[8]))]"
- },
- "aiSearchServiceSubscriptionId": {
- "type": "string",
- "value": "[variables('aiSearchServiceSubscriptionId')]"
- },
- "aiSearchServiceResourceGroupName": {
- "type": "string",
- "value": "[variables('aiSearchServiceResourceGroupName')]"
- },
- "cosmosDBSubscriptionId": {
- "type": "string",
- "value": "[variables('cosmosDBSubscriptionId')]"
- },
- "cosmosDBResourceGroupName": {
- "type": "string",
- "value": "[variables('cosmosDBResourceGroupName')]"
- },
- "azureStorageSubscriptionId": {
- "type": "string",
- "value": "[variables('azureStorageSubscriptionId')]"
- },
- "azureStorageResourceGroupName": {
- "type": "string",
- "value": "[variables('azureStorageResourceGroupName')]"
- },
- "dnsZoneExists": {
- "type": "array",
- "copy": {
- "count": "[length(parameters('dnsZoneNames'))]",
- "input": {
- "name": "[parameters('dnsZoneNames')[copyIndex()]]",
- "exists": "[not(empty(parameters('existingDnsZones')[parameters('dnsZoneNames')[copyIndex()]]))]"
- }
- }
- }
- }
- }
- }
- },
{
"type": "Microsoft.Resources/deployments",
"apiVersion": "2025-04-01",
@@ -1397,23 +1402,23 @@
"cosmosDBName": {
"value": "[variables('cosmosDBName')]"
},
- "aiSearchResourceId": {
- "value": "[parameters('aiSearchResourceId')]"
+ "existingAiSearchResourceId": {
+ "value": "[parameters('existingAiSearchResourceId')]"
},
"aiSearchExists": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('validate-existing-resources-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.aiSearchExists.value]"
+ "value": "[variables('aiSearchExists')]"
},
- "azureStorageAccountResourceId": {
- "value": "[parameters('azureStorageAccountResourceId')]"
+ "existingAzureStorageAccountResourceId": {
+ "value": "[parameters('existingAzureStorageAccountResourceId')]"
},
"azureStorageExists": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('validate-existing-resources-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.azureStorageExists.value]"
+ "value": "[variables('azureStorageExists')]"
},
- "cosmosDBResourceId": {
- "value": "[parameters('azureCosmosDBAccountResourceId')]"
+ "existingCosmosDBResourceId": {
+ "value": "[parameters('existingAzureCosmosDBAccountResourceId')]"
},
"cosmosDBExists": {
- "value": "[reference(resourceId('Microsoft.Resources/deployments', format('validate-existing-resources-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.cosmosDBExists.value]"
+ "value": "[variables('cosmosDBExists')]"
}
},
"template": {
@@ -1422,8 +1427,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.41.2.15936",
- "templateHash": "14445641712219672614"
+ "version": "0.43.8.12551",
+ "templateHash": "6113332263495979803"
}
},
"parameters": {
@@ -1451,22 +1456,22 @@
"description": "Name of the new Cosmos DB account"
}
},
- "aiSearchResourceId": {
+ "existingAiSearchResourceId": {
"type": "string",
"metadata": {
- "description": "The AI Search Service full ARM Resource ID. This is an optional field, and if not provided, the resource will be created."
+ "description": "The AI Search Service full ARM Resource ID. Optional β leave empty to create a new one."
}
},
- "azureStorageAccountResourceId": {
+ "existingAzureStorageAccountResourceId": {
"type": "string",
"metadata": {
- "description": "The AI Storage Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created."
+ "description": "The AI Storage Account full ARM Resource ID. Optional β leave empty to create a new one."
}
},
- "cosmosDBResourceId": {
+ "existingCosmosDBResourceId": {
"type": "string",
"metadata": {
- "description": "The Cosmos DB Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created."
+ "description": "The Cosmos DB Account full ARM Resource ID. Optional β leave empty to create a new one."
}
},
"aiSearchExists": {
@@ -1491,14 +1496,14 @@
}
},
"variables": {
- "cosmosParts": "[split(parameters('cosmosDBResourceId'), '/')]",
+ "cosmosParts": "[split(parameters('existingCosmosDBResourceId'), '/')]",
"canaryRegions": [
"eastus2euap",
"centraluseuap"
],
"cosmosDbRegion": "[if(contains(variables('canaryRegions'), parameters('location')), 'westus', parameters('location'))]",
- "acsParts": "[split(parameters('aiSearchResourceId'), '/')]",
- "azureStorageParts": "[split(parameters('azureStorageAccountResourceId'), '/')]"
+ "acsParts": "[split(parameters('existingAiSearchResourceId'), '/')]",
+ "azureStorageParts": "[split(parameters('existingAzureStorageAccountResourceId'), '/')]"
},
"resources": [
{
@@ -1632,10 +1637,7 @@
}
}
}
- },
- "dependsOn": [
- "[resourceId('Microsoft.Resources/deployments', format('validate-existing-resources-{0}-deployment', variables('uniqueSuffix')))]"
- ]
+ }
},
{
"type": "Microsoft.Resources/deployments",
@@ -1660,7 +1662,7 @@
"value": "[reference(resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix'))), '2025-04-01').outputs.cosmosDBName.value]"
},
"fabricWorkspaceResourceId": {
- "value": "[parameters('fabricWorkspaceResourceId')]"
+ "value": "[parameters('existingFabricWorkspaceResourceId')]"
},
"vnetName": {
"value": "[reference(resourceId('Microsoft.Resources/deployments', format('vnet-{0}-{1}-deployment', variables('trimVnetName'), variables('uniqueSuffix'))), '2025-04-01').outputs.virtualNetworkName.value]"
@@ -1705,8 +1707,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.41.2.15936",
- "templateHash": "4936875384661892557"
+ "version": "0.43.8.12551",
+ "templateHash": "9883330111104552489"
}
},
"parameters": {
@@ -1818,16 +1820,37 @@
"existingDnsZones": {
"type": "object",
"defaultValue": {
- "privatelink.services.ai.azure.com": "",
- "privatelink.openai.azure.com": "",
- "privatelink.cognitiveservices.azure.com": "",
- "privatelink.search.windows.net": "",
- "[format('privatelink.blob.{0}', environment().suffixes.storage)]": "",
- "privatelink.documents.azure.com": "",
- "privatelink.fabric.microsoft.com": ""
+ "privatelink.services.ai.azure.com": {
+ "subscriptionId": "",
+ "resourceGroup": ""
+ },
+ "privatelink.openai.azure.com": {
+ "subscriptionId": "",
+ "resourceGroup": ""
+ },
+ "privatelink.cognitiveservices.azure.com": {
+ "subscriptionId": "",
+ "resourceGroup": ""
+ },
+ "privatelink.search.windows.net": {
+ "subscriptionId": "",
+ "resourceGroup": ""
+ },
+ "[format('privatelink.blob.{0}', environment().suffixes.storage)]": {
+ "subscriptionId": "",
+ "resourceGroup": ""
+ },
+ "privatelink.documents.azure.com": {
+ "subscriptionId": "",
+ "resourceGroup": ""
+ },
+ "privatelink.fabric.microsoft.com": {
+ "subscriptionId": "",
+ "resourceGroup": ""
+ }
},
"metadata": {
- "description": "Map of DNS zone FQDNs to resource group names. If provided, reference existing DNS zones in this resource group instead of creating them."
+ "description": "Map of DNS zone FQDNs to an object describing where the zone lives. Each value must be an object with optional `subscriptionId` and `resourceGroup` properties. Empty `resourceGroup` means \"create the zone in this deployment's resource group\". Non-empty `resourceGroup` references an existing zone; empty `subscriptionId` defaults to the current subscription."
}
}
},
@@ -1842,20 +1865,8 @@
"storageDnsZoneName": "[format('privatelink.blob.{0}', environment().suffixes.storage)]",
"cosmosDBDnsZoneName": "privatelink.documents.azure.com",
"fabricDnsZoneName": "privatelink.fabric.microsoft.com",
- "aiServicesDnsZoneRG": "[parameters('existingDnsZones')[variables('aiServicesDnsZoneName')]]",
- "openAiDnsZoneRG": "[parameters('existingDnsZones')[variables('openAiDnsZoneName')]]",
- "cognitiveServicesDnsZoneRG": "[parameters('existingDnsZones')[variables('cognitiveServicesDnsZoneName')]]",
- "aiSearchDnsZoneRG": "[parameters('existingDnsZones')[variables('aiSearchDnsZoneName')]]",
- "storageDnsZoneRG": "[parameters('existingDnsZones')[variables('storageDnsZoneName')]]",
- "cosmosDBDnsZoneRG": "[parameters('existingDnsZones')[variables('cosmosDBDnsZoneName')]]",
- "fabricDnsZoneRG": "[coalesce(tryGet(parameters('existingDnsZones'), 'fabricDnsZoneName'), '')]",
- "aiServicesDnsZoneId": "[if(empty(variables('aiServicesDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('aiServicesDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('aiServicesDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('aiServicesDnsZoneName')))]",
- "openAiDnsZoneId": "[if(empty(variables('openAiDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('openAiDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('openAiDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('openAiDnsZoneName')))]",
- "cognitiveServicesDnsZoneId": "[if(empty(variables('cognitiveServicesDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('cognitiveServicesDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('cognitiveServicesDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('cognitiveServicesDnsZoneName')))]",
- "aiSearchDnsZoneId": "[if(empty(variables('aiSearchDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('aiSearchDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('aiSearchDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('aiSearchDnsZoneName')))]",
- "storageDnsZoneId": "[if(empty(variables('storageDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('storageDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('storageDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('storageDnsZoneName')))]",
- "cosmosDBDnsZoneId": "[if(empty(variables('cosmosDBDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('cosmosDBDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('cosmosDBDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('cosmosDBDnsZoneName')))]",
- "fabricDnsZoneId": "[if(variables('fabricPassedIn'), if(empty(variables('fabricDnsZoneRG')), resourceId('Microsoft.Network/privateDnsZones', variables('fabricDnsZoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('fabricDnsZoneRG')), 'Microsoft.Network/privateDnsZones', variables('fabricDnsZoneName'))), '')]"
+ "dnsZoneEntries": "[items(parameters('existingDnsZones'))]",
+ "dnsZoneKeys": "[map(variables('dnsZoneEntries'), lambda('e', lambdaVariables('e').key))]"
},
"resources": [
{
@@ -1969,167 +1980,6 @@
]
}
},
- {
- "condition": "[empty(variables('aiServicesDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones",
- "apiVersion": "2020-06-01",
- "name": "[variables('aiServicesDnsZoneName')]",
- "location": "global"
- },
- {
- "condition": "[empty(variables('openAiDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones",
- "apiVersion": "2020-06-01",
- "name": "[variables('openAiDnsZoneName')]",
- "location": "global"
- },
- {
- "condition": "[empty(variables('cognitiveServicesDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones",
- "apiVersion": "2020-06-01",
- "name": "[variables('cognitiveServicesDnsZoneName')]",
- "location": "global"
- },
- {
- "condition": "[empty(variables('aiSearchDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones",
- "apiVersion": "2020-06-01",
- "name": "[variables('aiSearchDnsZoneName')]",
- "location": "global"
- },
- {
- "condition": "[empty(variables('storageDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones",
- "apiVersion": "2020-06-01",
- "name": "[variables('storageDnsZoneName')]",
- "location": "global"
- },
- {
- "condition": "[empty(variables('cosmosDBDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones",
- "apiVersion": "2020-06-01",
- "name": "[variables('cosmosDBDnsZoneName')]",
- "location": "global"
- },
- {
- "condition": "[and(variables('fabricPassedIn'), empty(variables('fabricDnsZoneRG')))]",
- "type": "Microsoft.Network/privateDnsZones",
- "apiVersion": "2020-06-01",
- "name": "[variables('fabricDnsZoneName')]",
- "location": "global"
- },
- {
- "condition": "[empty(variables('aiServicesDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
- "apiVersion": "2024-06-01",
- "name": "[format('{0}/{1}', variables('aiServicesDnsZoneName'), format('aiServices-{0}-link', parameters('suffix')))]",
- "location": "global",
- "properties": {
- "virtualNetwork": {
- "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
- },
- "registrationEnabled": false
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones', variables('aiServicesDnsZoneName'))]"
- ]
- },
- {
- "condition": "[empty(variables('openAiDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
- "apiVersion": "2024-06-01",
- "name": "[format('{0}/{1}', variables('openAiDnsZoneName'), format('aiServicesOpenAI-{0}-link', parameters('suffix')))]",
- "location": "global",
- "properties": {
- "virtualNetwork": {
- "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
- },
- "registrationEnabled": false
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones', variables('openAiDnsZoneName'))]"
- ]
- },
- {
- "condition": "[empty(variables('cognitiveServicesDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
- "apiVersion": "2024-06-01",
- "name": "[format('{0}/{1}', variables('cognitiveServicesDnsZoneName'), format('aiServicesCognitiveServices-{0}-link', parameters('suffix')))]",
- "location": "global",
- "properties": {
- "virtualNetwork": {
- "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
- },
- "registrationEnabled": false
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones', variables('cognitiveServicesDnsZoneName'))]"
- ]
- },
- {
- "condition": "[empty(variables('aiSearchDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
- "apiVersion": "2024-06-01",
- "name": "[format('{0}/{1}', variables('aiSearchDnsZoneName'), format('aiSearch-{0}-link', parameters('suffix')))]",
- "location": "global",
- "properties": {
- "virtualNetwork": {
- "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
- },
- "registrationEnabled": false
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones', variables('aiSearchDnsZoneName'))]"
- ]
- },
- {
- "condition": "[empty(variables('storageDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
- "apiVersion": "2024-06-01",
- "name": "[format('{0}/{1}', variables('storageDnsZoneName'), format('storage-{0}-link', parameters('suffix')))]",
- "location": "global",
- "properties": {
- "virtualNetwork": {
- "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
- },
- "registrationEnabled": false
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones', variables('storageDnsZoneName'))]"
- ]
- },
- {
- "condition": "[empty(variables('cosmosDBDnsZoneRG'))]",
- "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
- "apiVersion": "2024-06-01",
- "name": "[format('{0}/{1}', variables('cosmosDBDnsZoneName'), format('cosmosDB-{0}-link', parameters('suffix')))]",
- "location": "global",
- "properties": {
- "virtualNetwork": {
- "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
- },
- "registrationEnabled": false
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones', variables('cosmosDBDnsZoneName'))]"
- ]
- },
- {
- "condition": "[and(variables('fabricPassedIn'), empty(variables('fabricDnsZoneRG')))]",
- "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
- "apiVersion": "2024-06-01",
- "name": "[format('{0}/{1}', variables('fabricDnsZoneName'), format('fabric-{0}-link', parameters('suffix')))]",
- "location": "global",
- "properties": {
- "virtualNetwork": {
- "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
- },
- "registrationEnabled": false
- },
- "dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones', variables('fabricDnsZoneName'))]"
- ]
- },
{
"type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups",
"apiVersion": "2024-05-01",
@@ -2139,31 +1989,28 @@
{
"name": "[format('{0}-dns-aiserv-config', parameters('aiAccountName'))]",
"properties": {
- "privateDnsZoneId": "[variables('aiServicesDnsZoneId')]"
+ "privateDnsZoneId": "[reference(resourceId('Microsoft.Resources/deployments', format('dns-{0}-{1}', replace(variables('dnsZoneEntries')[indexOf(variables('dnsZoneKeys'), variables('aiServicesDnsZoneName'))].key, '.', '-'), parameters('suffix'))), '2025-04-01').outputs.zoneId.value]"
}
},
{
"name": "[format('{0}-dns-openai-config', parameters('aiAccountName'))]",
"properties": {
- "privateDnsZoneId": "[variables('openAiDnsZoneId')]"
+ "privateDnsZoneId": "[reference(resourceId('Microsoft.Resources/deployments', format('dns-{0}-{1}', replace(variables('dnsZoneEntries')[indexOf(variables('dnsZoneKeys'), variables('openAiDnsZoneName'))].key, '.', '-'), parameters('suffix'))), '2025-04-01').outputs.zoneId.value]"
}
},
{
"name": "[format('{0}-dns-cogserv-config', parameters('aiAccountName'))]",
"properties": {
- "privateDnsZoneId": "[variables('cognitiveServicesDnsZoneId')]"
+ "privateDnsZoneId": "[reference(resourceId('Microsoft.Resources/deployments', format('dns-{0}-{1}', replace(variables('dnsZoneEntries')[indexOf(variables('dnsZoneKeys'), variables('cognitiveServicesDnsZoneName'))].key, '.', '-'), parameters('suffix'))), '2025-04-01').outputs.zoneId.value]"
}
}
]
},
"dependsOn": [
"[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('aiAccountName')))]",
- "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('aiServicesDnsZoneName'), format('aiServices-{0}-link', parameters('suffix')))]",
- "[resourceId('Microsoft.Network/privateDnsZones', variables('aiServicesDnsZoneName'))]",
- "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('cognitiveServicesDnsZoneName'), format('aiServicesCognitiveServices-{0}-link', parameters('suffix')))]",
- "[resourceId('Microsoft.Network/privateDnsZones', variables('cognitiveServicesDnsZoneName'))]",
- "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('openAiDnsZoneName'), format('aiServicesOpenAI-{0}-link', parameters('suffix')))]",
- "[resourceId('Microsoft.Network/privateDnsZones', variables('openAiDnsZoneName'))]"
+ "[resourceId('Microsoft.Resources/deployments', format('dns-{0}-{1}', replace(variables('dnsZoneEntries')[indexOf(variables('dnsZoneKeys'), variables('aiServicesDnsZoneName'))].key, '.', '-'), parameters('suffix')))]",
+ "[resourceId('Microsoft.Resources/deployments', format('dns-{0}-{1}', replace(variables('dnsZoneEntries')[indexOf(variables('dnsZoneKeys'), variables('openAiDnsZoneName'))].key, '.', '-'), parameters('suffix')))]",
+ "[resourceId('Microsoft.Resources/deployments', format('dns-{0}-{1}', replace(variables('dnsZoneEntries')[indexOf(variables('dnsZoneKeys'), variables('cognitiveServicesDnsZoneName'))].key, '.', '-'), parameters('suffix')))]"
]
},
{
@@ -2175,15 +2022,14 @@
{
"name": "[format('{0}-dns-config', parameters('aiSearchName'))]",
"properties": {
- "privateDnsZoneId": "[variables('aiSearchDnsZoneId')]"
+ "privateDnsZoneId": "[reference(resourceId('Microsoft.Resources/deployments', format('dns-{0}-{1}', replace(variables('dnsZoneEntries')[indexOf(variables('dnsZoneKeys'), variables('aiSearchDnsZoneName'))].key, '.', '-'), parameters('suffix'))), '2025-04-01').outputs.zoneId.value]"
}
}
]
},
"dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('aiSearchDnsZoneName'), format('aiSearch-{0}-link', parameters('suffix')))]",
- "[resourceId('Microsoft.Network/privateDnsZones', variables('aiSearchDnsZoneName'))]",
- "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('aiSearchName')))]"
+ "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('aiSearchName')))]",
+ "[resourceId('Microsoft.Resources/deployments', format('dns-{0}-{1}', replace(variables('dnsZoneEntries')[indexOf(variables('dnsZoneKeys'), variables('aiSearchDnsZoneName'))].key, '.', '-'), parameters('suffix')))]"
]
},
{
@@ -2195,14 +2041,13 @@
{
"name": "[format('{0}-dns-config', parameters('storageName'))]",
"properties": {
- "privateDnsZoneId": "[variables('storageDnsZoneId')]"
+ "privateDnsZoneId": "[reference(resourceId('Microsoft.Resources/deployments', format('dns-{0}-{1}', replace(variables('dnsZoneEntries')[indexOf(variables('dnsZoneKeys'), variables('storageDnsZoneName'))].key, '.', '-'), parameters('suffix'))), '2025-04-01').outputs.zoneId.value]"
}
}
]
},
"dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('storageDnsZoneName'), format('storage-{0}-link', parameters('suffix')))]",
- "[resourceId('Microsoft.Network/privateDnsZones', variables('storageDnsZoneName'))]",
+ "[resourceId('Microsoft.Resources/deployments', format('dns-{0}-{1}', replace(variables('dnsZoneEntries')[indexOf(variables('dnsZoneKeys'), variables('storageDnsZoneName'))].key, '.', '-'), parameters('suffix')))]",
"[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('storageName')))]"
]
},
@@ -2215,15 +2060,14 @@
{
"name": "[format('{0}-dns-config', parameters('cosmosDBName'))]",
"properties": {
- "privateDnsZoneId": "[variables('cosmosDBDnsZoneId')]"
+ "privateDnsZoneId": "[reference(resourceId('Microsoft.Resources/deployments', format('dns-{0}-{1}', replace(variables('dnsZoneEntries')[indexOf(variables('dnsZoneKeys'), variables('cosmosDBDnsZoneName'))].key, '.', '-'), parameters('suffix'))), '2025-04-01').outputs.zoneId.value]"
}
}
]
},
"dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('cosmosDBDnsZoneName'), format('cosmosDB-{0}-link', parameters('suffix')))]",
- "[resourceId('Microsoft.Network/privateDnsZones', variables('cosmosDBDnsZoneName'))]",
- "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('cosmosDBName')))]"
+ "[resourceId('Microsoft.Network/privateEndpoints', format('{0}-private-endpoint', parameters('cosmosDBName')))]",
+ "[resourceId('Microsoft.Resources/deployments', format('dns-{0}-{1}', replace(variables('dnsZoneEntries')[indexOf(variables('dnsZoneKeys'), variables('cosmosDBDnsZoneName'))].key, '.', '-'), parameters('suffix')))]"
]
},
{
@@ -2236,16 +2080,140 @@
{
"name": "[format('{0}-dns-config', variables('fabricWorkspaceName'))]",
"properties": {
- "privateDnsZoneId": "[variables('fabricDnsZoneId')]"
+ "privateDnsZoneId": "[if(variables('fabricPassedIn'), reference(resourceId('Microsoft.Resources/deployments', format('dns-{0}-{1}', replace(variables('dnsZoneEntries')[indexOf(variables('dnsZoneKeys'), variables('fabricDnsZoneName'))].key, '.', '-'), parameters('suffix'))), '2025-04-01').outputs.zoneId.value, '')]"
}
}
]
},
"dependsOn": [
- "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('fabricDnsZoneName'), format('fabric-{0}-link', parameters('suffix')))]",
- "[resourceId('Microsoft.Network/privateDnsZones', variables('fabricDnsZoneName'))]",
+ "[resourceId('Microsoft.Resources/deployments', format('dns-{0}-{1}', replace(variables('dnsZoneEntries')[indexOf(variables('dnsZoneKeys'), variables('fabricDnsZoneName'))].key, '.', '-'), parameters('suffix')))]",
"[resourceId('Microsoft.Network/privateEndpoints', format('{0}-fabric-private-endpoint', variables('fabricWorkspaceName')))]"
]
+ },
+ {
+ "copy": {
+ "name": "dnsZones",
+ "count": "[length(variables('dnsZoneEntries'))]"
+ },
+ "type": "Microsoft.Resources/deployments",
+ "apiVersion": "2025-04-01",
+ "name": "[format('dns-{0}-{1}', replace(variables('dnsZoneEntries')[copyIndex()].key, '.', '-'), parameters('suffix'))]",
+ "properties": {
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "mode": "Incremental",
+ "parameters": {
+ "zoneName": {
+ "value": "[variables('dnsZoneEntries')[copyIndex()].key]"
+ },
+ "existingResourceGroup": {
+ "value": "[coalesce(tryGet(variables('dnsZoneEntries')[copyIndex()].value, 'resourceGroup'), '')]"
+ },
+ "existingSubscriptionId": {
+ "value": "[coalesce(tryGet(variables('dnsZoneEntries')[copyIndex()].value, 'subscriptionId'), '')]"
+ },
+ "vnetId": {
+ "value": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('vnetSubscriptionId'), parameters('vnetResourceGroupName')), 'Microsoft.Network/virtualNetworks', parameters('vnetName'))]"
+ },
+ "suffix": {
+ "value": "[parameters('suffix')]"
+ },
+ "enabled": "[if(equals(variables('dnsZoneEntries')[copyIndex()].key, variables('fabricDnsZoneName')), createObject('value', variables('fabricPassedIn')), createObject('value', true()))]"
+ },
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.43.8.12551",
+ "templateHash": "9671566114533960613"
+ }
+ },
+ "parameters": {
+ "zoneName": {
+ "type": "string",
+ "metadata": {
+ "description": "Fully-qualified DNS zone name (e.g. privatelink.openai.azure.com)."
+ }
+ },
+ "existingResourceGroup": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Resource group of an existing zone. Empty means create the zone in this RG."
+ }
+ },
+ "existingSubscriptionId": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Subscription ID of an existing zone. Empty defaults to the current subscription. Only used when existingResourceGroup is non-empty."
+ }
+ },
+ "vnetId": {
+ "type": "string",
+ "metadata": {
+ "description": "ARM ID of the VNet to link the zone to."
+ }
+ },
+ "suffix": {
+ "type": "string",
+ "metadata": {
+ "description": "Suffix used to make the vnet-link name unique."
+ }
+ },
+ "enabled": {
+ "type": "bool",
+ "defaultValue": true,
+ "metadata": {
+ "description": "Disable the entire module (zone, link, output). Used to skip optional zones."
+ }
+ }
+ },
+ "variables": {
+ "shouldCreate": "[and(parameters('enabled'), empty(parameters('existingResourceGroup')))]",
+ "shouldReference": "[and(parameters('enabled'), not(empty(parameters('existingResourceGroup'))))]",
+ "effectiveExistingSubscriptionId": "[if(empty(parameters('existingSubscriptionId')), subscription().subscriptionId, parameters('existingSubscriptionId'))]"
+ },
+ "resources": [
+ {
+ "condition": "[variables('shouldCreate')]",
+ "type": "Microsoft.Network/privateDnsZones",
+ "apiVersion": "2020-06-01",
+ "name": "[parameters('zoneName')]",
+ "location": "global"
+ },
+ {
+ "condition": "[variables('shouldCreate')]",
+ "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks",
+ "apiVersion": "2024-06-01",
+ "name": "[format('{0}/{1}', parameters('zoneName'), format('{0}-{1}-link', replace(parameters('zoneName'), '.', '-'), parameters('suffix')))]",
+ "location": "global",
+ "properties": {
+ "virtualNetwork": {
+ "id": "[parameters('vnetId')]"
+ },
+ "registrationEnabled": false
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]"
+ ]
+ }
+ ],
+ "outputs": {
+ "zoneId": {
+ "type": "string",
+ "value": "[if(parameters('enabled'), if(variables('shouldCreate'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('effectiveExistingSubscriptionId'), parameters('existingResourceGroup')), 'Microsoft.Network/privateDnsZones', parameters('zoneName'))), '')]"
+ },
+ "zoneName": {
+ "type": "string",
+ "value": "[parameters('zoneName')]"
+ }
+ }
+ }
+ }
}
]
}
@@ -2315,8 +2283,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.41.2.15936",
- "templateHash": "17621807749606533298"
+ "version": "0.43.8.12551",
+ "templateHash": "5316230502381000043"
}
},
"parameters": {
@@ -2490,8 +2458,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.41.2.15936",
- "templateHash": "12210490978743733890"
+ "version": "0.43.8.12551",
+ "templateHash": "16292565234555392040"
}
},
"parameters": {
@@ -2523,7 +2491,7 @@
{
"type": "Microsoft.Resources/deployments",
"apiVersion": "2025-04-01",
- "name": "[format('storage-{0}-{1}-deployment', variables('azureStorageName'), variables('uniqueSuffix'))]",
+ "name": "[format('storage-ra-{0}-deployment', variables('uniqueSuffix'))]",
"subscriptionId": "[variables('azureStorageSubscriptionId')]",
"resourceGroup": "[variables('azureStorageResourceGroupName')]",
"properties": {
@@ -2545,8 +2513,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.41.2.15936",
- "templateHash": "10088093552813477461"
+ "version": "0.43.8.12551",
+ "templateHash": "10985949897065090550"
}
},
"parameters": {
@@ -2603,8 +2571,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.41.2.15936",
- "templateHash": "1708673367057519655"
+ "version": "0.43.8.12551",
+ "templateHash": "17331576029113373855"
}
},
"parameters": {
@@ -2667,8 +2635,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.41.2.15936",
- "templateHash": "18126535000597477931"
+ "version": "0.43.8.12551",
+ "templateHash": "15580039133385622424"
}
},
"parameters": {
@@ -2752,8 +2720,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.41.2.15936",
- "templateHash": "12585440226619351603"
+ "version": "0.43.8.12551",
+ "templateHash": "9752585897450875181"
}
},
"parameters": {
@@ -2810,12 +2778,11 @@
},
"dependsOn": [
"[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('accountName'), variables('uniqueSuffix')))]",
- "[resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix')))]",
"[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]",
"[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('aiSearchServiceSubscriptionId'), variables('aiSearchServiceResourceGroupName')), 'Microsoft.Resources/deployments', format('ai-search-ra-{0}-deployment', variables('uniqueSuffix')))]",
"[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('cosmosDBSubscriptionId'), variables('cosmosDBResourceGroupName')), 'Microsoft.Resources/deployments', format('cosmos-account-ra-{0}-deployment', variables('uniqueSuffix')))]",
"[resourceId('Microsoft.Resources/deployments', format('{0}-private-endpoint', variables('uniqueSuffix')))]",
- "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('azureStorageSubscriptionId'), variables('azureStorageResourceGroupName')), 'Microsoft.Resources/deployments', format('storage-{0}-{1}-deployment', variables('azureStorageName'), variables('uniqueSuffix')))]"
+ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('azureStorageSubscriptionId'), variables('azureStorageResourceGroupName')), 'Microsoft.Resources/deployments', format('storage-ra-{0}-deployment', variables('uniqueSuffix')))]"
]
},
{
@@ -2846,8 +2813,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.41.2.15936",
- "templateHash": "6504031662567695076"
+ "version": "0.43.8.12551",
+ "templateHash": "13556924300142468207"
}
},
"parameters": {
@@ -2894,7 +2861,8 @@
"[resourceId('Microsoft.Resources/deployments', format('capabilityHost-configuration-{0}-deployment', variables('uniqueSuffix')))]",
"[resourceId('Microsoft.Resources/deployments', format('dependencies-{0}-deployment', variables('uniqueSuffix')))]",
"[resourceId('Microsoft.Resources/deployments', format('{0}-{1}-deployment', variables('projectName'), variables('uniqueSuffix')))]",
- "[resourceId('Microsoft.Resources/deployments', format('format-project-workspace-id-{0}-deployment', variables('uniqueSuffix')))]"
+ "[resourceId('Microsoft.Resources/deployments', format('format-project-workspace-id-{0}-deployment', variables('uniqueSuffix')))]",
+ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('azureStorageSubscriptionId'), variables('azureStorageResourceGroupName')), 'Microsoft.Resources/deployments', format('storage-ra-{0}-deployment', variables('uniqueSuffix')))]"
]
},
{
@@ -2925,8 +2893,8 @@
"metadata": {
"_generator": {
"name": "bicep",
- "version": "0.41.2.15936",
- "templateHash": "10995262424499479562"
+ "version": "0.43.8.12551",
+ "templateHash": "12588788614140985947"
}
},
"parameters": {
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/azuredeploy.parameters.json b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azuredeploy.parameters.json
similarity index 50%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/azuredeploy.parameters.json
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azuredeploy.parameters.json
index f3621743e..0a2f1c70f 100644
--- a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/azuredeploy.parameters.json
+++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azuredeploy.parameters.json
@@ -56,13 +56,31 @@
"peSubnetPrefix": {
"value": ""
},
- "aiSearchResourceId": {
+ "existingAiSearchResourceId": {
"value": ""
},
- "azureStorageAccountResourceId": {
+ "existingAzureStorageAccountResourceId": {
"value": ""
},
- "azureCosmosDBAccountResourceId": {
+ "existingAzureCosmosDBAccountResourceId": {
+ "value": ""
+ },
+ "existingFabricWorkspaceResourceId": {
+ "value": ""
+ },
+ "existingPeSubnetResourceId": {
+ "value": ""
+ },
+ "existingAgentSubnetResourceId": {
+ "value": ""
+ },
+ "existingMcpSubnetResourceId": {
+ "value": ""
+ },
+ "mcpSubnetName": {
+ "value": ""
+ },
+ "mcpSubnetPrefix": {
"value": ""
},
"projectCapHost": {
@@ -70,12 +88,13 @@
},
"existingDnsZones": {
"value": {
- "privatelink.services.ai.azure.com": "",
- "privatelink.openai.azure.com": "",
- "privatelink.cognitiveservices.azure.com": "",
- "privatelink.search.windows.net": "",
- "privatelink.blob.core.windows.net": "",
- "privatelink.documents.azure.com": ""
+ "privatelink.services.ai.azure.com": { "subscriptionId": "", "resourceGroup": "" },
+ "privatelink.openai.azure.com": { "subscriptionId": "", "resourceGroup": "" },
+ "privatelink.cognitiveservices.azure.com": { "subscriptionId": "", "resourceGroup": "" },
+ "privatelink.search.windows.net": { "subscriptionId": "", "resourceGroup": "" },
+ "privatelink.blob.core.windows.net": { "subscriptionId": "", "resourceGroup": "" },
+ "privatelink.documents.azure.com": { "subscriptionId": "", "resourceGroup": "" },
+ "privatelink.fabric.microsoft.com": { "subscriptionId": "", "resourceGroup": "" }
}
}
}
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/createCapHost.sh b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/createCapHost.sh
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/createCapHost.sh
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/createCapHost.sh
diff --git a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/createUiDefinition.json b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/createUiDefinition.json
new file mode 100644
index 000000000..66bdc510f
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/createUiDefinition.json
@@ -0,0 +1,288 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/0.1.2-preview/CreateUIDefinition.MultiVm.json#",
+ "handler": "Microsoft.Azure.CreateUIDef",
+ "version": "0.1.2-preview",
+ "parameters": {
+ "config": {
+ "isWizard": true,
+ "basics": {
+ "description": "Deploy a network-isolated Microsoft Foundry account with private endpoints for AI Search, Cosmos DB, and Storage, plus a delegated subnet for tools (MCP, OpenAPI, Functions, A2A) running on a VNet.",
+ "location": {
+ "label": "Region",
+ "allowedValues": [
+ "westus", "westus2", "westus3", "eastus", "eastus2", "southcentralus", "canadaeast",
+ "westeurope", "uksouth", "swedencentral", "francecentral", "germanywestcentral", "italynorth", "spaincentral", "norwayeast", "polandcentral", "switzerlandnorth",
+ "japaneast", "koreacentral", "australiaeast", "southindia",
+ "uaenorth", "southafricanorth", "brazilsouth"
+ ]
+ }
+ }
+ },
+ "basics": [],
+ "steps": [
+ {
+ "name": "foundry",
+ "label": "Foundry account & project",
+ "elements": [
+ {
+ "name": "aiServices",
+ "type": "Microsoft.Common.TextBox",
+ "label": "AI Services name prefix",
+ "defaultValue": "aifoundry",
+ "toolTip": "2-40 lowercase letters or digits. A 4-character random suffix is appended to make the resource name unique.",
+ "constraints": {
+ "required": true,
+ "regex": "^[a-z0-9]{2,40}$",
+ "validationMessage": "2-40 lowercase letters or digits."
+ }
+ },
+ {
+ "name": "firstProjectName",
+ "type": "Microsoft.Common.TextBox",
+ "label": "Project name prefix",
+ "defaultValue": "project",
+ "constraints": {
+ "required": true,
+ "regex": "^[a-z0-9]{2,40}$",
+ "validationMessage": "2-40 lowercase letters or digits."
+ }
+ },
+ {
+ "name": "displayName",
+ "type": "Microsoft.Common.TextBox",
+ "label": "Project display name",
+ "defaultValue": "network secured agent project"
+ },
+ {
+ "name": "projectDescription",
+ "type": "Microsoft.Common.TextBox",
+ "label": "Project description",
+ "defaultValue": "A project for the AI Foundry account with network secured deployed Agent",
+ "multiLine": true
+ }
+ ]
+ },
+ {
+ "name": "model",
+ "label": "Model deployment",
+ "elements": [
+ {
+ "name": "modelName",
+ "type": "Microsoft.Common.DropDown",
+ "label": "Model",
+ "defaultValue": "gpt-4o-mini",
+ "constraints": {
+ "required": true,
+ "allowedValues": [
+ { "label": "gpt-4o-mini", "value": "gpt-4o-mini" },
+ { "label": "gpt-4o", "value": "gpt-4o" },
+ { "label": "gpt-4", "value": "gpt-4" }
+ ]
+ }
+ },
+ {
+ "name": "modelVersion",
+ "type": "Microsoft.Common.TextBox",
+ "label": "Model version",
+ "defaultValue": "2024-07-18",
+ "toolTip": "API version of the selected model."
+ },
+ {
+ "name": "modelSkuName",
+ "type": "Microsoft.Common.DropDown",
+ "label": "Deployment SKU",
+ "defaultValue": "GlobalStandard",
+ "constraints": {
+ "allowedValues": [
+ { "label": "GlobalStandard", "value": "GlobalStandard" },
+ { "label": "Standard", "value": "Standard" }
+ ]
+ }
+ },
+ {
+ "name": "modelCapacity",
+ "type": "Microsoft.Common.Slider",
+ "label": "Capacity (TPM x 1000)",
+ "min": 1,
+ "max": 300,
+ "defaultValue": 30,
+ "showStepMarkers": false
+ }
+ ]
+ },
+ {
+ "name": "network",
+ "label": "Networking",
+ "elements": [
+ {
+ "name": "vnetMode",
+ "type": "Microsoft.Common.OptionsGroup",
+ "label": "Virtual Network",
+ "defaultValue": "Create new",
+ "toolTip": "Create a brand-new VNet, or use an existing one (recommended for shared/enterprise environments).",
+ "constraints": {
+ "required": true,
+ "allowedValues": [
+ { "label": "Create new", "value": "new" },
+ { "label": "Use existing", "value": "existing" }
+ ]
+ }
+ },
+
+ {
+ "name": "newVnetName",
+ "type": "Microsoft.Common.TextBox",
+ "label": "VNet name",
+ "defaultValue": "agent-vnet",
+ "visible": "[equals(steps('network').vnetMode, 'new')]",
+ "constraints": { "required": true }
+ },
+ {
+ "name": "newVnetAddressPrefix",
+ "type": "Microsoft.Common.TextBox",
+ "label": "VNet address space",
+ "defaultValue": "192.168.0.0/16",
+ "visible": "[equals(steps('network').vnetMode, 'new')]",
+ "constraints": {
+ "required": true,
+ "regex": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/[0-9]{1,2}$",
+ "validationMessage": "Must be a valid CIDR (e.g. 192.168.0.0/16)."
+ }
+ },
+ {
+ "name": "newAgentSubnetPrefix",
+ "type": "Microsoft.Common.TextBox",
+ "label": "Agent subnet prefix",
+ "defaultValue": "192.168.0.0/24",
+ "visible": "[equals(steps('network').vnetMode, 'new')]"
+ },
+ {
+ "name": "newPeSubnetPrefix",
+ "type": "Microsoft.Common.TextBox",
+ "label": "Private-endpoint subnet prefix",
+ "defaultValue": "192.168.1.0/24",
+ "visible": "[equals(steps('network').vnetMode, 'new')]"
+ },
+ {
+ "name": "newMcpSubnetPrefix",
+ "type": "Microsoft.Common.TextBox",
+ "label": "MCP subnet prefix",
+ "defaultValue": "192.168.2.0/24",
+ "visible": "[equals(steps('network').vnetMode, 'new')]"
+ },
+
+ {
+ "name": "existingVnet",
+ "type": "Microsoft.Network.VirtualNetworkCombo",
+ "label": {
+ "virtualNetwork": "Existing VNet",
+ "subnets": "Subnets"
+ },
+ "visible": "[equals(steps('network').vnetMode, 'existing')]",
+ "scope": {
+ "subscriptionId": "[subscription().subscriptionId]"
+ },
+ "subnets": {
+ "agentSubnet": {
+ "label": "Agent subnet",
+ "constraints": { "minAddressCount": 32, "requireContiguousAddresses": true }
+ },
+ "peSubnet": {
+ "label": "Private-endpoint subnet",
+ "constraints": { "minAddressCount": 32, "requireContiguousAddresses": true }
+ },
+ "mcpSubnet": {
+ "label": "MCP subnet",
+ "constraints": { "minAddressCount": 32, "requireContiguousAddresses": true }
+ }
+ }
+ },
+ {
+ "name": "existingSubnetReuseInfo",
+ "type": "Microsoft.Common.InfoBox",
+ "visible": "[equals(steps('network').vnetMode, 'existing')]",
+ "options": {
+ "icon": "Info",
+ "text": "When using an existing VNet, the subnets you select above will be referenced as-is. Their existing NSGs, route tables, and delegations are preserved."
+ }
+ }
+ ]
+ },
+ {
+ "name": "byo",
+ "label": "Bring-your-own backing resources",
+ "elements": [
+ {
+ "name": "byoInfo",
+ "type": "Microsoft.Common.InfoBox",
+ "options": {
+ "icon": "Info",
+ "text": "Optional. Leave any of these unselected to have the template create a brand-new resource for you."
+ }
+ },
+ {
+ "name": "aiSearch",
+ "type": "Microsoft.Solutions.ResourceSelector",
+ "label": "Existing AI Search service (optional)",
+ "resourceType": "Microsoft.Search/searchServices",
+ "options": {
+ "filter": { "subscription": "onePerLine" }
+ }
+ },
+ {
+ "name": "cosmos",
+ "type": "Microsoft.Solutions.ResourceSelector",
+ "label": "Existing Cosmos DB account (optional)",
+ "resourceType": "Microsoft.DocumentDB/databaseAccounts"
+ },
+ {
+ "name": "storage",
+ "type": "Microsoft.Solutions.ResourceSelector",
+ "label": "Existing Storage account (optional)",
+ "resourceType": "Microsoft.Storage/storageAccounts"
+ },
+ {
+ "name": "fabric",
+ "type": "Microsoft.Common.TextBox",
+ "label": "Microsoft Fabric workspace ARM ID (optional)",
+ "toolTip": "Full ARM resource ID of an existing Fabric workspace. Leave empty to skip Fabric private link.",
+ "constraints": {
+ "regex": "^$|^/subscriptions/.+/resourceGroups/.+/providers/Microsoft.Fabric/.+$",
+ "validationMessage": "Must be empty or a valid Microsoft.Fabric ARM resource ID."
+ }
+ }
+ ]
+ }
+ ],
+ "outputs": {
+ "location": "[location()]",
+
+ "aiServices": "[steps('foundry').aiServices]",
+ "firstProjectName": "[steps('foundry').firstProjectName]",
+ "displayName": "[steps('foundry').displayName]",
+ "projectDescription": "[steps('foundry').projectDescription]",
+
+ "modelName": "[steps('model').modelName]",
+ "modelFormat": "OpenAI",
+ "modelVersion": "[steps('model').modelVersion]",
+ "modelSkuName": "[steps('model').modelSkuName]",
+ "modelCapacity": "[steps('model').modelCapacity]",
+
+ "vnetName": "[if(equals(steps('network').vnetMode, 'new'), steps('network').newVnetName, 'agent-vnet')]",
+ "vnetAddressPrefix": "[if(equals(steps('network').vnetMode, 'new'), steps('network').newVnetAddressPrefix, '')]",
+ "agentSubnetPrefix": "[if(equals(steps('network').vnetMode, 'new'), steps('network').newAgentSubnetPrefix, '')]",
+ "peSubnetPrefix": "[if(equals(steps('network').vnetMode, 'new'), steps('network').newPeSubnetPrefix, '')]",
+ "mcpSubnetPrefix": "[if(equals(steps('network').vnetMode, 'new'), steps('network').newMcpSubnetPrefix, '')]",
+
+ "existingVnetResourceId": "[if(equals(steps('network').vnetMode, 'existing'), steps('network').existingVnet.virtualNetwork.id, '')]",
+ "existingAgentSubnetResourceId": "[if(equals(steps('network').vnetMode, 'existing'), steps('network').existingVnet.subnets.agentSubnet.id, '')]",
+ "existingPeSubnetResourceId": "[if(equals(steps('network').vnetMode, 'existing'), steps('network').existingVnet.subnets.peSubnet.id, '')]",
+ "existingMcpSubnetResourceId": "[if(equals(steps('network').vnetMode, 'existing'), steps('network').existingVnet.subnets.mcpSubnet.id, '')]",
+
+ "existingAiSearchResourceId": "[coalesce(steps('byo').aiSearch.id, '')]",
+ "existingAzureCosmosDBAccountResourceId": "[coalesce(steps('byo').cosmos.id, '')]",
+ "existingAzureStorageAccountResourceId": "[coalesce(steps('byo').storage.id, '')]",
+ "existingFabricWorkspaceResourceId": "[steps('byo').fabric]"
+ }
+ }
+}
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/deleteCapHost.sh b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/deleteCapHost.sh
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/deleteCapHost.sh
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/deleteCapHost.sh
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/diagrams/deployment_flow.png b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/diagrams/deployment_flow.png
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/diagrams/deployment_flow.png
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/diagrams/deployment_flow.png
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/diagrams/generate_diagrams.py b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/diagrams/generate_diagrams.py
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/diagrams/generate_diagrams.py
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/diagrams/generate_diagrams.py
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/diagrams/layer1_network_foundation.png b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/diagrams/layer1_network_foundation.png
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/diagrams/layer1_network_foundation.png
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/diagrams/layer1_network_foundation.png
diff --git a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/diagrams/layer2_data_resources.png b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/diagrams/layer2_data_resources.png
new file mode 100644
index 000000000..2f8a80cac
Binary files /dev/null and b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/diagrams/layer2_data_resources.png differ
diff --git a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/diagrams/layer3_ai_services.png b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/diagrams/layer3_ai_services.png
new file mode 100644
index 000000000..498c159b2
Binary files /dev/null and b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/diagrams/layer3_ai_services.png differ
diff --git a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/diagrams/layer4_project_connections.png b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/diagrams/layer4_project_connections.png
new file mode 100644
index 000000000..82e57f9a1
Binary files /dev/null and b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/diagrams/layer4_project_connections.png differ
diff --git a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/diagrams/layer5_capability_host.png b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/diagrams/layer5_capability_host.png
new file mode 100644
index 000000000..b8c040870
Binary files /dev/null and b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/diagrams/layer5_capability_host.png differ
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/get-existing-resources.ps1 b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/get-existing-resources.ps1
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/get-existing-resources.ps1
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/get-existing-resources.ps1
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/main.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/main.bicep
similarity index 68%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/main.bicep
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/main.bicep
index 11d4c2a06..2ad04c322 100644
--- a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/main.bicep
+++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/main.bicep
@@ -47,8 +47,10 @@ Architecture:
])
param location string = 'eastus2'
-@description('Name for your AI Services resource.')
-param aiServices string = 'aiservices'
+@description('Name prefix for your AI Services (Cognitive Services) resource. Lowercase alphanumeric only; a 4-character random suffix will be appended.')
+@minLength(2)
+@maxLength(40)
+param aiServices string = 'aifoundry'
// Model deployment parameters
@description('The name of the model you want to deploy')
@@ -67,7 +69,9 @@ param deploymentTimestamp string = utcNow('yyyyMMddHHmmss')
var uniqueSuffix = substring(uniqueString('${resourceGroup().id}-${deploymentTimestamp}'), 0, 4)
var accountName = toLower('${aiServices}${uniqueSuffix}')
-@description('Name for your project resource.')
+@description('Name prefix for your Foundry project. A 4-character random suffix will be appended.')
+@minLength(2)
+@maxLength(40)
param firstProjectName string = 'project'
@description('This project will be a sub-resource of your account')
@@ -77,16 +81,16 @@ param projectDescription string = 'A project for the AI Foundry account with net
param displayName string = 'network secured agent project'
// Existing Virtual Network parameters
-@description('Virtual Network name for the Agent to create new or existing virtual network')
-param vnetName string = 'agent-vnet-test'
+@description('Virtual Network name. When `existingVnetResourceId` is set, this is ignored β the name is derived from the resource ID.')
+param vnetName string = 'agent-vnet'
-@description('The name of Agents Subnet to create new or existing subnet for agents')
+@description('Agent subnet name. Ignored when `existingAgentSubnetResourceId` is set.')
param agentSubnetName string = 'agent-subnet'
-@description('The name of Private Endpoint subnet to create new or existing subnet for private endpoints')
+@description('Private endpoint subnet name. Ignored when `existingPeSubnetResourceId` is set.')
param peSubnetName string = 'pe-subnet'
-@description('The name of MCP subnet for user-deployed Container Apps (e.g., MCP servers)')
+@description('MCP subnet name (hosts user-deployed Container Apps such as MCP servers). Ignored when `existingMcpSubnetResourceId` is set.')
param mcpSubnetName string = 'mcp-subnet'
//Existing standard Agent required resources
@@ -105,62 +109,69 @@ param peSubnetPrefix string = ''
@description('Address prefix for the MCP subnet. The default value is 192.168.2.0/24.')
param mcpSubnetPrefix string = ''
-@description('The AI Search Service full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.')
-param aiSearchResourceId string = ''
-@description('The AI Storage Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.')
-param azureStorageAccountResourceId string = ''
-@description('The Cosmos DB Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.')
-param azureCosmosDBAccountResourceId string = ''
+@description('Optional ARM Resource ID of an existing agent subnet. If provided, the subnet will be referenced as-is and will not be created/modified.')
+param existingAgentSubnetResourceId string = ''
-@description('The Microsoft Fabric Workspace full ARM Resource ID. This is an optional field for Fabric private link connectivity.')
-param fabricWorkspaceResourceId string = ''
+@description('Optional ARM Resource ID of an existing private endpoint subnet. If provided, the subnet will be referenced as-is and will not be created/modified.')
+param existingPeSubnetResourceId string = ''
+
+@description('Optional ARM Resource ID of an existing MCP subnet. If provided, the subnet will be referenced as-is and will not be created/modified.')
+param existingMcpSubnetResourceId string = ''
+
+@description('The AI Search Service full ARM Resource ID. Optional β leave empty to create a new one.')
+param existingAiSearchResourceId string = ''
+@description('The AI Storage Account full ARM Resource ID. Optional β leave empty to create a new one.')
+param existingAzureStorageAccountResourceId string = ''
+@description('The Cosmos DB Account full ARM Resource ID. Optional β leave empty to create a new one.')
+param existingAzureCosmosDBAccountResourceId string = ''
+
+@description('The Microsoft Fabric Workspace full ARM Resource ID. Optional β enables Fabric private link connectivity.')
+param existingFabricWorkspaceResourceId string = ''
//New Param for resource group of Private DNS zones
//@description('Optional: Resource group containing existing private DNS zones. If specified, DNS zones will not be created.')
//param existingDnsZonesResourceGroup string = ''
-@description('Object mapping DNS zone names to their resource group, or empty string to indicate creation')
+@description('Map of private DNS zone FQDNs to an object `{ subscriptionId, resourceGroup }` describing where the zone lives. Empty `resourceGroup` means "create the zone in this deployment\'s resource group". A non-empty `resourceGroup` references an existing zone in that RG; empty `subscriptionId` defaults to the current subscription, otherwise the zone is referenced cross-subscription. Note: when referencing an existing zone, the VNet link to that zone is NOT managed by this template β the caller must ensure the zone is already linked to the target VNet.')
param existingDnsZones object = {
- 'privatelink.services.ai.azure.com': ''
- 'privatelink.openai.azure.com': ''
- 'privatelink.cognitiveservices.azure.com': ''
- 'privatelink.search.windows.net': ''
- 'privatelink.blob.core.windows.net': ''
- 'privatelink.documents.azure.com': ''
- 'privatelink.analysis.windows.net': ''
+ 'privatelink.services.ai.azure.com': { subscriptionId: '', resourceGroup: '' }
+ 'privatelink.openai.azure.com': { subscriptionId: '', resourceGroup: '' }
+ 'privatelink.cognitiveservices.azure.com': { subscriptionId: '', resourceGroup: '' }
+ 'privatelink.search.windows.net': { subscriptionId: '', resourceGroup: '' }
+ 'privatelink.blob.${environment().suffixes.storage}': { subscriptionId: '', resourceGroup: '' }
+ 'privatelink.documents.azure.com': { subscriptionId: '', resourceGroup: '' }
+ 'privatelink.fabric.microsoft.com': { subscriptionId: '', resourceGroup: '' }
}
-@description('Zone Names for Validation of existing Private Dns Zones')
-param dnsZoneNames array = [
- 'privatelink.services.ai.azure.com'
- 'privatelink.openai.azure.com'
- 'privatelink.cognitiveservices.azure.com'
- 'privatelink.search.windows.net'
- 'privatelink.blob.core.windows.net'
- 'privatelink.documents.azure.com'
- 'privatelink.analysis.windows.net'
-]
-
var projectName = toLower('${firstProjectName}${uniqueSuffix}')
var cosmosDBName = toLower('${aiServices}${uniqueSuffix}cosmosdb')
var aiSearchName = toLower('${aiServices}${uniqueSuffix}search')
var azureStorageName = toLower('${aiServices}${uniqueSuffix}storage')
// Check if existing resources have been passed in
-var storagePassedIn = azureStorageAccountResourceId != ''
-var searchPassedIn = aiSearchResourceId != ''
-var cosmosPassedIn = azureCosmosDBAccountResourceId != ''
+var storagePassedIn = existingAzureStorageAccountResourceId != ''
+var searchPassedIn = existingAiSearchResourceId != ''
+var cosmosPassedIn = existingAzureCosmosDBAccountResourceId != ''
var existingVnetPassedIn = existingVnetResourceId != ''
-var acsParts = split(aiSearchResourceId, '/')
+// Existing-subnet flags. When a subnet ARM ID is provided we derive the subnet name
+// from the ID itself so we look up the right subnet (instead of trusting the *SubnetName param).
+var agentSubnetExists = existingAgentSubnetResourceId != ''
+var peSubnetExists = existingPeSubnetResourceId != ''
+var mcpSubnetExists = existingMcpSubnetResourceId != ''
+var effectiveAgentSubnetName = agentSubnetExists ? last(split(existingAgentSubnetResourceId, '/')) : agentSubnetName
+var effectivePeSubnetName = peSubnetExists ? last(split(existingPeSubnetResourceId, '/')) : peSubnetName
+var effectiveMcpSubnetName = mcpSubnetExists ? last(split(existingMcpSubnetResourceId, '/')) : mcpSubnetName
+
+var acsParts = split(existingAiSearchResourceId, '/')
var aiSearchServiceSubscriptionId = searchPassedIn ? acsParts[2] : subscription().subscriptionId
var aiSearchServiceResourceGroupName = searchPassedIn ? acsParts[4] : resourceGroup().name
-var cosmosParts = split(azureCosmosDBAccountResourceId, '/')
+var cosmosParts = split(existingAzureCosmosDBAccountResourceId, '/')
var cosmosDBSubscriptionId = cosmosPassedIn ? cosmosParts[2] : subscription().subscriptionId
var cosmosDBResourceGroupName = cosmosPassedIn ? cosmosParts[4] : resourceGroup().name
-var storageParts = split(azureStorageAccountResourceId, '/')
+var storageParts = split(existingAzureStorageAccountResourceId, '/')
var azureStorageSubscriptionId = storagePassedIn ? storageParts[2] : subscription().subscriptionId
var azureStorageResourceGroupName = storagePassedIn ? storageParts[4] : resourceGroup().name
@@ -181,14 +192,17 @@ module vnet 'modules-network-secured/network-agent-vnet.bicep' = {
vnetName: trimVnetName
useExistingVnet: existingVnetPassedIn
existingVnetResourceGroupName: vnetResourceGroupName
- agentSubnetName: agentSubnetName
- peSubnetName: peSubnetName
- mcpSubnetName: mcpSubnetName
+ agentSubnetName: effectiveAgentSubnetName
+ peSubnetName: effectivePeSubnetName
+ mcpSubnetName: effectiveMcpSubnetName
vnetAddressPrefix: vnetAddressPrefix
agentSubnetPrefix: agentSubnetPrefix
peSubnetPrefix: peSubnetPrefix
mcpSubnetPrefix: mcpSubnetPrefix
existingVnetSubscriptionId: vnetSubscriptionId
+ agentSubnetExists: agentSubnetExists
+ peSubnetExists: peSubnetExists
+ mcpSubnetExists: mcpSubnetExists
}
}
@@ -210,20 +224,14 @@ module aiAccount 'modules-network-secured/ai-account-identity.bicep' = {
}
}
/*
- Validate existing resources
- This module will check if the AI Search Service, Storage Account, and Cosmos DB Account already exist.
- If they do, it will set the corresponding output to true. If they do not exist, it will set the output to false.
+ Inline existence checks (replaces the previous validate-existing-resources.bicep module,
+ which was tautological: it set `*Exists = passedIn && (resource.name == parts[8])` where
+ `parts[8]` was the very same string used to reference the resource by name).
+ An empty resource ID means "create new".
*/
-module validateExistingResources 'modules-network-secured/validate-existing-resources.bicep' = {
- name: 'validate-existing-resources-${uniqueSuffix}-deployment'
- params: {
- aiSearchResourceId: aiSearchResourceId
- azureStorageAccountResourceId: azureStorageAccountResourceId
- azureCosmosDBAccountResourceId: azureCosmosDBAccountResourceId
- existingDnsZones: existingDnsZones
- dnsZoneNames: dnsZoneNames
- }
-}
+var aiSearchExists = existingAiSearchResourceId != ''
+var azureStorageExists = existingAzureStorageAccountResourceId != ''
+var cosmosDBExists = existingAzureCosmosDBAccountResourceId != ''
// This module will create new agent dependent resources
// A Cosmos DB account, an AI Search Service, and a Storage Account are created if they do not already exist
@@ -236,36 +244,23 @@ module aiDependencies 'modules-network-secured/standard-dependent-resources.bice
cosmosDBName: cosmosDBName
// AI Search Service parameters
- aiSearchResourceId: aiSearchResourceId
- aiSearchExists: validateExistingResources.outputs.aiSearchExists
+ existingAiSearchResourceId: existingAiSearchResourceId
+ aiSearchExists: aiSearchExists
// Storage Account
- azureStorageAccountResourceId: azureStorageAccountResourceId
- azureStorageExists: validateExistingResources.outputs.azureStorageExists
+ existingAzureStorageAccountResourceId: existingAzureStorageAccountResourceId
+ azureStorageExists: azureStorageExists
// Cosmos DB Account
- cosmosDBResourceId: azureCosmosDBAccountResourceId
- cosmosDBExists: validateExistingResources.outputs.cosmosDBExists
+ existingCosmosDBResourceId: existingAzureCosmosDBAccountResourceId
+ cosmosDBExists: cosmosDBExists
}
}
-resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' existing = {
- name: aiDependencies.outputs.azureStorageName
- scope: resourceGroup(azureStorageSubscriptionId, azureStorageResourceGroupName)
-}
-
-resource aiSearch 'Microsoft.Search/searchServices@2023-11-01' existing = {
- name: aiDependencies.outputs.aiSearchName
- scope: resourceGroup(
- aiDependencies.outputs.aiSearchServiceSubscriptionId,
- aiDependencies.outputs.aiSearchServiceResourceGroupName
- )
-}
-
-resource cosmosDB 'Microsoft.DocumentDB/databaseAccounts@2024-11-15' existing = {
- name: aiDependencies.outputs.cosmosDBName
- scope: resourceGroup(cosmosDBSubscriptionId, cosmosDBResourceGroupName)
-}
+// Note: previously this file declared `existing` references to storage / aiSearch / cosmosDB
+// solely to use them in module `dependsOn` blocks. That pattern is a no-op (dependsOn on
+// `existing` resources is silently ignored), so they were removed. The real dependency on
+// these resources flows implicitly through `aiDependencies.outputs.*` references in params.
// Private Endpoint and DNS Configuration
// This module sets up private network access for all Azure services:
@@ -280,7 +275,7 @@ module privateEndpointAndDNS 'modules-network-secured/private-endpoint-and-dns.b
aiSearchName: aiDependencies.outputs.aiSearchName // AI Search to secure
storageName: aiDependencies.outputs.azureStorageName // Storage to secure
cosmosDBName: aiDependencies.outputs.cosmosDBName
- fabricWorkspaceResourceId: fabricWorkspaceResourceId // Microsoft Fabric workspace (optional)
+ fabricWorkspaceResourceId: existingFabricWorkspaceResourceId // Microsoft Fabric workspace (optional)
vnetName: vnet.outputs.virtualNetworkName // VNet containing subnets
peSubnetName: vnet.outputs.peSubnetName // Subnet for private endpoints
suffix: uniqueSuffix // Unique identifier
@@ -294,11 +289,8 @@ module privateEndpointAndDNS 'modules-network-secured/private-endpoint-and-dns.b
storageAccountSubscriptionId: azureStorageSubscriptionId // Subscription ID for Storage Account
existingDnsZones: existingDnsZones
}
- dependsOn: [
- aiSearch // Ensure AI Search exists
- storage // Ensure Storage exists
- cosmosDB // Ensure Cosmos DB exists
- ]
+ // Dependencies on `aiDependencies` and `vnet` are implicit through param references
+ // (e.g. aiAccount.outputs, aiDependencies.outputs.*, vnet.outputs.*).
}
/*
@@ -329,9 +321,6 @@ module aiProject 'modules-network-secured/ai-project-identity.bicep' = {
}
dependsOn: [
privateEndpointAndDNS
- cosmosDB
- aiSearch
- storage
]
}
@@ -346,14 +335,13 @@ module formatProjectWorkspaceId 'modules-network-secured/format-project-workspac
Assigns the project SMI the storage blob data contributor role on the storage account
*/
module storageAccountRoleAssignment 'modules-network-secured/azure-storage-account-role-assignment.bicep' = {
- name: 'storage-${azureStorageName}-${uniqueSuffix}-deployment'
+ name: 'storage-ra-${uniqueSuffix}-deployment'
scope: resourceGroup(azureStorageSubscriptionId, azureStorageResourceGroupName)
params: {
azureStorageName: aiDependencies.outputs.azureStorageName
projectPrincipalId: aiProject.outputs.projectPrincipalId
}
dependsOn: [
- storage
privateEndpointAndDNS
]
}
@@ -367,7 +355,6 @@ module cosmosAccountRoleAssignments 'modules-network-secured/cosmosdb-account-ro
projectPrincipalId: aiProject.outputs.projectPrincipalId
}
dependsOn: [
- cosmosDB
privateEndpointAndDNS
]
}
@@ -381,7 +368,6 @@ module aiSearchRoleAssignments 'modules-network-secured/ai-search-role-assignmen
projectPrincipalId: aiProject.outputs.projectPrincipalId
}
dependsOn: [
- aiSearch
privateEndpointAndDNS
]
}
@@ -398,9 +384,6 @@ module addProjectCapabilityHost 'modules-network-secured/add-project-capability-
projectCapHost: projectCapHost
}
dependsOn: [
- aiSearch // Ensure AI Search exists
- storage // Ensure Storage exists
- cosmosDB
privateEndpointAndDNS
cosmosAccountRoleAssignments
storageAccountRoleAssignment
@@ -419,6 +402,7 @@ module storageContainersRoleAssignment 'modules-network-secured/blob-storage-con
}
dependsOn: [
addProjectCapabilityHost
+ storageAccountRoleAssignment
]
}
diff --git a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/main.bicepparam b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/main.bicepparam
new file mode 100644
index 000000000..baa9300ea
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/main.bicepparam
@@ -0,0 +1,81 @@
+using './main.bicep'
+
+// -----------------------------------------------------------------------------
+// Foundry account & project
+// -----------------------------------------------------------------------------
+param location = 'eastus2'
+param aiServices = 'contoso' // 2-40 lowercase chars; a 4-char suffix is appended
+param firstProjectName = 'project'
+param projectDescription = 'A project for the AI Foundry account with network secured deployed Agent'
+param displayName = 'project'
+
+// -----------------------------------------------------------------------------
+// Model deployment
+// -----------------------------------------------------------------------------
+param modelName = 'gpt-4o-mini'
+param modelFormat = 'OpenAI'
+param modelVersion = '2024-07-18'
+param modelSkuName = 'GlobalStandard'
+param modelCapacity = 1
+
+// -----------------------------------------------------------------------------
+// Networking
+//
+// Two scenarios:
+// 1) NEW VNet (default): leave `existingVnetResourceId` empty. The template will
+// create a new VNet plus three subnets (agent / pe / mcp). Set `vnetAddressPrefix`
+// and the per-subnet prefixes if you want non-default CIDRs.
+// 2) EXISTING VNet: set `existingVnetResourceId` to the VNet's full ARM ID. The
+// `vnetName` and per-subnet `*Name` params are then ignored β names come from the
+// ARM IDs. To **reuse** existing subnets without modification, also set the
+// matching `existing*SubnetResourceId` params (highly recommended for shared VNets;
+// otherwise the template will try to (re)create the subnets and may overwrite NSGs
+// or routing).
+// -----------------------------------------------------------------------------
+param vnetName = 'agent-vnet'
+param agentSubnetName = 'agent-subnet'
+param peSubnetName = 'pe-subnet'
+param mcpSubnetName = 'mcp-subnet'
+
+param vnetAddressPrefix = '' // e.g. '192.168.0.0/16'
+param agentSubnetPrefix = '' // e.g. '192.168.0.0/24'
+param peSubnetPrefix = '' // e.g. '192.168.1.0/24'
+param mcpSubnetPrefix = '' // e.g. '192.168.2.0/24'
+
+param existingVnetResourceId = ''
+// param existingVnetResourceId = '/subscriptions//resourceGroups//providers/Microsoft.Network/virtualNetworks/'
+
+// Optional - reuse existing subnets in the existing VNet (no create / no modify).
+// Provide the full ARM ID for each subnet you want to leave untouched.
+param existingAgentSubnetResourceId = ''
+param existingPeSubnetResourceId = ''
+param existingMcpSubnetResourceId = ''
+// param existingAgentSubnetResourceId = '/subscriptions//resourceGroups//providers/Microsoft.Network/virtualNetworks//subnets/agent-subnet'
+
+// -----------------------------------------------------------------------------
+// Bring-your-own backing resources (optional)
+// Leave empty to create new ones in the deployment resource group.
+// -----------------------------------------------------------------------------
+param existingAiSearchResourceId = ''
+param existingAzureStorageAccountResourceId = ''
+param existingAzureCosmosDBAccountResourceId = ''
+param existingFabricWorkspaceResourceId = ''
+
+// -----------------------------------------------------------------------------
+// Private DNS zones
+// Each value is an object: { subscriptionId: '', resourceGroup: '' }.
+// - Both empty => create the zone in THIS deployment RG.
+// - resourceGroup only => reference existing zone in that RG, current subscription.
+// - both set => reference existing zone in another subscription/RG.
+// IMPORTANT: When referencing an existing zone, this template does NOT create the
+// VNet link. You must ensure the shared zone is already linked to the target VNet.
+// -----------------------------------------------------------------------------
+param existingDnsZones = {
+ 'privatelink.services.ai.azure.com': { subscriptionId: '', resourceGroup: '' }
+ 'privatelink.openai.azure.com': { subscriptionId: '', resourceGroup: '' }
+ 'privatelink.cognitiveservices.azure.com': { subscriptionId: '', resourceGroup: '' }
+ 'privatelink.search.windows.net': { subscriptionId: '', resourceGroup: '' }
+ 'privatelink.blob.core.windows.net': { subscriptionId: '', resourceGroup: '' }
+ 'privatelink.documents.azure.com': { subscriptionId: '', resourceGroup: '' }
+ 'privatelink.fabric.microsoft.com': { subscriptionId: '', resourceGroup: '' }
+}
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/mcp-http-server/Dockerfile b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/mcp-http-server/Dockerfile
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/mcp-http-server/Dockerfile
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/mcp-http-server/Dockerfile
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/metadata.json b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/metadata.json
similarity index 95%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/metadata.json
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/metadata.json
index a91a5830a..05eda9eb0 100644
--- a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/metadata.json
+++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/metadata.json
@@ -18,7 +18,7 @@
},
"prerequisites": [
"Azure subscription with Owner or Contributor role",
- "Azure AI Account Owner role for creating AI Services",
+ "Foundry Account Owner role for creating AI Services",
"Sufficient quota for gpt-4o-mini model deployment"
],
"resources_created": [
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/add-project-capability-host.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/add-project-capability-host.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/add-project-capability-host.bicep
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/add-project-capability-host.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/ai-account-identity.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/ai-account-identity.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/ai-account-identity.bicep
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/ai-account-identity.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/ai-project-identity-unique.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/ai-project-identity-unique.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/ai-project-identity-unique.bicep
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/ai-project-identity-unique.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/ai-project-identity.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/ai-project-identity.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/ai-project-identity.bicep
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/ai-project-identity.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/ai-search-role-assignments.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/ai-search-role-assignments.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/ai-search-role-assignments.bicep
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/ai-search-role-assignments.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/azure-storage-account-role-assignment.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/azure-storage-account-role-assignment.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/azure-storage-account-role-assignment.bicep
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/azure-storage-account-role-assignment.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/blob-storage-container-role-assignments-unique.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/blob-storage-container-role-assignments-unique.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/blob-storage-container-role-assignments-unique.bicep
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/blob-storage-container-role-assignments-unique.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/blob-storage-container-role-assignments.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/blob-storage-container-role-assignments.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/blob-storage-container-role-assignments.bicep
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/blob-storage-container-role-assignments.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/cosmos-container-role-assignments.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/cosmos-container-role-assignments.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/cosmos-container-role-assignments.bicep
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/cosmos-container-role-assignments.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/cosmosdb-account-role-assignment.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/cosmosdb-account-role-assignment.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/cosmosdb-account-role-assignment.bicep
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/cosmosdb-account-role-assignment.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/existing-vnet.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/existing-vnet.bicep
new file mode 100644
index 000000000..1b7d4c491
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/existing-vnet.bicep
@@ -0,0 +1,138 @@
+/*
+Virtual Network Module
+This module works with existing virtual networks and required subnets.
+
+1. Flexibility:
+ - Works with any existing VNet address space
+ - Can use existing subnets or create new ones
+ - Cross-resource group support
+
+2. Security Features:
+ - Network isolation
+ - Subnet delegation for containerized workloads
+ - Private endpoint subnet for secure connectivity
+*/
+
+@description('The name of the existing virtual network')
+param vnetName string
+
+@description('Subscription ID of virtual network (if different from current subscription)')
+param vnetSubscriptionId string = subscription().subscriptionId
+
+@description('Resource Group name of the existing VNet (if different from current resource group)')
+param vnetResourceGroupName string = resourceGroup().name
+
+@description('The name of Agents Subnet')
+param agentSubnetName string = 'agent-subnet'
+
+@description('The name of Private Endpoint subnet')
+param peSubnetName string = 'pe-subnet'
+
+@description('The name of MCP subnet for user-deployed Container Apps')
+param mcpSubnetName string = 'mcp-subnet'
+
+@description('Address prefix for the agent subnet (only needed if creating new subnet)')
+param agentSubnetPrefix string = ''
+
+@description('Address prefix for the private endpoint subnet (only needed if creating new subnet)')
+param peSubnetPrefix string = ''
+
+@description('Address prefix for the MCP subnet (only needed if creating new subnet)')
+param mcpSubnetPrefix string = ''
+
+@description('Set to true to reference the agent subnet as existing (do not create or modify it)')
+param agentSubnetExists bool = false
+
+@description('Set to true to reference the private endpoint subnet as existing (do not create or modify it)')
+param peSubnetExists bool = false
+
+@description('Set to true to reference the MCP subnet as existing (do not create or modify it)')
+param mcpSubnetExists bool = false
+
+// Get the address space (array of CIDR strings)
+var vnetAddressSpace = existingVNet.properties.addressSpace.addressPrefixes[0]
+
+var agentSubnetSpaces = empty(agentSubnetPrefix) ? cidrSubnet(vnetAddressSpace, 24, 0) : agentSubnetPrefix
+var peSubnetSpaces = empty(peSubnetPrefix) ? cidrSubnet(vnetAddressSpace, 24, 1) : peSubnetPrefix
+var mcpSubnetSpaces = empty(mcpSubnetPrefix) ? cidrSubnet(vnetAddressSpace, 24, 2) : mcpSubnetPrefix
+
+// Reference the existing virtual network
+resource existingVNet 'Microsoft.Network/virtualNetworks@2024-05-01' existing = {
+ name: vnetName
+ scope: resourceGroup(vnetResourceGroupName)
+}
+
+// Create (or reference) the agent subnet
+// Subnet writes against the same VNet are serialized by ARM, so the three subnet
+// modules below are explicitly chained via dependsOn to avoid AnotherOperationInProgress.
+module agentSubnet 'subnet.bicep' = {
+ name: 'agent-subnet-${uniqueString(deployment().name, agentSubnetName)}'
+ scope: resourceGroup(vnetResourceGroupName)
+ params: {
+ vnetName: vnetName
+ subnetName: agentSubnetName
+ addressPrefix: agentSubnetSpaces
+ subnetExists: agentSubnetExists
+ delegations: [
+ {
+ name: 'Microsoft.App/environments'
+ properties: {
+ serviceName: 'Microsoft.App/environments'
+ }
+ }
+ ]
+ }
+}
+
+// Create (or reference) the private endpoint subnet
+module peSubnet 'subnet.bicep' = {
+ name: 'pe-subnet-${uniqueString(deployment().name, peSubnetName)}'
+ scope: resourceGroup(vnetResourceGroupName)
+ params: {
+ vnetName: vnetName
+ subnetName: peSubnetName
+ addressPrefix: peSubnetSpaces
+ subnetExists: peSubnetExists
+ delegations: []
+ }
+ dependsOn: [
+ agentSubnet
+ ]
+}
+
+// Create (or reference) the MCP subnet for user-deployed Container Apps
+module mcpSubnet 'subnet.bicep' = {
+ name: 'mcp-subnet-${uniqueString(deployment().name, mcpSubnetName)}'
+ scope: resourceGroup(vnetResourceGroupName)
+ params: {
+ vnetName: vnetName
+ subnetName: mcpSubnetName
+ addressPrefix: mcpSubnetSpaces
+ subnetExists: mcpSubnetExists
+ delegations: [
+ {
+ name: 'Microsoft.App/environments'
+ properties: {
+ serviceName: 'Microsoft.App/environments'
+ }
+ }
+ ]
+ }
+ dependsOn: [
+ peSubnet
+ ]
+}
+
+// Output variables
+output peSubnetName string = peSubnetName
+output agentSubnetName string = agentSubnetName
+output mcpSubnetName string = mcpSubnetName
+// Use module outputs (not string concatenation) so consumers get an implicit dependency
+// on the subnet modules and don't race ahead while subnets are still being written.
+output agentSubnetId string = agentSubnet.outputs.subnetId
+output peSubnetId string = peSubnet.outputs.subnetId
+output mcpSubnetId string = mcpSubnet.outputs.subnetId
+output virtualNetworkName string = existingVNet.name
+output virtualNetworkId string = existingVNet.id
+output virtualNetworkResourceGroup string = vnetResourceGroupName
+output virtualNetworkSubscriptionId string = vnetSubscriptionId
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/format-project-workspace-id.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/format-project-workspace-id.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/format-project-workspace-id.bicep
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/format-project-workspace-id.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/network-agent-vnet.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/network-agent-vnet.bicep
similarity index 67%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/network-agent-vnet.bicep
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/network-agent-vnet.bicep
index 7be3fa966..e321cfcb1 100644
--- a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/network-agent-vnet.bicep
+++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/network-agent-vnet.bicep
@@ -34,6 +34,15 @@ param peSubnetPrefix string = ''
@description('Address prefix for the MCP subnet')
param mcpSubnetPrefix string = ''
+@description('Set to true to reference the agent subnet as existing (do not create or modify it)')
+param agentSubnetExists bool = false
+
+@description('Set to true to reference the private endpoint subnet as existing (do not create or modify it)')
+param peSubnetExists bool = false
+
+@description('Set to true to reference the MCP subnet as existing (do not create or modify it)')
+param mcpSubnetExists bool = false
+
// Create new VNet if needed
module newVNet 'vnet.bicep' = if (!useExistingVnet) {
name: 'vnet-deployment'
@@ -63,25 +72,28 @@ module existingVNet 'existing-vnet.bicep' = if (useExistingVnet) {
agentSubnetPrefix: agentSubnetPrefix
peSubnetPrefix: peSubnetPrefix
mcpSubnetPrefix: mcpSubnetPrefix
+ agentSubnetExists: agentSubnetExists
+ peSubnetExists: peSubnetExists
+ mcpSubnetExists: mcpSubnetExists
}
}
// Provide unified outputs regardless of which module was used
output virtualNetworkName string = useExistingVnet
- ? existingVNet.outputs.virtualNetworkName
- : newVNet.outputs.virtualNetworkName
+ ? existingVNet!.outputs.virtualNetworkName
+ : newVNet!.outputs.virtualNetworkName
output virtualNetworkId string = useExistingVnet
- ? existingVNet.outputs.virtualNetworkId
- : newVNet.outputs.virtualNetworkId
+ ? existingVNet!.outputs.virtualNetworkId
+ : newVNet!.outputs.virtualNetworkId
output virtualNetworkSubscriptionId string = useExistingVnet
- ? existingVNet.outputs.virtualNetworkSubscriptionId
- : newVNet.outputs.virtualNetworkSubscriptionId
+ ? existingVNet!.outputs.virtualNetworkSubscriptionId
+ : newVNet!.outputs.virtualNetworkSubscriptionId
output virtualNetworkResourceGroup string = useExistingVnet
- ? existingVNet.outputs.virtualNetworkResourceGroup
- : newVNet.outputs.virtualNetworkResourceGroup
+ ? existingVNet!.outputs.virtualNetworkResourceGroup
+ : newVNet!.outputs.virtualNetworkResourceGroup
output agentSubnetName string = agentSubnetName
output peSubnetName string = peSubnetName
output mcpSubnetName string = mcpSubnetName
-output agentSubnetId string = useExistingVnet ? existingVNet.outputs.agentSubnetId : newVNet.outputs.agentSubnetId
-output peSubnetId string = useExistingVnet ? existingVNet.outputs.peSubnetId : newVNet.outputs.peSubnetId
-output mcpSubnetId string = useExistingVnet ? existingVNet.outputs.mcpSubnetId : newVNet.outputs.mcpSubnetId
+output agentSubnetId string = useExistingVnet ? existingVNet!.outputs.agentSubnetId : newVNet!.outputs.agentSubnetId
+output peSubnetId string = useExistingVnet ? existingVNet!.outputs.peSubnetId : newVNet!.outputs.peSubnetId
+output mcpSubnetId string = useExistingVnet ? existingVNet!.outputs.mcpSubnetId : newVNet!.outputs.mcpSubnetId
diff --git a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/private-dns-zone.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/private-dns-zone.bicep
new file mode 100644
index 000000000..31dcb9e2c
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/private-dns-zone.bicep
@@ -0,0 +1,54 @@
+/*
+Private DNS Zone (per-zone) module
+Creates a private DNS zone (or references an existing one) and links it to the supplied VNet.
+Used by `private-endpoint-and-dns.bicep` in a `for` loop over `existingDnsZones`.
+
+When `enabled` is false the module is a no-op (used to skip optional zones such as Fabric).
+*/
+
+@description('Fully-qualified DNS zone name (e.g. privatelink.openai.azure.com).')
+param zoneName string
+
+@description('Resource group of an existing zone. Empty means create the zone in this RG.')
+param existingResourceGroup string = ''
+
+@description('Subscription ID of an existing zone. Empty defaults to the current subscription. Only used when existingResourceGroup is non-empty.')
+param existingSubscriptionId string = ''
+
+@description('ARM ID of the VNet to link the zone to.')
+param vnetId string
+
+@description('Suffix used to make the vnet-link name unique.')
+param suffix string
+
+@description('Disable the entire module (zone, link, output). Used to skip optional zones.')
+param enabled bool = true
+
+var shouldCreate = enabled && empty(existingResourceGroup)
+var shouldReference = enabled && !empty(existingResourceGroup)
+var effectiveExistingSubscriptionId = empty(existingSubscriptionId) ? subscription().subscriptionId : existingSubscriptionId
+
+resource newZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (shouldCreate) {
+ name: zoneName
+ location: 'global'
+}
+
+resource existingZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = if (shouldReference) {
+ name: zoneName
+ scope: resourceGroup(effectiveExistingSubscriptionId, existingResourceGroup)
+}
+
+// Only link when we own the zone. If the user supplied an existing zone in another RG,
+// link management is their responsibility (and we may not have rights to write into that RG).
+resource link 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = if (shouldCreate) {
+ parent: newZone
+ name: '${replace(zoneName, '.', '-')}-${suffix}-link'
+ location: 'global'
+ properties: {
+ virtualNetwork: { id: vnetId }
+ registrationEnabled: false
+ }
+}
+
+output zoneId string = enabled ? (shouldCreate ? newZone.id : existingZone.id) : ''
+output zoneName string = zoneName
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/private-endpoint-and-dns.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/private-endpoint-and-dns.bicep
similarity index 54%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/private-endpoint-and-dns.bicep
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/private-endpoint-and-dns.bicep
index 96387c417..4693cdbeb 100644
--- a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/private-endpoint-and-dns.bicep
+++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/private-endpoint-and-dns.bicep
@@ -64,15 +64,15 @@ param cosmosDBSubscriptionId string = subscription().subscriptionId
@description('Resource group name for Cosmos DB account')
param cosmosDBResourceGroupName string = resourceGroup().name
-@description('Map of DNS zone FQDNs to resource group names. If provided, reference existing DNS zones in this resource group instead of creating them.')
+@description('Map of DNS zone FQDNs to an object describing where the zone lives. Each value must be an object with optional `subscriptionId` and `resourceGroup` properties. Empty `resourceGroup` means "create the zone in this deployment\'s resource group". Non-empty `resourceGroup` references an existing zone; empty `subscriptionId` defaults to the current subscription.')
param existingDnsZones object = {
- 'privatelink.services.ai.azure.com': ''
- 'privatelink.openai.azure.com': ''
- 'privatelink.cognitiveservices.azure.com': ''
- 'privatelink.search.windows.net': ''
- 'privatelink.blob.${environment().suffixes.storage}': ''
- 'privatelink.documents.azure.com': ''
- 'privatelink.fabric.microsoft.com': ''
+ 'privatelink.services.ai.azure.com': { subscriptionId: '', resourceGroup: '' }
+ 'privatelink.openai.azure.com': { subscriptionId: '', resourceGroup: '' }
+ 'privatelink.cognitiveservices.azure.com': { subscriptionId: '', resourceGroup: '' }
+ 'privatelink.search.windows.net': { subscriptionId: '', resourceGroup: '' }
+ 'privatelink.blob.${environment().suffixes.storage}': { subscriptionId: '', resourceGroup: '' }
+ 'privatelink.documents.azure.com': { subscriptionId: '', resourceGroup: '' }
+ 'privatelink.fabric.microsoft.com': { subscriptionId: '', resourceGroup: '' }
}
// ---- Resource references ----
@@ -219,14 +219,16 @@ resource fabricPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' =
}
}
-/* -------------------------------------------- Private DNS Zones -------------------------------------------- */
+/* -------------------------------------------- Private DNS Zones --------------------------------------------
-// Format: 1) Private DNS Zone
-// 2) Link Private DNS Zone to VNet
-// 3) Create DNS Zone Group for Private Endpoint
+ This block used to declare 7 zones Γ 3 resources each (~150 lines). It is now a single
+ `for` loop over `existingDnsZones`, with one tiny sub-module (`private-dns-zone.bicep`)
+ per zone. Per-PE DNS zone groups below look up zone IDs by zone name via `indexOf`.
-// Private DNS Zone for AI Services (Account)
-// 1) Enables custom DNS resolution for AI Services private endpoint
+ To skip the optional Fabric zone when no Fabric workspace is provided, the corresponding
+ module call is disabled (`enabled: false`) instead of being filtered out β keeping the
+ array indices stable and `indexOf` lookups safe.
+*/
var aiServicesDnsZoneName = 'privatelink.services.ai.azure.com'
var openAiDnsZoneName = 'privatelink.openai.azure.com'
@@ -236,178 +238,30 @@ var storageDnsZoneName = 'privatelink.blob.${environment().suffixes.storage}'
var cosmosDBDnsZoneName = 'privatelink.documents.azure.com'
var fabricDnsZoneName = 'privatelink.fabric.microsoft.com'
-// ---- DNS Zone Resource Group lookups ----
-var aiServicesDnsZoneRG = existingDnsZones[aiServicesDnsZoneName]
-var openAiDnsZoneRG = existingDnsZones[openAiDnsZoneName]
-var cognitiveServicesDnsZoneRG = existingDnsZones[cognitiveServicesDnsZoneName]
-var aiSearchDnsZoneRG = existingDnsZones[aiSearchDnsZoneName]
-var storageDnsZoneRG = existingDnsZones[storageDnsZoneName]
-var cosmosDBDnsZoneRG = existingDnsZones[cosmosDBDnsZoneName]
-var fabricDnsZoneRG = existingDnsZones.?fabricDnsZoneName ?? ''
-
-// ---- DNS Zone Resources and References ----
-resource aiServicesPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (empty(aiServicesDnsZoneRG)) {
- name: aiServicesDnsZoneName
- location: 'global'
-}
-
-// Reference existing private DNS zone if provided
-resource existingAiServicesPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = if (!empty(aiServicesDnsZoneRG)) {
- name: aiServicesDnsZoneName
- scope: resourceGroup(aiServicesDnsZoneRG)
-}
-//creating condition if user pass existing dns zones or not
-var aiServicesDnsZoneId = empty(aiServicesDnsZoneRG) ? aiServicesPrivateDnsZone.id : existingAiServicesPrivateDnsZone.id
-
-resource openAiPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (empty(openAiDnsZoneRG)) {
- name: openAiDnsZoneName
- location: 'global'
-}
-
-// Reference existing private DNS zone if provided
-resource existingOpenAiPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = if (!empty(openAiDnsZoneRG)) {
- name: openAiDnsZoneName
- scope: resourceGroup(openAiDnsZoneRG)
-}
-//creating condition if user pass existing dns zones or not
-var openAiDnsZoneId = empty(openAiDnsZoneRG) ? openAiPrivateDnsZone.id : existingOpenAiPrivateDnsZone.id
-
-resource cognitiveServicesPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (empty(cognitiveServicesDnsZoneRG)) {
- name: cognitiveServicesDnsZoneName
- location: 'global'
-}
-
-// Reference existing private DNS zone if provided
-resource existingCognitiveServicesPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = if (!empty(cognitiveServicesDnsZoneRG)) {
- name: cognitiveServicesDnsZoneName
- scope: resourceGroup(cognitiveServicesDnsZoneRG)
-}
-//creating condition if user pass existing dns zones or not
-var cognitiveServicesDnsZoneId = empty(cognitiveServicesDnsZoneRG)
- ? cognitiveServicesPrivateDnsZone.id
- : existingCognitiveServicesPrivateDnsZone.id
-
-resource aiSearchPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (empty(aiSearchDnsZoneRG)) {
- name: aiSearchDnsZoneName
- location: 'global'
-}
-
-// Reference existing private DNS zone if provided
-resource existingAiSearchPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = if (!empty(aiSearchDnsZoneRG)) {
- name: aiSearchDnsZoneName
- scope: resourceGroup(aiSearchDnsZoneRG)
-}
-//creating condition if user pass existing dns zones or not
-var aiSearchDnsZoneId = empty(aiSearchDnsZoneRG) ? aiSearchPrivateDnsZone.id : existingAiSearchPrivateDnsZone.id
-
-resource storagePrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (empty(storageDnsZoneRG)) {
- name: storageDnsZoneName
- location: 'global'
-}
-
-// Reference existing private DNS zone if provided
-resource existingStoragePrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = if (!empty(storageDnsZoneRG)) {
- name: storageDnsZoneName
- scope: resourceGroup(storageDnsZoneRG)
-}
-//creating condition if user pass existing dns zones or not
-var storageDnsZoneId = empty(storageDnsZoneRG) ? storagePrivateDnsZone.id : existingStoragePrivateDnsZone.id
-
-resource cosmosDBPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (empty(cosmosDBDnsZoneRG)) {
- name: cosmosDBDnsZoneName
- location: 'global'
-}
-
-// Reference existing private DNS zone if provided
-resource existingCosmosDBPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = if (!empty(cosmosDBDnsZoneRG)) {
- name: cosmosDBDnsZoneName
- scope: resourceGroup(cosmosDBDnsZoneRG)
-}
-//creating condition if user pass existing dns zones or not
-var cosmosDBDnsZoneId = empty(cosmosDBDnsZoneRG) ? cosmosDBPrivateDnsZone.id : existingCosmosDBPrivateDnsZone.id
-
-// Microsoft Fabric Private DNS Zone - only created if Fabric workspace is provided
-resource fabricPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (fabricPassedIn && empty(fabricDnsZoneRG)) {
- name: fabricDnsZoneName
- location: 'global'
-}
-
-// Reference existing Fabric private DNS zone if provided
-resource existingFabricPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = if (fabricPassedIn && !empty(fabricDnsZoneRG)) {
- name: fabricDnsZoneName
- scope: resourceGroup(fabricDnsZoneRG)
-}
-// Fabric DNS Zone ID - conditional based on whether Fabric is configured
-var fabricDnsZoneId = fabricPassedIn
- ? (empty(fabricDnsZoneRG) ? fabricPrivateDnsZone.id : existingFabricPrivateDnsZone.id)
- : ''
-
-// ---- DNS VNet Links ----
-resource aiServicesLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = if (empty(aiServicesDnsZoneRG)) {
- parent: aiServicesPrivateDnsZone
- location: 'global'
- name: 'aiServices-${suffix}-link'
- properties: {
- virtualNetwork: { id: vnet.id }
- registrationEnabled: false
- }
-}
-resource openAiLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = if (empty(openAiDnsZoneRG)) {
- parent: openAiPrivateDnsZone
- location: 'global'
- name: 'aiServicesOpenAI-${suffix}-link'
- properties: {
- virtualNetwork: { id: vnet.id }
- registrationEnabled: false
- }
-}
-resource cognitiveServicesLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = if (empty(cognitiveServicesDnsZoneRG)) {
- parent: cognitiveServicesPrivateDnsZone
- location: 'global'
- name: 'aiServicesCognitiveServices-${suffix}-link'
- properties: {
- virtualNetwork: { id: vnet.id }
- registrationEnabled: false
+var dnsZoneEntries = items(existingDnsZones)
+var dnsZoneKeys = map(dnsZoneEntries, e => e.key)
+
+module dnsZones 'private-dns-zone.bicep' = [for (entry, i) in dnsZoneEntries: {
+ name: 'dns-${replace(entry.key, '.', '-')}-${suffix}'
+ params: {
+ zoneName: entry.key
+ existingResourceGroup: entry.value.?resourceGroup ?? ''
+ existingSubscriptionId: entry.value.?subscriptionId ?? ''
+ vnetId: vnet.id
+ suffix: suffix
+ // Disable the Fabric zone when no Fabric workspace was supplied β saves a no-op zone.
+ enabled: entry.key == fabricDnsZoneName ? fabricPassedIn : true
}
-}
-resource aiSearchLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = if (empty(aiSearchDnsZoneRG)) {
- parent: aiSearchPrivateDnsZone
- location: 'global'
- name: 'aiSearch-${suffix}-link'
- properties: {
- virtualNetwork: { id: vnet.id }
- registrationEnabled: false
- }
-}
-resource storageLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = if (empty(storageDnsZoneRG)) {
- parent: storagePrivateDnsZone
- location: 'global'
- name: 'storage-${suffix}-link'
- properties: {
- virtualNetwork: { id: vnet.id }
- registrationEnabled: false
- }
-}
-resource cosmosDBLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = if (empty(cosmosDBDnsZoneRG)) {
- parent: cosmosDBPrivateDnsZone
- location: 'global'
- name: 'cosmosDB-${suffix}-link'
- properties: {
- virtualNetwork: { id: vnet.id }
- registrationEnabled: false
- }
-}
+}]
-// Fabric VNet Link - only created if Fabric workspace is provided
-resource fabricLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = if (fabricPassedIn && empty(fabricDnsZoneRG)) {
- parent: fabricPrivateDnsZone
- location: 'global'
- name: 'fabric-${suffix}-link'
- properties: {
- virtualNetwork: { id: vnet.id }
- registrationEnabled: false
- }
-}
+// ---- Per-zone ID lookups (used to wire DNS zone groups onto each Private Endpoint) ----
+var aiServicesDnsZoneId = dnsZones[indexOf(dnsZoneKeys, aiServicesDnsZoneName)].outputs.zoneId
+var openAiDnsZoneId = dnsZones[indexOf(dnsZoneKeys, openAiDnsZoneName)].outputs.zoneId
+var cognitiveServicesDnsZoneId = dnsZones[indexOf(dnsZoneKeys, cognitiveServicesDnsZoneName)].outputs.zoneId
+var aiSearchDnsZoneId = dnsZones[indexOf(dnsZoneKeys, aiSearchDnsZoneName)].outputs.zoneId
+var storageDnsZoneId = dnsZones[indexOf(dnsZoneKeys, storageDnsZoneName)].outputs.zoneId
+var cosmosDBDnsZoneId = dnsZones[indexOf(dnsZoneKeys, cosmosDBDnsZoneName)].outputs.zoneId
+var fabricDnsZoneId = fabricPassedIn ? dnsZones[indexOf(dnsZoneKeys, fabricDnsZoneName)].outputs.zoneId : ''
// ---- DNS Zone Groups ----
resource aiServicesDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = {
@@ -420,11 +274,7 @@ resource aiServicesDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGr
{ name: '${aiAccountName}-dns-cogserv-config', properties: { privateDnsZoneId: cognitiveServicesDnsZoneId } }
]
}
- dependsOn: [
- empty(aiServicesDnsZoneRG) ? aiServicesLink : null
- empty(openAiDnsZoneRG) ? openAiLink : null
- empty(cognitiveServicesDnsZoneRG) ? cognitiveServicesLink : null
- ]
+ // Implicit dependencies on the dnsZones[*] modules via the *DnsZoneId vars above.
}
resource aiSearchDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = {
parent: aiSearchPrivateEndpoint
@@ -434,9 +284,6 @@ resource aiSearchDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGrou
{ name: '${aiSearchName}-dns-config', properties: { privateDnsZoneId: aiSearchDnsZoneId } }
]
}
- dependsOn: [
- empty(aiSearchDnsZoneRG) ? aiSearchLink : null
- ]
}
resource storageDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = {
parent: storagePrivateEndpoint
@@ -446,9 +293,6 @@ resource storageDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroup
{ name: '${storageName}-dns-config', properties: { privateDnsZoneId: storageDnsZoneId } }
]
}
- dependsOn: [
- empty(storageDnsZoneRG) ? storageLink : null
- ]
}
resource cosmosDBDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = {
parent: cosmosDBPrivateEndpoint
@@ -458,9 +302,6 @@ resource cosmosDBDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGrou
{ name: '${cosmosDBName}-dns-config', properties: { privateDnsZoneId: cosmosDBDnsZoneId } }
]
}
- dependsOn: [
- empty(cosmosDBDnsZoneRG) ? cosmosDBLink : null
- ]
}
// Fabric DNS Zone Group - only created if Fabric workspace is provided
@@ -472,7 +313,4 @@ resource fabricDnsGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups
{ name: '${fabricWorkspaceName}-dns-config', properties: { privateDnsZoneId: fabricDnsZoneId } }
]
}
- dependsOn: [
- (fabricPassedIn && empty(fabricDnsZoneRG)) ? fabricLink : null
- ]
}
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/standard-dependent-resources.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/standard-dependent-resources.bicep
similarity index 86%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/standard-dependent-resources.bicep
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/standard-dependent-resources.bicep
index c4c9fb657..9af539a30 100644
--- a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/standard-dependent-resources.bicep
+++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/standard-dependent-resources.bicep
@@ -15,21 +15,21 @@ param azureStorageName string
@description('Name of the new Cosmos DB account')
param cosmosDBName string
-@description('The AI Search Service full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.')
-param aiSearchResourceId string
+@description('The AI Search Service full ARM Resource ID. Optional β leave empty to create a new one.')
+param existingAiSearchResourceId string
-@description('The AI Storage Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.')
-param azureStorageAccountResourceId string
+@description('The AI Storage Account full ARM Resource ID. Optional β leave empty to create a new one.')
+param existingAzureStorageAccountResourceId string
-@description('The Cosmos DB Account full ARM Resource ID. This is an optional field, and if not provided, the resource will be created.')
-param cosmosDBResourceId string
+@description('The Cosmos DB Account full ARM Resource ID. Optional β leave empty to create a new one.')
+param existingCosmosDBResourceId string
// param aiServiceExists bool
param aiSearchExists bool
param azureStorageExists bool
param cosmosDBExists bool
-var cosmosParts = split(cosmosDBResourceId, '/')
+var cosmosParts = split(existingCosmosDBResourceId, '/')
resource existingCosmosDB 'Microsoft.DocumentDB/databaseAccounts@2024-11-15' existing = if (cosmosDBExists) {
name: cosmosParts[8]
@@ -64,7 +64,7 @@ resource cosmosDB 'Microsoft.DocumentDB/databaseAccounts@2024-11-15' = if(!cosmo
}
}
-var acsParts = split(aiSearchResourceId, '/')
+var acsParts = split(existingAiSearchResourceId, '/')
resource existingSearchService 'Microsoft.Search/searchServices@2024-06-01-preview' existing = if (aiSearchExists) {
name: acsParts[8]
@@ -100,7 +100,7 @@ resource aiSearch 'Microsoft.Search/searchServices@2024-06-01-preview' = if(!aiS
}
}
-var azureStorageParts = split(azureStorageAccountResourceId, '/')
+var azureStorageParts = split(existingAzureStorageAccountResourceId, '/')
resource existingAzureStorageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = if (azureStorageExists) {
name: azureStorageParts[8]
diff --git a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/subnet.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/subnet.bicep
new file mode 100644
index 000000000..94088e811
--- /dev/null
+++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/subnet.bicep
@@ -0,0 +1,29 @@
+@description('Name of the virtual network')
+param vnetName string
+
+@description('Name of the subnet')
+param subnetName string
+
+@description('Address prefix for the subnet (only required when creating a new subnet)')
+param addressPrefix string = ''
+
+@description('Array of subnet delegations')
+param delegations array = []
+
+@description('Set to true to reference an existing subnet instead of creating one')
+param subnetExists bool = false
+
+resource newSubnet 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' = if (!subnetExists) {
+ name: '${vnetName}/${subnetName}'
+ properties: {
+ addressPrefix: addressPrefix
+ delegations: delegations
+ }
+}
+
+resource existingSubnet 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' existing = if (subnetExists) {
+ name: '${vnetName}/${subnetName}'
+}
+
+output subnetId string = subnetExists ? existingSubnet.id : newSubnet.id
+output subnetName string = subnetName
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/vnet.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/vnet.bicep
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/modules-network-secured/vnet.bicep
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/vnet.bicep
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/openapi-server/Dockerfile b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/openapi-server/Dockerfile
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/openapi-server/Dockerfile
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/openapi-server/Dockerfile
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/openapi-server/main.py b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/openapi-server/main.py
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/openapi-server/main.py
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/openapi-server/main.py
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/openapi-server/requirements.txt b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/openapi-server/requirements.txt
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/openapi-server/requirements.txt
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/openapi-server/requirements.txt
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/TESTING-GUIDE.md b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/TESTING-GUIDE.md
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/TESTING-GUIDE.md
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/TESTING-GUIDE.md
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/calculator_openapi.json b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/calculator_openapi.json
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/calculator_openapi.json
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/calculator_openapi.json
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/test_a2a_connector_agents_v2.py b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/test_a2a_connector_agents_v2.py
similarity index 95%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/test_a2a_connector_agents_v2.py
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/test_a2a_connector_agents_v2.py
index fd4c2ee45..95eecea13 100644
--- a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/test_a2a_connector_agents_v2.py
+++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/test_a2a_connector_agents_v2.py
@@ -220,6 +220,7 @@ def test_a2a_tool_via_agent(
connection_id: str,
endpoint_url: str = "",
label: str = "A2A Agent",
+ cleanup_agent: bool = True,
):
"""
Test that an agent can use the A2A tool to communicate with a remote agent
@@ -246,15 +247,12 @@ def test_a2a_tool_via_agent(
):
print(f"β Connected to AI Project at {PROJECT_ENDPOINT}")
- # Create A2A tool
a2a_tool = A2APreviewTool(
project_connection_id=connection_id,
)
- # Override endpoint URL if provided
if endpoint_url:
a2a_tool.base_url = endpoint_url
- # Create agent with A2A tool
agent = project_client.agents.create_version(
agent_name="a2a-tool-test",
definition=PromptAgentDefinition(
@@ -271,7 +269,6 @@ def test_a2a_tool_via_agent(
if endpoint_url:
print(f" A2A Endpoint Override: {endpoint_url}")
- # Use streaming to handle A2A response events
print(" Sending request via A2A tool (streaming)...")
full_text = ""
@@ -306,11 +303,13 @@ def test_a2a_tool_via_agent(
display_text = full_text[:500] + "..." if len(full_text) > 500 else full_text
print(f"\nβ Agent response: {display_text}")
- # Cleanup
- project_client.agents.delete_version(
- agent_name=agent.name, agent_version=agent.version
- )
- print(f" Cleaned up agent: {agent.name}")
+ if cleanup_agent:
+ project_client.agents.delete_version(
+ agent_name=agent.name, agent_version=agent.version
+ )
+ print(f" Cleaned up agent: {agent.name}")
+ else:
+ print(f" Preserved agent version: {agent.name}:{agent.version}")
if a2a_call_seen:
print(f"\nβ TEST PASSED: A2A tool via {label}")
@@ -341,7 +340,7 @@ def test_a2a_tool_via_agent(
traceback.print_exc()
# Cleanup agent if created
- if agent is not None:
+ if agent is not None and cleanup_agent:
try:
with (
DefaultAzureCredential() as credential,
@@ -355,6 +354,8 @@ def test_a2a_tool_via_agent(
print(f" Cleaned up agent: {agent.name}")
except Exception:
pass
+ elif agent is not None:
+ print(f" Preserved agent version after failure: {agent.name}:{agent.version}")
return False
@@ -391,6 +392,11 @@ def main():
default=1,
help="Number of retries for agent tests (default: 1)",
)
+ parser.add_argument(
+ "--keep-agent",
+ action="store_true",
+ help="Preserve created agent versions instead of deleting them at the end of the test",
+ )
args = parser.parse_args()
print("=" * 60)
@@ -426,6 +432,7 @@ def main():
A2A_CONNECTION_ID_PUBLIC,
A2A_ENDPOINT_PUBLIC,
"Public A2A Agent",
+ cleanup_agent=not args.keep_agent,
)
if result is not None:
results["agent_public"] = result
@@ -444,6 +451,7 @@ def main():
A2A_CONNECTION_ID_PRIVATE,
A2A_ENDPOINT_PRIVATE,
"Private A2A Agent",
+ cleanup_agent=not args.keep_agent,
)
if result is not None:
results["agent_private"] = result
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/test_agents_v2.py b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/test_agents_v2.py
similarity index 99%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/test_agents_v2.py
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/test_agents_v2.py
index 1cab26e80..1160c8a2c 100644
--- a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/test_agents_v2.py
+++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/test_agents_v2.py
@@ -57,7 +57,7 @@
from azure.ai.projects.models import (
MCPTool,
PromptAgentDefinition,
- AzureAISearchAgentTool,
+ AzureAISearchTool,
AzureAISearchToolResource,
AISearchIndexResource,
AzureAISearchQueryType,
@@ -406,7 +406,7 @@ def test_ai_search_tool():
print(f"β Connected to AI Project at {PROJECT_ENDPOINT}")
# Create AI Search tool with SIMPLE query type (our index doesn't have vector fields)
- search_tool = AzureAISearchAgentTool(
+ search_tool = AzureAISearchTool(
azure_ai_search=AzureAISearchToolResource(indexes=[
AISearchIndexResource(
project_connection_id=AI_SEARCH_CONNECTION_NAME,
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/test_ai_search_tool_agents_v2.py b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/test_ai_search_tool_agents_v2.py
similarity index 93%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/test_ai_search_tool_agents_v2.py
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/test_ai_search_tool_agents_v2.py
index e3eabc56d..02fd3e548 100644
--- a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/test_ai_search_tool_agents_v2.py
+++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/test_ai_search_tool_agents_v2.py
@@ -43,7 +43,7 @@
from azure.ai.projects import AIProjectClient
from azure.ai.projects.models import (
- AzureAISearchAgentTool,
+ AzureAISearchTool,
AzureAISearchToolResource,
AISearchIndexResource,
AzureAISearchQueryType,
@@ -183,7 +183,7 @@ def test_ai_search_connectivity():
return False
-def test_ai_search_tool_via_agent():
+def test_ai_search_tool_via_agent(cleanup_agent: bool = True):
"""
Test AI Search tool via Azure AI Agent.
@@ -218,7 +218,7 @@ def test_ai_search_tool_via_agent():
print(f"β Connected to AI Project at {PROJECT_ENDPOINT}")
# Create AI Search tool with SIMPLE query type
- search_tool = AzureAISearchAgentTool(
+ search_tool = AzureAISearchTool(
azure_ai_search=AzureAISearchToolResource(indexes=[
AISearchIndexResource(
project_connection_id=AI_SEARCH_CONNECTION_NAME,
@@ -263,12 +263,14 @@ def test_ai_search_tool_via_agent():
else:
print(f"\nβ Agent response: {output_text}")
- # Cleanup
- project_client.agents.delete_version(
- agent_name=agent.name,
- agent_version=agent.version
- )
- print(f" Cleaned up agent: {agent.name}")
+ if cleanup_agent:
+ project_client.agents.delete_version(
+ agent_name=agent.name,
+ agent_version=agent.version
+ )
+ print(f" Cleaned up agent: {agent.name}")
+ else:
+ print(f" Preserved agent version: {agent.name}:{agent.version}")
print("\n" + "=" * 60)
print("β TEST PASSED: AI Search tool via agent")
@@ -282,7 +284,7 @@ def test_ai_search_tool_via_agent():
traceback.print_exc()
# Cleanup on failure
- if agent:
+ if agent and cleanup_agent:
try:
project_client.agents.delete_version(
agent_name=agent.name,
@@ -290,6 +292,8 @@ def test_ai_search_tool_via_agent():
)
except:
pass
+ elif agent:
+ print(f" Preserved agent version after failure: {agent.name}:{agent.version}")
return False
@@ -325,9 +329,14 @@ def main():
default=0,
help="Number of times to retry failed tests (default: 0)"
)
-
+ parser.add_argument(
+ "--keep-agent",
+ action="store_true",
+ help="Preserve created agent versions instead of deleting them at the end of the test"
+ )
+
args = parser.parse_args()
-
+
print("=" * 60)
print("AI SEARCH TOOL TEST")
print("=" * 60)
@@ -340,37 +349,33 @@ def main():
print(f" AI Search Endpoint: {AI_SEARCH_ENDPOINT or '(not set - connectivity test skipped)'}")
results = {}
-
- # Run connectivity test
+
if args.test in ["connectivity", "all"]:
result = test_ai_search_connectivity()
if result is not None:
results['connectivity'] = result
- # Run agent test
if args.test in ["agent", "all"]:
for attempt in range(args.retry + 1):
if attempt > 0:
print(f"\n--- Retry attempt {attempt}/{args.retry} ---")
-
- result = test_ai_search_tool_via_agent()
+
+ result = test_ai_search_tool_via_agent(cleanup_agent=not args.keep_agent)
if result is not None:
results['agent'] = result
if result:
- break # Success, no need to retry
+ break
else:
- break # Skipped, no need to retry
+ break
- # Summary
print("\n" + "=" * 60)
print("TEST SUMMARY")
print("=" * 60)
-
+
for test_name, passed in results.items():
status = "β PASSED" if passed else "β FAILED"
print(f" {test_name}: {status}")
- # Exit with appropriate code
all_passed = all(results.values()) if results else True
if all_passed:
print("\n" + "=" * 60)
@@ -383,6 +388,5 @@ def main():
print("=" * 60)
sys.exit(1)
-
if __name__ == "__main__":
main()
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/test_azure_function_agents_v2.py b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/test_azure_function_agents_v2.py
similarity index 96%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/test_azure_function_agents_v2.py
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/test_azure_function_agents_v2.py
index 60531c4b3..2b72eac86 100644
--- a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/test_azure_function_agents_v2.py
+++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/test_azure_function_agents_v2.py
@@ -256,6 +256,7 @@ def test_function_openapi_tool_via_agent(
function_url: str,
label: str = "Azure Function",
expect_storage: bool = False,
+ cleanup_agent: bool = True,
):
"""Test that an agent can call an Azure Function via OpenAPI tool.
@@ -345,11 +346,13 @@ def test_function_openapi_tool_via_agent(
output = response.output_text
print(f"\nβ Agent response: {output}")
- # Cleanup
- project_client.agents.delete_version(
- agent_name=agent.name, agent_version=agent.version
- )
- print(f" Cleaned up agent: {agent.name}")
+ if cleanup_agent:
+ project_client.agents.delete_version(
+ agent_name=agent.name, agent_version=agent.version
+ )
+ print(f" Cleaned up agent: {agent.name}")
+ else:
+ print(f" Preserved agent version: {agent.name}:{agent.version}")
print(f"\nβ TEST PASSED: Azure Function as OpenAPI tool via {label}")
return True
@@ -370,7 +373,7 @@ def test_function_openapi_tool_via_agent(
traceback.print_exc()
- if agent is not None:
+ if agent is not None and cleanup_agent:
try:
with (
DefaultAzureCredential() as credential,
@@ -384,6 +387,8 @@ def test_function_openapi_tool_via_agent(
print(f" Cleaned up agent: {agent.name}")
except Exception:
pass
+ elif agent is not None:
+ print(f" Preserved agent version after failure: {agent.name}:{agent.version}")
return False
@@ -423,6 +428,11 @@ def main():
default=1,
help="Number of retries for agent tests (default: 1)",
)
+ parser.add_argument(
+ "--keep-agent",
+ action="store_true",
+ help="Preserve created agent versions instead of deleting them at the end of the test",
+ )
args = parser.parse_args()
print("=" * 60)
@@ -465,6 +475,7 @@ def main():
FUNCTION_APP_PUBLIC,
"Public Azure Function (no VNet)",
expect_storage=False,
+ cleanup_agent=not args.keep_agent,
)
if result is not None:
results["agent_public"] = result
@@ -482,6 +493,7 @@ def main():
FUNCTION_APP_PRIVATE,
"Private Azure Function (VNet Integration)",
expect_storage=True,
+ cleanup_agent=not args.keep_agent,
)
if result is not None:
results["agent_private"] = result
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/test_fabric_data_agent_v2.py b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/test_fabric_data_agent_v2.py
similarity index 100%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/test_fabric_data_agent_v2.py
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/test_fabric_data_agent_v2.py
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/test_mcp_tools_agents_v2.py b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/test_mcp_tools_agents_v2.py
similarity index 93%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/test_mcp_tools_agents_v2.py
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/test_mcp_tools_agents_v2.py
index 73501680d..ecaeae90d 100644
--- a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/test_mcp_tools_agents_v2.py
+++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/test_mcp_tools_agents_v2.py
@@ -261,7 +261,7 @@ def test_mcp_connectivity(mcp_url: str, label: str = "MCP Server"):
return False
-def test_mcp_tool_via_agent(mcp_url: str, label: str = "MCP Server"):
+def test_mcp_tool_via_agent(mcp_url: str, label: str = "MCP Server", cleanup_agent: bool = True):
"""Test that an agent can use MCP tool via the Data Proxy."""
print("\n" + "=" * 60)
print(f"TEST: MCP Tool via Agent - {label}")
@@ -332,12 +332,14 @@ def test_mcp_tool_via_agent(mcp_url: str, label: str = "MCP Server"):
print(f"\nβ Agent response: {response.output_text}")
- # Cleanup
- project_client.agents.delete_version(
- agent_name=agent.name,
- agent_version=agent.version
- )
- print(f" Cleaned up agent: {agent.name}")
+ if cleanup_agent:
+ project_client.agents.delete_version(
+ agent_name=agent.name,
+ agent_version=agent.version
+ )
+ print(f" Cleaned up agent: {agent.name}")
+ else:
+ print(f" Preserved agent version: {agent.name}:{agent.version}")
print(f"\nβ TEST PASSED: MCP tool via {label}")
return True
@@ -361,7 +363,7 @@ def test_mcp_tool_via_agent(mcp_url: str, label: str = "MCP Server"):
traceback.print_exc()
# Cleanup agent if created
- if agent is not None:
+ if agent is not None and cleanup_agent:
try:
with (
DefaultAzureCredential() as credential,
@@ -371,6 +373,8 @@ def test_mcp_tool_via_agent(mcp_url: str, label: str = "MCP Server"):
print(f" Cleaned up agent: {agent.name}")
except:
pass
+ elif agent is not None:
+ print(f" Preserved agent version after failure: {agent.name}:{agent.version}")
return False
@@ -389,6 +393,11 @@ def main():
default=1,
help="Number of retries for agent tests (default: 1)"
)
+ parser.add_argument(
+ "--keep-agent",
+ action="store_true",
+ help="Preserve created agent versions instead of deleting them at the end of the test"
+ )
args = parser.parse_args()
print("=" * 60)
@@ -411,7 +420,11 @@ def main():
for attempt in range(args.retry):
if attempt > 0:
print(f"\n Retry attempt {attempt + 1}/{args.retry}...")
- result = test_mcp_tool_via_agent(MCP_SERVER_PUBLIC, "Public MCP Server")
+ result = test_mcp_tool_via_agent(
+ MCP_SERVER_PUBLIC,
+ "Public MCP Server",
+ cleanup_agent=not args.keep_agent,
+ )
if result:
results['agent_public'] = True
break
@@ -423,7 +436,11 @@ def main():
for attempt in range(args.retry):
if attempt > 0:
print(f"\n Retry attempt {attempt + 1}/{args.retry}...")
- result = test_mcp_tool_via_agent(MCP_SERVER_PRIVATE, "Private MCP Server")
+ result = test_mcp_tool_via_agent(
+ MCP_SERVER_PRIVATE,
+ "Private MCP Server",
+ cleanup_agent=not args.keep_agent,
+ )
if result:
results['agent_private'] = True
break
diff --git a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/test_openapi_tool_agents_v2.py b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/test_openapi_tool_agents_v2.py
similarity index 94%
rename from infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/test_openapi_tool_agents_v2.py
rename to infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/test_openapi_tool_agents_v2.py
index 9a0381627..37b2f31c3 100644
--- a/infrastructure/infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup/tests/test_openapi_tool_agents_v2.py
+++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/tests/test_openapi_tool_agents_v2.py
@@ -210,7 +210,7 @@ def test_openapi_connectivity(server_url: str, label: str = "OpenAPI Server"):
return False
-def test_openapi_tool_via_agent(server_url: str, label: str = "OpenAPI Server"):
+def test_openapi_tool_via_agent(server_url: str, label: str = "OpenAPI Server", cleanup_agent: bool = True):
"""Test that an agent can use an OpenAPI tool via the Data Proxy."""
print("\n" + "=" * 60)
print(f"TEST: OpenAPI Tool via Agent - {label}")
@@ -282,11 +282,13 @@ def test_openapi_tool_via_agent(server_url: str, label: str = "OpenAPI Server"):
print(f"\nβ Agent response: {response.output_text}")
- # Cleanup
- project_client.agents.delete_version(
- agent_name=agent.name, agent_version=agent.version
- )
- print(f" Cleaned up agent: {agent.name}")
+ if cleanup_agent:
+ project_client.agents.delete_version(
+ agent_name=agent.name, agent_version=agent.version
+ )
+ print(f" Cleaned up agent: {agent.name}")
+ else:
+ print(f" Preserved agent version: {agent.name}:{agent.version}")
print(f"\nβ TEST PASSED: OpenAPI tool via {label}")
return True
@@ -310,7 +312,7 @@ def test_openapi_tool_via_agent(server_url: str, label: str = "OpenAPI Server"):
traceback.print_exc()
# Cleanup agent if created
- if agent is not None:
+ if agent is not None and cleanup_agent:
try:
with (
DefaultAzureCredential() as credential,
@@ -324,6 +326,8 @@ def test_openapi_tool_via_agent(server_url: str, label: str = "OpenAPI Server"):
print(f" Cleaned up agent: {agent.name}")
except Exception:
pass
+ elif agent is not None:
+ print(f" Preserved agent version after failure: {agent.name}:{agent.version}")
return False
@@ -358,6 +362,11 @@ def main():
default=1,
help="Number of retries for agent tests (default: 1)",
)
+ parser.add_argument(
+ "--keep-agent",
+ action="store_true",
+ help="Preserve created agent versions instead of deleting them at the end of the test",
+ )
args = parser.parse_args()
print("=" * 60)
@@ -371,7 +380,6 @@ def main():
results = {}
- # Connectivity test
if args.test in ["public", "all"] and OPENAPI_SERVER_PUBLIC:
result = test_openapi_connectivity(OPENAPI_SERVER_PUBLIC, "Public OpenAPI Server")
if result is not None:
@@ -384,13 +392,14 @@ def main():
if result is not None:
results["connectivity_private"] = result
- # Agent tests: OpenAPI Tool via Agent (Public)
if args.test in ["public", "all"] and OPENAPI_SERVER_PUBLIC:
for attempt in range(args.retry):
if attempt > 0:
print(f"\n Retry attempt {attempt + 1}/{args.retry}...")
result = test_openapi_tool_via_agent(
- OPENAPI_SERVER_PUBLIC, "Public OpenAPI Server"
+ OPENAPI_SERVER_PUBLIC,
+ "Public OpenAPI Server",
+ cleanup_agent=not args.keep_agent,
)
if result is not None:
results["agent_public"] = result
@@ -400,13 +409,14 @@ def main():
if "agent_public" not in results:
results["agent_public"] = False
- # Agent tests: OpenAPI Tool via Agent (Private)
if args.test in ["private", "all"] and OPENAPI_SERVER_PRIVATE:
for attempt in range(args.retry):
if attempt > 0:
print(f"\n Retry attempt {attempt + 1}/{args.retry}...")
result = test_openapi_tool_via_agent(
- OPENAPI_SERVER_PRIVATE, "Private OpenAPI Server"
+ OPENAPI_SERVER_PRIVATE,
+ "Private OpenAPI Server",
+ cleanup_agent=not args.keep_agent,
)
if result is not None:
results["agent_private"] = result
@@ -416,7 +426,6 @@ def main():
if "agent_private" not in results:
results["agent_private"] = False
- # Summary
print("\n" + "=" * 60)
print("TEST SUMMARY")
print("=" * 60)
diff --git a/infrastructure/infrastructure-setup-bicep/40-basic-agent-setup/README.md b/infrastructure/infrastructure-setup-bicep/40-basic-agent-setup/README.md
index a62fbf75c..6ac8db7fd 100644
--- a/infrastructure/infrastructure-setup-bicep/40-basic-agent-setup/README.md
+++ b/infrastructure/infrastructure-setup-bicep/40-basic-agent-setup/README.md
@@ -9,9 +9,9 @@ All agents created in this project will automatically use Microsoft managed, mul
### Prerequisites
1. To deploy the template, you must have the following roles:
- * **Azure AI Account Owner** or **Contributor**
+ * **Foundry Account Owner** or **Contributor**
1. To create your first agent you must have the permissions:
- * **Azure AI User**
+ * **Foundry User**
For more information, [see the getting started guide.](https://learn.microsoft.com/en-us/azure/ai-services/agents/environment-setup)
diff --git a/infrastructure/infrastructure-setup-bicep/41-standard-agent-setup/README.md b/infrastructure/infrastructure-setup-bicep/41-standard-agent-setup/README.md
index b4e8c2673..a9e389a68 100644
--- a/infrastructure/infrastructure-setup-bicep/41-standard-agent-setup/README.md
+++ b/infrastructure/infrastructure-setup-bicep/41-standard-agent-setup/README.md
@@ -2,7 +2,7 @@
## Required Permissions
1. To deploy this template and create a Standard Setup project you need the follow permissions:
- * **Azure AI Account Owner**
+ * **Foundry Account Owner**
* **Role Based Access Administrator**
For more information on the setup process, [see the getting started documentation.](https://learn.microsoft.com/en-us/azure/ai-services/agents/environment-setup)
diff --git a/infrastructure/infrastructure-setup-bicep/42-basic-agent-setup-with-customization/README.md b/infrastructure/infrastructure-setup-bicep/42-basic-agent-setup-with-customization/README.md
index ff298adee..49eb9f5f4 100644
--- a/infrastructure/infrastructure-setup-bicep/42-basic-agent-setup-with-customization/README.md
+++ b/infrastructure/infrastructure-setup-bicep/42-basic-agent-setup-with-customization/README.md
@@ -11,8 +11,8 @@ All agents created in this project will automatically use Microsoft-managed, mul
## Prerequisites
1. **Azure roles**
- - To deploy the template: **Azure AI Account Owner** or **Contributor**
- - To create your first agent: **Azure AI User**
+ - To deploy the template: **Foundry Account Owner** or **Contributor**
+ - To create your first agent: **Foundry User**
2. **Existing Azure OpenAI resource** β you must already have one.
Get the resource id in **Azure Portal**
diff --git a/infrastructure/infrastructure-setup-bicep/43-standard-agent-setup-with-customization/README.md b/infrastructure/infrastructure-setup-bicep/43-standard-agent-setup-with-customization/README.md
index 6be8e848c..902c4a030 100644
--- a/infrastructure/infrastructure-setup-bicep/43-standard-agent-setup-with-customization/README.md
+++ b/infrastructure/infrastructure-setup-bicep/43-standard-agent-setup-with-customization/README.md
@@ -3,7 +3,7 @@
## Required Permissions
1. To deploy this template and create a Standard Setup project you need the follow permissions:
- * **Azure AI Account Owner**
+ * **Foundry Account Owner**
* **Role Based Access Administrator**
For more information on the setup process, [see the getting started documentation.](https://learn.microsoft.com/en-us/azure/ai-services/agents/environment-setup)
diff --git a/infrastructure/infrastructure-setup-terraform/00-basic-azurerm/README.md b/infrastructure/infrastructure-setup-terraform/00-basic-azurerm/README.md
index c79b01842..73e33ec6f 100644
--- a/infrastructure/infrastructure-setup-terraform/00-basic-azurerm/README.md
+++ b/infrastructure/infrastructure-setup-terraform/00-basic-azurerm/README.md
@@ -23,9 +23,9 @@ This infrastructure-as-code (IaC) solution deploys Microsoft Foundry with public
- **Workload Subscription**
- **Role Based Access Control Administrator**: Needed over the resource group to create the relevant role assignments
- **Network Contributor**: Needed over the resource group to create virtual network and Private Endpoint resources
- - **Azure AI Account Owner**: Needed to create a cognitive services account and project
+ - **Foundry Account Owner**: Needed to create a cognitive services account and project
- **Owner or Role Based Access Administrator**: Needed to assign RBAC to the required resources (Cosmos DB, Azure AI Search, Storage)
- - **Azure AI User**: Needed to create and edit agents
+ - **Foundry User**: Needed to create and edit agents
2. **Register Resource Providers**
diff --git a/infrastructure/infrastructure-setup-terraform/00-basic/README.md b/infrastructure/infrastructure-setup-terraform/00-basic/README.md
index b5a846650..323249d17 100644
--- a/infrastructure/infrastructure-setup-terraform/00-basic/README.md
+++ b/infrastructure/infrastructure-setup-terraform/00-basic/README.md
@@ -23,9 +23,9 @@ This infrastructure-as-code (IaC) solution deploys Azure AI Foundry with public
- **Workload Subscription**
- **Role Based Access Control Administrator**: Needed over the resource group to create the relevant role assignments
- **Network Contributor**: Needed over the resource group to create virtual network and Private Endpoint resources
- - **Azure AI Account Owner**: Needed to create a cognitive services account and project
+ - **Foundry Account Owner**: Needed to create a cognitive services account and project
- **Owner or Role Based Access Administrator**: Needed to assign RBAC to the required resources (Cosmos DB, Azure AI Search, Storage)
- - **Azure AI User**: Needed to create and edit agents
+ - **Foundry User**: Needed to create and edit agents
2. **Register Resource Providers**
diff --git a/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/README.md b/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/README.md
index e221e47e1..8f2572cdb 100644
--- a/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/README.md
+++ b/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/README.md
@@ -36,9 +36,9 @@ This infrastructure-as-code (IaC) solution deploys a network-secured Azure AI ag
- **Workload Subscription**
- **Role Based Access Control Administrator**: Needed over the resource group to create the relevant role assignments
- **Network Contributor**: Needed over the resource group to create virtual network and Private Endpoint resources
- - **Azure AI Account Owner**: Needed to create a cognitive services account and project
+ - **Foundry Account Owner**: Needed to create a cognitive services account and project
- **Owner or Role Based Access Administrator**: Needed to assign RBAC to the required resources (Cosmos DB, Azure AI Search, Storage)
- - **Azure AI User**: Needed to create and edit agents
+ - **Foundry User**: Needed to create and edit agents
2. **Register Resource Providers**
@@ -193,7 +193,7 @@ The deployment creates an isolated network environment:
### Post Deployment
-1. Once all resources are provisioned, assign all developers who want to create/edit agents in the project the role: Azure AI User on the project scope.
+1. Once all resources are provisioned, assign all developers who want to create/edit agents in the project the role: Foundry User on the project scope.
### Core Components
diff --git a/infrastructure/infrastructure-setup-terraform/15b-private-network-standard-agent-setup-byovnet/README.md b/infrastructure/infrastructure-setup-terraform/15b-private-network-standard-agent-setup-byovnet/README.md
index dabbb0b94..fa1076d88 100644
--- a/infrastructure/infrastructure-setup-terraform/15b-private-network-standard-agent-setup-byovnet/README.md
+++ b/infrastructure/infrastructure-setup-terraform/15b-private-network-standard-agent-setup-byovnet/README.md
@@ -1,5 +1,5 @@
---
-description: This set of templates demonstrates how to set up Azure AI Agent Service with virtual network isolation with private network links to connect the agent to your secure data using a pre-existing virtual network.
+description: This set of templates demonstrates how to set up Microsoft Foundry Agent Service with virtual network isolation with private network links to connect the agent to your secure data using a pre-existing virtual network.
page_type: sample
products:
- azure
@@ -8,16 +8,8 @@ urlFragment: network-secured-agent
languages:
- hcl
---
-# Updates
-**06-27-2025** Initial release
-# Azure AI Agent Service: Standard Agent Setup with E2E Network Isolation with Bring-Your-Own Virtual Network
-
-> **IMPORTANT**
->
-> Private Class A subnet support is GA and available in the following regions. **Supported regions: Australia East, Brazil South, Canada East, East US, East US 2, France Central, Germany West Central, Italy North, Japan East, South Africa North, South Central US, South India, Spain Central, Sweden Central, UAE North, UK South, West Europe, West US, West US 3.**
->
-> Private Class B and C subnet support is already GA and available in all regions supported by Microsoft Foundry Agent Service. Deployment templates and setup steps are identical for Class A, B, and C subnets. For more on the supported regions of the Microsoft Foundry Agent service, see [Models supported by Microsoft Foundry Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/concepts/model-region-support?tabs=global-standard)
+# Microsoft Foundry Agent Service: Standard Agent Setup with E2E Network Isolation with Bring-Your-Own Virtual Network
## Key Information
@@ -25,9 +17,42 @@ languages:
- **All Foundry workspace resources should be in the same region as the VNet**, including CosmosDB, Storage Account, AI Search, Foundry Account, Project, Managed Identity. The only exception is within the Foundry Account, you may choose to deploy your model to a different region, and any cross-region communication will be handled securely within our network infrastructure.
- **Note:** Your Virtual Network can be in a different resource group than your Foundry workspace resources
-This infrastructure-as-code (IaC) solution deploys a network-secured Azure AI agent environment with private networking and role-based access control (RBAC). It supports scenarios where a customer has a pre-existing virtual network deployed to a workload subscription which is connected to a platform landing zone where the platform components (such as Private DNS Zones) are stored in the same or a separate subscription.
+---
+## Overview
+This infrastructure-as-code (IaC) solution deploys a network-secured Microsoft Foundry agent environment with private networking and role-based access control (RBAC).
+
+Standard setup supports private network isolation through utilizing **Bring Your Own Virtual Network (BYO VNet)** approach, also known as **custom VNet support with subnet delegation.** This template is designed for scenarios where a customer has a pre-existing virtual network deployed to a workload subscription which is connected to a platform landing zone where the platform components (such as Private DNS Zones) are stored in the same or a separate subscription.
+
+This implementation gives you full control over the inbound and outbound communication paths for your agent. You can restrict access to only the resources explicitly required by your agent, such as storage accounts, databases, or APIs, while blocking all other traffic by default. This approach ensures that your agent operates within a tightly scoped network boundary, reducing the risk of data leakage or unauthorized access. By default, this setup simplifies security configuration while enforcing strong isolation guarantees, ensuring that each agent deployment remains secure, compliant, and aligned with enterprise networking policies.
+
+If you wish to deploy a standalone deployment that creates all networking resources for you, use the [15a-private-network-standard-agent-setup deployment](../15a-private-network-standard-agent-setup/README.md).
+
+---
+
+## When to Use This Template
+
+Use this template when you need:
+- **Full end-to-end network isolation** β All resources behind private endpoints with no public internet access
+- **Pre-existing VNet and DNS integration** β You already have a virtual network and Private DNS Zones deployed, possibly in a separate subscription
+- **Enterprise landing zone compatibility** β Cross-subscription support for hub-spoke or platform landing zone architectures
+- **Standard agent setup with BYO resources** β Customer-managed Storage, Cosmos DB, and AI Search for data residency and compliance
+- **System Assigned Managed Identity** β Simplified identity management with platform-managed credentials
+
+### Template Decision Guide
-If you wish to deploy a standalone deployment use the [15a-private-network-standard-agent-setup deployment](../15a-private-network-standard-agent-setup/README.md).
+Use the table below to choose the right Terraform infrastructure template for your scenario:
+
+| Template | Agent Type | Networking | Identity | Key Use Case |
+|----------|-----------|------------|----------|-------------|
+| [**15b** (this template)](../15b-private-network-standard-agent-setup-byovnet/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | E2E network isolation with pre-existing VNet and cross-subscription DNS |
+| [**15a**](../15a-private-network-standard-agent-setup/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | Standalone E2E network isolation (creates VNet, DNS zones, and resource group) |
+| [**17**](../17-private-network-standard-user-assigned-identity-agent-setup/) | Standard (BYO resources) | BYO VNet + Private Endpoints | **User Assigned MI** | Same as 15a but with user-managed identity |
+| [**16**](../16-private-network-standard-agent-apim-setup-preview/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | Same as 15a **plus** private APIM integration (preview) |
+| [**19**](../19-hybrid-private-resources-agent-setup/) | Standard (BYO resources) | Hybrid (selective private/public) | System Assigned MI | Hybrid networking with selective private endpoints |
+| [**10**](../10-private-network-basic/) | Basic (platform-managed) | BYO VNet + Private Endpoints | System Assigned MI | Basic Foundry with private networking β no agent BYO resources |
+| [**41**](../41-standard-agent-setup/) | Standard (BYO resources) | **Public** (no VNet) | System Assigned MI | Standard agents without network isolation |
+
+---
## Prerequisites
@@ -39,9 +64,9 @@ If you wish to deploy a standalone deployment use the [15a-private-network-stand
- **Workload Subscription**
- **Role Based Access Control Administrator**: Needed over the resource group to create relevant role assignments
- **Network Contributor**: Needed over the resource group to create the Private Endpoints
- - **Azure AI Account Owner**: Needed to create a cognitive services account and project
+ - **Foundry Account Owner**: Needed to create a cognitive services account and project
- **Owner or Role Based Access Administrator**: Needed to assign RBAC to the required resources (Cosmos DB, Azure AI Search, Storage)
- - **Azure AI User**: Needed to create and edit agents
+ - **Foundry User**: Needed to create and edit agents
2. **Register Resource Providers**
@@ -80,6 +105,29 @@ If you wish to deploy a standalone deployment use the [15a-private-network-stand
- privatelink.blob.core.windows.net
- privatelink.search.windows.net
- privatelink.documents.azure.com
+
+### Limitations / Known Issues
+
+1. The delegated agent subnet must be exclusively used by a single Foundry account. It cannot be shared across accounts.
+2. The Foundry resource and the virtual network must be in the same Azure region. BYO resources (Storage, Cosmos DB, AI Search) may be in different regions.
+3. Private Class A IP address ranges (10.x.x.x) are only supported in the following regions: **Australia East, Brazil South, Canada East, East US, East US 2, France Central, Germany West Central, Italy North, Japan East, South Africa North, South Central US, South India, Spain Central, Sweden Central, UAE North, UK South, West US, West US 3.** Use Class B (172.16.x.x) or C (192.168.x.x) ranges for other regions.
+4. This template does **not** support tools (MCP servers, OpenAPI tools, Azure Functions, A2A) behind the VNet. Use [template 19](../19-hybrid-private-resources-agent-setup/) for that scenario.
+5. There is no upgrade path from BYO VNet (this template) to Managed Virtual Network. A Foundry resource redeployment is required.
+6. All projects within the same Foundry account share model deployments. Per-project model isolation is not supported.
+7. Cosmos DB is deployed as single-region. Multi-region replication must be configured manually post-deployment.
+
+### Account Deletion Prerequisites and Cleanup Guidance
+
+Before deleting an **Account** resource, it is essential to first delete the associated **Account Capability Host**. Failure to do so may result in residual dependenciesβsuch as subnets and other provisioned resources (e.g., ACA applications)βremaining linked to the capability host. This can lead to errors such as **"Subnet already in use"** when attempting to reuse the same subnet in a different account deployment.
+
+**Cleanup Options**
+
+**1. Full Account Removal**: To completely remove an account, you must delete and purge the account. Simply deleting the account is not sufficient, you must purge so that deletion of the associated capability host is triggered. The service will automatically handle the removal of the capability host and any linked resources in the background. To purge the account, use the following [link](https://learn.microsoft.com/en-us/azure/ai-services/recover-purge-resources?tabs=azure-portal#purge-a-deleted-resource). Please allow approximately max of 20 minutes for all resources to be fully unlinked from the account.
+
+**2. Retain Account, Remove Capability Host**: If you intend to retain the account but remove the capability host, you must delete the capability host resource directly. After deletion, allow approximately max of 20 minutes for all resources to be fully unlinked from the account.
+
+> **Important**: Before deleting the account capability host, ensure that the **project capability host** is deleted first.
+
---
## Template Customization
@@ -140,6 +188,21 @@ terraform init
terraform apply
```
+> **Note:** To access a private Foundry resource securely, use one of the following:
+> - A VM or jump box on the virtual network, optionally accessed through Azure Bastion
+> - Azure VPN Gateway
+> - Azure ExpressRoute
+
+### Cleanup
+
+To delete all resources created by this template, run:
+
+```bash
+terraform destroy
+```
+
+> **Important**: If you need to reuse the same subnet, follow the [Account Deletion Prerequisites and Cleanup Guidance](#account-deletion-prerequisites-and-cleanup-guidance) to properly purge the account and wait for the capability host to fully unlink (~20 minutes).
+
## Architecture Overview
The architecture this deployment supports is pictured below with the resources deployed by these templates highlighted in red. You can reference official Microsoft documentation for details on the networking set-up on [Microsoft Learn](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/virtual-networks).
@@ -147,6 +210,46 @@ The architecture this deployment supports is pictured below with the resources d

+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Secure Access (VPN Gateway / ExpressRoute / Azure Bastion) β
+ββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
+ β
+ ββββββββββββββββΌβββββββββββββββ
+ β Microsoft Foundry β
+ β (publicNetworkAccess: β
+ β DISABLED) β
+ β β
+ β ββββββββββββββββββββββββββ β
+ β β Foundry Project β β
+ β β (Agent Workspace) β β
+ β βββββββββββββ¬βββββββββββββ β
+ ββββββββββββββββΌβββββββββββββββ
+ β Subnet Delegation
+ ββββββββββββββββΌβββββββββββββββ
+ β BYO Virtual Network β
+ β (Pre-existing) β
+ β β
+ β ββββββββββββββββββββββββ β
+ β β Agent Subnet β β
+ β β (e.g. 192.168.0.0/24) β β βββ Delegated to
+ β β Microsoft.App/envs β β Microsoft.App/environments
+ β ββββββββββββββββββββββββ β
+ β β
+ β ββββββββββββββββββββββββ β
+ β β PE Subnet β β
+ β β (e.g. 192.168.1.0/24) β β
+ β β β β
+ β β ββββββββββ ββββββββββ β β
+ β β βStorage β βCosmos β β β βββ Private endpoints
+ β β ββββββββββ ββββββββββ β β (no public access)
+ β β ββββββββββ ββββββββββ β β
+ β β βSearch β βFoundry β β β
+ β β ββββββββββ ββββββββββ β β
+ β ββββββββββββββββββββββββ β
+ ββββββββββββββββββββββββββββββββ
+```
+
### Step-by-Step Provisioning Process (main.tf)
@@ -195,22 +298,99 @@ The deployment creates an isolated network environment:
### Post Deployment
-1. Once all resources are provisioned, assign all developers who want to create/edit agents in the project the role: Azure AI User on the project scope.
+1. Once all resources are provisioned, assign all developers who want to create/edit agents in the project the role: Foundry User on the project scope.
### Core Components
1. **Microsoft Foundry Resource**
- Central orchestration point
- Manages service connections
- - Network-isolated capability hosts
+ - Set networking and policy configurations
+
2. **AI Project**
- - Workspace configuration
+ - Defines the workspace configuration
- Service integration
- - Agent deployment
-3. **Supporting Services for Standard Agent Deployment**
- - Azure AI Search
- - CosmosDB
- - Storage Account
+ - Agents are created within a specific project, and each project acts as an isolated workspace. This means:
+ - All agents in the same project share access to the same file storage, thread storage (conversation history), and search indexes.
+ - Data is isolated between projects. Agents in one project cannot access resources from another.
+
+3. **Bring Your Own (BYO) Azure Resources**: ensures all sensitive data remains under customer control. All agents created using our service are stateful, meaning they retain information across interactions. With this setup, agent states are automatically stored in customer-managed, single-tenant resources. The required Bring Your Own Resources include:
+ - **BYO File Storage**: All files uploaded by developers (during agent configuration) or end-users (during interactions) are stored directly in the customer's Azure Storage account.
+ - **BYO Search**: All vector stores created by the agent leverage the customer's Azure AI Search resource.
+ - **BYO Thread Storage**: All customer messages and conversation history will be stored in the customer's own Azure Cosmos DB account.
+
+By bundling these BYO features (file storage, search, and thread storage), the standard setup guarantees that your deployment is secure by default. All data processed by Microsoft Foundry Agent Service is automatically stored at rest in your own Azure resources, helping you meet internal policies, compliance requirements, and enterprise security standards.
+
+### Azure Resources Created
+
+**Microsoft Foundry (Cognitive Services)**
+- Type: Microsoft.CognitiveServices/accounts
+- Kind: AIServices
+- SKU: S0
+- Identity: System-assigned
+- Features:
+ - Custom subdomain name
+ - Disabled public network access
+ - Network ACLs with Azure Services bypass
+
+**AI Model Deployment**
+- Type: Microsoft.CognitiveServices/accounts/deployments
+- SKU: Based on deployment configuration, capacity set by model capacity
+- Model properties:
+ - Name, Format, and Version configurable via variables
+
+**Azure AI Search**
+- Type: Microsoft.Search/searchServices
+- SKU: standard
+- Partition Count: 1
+- Replica Count: 1
+- Hosting Mode: default
+- Features:
+ - Disabled public network access
+ - AAD auth with HTTP 401 challenge
+ - System-assigned managed identity
+
+**Storage Account**
+- Type: Microsoft.Storage/storageAccounts
+- Kind: StorageV2
+- SKU: ZRS or GRS (region dependent)
+- Features:
+ - Blob service
+ - Minimum TLS Version: 1.2
+ - Block public blob access
+ - Disabled public network access
+ - Force Azure AD authentication (SharedKey access disabled)
+
+**Cosmos DB Account**
+- Type: Microsoft.DocumentDB/databaseAccounts
+- Kind: GlobalDocumentDB (SQL API)
+- Consistency Level: Session
+- Database Account Offer Type: Standard
+- Features:
+ - Disabled public network access
+ - Disabled local auth
+ - Single region deployment
+
+### Network Security Design
+
+This implementation utilizes a BYO VNet (Bring Your Own Virtual Network) approach with subnet delegation. The pre-existing virtual network must include subnets for agent delegation and private endpoints.
+
+**Private Endpoints**
+
+Private endpoints ensure secure, internal-only connectivity. Private endpoints are created for the following:
+- Microsoft Foundry
+- Azure AI Search
+- Azure Storage
+- Azure Cosmos DB
+
+**Private DNS Zones**
+
+| Private Link Resource Type | Sub Resource | Private DNS Zone Name | Public DNS Zone Forwarders |
+|----------------------------|--------------|------------------------|-----------------------------|
+| **Microsoft Foundry** | account | `privatelink.cognitiveservices.azure.com`
`privatelink.openai.azure.com`
`privatelink.services.ai.azure.com` | `cognitiveservices.azure.com`
`openai.azure.com`
`services.ai.azure.com` |
+| **Azure AI Search** | searchService| `privatelink.search.windows.net` | `search.windows.net` |
+| **Azure Cosmos DB** | Sql | `privatelink.documents.azure.com` | `documents.azure.com` |
+| **Azure Storage** | blob | `privatelink.blob.core.windows.net` | `blob.core.windows.net` |
---
@@ -241,14 +421,15 @@ The deployment creates an isolated network environment:
- **Cosmos DB for NoSQL**
- Cosmos DB Operator (`230815da-be43-4aae-9cb4-875f7bd000aa`)
- Cosmos DB Built-in Data Contributor
- - Cosmos DB for NoSQL container: `<${projectWorkspaceId}>-thread-message-store`
- - Cosmos DB for NoSQL container: `<${projectWorkspaceId}>-agent-entity-store`
+ - Three containers will automatically be provisioned during the create capability host process:
+ - Cosmos DB for NoSQL container: `<${projectWorkspaceId}>-thread-message-store`
+ - Cosmos DB for NoSQL container: `<${projectWorkspaceId}>-system-thread-message-store`
+ - Cosmos DB for NoSQL container: `<${projectWorkspaceId}>-agent-entity-store`
### Network Security
- Public network access disabled
- Private endpoints for all services
-- Service endpoints for Azure services
- Network ACLs with deny by default
---
diff --git a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/README.md b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/README.md
new file mode 100644
index 000000000..ddc6dff2b
--- /dev/null
+++ b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/README.md
@@ -0,0 +1,185 @@
+---
+description: This template demonstrates how to set up Microsoft Foundry with managed virtual network isolation using Terraform and AzAPI.
+page_type: sample
+products:
+- azure
+- azure-resource-manager
+urlFragment: managed-network-secured-agent
+languages:
+- hcl
+---
+
+# Microsoft Foundry: Standard Agent Setup with Managed Virtual Network (Terraform)
+
+This folder provides a Terraform implementation for deploying Microsoft Foundry with managed virtual network isolation and private connectivity to supporting resources.
+
+## Overview
+
+This infrastructure-as-code (IaC) template provisions a Foundry account and project using preview APIs, then configures a managed network with outbound rules to private endpoints for supporting resources.
+
+The template is designed for scenarios where you want:
+- Managed virtual network isolation (Microsoft-managed network boundary)
+- Foundry project setup for agent workloads
+- Customer-managed storage/search/thread resources (BYO resources created by this template)
+- Private endpoint + Private DNS based connectivity
+
+## What This Template Deploys
+
+Core resources:
+- Resource group (name is suffixed with a random ID)
+- Microsoft Foundry account (`Microsoft.CognitiveServices/accounts`, kind `AIServices`)
+- Foundry project (`Microsoft.CognitiveServices/accounts/projects`)
+- Managed network configuration on the Foundry account
+- Managed network outbound rules for enabled backend resources
+
+Optional backend resources (feature-flag driven):
+- Azure Storage account
+- Azure AI Search service
+- Azure Cosmos DB for NoSQL account
+- Virtual network and subnets
+- Private endpoints
+- Private DNS zones and VNet links
+- Optional Windows VM + Bastion + Key Vault for jump-box access workflows
+
+Project wiring and access setup:
+- Project connections for Storage, AI Search, and Cosmos DB
+- Project capability host (when Storage + Search + Cosmos are all enabled)
+- Required RBAC role assignments for Foundry account and project identities
+
+## Feature Flags
+
+The deployment is controlled by boolean feature flags:
+
+- `enable_networking`
+- `enable_storage`
+- `enable_aisearch`
+- `enable_cosmos`
+- `enable_vm`
+- `enable_dns`
+
+For a full managed-network private topology, set these to `true`:
+- `enable_networking`
+- `enable_storage`
+- `enable_aisearch`
+- `enable_cosmos`
+- `enable_dns`
+
+## Prerequisites
+
+1. Active Azure subscription with permissions to create resources and assign RBAC roles.
+1. Azure CLI authenticated to the target subscription.
+1. Terraform CLI (1.x).
+1. Access to preview API operations used by this template through AzAPI.
+1. Resource providers registered in the target subscription:
+
+```bash
+az provider register --namespace "Microsoft.CognitiveServices"
+az provider register --namespace "Microsoft.Network"
+az provider register --namespace "Microsoft.Storage"
+az provider register --namespace "Microsoft.Search"
+az provider register --namespace "Microsoft.DocumentDB"
+az provider register --namespace "Microsoft.KeyVault"
+az provider register --namespace "Microsoft.ContainerService"
+az provider register --namespace "Microsoft.App"
+```
+
+Recommended project-level post-deployment access:
+- Assign developers the `Foundry User` role on the Foundry project scope.
+
+## Variables
+
+Required:
+
+| Variable | Description |
+|---|---|
+| `subscription_id` | Azure subscription ID used by the `azurerm` provider |
+| `resource_group_name` | Base resource group name (template appends a random suffix) |
+
+Conditionally required:
+
+| Variable | Required when | Description |
+|---|---|---|
+| `vm_admin_username` | `enable_vm = true` | Admin username for the optional Windows VM |
+
+Common optional variables:
+
+| Variable | Default | Description |
+|---|---|---|
+| `location` | `uaenorth` | Azure region for resource deployment |
+| `foundry_identifier` | `foundry` | Prefix used to build Foundry account name |
+| `tags` | `{}` | Tags applied to resources |
+| `allowed_public_ips` | `[]` | Allowed CIDRs for Key Vault network ACLs (VM scenario) |
+| `vnet_name` | `vnet-aifoundry` | VNet name |
+| `vnet_address_prefix` | `10.0.0.0/16` | VNet CIDR |
+| `private_endpoints_subnet_name` | `snet-privateendpoints` | Private endpoint subnet name |
+| `private_endpoints_subnet_prefix` | `10.0.1.0/24` | Private endpoint subnet CIDR |
+| `vm_subnet_name` | `snet-vms` | VM subnet name |
+| `vm_subnet_prefix` | `10.0.2.0/24` | VM subnet CIDR |
+| `bastion_subnet_prefix` | `10.0.3.0/26` | Azure Bastion subnet CIDR |
+| `bastion_name` | `bastion-aifoundry` | Bastion name |
+| `vm_name` | `vm-win2025` | Windows VM name |
+
+Use [terraform.tfvars.example](terraform.tfvars.example) as a starting point.
+
+## Deploy
+
+1. Create a tfvars file from the example:
+
+```bash
+cp terraform.tfvars.example terraform.tfvars
+```
+
+2. Update at least:
+- `subscription_id`
+- `resource_group_name`
+- feature flags for your scenario
+- `vm_admin_username` if `enable_vm = true`
+
+3. Initialize and deploy:
+
+```bash
+terraform init
+terraform plan
+terraform apply
+```
+
+## Important Behavior and Limitations
+
+1. This template uses preview API versions via AzAPI for Foundry account/project/managed network resources.
+1. The managed network is configured with:
+ - `managedNetworkKind = "V2"`
+ - `isolationMode = "AllowInternetOutbound"`
+1. Project capability host creation is gated behind all three backend services being enabled:
+ - `enable_storage = true`
+ - `enable_aisearch = true`
+ - `enable_cosmos = true`
+1. Several explicit waits (`time_sleep`) are used to reduce transient failures from RBAC and outbound-rule propagation.
+1. Resource names are suffixed with a random ID to reduce naming collisions.
+
+## Outputs
+
+The template exposes outputs for key resources, including:
+- Resource group
+- VNet/subnets (when enabled)
+- Storage account (when enabled)
+- Cosmos DB account (when enabled)
+- AI Search service (when enabled)
+- Foundry account name/ID/endpoint
+- Private DNS zone IDs map (when DNS is enabled)
+- VM/Bastion/Key Vault outputs (when VM is enabled)
+
+See [outputs.tf](outputs.tf) for the full list.
+
+## Cleanup
+
+```bash
+terraform destroy
+```
+
+If you plan to reuse network/subnet resources for another Foundry deployment, allow time for managed dependencies to fully unlink after deletion.
+
+## References
+
+- [Configure managed network in Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/)
+- [AzAPI provider documentation](https://registry.terraform.io/providers/Azure/azapi/latest/docs)
+- [AzureRM provider documentation](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs)
\ No newline at end of file
diff --git a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/ai-foundry.tf b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/ai-foundry.tf
similarity index 97%
rename from infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/ai-foundry.tf
rename to infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/ai-foundry.tf
index 52e02d972..3bfe8f6bc 100644
--- a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/ai-foundry.tf
+++ b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/ai-foundry.tf
@@ -1,469 +1,469 @@
-# AI Foundry / Cognitive Services Account using AzAPI for preview features
-resource "azapi_resource" "cognitive_account" {
- type = "Microsoft.CognitiveServices/accounts@2025-10-01-preview"
- name = local.foundry_name
- location = azurerm_resource_group.main.location
- parent_id = azurerm_resource_group.main.id
-
- schema_validation_enabled = false
-
- identity {
- type = "SystemAssigned"
- }
-
- body = {
- sku = {
- name = "S0"
- }
- kind = "AIServices"
- properties = merge(
- {
- allowProjectManagement = true
- apiProperties = {}
- customSubDomainName = local.foundry_name
- disableLocalAuth = true
- networkAcls = {
- defaultAction = "Deny"
- virtualNetworkRules = []
- ipRules = []
- }
- networkInjections = [
- {
- scenario = "agent"
- subnetArmId = ""
- useMicrosoftManagedNetwork = true
- }
- ]
- publicNetworkAccess = "Disabled"
- },
- var.enable_storage ? {
- userOwnedStorage = [
- {
- resourceId = azurerm_storage_account.main[0].id
- }
- ]
- } : {},
- var.enable_cosmos ? {
- userOwnedCosmosDB = [
- {
- resourceId = azurerm_cosmosdb_account.main[0].id
- }
- ]
- } : {},
- var.enable_aisearch ? {
- userOwnedSearch = [
- {
- resourceId = azurerm_search_service.main[0].id
- }
- ]
- } : {}
- )
- }
-
- tags = merge(
- var.tags,
- {
- environment = "lab"
- }
- )
-
- lifecycle {
- ignore_changes = [
- body["properties"]["restore"],
- output
- ]
- }
-
- depends_on = [
- azurerm_storage_account.main,
- azurerm_cosmosdb_account.main,
- azurerm_search_service.main
- ]
-}
-
-# Role Assignment: Network Connection Approver for AI Foundry Account Identity
-# This role is required for the AI Foundry account to approve managed network private endpoint connections
-resource "azurerm_role_assignment" "foundry_network_connection_approver" {
- scope = azurerm_resource_group.main.id
- role_definition_name = "Azure AI Enterprise Network Connection Approver"
- principal_id = azapi_resource.cognitive_account.identity[0].principal_id
-}
-
-# Role Assignment: Storage Blob Data Contributor
-resource "azurerm_role_assignment" "foundry_storage_blob" {
- count = var.enable_storage ? 1 : 0
- scope = azurerm_storage_account.main[0].id
- role_definition_name = "Storage Blob Data Contributor"
- principal_id = azapi_resource.cognitive_account.identity[0].principal_id
-}
-
-# Role Assignment: Contributor on Storage Account
-resource "azurerm_role_assignment" "foundry_storage_contributor" {
- count = var.enable_storage ? 1 : 0
- scope = azurerm_storage_account.main[0].id
- role_definition_name = "Contributor"
- principal_id = azapi_resource.cognitive_account.identity[0].principal_id
-}
-
-# Private Endpoint for AI Foundry
-resource "azurerm_private_endpoint" "cognitive_services" {
- count = var.enable_networking ? 1 : 0
- name = "${local.foundry_name}-pe"
- resource_group_name = azurerm_resource_group.main.name
- location = azurerm_resource_group.main.location
- subnet_id = azurerm_subnet.private_endpoints[0].id
-
- private_service_connection {
- name = "${local.foundry_name}-psc"
- private_connection_resource_id = azapi_resource.cognitive_account.id
- is_manual_connection = false
- subresource_names = ["account"]
- }
-
- dynamic "private_dns_zone_group" {
- for_each = var.enable_dns ? [1] : []
- content {
- name = "cognitive-services-dns-zone-group"
- private_dns_zone_ids = [
- azurerm_private_dns_zone.cognitive_services[0].id,
- azurerm_private_dns_zone.openai[0].id,
- azurerm_private_dns_zone.aifoundry_api[0].id,
- azurerm_private_dns_zone.aifoundry_notebooks[0].id,
- azurerm_private_dns_zone.aifoundry_services[0].id
- ]
- }
- }
-}
-
-# Managed Network Configuration
-resource "azapi_resource" "managed_network" {
- type = "Microsoft.CognitiveServices/accounts/managedNetworks@2025-10-01-preview"
- name = "default"
- parent_id = azapi_resource.cognitive_account.id
-
- schema_validation_enabled = false
-
- body = {
- properties = {
- managedNetwork = {
- isolationMode = "AllowInternetOutbound"
- managedNetworkKind = "V2"
- provisionNetworkNow = true
- }
- }
- }
-}
-
-# Wait for Storage Account to be fully created before creating outbound rule
-resource "time_sleep" "wait_storage" {
- count = var.enable_storage ? 1 : 0
- create_duration = "10m"
-
- depends_on = [
- azurerm_storage_account.main,
- azurerm_private_endpoint.storage_blob
- ]
-}
-
-# Managed Network Outbound Rule for Storage Account
-resource "azapi_resource" "storage_outbound_rule" {
- count = var.enable_storage ? 1 : 0
- type = "Microsoft.CognitiveServices/accounts/managedNetworks/outboundRules@2025-10-01-preview"
- name = "storage-blob-rule"
- parent_id = azapi_resource.managed_network.id
-
- schema_validation_enabled = false
-
- body = {
- properties = {
- type = "PrivateEndpoint"
- destination = {
- serviceResourceId = azurerm_storage_account.main[0].id
- subresourceTarget = "blob"
- }
- category = "UserDefined"
- }
- }
-
- depends_on = [
- time_sleep.wait_storage,
- azurerm_role_assignment.foundry_network_connection_approver,
- azurerm_role_assignment.foundry_storage_blob,
- azurerm_role_assignment.foundry_storage_contributor
- ]
-}
-
-# AI Foundry Project
-resource "azapi_resource" "ai_foundry_project" {
- type = "Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview"
- name = "firstProject"
- location = var.location
- parent_id = azapi_resource.cognitive_account.id
-
- schema_validation_enabled = false
-
- identity {
- type = "SystemAssigned"
- }
-
- body = {
- properties = {
- description = "AI Foundry Project"
- }
- }
-
- depends_on = [
- azapi_resource.cognitive_account
- ]
-}
-
-# Wait for RBAC propagation
-resource "time_sleep" "wait_rbac" {
- create_duration = "60s"
-
- depends_on = [
- azurerm_role_assignment.foundry_storage_blob,
- azurerm_role_assignment.foundry_storage_contributor
- ]
-}
-
-# Role Assignment: Project Identity - Storage Blob Data Contributor
-resource "azurerm_role_assignment" "project_storage_blob" {
- count = var.enable_storage ? 1 : 0
- scope = azurerm_storage_account.main[0].id
- role_definition_name = "Storage Blob Data Contributor"
- principal_id = azapi_resource.ai_foundry_project.identity[0].principal_id
-}
-
-# Role Assignment: Project Identity - AI Search Index Data Contributor
-resource "azurerm_role_assignment" "project_search_index" {
- count = var.enable_aisearch ? 1 : 0
- scope = azurerm_search_service.main[0].id
- role_definition_name = "Search Index Data Contributor"
- principal_id = azapi_resource.ai_foundry_project.identity[0].principal_id
-}
-
-# Role Assignment: Project Identity - AI Search Service Contributor
-resource "azurerm_role_assignment" "project_search_contributor" {
- count = var.enable_aisearch ? 1 : 0
- scope = azurerm_search_service.main[0].id
- role_definition_name = "Search Service Contributor"
- principal_id = azapi_resource.ai_foundry_project.identity[0].principal_id
-}
-
-# Role Assignment: Project Identity - Cosmos DB Account Reader
-resource "azurerm_role_assignment" "project_cosmos_reader" {
- count = var.enable_cosmos ? 1 : 0
- scope = azurerm_cosmosdb_account.main[0].id
- role_definition_name = "Cosmos DB Account Reader Role"
- principal_id = azapi_resource.ai_foundry_project.identity[0].principal_id
-}
-
-# Role Assignment: Project Identity - Cosmos DB Operator (required before capability host)
-resource "azurerm_role_assignment" "project_cosmos_operator" {
- count = var.enable_cosmos ? 1 : 0
- scope = azurerm_cosmosdb_account.main[0].id
- role_definition_name = "Cosmos DB Operator"
- principal_id = azapi_resource.ai_foundry_project.identity[0].principal_id
-}
-
-# Wait for project-level RBAC propagation before creating capability host
-# This prevents capability host creation failures due to permissions not being ready
-resource "time_sleep" "wait_project_rbac" {
- create_duration = "90s"
-
- depends_on = [
- azurerm_role_assignment.project_storage_blob,
- azurerm_role_assignment.project_search_index,
- azurerm_role_assignment.project_search_contributor,
- azurerm_role_assignment.project_cosmos_reader,
- azurerm_role_assignment.project_cosmos_operator
- ]
-}
-
-# Wait for managed network outbound rules to fully provision
-# Outbound rules need additional time beyond creation to be in Succeeded state
-# Azure managed network provisioning can take several minutes
-resource "time_sleep" "wait_outbound_rules" {
- create_duration = "600s"
-
- depends_on = [
- azapi_resource.storage_outbound_rule,
- azapi_resource.cosmos_outbound_rule,
- azapi_resource.aisearch_outbound_rule
- ]
-}
-
-# AI Foundry Project Capability Host (matches Bicep implementation)
-# This configures the capability host at the project level with connection references
-resource "azapi_resource" "project_capability_host" {
- count = var.enable_storage && var.enable_cosmos && var.enable_aisearch ? 1 : 0
-
- type = "Microsoft.CognitiveServices/accounts/projects/capabilityHosts@2025-04-01-preview"
- name = "caphostproj"
- parent_id = azapi_resource.ai_foundry_project.id
-
- schema_validation_enabled = false
-
- body = {
- properties = {
- capabilityHostKind = "Agents"
- vectorStoreConnections = [azurerm_search_service.main[0].name]
- storageConnections = [azurerm_storage_account.main[0].name]
- threadStorageConnections = [azurerm_cosmosdb_account.main[0].name]
- }
- }
-
- depends_on = [
- # Core resources must exist
- azapi_resource.ai_foundry_project,
- azapi_resource.conn_aisearch,
- azapi_resource.conn_cosmosdb,
- azapi_resource.conn_storage,
- # Project role assignments must be complete (matching Bicep dependencies)
- azurerm_role_assignment.project_cosmos_reader,
- azurerm_role_assignment.project_cosmos_operator,
- azurerm_role_assignment.project_storage_blob,
- azurerm_role_assignment.project_search_index,
- azurerm_role_assignment.project_search_contributor,
- # Wait for RBAC propagation
- time_sleep.wait_project_rbac,
- # CRITICAL: All outbound rules must be created AND provisioned before capability host
- # The capability host validates that outbound rules exist and are in Succeeded state
- azapi_resource.storage_outbound_rule,
- azapi_resource.cosmos_outbound_rule,
- azapi_resource.aisearch_outbound_rule,
- time_sleep.wait_outbound_rules
- ]
-}
-
-# Connection: AI Search
-resource "azapi_resource" "conn_aisearch" {
- count = var.enable_aisearch ? 1 : 0
- type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview"
- name = azurerm_search_service.main[0].name
- parent_id = azapi_resource.ai_foundry_project.id
-
- schema_validation_enabled = false
-
- body = {
- properties = {
- category = "CognitiveSearch"
- target = "https://${azurerm_search_service.main[0].name}.search.windows.net"
- authType = "AAD"
- metadata = {
- ApiType = "Azure"
- ResourceId = azurerm_search_service.main[0].id
- location = azurerm_search_service.main[0].location
- }
- }
- }
-}
-
-# Connection: Cosmos DB
-resource "azapi_resource" "conn_cosmosdb" {
- count = var.enable_cosmos ? 1 : 0
- type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview"
- name = azurerm_cosmosdb_account.main[0].name
- parent_id = azapi_resource.ai_foundry_project.id
-
- schema_validation_enabled = false
-
- body = {
- properties = {
- category = "CosmosDb"
- target = azurerm_cosmosdb_account.main[0].endpoint
- authType = "AAD"
- metadata = {
- ApiType = "Azure"
- ResourceId = azurerm_cosmosdb_account.main[0].id
- location = azurerm_cosmosdb_account.main[0].location
- }
- }
- }
-
- depends_on = [
- azapi_resource.conn_aisearch
- ]
-}
-
-# Connection: Storage Account
-resource "azapi_resource" "conn_storage" {
- count = var.enable_storage ? 1 : 0
- type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview"
- name = azurerm_storage_account.main[0].name
- parent_id = azapi_resource.ai_foundry_project.id
-
- schema_validation_enabled = false
-
- body = {
- properties = {
- category = "AzureStorageAccount"
- target = azurerm_storage_account.main[0].primary_blob_endpoint
- authType = "AAD"
- metadata = {
- ApiType = "Azure"
- ResourceId = azurerm_storage_account.main[0].id
- location = azurerm_storage_account.main[0].location
- }
- }
- }
-
- depends_on = [
- azapi_resource.conn_cosmosdb
- ]
-}
-
-# Local variable to format project workspace ID as GUID
-# The project.properties.internalId comes back as a 32-char hex string
-# We need to format it as 8-4-4-4-12 GUID format
-locals {
- # Extract the workspace ID from the project output
- project_workspace_id_raw = try(jsondecode(azapi_resource.ai_foundry_project.output).properties.internalId, "")
-
- # Format as GUID if we have a valid 32-character string
- project_workspace_id_guid = length(local.project_workspace_id_raw) == 32 ? format(
- "%s-%s-%s-%s-%s",
- substr(local.project_workspace_id_raw, 0, 8),
- substr(local.project_workspace_id_raw, 8, 4),
- substr(local.project_workspace_id_raw, 12, 4),
- substr(local.project_workspace_id_raw, 16, 4),
- substr(local.project_workspace_id_raw, 20, 12)
- ) : ""
-}
-
-# Role Assignment: Storage Blob Data Owner with ABAC condition
-# This must be assigned AFTER the capability host is created
-# The condition restricts access to containers starting with workspace ID and ending with -azureml-agent
-resource "azurerm_role_assignment" "project_storage_blob_owner_containers" {
- count = var.enable_storage ? 1 : 0
- scope = azurerm_storage_account.main[0].id
- role_definition_name = "Storage Blob Data Owner"
- principal_id = azapi_resource.ai_foundry_project.identity[0].principal_id
-
- # ABAC condition matching Bicep template
- condition = "((!(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/tags/read'}) AND !(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/filter/action'}) AND !(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/tags/write'})) OR (@Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringStartsWithIgnoreCase '${local.project_workspace_id_guid}' AND @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringLikeIgnoreCase '*-azureml-agent'))"
- condition_version = "2.0"
-
- depends_on = [
- azapi_resource.project_capability_host
- ]
-}
-
-# Role Assignment: Cosmos DB Built-in Data Contributor
-# This must be assigned AFTER the capability host is created
-resource "azurerm_cosmosdb_sql_role_assignment" "project_cosmos_builtin_contributor" {
- count = var.enable_cosmos ? 1 : 0
- resource_group_name = azurerm_resource_group.main.name
- account_name = azurerm_cosmosdb_account.main[0].name
-
- # Cosmos DB Built-in Data Contributor role
- role_definition_id = "${azurerm_cosmosdb_account.main[0].id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002"
-
- principal_id = azapi_resource.ai_foundry_project.identity[0].principal_id
- scope = azurerm_cosmosdb_account.main[0].id
-
- depends_on = [
- azapi_resource.project_capability_host
- ]
-}
+# AI Foundry / Cognitive Services Account using AzAPI for preview features
+resource "azapi_resource" "cognitive_account" {
+ type = "Microsoft.CognitiveServices/accounts@2025-10-01-preview"
+ name = local.foundry_name
+ location = azurerm_resource_group.main.location
+ parent_id = azurerm_resource_group.main.id
+
+ schema_validation_enabled = false
+
+ identity {
+ type = "SystemAssigned"
+ }
+
+ body = {
+ sku = {
+ name = "S0"
+ }
+ kind = "AIServices"
+ properties = merge(
+ {
+ allowProjectManagement = true
+ apiProperties = {}
+ customSubDomainName = local.foundry_name
+ disableLocalAuth = true
+ networkAcls = {
+ defaultAction = "Deny"
+ virtualNetworkRules = []
+ ipRules = []
+ }
+ networkInjections = [
+ {
+ scenario = "agent"
+ subnetArmId = ""
+ useMicrosoftManagedNetwork = true
+ }
+ ]
+ publicNetworkAccess = "Disabled"
+ },
+ var.enable_storage ? {
+ userOwnedStorage = [
+ {
+ resourceId = azurerm_storage_account.main[0].id
+ }
+ ]
+ } : {},
+ var.enable_cosmos ? {
+ userOwnedCosmosDB = [
+ {
+ resourceId = azurerm_cosmosdb_account.main[0].id
+ }
+ ]
+ } : {},
+ var.enable_aisearch ? {
+ userOwnedSearch = [
+ {
+ resourceId = azurerm_search_service.main[0].id
+ }
+ ]
+ } : {}
+ )
+ }
+
+ tags = merge(
+ var.tags,
+ {
+ environment = "lab"
+ }
+ )
+
+ lifecycle {
+ ignore_changes = [
+ body["properties"]["restore"],
+ output
+ ]
+ }
+
+ depends_on = [
+ azurerm_storage_account.main,
+ azurerm_cosmosdb_account.main,
+ azurerm_search_service.main
+ ]
+}
+
+# Role Assignment: Network Connection Approver for AI Foundry Account Identity
+# This role is required for the AI Foundry account to approve managed network private endpoint connections
+resource "azurerm_role_assignment" "foundry_network_connection_approver" {
+ scope = azurerm_resource_group.main.id
+ role_definition_name = "Azure AI Enterprise Network Connection Approver"
+ principal_id = azapi_resource.cognitive_account.identity[0].principal_id
+}
+
+# Role Assignment: Storage Blob Data Contributor
+resource "azurerm_role_assignment" "foundry_storage_blob" {
+ count = var.enable_storage ? 1 : 0
+ scope = azurerm_storage_account.main[0].id
+ role_definition_name = "Storage Blob Data Contributor"
+ principal_id = azapi_resource.cognitive_account.identity[0].principal_id
+}
+
+# Role Assignment: Contributor on Storage Account
+resource "azurerm_role_assignment" "foundry_storage_contributor" {
+ count = var.enable_storage ? 1 : 0
+ scope = azurerm_storage_account.main[0].id
+ role_definition_name = "Contributor"
+ principal_id = azapi_resource.cognitive_account.identity[0].principal_id
+}
+
+# Private Endpoint for AI Foundry
+resource "azurerm_private_endpoint" "cognitive_services" {
+ count = var.enable_networking ? 1 : 0
+ name = "${local.foundry_name}-pe"
+ resource_group_name = azurerm_resource_group.main.name
+ location = azurerm_resource_group.main.location
+ subnet_id = azurerm_subnet.private_endpoints[0].id
+
+ private_service_connection {
+ name = "${local.foundry_name}-psc"
+ private_connection_resource_id = azapi_resource.cognitive_account.id
+ is_manual_connection = false
+ subresource_names = ["account"]
+ }
+
+ dynamic "private_dns_zone_group" {
+ for_each = var.enable_dns ? [1] : []
+ content {
+ name = "cognitive-services-dns-zone-group"
+ private_dns_zone_ids = [
+ azurerm_private_dns_zone.cognitive_services[0].id,
+ azurerm_private_dns_zone.openai[0].id,
+ azurerm_private_dns_zone.aifoundry_api[0].id,
+ azurerm_private_dns_zone.aifoundry_notebooks[0].id,
+ azurerm_private_dns_zone.aifoundry_services[0].id
+ ]
+ }
+ }
+}
+
+# Managed Network Configuration
+resource "azapi_resource" "managed_network" {
+ type = "Microsoft.CognitiveServices/accounts/managedNetworks@2025-10-01-preview"
+ name = "default"
+ parent_id = azapi_resource.cognitive_account.id
+
+ schema_validation_enabled = false
+
+ body = {
+ properties = {
+ managedNetwork = {
+ isolationMode = "AllowInternetOutbound"
+ managedNetworkKind = "V2"
+ provisionNetworkNow = true
+ }
+ }
+ }
+}
+
+# Wait for Storage Account to be fully created before creating outbound rule
+resource "time_sleep" "wait_storage" {
+ count = var.enable_storage ? 1 : 0
+ create_duration = "10m"
+
+ depends_on = [
+ azurerm_storage_account.main,
+ azurerm_private_endpoint.storage_blob
+ ]
+}
+
+# Managed Network Outbound Rule for Storage Account
+resource "azapi_resource" "storage_outbound_rule" {
+ count = var.enable_storage ? 1 : 0
+ type = "Microsoft.CognitiveServices/accounts/managedNetworks/outboundRules@2025-10-01-preview"
+ name = "storage-blob-rule"
+ parent_id = azapi_resource.managed_network.id
+
+ schema_validation_enabled = false
+
+ body = {
+ properties = {
+ type = "PrivateEndpoint"
+ destination = {
+ serviceResourceId = azurerm_storage_account.main[0].id
+ subresourceTarget = "blob"
+ }
+ category = "UserDefined"
+ }
+ }
+
+ depends_on = [
+ time_sleep.wait_storage,
+ azurerm_role_assignment.foundry_network_connection_approver,
+ azurerm_role_assignment.foundry_storage_blob,
+ azurerm_role_assignment.foundry_storage_contributor
+ ]
+}
+
+# AI Foundry Project
+resource "azapi_resource" "ai_foundry_project" {
+ type = "Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview"
+ name = "firstProject"
+ location = var.location
+ parent_id = azapi_resource.cognitive_account.id
+
+ schema_validation_enabled = false
+
+ identity {
+ type = "SystemAssigned"
+ }
+
+ body = {
+ properties = {
+ description = "AI Foundry Project"
+ }
+ }
+
+ depends_on = [
+ azapi_resource.cognitive_account
+ ]
+}
+
+# Wait for RBAC propagation
+resource "time_sleep" "wait_rbac" {
+ create_duration = "60s"
+
+ depends_on = [
+ azurerm_role_assignment.foundry_storage_blob,
+ azurerm_role_assignment.foundry_storage_contributor
+ ]
+}
+
+# Role Assignment: Project Identity - Storage Blob Data Contributor
+resource "azurerm_role_assignment" "project_storage_blob" {
+ count = var.enable_storage ? 1 : 0
+ scope = azurerm_storage_account.main[0].id
+ role_definition_name = "Storage Blob Data Contributor"
+ principal_id = azapi_resource.ai_foundry_project.identity[0].principal_id
+}
+
+# Role Assignment: Project Identity - AI Search Index Data Contributor
+resource "azurerm_role_assignment" "project_search_index" {
+ count = var.enable_aisearch ? 1 : 0
+ scope = azurerm_search_service.main[0].id
+ role_definition_name = "Search Index Data Contributor"
+ principal_id = azapi_resource.ai_foundry_project.identity[0].principal_id
+}
+
+# Role Assignment: Project Identity - AI Search Service Contributor
+resource "azurerm_role_assignment" "project_search_contributor" {
+ count = var.enable_aisearch ? 1 : 0
+ scope = azurerm_search_service.main[0].id
+ role_definition_name = "Search Service Contributor"
+ principal_id = azapi_resource.ai_foundry_project.identity[0].principal_id
+}
+
+# Role Assignment: Project Identity - Cosmos DB Account Reader
+resource "azurerm_role_assignment" "project_cosmos_reader" {
+ count = var.enable_cosmos ? 1 : 0
+ scope = azurerm_cosmosdb_account.main[0].id
+ role_definition_name = "Cosmos DB Account Reader Role"
+ principal_id = azapi_resource.ai_foundry_project.identity[0].principal_id
+}
+
+# Role Assignment: Project Identity - Cosmos DB Operator (required before capability host)
+resource "azurerm_role_assignment" "project_cosmos_operator" {
+ count = var.enable_cosmos ? 1 : 0
+ scope = azurerm_cosmosdb_account.main[0].id
+ role_definition_name = "Cosmos DB Operator"
+ principal_id = azapi_resource.ai_foundry_project.identity[0].principal_id
+}
+
+# Wait for project-level RBAC propagation before creating capability host
+# This prevents capability host creation failures due to permissions not being ready
+resource "time_sleep" "wait_project_rbac" {
+ create_duration = "90s"
+
+ depends_on = [
+ azurerm_role_assignment.project_storage_blob,
+ azurerm_role_assignment.project_search_index,
+ azurerm_role_assignment.project_search_contributor,
+ azurerm_role_assignment.project_cosmos_reader,
+ azurerm_role_assignment.project_cosmos_operator
+ ]
+}
+
+# Wait for managed network outbound rules to fully provision
+# Outbound rules need additional time beyond creation to be in Succeeded state
+# Azure managed network provisioning can take several minutes
+resource "time_sleep" "wait_outbound_rules" {
+ create_duration = "600s"
+
+ depends_on = [
+ azapi_resource.storage_outbound_rule,
+ azapi_resource.cosmos_outbound_rule,
+ azapi_resource.aisearch_outbound_rule
+ ]
+}
+
+# AI Foundry Project Capability Host (matches Bicep implementation)
+# This configures the capability host at the project level with connection references
+resource "azapi_resource" "project_capability_host" {
+ count = var.enable_storage && var.enable_cosmos && var.enable_aisearch ? 1 : 0
+
+ type = "Microsoft.CognitiveServices/accounts/projects/capabilityHosts@2025-04-01-preview"
+ name = "caphostproj"
+ parent_id = azapi_resource.ai_foundry_project.id
+
+ schema_validation_enabled = false
+
+ body = {
+ properties = {
+ capabilityHostKind = "Agents"
+ vectorStoreConnections = [azurerm_search_service.main[0].name]
+ storageConnections = [azurerm_storage_account.main[0].name]
+ threadStorageConnections = [azurerm_cosmosdb_account.main[0].name]
+ }
+ }
+
+ depends_on = [
+ # Core resources must exist
+ azapi_resource.ai_foundry_project,
+ azapi_resource.conn_aisearch,
+ azapi_resource.conn_cosmosdb,
+ azapi_resource.conn_storage,
+ # Project role assignments must be complete (matching Bicep dependencies)
+ azurerm_role_assignment.project_cosmos_reader,
+ azurerm_role_assignment.project_cosmos_operator,
+ azurerm_role_assignment.project_storage_blob,
+ azurerm_role_assignment.project_search_index,
+ azurerm_role_assignment.project_search_contributor,
+ # Wait for RBAC propagation
+ time_sleep.wait_project_rbac,
+ # CRITICAL: All outbound rules must be created AND provisioned before capability host
+ # The capability host validates that outbound rules exist and are in Succeeded state
+ azapi_resource.storage_outbound_rule,
+ azapi_resource.cosmos_outbound_rule,
+ azapi_resource.aisearch_outbound_rule,
+ time_sleep.wait_outbound_rules
+ ]
+}
+
+# Connection: AI Search
+resource "azapi_resource" "conn_aisearch" {
+ count = var.enable_aisearch ? 1 : 0
+ type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview"
+ name = azurerm_search_service.main[0].name
+ parent_id = azapi_resource.ai_foundry_project.id
+
+ schema_validation_enabled = false
+
+ body = {
+ properties = {
+ category = "CognitiveSearch"
+ target = "https://${azurerm_search_service.main[0].name}.search.windows.net"
+ authType = "AAD"
+ metadata = {
+ ApiType = "Azure"
+ ResourceId = azurerm_search_service.main[0].id
+ location = azurerm_search_service.main[0].location
+ }
+ }
+ }
+}
+
+# Connection: Cosmos DB
+resource "azapi_resource" "conn_cosmosdb" {
+ count = var.enable_cosmos ? 1 : 0
+ type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview"
+ name = azurerm_cosmosdb_account.main[0].name
+ parent_id = azapi_resource.ai_foundry_project.id
+
+ schema_validation_enabled = false
+
+ body = {
+ properties = {
+ category = "CosmosDb"
+ target = azurerm_cosmosdb_account.main[0].endpoint
+ authType = "AAD"
+ metadata = {
+ ApiType = "Azure"
+ ResourceId = azurerm_cosmosdb_account.main[0].id
+ location = azurerm_cosmosdb_account.main[0].location
+ }
+ }
+ }
+
+ depends_on = [
+ azapi_resource.conn_aisearch
+ ]
+}
+
+# Connection: Storage Account
+resource "azapi_resource" "conn_storage" {
+ count = var.enable_storage ? 1 : 0
+ type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview"
+ name = azurerm_storage_account.main[0].name
+ parent_id = azapi_resource.ai_foundry_project.id
+
+ schema_validation_enabled = false
+
+ body = {
+ properties = {
+ category = "AzureStorageAccount"
+ target = azurerm_storage_account.main[0].primary_blob_endpoint
+ authType = "AAD"
+ metadata = {
+ ApiType = "Azure"
+ ResourceId = azurerm_storage_account.main[0].id
+ location = azurerm_storage_account.main[0].location
+ }
+ }
+ }
+
+ depends_on = [
+ azapi_resource.conn_cosmosdb
+ ]
+}
+
+# Local variable to format project workspace ID as GUID
+# The project.properties.internalId comes back as a 32-char hex string
+# We need to format it as 8-4-4-4-12 GUID format
+locals {
+ # Extract the workspace ID from the project output
+ project_workspace_id_raw = try(jsondecode(azapi_resource.ai_foundry_project.output).properties.internalId, "")
+
+ # Format as GUID if we have a valid 32-character string
+ project_workspace_id_guid = length(local.project_workspace_id_raw) == 32 ? format(
+ "%s-%s-%s-%s-%s",
+ substr(local.project_workspace_id_raw, 0, 8),
+ substr(local.project_workspace_id_raw, 8, 4),
+ substr(local.project_workspace_id_raw, 12, 4),
+ substr(local.project_workspace_id_raw, 16, 4),
+ substr(local.project_workspace_id_raw, 20, 12)
+ ) : ""
+}
+
+# Role Assignment: Storage Blob Data Owner with ABAC condition
+# This must be assigned AFTER the capability host is created
+# The condition restricts access to containers starting with workspace ID and ending with -azureml-agent
+resource "azurerm_role_assignment" "project_storage_blob_owner_containers" {
+ count = var.enable_storage ? 1 : 0
+ scope = azurerm_storage_account.main[0].id
+ role_definition_name = "Storage Blob Data Owner"
+ principal_id = azapi_resource.ai_foundry_project.identity[0].principal_id
+
+ # ABAC condition matching Bicep template
+ condition = "((!(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/tags/read'}) AND !(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/filter/action'}) AND !(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/tags/write'})) OR (@Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringStartsWithIgnoreCase '${local.project_workspace_id_guid}' AND @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringLikeIgnoreCase '*-azureml-agent'))"
+ condition_version = "2.0"
+
+ depends_on = [
+ azapi_resource.project_capability_host
+ ]
+}
+
+# Role Assignment: Cosmos DB Built-in Data Contributor
+# This must be assigned AFTER the capability host is created
+resource "azurerm_cosmosdb_sql_role_assignment" "project_cosmos_builtin_contributor" {
+ count = var.enable_cosmos ? 1 : 0
+ resource_group_name = azurerm_resource_group.main.name
+ account_name = azurerm_cosmosdb_account.main[0].name
+
+ # Cosmos DB Built-in Data Contributor role
+ role_definition_id = "${azurerm_cosmosdb_account.main[0].id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002"
+
+ principal_id = azapi_resource.ai_foundry_project.identity[0].principal_id
+ scope = azurerm_cosmosdb_account.main[0].id
+
+ depends_on = [
+ azapi_resource.project_capability_host
+ ]
+}
diff --git a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/aisearch.tf b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/aisearch.tf
similarity index 96%
rename from infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/aisearch.tf
rename to infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/aisearch.tf
index 1380f4134..709e9fda3 100644
--- a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/aisearch.tf
+++ b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/aisearch.tf
@@ -1,109 +1,109 @@
-# AI Search Service
-resource "azurerm_search_service" "main" {
- count = var.enable_aisearch ? 1 : 0
- name = local.aisearch_name
- resource_group_name = azurerm_resource_group.main.name
- location = azurerm_resource_group.main.location
- sku = "standard"
- replica_count = 1
- partition_count = 1
-
- public_network_access_enabled = true
-
- identity {
- type = "SystemAssigned"
- }
-
- tags = merge(
- var.tags,
- {
- environment = "lab"
- }
- )
-}
-
-# Private Endpoint for AI Search
-resource "azurerm_private_endpoint" "aisearch" {
- count = var.enable_aisearch && var.enable_networking ? 1 : 0
- name = "${azurerm_search_service.main[0].name}-pe"
- resource_group_name = azurerm_resource_group.main.name
- location = azurerm_resource_group.main.location
- subnet_id = azurerm_subnet.private_endpoints[0].id
-
- private_service_connection {
- name = "${azurerm_search_service.main[0].name}-psc"
- private_connection_resource_id = azurerm_search_service.main[0].id
- is_manual_connection = false
- subresource_names = ["searchService"]
- }
-
- private_dns_zone_group {
- name = "aisearch-dns-zone-group"
- private_dns_zone_ids = [azurerm_private_dns_zone.aisearch[0].id]
- }
-
- tags = merge(
- var.tags,
- {
- environment = "lab"
- }
- )
-}
-
-# Wait for AI Search to be fully created before creating outbound rule
-
-# Wait for AI Search to be fully created before creating outbound rule
-resource "time_sleep" "wait_aisearch" {
- count = var.enable_aisearch ? 1 : 0
- create_duration = "10m"
-
- depends_on = [
- azurerm_search_service.main,
- azurerm_private_endpoint.aisearch
- ]
-}
-
-# Managed Network Outbound Rule for AI Search Service
-resource "azapi_resource" "aisearch_outbound_rule" {
- count = var.enable_aisearch ? 1 : 0
- type = "Microsoft.CognitiveServices/accounts/managedNetworks/outboundRules@2025-10-01-preview"
- name = "aisearch-rule"
- parent_id = azapi_resource.managed_network.id
-
- schema_validation_enabled = false
-
- body = {
- properties = {
- type = "PrivateEndpoint"
- destination = {
- serviceResourceId = azurerm_search_service.main[0].id
- subresourceTarget = "searchService"
- }
- category = "UserDefined"
- }
- }
-
- depends_on = [
- time_sleep.wait_aisearch,
- azurerm_role_assignment.foundry_network_connection_approver,
- azurerm_role_assignment.project_search_index,
- azurerm_role_assignment.project_search_contributor
- ]
-}
-
-# Role Assignment: Current user needs Search Service Contributor
-resource "azurerm_role_assignment" "current_user_search_contributor" {
- count = var.enable_aisearch ? 1 : 0
- scope = azurerm_search_service.main[0].id
- role_definition_name = "Search Service Contributor"
- principal_id = data.azurerm_client_config.current.object_id
-}
-
-# Role Assignment: Current user needs Search Index Data Contributor
-resource "azurerm_role_assignment" "current_user_search_index" {
- count = var.enable_aisearch ? 1 : 0
- scope = azurerm_search_service.main[0].id
- role_definition_name = "Search Index Data Contributor"
- principal_id = data.azurerm_client_config.current.object_id
-}
-
+# AI Search Service
+resource "azurerm_search_service" "main" {
+ count = var.enable_aisearch ? 1 : 0
+ name = local.aisearch_name
+ resource_group_name = azurerm_resource_group.main.name
+ location = azurerm_resource_group.main.location
+ sku = "standard"
+ replica_count = 1
+ partition_count = 1
+
+ public_network_access_enabled = true
+
+ identity {
+ type = "SystemAssigned"
+ }
+
+ tags = merge(
+ var.tags,
+ {
+ environment = "lab"
+ }
+ )
+}
+
+# Private Endpoint for AI Search
+resource "azurerm_private_endpoint" "aisearch" {
+ count = var.enable_aisearch && var.enable_networking ? 1 : 0
+ name = "${azurerm_search_service.main[0].name}-pe"
+ resource_group_name = azurerm_resource_group.main.name
+ location = azurerm_resource_group.main.location
+ subnet_id = azurerm_subnet.private_endpoints[0].id
+
+ private_service_connection {
+ name = "${azurerm_search_service.main[0].name}-psc"
+ private_connection_resource_id = azurerm_search_service.main[0].id
+ is_manual_connection = false
+ subresource_names = ["searchService"]
+ }
+
+ private_dns_zone_group {
+ name = "aisearch-dns-zone-group"
+ private_dns_zone_ids = [azurerm_private_dns_zone.aisearch[0].id]
+ }
+
+ tags = merge(
+ var.tags,
+ {
+ environment = "lab"
+ }
+ )
+}
+
+# Wait for AI Search to be fully created before creating outbound rule
+
+# Wait for AI Search to be fully created before creating outbound rule
+resource "time_sleep" "wait_aisearch" {
+ count = var.enable_aisearch ? 1 : 0
+ create_duration = "10m"
+
+ depends_on = [
+ azurerm_search_service.main,
+ azurerm_private_endpoint.aisearch
+ ]
+}
+
+# Managed Network Outbound Rule for AI Search Service
+resource "azapi_resource" "aisearch_outbound_rule" {
+ count = var.enable_aisearch ? 1 : 0
+ type = "Microsoft.CognitiveServices/accounts/managedNetworks/outboundRules@2025-10-01-preview"
+ name = "aisearch-rule"
+ parent_id = azapi_resource.managed_network.id
+
+ schema_validation_enabled = false
+
+ body = {
+ properties = {
+ type = "PrivateEndpoint"
+ destination = {
+ serviceResourceId = azurerm_search_service.main[0].id
+ subresourceTarget = "searchService"
+ }
+ category = "UserDefined"
+ }
+ }
+
+ depends_on = [
+ time_sleep.wait_aisearch,
+ azurerm_role_assignment.foundry_network_connection_approver,
+ azurerm_role_assignment.project_search_index,
+ azurerm_role_assignment.project_search_contributor
+ ]
+}
+
+# Role Assignment: Current user needs Search Service Contributor
+resource "azurerm_role_assignment" "current_user_search_contributor" {
+ count = var.enable_aisearch ? 1 : 0
+ scope = azurerm_search_service.main[0].id
+ role_definition_name = "Search Service Contributor"
+ principal_id = data.azurerm_client_config.current.object_id
+}
+
+# Role Assignment: Current user needs Search Index Data Contributor
+resource "azurerm_role_assignment" "current_user_search_index" {
+ count = var.enable_aisearch ? 1 : 0
+ scope = azurerm_search_service.main[0].id
+ role_definition_name = "Search Index Data Contributor"
+ principal_id = data.azurerm_client_config.current.object_id
+}
+
diff --git a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/cosmos.tf b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/cosmos.tf
similarity index 97%
rename from infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/cosmos.tf
rename to infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/cosmos.tf
index fd6c8d210..a99841ac3 100644
--- a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/cosmos.tf
+++ b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/cosmos.tf
@@ -1,117 +1,117 @@
-# Cosmos DB Account
-resource "azurerm_cosmosdb_account" "main" {
- count = var.enable_cosmos ? 1 : 0
- name = local.cosmos_name
- resource_group_name = azurerm_resource_group.main.name
- location = azurerm_resource_group.main.location
- offer_type = "Standard"
- kind = "GlobalDocumentDB"
-
- consistency_policy {
- consistency_level = "Session"
- max_interval_in_seconds = 5
- max_staleness_prefix = 100
- }
-
- geo_location {
- location = azurerm_resource_group.main.location
- failover_priority = 0
- }
-
- public_network_access_enabled = false
- network_acl_bypass_for_azure_services = false
- local_authentication_disabled = true
-
- tags = merge(
- var.tags,
- {
- environment = "lab"
- }
- )
-}
-
-# Private Endpoint for Cosmos DB
-resource "azurerm_private_endpoint" "cosmos" {
- count = var.enable_cosmos && var.enable_networking ? 1 : 0
- name = "${azurerm_cosmosdb_account.main[0].name}-pe"
- resource_group_name = azurerm_resource_group.main.name
- location = azurerm_resource_group.main.location
- subnet_id = azurerm_subnet.private_endpoints[0].id
-
- private_service_connection {
- name = "${azurerm_cosmosdb_account.main[0].name}-psc"
- private_connection_resource_id = azurerm_cosmosdb_account.main[0].id
- is_manual_connection = false
- subresource_names = ["Sql"]
- }
-
- private_dns_zone_group {
- name = "cosmos-dns-zone-group"
- private_dns_zone_ids = [azurerm_private_dns_zone.cosmos[0].id]
- }
-
- tags = merge(
- var.tags,
- {
- environment = "lab"
- }
- )
-}
-
-# Role Assignment: AI Foundry Account Identity - Contributor on Cosmos DB
-resource "azurerm_role_assignment" "foundry_cosmos_contributor" {
- count = var.enable_cosmos ? 1 : 0
- scope = azurerm_cosmosdb_account.main[0].id
- role_definition_name = "Contributor"
- principal_id = azapi_resource.cognitive_account.identity[0].principal_id
-}
-
-# Wait for Cosmos DB to be fully created before creating outbound rule
-resource "time_sleep" "wait_cosmos" {
- count = var.enable_cosmos ? 1 : 0
- create_duration = "10m"
-
- depends_on = [
- azurerm_cosmosdb_account.main,
- azurerm_private_endpoint.cosmos
- ]
-}
-
-# Managed Network Outbound Rule for Cosmos DB Account
-resource "azapi_resource" "cosmos_outbound_rule" {
- count = var.enable_cosmos ? 1 : 0
- type = "Microsoft.CognitiveServices/accounts/managedNetworks/outboundRules@2025-10-01-preview"
- name = "cosmos-sql-rule"
- parent_id = azapi_resource.managed_network.id
-
- schema_validation_enabled = false
-
- body = {
- properties = {
- type = "PrivateEndpoint"
- destination = {
- serviceResourceId = azurerm_cosmosdb_account.main[0].id
- subresourceTarget = "Sql"
- }
- category = "UserDefined"
- }
- }
-
- depends_on = [
- time_sleep.wait_cosmos,
- azurerm_role_assignment.foundry_network_connection_approver,
- azurerm_role_assignment.foundry_cosmos_contributor,
- azurerm_role_assignment.project_cosmos_reader,
- azurerm_role_assignment.project_cosmos_operator
- ]
-}
-
-# Role Assignment: Current user needs Cosmos DB Built-in Data Contributor
-resource "azurerm_cosmosdb_sql_role_assignment" "current_user" {
- count = var.enable_cosmos ? 1 : 0
- resource_group_name = azurerm_resource_group.main.name
- account_name = azurerm_cosmosdb_account.main[0].name
- role_definition_id = "${azurerm_cosmosdb_account.main[0].id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002"
- principal_id = data.azurerm_client_config.current.object_id
- scope = azurerm_cosmosdb_account.main[0].id
-}
+# Cosmos DB Account
+resource "azurerm_cosmosdb_account" "main" {
+ count = var.enable_cosmos ? 1 : 0
+ name = local.cosmos_name
+ resource_group_name = azurerm_resource_group.main.name
+ location = azurerm_resource_group.main.location
+ offer_type = "Standard"
+ kind = "GlobalDocumentDB"
+
+ consistency_policy {
+ consistency_level = "Session"
+ max_interval_in_seconds = 5
+ max_staleness_prefix = 100
+ }
+
+ geo_location {
+ location = azurerm_resource_group.main.location
+ failover_priority = 0
+ }
+
+ public_network_access_enabled = false
+ network_acl_bypass_for_azure_services = false
+ local_authentication_disabled = true
+
+ tags = merge(
+ var.tags,
+ {
+ environment = "lab"
+ }
+ )
+}
+
+# Private Endpoint for Cosmos DB
+resource "azurerm_private_endpoint" "cosmos" {
+ count = var.enable_cosmos && var.enable_networking ? 1 : 0
+ name = "${azurerm_cosmosdb_account.main[0].name}-pe"
+ resource_group_name = azurerm_resource_group.main.name
+ location = azurerm_resource_group.main.location
+ subnet_id = azurerm_subnet.private_endpoints[0].id
+
+ private_service_connection {
+ name = "${azurerm_cosmosdb_account.main[0].name}-psc"
+ private_connection_resource_id = azurerm_cosmosdb_account.main[0].id
+ is_manual_connection = false
+ subresource_names = ["Sql"]
+ }
+
+ private_dns_zone_group {
+ name = "cosmos-dns-zone-group"
+ private_dns_zone_ids = [azurerm_private_dns_zone.cosmos[0].id]
+ }
+
+ tags = merge(
+ var.tags,
+ {
+ environment = "lab"
+ }
+ )
+}
+
+# Role Assignment: AI Foundry Account Identity - Contributor on Cosmos DB
+resource "azurerm_role_assignment" "foundry_cosmos_contributor" {
+ count = var.enable_cosmos ? 1 : 0
+ scope = azurerm_cosmosdb_account.main[0].id
+ role_definition_name = "Contributor"
+ principal_id = azapi_resource.cognitive_account.identity[0].principal_id
+}
+
+# Wait for Cosmos DB to be fully created before creating outbound rule
+resource "time_sleep" "wait_cosmos" {
+ count = var.enable_cosmos ? 1 : 0
+ create_duration = "10m"
+
+ depends_on = [
+ azurerm_cosmosdb_account.main,
+ azurerm_private_endpoint.cosmos
+ ]
+}
+
+# Managed Network Outbound Rule for Cosmos DB Account
+resource "azapi_resource" "cosmos_outbound_rule" {
+ count = var.enable_cosmos ? 1 : 0
+ type = "Microsoft.CognitiveServices/accounts/managedNetworks/outboundRules@2025-10-01-preview"
+ name = "cosmos-sql-rule"
+ parent_id = azapi_resource.managed_network.id
+
+ schema_validation_enabled = false
+
+ body = {
+ properties = {
+ type = "PrivateEndpoint"
+ destination = {
+ serviceResourceId = azurerm_cosmosdb_account.main[0].id
+ subresourceTarget = "Sql"
+ }
+ category = "UserDefined"
+ }
+ }
+
+ depends_on = [
+ time_sleep.wait_cosmos,
+ azurerm_role_assignment.foundry_network_connection_approver,
+ azurerm_role_assignment.foundry_cosmos_contributor,
+ azurerm_role_assignment.project_cosmos_reader,
+ azurerm_role_assignment.project_cosmos_operator
+ ]
+}
+
+# Role Assignment: Current user needs Cosmos DB Built-in Data Contributor
+resource "azurerm_cosmosdb_sql_role_assignment" "current_user" {
+ count = var.enable_cosmos ? 1 : 0
+ resource_group_name = azurerm_resource_group.main.name
+ account_name = azurerm_cosmosdb_account.main[0].name
+ role_definition_id = "${azurerm_cosmosdb_account.main[0].id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002"
+ principal_id = data.azurerm_client_config.current.object_id
+ scope = azurerm_cosmosdb_account.main[0].id
+}
diff --git a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/dns.tf b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/dns.tf
similarity index 97%
rename from infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/dns.tf
rename to infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/dns.tf
index dbc0661c0..65c8201ee 100644
--- a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/dns.tf
+++ b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/dns.tf
@@ -1,183 +1,183 @@
-# Private DNS Zones
-resource "azurerm_private_dns_zone" "cognitive_services" {
- count = var.enable_dns ? 1 : 0
- name = "privatelink.cognitiveservices.azure.com"
- resource_group_name = azurerm_resource_group.main.name
-}
-
-resource "azurerm_private_dns_zone" "storage_blob" {
- count = var.enable_dns ? 1 : 0
- name = "privatelink.blob.core.windows.net"
- resource_group_name = azurerm_resource_group.main.name
-}
-
-resource "azurerm_private_dns_zone" "storage_file" {
- count = var.enable_dns ? 1 : 0
- name = "privatelink.file.core.windows.net"
- resource_group_name = azurerm_resource_group.main.name
-}
-
-resource "azurerm_private_dns_zone" "storage_table" {
- count = var.enable_dns ? 1 : 0
- name = "privatelink.table.core.windows.net"
- resource_group_name = azurerm_resource_group.main.name
-}
-
-resource "azurerm_private_dns_zone" "storage_queue" {
- count = var.enable_dns ? 1 : 0
- name = "privatelink.queue.core.windows.net"
- resource_group_name = azurerm_resource_group.main.name
-}
-
-resource "azurerm_private_dns_zone" "key_vault" {
- count = var.enable_dns ? 1 : 0
- name = "privatelink.vaultcore.azure.net"
- resource_group_name = azurerm_resource_group.main.name
-}
-
-resource "azurerm_private_dns_zone" "container_registry" {
- count = var.enable_dns ? 1 : 0
- name = "privatelink.azurecr.io"
- resource_group_name = azurerm_resource_group.main.name
-}
-
-resource "azurerm_private_dns_zone" "openai" {
- count = var.enable_dns ? 1 : 0
- name = "privatelink.openai.azure.com"
- resource_group_name = azurerm_resource_group.main.name
-}
-
-resource "azurerm_private_dns_zone" "aifoundry_api" {
- count = var.enable_dns ? 1 : 0
- name = "privatelink.api.azureml.ms"
- resource_group_name = azurerm_resource_group.main.name
-}
-
-resource "azurerm_private_dns_zone" "aifoundry_notebooks" {
- count = var.enable_dns ? 1 : 0
- name = "privatelink.notebooks.azure.net"
- resource_group_name = azurerm_resource_group.main.name
-}
-
-resource "azurerm_private_dns_zone" "aifoundry_services" {
- count = var.enable_dns ? 1 : 0
- name = "privatelink.services.ai.azure.com"
- resource_group_name = azurerm_resource_group.main.name
-}
-
-resource "azurerm_private_dns_zone" "cosmos" {
- count = var.enable_dns ? 1 : 0
- name = "privatelink.documents.azure.com"
- resource_group_name = azurerm_resource_group.main.name
-}
-
-resource "azurerm_private_dns_zone" "aisearch" {
- count = var.enable_dns ? 1 : 0
- name = "privatelink.search.windows.net"
- resource_group_name = azurerm_resource_group.main.name
-}
-
-# VNet Links
-resource "azurerm_private_dns_zone_virtual_network_link" "cognitive_services" {
- count = var.enable_dns && var.enable_networking ? 1 : 0
- name = "${var.vnet_name}-link"
- resource_group_name = azurerm_resource_group.main.name
- private_dns_zone_name = azurerm_private_dns_zone.cognitive_services[0].name
- virtual_network_id = azurerm_virtual_network.main[0].id
-}
-
-resource "azurerm_private_dns_zone_virtual_network_link" "storage_blob" {
- count = var.enable_dns && var.enable_networking ? 1 : 0
- name = "${var.vnet_name}-blob-link"
- resource_group_name = azurerm_resource_group.main.name
- private_dns_zone_name = azurerm_private_dns_zone.storage_blob[0].name
- virtual_network_id = azurerm_virtual_network.main[0].id
-}
-
-resource "azurerm_private_dns_zone_virtual_network_link" "storage_file" {
- count = var.enable_dns && var.enable_networking ? 1 : 0
- name = "${var.vnet_name}-file-link"
- resource_group_name = azurerm_resource_group.main.name
- private_dns_zone_name = azurerm_private_dns_zone.storage_file[0].name
- virtual_network_id = azurerm_virtual_network.main[0].id
-}
-
-resource "azurerm_private_dns_zone_virtual_network_link" "storage_table" {
- count = var.enable_dns && var.enable_networking ? 1 : 0
- name = "${var.vnet_name}-table-link"
- resource_group_name = azurerm_resource_group.main.name
- private_dns_zone_name = azurerm_private_dns_zone.storage_table[0].name
- virtual_network_id = azurerm_virtual_network.main[0].id
-}
-
-resource "azurerm_private_dns_zone_virtual_network_link" "storage_queue" {
- count = var.enable_dns && var.enable_networking ? 1 : 0
- name = "${var.vnet_name}-queue-link"
- resource_group_name = azurerm_resource_group.main.name
- private_dns_zone_name = azurerm_private_dns_zone.storage_queue[0].name
- virtual_network_id = azurerm_virtual_network.main[0].id
-}
-
-resource "azurerm_private_dns_zone_virtual_network_link" "key_vault" {
- count = var.enable_dns && var.enable_networking ? 1 : 0
- name = "${var.vnet_name}-keyvault-link"
- resource_group_name = azurerm_resource_group.main.name
- private_dns_zone_name = azurerm_private_dns_zone.key_vault[0].name
- virtual_network_id = azurerm_virtual_network.main[0].id
-}
-
-resource "azurerm_private_dns_zone_virtual_network_link" "container_registry" {
- count = var.enable_dns && var.enable_networking ? 1 : 0
- name = "${var.vnet_name}-acr-link"
- resource_group_name = azurerm_resource_group.main.name
- private_dns_zone_name = azurerm_private_dns_zone.container_registry[0].name
- virtual_network_id = azurerm_virtual_network.main[0].id
-}
-
-resource "azurerm_private_dns_zone_virtual_network_link" "openai" {
- count = var.enable_dns && var.enable_networking ? 1 : 0
- name = "${var.vnet_name}-openai-link"
- resource_group_name = azurerm_resource_group.main.name
- private_dns_zone_name = azurerm_private_dns_zone.openai[0].name
- virtual_network_id = azurerm_virtual_network.main[0].id
-}
-
-resource "azurerm_private_dns_zone_virtual_network_link" "aifoundry_api" {
- count = var.enable_dns && var.enable_networking ? 1 : 0
- name = "${var.vnet_name}-aifoundryapi-link"
- resource_group_name = azurerm_resource_group.main.name
- private_dns_zone_name = azurerm_private_dns_zone.aifoundry_api[0].name
- virtual_network_id = azurerm_virtual_network.main[0].id
-}
-
-resource "azurerm_private_dns_zone_virtual_network_link" "aifoundry_notebooks" {
- count = var.enable_dns && var.enable_networking ? 1 : 0
- name = "${var.vnet_name}-aifoundrynb-link"
- resource_group_name = azurerm_resource_group.main.name
- private_dns_zone_name = azurerm_private_dns_zone.aifoundry_notebooks[0].name
- virtual_network_id = azurerm_virtual_network.main[0].id
-}
-
-resource "azurerm_private_dns_zone_virtual_network_link" "aifoundry_services" {
- count = var.enable_dns && var.enable_networking ? 1 : 0
- name = "${var.vnet_name}-aifoundrysvc-link"
- resource_group_name = azurerm_resource_group.main.name
- private_dns_zone_name = azurerm_private_dns_zone.aifoundry_services[0].name
- virtual_network_id = azurerm_virtual_network.main[0].id
-}
-
-resource "azurerm_private_dns_zone_virtual_network_link" "cosmos" {
- count = var.enable_dns && var.enable_networking ? 1 : 0
- name = "${var.vnet_name}-cosmos-link"
- resource_group_name = azurerm_resource_group.main.name
- private_dns_zone_name = azurerm_private_dns_zone.cosmos[0].name
- virtual_network_id = azurerm_virtual_network.main[0].id
-}
-
-resource "azurerm_private_dns_zone_virtual_network_link" "aisearch" {
- count = var.enable_dns && var.enable_networking ? 1 : 0
- name = "${var.vnet_name}-aisearch-link"
- resource_group_name = azurerm_resource_group.main.name
- private_dns_zone_name = azurerm_private_dns_zone.aisearch[0].name
- virtual_network_id = azurerm_virtual_network.main[0].id
-}
+# Private DNS Zones
+resource "azurerm_private_dns_zone" "cognitive_services" {
+ count = var.enable_dns ? 1 : 0
+ name = "privatelink.cognitiveservices.azure.com"
+ resource_group_name = azurerm_resource_group.main.name
+}
+
+resource "azurerm_private_dns_zone" "storage_blob" {
+ count = var.enable_dns ? 1 : 0
+ name = "privatelink.blob.core.windows.net"
+ resource_group_name = azurerm_resource_group.main.name
+}
+
+resource "azurerm_private_dns_zone" "storage_file" {
+ count = var.enable_dns ? 1 : 0
+ name = "privatelink.file.core.windows.net"
+ resource_group_name = azurerm_resource_group.main.name
+}
+
+resource "azurerm_private_dns_zone" "storage_table" {
+ count = var.enable_dns ? 1 : 0
+ name = "privatelink.table.core.windows.net"
+ resource_group_name = azurerm_resource_group.main.name
+}
+
+resource "azurerm_private_dns_zone" "storage_queue" {
+ count = var.enable_dns ? 1 : 0
+ name = "privatelink.queue.core.windows.net"
+ resource_group_name = azurerm_resource_group.main.name
+}
+
+resource "azurerm_private_dns_zone" "key_vault" {
+ count = var.enable_dns ? 1 : 0
+ name = "privatelink.vaultcore.azure.net"
+ resource_group_name = azurerm_resource_group.main.name
+}
+
+resource "azurerm_private_dns_zone" "container_registry" {
+ count = var.enable_dns ? 1 : 0
+ name = "privatelink.azurecr.io"
+ resource_group_name = azurerm_resource_group.main.name
+}
+
+resource "azurerm_private_dns_zone" "openai" {
+ count = var.enable_dns ? 1 : 0
+ name = "privatelink.openai.azure.com"
+ resource_group_name = azurerm_resource_group.main.name
+}
+
+resource "azurerm_private_dns_zone" "aifoundry_api" {
+ count = var.enable_dns ? 1 : 0
+ name = "privatelink.api.azureml.ms"
+ resource_group_name = azurerm_resource_group.main.name
+}
+
+resource "azurerm_private_dns_zone" "aifoundry_notebooks" {
+ count = var.enable_dns ? 1 : 0
+ name = "privatelink.notebooks.azure.net"
+ resource_group_name = azurerm_resource_group.main.name
+}
+
+resource "azurerm_private_dns_zone" "aifoundry_services" {
+ count = var.enable_dns ? 1 : 0
+ name = "privatelink.services.ai.azure.com"
+ resource_group_name = azurerm_resource_group.main.name
+}
+
+resource "azurerm_private_dns_zone" "cosmos" {
+ count = var.enable_dns ? 1 : 0
+ name = "privatelink.documents.azure.com"
+ resource_group_name = azurerm_resource_group.main.name
+}
+
+resource "azurerm_private_dns_zone" "aisearch" {
+ count = var.enable_dns ? 1 : 0
+ name = "privatelink.search.windows.net"
+ resource_group_name = azurerm_resource_group.main.name
+}
+
+# VNet Links
+resource "azurerm_private_dns_zone_virtual_network_link" "cognitive_services" {
+ count = var.enable_dns && var.enable_networking ? 1 : 0
+ name = "${var.vnet_name}-link"
+ resource_group_name = azurerm_resource_group.main.name
+ private_dns_zone_name = azurerm_private_dns_zone.cognitive_services[0].name
+ virtual_network_id = azurerm_virtual_network.main[0].id
+}
+
+resource "azurerm_private_dns_zone_virtual_network_link" "storage_blob" {
+ count = var.enable_dns && var.enable_networking ? 1 : 0
+ name = "${var.vnet_name}-blob-link"
+ resource_group_name = azurerm_resource_group.main.name
+ private_dns_zone_name = azurerm_private_dns_zone.storage_blob[0].name
+ virtual_network_id = azurerm_virtual_network.main[0].id
+}
+
+resource "azurerm_private_dns_zone_virtual_network_link" "storage_file" {
+ count = var.enable_dns && var.enable_networking ? 1 : 0
+ name = "${var.vnet_name}-file-link"
+ resource_group_name = azurerm_resource_group.main.name
+ private_dns_zone_name = azurerm_private_dns_zone.storage_file[0].name
+ virtual_network_id = azurerm_virtual_network.main[0].id
+}
+
+resource "azurerm_private_dns_zone_virtual_network_link" "storage_table" {
+ count = var.enable_dns && var.enable_networking ? 1 : 0
+ name = "${var.vnet_name}-table-link"
+ resource_group_name = azurerm_resource_group.main.name
+ private_dns_zone_name = azurerm_private_dns_zone.storage_table[0].name
+ virtual_network_id = azurerm_virtual_network.main[0].id
+}
+
+resource "azurerm_private_dns_zone_virtual_network_link" "storage_queue" {
+ count = var.enable_dns && var.enable_networking ? 1 : 0
+ name = "${var.vnet_name}-queue-link"
+ resource_group_name = azurerm_resource_group.main.name
+ private_dns_zone_name = azurerm_private_dns_zone.storage_queue[0].name
+ virtual_network_id = azurerm_virtual_network.main[0].id
+}
+
+resource "azurerm_private_dns_zone_virtual_network_link" "key_vault" {
+ count = var.enable_dns && var.enable_networking ? 1 : 0
+ name = "${var.vnet_name}-keyvault-link"
+ resource_group_name = azurerm_resource_group.main.name
+ private_dns_zone_name = azurerm_private_dns_zone.key_vault[0].name
+ virtual_network_id = azurerm_virtual_network.main[0].id
+}
+
+resource "azurerm_private_dns_zone_virtual_network_link" "container_registry" {
+ count = var.enable_dns && var.enable_networking ? 1 : 0
+ name = "${var.vnet_name}-acr-link"
+ resource_group_name = azurerm_resource_group.main.name
+ private_dns_zone_name = azurerm_private_dns_zone.container_registry[0].name
+ virtual_network_id = azurerm_virtual_network.main[0].id
+}
+
+resource "azurerm_private_dns_zone_virtual_network_link" "openai" {
+ count = var.enable_dns && var.enable_networking ? 1 : 0
+ name = "${var.vnet_name}-openai-link"
+ resource_group_name = azurerm_resource_group.main.name
+ private_dns_zone_name = azurerm_private_dns_zone.openai[0].name
+ virtual_network_id = azurerm_virtual_network.main[0].id
+}
+
+resource "azurerm_private_dns_zone_virtual_network_link" "aifoundry_api" {
+ count = var.enable_dns && var.enable_networking ? 1 : 0
+ name = "${var.vnet_name}-aifoundryapi-link"
+ resource_group_name = azurerm_resource_group.main.name
+ private_dns_zone_name = azurerm_private_dns_zone.aifoundry_api[0].name
+ virtual_network_id = azurerm_virtual_network.main[0].id
+}
+
+resource "azurerm_private_dns_zone_virtual_network_link" "aifoundry_notebooks" {
+ count = var.enable_dns && var.enable_networking ? 1 : 0
+ name = "${var.vnet_name}-aifoundrynb-link"
+ resource_group_name = azurerm_resource_group.main.name
+ private_dns_zone_name = azurerm_private_dns_zone.aifoundry_notebooks[0].name
+ virtual_network_id = azurerm_virtual_network.main[0].id
+}
+
+resource "azurerm_private_dns_zone_virtual_network_link" "aifoundry_services" {
+ count = var.enable_dns && var.enable_networking ? 1 : 0
+ name = "${var.vnet_name}-aifoundrysvc-link"
+ resource_group_name = azurerm_resource_group.main.name
+ private_dns_zone_name = azurerm_private_dns_zone.aifoundry_services[0].name
+ virtual_network_id = azurerm_virtual_network.main[0].id
+}
+
+resource "azurerm_private_dns_zone_virtual_network_link" "cosmos" {
+ count = var.enable_dns && var.enable_networking ? 1 : 0
+ name = "${var.vnet_name}-cosmos-link"
+ resource_group_name = azurerm_resource_group.main.name
+ private_dns_zone_name = azurerm_private_dns_zone.cosmos[0].name
+ virtual_network_id = azurerm_virtual_network.main[0].id
+}
+
+resource "azurerm_private_dns_zone_virtual_network_link" "aisearch" {
+ count = var.enable_dns && var.enable_networking ? 1 : 0
+ name = "${var.vnet_name}-aisearch-link"
+ resource_group_name = azurerm_resource_group.main.name
+ private_dns_zone_name = azurerm_private_dns_zone.aisearch[0].name
+ virtual_network_id = azurerm_virtual_network.main[0].id
+}
diff --git a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/keyvault.tf b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/keyvault.tf
similarity index 96%
rename from infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/keyvault.tf
rename to infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/keyvault.tf
index 164ca781c..a4b7ba51f 100644
--- a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/keyvault.tf
+++ b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/keyvault.tf
@@ -1,135 +1,135 @@
-# Generate a random password for VM admin
-resource "random_password" "vm_admin" {
- count = var.enable_vm ? 1 : 0
- length = 24
- special = true
- override_special = "!@#$%^&*()-_=+[]{}:?"
- min_upper = 2
- min_lower = 2
- min_numeric = 2
- min_special = 2
-}
-
-# Key Vault
-resource "azurerm_key_vault" "main" {
- count = var.enable_vm ? 1 : 0
- name = local.keyvault_name
- location = azurerm_resource_group.main.location
- resource_group_name = azurerm_resource_group.main.name
- tenant_id = data.azurerm_client_config.current.tenant_id
- sku_name = "standard"
- soft_delete_retention_days = 7
- purge_protection_enabled = false
-
- # Enable public network access with network rules to allow specific IPs
- public_network_access_enabled = length(var.allowed_public_ips) > 0 ? true : false
-
- # Network ACLs to restrict access
- network_acls {
- bypass = "AzureServices"
- default_action = length(var.allowed_public_ips) > 0 ? "Deny" : "Allow"
- ip_rules = var.allowed_public_ips
- }
-
- # Enable RBAC for access control
- rbac_authorization_enabled = true
-
- tags = {
- environment = "lab"
- }
-}
-
-# Private Endpoint for Key Vault
-resource "azurerm_private_endpoint" "keyvault" {
- count = var.enable_vm && var.enable_networking ? 1 : 0
- name = "${azurerm_key_vault.main[0].name}-pe"
- resource_group_name = azurerm_resource_group.main.name
- location = azurerm_resource_group.main.location
- subnet_id = azurerm_subnet.private_endpoints[0].id
-
- private_service_connection {
- name = "${azurerm_key_vault.main[0].name}-psc"
- private_connection_resource_id = azurerm_key_vault.main[0].id
- is_manual_connection = false
- subresource_names = ["vault"]
- }
-
- private_dns_zone_group {
- name = "keyvault-dns-zone-group"
- private_dns_zone_ids = [azurerm_private_dns_zone.key_vault[0].id]
- }
-
- tags = merge(
- var.tags,
- {
- environment = "lab"
- }
- )
-}
-
-# Role Assignment: Key Vault Administrator for current user
-resource "azurerm_role_assignment" "current_user_keyvault_admin" {
- count = var.enable_vm ? 1 : 0
- scope = azurerm_key_vault.main[0].id
- role_definition_name = "Key Vault Administrator"
- principal_id = data.azurerm_client_config.current.object_id
-}
-
-# Role Assignment: Key Vault Secrets User for VM managed identity
-# This allows the VM to read secrets (useful for future automation scenarios)
-resource "azurerm_role_assignment" "vm_keyvault_secrets_user" {
- count = var.enable_vm ? 1 : 0
- scope = azurerm_key_vault.main[0].id
- role_definition_name = "Key Vault Secrets User"
- principal_id = azurerm_windows_virtual_machine.main[0].identity[0].principal_id
-}
-
-# Wait for RBAC propagation for Key Vault
-resource "time_sleep" "wait_keyvault_rbac" {
- count = var.enable_vm ? 1 : 0
- create_duration = "30s"
-
- depends_on = [
- azurerm_role_assignment.current_user_keyvault_admin
- ]
-}
-
-# Store VM admin password in Key Vault
-resource "azurerm_key_vault_secret" "vm_admin_password" {
- count = var.enable_vm ? 1 : 0
- name = "vm-admin-password"
- value = random_password.vm_admin[0].result
- key_vault_id = azurerm_key_vault.main[0].id
-
- depends_on = [
- time_sleep.wait_keyvault_rbac
- ]
-
- tags = merge(
- var.tags,
- {
- environment = "lab"
- purpose = "VM administrator password"
- }
- )
-}
-
-# Store VM admin username in Key Vault for reference
-resource "azurerm_key_vault_secret" "vm_admin_username" {
- count = var.enable_vm ? 1 : 0
- name = "vm-admin-username"
- value = var.vm_admin_username
- key_vault_id = azurerm_key_vault.main[0].id
-
- depends_on = [
- time_sleep.wait_keyvault_rbac
- ]
-
- tags = merge(
- var.tags,
- {
- environment = "lab"
- purpose = "VM administrator username"
- }
- )
-}
+# Generate a random password for VM admin
+resource "random_password" "vm_admin" {
+ count = var.enable_vm ? 1 : 0
+ length = 24
+ special = true
+ override_special = "!@#$%^&*()-_=+[]{}:?"
+ min_upper = 2
+ min_lower = 2
+ min_numeric = 2
+ min_special = 2
+}
+
+# Key Vault
+resource "azurerm_key_vault" "main" {
+ count = var.enable_vm ? 1 : 0
+ name = local.keyvault_name
+ location = azurerm_resource_group.main.location
+ resource_group_name = azurerm_resource_group.main.name
+ tenant_id = data.azurerm_client_config.current.tenant_id
+ sku_name = "standard"
+ soft_delete_retention_days = 7
+ purge_protection_enabled = false
+
+ # Enable public network access with network rules to allow specific IPs
+ public_network_access_enabled = length(var.allowed_public_ips) > 0 ? true : false
+
+ # Network ACLs to restrict access
+ network_acls {
+ bypass = "AzureServices"
+ default_action = length(var.allowed_public_ips) > 0 ? "Deny" : "Allow"
+ ip_rules = var.allowed_public_ips
+ }
+
+ # Enable RBAC for access control
+ rbac_authorization_enabled = true
+
+ tags = {
+ environment = "lab"
+ }
+}
+
+# Private Endpoint for Key Vault
+resource "azurerm_private_endpoint" "keyvault" {
+ count = var.enable_vm && var.enable_networking ? 1 : 0
+ name = "${azurerm_key_vault.main[0].name}-pe"
+ resource_group_name = azurerm_resource_group.main.name
+ location = azurerm_resource_group.main.location
+ subnet_id = azurerm_subnet.private_endpoints[0].id
+
+ private_service_connection {
+ name = "${azurerm_key_vault.main[0].name}-psc"
+ private_connection_resource_id = azurerm_key_vault.main[0].id
+ is_manual_connection = false
+ subresource_names = ["vault"]
+ }
+
+ private_dns_zone_group {
+ name = "keyvault-dns-zone-group"
+ private_dns_zone_ids = [azurerm_private_dns_zone.key_vault[0].id]
+ }
+
+ tags = merge(
+ var.tags,
+ {
+ environment = "lab"
+ }
+ )
+}
+
+# Role Assignment: Key Vault Administrator for current user
+resource "azurerm_role_assignment" "current_user_keyvault_admin" {
+ count = var.enable_vm ? 1 : 0
+ scope = azurerm_key_vault.main[0].id
+ role_definition_name = "Key Vault Administrator"
+ principal_id = data.azurerm_client_config.current.object_id
+}
+
+# Role Assignment: Key Vault Secrets User for VM managed identity
+# This allows the VM to read secrets (useful for future automation scenarios)
+resource "azurerm_role_assignment" "vm_keyvault_secrets_user" {
+ count = var.enable_vm ? 1 : 0
+ scope = azurerm_key_vault.main[0].id
+ role_definition_name = "Key Vault Secrets User"
+ principal_id = azurerm_windows_virtual_machine.main[0].identity[0].principal_id
+}
+
+# Wait for RBAC propagation for Key Vault
+resource "time_sleep" "wait_keyvault_rbac" {
+ count = var.enable_vm ? 1 : 0
+ create_duration = "30s"
+
+ depends_on = [
+ azurerm_role_assignment.current_user_keyvault_admin
+ ]
+}
+
+# Store VM admin password in Key Vault
+resource "azurerm_key_vault_secret" "vm_admin_password" {
+ count = var.enable_vm ? 1 : 0
+ name = "vm-admin-password"
+ value = random_password.vm_admin[0].result
+ key_vault_id = azurerm_key_vault.main[0].id
+
+ depends_on = [
+ time_sleep.wait_keyvault_rbac
+ ]
+
+ tags = merge(
+ var.tags,
+ {
+ environment = "lab"
+ purpose = "VM administrator password"
+ }
+ )
+}
+
+# Store VM admin username in Key Vault for reference
+resource "azurerm_key_vault_secret" "vm_admin_username" {
+ count = var.enable_vm ? 1 : 0
+ name = "vm-admin-username"
+ value = var.vm_admin_username
+ key_vault_id = azurerm_key_vault.main[0].id
+
+ depends_on = [
+ time_sleep.wait_keyvault_rbac
+ ]
+
+ tags = merge(
+ var.tags,
+ {
+ environment = "lab"
+ purpose = "VM administrator username"
+ }
+ )
+}
diff --git a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/main.tf b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/main.tf
similarity index 96%
rename from infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/main.tf
rename to infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/main.tf
index ffa77dde4..cc6178152 100644
--- a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/main.tf
+++ b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/main.tf
@@ -1,27 +1,27 @@
-resource "random_id" "suffix" {
- byte_length = 4
-}
-
-data "azurerm_client_config" "current" {}
-
-locals {
- resource_suffix = random_id.suffix.hex
- rg_name = "${var.resource_group_name}-${local.resource_suffix}"
- foundry_name = "${var.foundry_identifier}-${local.resource_suffix}"
- storage_name = "st${local.resource_suffix}"
- aisearch_name = "srch-${local.resource_suffix}"
- cosmos_name = "cosmos-${local.resource_suffix}"
- keyvault_name = "kv-${local.resource_suffix}"
-}
-
-resource "azurerm_resource_group" "main" {
- name = local.rg_name
- location = var.location
-
- tags = merge(
- var.tags,
- {
- environment = "lab"
- }
- )
-}
+resource "random_id" "suffix" {
+ byte_length = 4
+}
+
+data "azurerm_client_config" "current" {}
+
+locals {
+ resource_suffix = random_id.suffix.hex
+ rg_name = "${var.resource_group_name}-${local.resource_suffix}"
+ foundry_name = "${var.foundry_identifier}-${local.resource_suffix}"
+ storage_name = "st${local.resource_suffix}"
+ aisearch_name = "srch-${local.resource_suffix}"
+ cosmos_name = "cosmos-${local.resource_suffix}"
+ keyvault_name = "kv-${local.resource_suffix}"
+}
+
+resource "azurerm_resource_group" "main" {
+ name = local.rg_name
+ location = var.location
+
+ tags = merge(
+ var.tags,
+ {
+ environment = "lab"
+ }
+ )
+}
diff --git a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/network.tf b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/network.tf
similarity index 97%
rename from infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/network.tf
rename to infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/network.tf
index 1d9ca355b..5e4bec2a8 100644
--- a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/network.tf
+++ b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/network.tf
@@ -1,155 +1,155 @@
-# Virtual Network
-resource "azurerm_virtual_network" "main" {
- count = var.enable_networking ? 1 : 0
- name = var.vnet_name
- location = var.location
- resource_group_name = azurerm_resource_group.main.name
- address_space = [var.vnet_address_prefix]
-}
-
-# Subnets
-resource "azurerm_subnet" "private_endpoints" {
- count = var.enable_networking ? 1 : 0
- name = var.private_endpoints_subnet_name
- resource_group_name = azurerm_resource_group.main.name
- virtual_network_name = azurerm_virtual_network.main[0].name
- address_prefixes = [var.private_endpoints_subnet_prefix]
- default_outbound_access_enabled = true
-}
-
-resource "azurerm_subnet" "vms" {
- count = var.enable_vm ? 1 : 0
- name = var.vm_subnet_name
- resource_group_name = azurerm_resource_group.main.name
- virtual_network_name = azurerm_virtual_network.main[0].name
- address_prefixes = [var.vm_subnet_prefix]
- default_outbound_access_enabled = true
-}
-
-resource "azurerm_subnet" "bastion" {
- count = var.enable_vm ? 1 : 0
- name = "AzureBastionSubnet"
- resource_group_name = azurerm_resource_group.main.name
- virtual_network_name = azurerm_virtual_network.main[0].name
- address_prefixes = [var.bastion_subnet_prefix]
- default_outbound_access_enabled = true
-}
-
-# Bastion Public IP
-resource "azurerm_public_ip" "bastion" {
- count = var.enable_vm ? 1 : 0
- name = "${var.bastion_name}-pip"
- location = var.location
- resource_group_name = azurerm_resource_group.main.name
- allocation_method = "Static"
- sku = "Standard"
-}
-
-# Azure Bastion
-resource "azurerm_bastion_host" "main" {
- count = var.enable_vm ? 1 : 0
- name = var.bastion_name
- location = var.location
- resource_group_name = azurerm_resource_group.main.name
- sku = "Standard"
-
- ip_configuration {
- name = "bastionIpConfig"
- subnet_id = azurerm_subnet.bastion[0].id
- public_ip_address_id = azurerm_public_ip.bastion[0].id
- }
-
- # Enable features for enhanced connectivity
- tunneling_enabled = true
- shareable_link_enabled = false
- ip_connect_enabled = true
- copy_paste_enabled = true
- file_copy_enabled = true
-
- tags = merge(
- var.tags,
- {
- environment = "lab"
- }
- )
-}
-
-# Network Interface for VM
-resource "azurerm_network_interface" "vm" {
- count = var.enable_vm ? 1 : 0
- name = "${var.vm_name}-nic"
- location = var.location
- resource_group_name = azurerm_resource_group.main.name
-
- ip_configuration {
- name = "ipconfig1"
- subnet_id = azurerm_subnet.vms[0].id
- private_ip_address_allocation = "Dynamic"
- }
-}
-
-# Windows Server 2025 VM with Entra ID Authentication
-resource "azurerm_windows_virtual_machine" "main" {
- count = var.enable_vm ? 1 : 0
- name = var.vm_name
- location = var.location
- resource_group_name = azurerm_resource_group.main.name
- size = "Standard_B2s"
- admin_username = var.vm_admin_username
- admin_password = random_password.vm_admin[0].result
-
- network_interface_ids = [
- azurerm_network_interface.vm[0].id
- ]
-
- os_disk {
- caching = "ReadWrite"
- storage_account_type = "Standard_LRS"
- }
-
- source_image_reference {
- publisher = "MicrosoftWindowsServer"
- offer = "WindowsServer"
- sku = "2025-datacenter-azure-edition"
- version = "latest"
- }
-
- # Enable system-assigned managed identity for Entra ID login
- identity {
- type = "SystemAssigned"
- }
-
- # Enable Azure Hybrid Benefit for Windows Server
- license_type = "Windows_Server"
-
- automatic_updates_enabled = true
- provision_vm_agent = true
- patch_mode = "AutomaticByPlatform"
- patch_assessment_mode = "AutomaticByPlatform"
-
- tags = merge(
- var.tags,
- {
- environment = "lab"
- }
- )
-}
-
-# AAD Login Extension for Windows
-resource "azurerm_virtual_machine_extension" "aad_login" {
- count = var.enable_vm ? 1 : 0
- name = "AADLoginForWindows"
- virtual_machine_id = azurerm_windows_virtual_machine.main[0].id
- publisher = "Microsoft.Azure.ActiveDirectory"
- type = "AADLoginForWindows"
- type_handler_version = "2.0"
- auto_upgrade_minor_version = true
-}
-
-# Role Assignment: Virtual Machine Administrator Login for current user
-resource "azurerm_role_assignment" "vm_admin_login" {
- count = var.enable_vm ? 1 : 0
- scope = azurerm_windows_virtual_machine.main[0].id
- role_definition_name = "Virtual Machine Administrator Login"
- principal_id = data.azurerm_client_config.current.object_id
-}
+# Virtual Network
+resource "azurerm_virtual_network" "main" {
+ count = var.enable_networking ? 1 : 0
+ name = var.vnet_name
+ location = var.location
+ resource_group_name = azurerm_resource_group.main.name
+ address_space = [var.vnet_address_prefix]
+}
+
+# Subnets
+resource "azurerm_subnet" "private_endpoints" {
+ count = var.enable_networking ? 1 : 0
+ name = var.private_endpoints_subnet_name
+ resource_group_name = azurerm_resource_group.main.name
+ virtual_network_name = azurerm_virtual_network.main[0].name
+ address_prefixes = [var.private_endpoints_subnet_prefix]
+ default_outbound_access_enabled = true
+}
+
+resource "azurerm_subnet" "vms" {
+ count = var.enable_vm ? 1 : 0
+ name = var.vm_subnet_name
+ resource_group_name = azurerm_resource_group.main.name
+ virtual_network_name = azurerm_virtual_network.main[0].name
+ address_prefixes = [var.vm_subnet_prefix]
+ default_outbound_access_enabled = true
+}
+
+resource "azurerm_subnet" "bastion" {
+ count = var.enable_vm ? 1 : 0
+ name = "AzureBastionSubnet"
+ resource_group_name = azurerm_resource_group.main.name
+ virtual_network_name = azurerm_virtual_network.main[0].name
+ address_prefixes = [var.bastion_subnet_prefix]
+ default_outbound_access_enabled = true
+}
+
+# Bastion Public IP
+resource "azurerm_public_ip" "bastion" {
+ count = var.enable_vm ? 1 : 0
+ name = "${var.bastion_name}-pip"
+ location = var.location
+ resource_group_name = azurerm_resource_group.main.name
+ allocation_method = "Static"
+ sku = "Standard"
+}
+
+# Azure Bastion
+resource "azurerm_bastion_host" "main" {
+ count = var.enable_vm ? 1 : 0
+ name = var.bastion_name
+ location = var.location
+ resource_group_name = azurerm_resource_group.main.name
+ sku = "Standard"
+
+ ip_configuration {
+ name = "bastionIpConfig"
+ subnet_id = azurerm_subnet.bastion[0].id
+ public_ip_address_id = azurerm_public_ip.bastion[0].id
+ }
+
+ # Enable features for enhanced connectivity
+ tunneling_enabled = true
+ shareable_link_enabled = false
+ ip_connect_enabled = true
+ copy_paste_enabled = true
+ file_copy_enabled = true
+
+ tags = merge(
+ var.tags,
+ {
+ environment = "lab"
+ }
+ )
+}
+
+# Network Interface for VM
+resource "azurerm_network_interface" "vm" {
+ count = var.enable_vm ? 1 : 0
+ name = "${var.vm_name}-nic"
+ location = var.location
+ resource_group_name = azurerm_resource_group.main.name
+
+ ip_configuration {
+ name = "ipconfig1"
+ subnet_id = azurerm_subnet.vms[0].id
+ private_ip_address_allocation = "Dynamic"
+ }
+}
+
+# Windows Server 2025 VM with Entra ID Authentication
+resource "azurerm_windows_virtual_machine" "main" {
+ count = var.enable_vm ? 1 : 0
+ name = var.vm_name
+ location = var.location
+ resource_group_name = azurerm_resource_group.main.name
+ size = "Standard_B2s"
+ admin_username = var.vm_admin_username
+ admin_password = random_password.vm_admin[0].result
+
+ network_interface_ids = [
+ azurerm_network_interface.vm[0].id
+ ]
+
+ os_disk {
+ caching = "ReadWrite"
+ storage_account_type = "Standard_LRS"
+ }
+
+ source_image_reference {
+ publisher = "MicrosoftWindowsServer"
+ offer = "WindowsServer"
+ sku = "2025-datacenter-azure-edition"
+ version = "latest"
+ }
+
+ # Enable system-assigned managed identity for Entra ID login
+ identity {
+ type = "SystemAssigned"
+ }
+
+ # Enable Azure Hybrid Benefit for Windows Server
+ license_type = "Windows_Server"
+
+ automatic_updates_enabled = true
+ provision_vm_agent = true
+ patch_mode = "AutomaticByPlatform"
+ patch_assessment_mode = "AutomaticByPlatform"
+
+ tags = merge(
+ var.tags,
+ {
+ environment = "lab"
+ }
+ )
+}
+
+# AAD Login Extension for Windows
+resource "azurerm_virtual_machine_extension" "aad_login" {
+ count = var.enable_vm ? 1 : 0
+ name = "AADLoginForWindows"
+ virtual_machine_id = azurerm_windows_virtual_machine.main[0].id
+ publisher = "Microsoft.Azure.ActiveDirectory"
+ type = "AADLoginForWindows"
+ type_handler_version = "2.0"
+ auto_upgrade_minor_version = true
+}
+
+# Role Assignment: Virtual Machine Administrator Login for current user
+resource "azurerm_role_assignment" "vm_admin_login" {
+ count = var.enable_vm ? 1 : 0
+ scope = azurerm_windows_virtual_machine.main[0].id
+ role_definition_name = "Virtual Machine Administrator Login"
+ principal_id = data.azurerm_client_config.current.object_id
+}
diff --git a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/outputs.tf b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/outputs.tf
similarity index 97%
rename from infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/outputs.tf
rename to infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/outputs.tf
index ee30382cd..29618a2bd 100644
--- a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/outputs.tf
+++ b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/outputs.tf
@@ -1,155 +1,155 @@
-# Outputs
-output "resource_group_name" {
- description = "The name of the resource group"
- value = azurerm_resource_group.main.name
-}
-
-output "resource_group_location" {
- description = "The location of the resource group"
- value = azurerm_resource_group.main.location
-}
-
-output "vnet_id" {
- description = "The ID of the virtual network"
- value = var.enable_networking ? azurerm_virtual_network.main[0].id : null
-}
-
-output "vnet_name" {
- description = "The name of the virtual network"
- value = var.enable_networking ? azurerm_virtual_network.main[0].name : null
-}
-
-output "private_endpoints_subnet_id" {
- description = "The ID of the private endpoints subnet"
- value = var.enable_networking ? azurerm_subnet.private_endpoints[0].id : null
-}
-
-output "vm_subnet_id" {
- description = "The ID of the VM subnet"
- value = var.enable_vm ? azurerm_subnet.vms[0].id : null
-}
-
-output "bastion_id" {
- description = "The ID of the Azure Bastion"
- value = var.enable_vm ? azurerm_bastion_host.main[0].id : null
-}
-
-output "bastion_dns_name" {
- description = "The DNS name of the Azure Bastion"
- value = var.enable_vm ? azurerm_bastion_host.main[0].dns_name : null
-}
-
-output "windows_vm_id" {
- description = "The ID of the Windows VM"
- value = var.enable_vm ? azurerm_windows_virtual_machine.main[0].id : null
-}
-
-output "windows_vm_private_ip" {
- description = "The private IP address of the Windows VM"
- value = var.enable_vm ? azurerm_network_interface.vm[0].private_ip_address : null
-}
-
-output "windows_vm_computer_name" {
- description = "The computer name of the Windows VM"
- value = var.enable_vm ? azurerm_windows_virtual_machine.main[0].computer_name : null
-}
-
-output "key_vault_id" {
- description = "The ID of the Key Vault"
- value = var.enable_vm ? azurerm_key_vault.main[0].id : null
-}
-
-output "key_vault_name" {
- description = "The name of the Key Vault"
- value = var.enable_vm ? azurerm_key_vault.main[0].name : null
-}
-
-output "key_vault_uri" {
- description = "The URI of the Key Vault"
- value = var.enable_vm ? azurerm_key_vault.main[0].vault_uri : null
-}
-
-output "vm_admin_password_secret_id" {
- description = "The Key Vault secret ID containing the VM admin password"
- value = var.enable_vm ? azurerm_key_vault_secret.vm_admin_password[0].id : null
- sensitive = true
-}
-
-output "storage_account_id" {
- description = "The ID of the storage account"
- value = var.enable_storage ? azurerm_storage_account.main[0].id : null
-}
-
-output "storage_account_name" {
- description = "The name of the storage account"
- value = var.enable_storage ? azurerm_storage_account.main[0].name : null
-}
-
-output "cosmos_account_id" {
- description = "The ID of the Cosmos DB account"
- value = var.enable_cosmos ? azurerm_cosmosdb_account.main[0].id : null
-}
-
-output "cosmos_account_name" {
- description = "The name of the Cosmos DB account"
- value = var.enable_cosmos ? azurerm_cosmosdb_account.main[0].name : null
-}
-
-output "cosmos_account_endpoint" {
- description = "The endpoint of the Cosmos DB account"
- value = var.enable_cosmos ? azurerm_cosmosdb_account.main[0].endpoint : null
-}
-
-output "aisearch_id" {
- description = "The ID of the AI Search service"
- value = var.enable_aisearch ? azurerm_search_service.main[0].id : null
-}
-
-output "aisearch_name" {
- description = "The name of the AI Search service"
- value = var.enable_aisearch ? azurerm_search_service.main[0].name : null
-}
-
-output "aisearch_endpoint" {
- description = "The endpoint of the AI Search service"
- value = var.enable_aisearch ? "https://${azurerm_search_service.main[0].name}.search.windows.net" : null
-}
-
-output "ai_foundry_id" {
- description = "The ID of the AI Foundry / Cognitive Services account"
- value = azapi_resource.cognitive_account.id
-}
-
-output "ai_foundry_name" {
- description = "The name of the AI Foundry / Cognitive Services account"
- value = azapi_resource.cognitive_account.name
-}
-
-output "ai_foundry_endpoint" {
- description = "The endpoint of the AI Foundry / Cognitive Services account"
- value = "https://${azapi_resource.cognitive_account.name}.cognitiveservices.azure.com/"
-}
-
-output "ai_foundry_custom_subdomain" {
- description = "The custom subdomain name of the AI Foundry account"
- value = try(jsondecode(azapi_resource.cognitive_account.output).properties.customSubDomainName, azapi_resource.cognitive_account.name)
-}
-
-output "private_dns_zone_ids" {
- description = "Map of private DNS zone IDs"
- value = var.enable_dns ? {
- cognitive_services = azurerm_private_dns_zone.cognitive_services[0].id
- storage_blob = azurerm_private_dns_zone.storage_blob[0].id
- storage_file = azurerm_private_dns_zone.storage_file[0].id
- storage_table = azurerm_private_dns_zone.storage_table[0].id
- storage_queue = azurerm_private_dns_zone.storage_queue[0].id
- key_vault = azurerm_private_dns_zone.key_vault[0].id
- container_registry = azurerm_private_dns_zone.container_registry[0].id
- openai = azurerm_private_dns_zone.openai[0].id
- aifoundry_api = azurerm_private_dns_zone.aifoundry_api[0].id
- aifoundry_notebooks = azurerm_private_dns_zone.aifoundry_notebooks[0].id
- aifoundry_services = azurerm_private_dns_zone.aifoundry_services[0].id
- cosmos = azurerm_private_dns_zone.cosmos[0].id
- aisearch = azurerm_private_dns_zone.aisearch[0].id
- } : {}
-}
+# Outputs
+output "resource_group_name" {
+ description = "The name of the resource group"
+ value = azurerm_resource_group.main.name
+}
+
+output "resource_group_location" {
+ description = "The location of the resource group"
+ value = azurerm_resource_group.main.location
+}
+
+output "vnet_id" {
+ description = "The ID of the virtual network"
+ value = var.enable_networking ? azurerm_virtual_network.main[0].id : null
+}
+
+output "vnet_name" {
+ description = "The name of the virtual network"
+ value = var.enable_networking ? azurerm_virtual_network.main[0].name : null
+}
+
+output "private_endpoints_subnet_id" {
+ description = "The ID of the private endpoints subnet"
+ value = var.enable_networking ? azurerm_subnet.private_endpoints[0].id : null
+}
+
+output "vm_subnet_id" {
+ description = "The ID of the VM subnet"
+ value = var.enable_vm ? azurerm_subnet.vms[0].id : null
+}
+
+output "bastion_id" {
+ description = "The ID of the Azure Bastion"
+ value = var.enable_vm ? azurerm_bastion_host.main[0].id : null
+}
+
+output "bastion_dns_name" {
+ description = "The DNS name of the Azure Bastion"
+ value = var.enable_vm ? azurerm_bastion_host.main[0].dns_name : null
+}
+
+output "windows_vm_id" {
+ description = "The ID of the Windows VM"
+ value = var.enable_vm ? azurerm_windows_virtual_machine.main[0].id : null
+}
+
+output "windows_vm_private_ip" {
+ description = "The private IP address of the Windows VM"
+ value = var.enable_vm ? azurerm_network_interface.vm[0].private_ip_address : null
+}
+
+output "windows_vm_computer_name" {
+ description = "The computer name of the Windows VM"
+ value = var.enable_vm ? azurerm_windows_virtual_machine.main[0].computer_name : null
+}
+
+output "key_vault_id" {
+ description = "The ID of the Key Vault"
+ value = var.enable_vm ? azurerm_key_vault.main[0].id : null
+}
+
+output "key_vault_name" {
+ description = "The name of the Key Vault"
+ value = var.enable_vm ? azurerm_key_vault.main[0].name : null
+}
+
+output "key_vault_uri" {
+ description = "The URI of the Key Vault"
+ value = var.enable_vm ? azurerm_key_vault.main[0].vault_uri : null
+}
+
+output "vm_admin_password_secret_id" {
+ description = "The Key Vault secret ID containing the VM admin password"
+ value = var.enable_vm ? azurerm_key_vault_secret.vm_admin_password[0].id : null
+ sensitive = true
+}
+
+output "storage_account_id" {
+ description = "The ID of the storage account"
+ value = var.enable_storage ? azurerm_storage_account.main[0].id : null
+}
+
+output "storage_account_name" {
+ description = "The name of the storage account"
+ value = var.enable_storage ? azurerm_storage_account.main[0].name : null
+}
+
+output "cosmos_account_id" {
+ description = "The ID of the Cosmos DB account"
+ value = var.enable_cosmos ? azurerm_cosmosdb_account.main[0].id : null
+}
+
+output "cosmos_account_name" {
+ description = "The name of the Cosmos DB account"
+ value = var.enable_cosmos ? azurerm_cosmosdb_account.main[0].name : null
+}
+
+output "cosmos_account_endpoint" {
+ description = "The endpoint of the Cosmos DB account"
+ value = var.enable_cosmos ? azurerm_cosmosdb_account.main[0].endpoint : null
+}
+
+output "aisearch_id" {
+ description = "The ID of the AI Search service"
+ value = var.enable_aisearch ? azurerm_search_service.main[0].id : null
+}
+
+output "aisearch_name" {
+ description = "The name of the AI Search service"
+ value = var.enable_aisearch ? azurerm_search_service.main[0].name : null
+}
+
+output "aisearch_endpoint" {
+ description = "The endpoint of the AI Search service"
+ value = var.enable_aisearch ? "https://${azurerm_search_service.main[0].name}.search.windows.net" : null
+}
+
+output "ai_foundry_id" {
+ description = "The ID of the AI Foundry / Cognitive Services account"
+ value = azapi_resource.cognitive_account.id
+}
+
+output "ai_foundry_name" {
+ description = "The name of the AI Foundry / Cognitive Services account"
+ value = azapi_resource.cognitive_account.name
+}
+
+output "ai_foundry_endpoint" {
+ description = "The endpoint of the AI Foundry / Cognitive Services account"
+ value = "https://${azapi_resource.cognitive_account.name}.cognitiveservices.azure.com/"
+}
+
+output "ai_foundry_custom_subdomain" {
+ description = "The custom subdomain name of the AI Foundry account"
+ value = try(jsondecode(azapi_resource.cognitive_account.output).properties.customSubDomainName, azapi_resource.cognitive_account.name)
+}
+
+output "private_dns_zone_ids" {
+ description = "Map of private DNS zone IDs"
+ value = var.enable_dns ? {
+ cognitive_services = azurerm_private_dns_zone.cognitive_services[0].id
+ storage_blob = azurerm_private_dns_zone.storage_blob[0].id
+ storage_file = azurerm_private_dns_zone.storage_file[0].id
+ storage_table = azurerm_private_dns_zone.storage_table[0].id
+ storage_queue = azurerm_private_dns_zone.storage_queue[0].id
+ key_vault = azurerm_private_dns_zone.key_vault[0].id
+ container_registry = azurerm_private_dns_zone.container_registry[0].id
+ openai = azurerm_private_dns_zone.openai[0].id
+ aifoundry_api = azurerm_private_dns_zone.aifoundry_api[0].id
+ aifoundry_notebooks = azurerm_private_dns_zone.aifoundry_notebooks[0].id
+ aifoundry_services = azurerm_private_dns_zone.aifoundry_services[0].id
+ cosmos = azurerm_private_dns_zone.cosmos[0].id
+ aisearch = azurerm_private_dns_zone.aisearch[0].id
+ } : {}
+}
diff --git a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/providers.tf b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/providers.tf
similarity index 94%
rename from infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/providers.tf
rename to infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/providers.tf
index 7bbdebe8b..f137e3664 100644
--- a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/providers.tf
+++ b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/providers.tf
@@ -1,29 +1,29 @@
-terraform {
- required_version = ">= 1.0"
-
- required_providers {
- azurerm = {
- source = "hashicorp/azurerm"
- version = "~> 4.0"
- }
- azapi = {
- source = "azure/azapi"
- version = "~> 2.0"
- }
- random = {
- source = "hashicorp/random"
- version = "~> 3.0"
- }
- }
-}
-
-provider "azurerm" {
- subscription_id = var.subscription_id
- storage_use_azuread = true
-
- features {
- resource_group {
- prevent_deletion_if_contains_resources = false
- }
- }
-}
+terraform {
+ required_version = ">= 1.0"
+
+ required_providers {
+ azurerm = {
+ source = "hashicorp/azurerm"
+ version = "~> 4.0"
+ }
+ azapi = {
+ source = "azure/azapi"
+ version = "~> 2.0"
+ }
+ random = {
+ source = "hashicorp/random"
+ version = "~> 3.0"
+ }
+ }
+}
+
+provider "azurerm" {
+ subscription_id = var.subscription_id
+ storage_use_azuread = true
+
+ features {
+ resource_group {
+ prevent_deletion_if_contains_resources = false
+ }
+ }
+}
diff --git a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/storage.tf b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/storage.tf
similarity index 97%
rename from infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/storage.tf
rename to infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/storage.tf
index 6e1be16a2..702d6c561 100644
--- a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/storage.tf
+++ b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/storage.tf
@@ -1,147 +1,147 @@
-# Storage Account
-resource "azurerm_storage_account" "main" {
- count = var.enable_storage ? 1 : 0
- name = local.storage_name
- resource_group_name = azurerm_resource_group.main.name
- location = azurerm_resource_group.main.location
- account_tier = "Standard"
- account_replication_type = "LRS"
- account_kind = "StorageV2"
- shared_access_key_enabled = false
- allow_nested_items_to_be_public = false
-
- # Disable public network access - only accessible via private endpoints
- public_network_access_enabled = false
-
- # Ignore changes to queue/blob/file/table properties to avoid validation issues
- lifecycle {
- ignore_changes = [
- queue_properties,
- blob_properties,
- share_properties
- ]
- }
-
- tags = merge(
- var.tags,
- {
- environment = "lab"
- }
- )
-}
-
-# Private Endpoint for Blob
-resource "azurerm_private_endpoint" "storage_blob" {
- count = var.enable_storage && var.enable_networking ? 1 : 0
- name = "${azurerm_storage_account.main[0].name}-blob-pe"
- resource_group_name = azurerm_resource_group.main.name
- location = azurerm_resource_group.main.location
- subnet_id = azurerm_subnet.private_endpoints[0].id
-
- private_service_connection {
- name = "${azurerm_storage_account.main[0].name}-blob-psc"
- private_connection_resource_id = azurerm_storage_account.main[0].id
- is_manual_connection = false
- subresource_names = ["blob"]
- }
-
- private_dns_zone_group {
- name = "blob-dns-zone-group"
- private_dns_zone_ids = [azurerm_private_dns_zone.storage_blob[0].id]
- }
-}
-
-# Private Endpoint for File
-resource "azurerm_private_endpoint" "storage_file" {
- count = var.enable_storage && var.enable_networking ? 1 : 0
- name = "${azurerm_storage_account.main[0].name}-file-pe"
- resource_group_name = azurerm_resource_group.main.name
- location = azurerm_resource_group.main.location
- subnet_id = azurerm_subnet.private_endpoints[0].id
-
- private_service_connection {
- name = "${azurerm_storage_account.main[0].name}-file-psc"
- private_connection_resource_id = azurerm_storage_account.main[0].id
- is_manual_connection = false
- subresource_names = ["file"]
- }
-
- private_dns_zone_group {
- name = "file-dns-zone-group"
- private_dns_zone_ids = [azurerm_private_dns_zone.storage_file[0].id]
- }
-}
-
-# Private Endpoint for Table
-resource "azurerm_private_endpoint" "storage_table" {
- count = var.enable_storage && var.enable_networking ? 1 : 0
- name = "${azurerm_storage_account.main[0].name}-table-pe"
- resource_group_name = azurerm_resource_group.main.name
- location = azurerm_resource_group.main.location
- subnet_id = azurerm_subnet.private_endpoints[0].id
-
- private_service_connection {
- name = "${azurerm_storage_account.main[0].name}-table-psc"
- private_connection_resource_id = azurerm_storage_account.main[0].id
- is_manual_connection = false
- subresource_names = ["table"]
- }
-
- private_dns_zone_group {
- name = "table-dns-zone-group"
- private_dns_zone_ids = [azurerm_private_dns_zone.storage_table[0].id]
- }
-}
-
-# Private Endpoint for Queue
-resource "azurerm_private_endpoint" "storage_queue" {
- count = var.enable_storage && var.enable_networking ? 1 : 0
- name = "${azurerm_storage_account.main[0].name}-queue-pe"
- resource_group_name = azurerm_resource_group.main.name
- location = azurerm_resource_group.main.location
- subnet_id = azurerm_subnet.private_endpoints[0].id
-
- private_service_connection {
- name = "${azurerm_storage_account.main[0].name}-queue-psc"
- private_connection_resource_id = azurerm_storage_account.main[0].id
- is_manual_connection = false
- subresource_names = ["queue"]
- }
-
- private_dns_zone_group {
- name = "queue-dns-zone-group"
- private_dns_zone_ids = [azurerm_private_dns_zone.storage_queue[0].id]
- }
-}
-
-# Role Assignment: Current user needs Storage Blob Data Contributor for Terraform to manage storage
-resource "azurerm_role_assignment" "current_user_storage_blob" {
- count = var.enable_storage ? 1 : 0
- scope = azurerm_storage_account.main[0].id
- role_definition_name = "Storage Blob Data Contributor"
- principal_id = data.azurerm_client_config.current.object_id
-}
-
-# Role Assignment: Current user needs Storage Queue Data Contributor
-resource "azurerm_role_assignment" "current_user_storage_queue" {
- count = var.enable_storage ? 1 : 0
- scope = azurerm_storage_account.main[0].id
- role_definition_name = "Storage Queue Data Contributor"
- principal_id = data.azurerm_client_config.current.object_id
-}
-
-# Role Assignment: Current user needs Storage File Data SMB Share Contributor
-resource "azurerm_role_assignment" "current_user_storage_file" {
- count = var.enable_storage ? 1 : 0
- scope = azurerm_storage_account.main[0].id
- role_definition_name = "Storage File Data SMB Share Contributor"
- principal_id = data.azurerm_client_config.current.object_id
-}
-
-# Role Assignment: Current user needs Storage Table Data Contributor
-resource "azurerm_role_assignment" "current_user_storage_table" {
- count = var.enable_storage ? 1 : 0
- scope = azurerm_storage_account.main[0].id
- role_definition_name = "Storage Table Data Contributor"
- principal_id = data.azurerm_client_config.current.object_id
-}
+# Storage Account
+resource "azurerm_storage_account" "main" {
+ count = var.enable_storage ? 1 : 0
+ name = local.storage_name
+ resource_group_name = azurerm_resource_group.main.name
+ location = azurerm_resource_group.main.location
+ account_tier = "Standard"
+ account_replication_type = "LRS"
+ account_kind = "StorageV2"
+ shared_access_key_enabled = false
+ allow_nested_items_to_be_public = false
+
+ # Disable public network access - only accessible via private endpoints
+ public_network_access_enabled = false
+
+ # Ignore changes to queue/blob/file/table properties to avoid validation issues
+ lifecycle {
+ ignore_changes = [
+ queue_properties,
+ blob_properties,
+ share_properties
+ ]
+ }
+
+ tags = merge(
+ var.tags,
+ {
+ environment = "lab"
+ }
+ )
+}
+
+# Private Endpoint for Blob
+resource "azurerm_private_endpoint" "storage_blob" {
+ count = var.enable_storage && var.enable_networking ? 1 : 0
+ name = "${azurerm_storage_account.main[0].name}-blob-pe"
+ resource_group_name = azurerm_resource_group.main.name
+ location = azurerm_resource_group.main.location
+ subnet_id = azurerm_subnet.private_endpoints[0].id
+
+ private_service_connection {
+ name = "${azurerm_storage_account.main[0].name}-blob-psc"
+ private_connection_resource_id = azurerm_storage_account.main[0].id
+ is_manual_connection = false
+ subresource_names = ["blob"]
+ }
+
+ private_dns_zone_group {
+ name = "blob-dns-zone-group"
+ private_dns_zone_ids = [azurerm_private_dns_zone.storage_blob[0].id]
+ }
+}
+
+# Private Endpoint for File
+resource "azurerm_private_endpoint" "storage_file" {
+ count = var.enable_storage && var.enable_networking ? 1 : 0
+ name = "${azurerm_storage_account.main[0].name}-file-pe"
+ resource_group_name = azurerm_resource_group.main.name
+ location = azurerm_resource_group.main.location
+ subnet_id = azurerm_subnet.private_endpoints[0].id
+
+ private_service_connection {
+ name = "${azurerm_storage_account.main[0].name}-file-psc"
+ private_connection_resource_id = azurerm_storage_account.main[0].id
+ is_manual_connection = false
+ subresource_names = ["file"]
+ }
+
+ private_dns_zone_group {
+ name = "file-dns-zone-group"
+ private_dns_zone_ids = [azurerm_private_dns_zone.storage_file[0].id]
+ }
+}
+
+# Private Endpoint for Table
+resource "azurerm_private_endpoint" "storage_table" {
+ count = var.enable_storage && var.enable_networking ? 1 : 0
+ name = "${azurerm_storage_account.main[0].name}-table-pe"
+ resource_group_name = azurerm_resource_group.main.name
+ location = azurerm_resource_group.main.location
+ subnet_id = azurerm_subnet.private_endpoints[0].id
+
+ private_service_connection {
+ name = "${azurerm_storage_account.main[0].name}-table-psc"
+ private_connection_resource_id = azurerm_storage_account.main[0].id
+ is_manual_connection = false
+ subresource_names = ["table"]
+ }
+
+ private_dns_zone_group {
+ name = "table-dns-zone-group"
+ private_dns_zone_ids = [azurerm_private_dns_zone.storage_table[0].id]
+ }
+}
+
+# Private Endpoint for Queue
+resource "azurerm_private_endpoint" "storage_queue" {
+ count = var.enable_storage && var.enable_networking ? 1 : 0
+ name = "${azurerm_storage_account.main[0].name}-queue-pe"
+ resource_group_name = azurerm_resource_group.main.name
+ location = azurerm_resource_group.main.location
+ subnet_id = azurerm_subnet.private_endpoints[0].id
+
+ private_service_connection {
+ name = "${azurerm_storage_account.main[0].name}-queue-psc"
+ private_connection_resource_id = azurerm_storage_account.main[0].id
+ is_manual_connection = false
+ subresource_names = ["queue"]
+ }
+
+ private_dns_zone_group {
+ name = "queue-dns-zone-group"
+ private_dns_zone_ids = [azurerm_private_dns_zone.storage_queue[0].id]
+ }
+}
+
+# Role Assignment: Current user needs Storage Blob Data Contributor for Terraform to manage storage
+resource "azurerm_role_assignment" "current_user_storage_blob" {
+ count = var.enable_storage ? 1 : 0
+ scope = azurerm_storage_account.main[0].id
+ role_definition_name = "Storage Blob Data Contributor"
+ principal_id = data.azurerm_client_config.current.object_id
+}
+
+# Role Assignment: Current user needs Storage Queue Data Contributor
+resource "azurerm_role_assignment" "current_user_storage_queue" {
+ count = var.enable_storage ? 1 : 0
+ scope = azurerm_storage_account.main[0].id
+ role_definition_name = "Storage Queue Data Contributor"
+ principal_id = data.azurerm_client_config.current.object_id
+}
+
+# Role Assignment: Current user needs Storage File Data SMB Share Contributor
+resource "azurerm_role_assignment" "current_user_storage_file" {
+ count = var.enable_storage ? 1 : 0
+ scope = azurerm_storage_account.main[0].id
+ role_definition_name = "Storage File Data SMB Share Contributor"
+ principal_id = data.azurerm_client_config.current.object_id
+}
+
+# Role Assignment: Current user needs Storage Table Data Contributor
+resource "azurerm_role_assignment" "current_user_storage_table" {
+ count = var.enable_storage ? 1 : 0
+ scope = azurerm_storage_account.main[0].id
+ role_definition_name = "Storage Table Data Contributor"
+ principal_id = data.azurerm_client_config.current.object_id
+}
diff --git a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/terraform.tfvars.example b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/terraform.tfvars.example
similarity index 97%
rename from infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/terraform.tfvars.example
rename to infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/terraform.tfvars.example
index 75a56c12d..34ccbebd2 100644
--- a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/terraform.tfvars.example
+++ b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/terraform.tfvars.example
@@ -1,42 +1,42 @@
-# Example Terraform Variables File
-# Copy this file to terraform.tfvars and update with your values
-subscription_id = "%subscription_id%"
-resource_group_name = "foundry-mvnet-rg"
-location = "eastus2"
-foundry_identifier = "foundry-v1" # Avoid issues with soft delete, helps make the resource unique
-
-# Tags to apply to all resources
-tags = {
- Environment = "lab"
- Project = "AI Foundry"
- ManagedBy = "Terraform"
-}
-
-# Feature Flags - Enable/disable resources
-enable_networking = true
-enable_storage = true
-enable_aisearch = true
-enable_cosmos = true
-enable_vm = false
-enable_dns = true
-
-# Network Configuration (only used if enable_networking = true)
-vnet_name = "foundry-vnet"
-vnet_address_prefix = "10.0.0.0/16"
-private_endpoints_subnet_name = "private-endpoints-subnet"
-private_endpoints_subnet_prefix = "10.0.1.0/24"
-vm_subnet_name = "vm-subnet"
-vm_subnet_prefix = "10.0.2.0/24"
-bastion_subnet_prefix = "10.0.3.0/26"
-
-# Bastion Configuration (only used if enable_vm = true)
-bastion_name = "foundry-bastion"
-
-# Virtual Machine Configuration (only used if enable_vm = true)
-vm_name = "foundry-vm"
-vm_admin_username = "azureadmin"
-
-# Key Vault Network Configuration
-allowed_public_ips = ["0.0.0.0/32"] # Replace with your public IP in CIDR format (e.g., "1.2.3.4/32")
-
-# Note: VM admin password is automatically generated and stored in Azure Key Vault
+# Example Terraform Variables File
+# Copy this file to terraform.tfvars and update with your values
+subscription_id = "%subscription_id%"
+resource_group_name = "foundry-mvnet-rg"
+location = "eastus2"
+foundry_identifier = "foundry-v1" # Avoid issues with soft delete, helps make the resource unique
+
+# Tags to apply to all resources
+tags = {
+ Environment = "lab"
+ Project = "AI Foundry"
+ ManagedBy = "Terraform"
+}
+
+# Feature Flags - Enable/disable resources
+enable_networking = true
+enable_storage = true
+enable_aisearch = true
+enable_cosmos = true
+enable_vm = false
+enable_dns = true
+
+# Network Configuration (only used if enable_networking = true)
+vnet_name = "foundry-vnet"
+vnet_address_prefix = "10.0.0.0/16"
+private_endpoints_subnet_name = "private-endpoints-subnet"
+private_endpoints_subnet_prefix = "10.0.1.0/24"
+vm_subnet_name = "vm-subnet"
+vm_subnet_prefix = "10.0.2.0/24"
+bastion_subnet_prefix = "10.0.3.0/26"
+
+# Bastion Configuration (only used if enable_vm = true)
+bastion_name = "foundry-bastion"
+
+# Virtual Machine Configuration (only used if enable_vm = true)
+vm_name = "foundry-vm"
+vm_admin_username = "azureadmin"
+
+# Key Vault Network Configuration
+allowed_public_ips = ["0.0.0.0/32"] # Replace with your public IP in CIDR format (e.g., "1.2.3.4/32")
+
+# Note: VM admin password is automatically generated and stored in Azure Key Vault
diff --git a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/variables.tf b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/variables.tf
similarity index 95%
rename from infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/variables.tf
rename to infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/variables.tf
index cd39bb89a..3f4db5731 100644
--- a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/variables.tf
+++ b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network/variables.tf
@@ -1,130 +1,130 @@
-variable "subscription_id" {
- description = "The Azure subscription ID"
- type = string
-}
-
-variable "resource_group_name" {
- description = "The name of the resource group"
- type = string
-}
-
-variable "location" {
- description = "Azure region for resources"
- type = string
- default = "uaenorth"
-}
-
-variable "foundry_identifier" {
- description = "Unique identifier for the AI Foundry account name (change this to recreate the foundry account)"
- type = string
- default = "foundry"
-}
-
-variable "tags" {
- description = "A map of tags to apply to all resources"
- type = map(string)
- default = {}
-}
-
-variable "allowed_public_ips" {
- description = "List of public IP addresses (CIDR format) allowed to access Key Vault"
- type = list(string)
- default = []
-}
-
-# Feature flags for optional resources
-variable "enable_networking" {
- description = "Enable VNet, subnets, and network infrastructure"
- type = bool
- default = false
-}
-
-variable "enable_storage" {
- description = "Enable Storage Account and its private endpoints"
- type = bool
- default = false
-}
-
-variable "enable_aisearch" {
- description = "Enable AI Search Service and its private endpoint"
- type = bool
- default = false
-}
-
-variable "enable_cosmos" {
- description = "Enable Cosmos DB Account and its private endpoint"
- type = bool
- default = false
-}
-
-variable "enable_vm" {
- description = "Enable Windows VM and Azure Bastion"
- type = bool
- default = false
-}
-
-variable "enable_dns" {
- description = "Enable Private DNS Zones and VNet links"
- type = bool
- default = false
-}
-
-variable "vnet_name" {
- description = "Name of the virtual network"
- type = string
- default = "vnet-aifoundry"
-}
-
-variable "vnet_address_prefix" {
- description = "Address prefix for the virtual network"
- type = string
- default = "10.0.0.0/16"
-}
-
-variable "private_endpoints_subnet_name" {
- description = "Name of the private endpoints subnet"
- type = string
- default = "snet-privateendpoints"
-}
-
-variable "private_endpoints_subnet_prefix" {
- description = "Address prefix for private endpoints subnet"
- type = string
- default = "10.0.1.0/24"
-}
-
-variable "vm_subnet_name" {
- description = "Name of the VM subnet"
- type = string
- default = "snet-vms"
-}
-
-variable "vm_subnet_prefix" {
- description = "Address prefix for VM subnet"
- type = string
- default = "10.0.2.0/24"
-}
-
-variable "bastion_subnet_prefix" {
- description = "Address prefix for Azure Bastion subnet"
- type = string
- default = "10.0.3.0/26"
-}
-
-variable "bastion_name" {
- description = "Name of Azure Bastion"
- type = string
- default = "bastion-aifoundry"
-}
-
-variable "vm_name" {
- description = "Name of the virtual machine"
- type = string
- default = "vm-win2025"
-}
-
-variable "vm_admin_username" {
- description = "Admin username for the VM"
- type = string
- sensitive = true
-}
+variable "subscription_id" {
+ description = "The Azure subscription ID"
+ type = string
+}
+
+variable "resource_group_name" {
+ description = "The name of the resource group"
+ type = string
+}
+
+variable "location" {
+ description = "Azure region for resources"
+ type = string
+ default = "uaenorth"
+}
+
+variable "foundry_identifier" {
+ description = "Unique identifier for the AI Foundry account name (change this to recreate the foundry account)"
+ type = string
+ default = "foundry"
+}
+
+variable "tags" {
+ description = "A map of tags to apply to all resources"
+ type = map(string)
+ default = {}
+}
+
+variable "allowed_public_ips" {
+ description = "List of public IP addresses (CIDR format) allowed to access Key Vault"
+ type = list(string)
+ default = []
+}
+
+# Feature flags for optional resources
+variable "enable_networking" {
+ description = "Enable VNet, subnets, and network infrastructure"
+ type = bool
+ default = false
+}
+
+variable "enable_storage" {
+ description = "Enable Storage Account and its private endpoints"
+ type = bool
+ default = false
+}
+
+variable "enable_aisearch" {
+ description = "Enable AI Search Service and its private endpoint"
+ type = bool
+ default = false
+}
+
+variable "enable_cosmos" {
+ description = "Enable Cosmos DB Account and its private endpoint"
+ type = bool
+ default = false
+}
+
+variable "enable_vm" {
+ description = "Enable Windows VM and Azure Bastion"
+ type = bool
+ default = false
+}
+
+variable "enable_dns" {
+ description = "Enable Private DNS Zones and VNet links"
+ type = bool
+ default = false
+}
+
+variable "vnet_name" {
+ description = "Name of the virtual network"
+ type = string
+ default = "vnet-aifoundry"
+}
+
+variable "vnet_address_prefix" {
+ description = "Address prefix for the virtual network"
+ type = string
+ default = "10.0.0.0/16"
+}
+
+variable "private_endpoints_subnet_name" {
+ description = "Name of the private endpoints subnet"
+ type = string
+ default = "snet-privateendpoints"
+}
+
+variable "private_endpoints_subnet_prefix" {
+ description = "Address prefix for private endpoints subnet"
+ type = string
+ default = "10.0.1.0/24"
+}
+
+variable "vm_subnet_name" {
+ description = "Name of the VM subnet"
+ type = string
+ default = "snet-vms"
+}
+
+variable "vm_subnet_prefix" {
+ description = "Address prefix for VM subnet"
+ type = string
+ default = "10.0.2.0/24"
+}
+
+variable "bastion_subnet_prefix" {
+ description = "Address prefix for Azure Bastion subnet"
+ type = string
+ default = "10.0.3.0/26"
+}
+
+variable "bastion_name" {
+ description = "Name of Azure Bastion"
+ type = string
+ default = "bastion-aifoundry"
+}
+
+variable "vm_name" {
+ description = "Name of the virtual machine"
+ type = string
+ default = "vm-win2025"
+}
+
+variable "vm_admin_username" {
+ description = "Admin username for the VM"
+ type = string
+ sensitive = true
+}
diff --git a/migration/README.md b/migration/README.md
index f1e317dd9..5087ffc45 100644
--- a/migration/README.md
+++ b/migration/README.md
@@ -72,7 +72,7 @@ Ready-to-run:
## RBAC β Required permissions
-The signed-in user needs **Azure AI User** on the AI Services resource.
+The signed-in user needs **Foundry User** on the AI Services resource.
Without this role you will see `401` or `403` errors.
### Check your current roles
@@ -87,7 +87,7 @@ az role assignment list --assignee "nikhowlett@microsoft.com" `
```powershell
az role assignment create `
- --role "Azure AI User" `
+ --role "Foundry User" `
--assignee "nikhowlett@microsoft.com" `
--scope "/subscriptions/47f1c914-e299-4953-a99d-3e34644cfe1c/resourceGroups/rg-nikhowlett-6102/providers/Microsoft.CognitiveServices/accounts/nikhowlett-6102-resource"
```
@@ -158,8 +158,8 @@ Linux / macOS: use `./migrate.sh` with the same flags.
| `failed to connect to the docker API` | Docker Desktop is not running β start it and wait for the whale icon |
| `AADSTS53003` or `You don't have access to this` | Corporate Conditional Access blocked sign-in inside Linux containers β use host Azure CLI login; `migrate-docker.ps1` now does this automatically |
| `az : The term 'az' is not recognized` | Install Azure CLI, or rerun `migrate-docker.ps1` on Windows and let it install Azure CLI with `winget` |
-| `401 Unauthorized` | Check RBAC β you need **Azure AI User** on the resource (see above) |
-| `403 Forbidden` | Assign **Azure AI User** role (see RBAC section above) |
+| `401 Unauthorized` | Check RBAC β you need **Foundry User** on the resource (see above) |
+| `403 Forbidden` | Assign **Foundry User** role (see RBAC section above) |
| `Could not switch to subscription` | Run `az login --tenant 72f988bf-86f1-41af-91ab-2d7cd011db47` |
| Items missing from `--list` | Both endpoints are checked; if still missing the item may have been deleted |
| Docker Desktop crashes on startup | Delete `%USERPROFILE%\.docker\contexts\meta` and restart |
diff --git a/migration/run-migration-docker-auth.ps1 b/migration/run-migration-docker-auth.ps1
index c22089459..58d956928 100644
--- a/migration/run-migration-docker-auth.ps1
+++ b/migration/run-migration-docker-auth.ps1
@@ -332,9 +332,9 @@ try {
if ($exitCode -eq 1) {
Write-Host ""
Write-Host "${Yellow}π If you saw 401/403 errors, check your RBAC role assignments:${Reset}"
- Write-Host " β’ Reading assistants/agents : ${Blue}Azure AI User${Reset} on the resource (minimum for Foundry)"
- Write-Host " β’ Creating/writing agents : ${Blue}Azure AI User${Reset} on the resource"
- Write-Host " β’ Uploading/downloading files: ${Blue}Azure AI User${Reset} on the resource"
+ Write-Host " β’ Reading assistants/agents : ${Blue}Foundry User${Reset} on the resource (minimum for Foundry)"
+ Write-Host " β’ Creating/writing agents : ${Blue}Foundry User${Reset} on the resource"
+ Write-Host " β’ Uploading/downloading files: ${Blue}Foundry User${Reset} on the resource"
Write-Host " β’ Managing connections (ARM) : ${Blue}Contributor${Reset} on the resource"
Write-Host ""
Write-Host " Assign via: Azure portal β Resource β Access control (IAM) β Add role assignment"
diff --git a/migration/run-migration-docker-auth.sh b/migration/run-migration-docker-auth.sh
index a73bc4d8f..2c6e48882 100644
--- a/migration/run-migration-docker-auth.sh
+++ b/migration/run-migration-docker-auth.sh
@@ -245,9 +245,9 @@ else
if [ $EXIT_CODE -eq 1 ]; then
echo ""
echo -e "${YELLOW}π If you saw 401/403 errors, check your RBAC role assignments:${NC}"
- echo " β’ Reading assistants/agents : Azure AI User on the resource (minimum for Foundry)"
- echo " β’ Creating/writing agents : Azure AI User on the resource"
- echo " β’ Uploading/downloading files: Azure AI User on the resource"
+ echo " β’ Reading assistants/agents : Foundry User on the resource (minimum for Foundry)"
+ echo " β’ Creating/writing agents : Foundry User on the resource"
+ echo " β’ Uploading/downloading files: Foundry User on the resource"
echo " β’ Managing connections (ARM) : Contributor on the resource"
echo ""
echo " Assign via: Azure portal β Resource β Access control (IAM) β Add role assignment"
diff --git a/migration/v1_to_v2_migration.py b/migration/v1_to_v2_migration.py
index b2a911a6d..965384322 100644
--- a/migration/v1_to_v2_migration.py
+++ b/migration/v1_to_v2_migration.py
@@ -442,29 +442,29 @@ def _print_rbac_guidance(status_code: int, url: str, method: str = "GET"):
print("β Scope : Resource / resource-group β")
elif is_files and is_write:
print("β Operation : Upload files to target β")
- print("β Role : Azure AI User (dataActions: CognitiveServices/*) β")
+ print("β Role : Foundry User (dataActions: CognitiveServices/*) β")
print("β Scope : Foundry resource or project β")
elif is_files:
print("β Operation : Download/list files from source β")
- print("β Role : Azure AI User or Cognitive Services OpenAI User β")
+ print("β Role : Foundry User or Cognitive Services OpenAI User β")
print("β Scope : Source AI resource β")
elif (is_agents or is_assistants) and is_write:
print("β Operation : Create/write v2 agents β")
- print("β Role : Azure AI User (dataActions: CognitiveServices/*) β")
+ print("β Role : Foundry User (dataActions: CognitiveServices/*) β")
print("β Scope : Foundry resource or project β")
print("β NOTE : 'Cognitive Services OpenAI User' IS sufficient for β")
- print("β assistants/* but Azure AI User is recommended. β")
+ print("β assistants/* but Foundry User is recommended. β")
elif is_agents or is_assistants:
print("β Operation : Read assistants / agents β")
- print("β Role : Azure AI User or Cognitive Services OpenAI User β")
+ print("β Role : Foundry User or Cognitive Services OpenAI User β")
print("β Scope : Source or target AI resource β")
else:
print("β Operation : General data-plane call β")
- print("β Role : Azure AI User (recommended minimum for Foundry) β")
+ print("β Role : Foundry User (recommended minimum for Foundry) β")
print("β β")
print("β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£")
print("β Assign roles: Azure portal β Resource β Access control (IAM) β")
- print("β Or: az role assignment create --role 'Azure AI User' β")
+ print("β Or: az role assignment create --role 'Foundry User' β")
print("β --assignee --scope /subscriptions//... β")
print(f"β {RBAC_DOCS.splitlines()[0]:<67s}β")
print(f"β {RBAC_DOCS.splitlines()[1]:<67s}β")
diff --git a/samples-mistral/python/0000950170-25-100226.pdf b/samples-mistral/python/0000950170-25-100226.pdf
index 09918b716..83440cb40 100644
Binary files a/samples-mistral/python/0000950170-25-100226.pdf and b/samples-mistral/python/0000950170-25-100226.pdf differ
diff --git a/samples/REST/model-router/deploy-model-router-all-configs.sh b/samples/REST/model-router/deploy-model-router-all-configs.sh
new file mode 100644
index 000000000..4690352d1
--- /dev/null
+++ b/samples/REST/model-router/deploy-model-router-all-configs.sh
@@ -0,0 +1,82 @@
+# Deploys a model-router deployment with both a routing mode and a custom model subset.
+# WSL example: bash /mnt/c/Work/repos/foundry-samples-pr/samples/REST/model-router/deploy-model-router-all-configs.sh quality
+
+SUBSCRIPTION_ID=""
+RESOURCE_GROUP=""
+ACCOUNT_NAME=""
+DEPLOYMENT_NAME=""
+API_VERSION="2025-10-01-preview"
+SKU_NAME="GlobalStandard"
+SKU_CAPACITY="10"
+MODEL_ROUTER_FORMAT="OpenAI"
+MODEL_ROUTER_NAME="model-router"
+MODEL_ROUTER_VERSION="2025-11-18"
+# Valid values: balanced (default) | cost | quality
+ROUTING_MODE="balanced"
+SUBSET_1_FORMAT="OpenAI"
+SUBSET_1_NAME="gpt-4.1"
+SUBSET_1_VERSION="2025-04-14"
+SUBSET_2_FORMAT="OpenAI"
+SUBSET_2_NAME="gpt-5.2-chat"
+SUBSET_2_VERSION="2025-12-11"
+SUBSET_3_FORMAT="Meta"
+SUBSET_3_NAME="Llama-4-Maverick-17B-128E-Instruct-FP8"
+SUBSET_3_VERSION="1"
+
+if [ $# -gt 0 ]; then
+ ROUTING_MODE="$1"
+fi
+
+if [ -z "${AZURE_AI_AUTH_TOKEN:-}" ]; then
+ AZURE_AI_AUTH_TOKEN="$(az account get-access-token --resource https://management.azure.com --query accessToken -o tsv)"
+fi
+
+case "${ROUTING_MODE}" in
+ balanced|cost|quality)
+ ;;
+ *)
+ echo "Invalid ROUTING_MODE: ${ROUTING_MODE}. Valid values: balanced | cost | quality" >&2
+ exit 1
+ ;;
+esac
+
+#
+curl -X PUT "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.CognitiveServices/accounts/${ACCOUNT_NAME}/deployments/${DEPLOYMENT_NAME}?api-version=${API_VERSION}" \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer $AZURE_AI_AUTH_TOKEN" \
+ -d @- <
diff --git a/samples/REST/model-router/deploy-model-router-model-subset.sh b/samples/REST/model-router/deploy-model-router-model-subset.sh
new file mode 100644
index 000000000..6763509a5
--- /dev/null
+++ b/samples/REST/model-router/deploy-model-router-model-subset.sh
@@ -0,0 +1,66 @@
+# Deploys a model-router deployment that routes only to a custom subset of models.
+# WSL example: bash /mnt/c/Work/repos/foundry-samples-pr/samples/REST/model-router/deploy-model-router-model-subset.sh
+
+SUBSCRIPTION_ID=""
+RESOURCE_GROUP=""
+ACCOUNT_NAME=""
+DEPLOYMENT_NAME=""
+API_VERSION="2025-10-01-preview"
+SKU_NAME="GlobalStandard"
+SKU_CAPACITY="10"
+MODEL_ROUTER_FORMAT="OpenAI"
+MODEL_ROUTER_NAME="model-router"
+MODEL_ROUTER_VERSION="2025-11-18"
+SUBSET_1_FORMAT="OpenAI"
+SUBSET_1_NAME="gpt-4.1"
+SUBSET_1_VERSION="2025-04-14"
+SUBSET_2_FORMAT="OpenAI"
+SUBSET_2_NAME="gpt-5.2-chat"
+SUBSET_2_VERSION="2025-12-11"
+SUBSET_3_FORMAT="Meta"
+SUBSET_3_NAME="Llama-4-Maverick-17B-128E-Instruct-FP8"
+SUBSET_3_VERSION="1"
+
+if [ -z "${AZURE_AI_AUTH_TOKEN:-}" ]; then
+ AZURE_AI_AUTH_TOKEN="$(az account get-access-token --resource https://management.azure.com --query accessToken -o tsv)"
+fi
+
+#
+curl -X PUT "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.CognitiveServices/accounts/${ACCOUNT_NAME}/deployments/${DEPLOYMENT_NAME}?api-version=${API_VERSION}" \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer $AZURE_AI_AUTH_TOKEN" \
+ -d @- <
\ No newline at end of file
diff --git a/samples/REST/model-router/deploy-model-router-routing-mode.sh b/samples/REST/model-router/deploy-model-router-routing-mode.sh
new file mode 100644
index 000000000..81a62bf8b
--- /dev/null
+++ b/samples/REST/model-router/deploy-model-router-routing-mode.sh
@@ -0,0 +1,57 @@
+
+# Deploys a model-router deployment with a selected routing mode.
+# WSL example: bash /mnt/c/Work/repos/foundry-samples-pr/samples/REST/model-router/deploy-model-router-routing-mode.sh quality
+
+SUBSCRIPTION_ID=""
+RESOURCE_GROUP=""
+ACCOUNT_NAME=""
+DEPLOYMENT_NAME=""
+API_VERSION="2025-10-01-preview"
+# Valid values: balanced (default) | cost | quality
+ROUTING_MODE="balanced"
+SKU_NAME="GlobalStandard"
+SKU_CAPACITY="10"
+MODEL_FORMAT="OpenAI"
+MODEL_NAME="model-router"
+MODEL_VERSION="2025-11-18"
+
+if [ $# -gt 0 ]; then
+ ROUTING_MODE="$1"
+fi
+
+if [ -z "${AZURE_AI_AUTH_TOKEN:-}" ]; then
+ AZURE_AI_AUTH_TOKEN="$(az account get-access-token --resource https://management.azure.com --query accessToken -o tsv)"
+fi
+
+case "${ROUTING_MODE}" in
+ balanced|cost|quality)
+ ;;
+ *)
+ echo "Invalid ROUTING_MODE: ${ROUTING_MODE}. Valid values: balanced | cost | quality" >&2
+ exit 1
+ ;;
+esac
+
+#
+curl -X PUT "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.CognitiveServices/accounts/${ACCOUNT_NAME}/deployments/${DEPLOYMENT_NAME}?api-version=${API_VERSION}" \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer $AZURE_AI_AUTH_TOKEN" \
+ -d @- <
\ No newline at end of file
diff --git a/samples/REST/quickstart/quickstart-responses.sh b/samples/REST/quickstart/quickstart-responses.sh
index f030676af..e50040930 100644
--- a/samples/REST/quickstart/quickstart-responses.sh
+++ b/samples/REST/quickstart/quickstart-responses.sh
@@ -2,6 +2,6 @@ curl -X POST https://YOUR-FOUNDRY-RESOURCE-NAME.services.ai.azure.com/api/projec
-H "Content-Type: application/json" \
-H "Authorization: Bearer $AZURE_AI_AUTH_TOKEN" \
-d '{
- "model": "gpt-4.1-mini",
+ "model": "gpt-5.1-mini",
"input": "What is the size of France in square miles?"
-}'
\ No newline at end of file
+}'
diff --git a/samples/cli/training/README.md b/samples/cli/training/README.md
new file mode 100644
index 000000000..6c58948e3
--- /dev/null
+++ b/samples/cli/training/README.md
@@ -0,0 +1,34 @@
+# Training Job Templates
+
+This directory contains starter templates for submitting training jobs in Azure AI Foundry using the Azure Developer CLI (azd) training extension (`azure.ai.training`).
+
+The goal is a code-first, automatable CLI workflow for custom training jobs in Foundry, optimized for ML engineers and data scientists who prefer terminal workflows and need repeatable execution.
+
+When you run `azd ai training init` with a template flag, these templates are pulled locally to provide sample configurations for your training jobs:
+
+```bash
+azd ai training init -t
+```
+
+## Templates
+
+| Template | Link | Description |
+|----------|------|-------------|
+| Hello World | [sample_hello_world.yaml](sample_hello_world.yaml) | Minimal `commandJob` template that echoes `"hello world"`. Useful as a starting point to validate your compute and environment setup. |
+| Custom Training Job | [sample_training_job.yaml](sample_training_job.yaml) | End-to-end `commandJob` that runs a Python training script ([src/train.py](src/train.py)) over a JSONL dataset ([train_data/sample.jsonl](train_data/sample.jsonl)) and writes a result file output. |
+
+## Layout
+
+```
+training/
+βββ sample_hello_world.yaml # Minimal job
+βββ sample_training_job.yaml # Custom code training job
+βββ src/
+β βββ train.py # Training script invoked by sample_training_job.yaml
+βββ train_data/
+ βββ sample.jsonl # Sample input data mounted as the train_data input
+```
+
+## Placeholders
+
+Templates use `` for fields that depend on the user's workspace or job requirements, such as `identity`, `compute`, `environment`, `instance_type`, `slaTier`, and `priority`. Replace these values before submitting the job.
\ No newline at end of file
diff --git a/samples/cli/training/sample_hello_world.yaml b/samples/cli/training/sample_hello_world.yaml
new file mode 100644
index 000000000..a40adf013
--- /dev/null
+++ b/samples/cli/training/sample_hello_world.yaml
@@ -0,0 +1,15 @@
+$schema: https://azuremlschemas.azureedge.net/latest/commandJob.schema.json
+type: command
+display_name: cli-hello-world-sample
+description: Sample hello world job using AZD Foundry Training CLI
+command: echo "hello world"
+environment:
+compute:
+resources:
+ instance_count: 1
+ instance_type:
+ properties:
+ AISuperComputer:
+ imageVersion: ""
+ slaTier:
+ priority:
\ No newline at end of file
diff --git a/samples/cli/training/sample_training_job.yaml b/samples/cli/training/sample_training_job.yaml
new file mode 100644
index 000000000..61346af51
--- /dev/null
+++ b/samples/cli/training/sample_training_job.yaml
@@ -0,0 +1,28 @@
+$schema: https://azuremlschemas.azureedge.net/latest/commandJob.schema.json
+type: command
+display_name: cli-training-job-sample
+description: Sample training job using AZD Foundry Training CLI
+command: python ${{inputs.code_dataset}}/train.py --input-folder ${{inputs.train_data}} --input-file-name "sample.jsonl" --result-file ${{outputs.result_file}}
+identity:
+environment:
+compute:
+resources:
+ instance_count: 1
+ instance_type:
+ properties:
+ AISuperComputer:
+ imageVersion: ""
+ slaTier:
+ priority:
+inputs:
+ train_data:
+ type: uri_folder
+ path: .\train_data
+ mode: download
+ code_dataset:
+ type: uri_folder
+ path: .\src
+ mode: download
+outputs:
+ result_file:
+ type: uri_folder
\ No newline at end of file
diff --git a/samples/cli/training/src/train.py b/samples/cli/training/src/train.py
new file mode 100644
index 000000000..6a0b3ae50
--- /dev/null
+++ b/samples/cli/training/src/train.py
@@ -0,0 +1,105 @@
+from __future__ import annotations
+
+import argparse
+import json
+import random
+from pathlib import Path
+from time import time
+
+import mlflow
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(
+ description="Print the provided JSONL input folder contents to stdout."
+ )
+ parser.add_argument(
+ "--input-folder",
+ required=True,
+ help="Path to the mounted JSONL input folder.",
+ )
+ parser.add_argument(
+ "--input-file-name",
+ default="sample.jsonl",
+ help="JSONL file name to read from the mounted input folder.",
+ )
+ parser.add_argument(
+ "--result-file",
+ required=True,
+ help="Path to the output folder where result_file will be written.",
+ )
+ return parser.parse_args()
+
+
+def main() -> int:
+ args = parse_args()
+ input_folder = Path(args.input_folder).expanduser()
+ if not input_folder.exists():
+ raise FileNotFoundError(f"Input folder does not exist: {input_folder}")
+ if not input_folder.is_dir():
+ raise NotADirectoryError(f"Expected a folder path but got a file: {input_folder}")
+
+ input_path = input_folder / args.input_file_name
+ if not input_path.exists():
+ raise FileNotFoundError(
+ f"Input file does not exist under the mounted folder: {input_path}"
+ )
+
+ print(f"Reading input file from folder: {input_path}")
+ print("=== BEGIN RAW INPUT ===")
+ raw_text = input_path.read_text(encoding="utf-8")
+ print(raw_text.rstrip())
+ print("=== END RAW INPUT ===")
+
+ print("=== BEGIN PARSED JSONL ===")
+ for line_number, line in enumerate(raw_text.splitlines(), start=1):
+ if not line.strip():
+ continue
+ record = json.loads(line)
+ print(f"Line {line_number}:")
+ print(json.dumps(record, indent=2, sort_keys=True))
+ print("=== END PARSED JSONL ===")
+
+ # ββ MLflow logging βββββββββββββββββββββββββββββββββββββββββββββββββ
+ print("=== BEGIN MLFLOW LOGGING ===")
+ mlflow.autolog(disable=True)
+
+ num_epochs = 5
+ for epoch in range(1, num_epochs + 1):
+ loss = 1.0 / epoch + random.uniform(-0.05, 0.05)
+ accuracy = 1.0 - loss / 2.0
+ mlflow.log_metrics({"loss": loss, "accuracy": accuracy}, step=epoch)
+ print(f" Epoch {epoch}: loss={loss:.4f} accuracy={accuracy:.4f}")
+
+ mlflow.log_params({
+ "epochs": num_epochs,
+ "learning_rate": 0.001,
+ "batch_size": 32,
+ "optimizer": "adam",
+ })
+ mlflow.log_metric("final_loss", loss)
+ mlflow.log_metric("final_accuracy", accuracy)
+ print("=== END MLFLOW LOGGING ===")
+
+ # ββ Write result_file output βββββββββββββββββββββββββββββββββββββββ
+ output_folder = Path(args.result_file).expanduser()
+ output_folder.mkdir(parents=True, exist_ok=True)
+ result_path = output_folder / "result_file"
+ result_path.write_text(
+ json.dumps(
+ {
+ "final_loss": loss,
+ "final_accuracy": accuracy,
+ "epochs": num_epochs,
+ "input_file": str(input_path),
+ },
+ indent=2,
+ ),
+ encoding="utf-8",
+ )
+ print(f"Wrote result file to: {result_path}")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/samples/cli/training/train_data/sample.jsonl b/samples/cli/training/train_data/sample.jsonl
new file mode 100644
index 000000000..8c9cfb431
--- /dev/null
+++ b/samples/cli/training/train_data/sample.jsonl
@@ -0,0 +1,2 @@
+{"query":"What is the importance of choosing the right provider in getting the most value out of your health insurance plan?","ground_truth":"Choosing the right provider is an important part of getting the most value out of your health insurance plan. With Northwind Health Plus, you have access to an extensive network of in-network providers. Working with these providers is an essential part of getting the most value out of your plan.\n[Northwind_Health_Plus_Benefits_Details-3.pdf]","response":"Choosing the right provider is important in getting the most value out of your health insurance plan for several reasons.\n\n1. Cost savings: In-network providers offer cost savings compared to out-of-network providers [Northwind_Health_Plus_Benefits_Details.pdf]. By choosing an in-network provider, you can minimize your out-of-pocket expenses and maximize the benefits provided by your health insurance plan.\n\n2. Coverage: In-network providers are part of the network of healthcare professionals that are contracted with your health insurance plan [Northwind_Health_Plus_Benefits_Details.pdf]. This means that they have agreed to accept the amount of payment offered by your health insurance plan, reducing the portion of the cost you are responsible for.\n\n3. Additional services and discounts: Out-of-network providers may not offer additional services or discounts that are available to in-network providers [Northwind_Standard_Benefits_Details.pdf]. By choosing an in-network provider, you may have access to additional benefits and cost-saving opportunities.\n\n4. Continuity of care: Continuity of care refers to being treated by the same provider or medical team over a period of time [Northwind_Health_Plus_Benefits_Details.pdf]. When you have continuity of care, your provider has a better understanding of your medical history, leading to more accurate diagnoses and treatments. This can result in better quality of care and more effective treatments.\n\n5. Personalized care: When you stay with the same provider for a period of time, they are more likely to know about any changes in your health and can offer more personalized care [Northwind_Health_Plus_Benefits_Details.pdf]. This can lead to better health outcomes and a higher level of satisfaction with your healthcare experience.\n\nIn summary, choosing the right provider is important for cost savings, coverage, continuity of care, and personalized care. By selecting an in-network provider, you can maximize the benefits of your health insurance plan and receive high-quality, affordable healthcare services. [Northwind_Health_Plus_Benefits_Details.pdf][Northwind_Standard_Benefits_Details.pdf]","context":"Northwind_Health_Plus_Benefits_Details.pdf: is in-network. β’ Use the searchable directory on the Northwind Health Plus website to find in-network providers in your area. β’ Ask your current provider if they are part of the Northwind Health Plus network. β’ Consider the cost savings associated with in-network providers when making your decision. β’ Consider the quality of care when choosing a provider. β’ Make sure the provider is familiar with your health insurance plan and its coverage. β’ Make sure the provider is available and can accommodate your schedule. Conclusion Choosing the right provider is an important part of getting the most value out of your health insurance plan. Northwind Health Plus offers an extensive network of in-network providers that can help you save money on health care services. By following the tips outlined above, you can make sure that you are choosing an in-network provider that is most convenient and cost-effective for you and your family. Continuity of Care HOW PROVIDERS AFFECT YOUR COSTS: Continuity of Care Itβs important to understand continuity of care when selecting a provider. Continuity of care is the process of being treated by the same provider or medical team over a period of time. When you have continuity of care, your provider has a better understanding of your medical history, enabling them to provide more accurate diagnoses and treatments. Continuity of care is important when selecting a provider because it ensures better quality of care. When you have continuity of care, your provider is more familiar with your medical history, which can lead to more effective treatments. Also, if you stay with the same provider for a period of time, the provider will be more likely to know about any changes in your health and can offer more personalized care. The Northwind Health Plus plan offers coverage for continuity of care. This means that if you have been seeing the same provider for a period of time, you may be able to continue\n\nNorthwind_Standard_Benefits_Details.pdf: Northwind Health. As a result, they are not required to accept the amount of payment offered by Northwind Health, meaning that you may be responsible for a greater portion of the cost. Additionally, out-of-network providers may not offer additional services or discounts that are available to in-network providers. When choosing a provider, it is important to make sure that the provider is in-network. While it is possible to receive care from out-of-network providers, it is important to understand that you will be responsible for a greater portion of the costs. To make sure that you are getting the best value for your health care expenses, it is recommended that you choose an in-network provider whenever possible. There are some exceptions when it comes to receiving care from out-of-network providers. If you are unable to find an in-network provider in your area or if you require a specific type of care that is not available from an in-network provider, you may receive care from an out- of-network provider. In these cases, the cost of care may be more expensive and you may be responsible for a greater portion of the costs. In addition to understanding the differences between in-network and out-of-network providers, it is important to understand the cost sharing associated with each. Northwind Standard does not cover the full cost of care for out-of-network providers, so you may be responsible for a greater portion of the costs. When choosing a provider, it is important to take into account the cost sharing associated with each provider. If you are unable to find an in-network provider in your area or if you require a specific type of care that is not available from an in-network provider, it is important to understand that you may be responsible for a greater portion of the costs. Finally, it is important to be aware of any additional fees that may be associated with receiving care from an out-of-network provider. Some providers may charge additional fees\n\nNorthwind_Standard_Benefits_Details.pdf: weekend hours. Choosing an in-network provider can help you save money on your health care costs. By following the tips above and researching your options, you can find a provider that is convenient, affordable, and in-network with your Northwind Standard plan. Continuity of Care HOW PROVIDERS AFFECT YOUR COSTS: CONTINUITY OF CARE At Contoso, we understand the importance of providing our employees with access to a quality, affordable health care plan. We are proud to offer our employees Northwind Standard, an insurance plan that provides coverage for medical, vision, and dental services. We also offer continuity of care, which is the ability to stay with the same provider for all your health care needs, from routine checkups to specialized care. Continuity of care is an important component of any health care plan, as it allows you to develop a relationship with your doctor, who is more likely to recognize any changes in your health and be able to provide better, more personalized care. With Northwind Standard, you can stay with the same provider throughout your health care journey, helping you to receive the best possible care. Continuity of care also helps you save money on health care costs. When you stay with the same provider, you are likely to pay less than if you switch around from provider to provider. This is because your provider already has all your medical records, and you wonβt need to pay for expensive tests and procedures that youβve already had done. There are a few exceptions to the continuity of care. If you move to a different area, you may need to find a new provider. Additionally, if you are in need of specialized care, such as surgery or a complex treatment, you may need to visit a specialist who does not accept your insurance plan. In these cases, you should be sure to check with your insurance company and/or provider to make sure you understand any associated costs.","latency":8.733296,"response_length":2160}
+{"query":"What should you do when choosing an in-network provider for your health care needs?","ground_truth":"When choosing an in-network provider for your health care needs, you should check with Northwind Health Plus to ensure that the provider is in-network. This is important because in-network providers charge lower rates than out-of-network providers.\n[Northwind_Health_Plus_Benefits_Details-3.pdf]","response":"When choosing an in-network provider for your health care needs, there are a few important factors to consider [Northwind_Standard_Benefits_Details.pdf]:\n\n1. Confirm that the provider is in-network with your health insurance plan. You can do this by calling the provider's office or using the provider search tool on the Northwind Health website.\n\n2. Check if the provider is accepting new patients. Some providers may be in-network but not currently accepting new patients.\n\n3. Consider the location of the provider. If the provider is too far away, it may be difficult for you to attend appointments.\n\n4. Take into account the provider's office hours. If you work during the day, you may need to find a provider that offers evening or weekend hours.\n\nChoosing an in-network provider can help you save money on your health care costs. When you choose an in-network provider, you typically pay lower copays and deductibles compared to out-of-network providers. Additionally, many services, such as preventive care, may be covered at no cost when received from an in-network provider [Northwind_Standard_Benefits_Details.pdf].\n\nContinuity of care is also an important consideration. Staying with the same provider allows you to develop a relationship with your doctor, who is more likely to recognize changes in your health and provide personalized care. It can also help you save money by avoiding duplicate tests and procedures. However, there may be exceptions to continuity of care, such as moving to a different area or needing specialized care that is not covered by your insurance plan [Northwind_Standard_Benefits_Details.pdf].","context":"Northwind_Standard_Benefits_Details.pdf: Northwind Health. As a result, they are not required to accept the amount of payment offered by Northwind Health, meaning that you may be responsible for a greater portion of the cost. Additionally, out-of-network providers may not offer additional services or discounts that are available to in-network providers. When choosing a provider, it is important to make sure that the provider is in-network. While it is possible to receive care from out-of-network providers, it is important to understand that you will be responsible for a greater portion of the costs. To make sure that you are getting the best value for your health care expenses, it is recommended that you choose an in-network provider whenever possible. There are some exceptions when it comes to receiving care from out-of-network providers. If you are unable to find an in-network provider in your area or if you require a specific type of care that is not available from an in-network provider, you may receive care from an out-of-network provider. In these cases, the cost of care may be more expensive and you may be responsible for a greater portion of the costs. In addition to understanding the differences between in-network and out-of-network providers, it is important to understand the cost sharing associated with each. Northwind Standard does not cover the full cost of care for out-of-network providers, so you may be responsible for a greater portion of the costs. When choosing a provider, it is important to take into account the cost sharing associated with each provider. If you are unable to find an in-network provider in your area or if you require a specific type of care that is not available from an in-network provider, it is important to understand that you may be responsible for a greater portion of the costs. Finally, it is important to be aware of any additional fees that may be associated with receiving care from an out-of-network provider. Some providers may charge additional fees\n\nNorthwind_Standard_Benefits_Details.pdf: of your Northwind Standard health plan. HOW PROVIDERS AFFECT YOUR COSTS In-Network Providers HOW PROVIDERS AFFECT YOUR COSTS When selecting a health insurance plan, one of the most important factors to consider is the network of in-network providers that are available with the plan. Northwind Standard offers a wide variety of in-network providers, ranging from primary care physicians, specialists, hospitals, and pharmacies. This allows you to choose a provider that is convenient for you and your family, while also helping you to keep your costs low. When you choose a provider that is in-network with your plan, you will typically pay lower copays and deductibles than you would with an out-of-network provider. In addition, many services, such as preventive care, may be covered at no cost when you receive care from an in-network provider. It is important to note, however, that Northwind Standard does not offer coverage for emergency services, mental health and substance abuse coverage, or out-of-network services. This means that you may have to pay out of pocket for these services if you receive them from an out-of-network provider. When choosing an in-network provider, there are a few tips to keep in mind. First, make sure that the provider you choose is in-network with your plan. You can confirm this by calling the provider's office and asking them if they are in-network with Northwind Standard. You can also use the provider search tool on the Northwind Health website to make sure your provider is in-network. Second, make sure that the provider you choose is accepting new patients. Some providers may be in-network but not be taking new patients. Third, consider the location of the provider. If the provider is too far away, it may be difficult for you to get to your appointments. Finally, consider the provider's office hours. If you work during the day, you may need to find a provider that has evening or weekend hours.\n\nNorthwind_Standard_Benefits_Details.pdf: weekend hours. Choosing an in-network provider can help you save money on your health care costs. By following the tips above and researching your options, you can find a provider that is convenient, affordable, and in-network with your Northwind Standard plan. Continuity of Care HOW PROVIDERS AFFECT YOUR COSTS: CONTINUITY OF CARE At Contoso, we understand the importance of providing our employees with access to a quality, affordable health care plan. We are proud to offer our employees Northwind Standard, an insurance plan that provides coverage for medical, vision, and dental services. We also offer continuity of care, which is the ability to stay with the same provider for all your health care needs, from routine checkups to specialized care. Continuity of care is an important component of any health care plan, as it allows you to develop a relationship with your doctor, who is more likely to recognize any changes in your health and be able to provide better, more personalized care. With Northwind Standard, you can stay with the same provider throughout your health care journey, helping you to receive the best possible care. Continuity of care also helps you save money on health care costs. When you stay with the same provider, you are likely to pay less than if you switch around from provider to provider. This is because your provider already has all your medical records, and you wonβt need to pay for expensive tests and procedures that youβve already had done. There are a few exceptions to the continuity of care. If you move to a different area, you may need to find a new provider. Additionally, if you are in need of specialized care, such as surgery or a complex treatment, you may need to visit a specialist who does not accept your insurance plan. In these cases, you should be sure to check with your insurance company and/or provider to make sure you understand any associated costs.","latency":6.809682,"response_length":1634}
diff --git a/samples/csharp/FoundryA365/infra/modules/application.bicep b/samples/csharp/FoundryA365/infra/modules/application.bicep
deleted file mode 100644
index 4f0c4b5a8..000000000
--- a/samples/csharp/FoundryA365/infra/modules/application.bicep
+++ /dev/null
@@ -1,38 +0,0 @@
-// Parameters for the application module
-param accountName string
-param projectName string
-param applicationName string
-
-// Application properties
-param displayName string = applicationName
-
-// Agent configuration
-param agents array = []
-
-// Reference to existing Cognitive Services account
-resource account 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = {
- name: accountName
-}
-
-// Reference to existing Cognitive Services project
-resource project 'Microsoft.CognitiveServices/accounts/projects@2025-06-01' existing = {
- parent: account
- name: projectName
-}
-
-// Cognitive Services Application (child resource of project)
-resource application 'Microsoft.CognitiveServices/accounts/projects/applications@2025-10-01-preview' = {
- parent: project
- name: applicationName
- properties: {
- displayName: displayName
- agents: agents
- authorizationPolicy: {
- AuthorizationScheme: 'Channels'
- }
- }
-}
-
-// Outputs
-output baseUrl string = application.properties.baseUrl
-output agentIdentityBlueprintId string = application.properties.agentIdentityBlueprint.clientId
diff --git a/samples/csharp/FoundryA365/scripts/build-docker-image.ps1 b/samples/csharp/FoundryA365/scripts/build-docker-image.ps1
deleted file mode 100644
index 46f41c220..000000000
--- a/samples/csharp/FoundryA365/scripts/build-docker-image.ps1
+++ /dev/null
@@ -1,31 +0,0 @@
-Set-Location "$($PSScriptRoot)/../src/hello_world_a365_agent"
-
-Remove-Item "./publish" -Recurse -Force -ErrorAction SilentlyContinue
-
-dotnet publish -c Release -o "./publish"
-
-
-$authorityEndpoint = "https://login.microsoftonline.com/$($env:TENANT_ID)"
-$azureOpenAIEndpoint = "https://$($env:ACCOUNT_NAME).openai.azure.com/"
-
-$projectClientId = az ad sp show --id $env:PROJECT_PRINCIPAL_ID --query appId -o tsv
-
-# if the projectClientId is null or empty, throw an error
-if ([string]::IsNullOrEmpty($projectClientId)) {
- throw "Failed to get project client ID for principal ID $($env:PROJECT_PRINCIPAL_ID)"
-}
-
-docker build -t hello-world-a365-agent:a365preview001 --build-arg BLUEPRINT_CLIENT_ID=$env:AGENT_IDENTITY_BLUEPRINT_ID --build-arg AUTHORITY_ENDPOINT=$authorityEndpoint --build-arg FEDERATED_CLIENT_ID=$projectClientId --build-arg AZURE_OPENAI_ENDPOINT=$azureOpenAIEndpoint --build-arg MODEL_DEPLOYMENT='gpt-4o' -f "./foundry-infra/Dockerfile" .
-
-$acrLoginServer = $env:AZURE_CONTAINER_REGISTRY_ENDPOINT
-
-# split the login server to get the registry name
-$registryName = $acrLoginServer.Split(".")[0]
-
-docker tag hello-world-a365-agent:a365preview001 $acrLoginServer/hello-world-a365-agent:a365preview001
-
-az acr login --name $registryName
-
-docker push $acrLoginServer/hello-world-a365-agent:a365preview001
-
-Remove-Item "./publish" -Recurse -Force -ErrorAction SilentlyContinue
diff --git a/samples/csharp/FoundryA365/scripts/create-application-deployment.ps1 b/samples/csharp/FoundryA365/scripts/create-application-deployment.ps1
deleted file mode 100644
index d6ef0f4e1..000000000
--- a/samples/csharp/FoundryA365/scripts/create-application-deployment.ps1
+++ /dev/null
@@ -1,79 +0,0 @@
-param(
- [string]$AgentName,
- [string]$AgentVersion
-)
-
-
-$ErrorActionPreference = "Stop"
-
-$applicationDeploymentBody = @{
- properties =@{
- displayName = "Foundry Agent Deployment"
- protocols = @(
- @{
- protocol = "Activity"
- version = "v1"
- }
- )
- agents = @(
- @{
- agentName = $AgentName
- agentVersion = $AgentVersion
- #TODO: AgentId
- }
- )
- deploymentType = "Hosted"
- minReplicas = 1
- maxReplicas = 1
- }
-}
-
-
-$createDeploymentUrl = "https://management.azure.com/subscriptions/$($env:AZURE_SUBSCRIPTION_ID)/resourceGroups/$($env:AZURE_RESOURCE_GROUP)/providers/Microsoft.CognitiveServices/accounts/$($env:ACCOUNT_NAME)/projects/$($env:PROJECT_NAME)/applications/$($env:APPLICATION_NAME)/agentDeployments/foundry-agent-deployment?api-version=2025-10-01-preview"
-$jsonDeploymentBody = $applicationDeploymentBody | ConvertTo-Json -Depth 100
-Write-Host "Creating application deployment..."
-Write-Host "JSON Body:"
-Write-Host $jsonDeploymentBody
-
-$managementToken = az account get-access-token --resource https://management.azure.com --query accessToken -o tsv
-# Send PUT request
-$responseDeployment = Invoke-RestMethod -Uri $createDeploymentUrl `
- -Method Put `
- -Headers @{
- "Content-Type" = "application/json"
- "Accept" = "application/json"
- "Authorization" = "Bearer $($managementToken)"
- } `
- -Body $jsonDeploymentBody
-
-Write-Host ""
-Write-Host "Response:"
-$responseDeployment | ConvertTo-Json -Depth 100 | Write-Host
-
-
-while ($true) {
- Start-Sleep -Seconds 10
- Write-Host "Get application deployment status..."
- $getDeploymentUrl = "https://management.azure.com/subscriptions/$($env:AZURE_SUBSCRIPTION_ID)/resourceGroups/$($env:AZURE_RESOURCE_GROUP)/providers/Microsoft.CognitiveServices/accounts/$($env:ACCOUNT_NAME)/projects/$($env:PROJECT_NAME)/applications/$($env:APPLICATION_NAME)/agentDeployments/foundry-agent-deployment?api-version=2025-10-01-preview"
-
- $responseDeployment = Invoke-RestMethod -Uri $getDeploymentUrl `
- -Method Get `
- -Headers @{
- "Content-Type" = "application/json"
- "Accept" = "application/json"
- "Authorization" = "Bearer $($managementToken)"
- } `
-
- Write-Host ""
- Write-Host "Response:"
- $responseDeployment | ConvertTo-Json -Depth 100 | Write-Host
- if ($responseDeployment.properties.state -eq "Running") {
- Write-Host "Deployment succeeded."
- break
- } elseif ($responseDeployment.properties.provisioningState -eq "Failed") {
- Write-Host "Deployment failed."
- break
- } else {
- Write-Host "Deployment in progress..."
- }
-}
\ No newline at end of file
diff --git a/samples/csharp/FoundryA365/scripts/create-foundry-container-agent.ps1 b/samples/csharp/FoundryA365/scripts/create-foundry-container-agent.ps1
deleted file mode 100644
index cf15f4e7d..000000000
--- a/samples/csharp/FoundryA365/scripts/create-foundry-container-agent.ps1
+++ /dev/null
@@ -1,50 +0,0 @@
-$ErrorActionPreference = "Stop"
-
-$agentUrl = "$($env:AZURE_AI_PROJECT_ENDPOINT)/agents/$($env:AGENT_NAME)/versions?api-version=2025-11-15-preview"
-
-$agentCreationBody = @{
- definition = @{
- kind = "hosted"
- image = "$($env:AZURE_CONTAINER_REGISTRY_ENDPOINT)/hello-world-a365-agent:a365preview001"
- cpu = "2"
- memory = "4Gi"
- environment_variables = @{}
- container_protocol_versions = @(
- @{
- protocol = "activity_protocol"
- version = "v1"
- }
- )
- }
- metadata = @{
- botid = "$($env:AGENT_IDENTITY_BLUEPRINT_ID)"
- }
- description="Foundry digital worker."
-}
-
-$jsonBody = $agentCreationBody | ConvertTo-Json -Depth 5
-
-$aiAzureToken = az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv
-
-
-Write-Host "Creating agent version..."
-Write-Host "JSON Body:"
-Write-Host $jsonBody
-
-# Send POST request
-$response = Invoke-RestMethod -Uri $agentUrl `
- -Method Post `
- -Headers @{
- "Content-Type" = "application/json"
- "Accept" = "application/json"
- "Authorization" = "Bearer $($aiAzureToken)"
- } `
- -Body $jsonBody
-
-Write-Host ""
-Write-Host "Response:"
-$response | ConvertTo-Json -Depth 100 | Write-Host
-
-# & "$PSScriptRoot/create-application-deployment.ps1" -AgentName $response.name -AgentVersion $response.version
-
-& "$PSScriptRoot/start-foundry-container-agent.ps1" -AgentName $response.name -AgentVersion $response.version
\ No newline at end of file
diff --git a/samples/csharp/FoundryA365/scripts/start-foundry-container-agent.ps1 b/samples/csharp/FoundryA365/scripts/start-foundry-container-agent.ps1
deleted file mode 100644
index 24284eed5..000000000
--- a/samples/csharp/FoundryA365/scripts/start-foundry-container-agent.ps1
+++ /dev/null
@@ -1,59 +0,0 @@
-param(
- [string]$AgentName,
- [string]$AgentVersion
-)
-
-$ErrorActionPreference = "Stop"
-
-
-# Helper: call REST API
-function Invoke-FoundryApi {
- param(
- [string]$Method,
- [string]$Url,
- [object]$Body = $null
- )
-
- $aiAzureToken = az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv
- $Headers = @{
- "Authorization" = "Bearer $aiAzureToken"
- "Content-Type" = "application/json"
- }
- if ($Body) {
- Invoke-RestMethod -Method $Method -Uri $Url -Headers $Headers -Body ($Body | ConvertTo-Json -Depth 100)
- } else {
- Invoke-RestMethod -Method $Method -Uri $Url -Headers $Headers
- }
-}
-
-# 1. Start container
-$startUrl = "$($env:AZURE_AI_PROJECT_ENDPOINT)/agents/$AgentName/versions/$AgentVersion/containers/default:start?api-version=2025-11-15-preview"
-$body = @{
- minReplicas = 1
- maxReplicas = 1
-}
-$operation = Invoke-FoundryApi -Method POST -Url $startUrl -Body $body
-Write-Host "Starting container (operation id: $($operation.id), status: $($operation.status))"
-
-# 2. Poll until operation is complete
-while ($operation.status -in @("NotStarted", "InProgress")) {
- Start-Sleep -Seconds 5
- $opUrl = "$($env:AZURE_AI_PROJECT_ENDPOINT)/agents/$AgentName/operations/$($operation.id)?api-version=2025-11-15-preview"
- $operation = Invoke-FoundryApi -Method GET -Url $opUrl
- Write-Host " Operation status: $($operation.status)"
-
- # 3. Check operation result
- switch ($operation.status) {
- "Succeeded" {
- $containerUrl = "$($env:AZURE_AI_PROJECT_ENDPOINT)/agents/$AgentName/versions/$AgentVersion/containers/default?api-version=2025-11-15-preview"
- $container = Invoke-FoundryApi -Method GET -Url $containerUrl
- Write-Host "Container status: $($container.status), created at: $($container.createdAt)"
- break
- }
- "Failed" {
- Write-Host "Operation failed. Error message: $($operation.error)"
- break
- }
- }
-}
-
diff --git a/samples/csharp/FoundryA365/src/hello_world_a365_agent/AgentLogic/AgentConfiguration.cs b/samples/csharp/FoundryA365/src/hello_world_a365_agent/AgentLogic/AgentConfiguration.cs
deleted file mode 100644
index 81c7f9210..000000000
--- a/samples/csharp/FoundryA365/src/hello_world_a365_agent/AgentLogic/AgentConfiguration.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-ο»Ώnamespace HelloWorldA365.AgentLogic;
-
-public static class AgentConfiguration
-{
- public static string? GetAgentEmailFilter(this IConfiguration configuration) =>
- configuration.GetValue("AgentConfiguration:AgentEmailFilter");
-
- public static string? GetCertificateData(this IConfiguration configuration) =>
- configuration["agent-blueprint-secret"];
-}
\ No newline at end of file
diff --git a/samples/csharp/FoundryA365/src/hello_world_a365_agent/AgentLogic/AgentLogicServiceFactory.cs b/samples/csharp/FoundryA365/src/hello_world_a365_agent/AgentLogic/AgentLogicServiceFactory.cs
deleted file mode 100644
index 6d0066395..000000000
--- a/samples/csharp/FoundryA365/src/hello_world_a365_agent/AgentLogic/AgentLogicServiceFactory.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-namespace HelloWorldA365.AgentLogic;
-
-using HelloWorldA365.AgentLogic.SemanticKernel;
-using HelloWorldA365.Models;
-using Microsoft.Agents.Builder;
-
-public sealed class AgentLogicServiceFactory(
- IConfiguration configuration,
- ILogger logger,
- SemanticKernelAgentLogicServiceFactory semanticKernelAgentLogicServiceFactory)
-{
- private readonly string implementationType = configuration["Type"] ?? "SK";
-
- ///
- /// Gets or creates a AgentLogicService instance for the specified agent.
- /// The implementation (Semantic Kernel vs OpenAI) is determined by the Type configuration setting.
- ///
- /// The agent to get the service for.
- /// A AgentLogicService instance.
- public async Task GetService(AgentMetadata agent, ITurnContext turnContext)
- {
- // Note: We should not cache the service per bot.
- // The service must be created per turn. Context is not desined to be shared across turns.
- return await CreateServiceAsync(agent, turnContext);
- }
-
- private async Task CreateServiceAsync(AgentMetadata agent, ITurnContext turnContext)
- {
- switch (implementationType.ToUpperInvariant())
- {
- case "SK":
- case "SEMANTICKERNEL":
- default:
- logger.LogInformation("Creating Semantic Kernel-based AgentLogicService for agent {AgentId}", agent.AgentId);
- return await semanticKernelAgentLogicServiceFactory.CreateAsync(agent, turnContext);
-
- }
- }
-}
\ No newline at end of file
diff --git a/samples/csharp/FoundryA365/src/hello_world_a365_agent/AgentLogic/AuthCache/AgentTokenCache.cs b/samples/csharp/FoundryA365/src/hello_world_a365_agent/AgentLogic/AuthCache/AgentTokenCache.cs
deleted file mode 100644
index aade664e0..000000000
--- a/samples/csharp/FoundryA365/src/hello_world_a365_agent/AgentLogic/AuthCache/AgentTokenCache.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-ο»Ώusing Azure.Core;
-using HelloWorldA365.Services;
-using System.Collections.Concurrent;
-
-namespace HelloWorldA365.AgentLogic.AuthCache;
-
-///
-/// Cache only for observability (exporter) scoped tokens per (agentId, tenantId).
-///
-public interface IAgentTokenCache
-{
- ///
- /// Registers (idempotent) a credential to be used for observability token acquisition.
- ///
- void RegisterObservability(string agentId, string tenantId, AgentTokenCredential credential, string[] observabilityScopes);
-
- ///
- /// Returns an observability token (cached inside the credential) or null on failure/not registered.
- ///
- string? GetObservabilityToken(string agentId, string tenantId);
-}
-
-public sealed class AgentTokenCache : IAgentTokenCache
-{
- private sealed record Entry(AgentTokenCredential Credential, string[] Scopes);
-
- private readonly ConcurrentDictionary _map = new();
-
- public void RegisterObservability(string agentId, string tenantId, AgentTokenCredential credential, string[] observabilityScopes)
- {
- // First registration wins; subsequent calls ignored (idempotent).
- _map.TryAdd(agentId, new Entry(credential, observabilityScopes));
- }
-
- public string? GetObservabilityToken(string agentId, string tenantId)
- {
- if (!_map.TryGetValue(agentId, out var entry))
- return null;
-
- try
- {
- // Use sync path; credential handles caching & refresh internally.
- var ctx = new TokenRequestContext(entry.Scopes);
- return entry.Credential.GetToken(ctx, CancellationToken.None).Token;
- }
- catch
- {
- return null;
- }
- }
-}
\ No newline at end of file
diff --git a/samples/csharp/FoundryA365/src/hello_world_a365_agent/AgentLogic/SemanticKernel/SemanticKernelAgentLogicService.cs b/samples/csharp/FoundryA365/src/hello_world_a365_agent/AgentLogic/SemanticKernel/SemanticKernelAgentLogicService.cs
deleted file mode 100644
index b39c9c040..000000000
--- a/samples/csharp/FoundryA365/src/hello_world_a365_agent/AgentLogic/SemanticKernel/SemanticKernelAgentLogicService.cs
+++ /dev/null
@@ -1,340 +0,0 @@
-namespace HelloWorldA365.AgentLogic.SemanticKernel;
-
-using HelloWorldA365.AgentLogic.AuthCache;
-using HelloWorldA365.Services;
-using Microsoft.Agents.Builder;
-using Microsoft.Agents.Builder.State;
-using Microsoft.Agents.Core.Models;
-using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.Agents;
-using Microsoft.SemanticKernel.ChatCompletion;
-using System;
-using System.Net.Http.Headers;
-using System.Text;
-using System.Text.Json;
-using System.Threading;
-using AgentMetadata = HelloWorldA365.Models.AgentMetadata;
-using Microsoft.Agents.A365.Tooling.Extensions.SemanticKernel.Services;
-using Microsoft.Agents.A365.Observability.Runtime.Common;
-using Microsoft.Agents.A365.Observability.Extensions.SemanticKernel;
-using Microsoft.Agents.A365.Notifications.Models;
-using Microsoft.Agents.A365.Observability.Common;
-
-///
-/// Semantic Kernel-based implementation of AgentLogicService.
-/// This contains all core business logic for a agent instance using Semantic Kernel.
-///
-public class SemanticKernelAgentLogicService : IAgentLogicService
-{
- private readonly Kernel _kernel;
- private readonly AgentMetadata _agentMetadata;
- private readonly ChatCompletionAgent _chatCompletionAgent;
- private readonly ILogger _logger;
-
- public SemanticKernelAgentLogicService(
- AgentTokenHelper tokenHelper,
- AgentMetadata agent,
- Kernel kernel,
- string certificateData,
- IConfiguration config,
- ILogger logger,
- IMcpToolRegistrationService mcpToolRegistrationService,
- IAgentTokenCache tokenCache)
- {
- _agentMetadata = agent ?? throw new ArgumentNullException(nameof(agent));
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
-
- // Register observability-only credential (separate instance to isolate caching if needed)
- var observabilityCredential = new AgentTokenCredential(tokenHelper, agent, certificateData);
- var obsScopes = EnvironmentUtils.GetObservabilityAuthenticationScope();
- tokenCache.RegisterObservability(agent.AgentId.ToString(), agent.TenantId.ToString(), observabilityCredential, obsScopes);
-
- var deployment = config["ModelDeployment"] ?? throw new ArgumentNullException("ModelDeployment");
- var endpoint = config["AzureOpenAIEndpoint"];
- var mem0Token = config["Mem0ApiToken"];
- if (string.IsNullOrWhiteSpace(deployment) || string.IsNullOrWhiteSpace(endpoint))
- {
- throw new InvalidOperationException("ModelDeployment and AzureOpenAIEndpoint must be configured.");
- }
-
- // Create an HttpClient for the Mem0 service if key is provided
- if (!string.IsNullOrWhiteSpace(mem0Token))
- {
- var httpClient = new HttpClient()
- {
- BaseAddress = new Uri("https://api.mem0.ai")
- };
- httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", mem0Token);
- }
-
- var instructions = AgentInstructions.GetInstructions(agent);
- _kernel = kernel;
- _chatCompletionAgent = new ChatCompletionAgent
- {
- // NOTE: This ID should match the agent ID for which the token is registered on L48-51 above
- Id = agent.AgentId.ToString(),
- Name = agent.EmailId,
- Instructions = instructions,
- Kernel = _kernel,
- Arguments = new KernelArguments(new PromptExecutionSettings()
- {
- FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
- }),
- }.WithTracing();
- }
-
- ///
- /// This processes message from activity protocol, aka ABS (Azure Bot Service).
- ///
- public async Task NewActivityReceived(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken)
- {
- var baggageScope = new BaggageBuilder()
- .FromTurnContext(turnContext)
- .CorrelationId(turnContext.Activity.RequestId)
- .Build();
-
- var incomingText = turnContext.Activity.Text;
- _logger.LogInformation("New activity received (Semantic Kernel): {IncomingText}", incomingText);
-
- // Log target recipient
- var recipient = turnContext.Activity.Recipient;
- var json = recipient != null ? JsonSerializer.Serialize(recipient) : "null";
- _logger.LogInformation("Target Recipient: {Recipient}", json);
-
- // Log sender information
- var sender = turnContext.Activity.From;
- var jsonSender = sender != null ? JsonSerializer.Serialize(sender) : "null";
- _logger.LogInformation("Sender: {Sender}", jsonSender);
-
- bool skipResponse = false;
- if (turnContext.Activity.ChannelId == "email" || turnContext.Activity.ChannelId == "agents:email")
- {
- var subject = string.Empty;
- if (turnContext.Activity.ChannelData is JsonElement jsonElement && jsonElement.TryGetProperty("subject", out var subjectProperty))
- {
- subject = subjectProperty.GetString() ?? string.Empty;
- }
-
- _logger.LogInformation("Extracted subject: {Subject}", subject);
- incomingText = $"Please respond to this email From: {sender!.Id}\nSubject: {subject}\nMessage: {incomingText}";
- if (!string.IsNullOrEmpty(sender?.Id) && sender.Id.Contains("MicrosoftExchange329e71ec88ae4615bbc36ab6ce41109"))
- {
- _logger.LogWarning("Non delivery response from MicrosoftExchange329e71ec88ae4615bbc36ab6ce41109 skipping further response to prevent infinite loop of email storm");
- skipResponse = true;
- }
- }
- else if (turnContext.Activity.ChannelId == "msteams")
- {
- // name and email missing in teams channel data
- incomingText = $"Respond to this chat message with chat id {turnContext.Activity.Conversation.Id} " +
- $"From: {sender?.Name} ({sender?.Id})\n" +
- $"Message: {incomingText}";
- }
- else if (turnContext.Activity.Type == ActivityTypes.InstallationUpdate)
- {
- incomingText = $"You were just added as a digital worker. Please send an email to {sender!.Id} with a information on what you can do.";
- }
-
- if (!skipResponse)
- {
- await foreach (var responseItem in InvokeAgentAsync(incomingText))
- {
- try
- {
- var responseText = responseItem.Message.Content ?? string.Empty;
- _logger.LogInformation("Sending response: {ResponseText}", responseText);
-
- await turnContext.SendActivityAsync(MessageFactory.Text(responseText), cancellationToken);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error sending response: {ResponseText}", responseItem.Message.Content);
- }
- }
- }
-
- await Task.CompletedTask;
- }
-
- public async Task NewEmailReceived(string fromEmail, string subject, string messageBody)
- {
- using var baggageScope = new BaggageBuilder()
- .TenantId(_agentMetadata.TenantId.ToString())
- .AgentId(_agentMetadata.AgentId.ToString())
- .Build();
-
- try
- {
- ChatHistoryAgentThread agentThread = new();
-
- var formattedMessage = $"Please respond to this email From: {fromEmail}\nSubject: {subject}\nMessage: {messageBody}";
-
- var responseText = new StringBuilder();
- await foreach (var responseItem in InvokeAgentAsync(formattedMessage, agentThread))
- {
- responseText.Append(responseItem.Message.Content ?? string.Empty);
- }
-
- return responseText.ToString();
- }
- catch (Exception ex)
- {
- throw new Exception($"Error processing message: {ex.Message}", ex);
- }
- }
-
- public async Task NewChatReceived(string chatId, string fromUser, string messageBody)
- {
- using var baggageScope = new BaggageBuilder()
- .TenantId(_agentMetadata.TenantId.ToString())
- .AgentId(_agentMetadata.AgentId.ToString())
- .Build();
-
- try
- {
- ChatHistoryAgentThread agentThread = new();
-
- // Create context and user messages
- var contextMessage = new ChatMessageContent(AuthorRole.System, $"You are chatting with {fromUser} via Teams - ChatId {chatId}");
- var userMessage = new ChatMessageContent(AuthorRole.User, messageBody);
- var messages = new List { contextMessage, userMessage };
-
- var responseText = new StringBuilder();
- await foreach (var responseItem in _chatCompletionAgent.InvokeAsync(messages, agentThread))
- {
- responseText.Append(responseItem.Message.Content ?? string.Empty);
- }
-
- // Reset status message and set presence to Available before returning
-
- return responseText.ToString();
- }
- catch (Exception ex)
- {
- throw new Exception($"Error processing chat message: {ex.Message}", ex);
- }
- }
-
-
- #region IAgentLogicService Event Handler Methods
-
- ///
- /// Handles email notification events from Messaging
- ///
- public async Task HandleEmailNotificationAsync(ITurnContext turnContext, ITurnState turnState, AgentNotificationActivity emailEvent)
- {
- _logger.LogInformation("Processing email notification - NotificationType: {NotificationType}",
- emailEvent.NotificationType);
-
- var emailContent = emailEvent.Text ?? string.Empty;
-
- // Collect all agent responses into a single text
- var responseText = new StringBuilder();
- await foreach (var responseItem in InvokeAgentAsync(emailContent))
- {
- responseText.Append(responseItem.Message.Content ?? string.Empty);
- }
-
- // var responseActivity = Microsoft.Agents.A365.Notifications.Models.EmailResponse.CreateEmailResponseActivity(responseText.ToString());
- // Create email response with the collected content
- var responseActivity2 = MessageFactory.Text("a");
- responseActivity2.Entities.Add(new EmailResponse(responseText.ToString()));
- await turnContext.SendActivityAsync(responseActivity2);
-
- }
-
- ///
- /// Handles document comment notification events (Word, Excel, PowerPoint) from Messaging
- ///
- public async Task HandleCommentNotificationAsync(ITurnContext turnContext, ITurnState turnState, AgentNotificationActivity commentEvent)
- {
- _logger.LogInformation("Processing comment notification - NotificationType: {NotificationType}",
- commentEvent.NotificationType);
-
- // For now returning a static response - can be enhanced with actual AI processing
- var responseText = "Hello this is a response to a comment notification received through Messaging.";
- var commentActivity = MessageFactory.Text(responseText);
- turnContext.SendActivityAsync(commentActivity);
- }
-
- ///
- /// Handles Teams message events from Messaging
- ///
- public async Task HandleTeamsMessageAsync(ITurnContext turnContext, ITurnState turnState, AgentNotificationActivity teamsEvent)
- {
- _logger.LogInformation("Processing Teams message event - From: {FromUser}",
- teamsEvent.From?.Name);
-
- var formattedMessage = $"Respond to this chat message with chat id {teamsEvent.Conversation?.Id} " +
- $"From: {teamsEvent.From?.Name} ({teamsEvent.From?.Id})\n" +
- $"Message: {teamsEvent.Text}";
-
- // For now returning a static response - can be enhanced with actual AI processing
- var responseText = "Hello this is a response to a Teams message received through Messaging.";
-
- _logger.LogInformation("Teams message response prepared: {ResponseText}", responseText);
- }
-
- ///
- /// Handles installation update events from Messaging
- ///
- public async Task HandleInstallationUpdateAsync(ITurnContext turnContext, ITurnState turnState, AgentNotificationActivity installationEvent)
- {
- _logger.LogInformation("Processing installation update event for {SenderId}", installationEvent.From?.Id);
-
- var formattedMessage = $"You were just added as a digital worker. Please send an email to {installationEvent.From?.Id} with information on what you can do.";
-
- // For now returning a static response - can be enhanced with actual AI processing
- var responseText = "Hello this is a response to an installation update received through Messaging.";
-
- _logger.LogInformation("Installation update response prepared: {ResponseText}", responseText);
- }
-
- ///
- /// Handles generic activity events that don't fit other categories
- ///
- public async Task NewActivityReceived(ITurnContext turnContext, ITurnState turnState, AgentNotificationActivity genericEvent)
- {
- _logger.LogInformation("Processing generic activity event - NotificationType: {NotificationType}",
- genericEvent.NotificationType);
-
- // For generic events, provide basic processing
- // For now, just return a static response
- var responseText = "Hello this is a response to a generic activity received through Messaging.";
-
- _logger.LogInformation("Generic activity response prepared: {ResponseText}", responseText);
- }
-
- #endregion
-
- #region Helper Methods
-
- ///
- /// Invokes the agent with the specified input text and returns the response stream.
- /// The caller is responsible for handling the responses (e.g., sending via turn context or collecting as string).
- ///
- /// The input text to send to the agent
- /// Optional agent thread to use. If null, creates a new empty thread.
- /// Cancellation token
- /// An async enumerable of agent response items
- private IAsyncEnumerable> InvokeAgentAsync(
- string incomingText,
- ChatHistoryAgentThread? agentThread = null,
- CancellationToken cancellationToken = default)
- {
- // NOTE: This won't retain history from previous messages in the thread currently
- // This could be added at a later time
- // For now, just always use new empty thread unless one is provided
- agentThread ??= new ChatHistoryAgentThread();
-
- var content = new ChatMessageContent
- {
- Role = AuthorRole.User,
- Content = incomingText,
- };
-
- return _chatCompletionAgent.InvokeAsync(content, agentThread, cancellationToken: cancellationToken);
- }
-
- #endregion
-}
\ No newline at end of file
diff --git a/samples/csharp/FoundryA365/src/hello_world_a365_agent/AgentLogic/SemanticKernel/SemanticKernelAgentLogicServiceFactory.cs b/samples/csharp/FoundryA365/src/hello_world_a365_agent/AgentLogic/SemanticKernel/SemanticKernelAgentLogicServiceFactory.cs
deleted file mode 100644
index 2dd2eb48f..000000000
--- a/samples/csharp/FoundryA365/src/hello_world_a365_agent/AgentLogic/SemanticKernel/SemanticKernelAgentLogicServiceFactory.cs
+++ /dev/null
@@ -1,83 +0,0 @@
-namespace HelloWorldA365.AgentLogic.SemanticKernel;
-
-using Azure.Core;
-using Azure.Identity;
-using HelloWorldA365.AgentLogic.AuthCache;
-using HelloWorldA365.Mcp;
-using HelloWorldA365.Models; // added for PresenceState
-using HelloWorldA365.Services;
-using Microsoft.Agents.A365.Tooling.Extensions.SemanticKernel.Services;
-using Microsoft.Agents.Builder;
-using Microsoft.Agents.Builder.App.UserAuth;
-using Microsoft.SemanticKernel;
-
-///
-/// There are still some work left here:
-/// 1- The factory structure doesn't follow the factory pattern.
-/// 2- There are constants that need to be moved to configuration (TBD on what configuration).
-/// 3- We need a way to dynamically build the MCP server URL. It is environment specific and it won't work in prod as is.
-/// 4- still the way we get the AA cert seems hacky.
-/// 5- Scope needs to be updated.
-/// 6- Remove disabling cert validation.
-///
-public sealed class SemanticKernelAgentLogicServiceFactory(
- IConfiguration configuration,
- IServiceProvider serviceProvider,
- ILogger logger,
- McpToolDiscovery mcpToolDiscovery,
- AgentTokenHelper tokenHelper,
- IMcpToolRegistrationService mcpToolRegistrationService,
- IAgentTokenCache tokenCache)
-{
- private readonly string certificateData = configuration.GetCertificateData() ?? string.Empty;
-
- public async Task CreateAsync(AgentMetadata agent, ITurnContext turnContext)
- {
- var kernelBuilder = Kernel.CreateBuilder();
- AddModel(kernelBuilder);
- var kernel = kernelBuilder.Build();
- await ConfigureKernelPlugins(agent, kernel, turnContext);
- // Resolve GraphService for constructor injection
- var scopedServiceProvider = serviceProvider.CreateScope().ServiceProvider;
-
- // Attempt to set presence to busy and status message indicating active work
-
- return new SemanticKernelAgentLogicService(tokenHelper, agent, kernel, certificateData, configuration, logger, mcpToolRegistrationService, tokenCache);
- }
-
- private async Task ConfigureKernelPlugins(AgentMetadata agent, Kernel kernel, ITurnContext turnContext)
- {
- var scopedServiceProvider = serviceProvider.CreateScope().ServiceProvider;
- // Prod scope for MCP servers.
- // aka.ms/atg/repo
- // https://github.com/bap-microsoft/MCP-Platform?tab=readme-ov-file#environments
- var requestContext = new TokenRequestContext(["ea9ffc3e-8a23-4a7d-836d-234d7c7565c1/.default"]);
- var tokenCredential = new AgentTokenCredential(tokenHelper, agent, certificateData);
- var accessToken = tokenCredential.GetTokenAsync(requestContext, CancellationToken.None).GetAwaiter().GetResult();
- string agentUserId = agent.UserId.ToString();
- var environmentId = configuration["McpPlatformEnvironmentId"] ?? Environment.GetEnvironmentVariable("McpPlatformEnvironmentId");
- if (string.IsNullOrEmpty(environmentId))
- {
- environmentId = $"Default-{agent.TenantId.ToString()}";
- }
- UserAuthorization userAuthorization = null;
- string authHandlerName = string.Empty;
-
- await mcpToolRegistrationService.AddToolServersToAgentAsync(kernel, userAuthorization, authHandlerName, turnContext, accessToken.Token);
- }
-
- private IKernelBuilder AddModel(IKernelBuilder kernelBuilder)
- {
- var deployment = configuration["ModelDeployment"] ?? throw new ArgumentNullException("ModelDeployment");
- var azureOpenAiEndpoint = configuration["AzureOpenAIEndpoint"] ?? throw new ArgumentNullException("AzureOpenAIEndPoint");
- // Kept this for people who use API key in settings.
- // var apiKey = _configuration["OpenAiApiKey"] ?? throw new ArgumentNullException("OpenAiApiKey");
-
- return kernelBuilder.AddAzureOpenAIChatCompletion(
- deploymentName: deployment,
- endpoint: azureOpenAiEndpoint,
- // Ensure token is always picked up from terminal
- new DefaultAzureCredential()
- );
- }
-}
\ No newline at end of file
diff --git a/samples/csharp/FoundryA365/src/hello_world_a365_agent/HelloWorldA365Agent.csproj b/samples/csharp/FoundryA365/src/hello_world_a365_agent/HelloWorldA365Agent.csproj
deleted file mode 100644
index 6f0bb7510..000000000
--- a/samples/csharp/FoundryA365/src/hello_world_a365_agent/HelloWorldA365Agent.csproj
+++ /dev/null
@@ -1,62 +0,0 @@
-ο»Ώ
-
-
- HelloWorldA365
- net9.0
- enable
- enable
- 6d3109d3-6ff5-438e-aa23-cddd3703b4cb
- SKEXP0001;SKEXP0110;SKEXP0130
-
-
-
- 1.12.0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/samples/csharp/FoundryA365/src/hello_world_a365_agent/HelloWorldA365Agent.sln b/samples/csharp/FoundryA365/src/hello_world_a365_agent/HelloWorldA365Agent.sln
deleted file mode 100644
index 7388567e4..000000000
--- a/samples/csharp/FoundryA365/src/hello_world_a365_agent/HelloWorldA365Agent.sln
+++ /dev/null
@@ -1,56 +0,0 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.5.2.0
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloWorldA365Agent", "HelloWorldA365Agent.csproj", "{036DE73A-2484-FA6D-3988-3D7DB76EB74E}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {036DE73A-2484-FA6D-3988-3D7DB76EB74E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {036DE73A-2484-FA6D-3988-3D7DB76EB74E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {036DE73A-2484-FA6D-3988-3D7DB76EB74E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {036DE73A-2484-FA6D-3988-3D7DB76EB74E}.Release|Any CPU.Build.0 = Release|Any CPU
- {70E6B5E9-1629-C773-71BF-524D7576D367}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {70E6B5E9-1629-C773-71BF-524D7576D367}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {70E6B5E9-1629-C773-71BF-524D7576D367}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {70E6B5E9-1629-C773-71BF-524D7576D367}.Release|Any CPU.Build.0 = Release|Any CPU
- {0748F01D-BBD8-D794-DFCA-BA70E95C35A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {0748F01D-BBD8-D794-DFCA-BA70E95C35A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {0748F01D-BBD8-D794-DFCA-BA70E95C35A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {0748F01D-BBD8-D794-DFCA-BA70E95C35A2}.Release|Any CPU.Build.0 = Release|Any CPU
- {86E3E929-5449-9E3F-2B1C-03E2B4760D04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {86E3E929-5449-9E3F-2B1C-03E2B4760D04}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {86E3E929-5449-9E3F-2B1C-03E2B4760D04}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {86E3E929-5449-9E3F-2B1C-03E2B4760D04}.Release|Any CPU.Build.0 = Release|Any CPU
- {7A3B78BE-C36A-C712-815C-93C5635FD9FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7A3B78BE-C36A-C712-815C-93C5635FD9FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7A3B78BE-C36A-C712-815C-93C5635FD9FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7A3B78BE-C36A-C712-815C-93C5635FD9FB}.Release|Any CPU.Build.0 = Release|Any CPU
- {C4FE6266-DE04-8002-1A47-6802C93816BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C4FE6266-DE04-8002-1A47-6802C93816BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C4FE6266-DE04-8002-1A47-6802C93816BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C4FE6266-DE04-8002-1A47-6802C93816BC}.Release|Any CPU.Build.0 = Release|Any CPU
- {FD1841CE-5F06-DA3F-DACC-828F06FD048D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FD1841CE-5F06-DA3F-DACC-828F06FD048D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FD1841CE-5F06-DA3F-DACC-828F06FD048D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FD1841CE-5F06-DA3F-DACC-828F06FD048D}.Release|Any CPU.Build.0 = Release|Any CPU
- {C87789D6-FE93-3871-1290-0BFEBE3D19EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C87789D6-FE93-3871-1290-0BFEBE3D19EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C87789D6-FE93-3871-1290-0BFEBE3D19EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C87789D6-FE93-3871-1290-0BFEBE3D19EA}.Release|Any CPU.Build.0 = Release|Any CPU
- {E099B67E-A7D7-B158-9101-7FCF3436F3D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E099B67E-A7D7-B158-9101-7FCF3436F3D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E099B67E-A7D7-B158-9101-7FCF3436F3D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E099B67E-A7D7-B158-9101-7FCF3436F3D3}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {A62A6059-F7FB-4D65-A242-68CC4925C04A}
- EndGlobalSection
-EndGlobal
diff --git a/samples/csharp/FoundryA365/src/hello_world_a365_agent/Mcp/McpAuthenticationHandler.cs b/samples/csharp/FoundryA365/src/hello_world_a365_agent/Mcp/McpAuthenticationHandler.cs
deleted file mode 100644
index 9990849db..000000000
--- a/samples/csharp/FoundryA365/src/hello_world_a365_agent/Mcp/McpAuthenticationHandler.cs
+++ /dev/null
@@ -1,113 +0,0 @@
-namespace HelloWorldA365.Mcp;
-
-using System.Net.Http.Headers;
-using Azure.Core;
-using HelloWorldA365.Models;
-using HelloWorldA365.Services;
-
-///
-/// HTTP message handler that automatically adds authentication tokens to requests
-/// using AgentTokenCredential for MCP endpoint authentication with built-in token caching.
-/// Only authenticates requests to the configured MCP server endpoint.
-///
-public class McpAuthenticationHandler : DelegatingHandler
-{
- private readonly AgentTokenCredential _tokenCredential;
- private readonly ILogger _logger;
- private readonly string[] _scopes;
- private readonly Uri _mcpServerEndpoint;
-
- public McpAuthenticationHandler(
- AgentTokenHelper tokenHelper,
- AgentMetadata agent,
- string certificateData,
- ILogger logger,
- string mcpServerEndpoint,
- string[]? scopes = null)
- {
- _tokenCredential = new AgentTokenCredential(tokenHelper, agent, certificateData);
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- _scopes = scopes ?? throw new ArgumentNullException(nameof(scopes));
-
- if (string.IsNullOrWhiteSpace(mcpServerEndpoint))
- {
- throw new ArgumentException("MCP server endpoint cannot be null or empty", nameof(mcpServerEndpoint));
- }
-
- _mcpServerEndpoint = new Uri(mcpServerEndpoint);
- _logger.LogInformation("MCPAuthenticationHandler configured for selective authentication on endpoint: {Endpoint}", _mcpServerEndpoint);
- }
-
- protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
- {
- // Only add authentication for requests to the configured MCP server endpoint
- if (ShouldAuthenticateRequest(request))
- {
- try
- {
- var requestContext = new TokenRequestContext(_scopes);
-
- var accessToken = await _tokenCredential.GetTokenAsync(requestContext, cancellationToken);
-
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Token);
-
- _logger.LogDebug("Added authentication token to MCP request for {RequestUri}, token expires at {ExpiresOn}",
- request.RequestUri, accessToken.ExpiresOn);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Failed to acquire authentication token for MCP request to {RequestUri}", request.RequestUri);
- throw;
- }
- }
- else
- {
- _logger.LogDebug("Skipping authentication for non-MCP request to {RequestUri}", request.RequestUri);
- }
-
- return await base.SendAsync(request, cancellationToken);
- }
-
- ///
- /// Determines if the request should be authenticated based on the target endpoint
- ///
- /// The HTTP request
- /// True if the request should be authenticated, false otherwise
- private bool ShouldAuthenticateRequest(HttpRequestMessage request)
- {
- if (request.RequestUri == null)
- {
- _logger.LogDebug("Request has no URI, skipping authentication");
- return false;
- }
-
- var requestUri = request.RequestUri;
-
- // Match scheme and host
- if (!string.Equals(requestUri.Scheme, _mcpServerEndpoint.Scheme, StringComparison.OrdinalIgnoreCase) ||
- !string.Equals(requestUri.Host, _mcpServerEndpoint.Host, StringComparison.OrdinalIgnoreCase))
- {
- _logger.LogDebug("Request scheme/host mismatch: Request={RequestScheme}://{RequestHost}, MCP={MCPScheme}://{MCPHost}",
- requestUri.Scheme, requestUri.Host, _mcpServerEndpoint.Scheme, _mcpServerEndpoint.Host);
- return false;
- }
-
- // Match port if specified
- if (requestUri.Port != _mcpServerEndpoint.Port)
- {
- _logger.LogDebug("Request port mismatch: Request={RequestPort}, MCP={MCPPort}",
- requestUri.Port, _mcpServerEndpoint.Port);
- return false;
- }
-
- var requestPath = requestUri.AbsolutePath.TrimEnd('/');
- var mcpPath = _mcpServerEndpoint.AbsolutePath.TrimEnd('/');
-
- var shouldAuthenticate = requestPath.StartsWith(mcpPath, StringComparison.OrdinalIgnoreCase);
-
- _logger.LogDebug("Path matching: Request='{RequestPath}', MCP='{MCPPath}', Should Authenticate={ShouldAuthenticate}",
- requestPath, mcpPath, shouldAuthenticate);
-
- return shouldAuthenticate;
- }
-}
\ No newline at end of file
diff --git a/samples/csharp/FoundryA365/src/hello_world_a365_agent/Mcp/McpClientHttpRequestLogger.cs b/samples/csharp/FoundryA365/src/hello_world_a365_agent/Mcp/McpClientHttpRequestLogger.cs
deleted file mode 100644
index ee28c035a..000000000
--- a/samples/csharp/FoundryA365/src/hello_world_a365_agent/Mcp/McpClientHttpRequestLogger.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-namespace HelloWorldA365.Mcp;
-
-using System.Text.Json;
-
-///
-/// HTTP logging handler for debugging purposes
-///
-public class McpClientHttpRequestLogger(ILogger logger) : DelegatingHandler
-{
- protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
- {
- // Log request
- logger.LogInformation("HTTP Request: {Method} {Uri}", request.Method, request.RequestUri);
- logger.LogInformation("Request Headers: {Headers}", JsonSerializer.Serialize(
- request.Headers.ToDictionary(h => h.Key, h => string.Join(", ", h.Value))
- ));
-
- if (request.Content != null)
- {
- var requestBody = await request.Content.ReadAsStringAsync(cancellationToken);
- logger.LogInformation("Request Body: {Body}", requestBody);
- }
-
- // Send request
- var response = await base.SendAsync(request, cancellationToken);
-
- // Log response
- logger.LogInformation("HTTP Response: {StatusCode} {ReasonPhrase}", (int)response.StatusCode, response.ReasonPhrase);
- logger.LogInformation("Response Headers: {Headers}", JsonSerializer.Serialize(
- response.Headers.ToDictionary(h => h.Key, h => string.Join(", ", h.Value))
- ));
-
- if (response.Content != null)
- {
- var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
- logger.LogInformation("Response Body: {Body}", responseBody);
- }
-
- return response;
- }
-}
\ No newline at end of file
diff --git a/samples/csharp/FoundryA365/src/hello_world_a365_agent/Mcp/McpFunctionWrapper.cs b/samples/csharp/FoundryA365/src/hello_world_a365_agent/Mcp/McpFunctionWrapper.cs
deleted file mode 100644
index 26f78f3aa..000000000
--- a/samples/csharp/FoundryA365/src/hello_world_a365_agent/Mcp/McpFunctionWrapper.cs
+++ /dev/null
@@ -1,138 +0,0 @@
-namespace HelloWorldA365.Mcp;
-
-using System.Text.Json;
-using Microsoft.SemanticKernel;
-
-///
-/// Wrapper class for MCP functions that allows parameter inspection and logging
-///
-public class McpFunctionWrapper(
- KernelFunction originalFunction,
- string functionName,
- ILogger logger,
- IConfiguration configuration)
-{
- public KernelFunction CreateWrappedFunction()
- {
- return KernelFunctionFactory.CreateFromMethod(
- method: InvokeWithLogging,
- functionName: functionName,
- description: originalFunction.Description,
- parameters: originalFunction.Metadata.Parameters,
- returnParameter: originalFunction.Metadata.ReturnParameter
- );
- }
-
- private async Task