Skip to content

10 Advanced Tips for Better Bicep Deployments

advanced azure bicep tips devops

From Novice to Pro

While Microsoft offers comprehensive documentation and tools for Bicep, information on Azure Bicep best practices and proper solution structuring is often scarce. To enhance the efficiency, security, and maintainability of your Bicep deployments, consider implementing a few strategic practices.

Are you looking to refine your Bicep IaC (Infrastructure as Code) deployments? Below, I’ve compiled a list of 10 essential tips that will help elevate your skills from a beginner to an advanced Bicep user, optimizing your deployment process.

Tip 1: Enforce naming conventions within your module

When setting up a new application, having a clear naming system is key to keeping things standardized and high quality. It’s smart to use prefixes, suffixes, and environment names that follow a specific structure. If you’re starting with a blank slate, known as a greenfield environment, you have a perfect chance to set these standards right from the get-go. By doing this within your Bicep module, you ensure that all your resources follow a consistent naming convention, making everything cleaner and easier to manage.

Here’s a simple breakdown of what each part of a name can include:

  • Prefix: Use a short version of your company’s name to start each resource name. This helps in creating unique names, which is often a requirement for many Azure resources.
  • Name: This is the main identifier of the resource.
  • Environment: A brief label for the environment, like ‘dev’ for development or ‘prd’ for production.
  • Location: Use a shorthand for the geographical location, like ‘wus’ for West US.

This approach not only simplifies naming but also solves common issues with name uniqueness in a straightforward way.

param prefix string = 'dkd'
param environment string
param location string
param name string

var resourcePrefix = '${prefix}${location}${name}${environment}'

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = {
  name: '${resourcePrefix}staccount'
  location: location
}

Tip 2: Validate your bicep files during build phase

To prevent errors during deployment, integrate linting and validation into the build phase of your DevOps pipeline. This step catches issues early, making the deployment smoother and less stressful.

Lint stage

The Bicep linter checks Bicep files for syntax errors and best practice violations.

- stage: Lint
  jobs:
  - job: LintCode
    displayName: Lint code
    steps:
      - script: |
          az bicep build --file deploy/main.bicep
        name: LintBicepCode
        displayName: Run Bicep linter

By default, the Bicep linter provides a warning when it detects a problem with your file. Azure Pipelines doesn’t treat linter warnings as problems that should stop your pipeline. To customize this behaviour, you create a bicepconfig.json

Validate state

The validate state checks whenever your bicep files are likely to deploy successfully to your Azure environment. This is a preflight validation check and runs more detailed checks.

- stage: Validate
  jobs:
  - job: ValidateBicepCode
    displayName: Validate Bicep code
    steps:
      - task: AzureResourceManagerTemplateDeployment@3
        name: RunPreflightValidation
        displayName: Run preflight validation
        inputs:
          connectedServiceName: $(ServiceConnectionName)
          location: $(deploymentDefaultLocation)
          deploymentMode: Validation
          resourceGroupName: $(ResourceGroupName)
          csmFile: deploy/main.bicep
          overrideParameters: >
            -environmentType $(EnvironmentType)

Tip 3: Create a private bicep repository

If you’re looking to share modules within your organization or across various source control repositories, consider setting up your own private Bicep repository. Think of this repository as a dedicated storage space where you can host and share Bicep modules, much like you would in a private locker. You get to control who in your organization has access to it.

This setup is similar to how container registries work with Docker or Kubernetes in Azure, although it’s versatile enough to store any kind of artifacts. Keep in mind, that there’s usually a small fee for using container registries.

Why go for a private repository? Here are the biggest benefits:

  • Consistency: Ensures standardization across your organization.
  • Efficiency: Cuts down on the need to copy and paste reusable modules.
  • Control: Offers support for versioning, helping you manage changes and updates smoothly.

Tip 4: Focus on security best practices

As we continue to standardize projects across the organization by using a private repository or uniform Bicep modules for all projects, we’re able to enhance control over deployed resources. This setup provides an excellent opportunity to strengthen your organization’s security posture. Here are a few practical steps you might consider:

  • Restrict Public Access to Blob Storage: Azure storage accounts typically allow open access, which can lead to unintended data exposure. To prevent this, ensure that your storage accounts deny public access and utilize signed URLs or Azure Private Link for secure connectivity.
  • Refine Permissions: Azure resources often come with broad permissions by default. Tightening these permissions and avoiding overly generous access rights can reduce the risk of unauthorized access.
  • Configure Role-Based Access Control (RBAC) Properly: Automatically grant access levels to your support teams (Levels 1, 2, and 3) appropriate to their environment, while restricting access to production environments for the development team. This ensures that everyone has the access they need without compromising security.
  • Secure Management Ports: Open management ports such as SSH and RDP can be vulnerable to attacks. It’s wise to block these ports from the internet and use tools like Network Security Groups (NSGs) or Azure Bastion for secure management access.
  • Enhance Keyvault Security: Enabling features like soft delete and diagnostic settings in Azure Keyvault helps track access patterns and identify potential security threats.

Tip 5: Facilitate for Azure naming rules and restrictions

Azure has specific rules for naming resources, and it’s easy to miss some of these unless you check the guidelines thoroughly. For a smoother experience, it’s a good idea to implement these naming conventions right in your Bicep modules.

Take storage accounts as an example—they need names that are between 3 and 24 characters long, consisting only of lowercase letters and numbers. Unlike other resources, hyphens aren’t allowed, which is a common trip-up point.

param storageAccountName string {
  @pattern('^[a-z0-9]{3,24}$')
  @minLength(3)
  @maxLength(24)
  metadata: {
    description: 'Storage account name must be between 3 and 24 characters in length and contain only lowercase letters and numbers. Hyphens are not allowed.'
  }
}

For a deeper dive into Bicep parameters and how to apply them, check out this resource here.

Tip 6: Implement (correct) dependency management

In Bicep, when you use the properties of one resource in another’s definition, you establish what’s known as an implicit dependency. This means Bicep will automatically prioritize the deployment of the referenced resource first, ensuring everything runs smoothly.

But for this to work effectively, it’s crucial to implement it properly. A good practice is to define each resource’s name just once and then simply refer to that name in other parts of your code. Avoid duplicating the resource name—it’s about keeping things neat and error-free.

To make your modules more manageable and your resources easier to reference later, I always include two output parameters so they can be used for later reference:

  • Resource-id: This is the ID of the resource once created.
  • Resource Name: As the name suggests, this is the name of the created resource.

Example:

StorageAcount.bicep
param resourceGroupName string
param location string
param accountName string
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = {
  name: accountName
  location: location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
  scope: resourceGroupName
}

output storageAccountId string = storageAccount.id
output storageAccountName string = storageAccount.name

Tip 7: Integration with KeyVault

Managed identity

Instead of hardcoding credentials or using service principals with secrets, leverage Azure Managed Identities. This approach enhances security by avoiding the storage of credentials in your Bicep files and simplifies identity management.

Dynamic secret retrieval during deployment

Use the getSecret function to dynamically retrieve secrets during deployment, which avoids the need to pass sensitive information through Bicep parameters or outputs. Ensure the resource calling the list function has appropriate permissions.

param sqlServerName string
param adminLogin string

param subscriptionId string
param kvResourceGroup string
param kvName string

resource kv 'Microsoft.KeyVault/vaults@2023-02-01' existing = {
  name: kvName
  scope: resourceGroup
}

module sql './sql.bicep' = {
  name: 'deploySQL'
  params: {
    sqlServerName: sqlServerName
    adminLogin: adminLogin
    adminPassword: kv.getSecret('vmAdminPassword')
  }
}

Use the @secure decorator to pass on this secure value to the underlying module.

param sqlServerName string
param adminLogin string

@secure()
param adminPassword string

resource sqlServer 'Microsoft.Sql/servers@2020-11-01-preview' = {
  name: sqlServerName
  location: resourceGroup().location
  properties: {
    administratorLogin: adminLogin
    administratorLoginPassword: adminPassword
    version: '12.0'
  }
}

Integrate Key Vault with Other Azure Services

Securely pass Key Vault secrets to other Azure resources that support Azure Key Vault references, such as App Service or Azure Functions.

resource appService 'Microsoft.Web/sites@2021-02-01' = {
  name: 'myAppService'
  properties: {
    siteConfig: {
      appSettings: [
        {
          name: 'DatabasePassword'
          value: @Microsoft.KeyVault(SecretUri=${keyVault.properties.vaultUri}/secrets/mySecret)'
        }
      ]
    }
  }
}

Tip 8: User-friendly names for RBAC

Unfortunately, dealing with RBAC in Bicep isn’t simpler than in ARM templates because we still have to use the GUID associated with each Azure built-in access role.

These roles, defined as separate resources rather than simple strings, must be linked using their fully qualified names in any role assignment.

Luckily, the community is always innovating, and I want to give a shout-out to Jos van der Til for a fantastic method that streamlines this process. By following his approach, you can significantly reduce the time it takes to manage these settings, letting developers focus more on development.

Before
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(resourceGroup().id, principalId, contributorRoleDefinition.id)
  properties: {
    roleDefinitionId: ‘/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe' // Storage Blob Data Contributor
    principalId: principalId
    principalType: 'ServicePrincipal'
  }
}
After
module roleAuthorization 'storageAuth.bicep' = {
    name: 'roleAuthorization'
    properties: {
        principalId: principalId
        storageAccountName: 'some-existing-storage-account'
        roleDefinition: 'Storage Blob Data Contributor'
    }
}

Tip 9: Monitor and audit deployments

When it comes to meeting auditing standards, it’s essential to maintain detailed logs of all deployment activities. This ensures that you can easily revisit any release to analyze potential issues. To prevent the overwriting of previous deployment logs, I recommend adopting a naming convention that incorporates the repository name and the release timestamp for each deployment. This strategy will help keep your deployment history organized and easily accessible for future reviews.

Step 1: Create a unique deployment name variable:

- pwsh: |
    Write-Host "Setting up the date time for build variable"
    $releasename='MyProject-$(Get-Date -format yyyyMMdd)'
    Write-Host "##vso[task.setvariable variable=currentTimeStamp]$(Get-Date -format yyyyMMdd)"
  displayName: 'Getting timestamp'

Step 2: Pass on this variable to the bicep deployment:

- task: AzureResourceManagerTemplateDeployment@3
  displayName: 'Release: Deploy IAC'
  inputs:
    azureResourceManagerConnection: '${{ parameters.AzureSubscription }}'
    subscriptionId: '${{ parameters.AzureSubscriptionId }}'
    templateLocation: Linked artifact
    csmFile: '$(System.DefaultWorkingDirectory)/_src/Main.bicep'
    deploymentName: '$(releasename)'

Troubleshooting and investigating deployment failures can be challenging, but there’s an effective way to manage this. Following the earlier advice on naming logs, it’s also beneficial to set up conditional rules in your DevOps pipeline. This setup allows you to automatically visualize error outputs if a deployment fails reducing time spent on the azure portal finding back the deployment.

Step 3: unique deployment name variable to visualize errors.

- task: AzurePowerShell@5
  displayName: 'Release: Get deployment outputs'
  inputs:
    azureSubscription: '${{ parameters.AzureSubscription }}'
    ScriptType: 'InlineScript'
    Inline: |
      Get-AzDeploymentOperation -DeploymentName $(releasename) | Where-Object {$_.ProvisioningState -match 'Failed'}
    azurePowerShellVersion: LatestVersion
  condition: Failed()

Tip 10: Follow up on Microsoft’s best practices

Microsoft offers a helpful guide outlining valuable tips and Azure Bicep best practices for using Bicep. I strongly suggest visiting the following link, as it provides additional insights and techniques that could enhance your Bicep deployments from basic to advanced levels:

https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/best-practices