BeginnerLesson 3 of 16

Resources, Parameters, Variables, and Outputs

Model Azure infrastructure cleanly — inputs in, resources out, values back to callers.

Simple Explanation (ELI5)

Imagine you fill out an order form to provision a network. Parameters are the fields on the form: environment name, region, address range. Variables are computed shorthand you derive from those inputs, like building the full VNet name from the environment string. The resource block is what actually gets created. Outputs are the receipt — values like the subnet ID returned to the caller so the next step does not need to re-query Azure.

Why Do We Need It?

Technical Explanation

Bicep compiles to ARM JSON. Every param, var, and output becomes a corresponding section in the ARM template. Bicep adds a type system with compile-time checking that ARM JSON lacks: string, int, bool, array, object, and secureString are all first-class types.

Parameter Decorators Reference

DecoratorPurposeExample use
@descriptionDocuments the parameter in portal and ARM APIShown in deployment UX
@allowedValidates input against an explicit allowlistEnvironment names, SKU tiers
@minLength / @maxLengthString or array length constraintStorage account: 3-24 chars
@secure()Marks the param secret; ARM never logs its valueAdmin passwords, connection strings
@minValue / @maxValueNumeric range constraintNode count: 1 to 100

Real-world Code: Parameterised VNet and Subnet

bicep
@description('Target environment: dev, stage, or prod')
@allowed(['dev', 'stage', 'prod'])
param environment string = 'dev'

@description('Azure region for all resources')
param location string = resourceGroup().location

@minLength(3)
@maxLength(50)
param projectName string

// Variables derive reusable values from params
var vnetName = 'vnet-${projectName}-${environment}'
var vnetCidr = environment == 'prod' ? '10.0.0.0/16' : '10.1.0.0/16'
var subnetCidr = environment == 'prod' ? '10.0.1.0/24' : '10.1.1.0/24'

resource vnet 'Microsoft.Network/virtualNetworks@2023-09-01' = {
  name: vnetName
  location: location
  properties: {
    addressSpace: {
      addressPrefixes: [vnetCidr]
    }
    subnets: [
      {
        name: 'snet-workloads'
        properties: {
          addressPrefix: subnetCidr
        }
      }
    ]
  }
}

// Outputs surface resource identifiers for downstream consumers
output vnetId string = vnet.id
output vnetName string = vnet.name
output subnetId string = vnet.properties.subnets[0].id

Useful Commands

bash
# Compile and validate
az bicep build --file main.bicep

# Preview changes without deploying
az deployment group what-if \
  --resource-group rg-platform-dev \
  --template-file main.bicep \
  --parameters @dev.parameters.json

# Deploy
az deployment group create \
  --resource-group rg-platform-dev \
  --template-file main.bicep \
  --parameters @dev.parameters.json \
  --name deploy-networking

# Capture an output for the next pipeline step
az deployment group show \
  -g rg-platform-dev -n deploy-networking \
  --query 'properties.outputs.subnetId.value' -o tsv

Hands-on Steps

  1. Save the template as main.bicep and run az bicep build to verify no compile errors.
  2. Create dev.parameters.json with projectName=platform and environment=dev.
  3. Run what-if to review the planned changes before committing.
  4. Deploy to a dev resource group and confirm the VNet appears in the portal.
  5. Capture the subnetId output and pass it to an AKS module in the next tutorial.

Debugging Scenarios

Scenario 1: Required Parameter Missing from .parameters.json

Symptom: Azure CLI reports InvalidParameter: Required parameter 'projectName' has no value

Scenario 2: @allowed Validation Rejected at Pre-flight

Symptom: ARM returns InvalidTemplateDeployment: Allowed values are dev, stage, prod

Scenario 3: Output References a Property Path That Does Not Exist

Symptom: Bicep compile error referencing an undefined property on the resource object

Interview Questions

Beginner

What is the difference between a param and a var in Bicep?

A param is an external input provided by the caller at deployment time. A var is an internally computed value derived inside the template and not settable by the caller.

When should a parameter have a default value?

When most deployments share a reasonable baseline. Defaults reduce what callers must specify while still allowing targeted overrides, such as a larger VM SKU in prod.

What does @secure() do to a parameter?

It instructs ARM to treat the value as a secret. The value is never logged in deployment history or shown in the portal, and it cannot appear in outputs or variables that flow into unsecured properties.

Why are outputs important in a modular Bicep design?

They return values like subnet IDs and key vault URIs to caller templates so downstream modules can consume them without re-querying Azure or hardcoding resource names.

What happens when a caller passes a value not in @allowed?

Azure rejects the deployment at pre-flight validation before any resource is created, returning a clear error with the allowed values listed.

Intermediate

How do you pass outputs from a networking module to an AKS module?

The networking module declares an output for the subnet ID. The root template captures it as networkingModule.outputs.subnetId and passes it as a parameter to the AKS module declaration.

Why is it unsafe to output a @secure() parameter value?

ARM explicitly blocks this. If you try to assign a secure param to an output, the deployment fails with a clear error. This prevents secrets from appearing in deployment history or being readable via APIs.

How can one Bicep template handle both dev and prod sizing differences?

Use conditional variable expressions: var vmSku = environment == 'prod' ? 'Standard_D4s_v3' : 'Standard_B2s'. A single environment parameter drives all sizing decisions consistently.

When would an object-type param make more sense than individual params?

When a group of settings always travel together, such as network configuration (addressPrefix, subnetName, nsgName). An object param keeps them cohesive and simplifies the module interface.

What does @minLength(3) @maxLength(24) on a storage account name param protect against?

It prevents deployment with an invalid name at pre-flight, before the resource is attempted. Azure storage accounts must be 3-24 characters; catching it early avoids a runtime failure mid-deployment.

Scenario-based

A junior engineer hardcoded the environment name inside the Bicep file. What is the problem and how do you fix it?

A hardcoded value means the same template cannot target multiple environments. Move the environment name to a param with @allowed and a default of 'dev'. The same template now deploys correctly to any environment.

Prod failed because the VNet CIDR conflicted with an existing network. How could params have prevented this?

The address space should be either a parameter or a variable driven by the environment parameter. Each environment gets a distinct CIDR block, eliminating the collision at design time.

A CI/CD pipeline step needs the subnet ID after a Bicep deployment. What is the cleanest pattern?

Declare a subnetId output in the template. After deployment, extract it with az deployment group show and the --query flag. Set it as a pipeline variable for the next step that provisions workloads.

Real-world Usage

Production Bicep projects keep a single main.bicep template alongside per-environment parameter files: dev.parameters.json, stage.parameters.json, prod.parameters.json. The CI/CD pipeline selects the right parameter file based on the branch or environment context. This pattern keeps infrastructure definitions consistent while allowing environment-specific overrides for sizing, SKU tiers, and CIDR blocks.

Summary

Parameters make templates reusable across environments. Variables keep complex expressions DRY and readable. Outputs wire deployments together in modular architectures. Mastering decorators, defaults, and output chaining separates practical Bicep fluency from superficial familiarity.