Bicep Deployment Stack

Table of Contents
Overview
This guide describes a scalable Bicep Deployment Stack designed for efficient infrastructure deployment using Bicep, Azure Deployment Stacks, and GitLab pipelines. The solution is modular and reusable, following best practices for Infrastructure as Code (IaC).
Prerequisites
Before you begin, ensure you have the following:
- An active Azure subscription
- Access to a GitLab instance (self-hosted or gitlab.com)
- Docker installed on your local machine or CI environment
- Basic knowledge of Bicep, Azure Resource Manager (ARM), and CI/CD concepts
- Visual Studio Code
Components
The stack consists of the following components:
1. Azure Subscription
- You need one or more Azure subscriptions (e.g., Dev, Test, Prod).
2. GitLab Runner
- You have to add contributor permissions to your GitLab runner, or use multiple runners for each environment. In my case, I use one App Registration for one Subscription. At large scale, I recommend using a Kubernetes Cluster with Managed Identities for each Subscription.
3. Docker Image with Tooling
- Contains all required tools: Bicep CLI, Azure CLI, PowerShell, and supporting scripts.
- Ensures consistent build and deployment environments across projects.
- Located in the
bicep-base-image
directory.
4. Stack Repository
- Central repository for pipeline definitions and stack versioning.
- Houses the
bice.gitlab-ci.yml
file, which defines the CI/CD pipeline. - Enables version control and traceability for stack changes.
5. Inheritable Pipeline Definition
- The
bice.gitlab-ci.yml
pipeline is designed to be inherited by multiple projects. - Example: The
bicep-virtualmachine-deployment
project can reuse the same pipeline definition for consistent deployments. - Promotes DRY (Don’t Repeat Yourself) principles and simplifies maintenance.
6. Stack Versioning
- Each deployment stack is versioned, allowing controlled rollouts and easy rollbacks.
- Versioning is managed within the stack repository.
How To
- Create an Azure Subscription.
- Create an App Registration with a Client Secret. Add a Role Assignment with
Contributor
to your Subscription. - Add the following GitLab variables to your
bicep-deployment-stack
:- AZURE_TENANT_ID
- AZURE_SUBSCRIPTION_ID
- AZURE_APPLICATION_ID
- AZURE_CLIENT_SECRET (Masked)
- Create three new repositories on GitLab:
- Repository for the base image with the Dockerfile.
- Repository for the deployment stack.
- Repository for the effective deployment of your infrastructure on Azure.
- (Optional): You can create more repositories to deploy different infrastructure use cases.
Dockerfile
This is an example of how the base image for your Bicep deployment stack could look. It can also be used for the VS Code devcontainer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
FROM mcr.microsoft.com/azure-powershell:14.0.0-ubuntu-22.04
RUN apt-get update && \
apt-get upgrade -y && \
apt-get -y install curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
LABEL maintainer="services@webflow.ch"
ARG BICEP_VERSION="0.35.1"
# ARG AZ_RESOURCES_VERSION="7.7.0"
RUN curl -Lo bicep "https://github.com/Azure/bicep/releases/download/v${BICEP_VERSION}/bicep-linux-x64" && \
chmod +x ./bicep && \
mv ./bicep /usr/local/bin/bicep && \
apt-get update
# Install specific PowerShell modules
# RUN pwsh -Command "Set-PSRepository -Name PSGallery -InstallationPolicy Trusted; \
# Install-Module -Name Az.Resources -RequiredVersion '${AZ_RESOURCES_VERSION}' \
# -Scope AllUsers -Verbose -Force"
COPY scripts/ /usr/local/bin
# It is not recommended to use the root user
USER root
Scripts
- Lint-Bicep.ps1 – Lints Bicep files for syntax and best practices.
- Build-Bicep.ps1 – Builds Bicep files into ARM templates.
- Deploy-Bicep.ps1 – Deploys ARM templates to Azure.
- Connect-Azure.ps1 – Authenticates and connects to Azure using a service principal.
Each script is designed to be used within the CI/CD pipeline or locally for development and testing.
Bicep Deployment Stack
Pipeline definitions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# .gitlab-ci.yml
workflow:
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_MERGE_REQUEST_IID
- if: $CI_COMMIT_TAG
stages:
- Publish
variables:
VERSION: "1.0.${CI_PIPELINE_IID}"
gitlab:release:
stage: Publish
image: registry.gitlab.com/gitlab-org/release-cli
script:
- echo "Create release"
release:
name: 'bicep-deployment-stack'
description: 'Release for Bicep deployment stack'
tag_name: $VERSION
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# bicep.gitlab-ci.yml
image: webflowch/bicep-deployment-image:1.2
workflow:
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_MERGE_REQUEST_IID
- if: $CI_COMMIT_TAG
stages:
- build
- test
- deploy
variables:
LOCATION: 'westeurope'
STAGE: 'prod'
bicep-lint:
stage: build
script:
- echo "Linting Bicep files..."
- pwsh -Command "Lint-Bicep.ps1 -Path './src' -Recurse"
rules:
- if: $CI_COMMIT_BRANCH
when: always
- if: $CI_MERGE_REQUEST_ID
build-bicep:
stage: build
script:
- echo "Building Bicep files..."
- pwsh -Command "Build-Bicep.ps1 -File './src/main.bicep' -ParamFile './config/main-${STAGE}-${LOCATION}.bicepparam' -OutPath './artifacts'"
artifacts:
paths:
- ./artifacts
expire_in: 1 hour
when: on_success
rules:
- if: $CI_COMMIT_BRANCH
when: always
- if: $CI_MERGE_REQUEST_ID
deploy-test:
stage: deploy
before_script:
- echo "Login to Azure..."
- pwsh -Command "Connect-Azure.ps1 -TenantId ${AZURE_TENANT_ID} -SubscriptionId ${AZURE_SUBSCRIPTION_ID} -ApplicationId ${AZURE_APPLICATION_ID} -ClientSecret (ConvertTo-SecureString ${AZURE_CLIENT_SECRET} -AsPlainText -Force) -UseServicePrincipal"
script:
- echo "Deploying to test environment..."
- pwsh -Command "Deploy-Bicep.ps1 -DeploymentName 'bicep-deployment-stack' -TemplateFile './artifacts/main.json' -TemplateParameterFile './artifacts/main-${STAGE}-${LOCATION}.parameters.json' -Location '${LOCATION}' -Test"
needs:
- bicep-lint
- build-bicep
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
deploy:
stage: deploy
before_script:
- echo "Login to Azure..."
- pwsh -Command "Connect-Azure.ps1 -TenantId ${AZURE_TENANT_ID} -SubscriptionId ${AZURE_SUBSCRIPTION_ID} -ApplicationId ${AZURE_APPLICATION_ID} -ClientSecret (ConvertTo-SecureString ${AZURE_CLIENT_SECRET} -AsPlainText -Force) -UseServicePrincipal"
script:
- echo "Deploying to production environment..."
- pwsh -Command "Deploy-Bicep.ps1 -DeploymentName 'bicep-deployment-stack' -TemplateFile './artifacts/main.json' -TemplateParameterFile './artifacts/main-${STAGE}-${LOCATION}.parameters.json' -Location '${LOCATION}'"
needs:
- deploy-test
- build-bicep
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Bicep Deployment
Just include the bicep.gitlab-ci.yml
file in your project with the correct version ref
.
1
2
3
4
include:
- project: 'webflow-development/bicep/bicep-deployment-stack'
file: 'bicep.gitlab-ci.yml'
ref: '1.0.41'
Here is the required file structure for the Bicep deployment stack.
│ .gitignore
│ .gitlab-ci.yml
│ README.md
├───.devcontainer
│ devcontainer.json
├───config
│ main-dev-westeurope.bicepparam
│ main-prod-westeurope.bicepparam
│ main-test-westeurope.bicepparam
└───src
│ main.bicep
│
└───modules
tags.bicep
virtualmachine.bicep
Related Links
- Bicep Documentation
- Azure Documentation
- GitLab CI/CD Documentation
- Docker Documentation
- Infrastructure as Code (IaC) Concepts
For more details or questions, feel free to reach out or open an issue in the repository.