Skip to content

Step 4 — Infra-as-Code Plan

Generated by Bicep Planner agent | 2026-03-11

⬅️ Previous📑 IndexNext ➡️
Governance ConstraintsDemo IndexBicep templates

This plan defines the Bicep implementation for the Nordic Fresh Foods (FreshConnect MVP) platform — a cost-optimized N-tier web application in swedencentral. The deployment uses Azure Verified Modules (AVM) for all resources and a 5-phase deployment strategy with approval gates.

PropertyValue
Projectnordic-fresh-foods
IaC ToolBicep (AVM-first)
Regionswedencentral
EnvironmentsDev + Prod
Deployment StrategyPhased (5 phases)
Total Resources19 resource types
Estimated Monthly Cost~$204 (Prod + Dev steady-state)
Budget<€1,000/month
ComplianceGDPR, PCI-DSS
  • 9 mandatory tags on resource groups (Azure Policy Deny) — expanded from original 4
  • Azure AD-only auth required on SQL Server (MCAPSGov Deny)
  • MFA required for deployment operations (Management Group Deny)
  • Tag inheritance via Modify policy: 9 tags auto-propagate from RG to child resources

See Governance Constraints for full details.


#ResourceTypeAVM ModuleVersionSKU / Tier
1Resource GroupMicrosoft.Resources/resourceGroupsN/A (subscription-level)
2Virtual NetworkMicrosoft.Network/virtualNetworksbr/public:avm/res/network/virtual-network0.7.2Standard (3 subnets)
3Log Analytics WorkspaceMicrosoft.OperationalInsights/workspacesbr/public:avm/res/operational-insights/workspace0.15.0Pay-per-GB
4Application InsightsMicrosoft.Insights/componentsbr/public:avm/res/insights/component0.7.1Workspace-based
5Key VaultMicrosoft.KeyVault/vaultsbr/public:avm/res/key-vault/vault0.13.3Standard
6Azure SQL ServerMicrosoft.Sql/serversbr/public:avm/res/sql/server0.21.1
7Azure SQL DatabaseMicrosoft.Sql/servers/databases(via SQL Server module)S0 (10 DTU)
8Storage AccountMicrosoft.Storage/storageAccountsbr/public:avm/res/storage/storage-account0.32.0Standard LRS
9Private DNS Zone (SQL)Microsoft.Network/privateDnsZonesbr/public:avm/res/network/private-dns-zone0.8.1privatelink.database.windows.net
10Private DNS Zone (Blob)Microsoft.Network/privateDnsZonesbr/public:avm/res/network/private-dns-zone0.8.1privatelink.blob.core.windows.net
11Private Endpoint (SQL)Microsoft.Network/privateEndpointsbr/public:avm/res/network/private-endpoint0.12.0
12Private Endpoint (Blob)Microsoft.Network/privateEndpointsbr/public:avm/res/network/private-endpoint0.12.0
13App Service PlanMicrosoft.Web/serverfarmsbr/public:avm/res/web/serverfarm0.7.0S1 Linux (2 instances)
14App ServiceMicrosoft.Web/sitesbr/public:avm/res/web/site0.22.0S1 (on plan)
15Budget Alert (Aggregate)Microsoft.Consumption/budgetsRaw Bicep resource€1,000/month (subscription)
16Budget Alert (Prod)Microsoft.Consumption/budgetsRaw Bicep resource€800/month (RG-scoped)
17Budget Alert (Dev)Microsoft.Consumption/budgetsRaw Bicep resource€200/month (RG-scoped)
18Private DNS Zone (KV)Microsoft.Network/privateDnsZonesbr/public:avm/res/network/private-dns-zone0.8.1privatelink.vaultcore.azure.net
19Private Endpoint (KV)Microsoft.Network/privateEndpointsbr/public:avm/res/network/private-endpoint0.12.0
ResourceProd SKUDev SKUNotes
App Service PlanS1 (2 instances)B1 (1 instance)No autoscale in dev
Azure SQL DatabaseS0 (10 DTU)Basic (5 DTU)Reduced capacity
Virtual Network3 subnets1 subnetNo PE subnet in dev
Private Endpoints2 (SQL + Storage)0Not needed for dev
Private DNS Zones20Not needed for dev

infra/bicep/nordic-fresh-foods/
├── main.bicep # Orchestrator — all module calls
├── main.bicepparam # Parameter file (prod defaults)
├── main.dev.bicepparam # Parameter file (dev overrides)
├── deploy.ps1 # Deployment script (phased)
├── modules/
│ ├── network.bicep # VNet + subnets + NSGs
│ ├── monitoring.bicep # Log Analytics + App Insights
│ ├── keyvault.bicep # Key Vault + access policies
│ ├── sql.bicep # SQL Server + Database + PE
│ ├── storage.bicep # Storage Account + PE
│ ├── dns.bicep # Private DNS Zones + VNet links
│ ├── compute.bicep # App Service Plan + App Service
│ └── budget.bicep # Consumption budget + alerts

Every module accepts these standard parameters:

Parameters (standard):
location: string # Region (default: resourceGroup().location)
tags: object # All 11 tags (9 policy + 2 best-practice)
uniqueSuffix: string # uniqueString(resourceGroup().id)
environment: string # 'dev' | 'prod'
# Policy-enforced tag keys (EXACT names from Azure Policy Deny rule):
# 1. environment 2. owner
# 3. costcenter 4. application
# 5. workload 6. sla
# 7. backup-policy 8. maint-window
# 9. technical-contact
# Best-practice additions: ManagedBy, Project
Parameters (module-specific):
# Each module defines additional params as needed
Outputs (standard):
resourceId: string # Resource ID
resourceName: string # Resource name
principalId: string # Managed Identity principal (where applicable)
flowchart TB
    Main["main.bicep"] -->|"location, tags, uniqueSuffix, env"| Network["network.bicep"]
    Main -->|"location, tags, uniqueSuffix"| Monitoring["monitoring.bicep"]
    Main -->|"location, tags, uniqueSuffix, vnetId"| KeyVault["keyvault.bicep"]
    Main -->|"location, tags, uniqueSuffix, subnetId, dnsZoneId"| SQL["sql.bicep"]
    Main -->|"location, tags, uniqueSuffix, subnetId, dnsZoneId"| Storage["storage.bicep"]
    Main -->|"location, tags, vnetId"| DNS["dns.bicep"]
    Main -->|"location, tags, uniqueSuffix, subnetId, appInsightsKey, kvUri"| Compute["compute.bicep"]
    Main -->|"tags, budgetAmount"| Budget["budget.bicep"]

resource: main.bicep
type: orchestrator
dependencies: []
config:
- Define all parameters (location, environment, tags, SKU overrides)
- Generate uniqueSuffix: uniqueString(resourceGroup().id)
- Build consolidated tags object with EXACT policy-enforced keys:
environment, owner, costcenter, application, workload, sla,
backup-policy, maint-window, technical-contact
+ ManagedBy, Project (best-practice)
- Call all modules in dependency order
- Output key resource IDs and endpoints
resource: Microsoft.Network/virtualNetworks
module: br/public:avm/res/network/virtual-network:0.7.2
sku: Standard
dependencies: []
config:
name: vnet-nordic-fresh-foods-{env}
addressPrefixes: ["10.0.0.0/16"]
subnets:
- name: snet-app
addressPrefix: "10.0.1.0/24"
delegation: Microsoft.Web/serverFarms
nsg: nsg-app-{env}
- name: snet-data
addressPrefix: "10.0.2.0/24"
nsg: nsg-data-{env}
- name: snet-pe
addressPrefix: "10.0.3.0/24"
nsg: nsg-pe-{env}
privateEndpointNetworkPolicies: Enabled
conditional: Dev uses snet-app only (no snet-pe, synthetic data only)
diagnosticSettings:
vnet:
workspaceResourceId: logAnalytics.outputs.resourceId
categoryGroup: allLogs # VMProtectionAlerts
metricCategories: AllMetrics
nsg: # All NSGs — NetworkSecurityGroupEvent + RuleCounter
workspaceResourceId: logAnalytics.outputs.resourceId
categoryGroup: allLogs
tags: all 11 tags
naming: CAF — vnet-{project}-{env}

Task 3: monitoring.bicep — Log Analytics + Application Insights

Section titled “Task 3: monitoring.bicep — Log Analytics + Application Insights”
resource: Microsoft.OperationalInsights/workspaces + Microsoft.Insights/components
modules:
- br/public:avm/res/operational-insights/workspace:0.15.0
- br/public:avm/res/insights/component:0.7.1
sku: Pay-per-GB
dependencies: []
config:
logAnalytics:
name: log-nordic-fresh-foods-{env}
retentionInDays: 30
dailyQuotaGb: 5
appInsights:
name: appi-nordic-fresh-foods-{env}
kind: web
applicationType: web
workspaceResourceId: logAnalytics.outputs.resourceId
samplingPercentage: 50 # reduce ingestion cost; tune post-launch
ingestionGuardrails:
prod:
dailyQuotaGb: 2 # aligned to ~$204 cost estimate
expectedMonthlyGb: 1-3
dev:
dailyQuotaGb: 0.5
expectedMonthlyGb: 0.1-0.5
dataCollectionRules:
- Filter verbose dependency telemetry in prod
- Exclude health-check request logging
- Set 30-day retention (matches Log Analytics)
tags: all 11 tags
naming: CAF — log-{project}-{env}, appi-{project}-{env}
resource: Microsoft.KeyVault/vaults
module: br/public:avm/res/key-vault/vault:0.13.3
sku: Standard
dependencies: [network.bicep]
config:
name: kv-nff-{env}-{suffix}
enableRbacAuthorization: true
enablePurgeProtection: true
enableSoftDelete: true
softDeleteRetentionInDays: 90
publicNetworkAccess: Disabled (prod) | Enabled (dev)
networkAcls:
defaultAction: Deny
bypass: AzureServices
privateEndpoint: # PROD ONLY — required since publicNetworkAccess: Disabled
name: pe-kv-nordic-fresh-foods-{env}
subnetId: snet-pe
groupId: vault
privateDnsZoneId: privatelink.vaultcore.azure.net
secretConsumption:
pattern: Key Vault References in App Service appSettings
example: "@Microsoft.KeyVault(SecretUri=https://kv-nff-prod-{suffix}.vault.azure.net/secrets/{name})"
diagnosticSettings:
workspaceResourceId: logAnalytics.outputs.resourceId
categoryGroup: audit # AuditEvent category — required by MCAPSGov Audit
metricCategories: AllMetrics
tags: all 11 tags
naming: CAF — kv-{short}-{env}-{suffix} (max 24 chars)
resource: Microsoft.Network/privateDnsZones
module: br/public:avm/res/network/private-dns-zone:0.8.1
dependencies: [network.bicep]
config:
zones:
- name: privatelink.database.windows.net
virtualNetworkLinks:
- virtualNetworkResourceId: vnet.outputs.resourceId
- name: privatelink.blob.core.windows.net
virtualNetworkLinks:
- virtualNetworkResourceId: vnet.outputs.resourceId
- name: privatelink.vaultcore.azure.net
virtualNetworkLinks:
- virtualNetworkResourceId: vnet.outputs.resourceId
conditional: Prod only (skip for dev)
tags: all 11 tags

Task 6: sql.bicep — Azure SQL Server + Database + Private Endpoint

Section titled “Task 6: sql.bicep — Azure SQL Server + Database + Private Endpoint”
resource: Microsoft.Sql/servers + databases
module: br/public:avm/res/sql/server:0.21.1
sku: S0 (10 DTU) prod | Basic (5 DTU) dev
dependencies: [network.bicep, dns.bicep, monitoring.bicep]
config:
server:
name: sql-nordic-fresh-foods-{env}
minimalTlsVersion: '1.2'
administrators:
azureADOnlyAuthentication: true # POLICY REQUIRED (Deny)
login: Entra admin group
sid: <admin-group-object-id>
principalType: Group
publicNetworkAccess: Disabled (prod) | Enabled (dev)
database:
name: sqldb-freshconnect-{env}
sku: { name: S0, tier: Standard } (prod) | { name: Basic, tier: Basic } (dev)
maxSizeBytes: 268435456000 # 250 GB
privateEndpoint:
name: pe-sql-nordic-fresh-foods-{env}
subnetId: snet-pe
groupId: sqlServer
privateDnsZoneId: privatelink.database.windows.net
conditional: Prod only
diagnosticSettings:
workspaceResourceId: logAnalytics.outputs.resourceId
categoryGroup: allLogs
metricCategories: AllMetrics
securityAlertPolicies: # Defender for SQL — satisfies MCAPSGov Audit
state: Enabled
emailAccountAdmins: true
retentionDays: 30
tags: all 11 tags
naming: CAF — sql-{project}-{env}, sqldb-{app}-{env}
serverIdentity:
type: SystemAssigned # Required for SQL to resolve Entra principals
graphPermissions:
- User.Read.All # Resolve managed identity display names
note: |
The SQL logical server needs a system-assigned identity with Microsoft Graph
User.Read.All (or Directory Readers role) to resolve App Service managed
identity principals during CREATE USER ... FROM EXTERNAL PROVIDER.
Alternative: assign Directory Readers role to the SQL server identity.
postDeployment:
- step: Create contained database user for App Service MI
command: |
CREATE USER [app-nordic-fresh-foods-{env}-{suffix}] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [app-nordic-fresh-foods-{env}-{suffix}];
ALTER ROLE db_datawriter ADD MEMBER [app-nordic-fresh-foods-{env}-{suffix}];
timing: After Phase 5 (App Service MI principal available)
prerequisite: |
1. Entra admin group must be authenticated to SQL
2. SQL server identity must have Graph permissions (see serverIdentity above)
executionPath:
prod: |
Use AzureCLI deploymentScript resource (Microsoft.Resources/deploymentScripts)
running inside the VNet (subnetResourceIds: [snet-app]) to reach SQL via PE.
Script authenticates as the Entra admin group's service principal.
dev: |
Direct sqlcmd from deployer workstation (public access enabled on dev SQL).
note: Required because azureADOnlyAuthentication=true prevents SQL auth

Task 7: storage.bicep — Storage Account + Private Endpoint

Section titled “Task 7: storage.bicep — Storage Account + Private Endpoint”
resource: Microsoft.Storage/storageAccounts
module: br/public:avm/res/storage/storage-account:0.32.0
sku: Standard_LRS
dependencies: [network.bicep, dns.bicep, monitoring.bicep]
config:
name: stnff{env}{suffix}
kind: StorageV2
minimumTlsVersion: TLS1_2
supportsHttpsTrafficOnly: true
allowBlobPublicAccess: false
allowSharedKeyAccess: false
publicNetworkAccess: Disabled (prod) | Enabled (dev)
networkAcls:
defaultAction: Deny (prod) | Allow (dev)
bypass: AzureServices
blobServices:
containers:
- name: product-images
publicAccess: None
- name: assets
publicAccess: None
privateEndpoints:
- name: pe-st-nordic-fresh-foods-{env}
subnetResourceId: snet-pe
service: blob
privateDnsZoneResourceIds: [privatelink.blob.core.windows.net]
conditional: Prod only
diagnosticSettings:
workspaceResourceId: logAnalytics.outputs.resourceId
tags: all 11 tags
naming: CAF — st{short}{env}{suffix} (max 24 chars, no hyphens)

Task 8: compute.bicep — App Service Plan + App Service

Section titled “Task 8: compute.bicep — App Service Plan + App Service”
resource: Microsoft.Web/serverfarms + Microsoft.Web/sites
modules:
- br/public:avm/res/web/serverfarm:0.7.0
- br/public:avm/res/web/site:0.22.0
sku: S1 (prod) | B1 (dev)
dependencies: [network.bicep, keyvault.bicep, monitoring.bicep, sql.bicep, storage.bicep]
config:
plan:
name: asp-nordic-fresh-foods-{env}
kind: linux
sku: { name: S1, capacity: 2 } (prod) | { name: B1, capacity: 1 } (dev)
reserved: true # Linux
site:
name: app-nordic-fresh-foods-{env}-{suffix}
kind: app,linux
httpsOnly: true
identity:
type: SystemAssigned
siteConfig:
minTlsVersion: '1.2'
ftpsState: FtpsOnly
alwaysOn: true (prod) | false (dev)
vnetRouteAllEnabled: true
http20Enabled: true
virtualNetworkSubnetId: snet-app
appSettings:
- name: APPLICATIONINSIGHTS_CONNECTION_STRING
value: appInsights.outputs.connectionString
- name: AZURE_KEY_VAULT_URI
value: keyVault.outputs.uri
autoscaleSettings: # Prod only
minCount: 2
maxCount: 3
rules:
- metricName: CpuPercentage
operator: GreaterThan
threshold: 70
direction: Increase
changeCount: 1
- metricName: CpuPercentage
operator: LessThan
threshold: 30
direction: Decrease
changeCount: 1
roleAssignments:
- roleDefinitionId: Key Vault Secrets User
principalId: site.outputs.principalId
- roleDefinitionId: Storage Blob Data Contributor
principalId: site.outputs.principalId
diagnosticSettings:
workspaceResourceId: logAnalytics.outputs.resourceId
tags: all 11 tags
naming: CAF — asp-{project}-{env}, app-{project}-{env}-{suffix}

Task 9: budget.bicep — Consumption Budget + Alerts

Section titled “Task 9: budget.bicep — Consumption Budget + Alerts”
resource: Microsoft.Consumption/budgets
module: Raw Bicep (no subscription-scope AVM)
dependencies: []
config:
budgetTopology:
aggregate:
name: budget-nordic-fresh-foods
scope: subscription
amount: budgetAmount # parameterized, default EUR 1000
timeGrain: Monthly
category: Cost
perEnvironment:
- name: budget-nff-prod
scope: resourceGroup (rg-nordic-fresh-foods-prod)
amount: budgetAmountProd # parameterized, default EUR 800
- name: budget-nff-dev
scope: resourceGroup (rg-nordic-fresh-foods-dev)
amount: budgetAmountDev # parameterized, default EUR 200
notifications:
- operator: GreaterThan
threshold: 80
contactEmails: [budgetContactEmail] # parameterized
thresholdType: Forecasted
- operator: GreaterThan
threshold: 100
contactEmails: [budgetContactEmail]
thresholdType: Forecasted
actionGroupId: actionGroup.outputs.resourceId # escalation
- operator: GreaterThan
threshold: 120
contactEmails: [budgetContactEmail]
thresholdType: Forecasted
actionGroupId: actionGroup.outputs.resourceId # escalation
- operator: GreaterThan
threshold: 90
contactEmails: [budgetContactEmail]
thresholdType: Actual
anomalyDetection:
note: |
Azure Cost Anomaly Alerts are a SEPARATE Cost Management capability,
not a property of Microsoft.Consumption/budgets. Implement via
Microsoft.CostManagement/scheduledActions (subscription scope) or
configure in Azure Portal > Cost Management > Cost alerts > Anomaly alerts.
The Code Generator should create a separate cost-anomaly-alert module or
document as a post-deployment manual step.
scope: subscription
contactEmails: [budgetContactEmail, technicalContact]
tags: all 11 tags
resource: main.bicepparam + main.dev.bicepparam
type: configuration
dependencies: [all modules]
config:
main.bicepparam:\n environment: 'prod'\n location: swedencentral\n tags: prod values for all 11 tags (9 policy-enforced keys:\n environment, owner, costcenter, application, workload,\n sla, backup-policy, maint-window, technical-contact\n + ManagedBy, Project)\n sqlAdminGroupObjectId: <to-be-provided>\n sqlAdminGroupName: <to-be-provided>\n budgetAmount: 800 # EUR, prod RG-scoped\n budgetContactEmail: <parameterized>\n technicalContact: <from technical-contact tag>\n main.dev.bicepparam:\n environment: 'dev'\n enablePrivateEndpoints: false\n appServicePlanSku: { name: B1, capacity: 1 }\n sqlDatabaseSku: { name: Basic, tier: Basic }\n budgetAmount: 200 # EUR, dev RG-scoped\n tags: dev values for all 11 tags", "oldString": " main.bicepparam:\n environment: 'prod'\n location: swedencentral\n tags: prod values for all 11 tags\n sqlAdminGroupObjectId: <to-be-provided>\n sqlAdminGroupName: <to-be-provided>\n budgetContactEmail: cto@nordicfreshfoods.eu\n main.dev.bicepparam:\n environment: 'dev'\n enablePrivateEndpoints: false\n appServicePlanSku: { name: B1, capacity: 1 }\n sqlDatabaseSku: { name: Basic, tier: Basic }\n tags: dev values for all 11 tags
resource: PowerShell deployment script
type: tooling
dependencies: [all modules]
config:
- Phase-aware deployment with az deployment group create
- What-If preview before each phase
- Approval gates between phases
- Error handling and rollback guidance
- MFA prerequisite check
- Activity Log diagnostic routing (subscription-scoped, Phase 2):
az monitor diagnostic-settings subscription create
--name "activity-to-law"
--workspace logAnalyticsWorkspaceId
--logs '[{"category":"Administrative","enabled":true},{"category":"Security","enabled":true},{"category":"Policy","enabled":true}]'
- SQL contained user bootstrap (Phase 5 post-deploy):
Prod: AzureCLI deploymentScript resource in snet-app (VNet-injected)
Dev: Direct sqlcmd from deployer workstation
rollbackMatrix:
phase1:
trigger: VNet/NSG deployment failure
action: Delete resource group and retry
checkpoint: RG created with 9 tags
phase2:
trigger: Log Analytics/AppInsights failure
action: Re-run Phase 2 deployment (idempotent)
checkpoint: workspace-id available
phase3:
trigger: Key Vault or DNS zone failure
action: Re-run Phase 3 (idempotent); verify DNS links
checkpoint: KV accessible, DNS zones linked
phase4:
trigger: SQL/Storage/PE failure
action: Delete failed PE resources, re-run Phase 4
checkpoint: PE connectivity verified, public access disabled
phase5:
trigger: App Service or role assignment failure
action: Re-run Phase 5; verify MI principal before role assignments
checkpoint: Health endpoint responds 200
reEntrySemantics:
- All modules use incremental deployment mode (safe to re-run)
- State preserved between phases via resource group
- deploy.ps1 accepts -StartFromPhase parameter for re-entry

OrderResourceModuleDependencies
1.1Resource Groupaz group create (CLI)None
1.2Virtual Network + Subnets + NSGsnetwork.bicepResource Group

Gate: Verify RG exists with all 9 tags; VNet has correct subnets.

OrderResourceModuleDependencies
2.1Log Analytics Workspacemonitoring.bicepPhase 1
2.2Application Insightsmonitoring.bicepPhase 1

Gate: Verify Log Analytics workspace ID available for downstream modules.

Phase 3: Security + DNS (Estimated: 3 min)

Section titled “Phase 3: Security + DNS (Estimated: 3 min)”
OrderResourceModuleDependencies
3.1Key Vaultkeyvault.bicepPhase 1 (VNet)
3.2Private DNS Zone (SQL)dns.bicepPhase 1 (VNet)
3.3Private DNS Zone (Blob)dns.bicepPhase 1 (VNet)
3.4Private DNS Zone (KV)dns.bicepPhase 1 (VNet)
3.5Private Endpoint (KV)keyvault.bicepPhase 1 (VNet), 3.4 (DNS)

Gate: Verify Key Vault accessible via PE; DNS zones linked to VNet.

OrderResourceModuleDependencies
4.1Azure SQL Server + Databasesql.bicepPhase 2 (monitoring), Phase 3 (DNS)
4.2Private Endpoint (SQL)sql.bicepPhase 1 (VNet), Phase 3 (DNS)
4.3Storage Accountstorage.bicepPhase 2 (monitoring), Phase 3 (DNS)
4.4Private Endpoint (Storage)storage.bicepPhase 1 (VNet), Phase 3 (DNS)

Gate: Verify SQL + Storage accessible via private endpoints; public access disabled.

Phase 5: Compute + Budget (Estimated: 4 min)

Section titled “Phase 5: Compute + Budget (Estimated: 4 min)”
OrderResourceModuleDependencies
5.1App Service Plancompute.bicepPhase 1 (VNet)
5.2App Servicecompute.bicepPhase 2 (App Insights), Phase 3 (KV), Phase 4 (SQL, Storage)
5.3Role Assignmentscompute.bicepPhase 5.2 (MI principal ID)
5.4Autoscale Settingscompute.bicepPhase 5.1 (ASP)
5.5Budget Alertbudget.bicepResource Group

Gate: Verify App Service health endpoint responds; MI can access KV and Storage; SQL contained user created for App Service MI.

Total Estimated Deployment Time: ~17 minutes (excluding approval gates)

Section titled “Total Estimated Deployment Time: ~17 minutes (excluding approval gates)”

Dependency Diagram

Source: Dependency diagram Excalidraw source

Resource Group
├── Virtual Network (+ Subnets, NSGs)
│ ├── Private DNS Zone (SQL) ─── VNet Link
│ ├── Private DNS Zone (Blob) ── VNet Link
│ ├── Key Vault
│ ├── SQL Server + Database
│ │ └── Private Endpoint (SQL) ── DNS Zone Group
│ ├── Storage Account
│ │ └── Private Endpoint (Blob) ── DNS Zone Group
│ └── App Service Plan
│ └── App Service (VNet Integration)
│ ├── → Key Vault (RBAC: Secrets User)
│ ├── → SQL Database (contained user)
│ └── → Storage (RBAC: Blob Data Contributor)
├── Log Analytics Workspace
│ └── Application Insights
└── Budget Alert

The editable source for this diagram is the Excalidraw file linked above.


Runtime Flow Diagram

Source: Runtime flow Excalidraw source

User Request
App Service (S1, VNet-integrated)
├──→ Application Insights (telemetry)
├──→ Key Vault (secrets, via MI)
├──[Private Endpoint]──→ Azure SQL (orders, users, inventory)
├──[Private Endpoint]──→ Storage Account (product images)
├──[Outbound REST]──→ Payment Gateway (external)
├──[Outbound REST]──→ Maps/Routing API (external)
└──[Outbound REST]──→ Email/SMS Provider (external)
Log Analytics ◄── Diagnostic Settings (all resources)
Budget Alert ──→ Email notification (CTO)

The editable source for this diagram is the Excalidraw file linked above.


| Resource | Pattern | Example (Prod) | Example (Dev) | Max Length | | ----------------------- | ------------------------------ | ------------------------------------ | ----------------------------------- | ---------- | --- | ---------- | ---------------- | --------------- | --- | --- | --- | -------- | -------------- | ------------- | --- | --- | ----------------- | ---------- | ---------------- | --------------- | --- | --- | --- | -------- | -------------- | ------------- | --- | --- | | Resource Group | rg-{project}-{env} | rg-nordic-fresh-foods-prod | rg-nordic-fresh-foods-dev | 90 | | Virtual Network | vnet-{project}-{env} | vnet-nordic-fresh-foods-prod | vnet-nordic-fresh-foods-dev | 64 | | Subnet (App) | snet-app | snet-app | snet-app | 80 | | Subnet (Data) | snet-data | snet-data | — | 80 | | Subnet (PE) | snet-pe | snet-pe | — | 80 | | NSG (App) | nsg-app-{env} | nsg-app-prod | nsg-app-dev | 80 | \n | NSG (Data) | nsg-data-{env} | nsg-data-prod | — | 80 | \n | NSG (PE) | nsg-pe-{env} | nsg-pe-prod | — | 80 | ”, “oldString”: ” | NSG (Data) | nsg-data-{env} | nsg-data-prod | — | 80 | \n | NSG (PE) | nsg-pe-{env} | nsg-pe-prod | — | 80 | | Log Analytics | log-{project}-{env} | log-nordic-fresh-foods-prod | log-nordic-fresh-foods-dev | 63 | | App Insights | appi-{project}-{env} | appi-nordic-fresh-foods-prod | appi-nordic-fresh-foods-dev | 255 | | Key Vault | kv-{short}-{env}-{suffix} | kv-nff-prod-a1b2c3 | kv-nff-dev-a1b2c3 | 24 | | SQL Server | sql-{project}-{env} | sql-nordic-fresh-foods-prod | sql-nordic-fresh-foods-dev | 63 | | SQL Database | sqldb-{app}-{env} | sqldb-freshconnect-prod | sqldb-freshconnect-dev | 128 | | Storage Account | st{short}{env}{suffix} | stnffproda1b2c3 | stnffdeva1b2c3 | 24 | | App Service Plan | asp-{project}-{env} | asp-nordic-fresh-foods-prod | asp-nordic-fresh-foods-dev | 60 | | App Service | app-{project}-{env}-{suffix} | app-nordic-fresh-foods-prod-a1b2c3 | app-nordic-fresh-foods-dev-a1b2c3 | 60 | | Private Endpoint (SQL) | pe-sql-{project}-{env} | pe-sql-nordic-fresh-foods-prod | — | 80 | | Private Endpoint (Blob) | pe-st-{project}-{env} | pe-st-nordic-fresh-foods-prod | — | 80 | | Private Endpoint (KV) | pe-kv-{project}-{env} | pe-kv-nordic-fresh-foods-prod | — | 80 | | Budget (Aggregate) | budget-{project} | budget-nordic-fresh-foods | — | 63 | | Budget (Prod) | budget-{project}-{env} | budget-nff-prod | — | 63 | | Budget (Dev) | budget-{project}-{env} | budget-nff-dev | — | 63 |

Unique suffix: uniqueString(resourceGroup().id) — first 6 characters used for length-constrained resources (KV, Storage, App Service).


ServiceIdentity TypeRole Assignments
App ServiceSystem-assigned MIKey Vault Secrets User, Storage Blob Data Contributor
SQL ServerAzure AD-only authEntra admin group as server admin
Key VaultN/A (RBAC)App Service MI → Secrets User
StorageN/A (RBAC)App Service MI → Blob Data Contributor
ServicePublic Access (Prod)Public Access (Dev)Private Endpoint
Azure SQLDisabledEnabledYes (prod)
Storage AccountDisabledEnabledYes (prod)
Key VaultDisabled (bypass: AzureServices¹)EnabledYes (prod, via PE + privatelink.vaultcore.azure.net)

¹ Key Vault bypass: AzureServices: Allows Azure trusted first-party services (e.g., Azure Backup, Azure Resource Manager) to access the vault without traversing the PE. App Service accesses Key Vault via PE + Key Vault References. The trusted-services bypass is intentionally retained for platform operations (backup, policy evaluation) and does not weaken the PCI segmentation boundary because no CHD is stored in Key Vault. | App Service | Public (web-facing) | Public | N/A (VNet integration outbound) |

NSG Coverage: All subnets (snet-app, snet-data, snet-pe) have dedicated NSGs.

[!NOTE] Dev environment uses a single subnet with no private endpoints or NSG isolation. Dev is a non-PCI/non-production-data environment with synthetic test data only. This deviation is an accepted exception from the prod compliance boundary.

Disaster Recovery — RTO/RPO Execution Table

Section titled “Disaster Recovery — RTO/RPO Execution Table”
ResourceRTO TargetRPO TargetBackup SourceRestore MethodValidation
App Service + Plan24hN/A (stateless)Bicep templatesRedeploy to germanywestcentralHealth endpoint 200
Azure SQL Database24h12hGeo-replicated backup (default)az sql db restore --timeQuery row count
Storage Account (LRS)24h12hAzCopy scheduled export (daily)Restore from export blobFile count/size check
Key Vault24hN/A (soft-delete 90d)Built-in soft-delete/recoveraz keyvault recoverSecret access test
VNet + DNS + PEs24hN/A (IaC-defined)Bicep templatesRedeploy to failover regionPE connectivity test
App Insights / Log Analytics24hN/A (non-critical)Workspace data exportRedeploy workspace + restoreQuery validation

Failover region: germanywestcentral. Storage LRS does not provide regional redundancy; daily AzCopy export to a GRS backup account is the compensating control for the 12h RPO.

SettingValueApplies To
minTlsVersion'TLS1_2'All services
httpsOnlytrueApp Service
supportsHttpsTrafficOnlytrueStorage Account
ftpsStateFtpsOnlyApp Service
allowBlobPublicAccessfalseStorage Account
allowSharedKeyAccessfalseStorage Account
ResourceCategory / LogsMetricsOwner ModuleNotes
Azure SQL Server + DBSQLSecurityAuditEvents, allLogsAllMetricssql.bicepVia AVM diagnosticSettings
Storage AccountStorageRead, StorageWrite, StorageDeleteTransaction, Capacitystorage.bicepVia AVM diagnosticSettings
App ServiceAppServiceHTTPLogs, AppServiceConsoleLogs, AppServiceAppLogsAllMetricscompute.bicepVia AVM diagnosticSettings
Key VaultAuditEventAllMetricskeyvault.bicepRequired by MCAPSGov Audit
VNetVMProtectionAlertsAllMetricsnetwork.bicepNetwork flow analysis
NSG (all 3)NetworkSecurityGroupEvent, NetworkSecurityGroupRuleCounternetwork.bicepNSG flow logs
Private EndpointsAllMetricssql.bicep, storage.bicep, keyvault.bicepMetric-only (PE has no log categories)
Activity LogAdministrative, Security, Policysubscription-levelRoute via az monitor diagnostic-settings subscription create in deploy.ps1

All diagnostic settings route to log-nordic-fresh-foods-{env} (Log Analytics workspace). Activity Log routing is subscription-scoped and configured in the deployment script, not in module Bicep.

SettingValue
azureADOnlyAuthenticationtrue (Policy: Deny)
minimalTlsVersion'1.2'
Threat DetectionEnabled via Defender for SQL (Microsoft.Sql/servers/securityAlertPolicies)
Defender for SQLstate: Enabled, emailAccountAdmins: true — satisfies MCAPSGov Audit
AuditingEnabled via diagnostic settings (SQLSecurityAuditEvents)
Geo-backupEnabled (default)
PITR Retention30 days

PhaseTaskEstimate
Module development8 Bicep modules + main.bicep2–3 hours
Parameter filesProd + Dev .bicepparam30 min
Deployment scriptdeploy.ps1 (phased)30 min
TestingWhat-If + lint + build30 min
Deployment (prod)5 phases with gates~17 min
Deployment (dev)Single phase (no PEs)~8 min
Total~4–5 hours

[!IMPORTANT] Implementation Plan Review

MetricValue
Resources19 types (18 AVM + 1 raw Bicep)
AVM Coverage95% (18/19)
Governance Blockers2 resolved, 1 documented
Deployment Phases5 (Foundation → Observability → Security → Data → Compute)
Budget compliance€204/month of €1,000 budget (20%)
  • Approved — proceed to Bicep Code Generation (Step 5)
  • Approver:
  • Date:

DocumentLink
Azure Verified ModulesAVM Registry
CAF Naming ConventionsMicrosoft Learn
Private Endpoint ReferenceMicrosoft Learn
Bicep Best PracticesMicrosoft Learn
Governance ConstraintsGovernance Constraints