Handle differences between environments by using Bicep parameters

Completed

You've already learned about Bicep parameters. They help you specify values that can change between deployments of your Bicep files.

Parameters are commonly used to support the differences between your environments. For example, in your non-production environments, you often want to deploy inexpensive SKUs of your Azure resources. In production, you want to deploy SKUs that have better performance. And you might want to use different names for resources in each environment.

When you deploy your Bicep file, you provide values for each parameter. There are several options for how you specify the values for each parameter from your workflow, and how you specify separate values for each environment. In this unit, you'll learn about the approaches for specifying Bicep parameter values in a deployment workflow.

Parameter files

A parameter file is a JSON-formatted file that lists the parameter values that you want to use for each environment. You submit the parameter file to Azure Resource Manager when you submit the deployment.

Here's an example parameter file:

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "reviewApiUrl": {
      "value": "https://sandbox.contoso.com/reviews"
    }
  }
}

Parameter files can be committed to your Git repository alongside your Bicep file. You can then refer to the parameter file in your workflow template where you execute your deployment.

It's a good idea to establish a consistent environment-naming strategy for parameter files. For example, you might name your parameter files parameters.ENVIRONMENT_NAME.json, like parameters.Production.json. Then, you can use a workflow template input to automatically select the correct parameter file based on an input value.

on:
  workflow_call:
    inputs:
      environmentType:
        required: true
        type: string
      # ...

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
    # ...
    - uses: azure/arm-deploy@v1
      with:
        failOnStdErr: false
        resourceGroupName: ${{ inputs.resourceGroupName }}
        template: ./deploy/main.bicep
        parameters: ./deploy/azuredeploy.parameters.${{ inputs.environmentType }}.json

When you use parameter files, your workflow YAML files don't need to contain a list of parameters that need to be passed to your deployment steps individually. This is especially helpful when you have a large number of parameters.

A parameter file keeps the parameter values together in a single JSON file. The parameter files are also part of your Git repository, so they can get versioned in the same way as all your other code.

Important

Parameter files shouldn't be used for secure values. There's no way to protect the values of the secrets in the parameter files, and you should never commit secrets to your Git repository.

Workflow variables

GitHub Actions enables you to store workflow variables, which are useful for values that might be different between environments. They're also useful for values that you want to define only once and then reuse throughout your workflow.

Variables defined in a YAML file

You can define variables and set their values within a YAML file. This is useful when you need to reuse the same value multiple times. You can define a variable for a whole workflow, a job, or a single step:

env:
  MyVariable1: value1

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      MyVariable2: value2
    steps:
    - run: echo Hello world!
      env:
        MyVariable3: value3

Secrets defined in the web interface

Like Bicep parameter files, YAML files aren't suitable for secrets. Instead, you can define secrets by using the GitHub web interface. You can change the variable values at any time, and the workflow will read the updated values the next time it runs. GitHub Actions tries to hide the secrets' values in the workflow logs. This means you can store values that your Bicep file then accepts as parameters with the @secure() decorator.

Warning

By default, GitHub Actions obfuscates secret variable values in workflow logs, but you need to follow good practices as well. Your workflow steps have access to the values of secrets. If your workflow includes a step that doesn't handle a secret securely, there's a chance the secret value might be shown in the workflow logs. You should always carefully review the any changes to a workflow definition file to verify the secrets won't be mishandled.

When you create a secret, GitHub enables you to choose whether to scope it to your entire Git repository or to a specific environment. Environment-scoped secrets honor the protection rules you configure on your environments, so if you configure a required reviewer rule, a workflow can't access the secrets values until the specified GitHub user has approved your pipeline to deploy to that environment.

Environment-scoped secrets can be helpful, but they don't easily work with Azure Resource Manager's preflight validation or what-if operations. These operations need to communicate with Azure, which means they need a workload identity. You generally want to provide deployment approval after the preflight validation or what-if operations are completed, so that you but have a high degree of confidence in the changes that you're deploying. So, if you use environment-scoped secrets, the human review process happens too early in your workflow.

For this reason, you don't use environment-scoped secrets in this module's exercises. Instead, you create repository-scoped secrets with predictable names that include the environment name. This enables your workflow to identify the correct secret to use for each environment. In your own workflows, you might choose to use repository-scoped secrets, environment-scoped secrets, or even a mixture of both.

Note

You can also scope secrets to GitHub organizations. Although this isn't in scope for this module, we link to more information in the summary.

Use variables in your workflow

The way that you access a variable's value in your workflow depends on the type of variable.

Type Syntax
Variables defined in the same file ${{ env.VARIABLE_NAME }}
Inputs to a called workflow ${{ inputs.INPUT_NAME }}
Secrets ${{ secrets.SECRET_NAME }}

For example, when you run a Bicep deployment, you might use secrets to specify the Azure workload identity to use, a called workflow input to specify the resource group name, and a variable to specify the value of a parameter:

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      MyParameter: value-of-parameter
    steps:
    - uses: actions/checkout@v3
    - uses: azure/login@v1
      with:
        client-id: ${{ secrets.AZURE_CLIENT_ID }}
        tenant-id: ${{ secrets.AZURE_TENANT_ID }}
        subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
    - uses: azure/arm-deploy@v1
      with:
        failOnStdErr: false
        resourceGroupName: ${{ inputs.resourceGroupName }}
        template: ./deploy/main.bicep
        parameters: myParameter=${{ env.MyParameter }}

What's the best approach?

You've learned about several ways to handle the parameters that your Bicep file needs for your deployment. It's helpful to understand when you might use which approach.

Avoid unnecessary parameters

Parameters help you make your Bicep files reusable, but it's easy to define too many parameters. When you deploy a Bicep file, you need to provide a value for every parameter. In complex deployments against multiple environments, it gets hard to manage a large set of individual parameter values.

Consider making parameters optional where you can, and use default values that apply to most of your environments. You might then avoid the need for your workflows to pass in values for the parameters.

Also, keep in mind that parameters are often used in Bicep when resources need to connect to other resources. For example, if you have a website that needs to connect to a storage account, you'll need to provide the storage account name and access key. Keys are secure values. However, consider these other approaches when you're deploying this combination of resources:

  • Use the website's managed identity to access the storage account. When you create a managed identity, Azure automatically generates and manages its credentials. This approach simplifies the connection settings. It also means you don't have to handle secrets at all, so it's the most secure option.
  • Deploy the storage account and website together in the same Bicep template. Use Bicep modules to keep the website and storage resources together. Then, you can automatically look up the values for the storage account name and the key within the Bicep code, instead of passing in parameters.
  • Add the storage account's details to a key vault as a secret. The website code then loads the access key directly from the vault. This approach avoids the need to manage the key in the workflow at all.

Use workflow variables for small sets of parameters

If you have only a few parameters for your Bicep files, consider defining variables in your YAML file.

Use parameter files for large sets of parameters

If you have a large set of parameters for your Bicep files, consider using parameter files to keep the non-secure values together for each environment. Then, whenever you need to change the values, you can update a parameter file and commit your change.

This approach keeps your workflow steps simpler, because you don't need to explicitly set the value for every parameter.

Store secrets securely

Use an appropriate process for storing and handling secrets. Use GitHub secrets to store secrets in your GitHub repository, or use Key Vault to store secrets in Azure.

For secure parameters, remember to explicitly pass each parameter into your deployment steps.

GitHub can automatically scan your repository for secrets that have been accidentally committed, so that you can be notified. You can then remove and rotate the secrets. We link to more information about this feature in the summary.

Combine approaches

It's common to combine multiple approaches to handle your parameters. For example, you can store most of your parameter values in parameter files, and then set secure values by using a secret. The following example illustrates the combination:

on:
  workflow_dispatch:
    inputs:
      environmentType:
        required: true
        type: string
      resourceGroupName:
        required: true
        type: string
    secrets:
      AZURE_CLIENT_ID:
        required: true
      AZURE_TENANT_ID:
        required: true
      AZURE_SUBSCRIPTION_ID:
        required: true
      MySecureParameter:
        required: true

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - uses: azure/login@v1
      with:
        client-id: ${{ secrets.AZURE_CLIENT_ID }}
        tenant-id: ${{ secrets.AZURE_TENANT_ID }}
        subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
    - uses: azure/arm-deploy@v1
      with:
        failOnStdErr: false
        resourceGroupName: ${{ inputs.resourceGroupName }}
        template: ./deploy/main.bicep
        parameters: >
          ./deploy/azuredeploy.parameters.${{ inputs.environmentType }}.json
          mySecureParameter=${{ secrets.MySecureParameter }}

Tip

At the end of this example, the parameters value is provided as a YAML multiline string by using the > character. This makes the YAML file easier to read. It's equivalent to including the entire value on a single line.