Home > Docs > IaC & CI/CD Best Practices
Infrastructure-as-Code & CI/CD Best Practices for CSA-in-a-Box¶
Note
Quick Summary: Comprehensive guide for deploying a Cloud-Scale Analytics platform across 4 Azure subscriptions using Bicep and GitHub Actions — covers Bicep module organization, .bicepparam files, Deployment Stacks, what-if validation, OIDC auth, reusable workflows, matrix deployments, PSRule/Checkov/gitleaks security scanning, ALZ accelerators (AVM migration), progressive ring-based deployments, feature flags, policy-as-code, and a phased implementation plan with tool matrix.
Research Report | April 2026 Comprehensive guide for deploying a Cloud-Scale Analytics platform across 4 Azure subscriptions using Bicep and GitHub Actions.
Important
This document describes the target-state architecture and recommended improvements. For current CI/CD workflows, see .github/workflows/. For current deployment instructions, see QUICKSTART.md or GETTING_STARTED.md.
Note
CAF scenario update (CSA-0068). The legacy "Cloud-Scale Analytics" CAF scenario was deprecated in April 2026 and replaced by Microsoft CAF — Unify your data platform. Links in this document to https://learn.microsoft.com/azure/cloud-adoption-framework/scenarios/cloud-scale-analytics/... are preserved for historical cross-reference only. For authoritative 2026 guidance on Fabric, data mesh, and landing zones, follow the "Unify your data platform" link above.
📑 Table of Contents¶
- 🏗️ 1. Bicep Best Practices for Large-Scale Azure Deployments
- 1.1 Module Organization and Naming Conventions
- 1.2 Parameter Files per Environment
- 1.3 Bicep Module Registry (ACR)
- 1.4 Deployment Stacks vs Standard Deployments
- 1.5 What-If Deployments and Validation
- 1.6 Cross-Subscription and Cross-Resource-Group Deployments
- 1.7 Conditional Deployment Patterns
- 1.8 User-Defined Types and Compile-Time Imports
- 🔄 2. GitHub Actions CI/CD for Azure IaC
- 2.1 Workflow Organization for Multi-Subscription Deploys
- 2.2 OIDC Authentication (Federated Credentials)
- 2.3 Environment Protection Rules and Approvals
- 2.4 Reusable Workflows and Composite Actions
- 2.5 Secret Management with GitHub Environments
- 2.6 Bicep Lint, Validate, What-If in PR Checks
- 2.7 Deployment Gates and Rollback Strategies
- 2.8 Matrix Deployments for Multiple Subscriptions
- 🧪 3. Testing Infrastructure-as-Code
- 🏛️ 4. Azure Landing Zone Accelerators
- 🌍 5. Multi-Environment Deployment Patterns
- 🔒 6. Secret Scanning and Security in CI/CD
- 📋 7. Recommended Implementation Plan for CSA-in-a-Box
- 📎 Appendix A: Complete bicepconfig.json
- 🔑 Appendix B: GitHub Secrets Required
- 📚 Appendix C: Key Microsoft Documentation References
🏗️ 1. Bicep Best Practices for Large-Scale Azure Deployments¶
1.1 Module Organization and Naming Conventions¶
For a 4-subscription deployment (Management, Connectivity, DMLZ, DLZ), structure Bicep modules by domain and layer:
deploy/bicep/
_shared/ # Shared types, functions, variables
types.bicep # User-defined types (exported)
naming.bicep # Naming convention functions
tags.bicep # Standard tag definitions
modules/ # Reusable resource modules
networking/
virtualNetwork.bicep
privateEndpoint.bicep
privateDnsZone.bicep
vnetPeering.bicep
security/
keyVault.bicep
managedIdentity.bicep
storage/
storageAccount.bicep
dataLakeStorage.bicep
compute/
synapseWorkspace.bicep
databricksWorkspace.bicep
governance/
policyDefinition.bicep
policyAssignment.bicep
roleAssignment.bicep
monitoring/
logAnalyticsWorkspace.bicep
diagnosticSettings.bicep
orchestration/ # Composition modules per subscription
alz/
main.bicep # Landing Zone orchestrator
params.dev.bicepparam
params.test.bicepparam
params.prod.bicepparam
dmlz/
main.bicep # Data Management Landing Zone orchestrator
params.dev.bicepparam
params.test.bicepparam
params.prod.bicepparam
dlz/
main.bicep # Data Landing Zone orchestrator
params.dev.bicepparam
params.test.bicepparam
params.prod.bicepparam
connectivity/
main.bicep # Connectivity orchestrator
params.dev.bicepparam
params.test.bicepparam
params.prod.bicepparam
Naming conventions:
- Use lowerCamelCase for symbolic names:
synapseWorkspace,dataLakeStorage - Never use
namein symbolic names (e.g., usestorageAccountnotstorageAccountName) - Prefix resource names with shortcodes:
st(storage),kv(key vault),vnet(virtual network) - Use template expressions for resource names with
uniqueString():
param shortAppName string = 'csa'
param environment string = 'dev'
param storageAccountName string = '${shortAppName}${environment}${uniqueString(resourceGroup().id)}'
1.2 Parameter Files per Environment¶
Use .bicepparam files (native Bicep parameter files) instead of JSON parameter files. They support type safety and compile-time validation:
// params.dev.bicepparam
using '../main.bicep'
param environment = 'dev'
param location = 'eastus'
param networkConfig = {
vnetAddressPrefix: '10.0.0.0/16'
subnets: [
{ name: 'snet-data', addressPrefix: '10.0.1.0/24' }
{ name: 'snet-compute', addressPrefix: '10.0.2.0/24' }
]
}
param tags = {
Environment: 'Development'
Project: 'CSA-in-a-Box'
CostCenter: '12345'
}
// params.prod.bicepparam
using '../main.bicep'
param environment = 'prod'
param location = 'eastus'
param networkConfig = {
vnetAddressPrefix: '10.100.0.0/16'
subnets: [
{ name: 'snet-data', addressPrefix: '10.100.1.0/24' }
{ name: 'snet-compute', addressPrefix: '10.100.2.0/24' }
]
}
param tags = {
Environment: 'Production'
Project: 'CSA-in-a-Box'
CostCenter: '67890'
}
1.3 Bicep Module Registry (Azure Container Registry)¶
Publish reusable modules to an Azure Container Registry (ACR) for cross-team sharing:
Setup:
# Create registry
az acr create --name csaIacModules --resource-group rg-shared --sku Standard
# Publish a module
az bicep publish \
--file modules/networking/virtualNetwork.bicep \
--target br:csaiacmodules.azurecr.io/bicep/modules/networking/vnet:v1.0.0 \
--with-source
# Consume in Bicep
# module vnet 'br:csaiacmodules.azurecr.io/bicep/modules/networking/vnet:v1.0.0' = { ... }
Configure aliases in bicepconfig.json:
{
"moduleAliases": {
"br": {
"csaModules": {
"registry": "csaiacmodules.azurecr.io",
"modulePath": "bicep/modules"
}
}
}
}
Then consume modules with short aliases:
Versioning strategy:
- Use semantic versioning:
v1.0.0,v1.1.0,v2.0.0 - Publish with
--with-sourceflag so consumers can F12 into the source in VS Code - Publish modules in CI after merge to main; tag with git SHA + semver
1.4 Deployment Stacks vs Standard Deployments¶
Recommendation: Use Deployment Stacks for CSA-in-a-Box.
Deployment Stacks are a superset of standard deployments that add lifecycle management:
| Feature | Standard Deployment | Deployment Stack |
|---|---|---|
| Deploy resources | Yes | Yes |
| Track managed resources | No | Yes |
| Prevent unauthorized changes (deny settings) | No | Yes (DenyDelete, DenyWriteAndDelete) |
| Auto-cleanup on removal | No | Yes (deleteAll, deleteResources, detachAll) |
| Cross-scope deployment | Yes | Yes |
When to use Deployment Stacks:
- Managing complete environments (dev/test/prod)
- Protecting production resources from accidental deletion
- Cleaning up test environments after sprints
- Auditing which resources belong to which deployment
Example for CSA-in-a-Box:
# Create a subscription-scoped deployment stack for DMLZ
az stack sub create \
--name 'csa-dmlz-dev' \
--location 'eastus' \
--template-file 'deploy/bicep/orchestration/dmlz/main.bicep' \
--parameters 'deploy/bicep/orchestration/dmlz/params.dev.bicepparam' \
--deployment-resource-group 'rg-dmlz-dev' \
--action-on-unmanage 'deleteResources' \
--deny-settings-mode 'denyDelete' \
--deny-settings-excluded-principals '<deployment-sp-object-id>'
Warning
Known limitations (as of early 2026): - Maximum 800 deployment stacks per scope - Maximum 2,000 deny assignments per scope - What-if is not yet supported with deployment stacks - Microsoft Graph provider is not supported - Deleting resource groups can bypass deny assignments
1.5 What-If Deployments and Validation¶
What-if provides a preview of changes before deployment. It's critical for PR review workflows.
Validation levels (az CLI 2.76+):
Provider(default): Full validation including RBAC permission checksProviderNoRbac: Full validation, read-only permission checksTemplate: Static syntax/structure validation only (no Azure calls needed)
CI/CD integration pattern:
# In PR pipeline: run what-if and capture output
az deployment sub what-if \
--location eastus \
--template-file deploy/bicep/orchestration/dmlz/main.bicep \
--parameters deploy/bicep/orchestration/dmlz/params.dev.bicepparam \
--no-pretty-print > whatif-output.json
# Post what-if results as a PR comment
bicep snapshot command (new): Performs local-only comparison by generating a normalized JSON representation. Useful for catching logic changes without Azure connectivity:
1.6 Cross-Subscription and Cross-Resource-Group Deployments¶
For a 4-subscription architecture, use scoped modules:
targetScope = 'subscription'
// Parameters for cross-subscription targeting
param dmlzSubscriptionId string
param dlzSubscriptionId string
param connectivitySubscriptionId string
// Deploy to Data Management LZ subscription
module dmlzResources 'modules/dmlz.bicep' = {
name: 'deploy-dmlz'
scope: subscription(dmlzSubscriptionId)
}
// Deploy to Data Landing Zone subscription
module dlzResources 'modules/dlz.bicep' = {
name: 'deploy-dlz'
scope: subscription(dlzSubscriptionId)
}
// Deploy to a specific resource group in another subscription
module networkResources 'modules/networking.bicep' = {
name: 'deploy-networking'
scope: resourceGroup(connectivitySubscriptionId, 'rg-connectivity')
}
Important
The deploying identity needs appropriate RBAC on all target subscriptions. For management group scoped deployments, use targetScope = 'managementGroup' and deploy to child subscriptions.
1.7 Conditional Deployment Patterns¶
// Feature flags for infrastructure
param deployDataFactory bool = true
param deploySynapse bool = true
param deployDatabricks bool = false
param isProduction bool = false
// Conditional resource deployment
module dataFactory 'modules/compute/dataFactory.bicep' = if (deployDataFactory) {
name: 'deploy-adf'
params: {
name: 'adf-${environment}-${uniqueString(resourceGroup().id)}'
// Production gets managed VNet
managedVirtualNetworkEnabled: isProduction
}
}
// Conditional SKU selection
var synapseSkuName = isProduction ? 'DW1000c' : 'DW100c'
// Environment-based conditional logic
var diagnosticsRetentionDays = isProduction ? 365 : 30
1.8 User-Defined Types and Compile-Time Imports¶
User-defined types provide strong typing and validation. Combined with @export() and import, they enable shared type libraries:
Shared types library (_shared/types.bicep)
// _shared/types.bicep
@export()
type environmentType = 'dev' | 'test' | 'prod'
@export()
type networkConfigType = {
vnetName: string
addressPrefix: string
subnets: subnetConfigType[]
}
@export()
type subnetConfigType = {
name: string
addressPrefix: string
serviceEndpoints: string[]?
delegations: string[]?
privateEndpointNetworkPolicies: ('Enabled' | 'Disabled')?
}
@export()
@sealed()
type dataLakeConfigType = {
@description('Storage account name (3-24 chars, lowercase)')
@minLength(3)
@maxLength(24)
name: string
sku: 'Standard_LRS' | 'Standard_GRS' | 'Standard_ZRS'
containers: string[]
enableHierarchicalNamespace: bool
}
@export()
type tagType = {
Environment: string
Project: string
CostCenter: string
Owner: string?
ManagedBy: ('Bicep' | 'Terraform' | 'Manual')?
}
Consuming shared types:
// orchestration/dmlz/main.bicep
import { environmentType, networkConfigType, tagType } from '../../_shared/types.bicep'
param environment environmentType
param networkConfig networkConfigType
param tags tagType
Resource-derived types (Bicep CLI 0.34.1+):
// Derive types directly from Azure resource schemas
type storageAccountSku = resourceInput<'Microsoft.Storage/storageAccounts@2024-01-01'>.sku
type accountKind = resourceInput<'Microsoft.Storage/storageAccounts@2024-01-01'>.kind
🔄 2. GitHub Actions CI/CD for Azure IaC¶
2.1 Workflow Organization for Multi-Subscription Deploys¶
Recommended structure using reusable workflows:
.github/
workflows/
# Trigger workflows
ci.yml # PR validation
cd-dev.yml # Deploy to dev (on merge)
cd-prod.yml # Deploy to prod (manual)
# Reusable workflows
_validate.yml # Bicep lint + build + what-if
_deploy-subscription.yml # Deploy to a single subscription
_post-deploy-checks.yml # Smoke tests
_security-scan.yml # Checkov + gitleaks
actions/
# Composite actions
bicep-validate/action.yml # Lint + build
azure-what-if/action.yml # What-if with PR comment
post-deploy-verify/action.yml # Verification scripts
2.2 OIDC Authentication (Federated Credentials)¶
Always use OIDC over service principal secrets. OIDC eliminates long-lived credentials.
Setup for 4 subscriptions:
# 1. Create a Microsoft Entra App Registration
az ad app create --display-name "csa-inabox-github-actions"
# 2. Create federated credentials for each environment + branch
# For main branch deployments
az ad app federated-credential create \
--id <app-object-id> \
--parameters '{
"name": "github-main",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:<org>/csa-inabox:ref:refs/heads/main",
"audiences": ["api://AzureADTokenExchange"]
}'
# For PR validation (environment-scoped)
az ad app federated-credential create \
--id <app-object-id> \
--parameters '{
"name": "github-pr",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:<org>/csa-inabox:pull_request",
"audiences": ["api://AzureADTokenExchange"]
}'
# For environment-scoped credentials (production approvals)
az ad app federated-credential create \
--id <app-object-id> \
--parameters '{
"name": "github-prod-env",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:<org>/csa-inabox:environment:prod",
"audiences": ["api://AzureADTokenExchange"]
}'
# 3. Grant RBAC on each subscription
for SUB_ID in $MGMT_SUB $CONN_SUB $DMLZ_SUB $DLZ_SUB; do
az role assignment create \
--role "Contributor" \
--assignee <app-client-id> \
--scope "/subscriptions/$SUB_ID"
done
GitHub workflow usage:
permissions:
id-token: write # Required for OIDC
contents: read
steps:
- name: Azure Login (OIDC)
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_MGMT_SUBSCRIPTION_ID }}
2.3 Environment Protection Rules and Approvals¶
Configure GitHub Environments with protection rules:
| Environment | Protection Rules | Secrets |
|---|---|---|
dev | None (auto-deploy on merge) | Dev subscription IDs |
test | Wait timer (5 min) | Test subscription IDs |
prod | Required reviewers (2+), deployment branches (main only) | Prod subscription IDs |
Configuration in workflow:
jobs:
deploy-prod:
environment:
name: prod
url: https://portal.azure.com/#@tenant/resource/subscriptions/${{ secrets.AZURE_DLZ_SUBSCRIPTION_ID }}
runs-on: ubuntu-latest
steps:
# This job won't run until reviewers approve
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
# Environment-specific secrets
subscription-id: ${{ secrets.AZURE_DLZ_SUBSCRIPTION_ID }}
2.4 Reusable Workflows and Composite Actions¶
Reusable workflow for deploying to any subscription
# .github/workflows/_deploy-subscription.yml
name: Deploy to Subscription (Reusable)
on:
workflow_call:
inputs:
environment:
required: true
type: string
template_file:
required: true
type: string
parameters_file:
required: true
type: string
subscription_name:
required: true
type: string
description: "MGMT, CONN, DMLZ, or DLZ"
location:
required: false
type: string
default: "eastus"
dry_run:
required: false
type: boolean
default: false
secrets:
AZURE_CLIENT_ID:
required: true
AZURE_TENANT_ID:
required: true
AZURE_SUBSCRIPTION_ID:
required: true
outputs:
deployment_name:
description: "Name of the deployment"
value: ${{ jobs.deploy.outputs.deployment_name }}
jobs:
deploy:
name: Deploy ${{ inputs.subscription_name }}
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
outputs:
deployment_name: ${{ steps.deploy.outputs.name }}
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Azure Login (OIDC)
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Install Bicep
run: az bicep install
- name: What-If Preview
if: ${{ inputs.dry_run }}
run: |
az deployment sub what-if \
--location ${{ inputs.location }} \
--template-file ${{ inputs.template_file }} \
--parameters ${{ inputs.parameters_file }}
- name: Deploy
id: deploy
if: ${{ !inputs.dry_run }}
run: |
DEPLOYMENT_NAME="csa-${{ inputs.subscription_name }}-$(date +%Y%m%d-%H%M%S)"
az deployment sub create \
--name "$DEPLOYMENT_NAME" \
--location ${{ inputs.location }} \
--template-file ${{ inputs.template_file }} \
--parameters ${{ inputs.parameters_file }}
echo "name=$DEPLOYMENT_NAME" >> $GITHUB_OUTPUT
Caller workflow example
# .github/workflows/cd-dev.yml
name: Deploy to Dev
on:
push:
branches: [main]
paths: ["deploy/**"]
jobs:
deploy-dmlz:
uses: ./.github/workflows/_deploy-subscription.yml
with:
environment: dev
template_file: deploy/bicep/orchestration/dmlz/main.bicep
parameters_file: deploy/bicep/orchestration/dmlz/params.dev.bicepparam
subscription_name: DMLZ
secrets:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_DMLZ_SUBSCRIPTION_ID }}
deploy-dlz:
needs: deploy-dmlz
uses: ./.github/workflows/_deploy-subscription.yml
with:
environment: dev
template_file: deploy/bicep/orchestration/dlz/main.bicep
parameters_file: deploy/bicep/orchestration/dlz/params.dev.bicepparam
subscription_name: DLZ
secrets:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_DLZ_SUBSCRIPTION_ID }}
Composite action for Bicep validation:
# .github/actions/bicep-validate/action.yml
name: Bicep Validate
description: Lint, build, and optionally what-if a Bicep file
inputs:
template_file:
required: true
description: Path to the Bicep file
parameters_file:
required: false
description: Path to the parameters file
runs:
using: composite
steps:
- name: Install Bicep CLI
shell: bash
run: az bicep install
- name: Lint
shell: bash
run: az bicep lint --file ${{ inputs.template_file }}
- name: Build (compile check)
shell: bash
run: az bicep build --file ${{ inputs.template_file }}
2.5 Secret Management with GitHub Environments¶
GitHub Environments Configuration:
dev:
Secrets:
AZURE_CLIENT_ID # Same across environments (single app registration)
AZURE_TENANT_ID # Same across environments
AZURE_MGMT_SUBSCRIPTION_ID # Dev management subscription
AZURE_CONN_SUBSCRIPTION_ID # Dev connectivity subscription
AZURE_DMLZ_SUBSCRIPTION_ID # Dev DMLZ subscription
AZURE_DLZ_SUBSCRIPTION_ID # Dev DLZ subscription
Variables:
AZURE_REGION: eastus
RESOURCE_PREFIX: csa-dev
prod:
Protection Rules:
- Required reviewers: [lead-engineer, platform-team]
- Deployment branches: [main]
- Wait timer: 15 minutes
Secrets:
AZURE_CLIENT_ID
AZURE_TENANT_ID
AZURE_MGMT_SUBSCRIPTION_ID # Prod management subscription
AZURE_CONN_SUBSCRIPTION_ID # Prod connectivity subscription
AZURE_DMLZ_SUBSCRIPTION_ID # Prod DMLZ subscription
AZURE_DLZ_SUBSCRIPTION_ID # Prod DLZ subscription
Variables:
AZURE_REGION: eastus
RESOURCE_PREFIX: csa-prod
2.6 Bicep Lint, Validate, What-If in PR Checks¶
Complete PR validation workflow
# .github/workflows/ci.yml
name: PR Validation
on:
pull_request:
branches: [main]
paths: ["deploy/**/*.bicep", "deploy/**/*.bicepparam"]
permissions:
id-token: write
contents: read
pull-requests: write
jobs:
lint-and-build:
name: Lint & Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Bicep
run: az bicep install
- name: Lint all Bicep files
run: |
ERRORS=0
find deploy/ -name "*.bicep" | while read -r file; do
echo "::group::Linting $file"
if ! az bicep lint --file "$file" 2>&1; then
ERRORS=$((ERRORS + 1))
fi
echo "::endgroup::"
done
exit $ERRORS
- name: Build all orchestration main files
run: |
for main in deploy/bicep/orchestration/*/main.bicep; do
echo "::group::Building $main"
az bicep build --file "$main"
echo "::endgroup::"
done
what-if:
name: What-If Preview
needs: lint-and-build
runs-on: ubuntu-latest
strategy:
matrix:
zone: [alz, dmlz, dlz, connectivity]
steps:
- uses: actions/checkout@v4
- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_MGMT_SUBSCRIPTION_ID }}
- name: What-If ${{ matrix.zone }}
id: whatif
run: |
TEMPLATE="deploy/bicep/orchestration/${{ matrix.zone }}/main.bicep"
PARAMS="deploy/bicep/orchestration/${{ matrix.zone }}/params.dev.bicepparam"
if [ -f "$TEMPLATE" ] && [ -f "$PARAMS" ]; then
OUTPUT=$(az deployment sub what-if \
--location eastus \
--template-file "$TEMPLATE" \
--parameters "$PARAMS" \
--no-pretty-print 2>&1) || true
echo "output<<EOF" >> $GITHUB_OUTPUT
echo "$OUTPUT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
fi
- name: Comment PR with What-If
if: steps.whatif.outputs.output
uses: actions/github-script@v7
with:
script: |
const body = `### What-If Results: \`${{ matrix.zone }}\`\n\`\`\`\n${{ steps.whatif.outputs.output }}\n\`\`\``;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body
});
security-scan:
name: Security Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Checkov
uses: bridgecrewio/checkov-action@v12
with:
directory: deploy/
framework: bicep,arm
soft_fail: true
output_format: sarif
output_file_path: results.sarif
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
2.7 Deployment Gates and Rollback Strategies¶
Rollback approach for IaC:
Infrastructure doesn't "roll back" like application code. Instead:
- Redeploy previous known-good version: Keep deployment artifacts (Bicep files + params) in git; redeploy the commit before the broken change.
- Deployment Stacks with detach: If using deployment stacks, unmanaged resources can be detached rather than deleted.
- Git-based rollback workflow:
Rollback workflow
# .github/workflows/rollback.yml
name: Rollback Infrastructure
on:
workflow_dispatch:
inputs:
target_commit:
description: "Git commit SHA to roll back to"
required: true
zone:
description: "Zone to rollback"
type: choice
options: [alz, dmlz, dlz, connectivity]
environment:
type: choice
options: [dev, test, prod]
jobs:
rollback:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.target_commit }}
- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Redeploy from previous commit
run: |
az deployment sub create \
--location eastus \
--template-file "deploy/bicep/orchestration/${{ inputs.zone }}/main.bicep" \
--parameters "deploy/bicep/orchestration/${{ inputs.zone }}/params.${{ inputs.environment }}.bicepparam"
2.8 Matrix Deployments for Multiple Subscriptions¶
jobs:
deploy:
strategy:
max-parallel: 1 # Sequential for dependency ordering
matrix:
include:
- zone: alz
subscription_secret: AZURE_MGMT_SUBSCRIPTION_ID
order: 1
- zone: connectivity
subscription_secret: AZURE_CONN_SUBSCRIPTION_ID
order: 2
- zone: dmlz
subscription_secret: AZURE_DMLZ_SUBSCRIPTION_ID
order: 3
- zone: dlz
subscription_secret: AZURE_DLZ_SUBSCRIPTION_ID
order: 4
environment: ${{ inputs.environment }}
runs-on: ubuntu-latest
steps:
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets[matrix.subscription_secret] }}
- name: Deploy ${{ matrix.zone }}
run: |
az deployment sub create \
--location eastus \
--template-file "deploy/bicep/orchestration/${{ matrix.zone }}/main.bicep" \
--parameters "deploy/bicep/orchestration/${{ matrix.zone }}/params.${{ inputs.environment }}.bicepparam"
Note
For true ordered sequential deployment with dependencies, use separate jobs with needs: rather than a matrix. Matrices run jobs in parallel (or max-parallel: 1 but without dependency outputs between runs).
🧪 3. Testing Infrastructure-as-Code¶
3.1 Bicep Linting with Linter Rules¶
The Bicep linter is integrated into both the CLI and VS Code extension. Configure it via bicepconfig.json:
{
"analyzers": {
"core": {
"rules": {
"no-hardcoded-env-urls": { "level": "error" },
"no-hardcoded-location": { "level": "warning" },
"no-unused-params": { "level": "error" },
"no-unused-vars": { "level": "error" },
"outputs-should-not-contain-secrets": { "level": "error" },
"secure-parameter-default": { "level": "error" },
"use-recent-api-versions": { "level": "warning" },
"adminusername-should-not-be-literal": { "level": "error" },
"no-unnecessary-dependson": { "level": "warning" },
"simplify-interpolation": { "level": "warning" },
"use-resource-symbol-reference": { "level": "warning" },
"use-safe-access": { "level": "warning" },
"use-secure-value-for-secure-inputs": { "level": "error" },
"use-stable-resource-identifiers": { "level": "warning" },
"no-deployments-resources": { "level": "warning" },
"explicit-values-for-loc-params": { "level": "warning" }
}
}
}
}
Key rules for data platform deployments:
| Rule | Level | Why |
|---|---|---|
outputs-should-not-contain-secrets | Error | Prevents leaking keys/connection strings in outputs |
use-secure-value-for-secure-inputs | Error | Ensures secure params for passwords/keys |
no-hardcoded-env-urls | Error | Ensures cloud-agnostic templates |
no-hardcoded-location | Warning | Enforces parameterized locations |
use-recent-api-versions | Warning | Catches stale API versions |
CI integration:
# Lint with error exit code
az bicep lint --file main.bicep
# Build (implicitly runs linter)
az bicep build --file main.bicep
3.2 PSRule for Azure (Policy Compliance Testing)¶
PSRule for Azure provides 500+ pre-built rules that test IaC against Azure Well-Architected Framework pillars.
Installation and configuration:
# ps-rule.yaml (root of repo)
configuration:
AZURE_BICEP_FILE_EXPANSION: true
AZURE_BICEP_FILE_EXPANSION_TIMEOUT: 10
AZURE_PARAMETER_FILE_EXPANSION: true
input:
pathIgnore:
- ".github/"
- "codeqlDB/"
- "tools/"
- "scripts/"
- "node_modules/"
rule:
include:
- Azure.*
output:
culture: ["en-US"]
binding:
targetName:
- ResourceName
- name
targetType:
- ResourceType
- type
GitHub Actions integration:
# In CI workflow
- name: Run PSRule for Azure
uses: microsoft/ps-rule@v2
with:
inputPath: deploy/
modules: PSRule.Rules.Azure
baseline: Azure.Default
outputFormat: Sarif
outputPath: psrule-results.sarif
- name: Upload PSRule Results
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: psrule-results.sarif
Recommended baselines for data platforms:
Azure.Default: General best practicesAzure.GA_2024_06/Azure.GA_2025_*: GA service baselineAzure.MCSB.v1: Microsoft Cloud Security Benchmark rules
3.3 Checkov (IaC Security Scanning)¶
Checkov supports both Bicep and ARM templates with 750+ built-in policies.
# GitHub Actions integration
- name: Run Checkov
uses: bridgecrewio/checkov-action@v12
with:
directory: deploy/
framework: bicep,arm
check: >-
CKV_AZURE_35,CKV_AZURE_33,CKV_AZURE_17,
CKV_AZURE_14,CKV_AZURE_15
soft_fail: true
output_format: cli,sarif
output_file_path: console,results.sarif
download_external_modules: true
Key Checkov checks for data platforms:
| Check ID | Description |
|---|---|
| CKV_AZURE_33 | Ensure storage account uses private endpoint |
| CKV_AZURE_35 | Ensure storage account access is restricted |
| CKV_AZURE_36 | Ensure storage account uses customer managed key |
| CKV_AZURE_59 | Ensure Synapse workspace is not accessible publicly |
| CKV_AZURE_110 | Ensure Key Vault enables purge protection |
| CKV_AZURE_190 | Ensure Databricks workspace uses private endpoint |
Custom policy example
# custom_checks/data_lake_security.py
from checkov.common.models.enums import CheckResult, CheckCategories
from checkov.arm.base_resource_check import BaseResourceCheck
class DataLakeEncryption(BaseResourceCheck):
def __init__(self):
name = "Ensure Data Lake Storage uses encryption"
id = "CKV_CSA_001"
supported_resources = ['Microsoft.Storage/storageAccounts']
categories = [CheckCategories.ENCRYPTION]
super().__init__(name=name, id=id, categories=categories,
supported_resources=supported_resources)
def scan_resource_conf(self, conf):
if conf.get('properties', {}).get('encryption', {}).get('services', {}).get('blob', {}).get('enabled'):
return CheckResult.PASSED
return CheckResult.FAILED
3.4 Additional Security Scanning Tools¶
| Tool | What It Scans | Bicep/ARM Support | GitHub Actions |
|---|---|---|---|
| Checkov | IaC misconfigurations | Native Bicep + ARM | bridgecrewio/checkov-action@v12 |
| Trivy | IaC, containers, secrets | ARM JSON (compile Bicep first) | aquasecurity/trivy-action@master |
| tfsec | IaC misconfigurations | ARM JSON | Deprecated (use Trivy) |
| PSRule | Azure compliance/WAF | Native Bicep | microsoft/ps-rule@v2 |
| KICS | IaC misconfigurations | ARM JSON | checkmarx/kics-github-action@v2 |
Recommended scanning stack for CSA-in-a-Box:
- Bicep Linter (built-in) - Syntax and best practices
- PSRule for Azure - Azure Well-Architected compliance
- Checkov - Security misconfigurations
- gitleaks - Secret detection (covered in section 6)
3.5 Cost Estimation in CI Pipelines¶
# Cost estimation using Infracost
- name: Setup Infracost
uses: infracost/actions/setup@v3
with:
api-key: ${{ secrets.INFRACOST_API_KEY }}
# Note: Infracost has limited Bicep support.
# Alternative: Use Azure Pricing Calculator API or
# az deployment what-if with cost annotations
- name: Estimate costs from What-If
run: |
# Custom script to parse what-if output and estimate costs
# using Azure Retail Prices API
python scripts/estimate-costs.py \
--whatif-output whatif-output.json \
--region eastus
🏛️ 4. Azure Landing Zone Accelerators¶
4.1 ALZ-Bicep vs Azure Verified Modules (AVM)¶
Warning
Critical update: ALZ-Bicep is being deprecated.
| Aspect | ALZ-Bicep | Azure Verified Modules (AVM) |
|---|---|---|
| Status | Deprecated Feb 2026, archived Feb 2027 | Active, recommended |
| Modules | Standalone Bicep modules | Module registry on MCR |
| Community | Microsoft maintained | Microsoft + community |
| Consumption | Git clone + customize | br/public:avm/res/<provider>/<resource>:<version> |
| Testing | Manual | Built-in CI/CD tests |
Recommendation: Use AVM modules as the foundation, supplement with custom modules for data-platform-specific resources.
AVM module consumption:
// Consume AVM modules from Microsoft Container Registry
module vnet 'br/public:avm/res/network/virtual-network:0.5.0' = {
name: 'deploy-vnet'
params: {
name: 'vnet-csa-dev'
addressPrefixes: ['10.0.0.0/16']
subnets: [
{ name: 'snet-data', addressPrefix: '10.0.1.0/24' }
]
}
}
module keyVault 'br/public:avm/res/key-vault/vault:0.8.0' = {
name: 'deploy-kv'
params: {
name: 'kv-csa-dev'
enablePurgeProtection: true
enableSoftDelete: true
}
}
4.2 Extending ALZ for Data Platforms¶
The Cloud-Scale Analytics architecture maps to these landing zones:
Management Group Hierarchy:
Tenant Root
+-- csa-platform (Management Group)
| +-- csa-management (Subscription) # Log Analytics, Sentinel, Defender
| +-- csa-connectivity (Subscription) # Hub VNet, Firewall, DNS
|
+-- csa-landing-zones (Management Group)
+-- csa-data-management (Management Group)
| +-- csa-dmlz (Subscription) # Purview, shared data services
|
+-- csa-data-landing-zones (Management Group)
+-- csa-dlz-001 (Subscription) # Synapse, Data Lake, ADF
+-- csa-dlz-002 (Subscription) # Additional DLZ (scalable)
Reference deployment templates (from Microsoft):
| Template | Purpose | Per-deployment |
|---|---|---|
| Data Management Zone | Purview, shared integration runtime, catalog | One per analytics estate |
| Data Landing Zone | Storage, ingestion, compute services | One per domain/team |
| Data Product - Batch | Batch processing services | One or more per DLZ |
| Data Product - Streaming | Stream processing services | One or more per DLZ |
| Data Product - Analytics | Analytics and AI services | One or more per DLZ |
4.3 Custom Policy Definitions for Data Governance¶
Custom policy Bicep examples
// Custom policy: Require private endpoints on Synapse workspaces
resource synapsePrivateEndpointPolicy 'Microsoft.Authorization/policyDefinitions@2025-03-01' = {
name: 'csa-require-synapse-private-endpoint'
properties: {
policyType: 'Custom'
displayName: 'Require private endpoints for Synapse workspaces'
description: 'Synapse workspaces must use private endpoints for connectivity'
mode: 'Indexed'
metadata: {
category: 'Synapse'
version: '1.0.0'
}
policyRule: {
if: {
allOf: [
{
field: 'type'
equals: 'Microsoft.Synapse/workspaces'
}
{
field: 'Microsoft.Synapse/workspaces/publicNetworkAccess'
notEquals: 'Disabled'
}
]
}
then: {
effect: 'deny'
}
}
}
}
// Policy initiative for data governance
resource dataGovernanceInitiative 'Microsoft.Authorization/policySetDefinitions@2025-03-01' = {
name: 'csa-data-governance'
properties: {
policyType: 'Custom'
displayName: 'CSA Data Governance Initiative'
policyDefinitions: [
{ policyDefinitionId: synapsePrivateEndpointPolicy.id }
// Add: require encryption, require diagnostics, require tagging, etc.
]
}
}
Recommended built-in policies for data platforms:
Require secure transfer for storage accountsAzure Synapse workspaces should use private linkAzure Data Factory should use private linkDiagnostic logs in Azure Data Lake Store should be enabledAzure Databricks workspaces should be in a virtual networkStorage accounts should restrict network access
🌍 5. Multi-Environment Deployment Patterns¶
5.1 Recommended: Progressive Deployment (Ring-Based)¶
For data platforms, use a ring-based deployment pattern:
graph LR
PR["PR Created"] --> Lint["Lint + Build + Security Scan"]
Lint --> WhatIf["What-If Preview"]
WhatIf --> Merge["Merge to main"]
Merge --> R0["Ring 0: Dev<br/>(Automatic)"]
R0 --> R1["Ring 1: Test<br/>(5-min wait)"]
R1 --> R2["Ring 2: Prod<br/>(Manual approval<br/>2+ reviewers)"] # cd-progressive.yml
name: Progressive Deployment
on:
push:
branches: [main]
paths: ["deploy/**"]
jobs:
# Ring 0: Dev (automatic)
deploy-dev:
uses: ./.github/workflows/_deploy-all-zones.yml
with:
environment: dev
secrets: inherit
# Ring 1: Test (after dev succeeds)
deploy-test:
needs: deploy-dev
uses: ./.github/workflows/_deploy-all-zones.yml
with:
environment: test
secrets: inherit
# Ring 2: Prod (manual gate)
deploy-prod:
needs: deploy-test
uses: ./.github/workflows/_deploy-all-zones.yml
with:
environment: prod # Has required reviewers configured
secrets: inherit
5.2 Feature Flags for Infrastructure¶
Use Bicep parameters as feature flags:
// Feature flags - centralized in parameters file
param features = {
enableDataFactory: true
enableSynapse: true
enableDatabricks: false // Not yet ready for this env
enablePurview: true
enableStreamAnalytics: false // Phase 2 feature
enableCosmosDb: false // Phase 3 feature
enableDiagnosticLogs: true
enablePrivateEndpoints: true // Always true in prod
}
// Conditional deployment based on flags
module dataFactory 'modules/compute/dataFactory.bicep' = if (features.enableDataFactory) {
name: 'deploy-adf'
params: { ... }
}
module synapse 'modules/compute/synapse.bicep' = if (features.enableSynapse) {
name: 'deploy-synapse'
params: { ... }
}
5.3 Blue-Green for Infrastructure¶
True blue-green is complex for stateful infrastructure. For data platforms, use a slot-based approach for stateless components and in-place updates for stateful ones:
| Component Type | Strategy | Examples |
|---|---|---|
| Stateless (blue-green capable) | Deploy alongside, swap routing | Synapse SQL pools, Spark pools, ADF IRs, Load balancers |
| Stateful (in-place update only) | Incremental update | Data Lake, Cosmos DB, Purview catalog, Key Vault, Private DNS |
Pattern: Deploy new stateless infrastructure alongside existing, test, then swap DNS/routing:
// Blue-green for Synapse SQL pool
param activeSlot string = 'blue' // or 'green'
var slots = {
blue: { name: 'sqlpool-blue', sku: 'DW1000c' }
green: { name: 'sqlpool-green', sku: 'DW1000c' }
}
module activePool 'modules/synapseSqlPool.bicep' = {
name: 'deploy-active-pool'
params: {
name: slots[activeSlot].name
sku: slots[activeSlot].sku
state: 'Active'
}
}
5.4 Canary Deployments for Data Pipelines¶
For data pipelines, canary means routing a percentage of data through the new pipeline:
- Deploy new pipeline version alongside existing (v1 + v2)
- Route 10% of incoming data to v2 pipeline
- Compare output quality metrics (data quality, latency, completeness)
- If metrics pass: increase to 50%, then 100%
- If metrics fail: route 100% back to v1, investigate
This is implemented at the ADF/Synapse pipeline level rather than at the IaC level.
🔒 6. Secret Scanning and Security in CI/CD¶
6.1 Gitleaks Integration¶
# .github/workflows/ci.yml (add to existing)
secret-scan:
name: Secret Detection
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for scanning
- name: Run gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Custom gitleaks configuration (.gitleaks.toml)
title = "CSA-in-a-Box gitleaks config"
[extend]
useDefault = true
[[rules]]
id = "azure-storage-key"
description = "Azure Storage Account Key"
regex = '''(?i)(?:DefaultEndpointsProtocol|AccountKey|SharedAccessSignature)\s*=\s*[^\s;]+'''
tags = ["azure", "storage", "key"]
[[rules]]
id = "azure-connection-string"
description = "Azure Connection String"
regex = '''(?i)(?:Server|Data Source)=[^;]+;(?:Initial Catalog|Database)=[^;]+;'''
tags = ["azure", "connection-string"]
[allowlist]
paths = [
'''\.gitleaks\.toml$''',
'''docs/.*\.md$''',
'''.*_test\.go$''',
'''.*\.bicep$''', # Bicep files use param references, not actual secrets
]
6.2 GitHub Advanced Security¶
If available on your plan, enable:
- Secret scanning: Automatically detects Azure keys, SAS tokens, connection strings
- Secret scanning push protection: Blocks pushes containing detected secrets
- Code scanning: SARIF upload from Checkov/PSRule
- Dependabot: Monitors action versions and dependencies
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
groups:
azure-actions:
patterns:
- "azure/*"
6.3 Pre-Commit Hooks for Secret Detection¶
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: detect-private-key
- id: check-added-large-files
args: ["--maxkb=500"]
- id: no-commit-to-branch
args: ["--branch", "main"]
- repo: local
hooks:
- id: bicep-lint
name: Bicep Lint
entry: bash -c 'for f in "$@"; do az bicep lint --file "$f"; done'
language: system
types_or: [file]
files: '\.bicep$'
Installation:
6.4 Policy-as-Code with Azure Policy¶
Azure Policy enforces governance at the Azure control plane level. Combine with OPA/Gatekeeper for Kubernetes workloads if applicable:
Policy-as-Code Bicep examples
// Deploy policies as part of the ALZ deployment
targetScope = 'managementGroup'
// Enforce data residency
resource dataResidencyPolicy 'Microsoft.Authorization/policyAssignments@2025-03-01' = {
name: 'csa-data-residency'
properties: {
displayName: 'Restrict data platform resources to approved regions'
policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/e56962a6-4747-49cd-b67b-bf8b01975c4c'
parameters: {
listOfAllowedLocations: {
value: ['eastus', 'eastus2']
}
}
enforcementMode: 'Default'
}
}
// Enforce encryption at rest
resource encryptionPolicy 'Microsoft.Authorization/policyAssignments@2025-03-01' = {
name: 'csa-require-encryption'
properties: {
displayName: 'Require encryption for storage accounts'
policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/b2982f36-99f2-4db5-8eff-283140c09693'
enforcementMode: 'Default'
}
}
📋 7. Recommended Implementation Plan for CSA-in-a-Box¶
7.1 Current State Assessment¶
Based on the existing repository structure, the project already has:
- A
validate.ymlworkflow with Bicep lint, gitleaks, Checkov, and PowerShell lint - A
deploy.ymlworkflow with OIDC auth, what-if support, and per-zone deployment - Bicep modules organized by zone (ALZ, DMLZ, DLZ) with modules subdirectories
- Per-environment parameter files (referenced but may need
.bicepparammigration)
7.2 Recommended Improvements (Priority Order)¶
Phase 1: Foundation (Week 1-2)¶
- Migrate to
.bicepparamfiles from JSON parameter files - Create shared types library (
_shared/types.bicep) with exported types - Add
bicepconfig.jsonwith strict linter rules (see section 3.1) - Add PSRule for Azure to the validation pipeline
- Add
.pre-commit-config.yamlwith gitleaks + bicep-lint hooks
Phase 2: CI/CD Enhancement (Week 3-4)¶
- Refactor workflows to use reusable workflows (
_deploy-subscription.yml) - Configure GitHub Environments with protection rules for test/prod
- Add what-if PR comments using the matrix strategy (section 2.6)
- Set up OIDC federated credentials per environment
- Add Dependabot for GitHub Actions version monitoring
Phase 3: Module Registry & Governance (Week 5-6)¶
- Set up Azure Container Registry for Bicep module publishing
- Publish shared modules to ACR with semantic versioning
- Adopt AVM modules where available (networking, key vault, storage)
- Deploy custom Azure Policy initiative for data governance
- Consider Deployment Stacks for environment lifecycle management
Phase 4: Advanced Patterns (Week 7-8)¶
- Implement progressive (ring-based) deployment across environments
- Add cost estimation to PR checks
- Create rollback workflow for emergency scenarios
- Add post-deployment smoke tests for each zone
- Document architecture decisions in ADRs
7.3 Tool Matrix Summary¶
| Category | Tool | Priority | Status in Repo |
|---|---|---|---|
| Linting | Bicep Linter | P0 | Exists (enhance config) |
| Compliance | PSRule for Azure | P0 | Not yet |
| Security | Checkov | P0 | Exists |
| Secrets | gitleaks | P0 | Exists |
| Secrets | Pre-commit hooks | P1 | Not yet |
| Auth | OIDC (Workload Identity) | P0 | Exists |
| Deployment | What-if in PRs | P1 | Partial |
| Deployment | Deployment Stacks | P2 | Not yet |
| Deployment | Reusable Workflows | P1 | Not yet |
| Registry | ACR for Bicep modules | P2 | Not yet |
| Modules | Azure Verified Modules | P2 | Not yet |
| Policy | Azure Policy-as-Code | P1 | Partial (in ALZ) |
| Monitoring | Dependabot | P1 | Not yet |
| Cost | Cost estimation | P3 | Not yet |
7.4 CI/CD Pipeline Architecture (Target State)¶
graph TD
PR["PR Created/Updated"] --> Lint["Bicep Lint<br/>(all files)"]
PR --> Build["Bicep Build<br/>(compile check)"]
PR --> PSRule["PSRule for Azure<br/>(compliance)"]
PR --> Checkov["Checkov<br/>(security scan)"]
PR --> Gitleaks["gitleaks<br/>(secret detection)"]
PR --> WhatIf["What-If Preview<br/>(per zone, PR comment)"]
Lint & Build & PSRule & Checkov & Gitleaks & WhatIf --> Merge["PR Approved & Merged"]
Merge --> R0["Ring 0: Deploy to Dev<br/>(automatic)"]
R0 --> R0Seq["ALZ → Connectivity → DMLZ → DLZ"]
R0Seq --> Smoke0["Post-deploy smoke tests"]
Smoke0 --> R1["Ring 1: Deploy to Test<br/>(automatic, 5-min wait)"]
R1 --> R1Seq["Same sequence"]
R1Seq --> IntTest["Integration tests"]
IntTest --> R2["Ring 2: Deploy to Prod<br/>(manual approval)"]
R2 --> R2Seq["Same sequence"]
R2Seq --> ProdVerify["Production verification"]
ProdVerify --> Notify["Notify team"] 📎 Appendix A: Complete bicepconfig.json¶
Full bicepconfig.json
{
"analyzers": {
"core": {
"verbose": false,
"enabled": true,
"rules": {
"no-hardcoded-env-urls": { "level": "error" },
"no-hardcoded-location": { "level": "warning" },
"no-unused-params": { "level": "error" },
"no-unused-vars": { "level": "error" },
"no-unused-existing-resources": { "level": "warning" },
"outputs-should-not-contain-secrets": { "level": "error" },
"secure-parameter-default": { "level": "error" },
"secure-params-in-nested-deploy": { "level": "error" },
"secure-secrets-in-params": { "level": "error" },
"use-secure-value-for-secure-inputs": { "level": "error" },
"use-recent-api-versions": { "level": "warning" },
"adminusername-should-not-be-literal": { "level": "error" },
"no-unnecessary-dependson": { "level": "warning" },
"simplify-interpolation": { "level": "warning" },
"prefer-interpolation": { "level": "warning" },
"use-resource-symbol-reference": { "level": "warning" },
"use-safe-access": { "level": "warning" },
"use-stable-resource-identifiers": { "level": "warning" },
"no-deployments-resources": { "level": "warning" },
"explicit-values-for-loc-params": { "level": "warning" },
"use-parent-property": { "level": "warning" },
"no-conflicting-metadata": { "level": "warning" }
}
}
},
"moduleAliases": {
"br": {
"csaModules": {
"registry": "csaiacmodules.azurecr.io",
"modulePath": "bicep/modules"
},
"public": {
"registry": "mcr.microsoft.com",
"modulePath": "bicep"
}
}
},
"experimentalFeaturesEnabled": {
"userDefinedTypes": true,
"compileTimeImports": true
}
}
🔑 Appendix B: GitHub Secrets Required¶
Repository-level secrets:
GITLEAKS_LICENSE # For gitleaks-action
Environment: dev
AZURE_CLIENT_ID # App registration client ID
AZURE_TENANT_ID # Entra tenant ID
AZURE_MGMT_SUBSCRIPTION_ID # Management subscription
AZURE_CONN_SUBSCRIPTION_ID # Connectivity subscription
AZURE_DMLZ_SUBSCRIPTION_ID # DMLZ subscription
AZURE_DLZ_SUBSCRIPTION_ID # DLZ subscription
Environment: test
(Same structure, different subscription IDs)
Environment: prod
(Same structure, different subscription IDs)
Protection rules: 2 required reviewers, main branch only
📚 Appendix C: Key Microsoft Documentation References¶
- Bicep Best Practices
- Deployment Stacks
- User-Defined Types
- Compile-Time Imports
- Private Module Registry
- Deploy to Subscription Scope
- What-If Deployments
- Bicep Linter
- GitHub OIDC with Azure
- Microsoft CAF — Unify your data platform (2026 guidance) — replaces the deprecated Cloud-Scale Analytics scenario (April 2026)
- Cloud-Scale Analytics Architectures (deprecated, kept for historical cross-reference)
- PSRule for Azure
- GitHub Reusable Workflows
See also:
- ⌂ Index: Documentation home