Post

Buildkite CI/CD Pipelines for Azure From CLI Login to Full Bicep Deployments

How to build Buildkite CI/CD pipelines for Azure from Azure CLI authentication to full Bicep template deployments with lint, what-if, approval gates, and multi-scope support.

Buildkite CI/CD Pipelines for Azure From CLI Login to Full Bicep Deployments

Howdy Folks,

It’s been some time since I last delved into CI/CD tooling outside the usual suspects Azure DevOps Pipelines and GitHub Actions. But recently, I had a scenario that pushed me towards Buildkite, and I have to say, the experience was genuinely refreshing.

Here’s the situation: I had existing GitHub Actions workflows deploying Azure Landing Zones with Bicep. They worked great. But what if you need your CI/CD agent to run on your own infrastructure inside a private network, behind a firewall, on a machine you fully control? GitHub-hosted runners can’t always reach those private endpoints, and self-hosted GitHub Actions runners come with their own quirks. Buildkite is purpose-built for exactly this pattern.

In this post, I’ll walk you through two production-ready Buildkite pipelines I built for Azure:

  1. Pipeline 1 Azure CLI Login (the foundation)
  2. Pipeline 2 Full Bicep Template Deployment (lint → build → what-if → approve → deploy)

All the code is open-source and available in my GitHub repository. So let’s cut to the chase.


What is Buildkite and Why Should You Care?

Buildkite is a CI/CD platform that splits responsibilities in a way that’s fundamentally different from fully hosted platforms:

  • Buildkite SaaS orchestrates builds, stores pipeline definitions, and shows build results in the web UI
  • Buildkite Agent a small process you run on your own machines (VMs, containers, laptops) that picks up and executes jobs

This means your code, credentials, and sensitive data never leave your environment. For Azure deployments that need access to private VNets, on-premises resources, or sensitive credentials, this is a genuine advantage.

Buildkite Free Tier

Buildkite has a generous free plan no credit card required:

FeatureFree Plan
UsersUp to 3
PipelinesUnlimited
BuildsUnlimited
AgentsUnlimited (self-hosted)
Build History90 days
SecretsIncluded
Clusters1

The free plan doesn’t include SSO, audit logs, or priority support. For teams larger than 3 or with enterprise compliance requirements, you’ll need a paid plan.


Design Principles Scripts Over Inline YAML

Before I show you the pipelines, let me explain the design philosophy. This is important because it’s what makes these pipelines maintainable and reusable.

All logic lives in .sh files under .buildkite/scripts/. The pipeline YAML only defines step order and calls scripts. This gives you:

  • No $$ escaping of bash variables inside YAML if you’ve done this before, you know the pain
  • No heredocs or multi-line string gymnastics
  • Local testing every script can be tested with bash .buildkite/scripts/script-name.sh
  • Reusability the Bicep scripts are driven entirely by environment variables. To deploy a different template, create a new pipeline YAML and change the env vars no script changes needed

Each step is self-contained. Buildkite steps run in completely fresh environments nothing installed or set in one step persists to the next. So every step that needs Azure CLI re-runs the install and login scripts. They’re idempotent and complete quickly if the tool is already present.


Repository Structure

Here’s the full layout of the azure-buildkite-pipelines repository:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.
├── .buildkite/
│   ├── pipeline.yml                       # Pipeline 1: Azure CLI install + login
│   ├── pipeline-with-fallback.yml         # Pipeline 1 variant: env var fallback
│   ├── pipeline-bicep-landing-zone.yml    # Pipeline 2: Bicep lint, build, what-if, deploy
│   └── scripts/
│       ├── install-az.sh                  # Installs Azure CLI (shared, idempotent)
│       ├── az-login.sh                    # Authenticates via Buildkite Secrets
│       ├── az-login-with-fallback.sh      # Authenticates with env var fallback
│       ├── verify-access.sh               # Lists account and resource groups
│       ├── bicep-install.sh               # Installs Bicep CLI
│       ├── bicep-build.sh                 # Lints and compiles Bicep, outputs to deploy/
│       ├── bicep-whatif.sh                # What-if across all 4 deployment scopes
│       └── bicep-deploy.sh               # Deploy across all 4 deployment scopes
├── bicep/
│   └── main.bicep                         # Sample Bicep template
│   └── main.bicepparam                    # Sample Bicep parameter file
└── sample/
    ├── landing-zone.yml                   # Original GitHub Actions workflow (reference)
    ├── build-action.yml                   # Original build action (reference)
    └── deploy-action.yml                  # Original deploy action (reference)

The sample/ folder contains the original GitHub Actions workflows these pipelines were converted from useful for comparison if you’re migrating from Actions to Buildkite.


Authentication Service Principal with Buildkite Secrets

These pipelines authenticate to Azure using a service principal with a client secret. Credentials are stored in Buildkite Secrets, which are encrypted at rest and injected at runtime by the agent they are never exposed in logs or pipeline YAML.

Step 1 Create a Service Principal in Azure

1
2
3
4
az ad sp create-for-rbac \
  --name "buildkite-agent" \
  --role Contributor \
  --scopes /subscriptions/{your-subscription-id}

The output will look like this:

1
2
3
4
5
{
  "appId":       "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "password":    "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "tenant":      "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

Map these values to secrets:

  • appIdAZURE_CLIENT_ID
  • passwordAZURE_CLIENT_SECRET
  • tenantAZURE_TENANT_ID

For Bicep deployments at subscription scope that assign roles (e.g. landing zone patterns), the service principal also needs the Owner role, or at minimum Contributor plus User Access Administrator.

Step 2 Store Credentials as Buildkite Secrets

  1. In Buildkite, go to your pipeline
  2. Click SettingsSecrets
  3. Add each secret:
Secret NameValue
AZURE_CLIENT_IDService principal App ID
AZURE_CLIENT_SECRETService principal password
AZURE_TENANT_IDAzure AD tenant ID

At runtime, scripts retrieve these using:

1
AZURE_CLIENT_ID=$(buildkite-agent secret get AZURE_CLIENT_ID)

The agent fetches the value from the Buildkite API over a local socket the secret is never written to disk or printed to stdout.

Buildkite Secrets require agent v3.27.0 or higher. Check your version with buildkite-agent --version.

A Note on OIDC

The original GitHub Actions workflows used OpenID Connect (OIDC) for passwordless authentication no client secret stored anywhere; Azure trusts GitHub’s identity token directly.

Buildkite also supports OIDC. However, getting Buildkite OIDC to work with Azure requires several non-trivial steps

Documentation was getting updated by the time I write this post


Pipeline 1 Azure CLI Login

This is the foundation pipeline. It installs Azure CLI on the agent and authenticates with Azure using the service principal credentials stored in Buildkite Secrets.

Pipeline Flow

flowchart LR
    A["Step 1\nInstall Azure CLI\n& Login"] --> B[Wait]
    B --> C["Step 2\nVerify Azure\nAccess"]

Each step re-runs install-az.sh and az-login.sh because Buildkite steps run in completely isolated environments nothing persists between them.

The Pipeline YAML

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
agents:
  queue: "default"

steps:
  - label: ":azure: Install Azure CLI & Login"
    key: "azure-setup"
    command:
      - "bash .buildkite/scripts/install-az.sh"
      - "bash .buildkite/scripts/az-login.sh"
    retry:
      automatic:
        - exit_status: "*"
          limit: 2
    timeout_in_minutes: 10

  - wait

  - label: ":white_check_mark: Verify Azure Access"
    key: "verify-access"
    command:
      - "bash .buildkite/scripts/install-az.sh"
      - "bash .buildkite/scripts/az-login.sh"
      - "bash .buildkite/scripts/verify-access.sh"
    soft_fail: true
    timeout_in_minutes: 5

Notice how clean the YAML is no inline bash, no variable escaping, just script calls. The retry block gives the setup step two automatic retries (useful for transient network issues during CLI installation), and the verify step uses soft_fail: true so a listing failure won’t block the pipeline.

Key Scripts

install-az.sh Detects the agent’s OS and installs Azure CLI accordingly. Supports Debian/Ubuntu, RHEL/CentOS, and macOS. Skips installation if az is already present:

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
#!/bin/bash
set -euo pipefail

echo "--- :package: Installing Azure CLI"

if command -v az >/dev/null 2>&1; then
  echo "✅ Azure CLI already installed: $(az --version | head -1)"
  exit 0
fi

echo "Azure CLI not found, installing..."

if [ -x "$(command -v apt-get)" ]; then
  echo "Detected Debian/Ubuntu"
  # ... apt-get install azure-cli
elif [ -x "$(command -v yum)" ]; then
  echo "Detected RHEL/CentOS"
  # ... yum install azure-cli
elif [ -x "$(command -v brew)" ]; then
  echo "Detected macOS"
  brew update && brew install azure-cli
else
  echo "❌ Unsupported OS"
  exit 1
fi

az-login.sh Fetches credentials from Buildkite Secrets, runs az login --service-principal, and creates a Buildkite build annotation showing the authenticated account:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/bash
set -euo pipefail

echo "--- :key: Retrieving Azure credentials from Buildkite Secrets"

AZURE_CLIENT_ID=$(buildkite-agent secret get AZURE_CLIENT_ID)
AZURE_CLIENT_SECRET=$(buildkite-agent secret get AZURE_CLIENT_SECRET)
AZURE_TENANT_ID=$(buildkite-agent secret get AZURE_TENANT_ID)

echo "--- :azure: Logging in to Azure"

az login --service-principal \
  --username "${AZURE_CLIENT_ID}" \
  --password "${AZURE_CLIENT_SECRET}" \
  --tenant "${AZURE_TENANT_ID}" \
  --output none

echo "✅ Login successful"

ACCOUNT_NAME=$(az account show --query name -o tsv)
SUBSCRIPTION_ID=$(az account show --query id -o tsv)

echo "Account:         ${ACCOUNT_NAME}"
echo "Subscription ID: ${SUBSCRIPTION_ID}"

verify-access.sh A simple verification that runs az account show and az group list, then annotates the build:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
set -euo pipefail

echo "--- :mag: Verifying Azure access"

echo "Current account:"
az account show --output table

echo ""
echo "Available resource groups:"
az group list --output table

RG_COUNT=$(az group list --query "length([])" --output tsv)

buildkite-agent annotate --style info --context azure-verify <<EOF
### :mag: Azure Access Verified

**Resource Groups Found:** ${RG_COUNT}

You can now use Azure CLI in subsequent pipeline steps.
EOF

Pipeline Variants

PipelineWhen to Use
pipeline.ymlBuildkite Secrets are configured. Fails immediately if any secret is missing.
pipeline-with-fallback.ymlTesting without secrets, or migrating from environment variables. Falls back to env vars if secrets are unavailable.

Pipeline 2 Bicep Template Deployment

This is where it gets interesting. A full CI/CD pipeline for deploying Azure Bicep templates, converted from the GitHub Actions workflows in the sample/ folder. It supports all four Azure deployment scopes: subscription, tenant, management group, and resource group.

Pipeline Flow

flowchart TD
    A["🔧 Block Step\nConfigure Deployment\n(parameter file, subscription, location)"]
    A --> B["🔨 Lint & Build\naz bicep lint → build → build-params\nUpload deploy/ artifact"]
    B --> C[Wait]
    C --> D["🔍 What-If Deploy\nDownload artifact → az deployment what-if\nShows what would change"]
    D --> E[Wait]
    E --> F{"🚀 Approve Deployment\n(main branch only)"}
    F --> G["☁️ Deploy to Azure\nDownload artifact → az deployment create\n(main branch only)"]

The Pipeline YAML

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
72
73
74
75
76
77
env:
  BICEP_TEMPLATE_FILE_PATH: "bicep/main.bicep"
  BICEP_DEPLOYMENT_NAME: "deploy_bicep"
  AZURE_DEPLOYMENT_TYPE: "subscription"

steps:
  # Step 1: User selects deployment parameters
  - block: ":gear: Configure Deployment"
    prompt: "Select the deployment parameters for the landing zone"
    fields:
      - select: "Parameter File"
        key: "parameter_file_path"
        required: true
        options:
          - label: "Workload 1"
            value: "bicep/main.bicepparam"

      - select: "Target Subscription"
        key: "subscription_id"
        required: true
        options:
          - label: "sample-subscription-1"
            value: ""

      - select: "Location"
        key: "location"
        required: true
        default: "australiaeast"
        options:
          - label: "Australia East"
            value: "australiaeast"

  # Step 2: Lint & Build
  - label: ":bicep: Lint & Build"
    key: "build"
    command:
      - "bash .buildkite/scripts/install-az.sh"
      - "bash .buildkite/scripts/az-login.sh"
      - "bash .buildkite/scripts/bicep-install.sh"
      - "bash .buildkite/scripts/bicep-build.sh"
    artifact_paths:
      - "deploy/**"
    timeout_in_minutes: 15

  - wait

  # Step 3: What-If
  - label: ":mag: What-If Deploy"
    key: "whatif"
    command:
      - "bash .buildkite/scripts/install-az.sh"
      - "bash .buildkite/scripts/az-login.sh"
      - "bash .buildkite/scripts/bicep-install.sh"
      - "buildkite-agent artifact download 'deploy/**' ."
      - "bash .buildkite/scripts/bicep-whatif.sh"
    timeout_in_minutes: 15

  - wait

  # Step 4: Manual approval (main only)
  - block: ":rocket: Approve Deployment to Azure"
    prompt: "Review the What-If output above. Approve to deploy to Azure."
    if: build.branch == "main"

  # Step 5: Deploy (main only)
  - label: ":azure: Deploy to Azure"
    key: "deploy"
    command:
      - "bash .buildkite/scripts/install-az.sh"
      - "bash .buildkite/scripts/az-login.sh"
      - "bash .buildkite/scripts/bicep-install.sh"
      - "buildkite-agent artifact download 'deploy/**' ."
      - "bash .buildkite/scripts/bicep-deploy.sh"
    if: build.branch == "main"
    timeout_in_minutes: 30
    concurrency: 1
    concurrency_group: "bicep-landing-zone-deploy"

Let me break down the highlights:

  • Block step with selectable fields: The first step prompts the user to select the parameter file, target subscription, and Azure region equivalent to workflow_dispatch inputs in GitHub Actions. Values are stored as Buildkite meta-data and read by scripts.
  • Artifact passing: The build step compiles Bicep to JSON and uploads the deploy/ directory as a Buildkite artifact. Subsequent steps download it with buildkite-agent artifact download.
  • Branch-gated deployment: The approval gate and deploy step only appear on the main branch (if: build.branch == "main").
  • Concurrency control: concurrency: 1 and concurrency_group ensure only one deployment runs at a time no accidental parallel deployments.

The Bicep Scripts Deep Dive

bicep-build.sh Lint, Compile, and Package

This script does three things:

  1. Lints the Bicep template with az bicep lint
  2. Compiles .bicep to .json with az bicep build
  3. Converts .bicepparam to .parameters.json with az bicep build-params

Everything outputs to a deploy/ directory that gets uploaded as a build artifact.

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
#!/bin/bash
set -euo pipefail

BUILD_DIR="deploy"
BICEP_TEMPLATE_FILE_PATH="${BICEP_TEMPLATE_FILE_PATH:?BICEP_TEMPLATE_FILE_PATH env var is required}"

# Read parameter file path from env var or Buildkite meta-data
if [ -z "${BICEP_PARAMETER_FILE_PATH:-}" ]; then
  BICEP_PARAMETER_FILE_PATH="$(buildkite-agent meta-data get parameter_file_path 2>/dev/null || echo '')"
fi

mkdir -p "${BUILD_DIR}"

# Lint
echo "--- :mag: Linting Bicep template"
az bicep lint --file "${BICEP_TEMPLATE_FILE_PATH}"
echo "✅ Lint passed"

# Build template → JSON
echo "--- :hammer: Building Bicep template"
az bicep build --file "${BICEP_TEMPLATE_FILE_PATH}" --outdir "${BUILD_DIR}"

# Build / copy parameter file
if [[ "${BICEP_PARAMETER_FILE_PATH}" == *.bicepparam ]]; then
  PARAM_OUT="${BUILD_DIR}/$(basename "${BICEP_PARAMETER_FILE_PATH%.bicepparam}").parameters.json"
  az bicep build-params --file "${BICEP_PARAMETER_FILE_PATH}" --outfile "${PARAM_OUT}"
elif [[ "${BICEP_PARAMETER_FILE_PATH}" == *.json ]]; then
  cp "${BICEP_PARAMETER_FILE_PATH}" "${BUILD_DIR}/"
fi

echo "--- :white_check_mark: Build artifacts"
ls -la "${BUILD_DIR}/"

bicep-whatif.sh Preview Changes Safely

The what-if script is where the multi-scope magic happens. It reads AZURE_DEPLOYMENT_TYPE and routes to the correct az deployment scope:

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
case "${AZURE_DEPLOYMENT_TYPE}" in
  subscription)
    az account set --subscription "${AZURE_SUBSCRIPTION_ID}"
    az deployment sub what-if \
      --name     "${BICEP_DEPLOYMENT_NAME}" \
      --location "${AZURE_LOCATION}" \
      --subscription "${AZURE_SUBSCRIPTION_ID}" \
      --template-file "${TEMPLATE_JSON}" \
      "${PARAM_ARG[@]+"${PARAM_ARG[@]}"}"
    ;;
  tenant)
    az deployment tenant what-if \
      --name     "${BICEP_DEPLOYMENT_NAME}" \
      --location "${AZURE_LOCATION}" \
      --template-file "${TEMPLATE_JSON}" \
      "${PARAM_ARG[@]+"${PARAM_ARG[@]}"}"
    ;;
  managementgroup)
    az deployment mg what-if \
      --name               "${BICEP_DEPLOYMENT_NAME}" \
      --location           "${AZURE_LOCATION}" \
      --management-group-id "${AZURE_MANAGEMENT_GROUP_ID}" \
      --template-file      "${TEMPLATE_JSON}" \
      "${PARAM_ARG[@]+"${PARAM_ARG[@]}"}"
    ;;
  resourcegroup)
    az account set --subscription "${AZURE_SUBSCRIPTION_ID}"
    az deployment group what-if \
      --name           "${BICEP_DEPLOYMENT_NAME}" \
      --resource-group "${AZURE_RESOURCE_GROUP_NAME}" \
      --template-file  "${TEMPLATE_JSON}" \
      "${PARAM_ARG[@]+"${PARAM_ARG[@]}"}"
    ;;
esac

The same pattern is used in bicep-deploy.sh with what-if replaced by create.

Deployment Scopes

The pipeline supports all four Azure ARM deployment scopes out of the box:

ScopeAZURE_DEPLOYMENT_TYPEAdditional Env Vars Required
SubscriptionsubscriptionAZURE_SUBSCRIPTION_ID
Tenanttenant(requires tenant-level RBAC)
Management GroupmanagementgroupAZURE_MANAGEMENT_GROUP_ID
Resource GroupresourcegroupAZURE_SUBSCRIPTION_ID, AZURE_RESOURCE_GROUP_NAME

Switch scopes by changing the env block in your pipeline YAML:

1
2
3
env:
  AZURE_DEPLOYMENT_TYPE: "managementgroup"
  AZURE_MANAGEMENT_GROUP_ID: "your-mg-id"

The scripts are completely reusable no changes needed.


Getting Started Step by Step

1. Create a Buildkite Account

Sign up at buildkite.com. The free plan requires no credit card.

2. Install the Buildkite Agent

The agent runs on your own machine and executes pipeline jobs. Install it on any Linux, macOS, or Windows machine that has network access to Azure.

Linux (Debian/Ubuntu):

1
2
3
4
5
echo "deb https://apt.buildkite.com/buildkite-agent stable main" \
  | sudo tee /etc/apt/sources.list.d/buildkite-agent.list
curl -fsSL https://keys.openpgp.org/vks/v1/by-fingerprint/32A37959C2FA5C3C99EFBC32A79206696452D198 \
  | sudo gpg --dearmor -o /etc/apt/keyrings/buildkite-agent-archive-keyring.gpg
sudo apt-get update && sudo apt-get install -y buildkite-agent

macOS:

1
brew install buildkite/buildkite/buildkite-agent

Windows: Download the installer from Buildkite Windows Agent docs.

3. Configure and Start the Agent

1
2
# /etc/buildkite-agent/buildkite-agent.cfg (Linux)
token="your-agent-token-here"
1
sudo systemctl enable buildkite-agent && sudo systemctl start buildkite-agent

The agent will appear as online in the Buildkite UI under Agents.

4. Create Your Pipeline

In the Buildkite UI, create a new pipeline and point it at the repository. Set the pipeline steps source to “Read from repository” Buildkite will look for .buildkite/pipeline.yml automatically.

5. Store Your Azure Credentials

Follow the authentication steps above to create a service principal and store the credentials as Buildkite Secrets.

6. Trigger Your First Build

Push a commit or click “New Build” in the Buildkite UI. The agent will pick up the job and run the pipeline steps.


Reusing the Bicep Pipeline for Your Own Templates

This is one of my favourite parts. To deploy a completely different Bicep template, you don’t touch the scripts at all. Just create a new pipeline YAML:

1
2
3
4
5
env:
  BICEP_TEMPLATE_FILE_PATH: "bicep/modules/my-template/my-template.bicep"
  BICEP_DEPLOYMENT_NAME: "deploy_my_template"
  AZURE_DEPLOYMENT_TYPE: "resourcegroup"
  AZURE_RESOURCE_GROUP_NAME: "my-resource-group"

The bicep-build.sh, bicep-whatif.sh, and bicep-deploy.sh scripts are reusable with zero changes. That’s the power of the “scripts over inline YAML” approach.


Troubleshooting

ProblemCauseFix
az: command not foundAzure CLI not installedinstall-az.sh handles this automatically; verify agent OS compatibility
Bicep CLI not foundBicep not installedbicep-install.sh runs az bicep install; ensure az is authenticated first
No artifacts foundBuild artifact not downloadedUse buildkite-agent artifact download 'deploy/**' . in what-if and deploy steps
AZURE_SUBSCRIPTION_ID is requiredBlock step value not setVerify the block step field key is exactly subscription_id
Block step not appearingBranch conditionBlock steps appear on all branches; the deploy step is main-only via if: build.branch == "main"

Buildkite vs GitHub Actions vs Azure DevOps Quick Comparison

Since many of you are experienced with other CI/CD platforms, here’s a quick comparison to set expectations:

FeatureBuildkiteGitHub ActionsAzure DevOps
Agent hostingSelf-hosted onlyHosted + Self-hostedHosted + Self-hosted
Pipeline YAML location.buildkite/pipeline.yml.github/workflows/*.ymlazure-pipelines.yml
Secrets managementBuildkite SecretsGitHub SecretsPipeline Variables / Key Vault
OIDC for AzureSupported (complex setup)Native supportNative support (Workload Identity)
Block/approval stepsBuilt-in block stepenvironment protection rulesStage gates / approvals
Artifact passingartifact_paths + artifact downloadactions/upload-artifactPublishPipelineArtifact
Concurrency controlconcurrency + concurrency_groupconcurrency keyExclusive locks
Credential isolationNever leaves your infraHosted runner = GitHub infraHosted agent = Microsoft infra

The key differentiator is credential and code isolation. With Buildkite, your code and secrets stay on your infrastructure period.


Get the Code

The complete pipeline templates, scripts, and documentation are available in my GitHub repository:

azurewithdanidu/azure-buildkite-pipelines

The repository includes:

  • Two ready-to-use pipeline templates (CLI login + Bicep deployment)
  • Seven reusable bash scripts for Azure CLI, authentication, and Bicep operations
  • Full documentation for both pipelines
  • The original GitHub Actions workflows for reference/migration
  • A sample Bicep template to test with

Feel free to fork it, adapt it to your own templates, and reach out if you have any questions.


Wrapping Up

Buildkite is one of those tools that does one thing exceptionally well running CI/CD pipelines on your own infrastructure with a clean, minimal orchestration layer. If you need your Azure deployment pipelines to execute inside your own network boundary, or you simply prefer the security model of keeping credentials on your own machines, Buildkite is a solid choice.

The two pipelines I’ve shared here give you a production-ready starting point from basic Azure CLI authentication all the way to a full Bicep deployment workflow with linting, what-if previews, manual approval gates, and multi-scope support.

Hope this will help someone in need. Until next time…!


References:

This post is licensed under CC BY 4.0 by the author.