Deploy Azure Infrastructure with Bicep

Automate Azure infrastructure deployment using Bicep templates and GitHub Actions.

Introduction

30 Second Summary

In the real world, teams don't click through a cloud portal to create resources one by one. They write code that describes what they need, and a pipeline handles the rest automatically.

In this project, you will write a Bicep template that defines an Azure Storage Account, then build a GitHub Actions workflow that automatically deploys it to Azure whenever you push code changes.

What You'll Build

A fully automated infrastructure deployment pipeline that connects GitHub to Azure using Bicep templates and a CI/CD workflow.

By the end of this project, you'll have:

  • A Bicep template that declaratively provisions an Azure Storage Account with static website hosting.
  • A GitHub Actions workflow that deploys infrastructure automatically on every push to your repository.
  • A service principal with scoped RBAC permissions connecting GitHub to Azure securely.
  • Secret Mission: Add a staging environment with a manual approval gate to your pipeline.

Are there any prerequisites?

This project assumes basic Azure knowledge at an AZ-900 level and familiarity with the Azure Portal. You should also have a GitHub account and an Azure subscription ready to go.

Before We Start

You are building an automated infrastructure deployment pipeline that connects GitHub to Azure. By the end of this project, every push to your repository will automatically deploy Azure infrastructure using a Bicep template.

Before we write any infrastructure code, you need three tools in place: the Azure CLI to communicate with Azure from your terminal, the Bicep extension for VS Code to get syntax highlighting and validation, and a GitHub repository to store your code and trigger the pipeline.

In this step, get ready to:
  • Install Azure CLI and connect it to your Azure subscription.
  • Install the Bicep extension for VS Code.
  • Create and clone a GitHub repository for your project.
Install Azure CLI and verify it connects to your subscription

The Azure CLI is a command-line tool that lets you create and manage Azure resources directly from your terminal. You need it to deploy Bicep templates and create service principals later in this project.

  • Install Azure CLI by following the instructions for your operating system:

macOS

  • Install Azure CLI using Homebrew by running this command:
brew update && brew install azure-cli

What does this command do?

This updates your Homebrew package list and then installs the Azure CLI. Homebrew is the recommended package manager for macOS.

Windows

  • Install Azure CLI using WinGet by running this command in PowerShell:
winget install --exact --id Microsoft.AzureCLI

What does this command do?

WinGet is the Windows Package Manager. The --exact flag ensures you get the official Microsoft Azure CLI package.

  • Close and reopen your terminal after installation so the az command becomes available.
  • Confirm Azure CLI is installed by running:
az --version

✔️ I see a version number

Azure CLI is installed and ready. You should see output showing the azure-cli version along with some extension information.

ⓧ Command not found

The terminal cannot find the az command. Try these fixes:

  • Close and reopen your terminal (or restart VS Code) so PATH changes take effect.
  • On macOS, make sure Homebrew is installed first. Visit brew.sh if you need to install it.
  • On Windows, download the installer directly from https://aka.ms/installazurecliwindows if WinGet is unavailable.
  • Sign in to your Azure subscription by running:
az login

What happens when you run this?

A browser window opens asking you to sign in with your Microsoft account. After you authenticate, the terminal confirms which subscription you are connected to.

If a browser does not open automatically, run az login --use-device-code instead and follow the on-screen instructions.

  • Verify you are connected to the correct subscription by running:
az account show

You should see JSON output showing your subscription name and ID. Confirm the subscription name matches the one you want to use for this project.

Seeing the wrong subscription?

If you have multiple Azure subscriptions, the CLI may default to one you did not intend. Set the correct subscription by running az account set --subscription "Your Subscription Name" with your actual subscription name.

Help me connect Azure CLI to my subscription.

Install the Bicep extension for VS Code

The Bicep extension gives you syntax highlighting, autocompletion, and error detection when editing .bicep files. It catches mistakes before you deploy.

  • Open VS Code.
  • Open the Extensions panel by pressing Ctrl+Shift+X (Windows) or Cmd+Shift+X (macOS).
  • Type Bicep in the search bar.
  • Find the extension published by Microsoft and click Install.
  • Verify Bicep is available in your CLI by running:
az bicep version

✔️ I see a Bicep CLI version

Bicep is ready to go. The Azure CLI bundles a Bicep binary that handles template compilation.

ⓧ Bicep is not installed or not recognized

  • Run the following command to install the Bicep CLI binary through Azure CLI:
az bicep install

After that completes, run az bicep version again to confirm.

What is Bicep?

Bicep is a domain-specific language for deploying Azure resources. It compiles down to ARM templates (JSON) but is much easier to read and write. Think of it as a friendlier syntax layer on top of Azure Resource Manager.

Create a new GitHub repository and clone it locally

Your GitHub repository will store both the Bicep template and the GitHub Actions workflow file. Every push to this repository will trigger your deployment pipeline.

  • Open your browser and go to github.com/new.
  • Enter azure-bicep-pipeline as the repository name.
  • Leave the repository set to Public.
  • Check the box to Add a README file.
  • Click Create repository.

Why public?

GitHub Environments with protection rules (used in the Secret Mission) are free for public repositories. Private repositories require a paid GitHub plan for this feature.

  • On your new repository page, click the green Code button and copy the HTTPS URL.
  • Open your terminal and move to your Desktop by running:
cd ~/Desktop
  • Clone the repository by running:
git clone [[REPO_URL="https://github.com/YOUR_USERNAME/azure-bicep-pipeline.git"]]
  • Move into the cloned folder by running:
cd azure-bicep-pipeline
  • Open the folder in VS Code by running:
code .

VS Code opens with your azure-bicep-pipeline folder loaded. You should see the README.md file in the Explorer sidebar.

code command not found?

If the code command is not recognized, open VS Code manually, then press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows). Type Shell Command: Install 'code' command in PATH and select it. Then restart your terminal and try again.

Help me open my repository in VS Code.

Your environment is ready. Next up, you will write a Bicep template that defines your Azure infrastructure as code.

Write Your Bicep Template

Your tools are ready and your repository is waiting for code. Now it's time to define the infrastructure you want Azure to build for you.

Bicep is Azure's domain-specific language for declaring infrastructure. Instead of clicking through the Azure Portal to create resources, you write a template that describes what you want. Azure reads the template and builds everything for you.

In this step, get ready to:
  • Write a Bicep template that defines a storage account with static website hosting.
  • Validate the template for syntax errors and against Azure.
  • Deploy the template manually to confirm it works.
Create main.bicep with parameterized storage account configuration

A Bicep template has three main sections: parameters (inputs you can customize), resources (the infrastructure to create), and outputs (values to surface after deployment).

  • In VS Code's Explorer sidebar, right-click the empty area and select New File.
  • Name the file main.bicep and press Enter.

The file opens in the editor. You'll add the template in three chunks, starting with the parameters.

  • Define the template parameters and a computed variable by pasting this code into main.bicep:
@description('The prefix to use for the storage account name.')
@minLength(3)
@maxLength(11)
param storagePrefix string

@description('The SKU for the storage account.')
@allowed([
  'Standard_LRS'
  'Standard_GRS'
  'Standard_RAGRS'
])
param storageSKU string = 'Standard_LRS'

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

var uniqueStorageName = '${storagePrefix}${uniqueString(resourceGroup().id)}'

What does this code do?

  • storagePrefix is a required input between 3 and 11 characters. Azure storage account names must be globally unique, so you combine a short prefix with a hash.
  • storageSKU controls the replication strategy. The @allowed decorator restricts it to three valid options, with Standard_LRS (locally redundant) as the default.
  • location defaults to whatever region the resource group is in, so the storage account lands in the same region.
  • uniqueStorageName concatenates your prefix with a deterministic hash of the resource group ID, guaranteeing a globally unique name.

You won't see a visible change yet. The next chunk uses these parameters to declare the actual storage account resource.

  • Below the var uniqueStorageName line, add the storage account resource:
resource storageAccount 'Microsoft.Storage/storageAccounts@2025-08-01' = {
  name: uniqueStorageName
  location: location
  kind: 'StorageV2'
  sku: {
    name: storageSKU
  }
  properties: {
    supportsHttpsTrafficOnly: true
  }
}

What does this code do?

  • resource storageAccount declares a symbolic name you can reference elsewhere in the template.
  • The API version Microsoft.Storage/storageAccounts@2025-08-01 pins the resource to a specific Azure API contract.
  • kind: 'StorageV2' is required for static website hosting.
  • supportsHttpsTrafficOnly: true enforces encrypted connections, a security best practice.

You won't see a change yet. The next chunk enables static website hosting on this storage account and defines the template outputs.

  • Below the closing } of the storageAccount resource, add the blob service resource and outputs:
resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2025-08-01' = {
  parent: storageAccount
  name: 'default'
  properties: {
    staticWebsite: {
      enabled: true
      indexDocument: 'index.html'
      errorDocument404Path: '404.html'
    }
  }
}

output storageAccountName string = storageAccount.name
output staticWebsiteUrl string = storageAccount.properties.primaryEndpoints.web

What does this code do?

  • blobService is a child resource of the storage account. The parent: storageAccount line establishes that relationship.
  • The staticWebsite property enables Azure's free static website hosting feature, pointing to index.html as the default page.
  • The two output lines surface the generated storage account name and the public website URL after deployment.
  • Save main.bicep by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Check the editor for red or yellow squiggly underlines. The Bicep extension validates syntax in real time, so no underlines means your template is well-formed.

Seeing red underlines in the editor?

  • Check for missing closing braces }. Each opening brace must have a matching close.
  • Make sure the parent: storageAccount line references the exact symbolic name from the storage account resource above.
  • Verify the Bicep extension is installed. Press Ctrl+Shift+X (Windows) or Cmd+Shift+X (macOS) and search for "Bicep" to confirm.

Why is my Bicep file showing syntax errors in VS Code?

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on to validation.

ⓧ I'd like to double check the full code

Here is the complete main.bicep file. Compare it against yours line by line.

@description('The prefix to use for the storage account name.')
@minLength(3)
@maxLength(11)
param storagePrefix string

@description('The SKU for the storage account.')
@allowed([
  'Standard_LRS'
  'Standard_GRS'
  'Standard_RAGRS'
])
param storageSKU string = 'Standard_LRS'

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

var uniqueStorageName = '${storagePrefix}${uniqueString(resourceGroup().id)}'

resource storageAccount 'Microsoft.Storage/storageAccounts@2025-08-01' = {
  name: uniqueStorageName
  location: location
  kind: 'StorageV2'
  sku: {
    name: storageSKU
  }
  properties: {
    supportsHttpsTrafficOnly: true
  }
}

resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2025-08-01' = {
  parent: storageAccount
  name: 'default'
  properties: {
    staticWebsite: {
      enabled: true
      indexDocument: 'index.html'
      errorDocument404Path: '404.html'
    }
  }
}

output storageAccountName string = storageAccount.name
output staticWebsiteUrl string = storageAccount.properties.primaryEndpoints.web
Validate the template locally using Azure CLI

Before deploying to Azure, you can catch errors early by validating your template in two stages: a local syntax check and a remote validation against the Azure API.

  • Check for syntax errors by running this command in your terminal:
az bicep build --file main.bicep

What does this command do?

This compiles your Bicep template into a JSON ARM template. If there are syntax errors, you will see them printed in the terminal with file paths and line numbers. If the command completes silently with no output, your template is valid.

It also creates a main.json file in the same directory. That's the compiled ARM template Azure will ultimately use.

  • If the command returned no error output, your syntax is correct. You should also see a new main.json file appear in your Explorer sidebar.

Seeing syntax errors?

  • Read the error message carefully. It includes the line number and a description of what went wrong.
  • Make sure you are running the command from the same directory that contains main.bicep. Run ls to confirm the file is there.

Help me fix my Bicep build errors

The local build confirms syntax is valid. Now let's validate against Azure to ensure the resource configuration is accepted.

  • First, create a resource group to deploy into by running this command:
az group create --name [[RG_NAME="your resource group name"]] --location eastus

What is a resource group?

A resource group is a container that holds related Azure resources. It makes it easy to manage and delete everything at once when you're finished.

  • You should see JSON output in your terminal with "provisioningState": "Succeeded", confirming the resource group was created.
  • Now validate your template against Azure by running:
az deployment group validate --resource-group [[RG_NAME="your resource group name"]] --template-file main.bicep --parameters storagePrefix=[[STORAGE_PREFIX="your prefix (3-11 lowercase chars)"]]

What does this command do?

This sends your template to Azure for validation without actually deploying anything. Azure checks that the resource types, API versions, and property values are all valid against the real API. It catches configuration issues that a local syntax check cannot.

  • If validation passes, you will see JSON output describing the deployment properties. No error messages means you are ready to deploy.

Validation failed?

  • If you see a "resource group not found" error, double-check the resource group name matches what you created in the previous command.
  • If the error mentions storagePrefix, make sure your prefix value is between 3 and 11 lowercase characters with no special characters.
  • If your login has expired, run az login again to re-authenticate.

Help me fix my Bicep validation error

Deploy the template manually to confirm it works

Validation passed, which means Azure accepted your template definition. Now let's deploy it for real to see the storage account appear in your subscription.

  • Deploy your Bicep template to Azure by running:
az deployment group create --resource-group [[RG_NAME="your resource group name"]] --template-file main.bicep --parameters storagePrefix=[[STORAGE_PREFIX="your prefix (3-11 lowercase chars)"]]

What does this command do?

This tells Azure to actually provision the resources described in your template. Azure reads the Bicep file, converts it to ARM JSON internally, and creates the storage account with all the properties you specified.

The command may take 30-60 seconds to complete. When it finishes, you will see a large JSON response with deployment details.

  • Wait for the command to finish. Look for "provisioningState": "Succeeded" in the JSON output.
  • In the JSON output, scroll to the outputs section and note the staticWebsiteUrl value. This is the public URL for your static website.

Now let's verify the deployment visually in the Azure Portal.

  • Open the Azure Portal in your browser.
  • In the search bar at the top, type Storage accounts and select Storage accounts from the results.
  • Click on the storage account that starts with your prefix.
  • In the left sidebar, scroll down to the Data management section and click Static website.
  • Confirm that static website hosting shows as Enabled with index.html as the index document.

Don't see the storage account in the Portal?

  • Make sure you are looking at the correct subscription. Click your profile icon in the top right to verify.
  • Check that the az deployment group create command completed without errors. If the terminal shows a failure, re-read the error message for the specific issue.
  • Try refreshing the Portal page. Newly created resources can take a moment to appear.

Help me find my deployed storage account in Azure

Your Bicep template is proven and deployed. You have a storage account with static website hosting live in Azure. Next up, you'll create a service principal so GitHub Actions can deploy this template automatically without your manual intervention.

Create a Service Principal for GitHub Actions

In the last step, you deployed your Bicep template manually using the Azure CLI. Now it's time to automate that process so GitHub Actions can deploy infrastructure on your behalf whenever you push code.

For GitHub Actions to interact with Azure, it needs an identity. A service principal is an identity created specifically for automated tools and applications. You will create one, scope its permissions to your resource group, and store its credentials securely in GitHub.

In this step, get ready to:
  • Create a service principal with Contributor role scoped to your resource group.
  • Store the service principal credentials as GitHub repository secrets.
Create a service principal scoped to your resource group

A service principal with scoped permissions follows the principle of least privilege. Instead of giving GitHub Actions access to your entire Azure subscription, you limit it to only the resource group it needs.

  • First, retrieve your Azure subscription ID by running this command:
az account show --query id -o tsv

What does this command do?

The --query id flag extracts just the subscription ID from the full account details. The -o tsv flag outputs it as plain text with no quotes, making it easy to copy.

  • Copy the subscription ID that appears in your terminal. You will need it in the next command.
  • Create the service principal by running this command, replacing the placeholder values with your own subscription ID and resource group name:
az ad sp create-for-rbac --name "github-actions-deploy" --role contributor --scopes /subscriptions/[[SUBSCRIPTION_ID="your subscription ID"]]/resourceGroups/[[RESOURCE_GROUP="your resource group name"]] --sdk-auth

What does this command do?

This command creates a new service principal named github-actions-deploy in your Azure Active Directory.

  • The --role contributor flag gives it permission to create and manage resources (but not assign roles).
  • The --scopes flag limits those permissions to your specific resource group only.
  • The --sdk-auth flag formats the output as JSON that the azure/login GitHub Action expects.
  • You should see a JSON block printed in your terminal containing fields like clientId, clientSecret, subscriptionId, and tenantId.
  • Copy the entire JSON output (including the curly braces). You will paste it into GitHub in the next substep.

Protect these credentials

This JSON contains a clientSecret that grants access to your Azure resources. Do not commit it to source control or share it publicly.

🙋‍♀️ Seeing a deprecation warning?

You may see a message saying Option '--sdk-auth' has been deprecated and will be removed in a future release. This is expected. The command still works correctly and produces the JSON output you need.

If you see an error about an invalid scope, double-check that your subscription ID and resource group name are correct. Run az account show --query id -o tsv again to confirm.

help me troubleshoot the az ad sp create-for-rbac command failing

Store the credentials as a GitHub secret

GitHub needs these credentials to authenticate with Azure during workflow runs. Repository secrets are encrypted and only exposed to GitHub Actions workflows at runtime.

  • Open your azure-bicep-pipeline repository on GitHub in your browser.
  • Click the Settings tab in the top navigation bar of your repository.
  • In the left sidebar, click Secrets and variables, then click Actions.
  • Click the New repository secret button.
  • In the Name field, enter AZURE_CREDENTIALS.
  • In the Secret field, paste the entire JSON output you copied from the terminal.
  • Click Add secret.

Why store the full JSON?

The azure/login@v2 GitHub Action expects the creds parameter to contain a JSON object with clientId, clientSecret, tenantId, and subscriptionId fields. The --sdk-auth flag produced exactly this format.

  • Click New repository secret again.
  • In the Name field, enter AZURE_RG.
  • In the Secret field, enter your resource group name (the same one you used in the service principal command).
  • Click Add secret.
  • Click New repository secret one more time.
  • In the Name field, enter AZURE_SUBSCRIPTION.
  • In the Secret field, enter your subscription ID (the one you retrieved earlier).
  • Click Add secret.

You should now see three secrets listed on the Actions secrets page: AZURE_CREDENTIALS, AZURE_RG, and AZURE_SUBSCRIPTION.

Don't see three secrets listed?

Make sure you are in the correct repository (azure-bicep-pipeline). If you navigated to a different repo's settings by mistake, your secrets will not be available to your workflow.

Also confirm you clicked New repository secret (not New environment secret). Repository secrets are available to all workflows in the repo.

help me set up my GitHub repository secrets for Azure deployment

Your authentication bridge between GitHub and Azure is in place. Next, you will write the GitHub Actions workflow that uses these secrets to deploy your Bicep template automatically.

Build Your GitHub Actions Workflow

Your service principal is ready and your secrets are stored in GitHub. Now it's time to connect everything with a CI/CD pipeline that deploys your Bicep template automatically.

GitHub Actions uses workflow files written in YAML to define automated pipelines. You will create a workflow that triggers on every push to your main branch, authenticates with Azure using your service principal, and deploys your Bicep template.

In this step, get ready to:
  • Create a GitHub Actions workflow file in your repository.
  • Configure the workflow to authenticate with Azure and deploy your Bicep template.
  • Push your code and verify the pipeline deploys infrastructure automatically.
Create the workflow YAML file

GitHub Actions looks for workflow files inside a .github/workflows/ directory in your repository. Any .yml file placed there becomes an automated pipeline.

  • Create the required directory structure by running this command in your VS Code terminal:
mkdir -p .github/workflows

macOS

The -p flag creates any missing parent directories automatically.

Windows

If you are using PowerShell, run mkdir .github/workflows instead. PowerShell creates intermediate directories automatically without a flag.

  • Confirm the directory was created by running:
ls .github/workflows

You should see an empty directory with no errors.

  • Create a new file named deploy.yml inside the .github/workflows/ directory.
  • In VS Code's Explorer sidebar, right-click the workflows folder and select New File.
  • Name it deploy.yml.

Why .github/workflows?

This directory path is a GitHub convention. GitHub automatically detects any .yml or .yaml file placed here and registers it as a workflow in the Actions tab.

Configure the deployment steps using azure/arm-deploy

Your workflow needs three actions: check out the code, log into Azure, and deploy the Bicep template. Each action is a pre-built step maintained by GitHub or Microsoft.

  • Paste the following YAML into your deploy.yml file:
name: Deploy Bicep to Azure
on:
  push:
    branches:
      - main

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Log into Azure
        uses: azure/login@v2
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Deploy Bicep template
        uses: azure/arm-deploy@v2
        with:
          subscriptionId: ${{ secrets.AZURE_SUBSCRIPTION }}
          resourceGroupName: ${{ secrets.AZURE_RG }}
          template: ./main.bicep
          parameters: storagePrefix=biceplab
          failOnStdErr: false

What does this workflow do?

  • name: sets the display name shown in the GitHub Actions tab.
  • on: push: branches: - main triggers the workflow every time code is pushed to the main branch.
  • actions/checkout@v4 pulls your repository code onto the runner so subsequent steps can access your files.
  • azure/login@v2 authenticates with Azure using the service principal credentials stored in AZURE_CREDENTIALS.
  • azure/arm-deploy@v2 deploys your Bicep template to the specified resource group, passing storagePrefix=biceplab as a parameter.

💡 Why failOnStdErr: false?

Azure CLI sometimes writes informational messages to stderr even on success. Setting failOnStdErr: false prevents the workflow from failing on these harmless warnings.

  • Save deploy.yml.

YAML indentation errors?

YAML is whitespace-sensitive. Make sure you use spaces (not tabs) and that each nested level is indented by exactly 2 spaces. The Bicep extension in VS Code highlights YAML syntax issues if you have a YAML language extension installed.

help me fix YAML indentation errors in my GitHub Actions workflow

✔️ Awesome, I've got everything!

Great. Double check you have saved deploy.yml before moving on.

ⓧ I'd like to double check the full code

name: Deploy Bicep to Azure
on:
  push:
    branches:
      - main

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Log into Azure
        uses: azure/login@v2
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Deploy Bicep template
        uses: azure/arm-deploy@v2
        with:
          subscriptionId: ${{ secrets.AZURE_SUBSCRIPTION }}
          resourceGroupName: ${{ secrets.AZURE_RG }}
          template: ./main.bicep
          parameters: storagePrefix=biceplab
          failOnStdErr: false
Push and verify the pipeline runs successfully

Your workflow triggers on push to main. Committing and pushing your files will kick off the pipeline automatically.

  • Before pushing, confirm that main.bicep is in the root of your repository folder (the same level as the .github directory). Verify by running:
ls main.bicep

You should see main.bicep printed with no errors. If the workflow can't find this file, the deployment will fail.

main.bicep not found?

  • If you created main.bicep in a different folder earlier (for example, a separate project directory on your Desktop), copy it into your repository root now:
  • Run cp ~/Desktop/azure-bicep-pipeline/main.bicep . (macOS/Linux) or copy the file using File Explorer (Windows).
  • Make sure you are inside your repository folder when you run ls. Run pwd to check your current directory.

Help me get main.bicep into my GitHub repository

  • Commit all files and push to GitHub by running these commands:
git add .
git commit -m "Add Bicep template and GitHub Actions workflow"
git push origin main

What do these commands do?

  • git add . stages all new and modified files (main.bicep and deploy.yml).
  • git commit saves a snapshot of your staged files with a descriptive message.
  • git push origin main uploads your commit to GitHub, which triggers the workflow.
  • Open your repository on GitHub in your browser.
  • Click the Actions tab at the top of the repository page.
  • You should see a workflow run named Add Bicep template and GitHub Actions workflow (matching your commit message).
  • Click on it to watch the job progress in real time.

The workflow takes about 1-2 minutes to complete. A green checkmark next to the run means the deployment succeeded.

  • Verify the deployment in the Azure Portal by navigating to your resource group.
  • Open the Azure Portal and go to Resource groups.
  • Select your resource group and confirm you see a storage account whose name starts with biceplab.

Workflow failed with a red X?

Click on the failed run, then click the build-and-deploy job to see detailed logs. Common causes:

  • The AZURE_CREDENTIALS secret was not pasted correctly. Go to Settings > Secrets and variables > Actions and update the secret with the full JSON output.
  • The resource group name in AZURE_RG does not match the actual resource group you created. Check for typos.
  • The deploy.yml file is not inside .github/workflows/. Verify the folder structure in your repository.

help me debug my failed GitHub Actions workflow for Bicep deployment

Your pipeline is live. Next up, you will make a change to your Bicep template and watch the pipeline redeploy your infrastructure automatically.

Trigger a Change and Observe the Pipeline

Your pipeline is live and proven. Now it is time to see it respond to a real infrastructure change.

In production, teams constantly update infrastructure configuration. They add metadata tags for cost tracking, upgrade replication for durability, or scale resources up. The pipeline you built ensures every change flows through version control and deploys automatically. In this step, you will modify your Bicep template and watch GitHub Actions pick up the change without any manual intervention.

In this step, get ready to:
  • Update the Bicep template with an environment tag and a new default SKU.
  • Push the change and verify the pipeline deploys the update to Azure automatically.
Modify the Bicep template to add a tag and change the SKU

You will make two changes to main.bicep. First, you will change the default replication strategy from Standard_LRS to Standard_GRS. Second, you will add an environment tag to the storage account resource.

  • Switch back to VS Code with your azure-bicep-pipeline repository open.
  • Open main.bicep and find this line near the top of the file:
param storageSKU string = 'Standard_LRS'
  • Replace 'Standard_LRS' with 'Standard_GRS', so the line now looks like this:
param storageSKU string = 'Standard_GRS'

What does this change do?

Standard_LRS stores three copies of your data in a single data center. Standard_GRS adds geo-redundancy by replicating your data to a secondary region hundreds of miles away. This is a common production upgrade for disaster recovery.

  • Now scroll down to the storageAccount resource declaration. Find the closing brace of the sku block:
  sku: {
    name: storageSKU
  }
  • Directly below the sku block's closing brace, add the following tags block:
  tags: {
    environment: 'production'
  }

What do tags do in Azure?

Tags are key-value pairs that help you organize and track Azure resources. Teams use them for cost allocation, environment identification, and governance policies.

By defining tags in your Bicep template, you ensure every deployment automatically applies consistent metadata. No manual tagging required.

  • Save main.bicep.

Seeing red squiggles in VS Code?

Make sure the tags block sits between the sku closing brace and the properties block. Check that you have matching curly braces and that indentation uses two spaces.

help me fix a Bicep syntax error in my tags block

✔️ Awesome, I've got everything!

Great. Double check you have saved main.bicep before moving on.

ⓧ I'd like to double check the full code

Here is what your full main.bicep should look like after both edits:

Your file has three chunks you built earlier. Scroll up and confirm each one is present, in this order:

  1. Parameters section (storagePrefix, storageSKU with default 'Standard_GRS', location) and the uniqueStorageName variable.
  2. The storageAccount resource with sku, tags (with environment: 'production'), and properties.
  3. The blobService resource with staticWebsite enabled, followed by the two output lines.
Push the change and verify the pipeline updates the resource

With your template updated, it is time to commit and push. The pipeline will detect the change on the main branch and deploy it to Azure automatically.

  • Commit and push your changes by running these commands in your terminal:
git add .
git commit -m "Add environment tag and upgrade SKU to Standard_GRS"
git push

What happens when you push?

Your workflow file has on: push: branches: - main as its trigger. The moment GitHub receives your push, it launches the build-and-deploy job. That job logs into Azure, then runs azure/arm-deploy@v2 with your updated Bicep template.

Azure compares the new desired state (GRS replication, environment tag) against the existing resource and applies only the differences.

  • Open your GitHub repository in your browser.
  • Click the Actions tab at the top of the repository page.
  • You should see a new workflow run triggered by your latest commit. Click on it to watch the progress.
  • Wait for the workflow run to show a green checkmark, confirming it completed successfully.

Workflow failed with a red X?

Open the failed run and expand the Deploy Bicep template step to read the error message. Common causes include a typo in the tags block (missing colon or mismatched braces) or the storageSKU default not matching one of the values in the @allowed list.

help me debug a failed GitHub Actions Bicep deployment

  • Now verify the changes in the Azure Portal. Navigate to your storage account.
  • On the storage account's Overview page, look for existing tags in the overview section. You should see environment: production listed.
  • Under Data management, select Redundancy. Confirm the replication setting now shows Geo-redundant storage (GRS).

Why is this powerful?

You changed two lines in a template file, pushed to Git, and your live Azure infrastructure updated automatically. No portal clicks, no manual CLI commands, no room for human error. This is exactly the workflow that AZ-400 tests: source-controlled infrastructure flowing through a CI/CD pipeline.

Secret mission

Add a Staging Environment with Manual Approval

Your pipeline deploys to one environment. Real teams deploy to staging first, then promote to production only after someone approves. Can you split your pipeline into two environments with a manual approval gate between them?

Nice Work!

Nice Work!

You just built a fully automated infrastructure deployment pipeline from scratch. Every push to your repository now provisions real Azure resources through code, exactly the way production teams operate.

Here's what you accomplished:

  • Wrote a Bicep template that declaratively defines Azure infrastructure, including a storage account with static website hosting and parameterized configuration.
  • Created a service principal with least-privilege RBAC scoping and managed secrets securely in GitHub.
  • Built a GitHub Actions CI/CD pipeline that automatically deploys Bicep templates to Azure on every push to the main branch.
  • Secret Mission: Implemented environment-based deployment with a staging and production split, connected by a manual approval gate.

Ready to quiz yourself?