Review a Cross-Tenant Azure Change
Review a production network change across two fictional Azure tenants.
Introduction
30 Second Summary
Joining a production initiative often means inheriting diagrams, tickets, and half-finished decisions. The challenge is finding the change that matters without losing the wider dependency chain.
In this project, you will review a fictional production transit VNet change across two Azure tenants. The finished review links the Bicep evidence to its dependencies, risks, and approval boundary.
What You'll Build
You'll finish with a production-readiness package that lets a reviewer trace the change from architecture and ticket evidence to a clear approve-or-block decision.
By the end of this project, you'll have:
- A cross-tenant architecture map that shows the runner-to-application path and the boundary between existing non-production resources and planned production work.
- A set of local Bicep checks that prove the reusable VNet module and its environment parameters compile without deploying Azure resources.
- A production change decision that records blocking defects, dependencies, implementation steps, validation evidence, rollback controls, and required reviews.
- Secret Mission: Triage a self-hosted runner that cannot reach a private Azure Container Registry endpoint, then hand the evidence to the correct team.
Are there any prerequisites?
You need Windows with WSL and Ubuntu already working. The setup step installs Azure CLI and Bicep for local validation, with no repository connection or Azure sign-in required.
Before We Start
Before We Start...
Before you inspect the change, lock in the boundary of the exercise. You are reviewing fictional evidence for one production transit VNet ticket while keeping real company data outside every file and response.
Set Up the WSL Engineering Workstation
Your Ubuntu environment is already available through WSL. The remaining local checks depend on Azure CLI and Bicep.
You only need Azure CLI and Bicep for this project. Both tools lint and compile infrastructure files locally, so no repository authentication or Azure sign-in is required.
In this step, get ready to:
- Prepare Ubuntu for the Microsoft package installer.
- Install and verify Azure CLI 2.90.0.
- Install and verify Bicep 0.47.16.
Prepare Ubuntu
- Press the Windows key to open Windows search.
- Type Ubuntu in the search field.
- Press Enter to open the Ubuntu terminal.
- Refresh Ubuntu's package index and install curl by running these commands:
sudo apt-get update
sudo apt-get install -y curl
What do these commands do?
The first command refreshes Ubuntu's package list. The second installs the download tool used by Microsoft's installer.
Ubuntu may request your Linux password. The cursor stays still while you type, which is expected.
- Confirm curl is ready by running:
curl --version
You'll see a curl version on the first output line. Ubuntu is now ready to download the Azure CLI installer.
Package installation failed?
Check that Ubuntu has internet access if the package index cannot refresh. A corporate VPN or proxy can block package downloads inside WSL.
Help me troubleshoot the Ubuntu package installation.
Install Azure CLI
Microsoft provides an installation script for Ubuntu and Debian. Expect this command to take a minute or two while it adds the Microsoft package source and installs the CLI.
- Install Azure CLI by running this command:
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
What does this command do?
The script configures Microsoft's signed package source for your Ubuntu release. It then installs the azure-cli package.
This changes only your local Ubuntu environment. It does not sign you in or connect to an Azure tenant.
- Verify the installed Azure CLI version by running:
az version
You'll see JSON output with azure-cli set to 2.90.0 or a later compatible version.
Azure CLI command not found?
Close Ubuntu and open it again if az is unavailable after installation. A fresh shell reloads the executable path.
Help me troubleshoot the Azure CLI installation.
Install Bicep
- Install the verified Bicep release through Azure CLI by running:
az bicep install --version v0.47.16
Why is no sign-in required?
Azure CLI downloads Bicep v0.47.16 into your local CLI tools. The installation works without az login because no Azure resource is being accessed.
- Verify the Bicep installation by running:
az bicep version
You'll see Bicep CLI version 0.47.16. Your local infrastructure validation toolchain is ready.
Bicep installation failed?
Run the install command again if Azure CLI reports that Bicep is missing. Check your internet connection if the binary download cannot complete.
Help me troubleshoot the Bicep installation.
Your local checks are ready. Next, you'll use the meeting evidence to map the cross-tenant production path before touching any infrastructure file.
Build the Enterprise Control Plane Context
Map the Cross-Tenant Production Path
Your Azure CLI and Bicep tools now work locally without authentication. That gives you a safe place to inspect infrastructure evidence.
The production change sits inside a wider cross-tenant path. You need to understand the ownership boundary, the existing proof of concept, and the dependency order before reviewing the first ticket.
In this step, get ready to:
- Create a local folder for the fictional review evidence.
- Map the private path across both Azure tenants.
- Order the Phase 2 dependencies around the first ticket.
Create the local review folder
This project uses a local evidence folder instead of a personal repository. Nothing in the folder connects to a real tenant or company system.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create the project folder and move into it by running these commands:
mkdir azure-change-review
cd azure-change-review
- Confirm the folder location by running:
pwd
You'll see a path ending in Desktop/azure-change-review. This folder holds only fictional training evidence.
Folder path looks different?
Run cd ~/Desktop again if the displayed path does not include your Desktop. Then repeat the folder creation command.
Help me correct the local project path.
Map the cross-tenant path
In the fictional architecture, GitHub Actions uses a self-hosted runner in Tenant A. Private traffic crosses a transit VNet, Azure Firewall, and global VNet peering before it reaches the application platform in Tenant B.
- Create architecture.md with the fictional ownership and traffic path by running:
cat > architecture.md <<'EOF'
# Fictional Phase 2 architecture
## Tenant A
- Central GitHub organization and workflows.
- Self-hosted production runners.
- Production transit VNet and Azure Firewall.
## Tenant B
- Application platform.
- Production transit VNet.
- Private Azure Container Registry and Key Vault.
## Private traffic path
Developer -> repository -> runner -> Tenant A transit VNet -> firewall -> global peering -> Tenant B transit VNet -> registry or application.
## Boundary
Non-production proves the pattern. Production remains planned and approval-controlled.
EOF
What does the architecture separate?
Tenant A owns the automation entry point and the first network controls. Tenant B owns the application platform and its private services.
Global VNet peering connects the two transit networks. The peering is a separate production change because both tenant owners must configure their side.
- Display the completed architecture map by running:
cat architecture.md
You'll see two tenant sections followed by one private traffic path. The map shows where ownership changes before the traffic reaches Tenant B.
Architecture file incomplete?
Check that the final EOF is on its own line if the terminal keeps waiting. Press Enter after that line to finish the file.
Help me repair the architecture file.
Order the production dependencies
Azure Container Registry depends on more than its resource definition. Network routing, private DNS, firewall rules, identity permissions, and Key Vault references must line up before a production workflow can use it.
- Create dependency-map.md with the Phase 2 sequence by running:
cat > dependency-map.md <<'EOF'
# Phase 2 dependency map
1. Confirm naming, regions, and non-overlapping CIDRs.
2. Prepare production resource groups.
3. Define production transit VNets and subnets.
4. Prepare Azure Firewall and route controls.
5. Configure both sides of global VNet peering.
6. Confirm private DNS resolution across the path.
7. Prepare ACR and its private endpoint.
8. Configure the service principal and RBAC.
9. Store approved secret references in Key Vault.
10. Prepare the production workflow and dry run.
11. Complete pull-request and change approvals.
12. Deploy, validate, capture evidence, and close the ticket.
Current project scope: review the first production transit VNet ticket.
EOF
- Display the dependency sequence by running:
cat dependency-map.md
You'll see the transit VNet work before firewall, peering, ACR, identity, workflow, and deployment. That order prevents a later ticket from pretending an earlier dependency already exists.
You can now explain the production path and the order of work. Next, you'll inspect the proven non-production files that the first production ticket must reuse carefully.
Validate the Non-Production Pattern
Review the Non-Production VNet Pattern
Your architecture map now shows where the first ticket sits in the production sequence. The review can move from the meeting narrative to concrete infrastructure evidence.
The non-production proof of concept supplies a reusable Bicep module and environment-specific parameters. You will validate both locally before treating them as a production starting point.
In this step, get ready to:
- Create and lint the reusable transit VNet module.
- Compile the known-good non-production parameter file.
- Review a reference workflow that repeats the same checks.
Validate the reusable VNet module
The shared module defines the network shape once. Each environment supplies its own region, name, address space, subnets, and environment label.
- Create the module and environment folders inside azure-change-review by running:
mkdir -p modules/networking environments/nonprod
- Confirm the two paths exist by running:
find . -maxdepth 2 -type d | sort
You'll see ./modules/networking and ./environments/nonprod in the output.
- Create modules/networking/main.bicep with the reusable VNet definition by running:
cat > modules/networking/main.bicep <<'EOF'
targetScope = 'resourceGroup'
// Accept values that change between environments.
param location string
param vnetName string
param addressPrefix string
param subnets array
param environment string
// Describe the planned transit VNet.
resource transitVnet 'Microsoft.Network/virtualNetworks@2025-07-01' = {
name: vnetName
location: location
tags: { environment: environment }
properties: {
addressSpace: { addressPrefixes: [addressPrefix] }
subnets: subnets
}
}
output virtualNetworkId string = transitVnet.id
EOF
What does this module define?
- The parameters keep environment values outside the reusable resource.
- The transitVnet resource describes the planned VNet without deploying it.
- The output exposes the resource identifier that a later deployment would return.
- Lint the reusable module by running:
az bicep lint --file modules/networking/main.bicep
The command finishes without an error. That result proves the module is structurally valid.
Seeing a lint error?
Check the braces around properties and confirm the resource type is Microsoft.Network/virtualNetworks@2025-07-01.
Help me diagnose the Bicep lint error.
Compile the non-production parameters
A native .bicepparam file links to the shared module and supplies one environment's values. These files store values as plain text, so credentials and secrets never belong in them.
Why use separate parameter files?
A shared module prevents every environment from redefining the same VNet structure. Separate parameter files make environment differences visible during review.
This structure makes each environment's proposed values easy to compare during review.
- Create environments/nonprod/params.bicepparam with the known-good fictional values by running:
cat > environments/nonprod/params.bicepparam <<'EOF'
using '../../modules/networking/main.bicep'
param location = 'westeurope'
param vnetName = 'vnet-harbor-nonprod-weu-transit-001'
param addressPrefix = '10.40.0.0/16'
param subnets = [
{ name: 'AzureFirewallSubnet', properties: { addressPrefix: '10.40.0.0/26' } }
{ name: 'snet-shared-services', properties: { addressPrefix: '10.40.1.0/24' } }
]
param environment = 'nonprod'
EOF
- Compile the non-production parameters by running:
az bicep build-params --file environments/nonprod/params.bicepparam
What does this command prove?
The command checks the parameter names against the linked module. It then creates environments/nonprod/params.json.
This is a local compilation. It creates no Azure resources and uses no credentials.
- Inspect the generated environment evidence by running:
grep -E 'nonprod|10.40.0.0/16|vnet-harbor-nonprod' environments/nonprod/params.json
You'll see the nonprod label, the 10.40.0.0/16 address space, and the fictional non-production VNet name.
Parameter build failed?
Confirm the using path starts two directories above the parameter file. The module must exist before Bicep can generate the JSON.
Help me troubleshoot the parameter build.
Review the automated gate
The next workflow is reference evidence only, so you do not create a repository or run it. GitHub Actions can repeat the local checks with read-only repository access and no Azure sign-in.
- Review this fictional pull-request workflow as reference evidence:
name: Validate Bicep
on:
pull_request:
branches: [main]
paths:
- 'modules/**/*.bicep'
- 'environments/**/*.bicepparam'
permissions:
contents: read
jobs:
validate:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7.0.1
- uses: azure/cli@v3.0.0
with:
azcliversion: 2.90.0
inlineScript: |
az bicep install --version v0.47.16
az bicep lint --file modules/networking/main.bicep
az bicep build-params --file environments/nonprod/params.bicepparam
What should a reviewer notice?
- The path filters run the workflow only when the relevant infrastructure files change.
- The contents: read permission keeps the validation job read-only.
- The pinned Azure CLI and Bicep versions keep local and automated checks aligned.
The non-production evidence is valid and reproducible. Next, you'll prepare a production candidate and test it against the ticket.
Expose the Copy-to-Production Trap
Find the Production Copy Defect
The proven non-production module and parameters now pass both local and automated-style checks. That gives the production ticket a credible starting point.
A production review still needs evidence that the values match the intended environment. You will compile the candidate first, then compare the generated JSON with the ticket.
In this step, get ready to:
- Record the fictional production acceptance criteria.
- Compile the production candidate before reviewing its values.
- Classify the production review findings.
Record the ticket criteria
The fictional ticket is the source of truth for the production values. It also keeps firewall and peering deployment outside the first change.
- Create ticket-plat-201.md with the production acceptance criteria by running:
cat > ticket-plat-201.md <<'EOF'
# PLAT-201: Prepare the production transit VNet
## Acceptance criteria
- Location: westeurope.
- VNet name: vnet-harbor-prod-weu-transit-001.
- VNet address space: 10.80.0.0/16.
- AzureFirewallSubnet: 10.80.0.0/26.
- Shared-services subnet: 10.80.1.0/24.
- Environment label: prod.
- Shared Bicep module remains unchanged.
- Local lint and parameter compilation pass.
## Boundaries
- Do not deploy Azure resources.
- Do not configure firewall rules or peering.
- Record review evidence and required approvals.
EOF
- Display the ticket criteria by running:
cat ticket-plat-201.md
You'll see six required production values and three change boundaries. The ticket gives the review a measurable definition of ready.
Compile the candidate
The candidate follows the established reuse approach. The next command places the known-good parameter structure in the production path.
- Create the production directory and copy the candidate into it by running:
mkdir -p environments/prod
cp environments/nonprod/params.bicepparam environments/prod/params.bicepparam
- Confirm the production candidate exists by running:
ls environments/prod
You'll see params.bicepparam in the production directory.
Before you compile the candidate, do you think Bicep can decide whether its values satisfy the production ticket?
- Compile the production candidate by running:
az bicep build-params --file environments/prod/params.bicepparam
The command completes successfully and creates environments/prod/params.json. This green result proves structural compatibility only.
What does the green build prove?
A Bicep build checks parameter names, types, and the linked module. It confirms that the parameter file fits the module's structure.
The check has no access to the acceptance criteria in ticket-plat-201.md. Production suitability still requires evidence from the ticket.
🙋♀️ Candidate failed before review?
Confirm the copied file remains under environments/prod so its relative using path still reaches the shared module.
Help me restore the intended green build.
Inspect and classify the findings
Now compare the generated evidence with the ticket. This is the point where production intent becomes testable.
Before you inspect the file, which fields would reveal that a structurally valid candidate belongs to another environment?
- Inspect the generated production values by running:
grep -E 'nonprod|10.40.0.0/16|vnet-harbor-nonprod' environments/prod/params.json
You'll see nonprod, 10.40.0.0/16, and the non-production VNet name inside the production output.
The green build exposed the gap
The successful compilation accepted values that belong to the wrong environment. A deployment based on this candidate would violate the ticket even though the automated structure check is green.
This is a blocking review result. Production correction and a fresh validation run are required before approval.
- Create review-findings.md with the blocking findings by running:
cat > review-findings.md <<'EOF'
# PLAT-201 review findings
Decision: Block pending correction.
## Blocking findings
1. The environment remains nonprod instead of prod.
2. The VNet name identifies the non-production network.
3. The VNet CIDR is 10.40.0.0/16 instead of 10.80.0.0/16.
4. Both subnet ranges come from the non-production address plan.
## Evidence boundary
Bicep compilation proves structural compatibility. Ticket comparison proves production suitability.
EOF
- Display the completed review findings by running:
cat review-findings.md
You'll see a blocked decision supported by four concrete mismatches. The finding is now ready for correction rather than approval.
You found the defect before deployment and tied it to the ticket. Next, you'll correct the candidate and turn the evidence into a controlled change-review package.
Raise the Production Change
Build the Production Change Review
Your review blocked the production candidate with evidence from the ticket and generated JSON. The copied values never reached a deployment command.
A senior review now needs a corrected candidate plus a clear change boundary. You will validate the fix, document the remaining dependencies, and prepare a stand-up update.
In this step, get ready to:
- Correct and revalidate the production parameters.
- Create the draft change-review package.
- Summarize progress, blockers, risks, and required reviews.
Correct and revalidate the candidate
The ticket supplies a fictional production name and non-overlapping address plan. The reusable module stays unchanged because its structure already passed the non-production review.
- Replace environments/prod/params.bicepparam with the corrected production values by running:
cat > environments/prod/params.bicepparam <<'EOF'
using '../../modules/networking/main.bicep'
// Apply the approved fictional production values.
param location = 'westeurope'
param vnetName = 'vnet-harbor-prod-weu-transit-001'
param addressPrefix = '10.80.0.0/16'
param subnets = [
{ name: 'AzureFirewallSubnet', properties: { addressPrefix: '10.80.0.0/26' } }
{ name: 'snet-shared-services', properties: { addressPrefix: '10.80.1.0/24' } }
]
param environment = 'prod'
EOF
What changed?
- The VNet name now identifies the production environment.
- The 10.80.0.0/16 range stays separate from the fictional non-production range.
- The firewall and shared-services subnet prefixes both sit inside the production address space.
Before you rebuild the file, which copied values should disappear from the generated JSON?
- Compile the corrected production parameters by running:
az bicep build-params --file environments/prod/params.bicepparam
- Inspect the corrected production evidence by running:
grep -E 'prod|10.80.0.0/16|vnet-harbor-prod' environments/prod/params.json
if grep -q 'nonprod' environments/prod/params.json; then echo 'FAIL: copied value remains'; else echo 'PASS: no nonprod values'; fi
You'll see the production label, production VNet name, production address range, and PASS: no nonprod values. The corrected candidate now matches the ticket's environment intent.
Correction still failing?
Confirm that you replaced the file under environments/prod and rebuilt it before checking the generated JSON.
Help me compare the corrected production evidence.
Create the change-review package
A passing build supports technical review, but it does not authorize production deployment. The draft change record keeps implementation, validation, rollback, and approval responsibilities visible.
- Create the scope and implementation sections in change-review.md by running:
cat > change-review.md <<'EOF'
# CR-PLAT-201: Production transit VNet review
Status: Draft.
Decision: Ready for technical review. Deployment is not authorized.
## Scope
Prepare the fictional Tenant B production transit VNet from the reviewed non-production pattern.
## Dependencies
- Firewall deployment and rules remain separate work.
- Both sides of global VNet peering require their own approved change.
- ACR, private DNS, identity, and workflow readiness are not assumed.
## Implementation
1. Confirm the approved production parameter file.
2. Run an Azure what-if in the authorized environment.
3. Deploy only after technical and change approvals.
EOF
- Display the first half of the change record by running:
cat change-review.md
You'll see a draft status and an explicit statement that deployment remains unauthorized. The dependency section prevents later work from being treated as complete.
- Append the validation, rollback, approval, and evidence sections to change-review.md by running:
cat >> change-review.md <<'EOF'
## Validation
1. Confirm the deployment provisioning state.
2. Confirm the VNet name, region, CIDR, tags, and subnets.
3. Validate routes, DNS, peering, and firewall evidence in their tickets.
## Rollback
Before deployment, close the change or revert the candidate. After deployment, assess dependencies before deleting any network resource.
## Approvals
- Technical review: Pending.
- Network owner review: Pending.
- Security review: Pending.
- Production change approval: Pending.
## Evidence
- Ticket criteria: Recorded.
- Local Bicep checks: Passed.
- Azure what-if: Pending authorized access.
- Deployment validation: Out of scope.
EOF
Why keep the controls separate?
Validation defines the evidence that proves the change worked. Rollback defines how to recover without deleting resources that later dependencies may use.
Every approval stays pending because this simulation prepares the review package only. It never claims permission to deploy.
- Display the complete change-review package by running:
cat change-review.md
You'll see scope, dependencies, implementation, validation, rollback, approvals, and evidence in one reviewable record.
Prepare the stand-up update
A useful stand-up update separates completed work from the next authorized action. It also names blockers and review owners without claiming that another team has approved the change.
- Create standup-update.md with the final update by running:
cat > standup-update.md <<'EOF'
# Stand-up update
Progress: Reviewed the non-production VNet pattern and corrected the production candidate against PLAT-201.
Evidence: Local Bicep lint and parameter compilation pass with the fictional production values.
Blockers: Azure what-if and deployment require authorized tenant access plus approved change windows.
Next: Request technical, network-owner, and security review of the draft change package.
Risk: Firewall, peering, DNS, identity, and ACR dependencies remain separate production work.
Reviews needed: Technical review and production change approval.
EOF
- Display the stand-up update by running:
cat standup-update.md
You'll see a concise update that names completed evidence, current blockers, the next action, remaining risk, and required reviews. That is enough context for the team to respond.
Secret mission
Triage Runner-to-ACR Connectivity
A production runner cannot reach a private container registry even though peering and the firewall look healthy. Trace the evidence to the first failed dependency, then prepare a focused team handoff.
Clean Up Your Resources
Clean Up Your Resources
This project creates only local fictional files, so there are no Azure resource charges or ongoing cloud costs. Decide whether to keep the evidence, pause your local work, or delete the project folder.
Resources you used:
- The local ~/Desktop/azure-change-review folder contains the fictional architecture, Bicep files, generated JSON, review findings, change record, stand-up update, and optional ACR triage handoff.
Keep everything running
No action needed. Choose this if you want to keep the fictional evidence for later review.
- Keep the ~/Desktop/azure-change-review folder in Ubuntu.
- Reuse the architecture and change-review files when you practise future tickets.
Pause - I'll come back to this later
No process continues running after the local commands finish. Pause by closing the terminal while keeping every file.
- Confirm your latest terminal commands have finished.
- Close the Ubuntu terminal window.
- Return later by opening Ubuntu and moving to ~/Desktop/azure-change-review.
Delete - I don't want to use this again
Remove all fictional project evidence and start fresh. The Azure CLI and Bicep installations remain available for other projects.
Deletion is permanent
Deleting ~/Desktop/azure-change-review permanently removes every local file created in this project. Copy anything you want to keep before continuing.
- Open the Ubuntu terminal.
- Delete the local project folder by running:
rm -rf ~/Desktop/azure-change-review
- Verify the folder is gone by running:
if [ ! -d ~/Desktop/azure-change-review ]; then echo 'Cleanup complete'; else echo 'Folder still exists'; fi
You'll see Cleanup complete. No cloud resource or remote repository needs deletion.
Nice Work!
Nice Work!
You did it! You turned an unfamiliar cross-tenant production initiative into a structured review with reproducible evidence, blocking findings, and a controlled change decision.
You've learned how to:
- Built a cross-tenant architecture map that traces the fictional runner-to-application path and separates current evidence from planned production work.
- Validated a reusable Bicep module with local linting and environment-specific .bicepparam compilation.
- Caught a production copy defect that passed structural validation, then prepared a controlled change-review package with evidence, rollback, and approval boundaries.
- Secret Mission: Traced a fictional runner-to-Azure Container Registry failure to private DNS resolution and wrote an evidence-based team handoff.
Ready to quiz yourself?