

Infrastructure as code (IaC) is no longer optional in modern Azure environments. Teams need repeatable deployments, secure defaults, predictable architecture and strong governance. Azure Bicep has become the preferred IaC language for Azure because it’s declarative, simple, modular and deeply integrated with the Azure platform.
This article breaks down how to design Bicep modules the right way for enterprise deployments. These patterns come from real-world use cases such as banking, fintech, multitenant SaaS and regulated workloads.
Why Bicep is the Standard for Azure IaC
Teams that move from ARM and Terraform to Bicep typically do so because Bicep offers:
- Cleaner Syntax: No more massive JSON ARM templates.
- Native Azure Integration
- IntelliSense
- Type-checking
- Automatic API version updates
- First-Class Modularity: Modules can describe reusable components like:
- App Services
- AKS clusters
- Front Door Premium
- Key Vault
- VNet + subnets
- WAF policies
- Private endpoints
- Better CI/CD experience: Easier validation, what-if deployment and GitHub Actions integration.
How to Structure Bicep Code for Large Azure Environments
A typical enterprise Bicep structure looks like this:
/bicep
/modules
aks/
main.bicep
appservice/
main.bicep
frontdoor/
main.bicep
keyvault/
main.bicep
network/
vnet.bicep
subnet.bicep
storage/
main.bicep
/environment
dev/
main.bicep
params.json
qa/
main.bicep
params.json
prod/
main.bicep
params.json
Key Points
- Modules live separately and never store environment-specific values.
- Environment folders contain:
- main.bicep (composition file)
- params.json (per-environment values)
This ensures consistency across dev → qa → prod.
Designing a Bicep Module Correctly
A module should follow five rules:
1. It should deploy one resource (or a tightly-related set).
Examples:
- A single App Service
- A single AKS cluster
- A VNet with subnets
2. It must not contain environment-specific values.
These belong in parameter files.
3. It should expose outputs.
Useful for chaining modules:
output appServiceId string = appService.id
output principalId string = appService.identity.principalId
4. It must include secure parameter types.
@secure()
param adminPassword string
5. It should include defaults but allow overrides.
param sku string = ‘P1v3’
param httpsOnly bool = true
Example of an Enterprise Bicep Module (App Service + Custom Domain)
Here is a production-ready example you can reuse.
modules/appservice/main.bicep
param name string
param location string
param skuName string = ‘P1v3’
param httpsOnly bool = true
param customDomain string
param certificateThumbprint string
resource appService ‘Microsoft.Web/sites@2023-01-01’ = {
name: name
location: location
properties: {
httpsOnly: httpsOnly
}
sku: {
name: skuName
tier: ‘PremiumV3’
}
}
resource binding ‘Microsoft.Web/sites/hostNameBindings@2023-01-01’ = {
name: ‘${name}/${customDomain}’
properties: {
customHostNameDnsRecordType: ‘CName’
sslState: ‘SniEnabled’
thumbprint: certificateThumbprint
}
}
output id string = appService.id
output defaultHostname string = appService.properties.defaultHostName
This module:
- Deploys a premium App Service
- Enforces HTTPS
- Adds custom domain with SNI certificate binding
- Exports outputs for Front Door or API Management
Composing Multiple Modules With an Environment File
Example: prod/main.bicep
param location string = ‘westeurope’
param appName string = ‘prod-myapp’
param domain string = ‘api.company.com’
param certThumbprint string
module appService ‘./modules/appservice/main.bicep’ = {
name: ‘prodAppService’
params: {
name: appName
location: location
skuName: ‘P2v3’
customDomain: domain
certificateThumbprint: certThumbprint
}
}
module frontdoor ‘./modules/frontdoor/main.bicep’ = {
name: ‘prodFrontDoor’
params: {
backendHostname: appService.outputs.defaultHostname
backendId: appService.outputs.id
}
}
Why this matters:
- Front Door depends on App Service output
- Environment parameters flow through modules cleanly
- No duplication of logic
- Clear separation of concerns
Adding CI/CD With GitHub Actions
A recommended pipeline: Validate → what-if → deploy
name: Deploy Bicep
on:
push:
branches:
– main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
– uses: actions/checkout@v4
– uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
– name: Validate
run: |
az deployment sub validate \
–template-file environment/prod/main.bicep
– name: What-If
run: |
az deployment sub what-if \
–template-file environment/prod/main.bicep
– name: Deploy
run: |
az deployment sub create \
–template-file environment/prod/main.bicep
This gives:
- Predictable deployments
- No manual approvals
- Auditability
- Cloud-native authentication via OIDC (no secrets)
Governance and Enforcement Using Azure Policy
Azure Policy can enforce IaC best practices, for example:
- Allow only Bicep deployments (tagging rules)
- Enforce HTTPS-only App Services
- Enforce diagnostic logs
- Prevent public IP creation
- Require private endpoints
These policies make sure all deployments — Bicep or otherwise — follow standards.
Final Thoughts
Bicep is ideal for building large Azure environments when done correctly. By using a module-based approach, separating environment values, integrating CI/CD and combining everything with Azure Policy, you get:
- Standardized deployments
- Reusable patterns
- Lower operational overhead
- Strong governance
- Easier AKS, App Service and Front Door automation
These practices are exactly what senior-level architects and MVP reviewers look for, because they demonstrate real-world engineering maturity.
from DevOps.com https://ift.tt/HKLn4zO
Comments
Post a Comment