Bicep Best Practices in 2025: Writing Production-Ready Infrastructure Code

Bicep has matured significantly since its introduction, and with that maturity comes evolved best practices. Whether you’re new to Bicep or looking to improve your existing templates, these recommendations will help you write cleaner, more maintainable infrastructure code.

Modern Syntax Improvements

Use User-Defined Types Instead of Open Types

Avoid generic object and array types. Instead, define precise types:

// ❌ Avoid
param networkConfig object

// ✅ Prefer
type networkConfigType = {
  vnetName: string
  addressPrefix: string
  subnets: subnetType[]
}

type subnetType = {
  name: string
  addressPrefix: string
  @description('Optional NSG resource ID')
  nsgId: string?
}

param networkConfig networkConfigType

Leverage Resource-Derived Types

When passing resource properties, use built-in resource types:

// ✅ Use resource-derived types for accurate typing
output storageAccountProperties resourceOutput<'Microsoft.Storage/storageAccounts@2023-01-01'> = storageAccount

Safe Dereference for Nullable Properties

Use the safe-dereference operator instead of verbose null checks:

// ❌ Verbose
var subnetId = virtualNetwork != null ? virtualNetwork.properties.subnets[0].id : ''

// ✅ Clean
var subnetId = virtualNetwork.?properties.?subnets[0].?id ?? ''

Resource Declaration Best Practices

Use Parent Property for Child Resources

Never construct child resource names with string concatenation:

// ❌ Avoid
resource subnet 'Microsoft.Network/virtualNetworks/subnets@2023-09-01' = {
  name: '${vnetName}/mysubnet'
  // ...
}

// ✅ Prefer
resource vnet 'Microsoft.Network/virtualNetworks@2023-09-01' existing = {
  name: vnetName
}

resource subnet 'Microsoft.Network/virtualNetworks/subnets@2023-09-01' = {
  parent: vnet
  name: 'mysubnet'
  // ...
}

Use Symbolic References Over Functions

Replace resourceId() and reference() with symbolic names:

// ❌ Avoid
output keyVaultUri string = reference(resourceId('Microsoft.KeyVault/vaults', kvName)).vaultUri

// ✅ Prefer
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
  name: kvName
}

output keyVaultUri string = keyVault.properties.vaultUri

Security Considerations

Always Secure Sensitive Parameters

@secure()
@description('The administrator password for the SQL Server')
param sqlAdminPassword string

@secure()
@description('API key for external service integration')
param apiKey string

Use Managed Identities

Avoid storing credentials when possible:

resource functionApp 'Microsoft.Web/sites@2023-01-01' = {
  name: functionAppName
  location: location
  identity: {
    type: 'SystemAssigned'
  }
  // ...
}

Module Usage Patterns

Pin Module Versions

Always specify exact versions for reproducibility:

// ✅ Pinned version
module storage 'br/public:avm/res/storage/storage-account:0.31.0' = {
  // ...
}

// ❌ Avoid unpinned or latest

Module Naming is Optional

The name property for modules is no longer required:

// ✅ Modern syntax - name is optional
module keyVault 'br/public:avm/res/key-vault/vault:0.13.3' = {
  params: {
    name: 'kv-${uniqueString(resourceGroup().id)}'
    location: location
  }
}

Parameters Files

Use Bicepparam Over JSON

Bicep parameters files (.bicepparam) offer better tooling support:

// main.bicepparam
using './main.bicep'

param environment = 'production'
param location = 'uksouth'
param tags = {
  Environment: 'Production'
  ManagedBy: 'Bicep'
}

Code Organisation

Structure for Maintainability

infrastructure/
├── main.bicep              # Entry point
├── main.bicepparam         # Parameters
├── modules/
│   ├── networking.bicep    # Network resources
│   ├── compute.bicep       # Compute resources
│   └── security.bicep      # Security resources
└── types/
    └── common.bicep        # Shared type definitions

Export Reusable Types

// types/common.bicep
@export()
type tagType = {
  Environment: string
  CostCentre: string?
  Owner: string
}

Linting and Validation

Address Diagnostic Codes

Pay attention to BCP warnings:

  • BCP036/BCP037 - May indicate incorrect property names
  • BCP081 - Suggests using newer API versions
  • BCP035 - Type mismatches requiring correction

Use Bicep Linter Configuration

// bicepconfig.json
{
  "analyzers": {
    "core": {
      "rules": {
        "no-unused-params": { "level": "warning" },
        "prefer-interpolation": { "level": "warning" },
        "secure-secrets-in-params": { "level": "error" }
      }
    }
  }
}

Conclusion

Following these best practices will result in Bicep code that is easier to maintain, more secure, and better aligned with the latest language capabilities. The investment in writing clean infrastructure code pays dividends as your Azure estate grows.

At Tech Design Concept, we help organisations establish Infrastructure as Code standards and implement robust deployment pipelines. Contact us to discuss how we can improve your IaC practices.


Want hands-on help with your Bicep templates? We offer code reviews and training sessions tailored to your team’s needs.