Bicep Tips and Tricks | #2 | Centralize Core Parameters with Types, Constructors, and Imports

Posted by Andrew Wilson on Wednesday, July 23, 2025

Overview

When building IaC templates we strive to enable them to be environment agnostic, configurable even. One of the mechanisms that we do this is through lots of “Core Parameters” that disseminate the fundamental details of our deployment and resources. The number of core parameters is often variable but the verdict is always true, three or more usually does the trick. These Core Parameters are passed into the main deployment template and any other sub deployment template (Module) too. These may include but are not limited to:

  • Environment
  • Location/Region
  • Project/Application Prefix

But why am I going on about this, why is it a problem. Well it’s not a problem as much as it’s a readability and maintainability issue. Think about it, every template now requires three or more core parameters defined, with every module call having to define them as parameters to be passed in. Following this, if there is a change, the change will need to occur everywhere, and let’s hope that the descriptions/metadata has been kept up to date too!

My recommendation to resolve this issue involves three Bicep concepts:

  1. User-defined data types
  2. User-defined functions
  3. Imports

Recommendation broken down

1. Define our source of truth Core Parameters | User-defined data type

User-defined data types provide us a way to define our own data object containing all the core parameters that we use throughout such as the following:

// Bicep

@description('Core Parameters Definition for Bicep Templates')
@sealed()
type coreParams = {
  @description('Location to deploy resources to')
  location: string
  @description('Environment to deploy to')
  environment: string
  @description('Project Prefix for resource naming')
  projectPrefix: string
}

The decorator @sealed() means that you are only permitting the use of properties specifically included in the type definition. This helps with making sure there is only one single source of truth for the coreParams definition.

The type definition means that we can pass through a single parameter to our deployment templates, and manage change in a single place.

2. Create a Constructor to setup our Core Parameters | User-defined function

To aid in the initialization of the coreParams type, I typically create a user-defined function (Constructor) which sets up the initial state by assigning values to the respective properties. This ensures the Core Params object starts in a valid, predictable state. The function would look like this:

// Bicep

@description('Core Parameters Constructor')
func coreParamsConstructor(
  location string,
  environment string,
  projectPrefix string
) coreParams => {
  location: location
  environment: environment
  projectPrefix: projectPrefix
}

3. Promote Shared Use of the Type and Constructor | Imports

The user defined type and function alone would not make this an effective recommendation due to the duplication of both the type and function definition on all templates. However, due to the recent introduction of exports and imports, we can continue on the path of a single and reusable single source of truth.

To make this work, what I would suggest is to create a new folder in your IaC location called CommonTypesAndFunctions, in this folder create a new bicep file called types.bicep. This is the shared bicep file that we are going to use for our new type and constructor.

An example of this setup is as follows:

  • Iac → CommonTypesAndFunctions → types.bicep

Shared Types Template

// Bicep

/**********************************
  Bicep Template: Shared Types
  Author: Andrew Wilson
***********************************/

// ** User Defined Types and Constructors **
// *****************************************

// TYPE: Core Parameters
// *********************

@export()
@description('Core Parameters Definition for Bicep Templates')
@sealed()
type coreParams = {
  @description('Location to deploy resources to')
  location: string
  @description('Environment to deploy to')
  environment: string
  @description('Project Prefix for resource naming')
  projectPrefix: string
}

@export()
@description('Core Parameters Constructor')
func coreParamsConstructor(
  location string,
  environment string,
  projectPrefix string
) coreParams => {
  location: location
  environment: environment
  projectPrefix: projectPrefix
}

The @export() decorator is used to allow the type and function to be imported into other Bicep files.

Now we can use the import syntax in our main and sub deployment (module) templates, allowing use of the single defined type and function. In the main template we import the constructor and assign the initialized object to our coreParameters variable. For our sub deployment (module) templates we import the coreParams type so we can use it as a parameter definition. Both combined allow simplified module reference definitions and assignments. This can be seen in the following example templates:

Main Deployment Template

// Bicep

/***************************************
Bicep Template: Main Deploy
Author: Andrew Wilson
****************************************/

targetScope = 'resourceGroup'

// ** Shared Imports **
// ********************

import { coreParamsConstructor } from './CommonTypesAndFunctions/types.bicep'

// ** Parameters **
// ****************

@description('Location to deploy resources to')
param location string = resourceGroup().location

@description('Environment to deploy to')
param environment string = 'dev'

@description('Project Prefix for resource naming')
param projectPrefix string = 'myProject'

// ** Variables **
// ***************

var coreParameters = coreParamsConstructor(location, environment, projectPrefix)

// ** Resources **
// ***************

@description('Demonstration of the core parameters being passed to a submodule')
module subDeploy './subDeploy.bicep' = {
  name: 'subDeploy'
  params: {
    coreParameters: coreParameters
  }
}

Sub Deployment (Module) Template

// Bicep

/***************************************
Bicep Template: Resources Deploy
Author: Andrew Wilson
****************************************/

targetScope = 'resourceGroup'

// ** Shared Imports **
// ********************

import { coreParams } from './CommonTypesAndFunctions/types.bicep'

// ** Parameters **
// ****************

@description('Core Parameters for the deployment')
param coreParameters coreParams

// ** Variables **
// ****************

@description('The name of the resource to deploy')
var resourceName = '${coreParameters.projectPrefix}-${coreParameters.environment}-resource'

Summary

Managing core parameters in Bicep templates can quickly become unwieldy as your infrastructure grows. By leveraging user-defined types, constructors, and imports, you can centralize your parameter definitions, improve readability, and simplify maintenance. This approach ensures consistency across your deployments and makes future changes easier to manage. Happy Bicep-ing!