Insight Tech APAC Blog Logo

Anti-Pattern 4: Not Using Outputs for Dependencies

trentsteenholdt
December 4, 2024

13 minutes to read

Azure Bicep Anti-Patterns

Welcome back to our series on Azure Bicep anti-patterns. In this fourth installment, we address a crucial aspect of Infrastructure as Code (IaC) that often gets overlooked: not using outputs for dependencies, or simply, not using outputs at all. Outputs in Bicep serve as the essential glue between modules and systems, and neglecting them can lead to unnecessary and increased complexity in your deployments. Let’s dive into why outputs matter and how to effectively utilise them in your Bicep projects.

Revisiting Modularisation

Before we delve into outputs, it’s important to recall the significance of modularisation as discussed in part two. Properly modularising your Bicep code enhances readability, maintainability, and reusability. Outputs play a pivotal role in this by enabling seamless communication between these modules.

The Issue: Ignoring Outputs

A common mistake in Bicep development is the failure to use outputs to manage dependencies between modules. Instead, developers might:

  • Rely heavily on dependsOn to manage resource deployment order. Then construct the resourceId of things by using resourceId() or worse vars.
  • Use hard-coded references or variables that are difficult to track and maintain.
  • Neglect to pass necessary information between modules, leading to tightly coupled and brittle deployments.

Why is this a problem?

  • Bottlenecks: Without proper outputs, managing dependencies becomes cumbersome, potentially causing delays and errors in the deployment process.
  • Complexity: Increased reliance on dependsOn and hard-coded references makes the codebase harder to understand and maintain.
  • Lack of Flexibility: Without outputs, integrating different modules or reusing them in different contexts becomes challenging.

Example of Ignoring Outputs

Consider a scenario where a Key Vault is deployed in one module, and its name is needed in another module for storing secrets. Without using outputs, you might handle this inefficiently like below.


var keyvaultName = 'myKeyvault'

// keyVault.bicep
resource keyVault 'Microsoft.KeyVault/vaults@2021-06-01-preview' = {
  name: keyvaultName 
  location: location
  properties: {
    // Key Vault properties
  }
}

// secrets.bicep
resource secret 'Microsoft.KeyVault/vaults/secrets@2021-06-01-preview' = {
  name: '${keyvaultName}/mySecret'
  properties: {
    value: 'superSecretValue'
  }
  dependsOn: [
    keyVault
  ]
}

In this example, secrets.bicep directly references a var called keyvaultName, creating a tight coupling to this code file and making the code less modular (can’t split it away or use elsewhere) and harder to manage.

The Solution: Leveraging Outputs

Using outputs in Bicep modules allows you to pass necessary information between modules cleanly and efficiently. This practice enhances modularity and reduces dependency-related complexities.

Defining Outputs in a Module

First, define outputs in the module that creates the resource.

// keyVault.bicep

param keyVaultName string

resource keyVault 'Microsoft.KeyVault/vaults@2021-06-01-preview' = {
  name: keyVaultName
  location: location
  properties: {
    // Key Vault properties
  }
}

output keyVaultName string = keyVault.name
output keyVaultId string = keyVault.id

Consuming Outputs in Another Module

Next, consume these outputs in the dependent module.

// secrets.bicep
param keyVaultName string

param keyVaultSecretName string

@secure()
param keyVaultSecretValue string

resource secret 'Microsoft.KeyVault/vaults/secrets@2021-06-01-preview' = {
  name: '${keyVaultName}/${keyVaultSecretName}'
  properties: {
    value: 'keyVaultSecret'
  }
}

Linking Modules with Outputs

Finally, link the modules in your main Bicep file using the outputs.

// main.bicep

param keyVaultName string = 'mykeyVault'

@secure()
param keyVaultSecretValue string = guid() # Overwritten in CI/CD with additional --parameters AzCLI call.

module keyVaultModule './keyVault.bicep' = {
  name: 'deployKeyVault'
  params: {
    keyVaultName: keyVaultName
    location: location
  }
}

module secretsModule './secrets.bicep' = {
  name: 'deploySecrets'
  params: {
    keyVaultName: keyVaultModule.outputs.keyVaultName
    keyVaultSecretName: 'password'
    keyVaultSecretValue: keyVaultSecretValue
  }
}

Note how doing this removed the need for dependsOn. The secretsModule will only run when the outputs are available to it. Additionally you can now take secrets.bicep and use that Bicep file in other projects and code bases as the string in for the keyVaultName can be any parameter.

Integrating Outputs with PowerShell

Handling outputs in automation scripts, say in CI/CD is also very easy to achieve. Using PowerShell, a language I use regularly in my day-to-day workings with IaC, is straightforward. Here’s how you can capture and utilise outputs from a Bicep deployment.

Example PowerShell Script

$deploymentResult = New-AzDeployment `
    -Name $name `
    -Location $location `
    -TemplateFile $templatePath `
    -TemplateParameterObject $parameters `
    -SkipTemplateParameterPrompt `
    -Confirm:$ConfirmPreference `
    -WhatIf:$WhatIfPreference `
    -Verbose

# Function to publish variables
function Publish-Variables(
  [Parameter(Mandatory = $true)]
  [Hashtable] $values,

  [Parameter(Mandatory = $false)]
  [switch] $isSecret = $false
) {
  $isLocal = $env:LOCAL_DEPLOYMENT -eq "True"
  $isAzureDevOps = $env:TF_BUILD -eq "True"
  $isGithubActions = $env:GITHUB_ACTIONS -eq "true"

  $localValues = @{}

  $values.Keys | ForEach-Object {
    $value = $values[$_]

    # Handle secure string
    if ($value -is [securestring]) {
      if (-not $isSecret) {
        throw "Attempted to publish SecureString $_ without -isSecret"
      }

      # Convert to plain text so we can interact with it
      $value = ConvertFrom-SecureString -SecureString $value -AsPlainText
    }

    # Convert non-strings to JSON so it can be stored as a string
    if ($value -isnot [string]) {
      $value = ConvertTo-Json $value -Compress
    }

    # Always store in an environment variable so that it can be retrieved again
    [System.Environment]::SetEnvironmentVariable($_, $value, [System.EnvironmentVariableTarget]::Process)

    # If we are running locally then stash the value (with encryption for secrets)
    if ($isLocal) {
      if (-not $isSecret) {
        $localValues[$_] = $value
      }
      else {
        $localValues[$_] = "encrypted:" + (New-LocalEncryptedSecret (ConvertTo-SecureString -AsPlainText $value -Force))
      }
    }

    # If we are running in Azure DevOps then we need to output the value as a variable
    if ($isAzureDevOps) {
      $output = @("task.setvariable variable=$_", "isOutput=true")

      if ($isSecret) {
        $output += "isSecret=true"
      }

      Write-Output "##vso[$($output -join ";");]$value"
    }

    # If we are running on GitHub Actions, we need to output the values as variable using Github Action syntax
    if ($isGithubActions) {
      if ($isSecret) {
        Write-Output "::add-mask::$($_)"
      }

      # source: https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/
      Write-Output "$($_)=$($value)" >> $env:GITHUB_OUTPUT
    }
  }

  # If we are running locally then we need to stash the new values into the local json file
  if ($isLocal) {
    $localVariablesFile = '.\local.outputs.json'

    $existingAsJson = Get-Content -Path $localVariablesFile -Raw -ErrorAction SilentlyContinue
    if (-not $existingAsJson) {
      $existingAsJson = "{}"
    }

    $existingAsHash = ConvertFrom-Json ($existingAsJson) -AsHashtable
    $existingAsHash.Keys | ForEach-Object {
      if (-not ($localValues[$_])) {
        $localValues[$_] = $existingAsHash[$_]
      }
    }

    Set-Content -Path $localVariablesFile -Value (ConvertTo-Json $localValues) -Force
  }
}

# Publish the outputs
Publish-Variables -values $deploymentResult.Outputs

In this script:

  1. Deploy the Bicep Template: The New-AzDeployment cmdlet deploys the Bicep template and captures the outputs.
  2. Publish Variables: The Publish-Variables function handles the outputs by setting environment variables, publishing them to Azure DevOps, or GitHub Actions as needed. It can even support local deployments if you set $env:LOCAL_DEPLOYMENT to true in your IDE’s terminal.

Best practices for using Outputs

  1. Define clear and readable outputs: Ensure that each module clearly defines the outputs it provides. This clarity makes it easier for other modules or scripts to consume them.

    output storageAccountId string = storageAccount.id
    output storageAccountName string = storageAccount.name
    
  2. Use Outputs for inter-module Communication: Instead of hard-coding references or relying on dependsOn, use outputs to pass necessary information between modules.

    // In main.bicep
    module storageModule './storage.bicep' = {
      name: 'deployStorage'
      params: {
        location: location
        tags: tags
      }
    }
    
    module appModule './app.bicep' = {
      name: 'deployApp'
      params: {
        storageAccountId: storageModule.outputs.storageAccountId
      }
    }
    
  3. Minimise dependsOn Usage: Let Bicep infer dependencies based on resource references and outputs. Explicitly define dependsOn only when necessary.

    // Letting Bicep infer the dependency
    resource app 'Microsoft.Web/sites@2021-02-01' = {
      name: 'myApp'
      location: location
      properties: {
        serverFarmId: appServicePlan.id
      }
    }
    
  4. Be consistent with naming conventions: Use consistent and descriptive names for outputs to make their purpose clear.

    output appServicePlanId string = appServicePlan.id
    output appServicePlanName string = appServicePlan.name
    

Conclusion

Outputs are an essential feature in Azure Bicep that facilitate clean and efficient communication between modules and external systems. By properly utilising outputs, you can eliminate unnecessary dependencies, reduce complexity, and enhance the maintainability of your IaC projects. Avoid the anti-pattern of ignoring outputs and embrace them as a best practice to streamline your infrastructure deployments.

What’s Next?

In our next post of this series, we’ll discuss the pitfalls of bypassing secure secret management by not storing secrets in Azure Key Vault and instead passing them directly through pipelines. Stay tuned as we continue to explore common anti-patterns and how to avoid them.