Skip to content
Back to skills

Bicep Conventions

ASecurity

Best practices for Azure Bicep Infrastructure as Code covering naming conventions (lowerCamelCase for identifiers, descriptive prefixes for resource names), parameter design with decorators and safe defaults, resource references using symbolic names and the existing keyword, import/export for shared types and user-defined functions, modularization patterns with versioned modules and ACR publishing, loadJsonContent for external configuration, .bicepparam parameter files, subscription-scoped de...

  • 3 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 11, 2026
ai-agentsgosqlexpressazurerefactoringapici/cdsecuritydocumentation

Works with

  • api

Security analysis

A100/100

Scanned September 11, 2026

npx -y skills add atc-net/atc-agentic-toolkit --skill bicep-conventions --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Bicep Conventions?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Bicep Conventions
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/atc-net-bicep-conventions/badge)](https://www.skillsdirectory.com/skills/atc-net-bicep-conventions)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: bicep-conventions
description: 'Best practices for Azure Bicep Infrastructure as Code covering naming conventions (lowerCamelCase for identifiers, descriptive prefixes for resource names), parameter design with decorators and safe defaults, resource references using symbolic names and the existing keyword, import/export for shared types and user-defined functions, modularization patterns with versioned modules and ACR publishing, loadJsonContent for external configuration, .bicepparam parameter files, subscription-scoped deployments, @batchSize for loop control, security practices (@secure, Key Vault references, no secrets in outputs), and documentation standards. Apply this skill whenever writing, reviewing, modifying, or refactoring .bicep files -- including when the user asks about Bicep parameter design, resource naming, module structure, deployment templates, or Azure IaC patterns, even if they do not explicitly mention "Bicep conventions."'
user-invocable: false
---

# Azure Bicep Conventions and Best Practices

Follow these conventions when writing, reviewing, or refactoring Bicep templates to produce secure, maintainable, and idiomatic Infrastructure as Code.

## File Structure

Organize every Bicep file in a consistent top-to-bottom order so readers always know where to look:

1. `import` statements (for shared types and functions from other files)
2. `targetScope` (if not the default `resourceGroup`)
3. `metadata` (optional -- file-level description)
4. `type` definitions (user-defined types)
5. `func` declarations (user-defined functions)
6. `param` declarations
7. `var` declarations
8. `resource` and `module` statements
9. `output` declarations

```bicep
import { environment as env, resourceName } from '../modules/naming.1.0.bicep'

targetScope = 'subscription'

metadata description = 'Deploys the core networking stack for the production environment.'

// Types and functions
@export()
type environment = 'DEV' | 'UAT' | 'PROD'

// Parameters
@description('The Azure region for all resources.')
param location string

// Variables
var vnetName = resourceName(environment, 'network', 'vnet')

// Resources
resource vnet 'Microsoft.Network/virtualNetworks@2024-01-01' = { ... }

// Outputs
output vnetId string = vnet.id
```

## Naming Conventions

### Symbolic Names

Symbolic names are identifiers used only within the Bicep file -- they never appear in Azure. They should be optimized for readability.

- Use **lowerCamelCase** for all symbolic names: parameters, variables, resources, modules, and outputs.
- Choose descriptive names that convey purpose: `storageAccount` rather than `sa`, `appServicePlan` rather than `asp`.
- Do not include the word `name` in a symbolic name that represents the resource itself -- `storageAccount` is the resource, `storageAccountName` is a string holding its Azure name.

```bicep
// Good
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = { ... }
param deploymentEnvironment string

// Avoid
resource sa 'Microsoft.Storage/storageAccounts@2023-05-01' = { ... }
param env string
```

### Azure Resource Names

Azure resource names (the `name` property) must be globally or regionally unique depending on the service. Use `uniqueString()` seeded with the resource group ID and a descriptive prefix to guarantee uniqueness while remaining identifiable.

```bicep
var storageAccountName = 'st${uniqueString(resourceGroup().id)}' // st + 13-char hash
var keyVaultName = 'kv-${workloadName}-${uniqueString(resourceGroup().id)}'
```

- Use consistent, short prefixes that identify the resource type (e.g., `st` for storage accounts, `kv-` for Key Vault, `vnet-` for virtual networks).
- Keep names within the service-specific length and character constraints.

## Parameters

Parameters are the public interface of a Bicep file or module. Design them for clarity and safety.

### Decorators

Always annotate parameters with `@description()` so that consumers and documentation generators understand each parameter's purpose:

```bicep
@description('The Azure region where resources will be deployed.')
param location string = resourceGroup().location

@description('The name of the workload, used as a prefix in resource names.')
@minLength(2)
@maxLength(10)
param workloadName string
```

Use validation decorators (`@minLength`, `@maxLength`, `@minValue`, `@maxValue`) to catch invalid input early. Use `@allowed()` sparingly -- it is appropriate for genuinely constrained sets (SKU names, Azure regions), but over-constraining makes templates brittle when new options become available.

### Defaults

Provide defaults that produce a safe, low-cost deployment suitable for test environments. This lets developers deploy without specifying every parameter while keeping production deployments explicit.

```bicep
@description('The SKU for the App Service plan.')
param appServicePlanSku string = 'B1'

@description('Whether to enable zone redundancy.')
param zoneRedundant bool = false
```

### Object and Array Parameters

When a resource requires several related settings, group them into an object parameter with a user-defined type rather than declaring many individual parameters:

```bicep
type networkConfig = {
  vnetAddressPrefix: string
  subnetAddressPrefix: string
  enableDdosProtection: bool
}

@description('Network configuration for the deployment.')
param network networkConfig = {
  vnetAddressPrefix: '10.0.0.0/16'
  subnetAddressPrefix: '10.0.1.0/24'
  enableDdosProtection: false
}
```

### Settings that Vary by Deployment

Use parameters for values that genuinely change between deployments: environment names, SKUs, region, feature flags. Values that are derived or constant belong in variables.

## Variables

Variables hold values that are computed from parameters or that simplify repeated expressions. Bicep auto-infers variable types, so do not annotate them.

```bicep
var appServiceName = 'app-${workloadName}-${environment}'
var isProduction = environment == 'prod'
var tags = {
  workload: workloadName
  environment: environment
  managedBy: 'bicep'
}
```

Use variables to avoid repeating complex expressions and to give meaningful names to intermediate values.

## Resource References

### Symbolic Names over Functions

Prefer symbolic references over `reference()`, `resourceId()`, or string-interpolated resource IDs. Symbolic references let Bicep infer dependencies automatically and catch errors at compile time.

```bicep
// Good -- symbolic reference
output storageEndpoint string = storageAccount.properties.primaryEndpoints.blob

// Avoid -- manual reference
output storageEndpoint string = reference(resourceId('Microsoft.Storage/storageAccounts', storageAccountName)).properties.primaryEndpoints.blob
```

### The existing Keyword

Use the `existing` keyword to reference resources that are already deployed rather than passing around resource IDs as strings:

```bicep
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
  name: keyVaultName
  scope: resourceGroup(keyVaultResourceGroupName)
}

resource secret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {
  parent: keyVault
  name: 'my-secret'
  properties: {
    value: secretValue
  }
}
```

### Child Resources

Use the `parent` property to declare child resources. Avoid deeply nested resource declarations because they reduce readability:

```bicep
// Good -- parent property
resource subnet 'Microsoft.Network/virtualNetworks/subnets@2024-01-01' = {
  parent: vnet
  name: 'snet-app'
  properties: {
    addressPrefix: '10.0.1.0/24'
  }
}

// Avoid -- deeply nested inline
resource vnet 'Microsoft.Network/virtualNetworks@2024-01-01' = {
  name: vnetName
  properties: {
    subnets: [
      {
        name: 'snet-app'
        properties: { addressPrefix: '10.0.1.0/24' }
      }
      // More subnets nested here become hard to maintain
    ]
  }
}
```

## API Versions

Always use the **latest stable** API version for each resource type. Avoid preview API versions in production templates unless you need a feature that is only available in preview. Pin to a specific date rather than a floating reference so builds are reproducible.

```bicep
// Good -- latest stable at time of writing
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = { ... }

// Avoid -- outdated
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-02-01' = { ... }
```

## Modularization

Break large templates into focused modules. Each module should represent a logical unit (a single resource with its supporting resources, or a pattern like "web app with monitoring").

### Module Design Principles

- **Consistent interface**: accept parameters for anything that varies, expose outputs for anything the caller needs.
- **Minimal surface**: pass only the parameters the module requires -- do not forward the entire parent parameter set.
- **Reusability**: a well-designed module works across environments without modification.

```bicep
// main.bicep
module webApp 'modules/web-app.bicep' = {
  name: 'deploy-web-app'
  params: {
    location: location
    appServicePlanId: appServicePlan.id
    appName: appName
    tags: tags
  }
}

// modules/web-app.bicep
@description('Resource ID of the App Service plan.')
param appServicePlanId string

@description('Name for the App Service.')
param appName string

@description('Azure region.')
param location string

@description('Tags to apply to all resources.')
param tags object

resource webApp 'Microsoft.Web/sites@2023-12-01' = {
  name: appName
  location: location
  tags: tags
  properties: {
    serverFarmId: appServicePlanId
    httpsOnly: true
  }
}

output webAppHostName string = webApp.properties.defaultHostName
```

### Resource Loops

Use `for` loops to deploy multiple instances of a resource from an array or range. This avoids copy-paste and keeps the template declarative:

```bicep
param subnetConfigs array = [
  { name: 'snet-app', addressPrefix: '10.0.1.0/24' }
  { name: 'snet-data', addressPrefix: '10.0.2.0/24' }
]

resource subnets 'Microsoft.Network/virtualNetworks/subnets@2024-01-01' = [
  for config in subnetConfigs: {
    parent: vnet
    name: config.name
    properties: {
      addressPrefix: config.addressPrefix
    }
  }
]
```

## Language Features

Use modern Bicep features to keep templates concise and readable:

- **String interpolation** over `concat()`:
  ```bicep
  var name = 'app-${workloadName}-${environment}'   // Good
  var name = concat('app-', workloadName, '-', environment)  // Avoid
  ```

- **Ternary operator** for conditional values:
  ```bicep
  var sku = isProduction ? 'P1v3' : 'B1'
  ```

- **Null-coalescing operator** (`??`) for fallback values:
  ```bicep
  var region = customRegion ?? resourceGroup().location
  ```

- **Spread operator** (`...`) for merging objects:
  ```bicep
  var allTags = { ...baseTags, ...environmentTags }
  ```

## Security

Bicep templates often handle credentials, connection strings, and other sensitive values. Mishandling them creates security vulnerabilities that persist in deployment history and logs.

### Sensitive Parameters

Mark parameters that accept secrets with `@secure()` so their values are never logged or displayed in deployment history:

```bicep
@secure()
@description('The administrator password for the SQL server.')
param sqlAdminPassword string
```

### Key Vault References

For production deployments, reference secrets from Azure Key Vault in parameter files rather than passing them as plain text. This keeps secrets out of source control entirely:

```json
{
  "sqlAdminPassword": {
    "reference": {
      "keyVault": { "id": "/subscriptions/.../providers/Microsoft.KeyVault/vaults/myVault" },
      "secretName": "sql-admin-password"
    }
  }
}
```

### Outputs

Never expose secrets, connection strings, passwords, or keys in outputs. Outputs are stored in plaintext in the deployment history and are visible to anyone with read access to the resource group. If a downstream resource needs a secret, pass it through Key Vault or use the `existing` keyword to look it up directly.

```bicep
// Good -- output only non-sensitive identifiers
output storageAccountId string = storageAccount.id
output storageAccountName string = storageAccount.name

// Dangerous -- never do this
output storageKey string = storageAccount.listKeys().keys[0].value
```

### HTTPS by Default

When deploying web-facing resources, enable HTTPS-only settings:

```bicep
resource webApp 'Microsoft.Web/sites@2023-12-01' = {
  name: appName
  location: location
  properties: {
    httpsOnly: true
  }
}

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: storageName
  location: location
  properties: {
    supportsHttpsTrafficOnly: true
    minimumTlsVersion: 'TLS1_2'
  }
}
```

## Tagging

Apply consistent tags to all resources for cost tracking, ownership, and operational purposes. Define tags as a variable or parameter and spread them onto every resource:

```bicep
var tags = {
  workload: workloadName
  environment: environment
  managedBy: 'bicep'
  costCenter: costCenter
}

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: storageName
  location: location
  tags: tags
  ...
}
```

## Dependencies

Bicep automatically infers deployment dependencies from symbolic references. Use explicit `dependsOn` only when there is an implicit ordering requirement that Bicep cannot detect (e.g., a role assignment must complete before a resource tries to access a storage account). Over-using `dependsOn` slows deployments by serializing operations that could run in parallel.

```bicep
// Implicit dependency -- Bicep infers that webApp depends on appServicePlan
resource webApp 'Microsoft.Web/sites@2023-12-01' = {
  properties: {
    serverFarmId: appServicePlan.id  // Bicep detects this reference
  }
}

// Explicit dependsOn -- only when necessary
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  dependsOn: [storageAccount]
  ...
}
```

## Imports, Exports, and User-Defined Functions

### Sharing Types and Functions Across Files

Use `@export()` to expose types and functions from a file, and `import` to consume them elsewhere. This is the primary mechanism for sharing naming conventions, custom types, and utility functions across a Bicep project.

```bicep
// modules/naming.1.0.bicep -- shared naming module

@export()
type environment = 'DEV' | 'UAT' | 'PROD'

@export()
@description('Get a resource name using the organizational naming convention')
func resourceName(
  environment environment,
  function string,
  resourceTypeAbbreviation string) string =>
  toLower(join([ 'org', 'platform', environment, function, resourceTypeAbbreviation, '01' ], '-'))

@export()
func resourceGroupName(environment environment) string =>
  'org-platform-${toLower(environment)}-rg'
```

```bicep
// environment/main.bicep -- consumer
import { environment as env, resourceName, resourceGroupName } from '../modules/naming.1.0.bicep'

param environment env

resource resourceGroup 'Microsoft.Resources/resourceGroups@2023-07-01' = {
  name: resourceGroupName(environment)
  location: location
}
```

### User-Defined Types

Define custom types with `@export()` to enforce consistent shapes across modules. This is especially useful for shared parameter structures like principal objects, IP allowlists, or configuration objects:

```bicep
// types/entra-principal.bicep
@description('Entra principal (user or group) with admin access')
@export()
type entraPrincipal = {
  @description('Display name of the principal')
  name: string
  @description('Object ID of the principal in Entra ID')
  objectId: string
  @description('Type of principal')
  type: 'User' | 'Group'
}
```

### Naming Convention Module Pattern

Centralizing all resource naming into a dedicated module is a key enterprise pattern. It ensures every resource across all templates follows the same naming standard. Version the module file (e.g., `naming.1.0.bicep`, `naming.1.1.bicep`) so that existing templates can pin to a known version while new features are added.

## External Configuration with loadJsonContent

Use `loadJsonContent()` to load structured configuration from JSON files. This keeps Bicep templates focused on resource definitions while externalizing environment-specific data, tag sets, or access control lists:

```bicep
// Load tags from a shared JSON file, keyed by environment
var tags = loadJsonContent('../../tags.json')['${environment}']

// Load admin access configuration
var adminAccess = loadJsonContent('../../admin-access.json')['${environment}']

resource resourceGroup 'Microsoft.Resources/resourceGroups@2023-07-01' = {
  name: rgName
  location: location
  tags: tags
}
```

Use the pattern `tags: empty(tags) ? null : tags` when tags may be an empty object and the resource does not accept empty tag objects.

## Parameter Files (.bicepparam)

Use `.bicepparam` files for environment-specific parameter values. These are type-safe and support expressions, unlike legacy JSON parameter files:

```bicep
// main.dev.bicepparam
using './main.bicep'

param environment = 'DEV'
param location = 'westeurope'
param zoneRedundant = false
```

```bicep
// main.prod.bicepparam
using './main.bicep'

param environment = 'PROD'
param location = 'westeurope'
param zoneRedundant = true
```

Prefer `.bicepparam` over JSON parameter files for new projects. Name them to match the template: `main.dev.bicepparam`, `main.prod.bicepparam`.

## Module Versioning and Registry Publishing

For enterprise teams sharing modules across repositories, publish versioned Bicep modules to an Azure Container Registry (ACR):

- Name module files with semver: `naming.1.0.bicep`, `role-assignments/cosmos.1.0.bicep`
- Publish to ACR: `br:myregistry.azurecr.io/bicep/modules/naming:1.0`
- Consumers reference published modules:
  ```bicep
  module naming 'br:myregistry.azurecr.io/bicep/modules/naming:1.0' = {
    name: 'naming'
  }
  ```
- Bump the minor version for backwards-compatible changes, major for breaking changes
- Automate publishing in CI/CD pipelines

## Additional Decorators

### @batchSize

Use `@batchSize()` on resource or module loops to control parallelism. Set to `1` for serial deployment when resources have ordering dependencies that Bicep cannot infer (e.g., role assignments that must complete one at a time):

```bicep
@batchSize(1)
module roleAssignments 'modules/role-assignment.bicep' = [for principal in principals: {
  name: 'role-${principal.name}'
  params: {
    principalId: principal.objectId
    roleDefinitionId: readerRoleId
  }
}]
```

## Subscription-Scoped Deployments

For deployments that create resource groups and orchestrate cross-group resources, set `targetScope = 'subscription'` and create resource groups inline:

```bicep
targetScope = 'subscription'

resource resourceGroup 'Microsoft.Resources/resourceGroups@2023-07-01' = {
  name: resourceGroupName(environment)
  location: location
  tags: tags
}

module network 'network.bicep' = {
  name: 'network'
  scope: resourceGroup
  params: {
    location: location
    environment: environment
    tags: tags
  }
}
```

Use `scope: resourceGroup` to target modules at specific resource groups. This pattern enables a single main.bicep to orchestrate an entire environment with multiple resource groups and cross-group dependencies.

## Documentation

Include `//` comments to explain non-obvious decisions, workarounds, and the reasoning behind specific configurations. Comments are free -- they do not appear in the compiled ARM template.

```bicep
// Using a Premium SKU here because the workload requires zone redundancy
// and the Standard SKU does not support availability zones in this region.
resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = {
  name: planName
  location: location
  sku: {
    name: 'P1v3'
    tier: 'PremiumV3'
  }
  properties: {
    zoneRedundant: true
  }
}
```

Use `@description()` on every parameter and output so that tooling (VS Code, `az deployment what-if`, documentation generators) can surface useful information without reading the template body.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…