Conditional deployment in Bicep

Conditional deployment in Bicep

In the Bicep infrastructure article I demonstrated how to write Bicep files and deploy them on Azure each time our pipeline runs. This enabled us to have our resources defined in code and in a later article we easily deployed a new environment using the same Bicep files.

Our QA environment was a mirror image of our production environment and that's okay for now, but this isn't always the case in real-life applications.

As you can see, we have a staging slot in our QA resource group, but do we really need it?

To Slot or Not to Slot on QA:

The immediate consequence of omitting the staging slot for our QA environment is a minor downtime during deployments. However, considering it's the QA environment, a brief pause is acceptable. Why? Let's break it down:

  1. Cost Efficiency: By doing away with the staging slot, we can downgrade from the S1 tier to the B1 tier for our app service plan specifically for the QA environment. This transition alone saves approximately 50 EUR/month.

  2. Resource Optimization: By aligning resources more appropriately to their intended purpose (QA vs. Production), we ensure that we're not over-provisioning for a testing environment.

Now that we decided to remove it, you may be tempted to just go into Azure and delete the deployment slot, then manually downgrade the tier of the App Service Plan. However, if you do that, then on the next pipeline run the slot will be created again and our App Service Plan will get back up to the S1 tier.

At first glance, it might seem tedious to modify Bicep files for every infrastructure change. However, consider this: using Bicep ensures consistent, automated, and error-free updates. And, realistically, how frequently do you actually alter your infrastructure?

To optimize our QA environment, we'll adapt our Bicep configurations to exclude the staging slot and shift the app service plan from S1 to B1. This approach is practical, cost-efficient, and tailored to our specific deployment needs. Let's do this in 3 steps:

  1. Introducing a parameter within our configuration files.

  2. Incorporating this parameter into our Bicep definitions

  3. Facilitating deployments contingent on the established parameter value.


Creating a parameter in configuration files

We introduce a hasStagingSlot parameter in our configuration files to determine whether a staging slot should be deployed.

For the Production environment, hasStagingSlot is set to true:

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
      "location": {
        "value": "westeurope"
      },
      "appName": {
        "value": "bogdan-todo"
      },
      "appSku": {
        "value": "S1"
      },
      "hasStagingSlot": {
        "value": true // on prod environment we want the slot to be deployed
      }
    }
  }

For the QA environment, hasStagingSlot is set to false:

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
      "location": {
        "value": "westeurope"
      },
      "appName": {
        "value": "bogdan-todo-qa"
      },
      "appSku": {
        "value": "B1"
      },
      "hasStagingSlot": {
        "value": false // on qa environment we don't want the slot to be deployed
      }
    }
  }

We also change the appSku value to B1.

Integrating with Bicep

The main.bicep file receives the hasStagingSlot variable and passes it onto app.bicep as a parameter.

...
param hasStagingSlot bool = false // we can add a default value if we want

module app 'app.bicep'  = {
  name: 'bogdan-todo-app'
  params:{
    location: location
    appName: appName
    appSku: appSku
    hasStagingSlot: hasStagingSlot // we send this parameter to our app.bicep module
  }
}

Conditional deployment based on the parameter value

In app.bicep, the deployment of the staging slot depends on the state of hasStagingSlot

app.bicep

...
param hasStagingSlot bool // this is our variable which decides if we're creating the slot or not
...

// using the if statement we will deploy the slot
// only if hasStagingSlot is true
resource webAppSlot 'Microsoft.Web/sites/slots@2022-03-01' = if (hasStagingSlot) {
  name: 'staging'
  location: location
  kind: 'app,linux'
  parent: webApp
  properties:{
    enabled: true
    httpsOnly: true
    siteConfig: {
      httpLoggingEnabled: true
      linuxFxVersion: 'DOTNETCORE|7.0'
    }
  }
}

This segment essentially states that the staging slot will only be deployed if hasStagingSlot is true.

There's one more thing we have to do, we need to remove the slot deployment from our qa.yaml stage:

...
- job: Deploy
  dependsOn: UpdateAzureResources
  steps:
    - task: DownloadPipelineArtifact@2
      displayName: 'Download pipeline artifact'
      inputs:
        buildType: 'current'
        artifactName: 'drop'
        targetPath: '$(Pipeline.Workspace)/drop'
    - task: AzureWebApp@1
      displayName: 'Deploy to QA'
      inputs:
        azureSubscription: 'AzureConnection'
        appType: 'webAppLinux'
        appName: 'bogdan-todo-qa-app'
        package: '$(Pipeline.Workspace)/drop'
        resourceGroupName: $(resourceGroupName)
        runtimeStack: 'DOTNETCORE|7.0'
        startUpCommand: 'dotnet ToDoApp.Server.dll'

Now let's deploy the changes and see what happens!

Our pipeline was deployed successfully, but when we look at our QA environment we still see the staging slot:

When a resource is missing from a Bicep file but exists in Azure, Bicep doesn't automatically delete it. Here's why:

  1. Safety: Deleting resources can be destructive. Imagine accidentally removing a database with valuable data. By not auto-deleting, Bicep ensures safety.

  2. State Management: Bicep and ARM templates are declarative. This means you declare your desired state, and Azure ensures that the actual state matches. However, they don't maintain a history of previous states. So, when a resource is omitted from the Bicep file, Bicep doesn't necessarily recognize it as a directive to delete; it just sees it as "not part of the current directive."

  3. Explicitness: The philosophy behind tools like Bicep is that operations, especially destructive ones, should be explicit. Removing a resource from a Bicep file is implicit. If you want to delete a resource, it's generally better to do it explicitly, ensuring you're aware of the ramifications.

This basically means we have to go do it ourselves manually. Now that we updated our Bicep files, the staging slot will only be used in the production environment.

Takeaways

When to Use Conditional Deployment:

  1. You need to maintain different configurations for dev/test vs. production.

  2. There are cost constraints and you want to deploy certain resources only when absolutely necessary.

How to do conditional deployments:

  1. Use configuration files with different values per environment

  2. Use the if statement for conditional deployments based on your conditions

Conclusion:

Conditional deployments provide precise control over our infrastructure configuration. They optimize resource usage and can significantly minimize deployment errors. However, like all tools, they have their appropriate applications. Assess your needs, understand the circumstances, and employ them wisely.

The full source code can be downloaded from here: https://dev.azure.com/bujdea/_git/AzureDevopsYamlPipeline

What's next

In the next article, I will demonstrate how to add application settings using Bicep. Feel free to subscribe to the newsletter below or follow me on Twitter if you'd like to be notified as soon as possible!

Did you find this article valuable?

Support Bogdan Bujdea by becoming a sponsor. Any amount is appreciated!