Part 15 of 1611 min read · 1 diagramsAI-assisted

CI/CD & Infrastructure as Code

Table of Contents#

  1. Deploying Everything This Series Has Built
  2. Bicep — Azure's Native IaC Language, Revisited
  3. Bicep Modules
  4. Deployment Stacks — Lifecycle Management
  5. Template Specs — Reusable, Versioned Templates
  6. Terraform on Azure — the azurerm Provider
  7. Choosing Between Bicep and Terraform
  8. Azure DevOps Pipelines
  9. GitHub Actions for Azure
  10. Choosing Between Azure DevOps and GitHub Actions
  11. Workload Identity Federation for CI/CD, Revisited
  12. Deployment Strategies for Application Code
  13. Environment Promotion and Approval Gates
  14. Drift Detection
  15. A Full Worked CI/CD Bootstrap for Meridian Freight
  16. Part 15 CLI Cheat Sheet
  17. Common Mistakes and Interview Traps
  18. Worked Practice Problems
  19. Summary and What's Next

Deploying Everything This Series Has Built#

Every resource this series has covered — VNets, VMs, Key Vaults, AKS clusters — should be defined as code and deployed through a pipeline, not created by hand through the portal. This chapter is where that becomes concrete.

Diagram

Bicep — Azure's Native IaC Language, Revisited#

Part 1 introduced Bicep as ARM JSON's cleaner authoring syntax; this chapter goes deeper on the patterns that make Bicep genuinely maintainable at real scale.

param location string = resourceGroup().location
param environmentName string

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: 'st${environmentName}${uniqueString(resourceGroup().id)}'
  location: location
  sku: { name: 'Standard_ZRS' }
  kind: 'StorageV2'
}
az deployment group create --resource-group rg-shipment-api-prod \
  --template-file main.bicep --parameters environmentName=prod

Bicep Modules#

module networking 'modules/networking.bicep' = {
  name: 'networking-deployment'
  params: {
    vnetAddressPrefix: '10.1.0.0/16'
    environmentName: environmentName
  }
}

Modules let a large deployment be decomposed into reusable, independently-testable units — a networking module, a compute module, a database module — each callable from a top-level template, directly mirroring the same decomposition discipline this series applied to application architecture (Part 11) now applied to infrastructure code itself.


Deployment Stacks — Lifecycle Management#

A genuinely important, current capability worth stating precisely: ordinary Bicep/ARM deployments are ADDITIVE by design — they create and update resources, but never remove a resource that disappears from the template. Deployment Stacks solve this directly.

az stack group create --name stack-shipment-api-prod --resource-group rg-shipment-api-prod \
  --template-file main.bicep --action-on-unmanage deleteResources --deny-settings-mode denyDelete

Why this matters concretely, worth stating the underlying reasoning: without a deployment stack, removing a resource block from a Bicep template leaves the ACTUAL resource still running in Azure, orphaned and unmanaged by the template — a real, easy-to-miss source of configuration drift and unnecessary cost. A deployment stack tracks exactly which resources belong to it and, based on the configured action-on-unmanage behavior, can automatically delete a resource removed from the template. --deny-settings-mode denyDelete is a genuinely powerful complementary feature — it generates a deny assignment (Part 2) preventing anyone from manually deleting a stack-managed resource outside the stack's own lifecycle, directly closing the exact "who deleted this and why" gap ad-hoc portal changes create. Worth knowing the real current limitation: What-If (Part 1's preview-before-apply command) is not supported within deployment stacks — application deployments needing What-If should still use standard Bicep deployments.


Template Specs — Reusable, Versioned Templates#

az ts create --name ts-standard-vnet --version "1.0" --resource-group rg-platform \
  --template-file vnet-template.bicep

Template Specs package a Bicep template as a versioned, shareable Azure resource itself — a platform team publishes a "standard VNet" template spec once, and every application team references a specific version, the same versioned, governed sharing model Part 3 established for Azure Compute Gallery images, now applied to infrastructure templates themselves.


Terraform on Azure — the azurerm Provider#

terraform {
  required_providers {
    azurerm = { source = "hashicorp/azurerm", version = "~> 4.0" }
  }
}

provider "azurerm" {
  features {}
  use_oidc = true
}

resource "azurerm_storage_account" "main" {
  name                     = "stmeridianfreight"
  resource_group_name      = azurerm_resource_group.main.name
  location                 = azurerm_resource_group.main.location
  account_tier             = "Standard"
  account_replication_type = "ZRS"
}

Worth stating explicitly why an organization might choose Terraform over Bicep despite Bicep being Azure-native: Terraform's provider ecosystem spans every major cloud plus hundreds of other services (DNS registrars, monitoring SaaS platforms, GitHub itself) in ONE consistent language and state model — a genuinely real advantage for an organization with infrastructure spanning beyond Azure alone, which Bicep, being Azure-specific, cannot address.


Choosing Between Bicep and Terraform#

FactorBicepTerraform
ScopeAzure-onlyMulti-cloud, hundreds of providers
State managementNone needed — Azure Resource Manager tracks state nativelyRequires explicit state file management (typically a remote backend)
Native platform integrationFirst-party, always current with new Azure features immediatelyProvider updates can lag behind brand-new Azure features
Team familiarityAzure-specific skillBroadly transferable across cloud providers

For Meridian Freight specifically, currently Azure-only: Bicep is the pragmatic default, with Terraform worth adopting only if a genuine multi-cloud requirement materializes — choosing Terraform purely for its general popularity, without an actual multi-cloud need, would add real state-management operational overhead for no corresponding benefit.


Azure DevOps Pipelines#

trigger:
  - main
pool:
  vmImage: ubuntu-latest
steps:
  - task: AzureCLI@2
    inputs:
      azureSubscription: 'meridian-prod-connection'
      scriptType: bash
      scriptLocation: inlineScript
      inlineScript: |
        az deployment group create --resource-group rg-shipment-api-prod --template-file main.bicep

Azure DevOps bundles Boards, Repos, Pipelines, Artifacts, and Test Plans in one integrated platform — worth choosing specifically when an organization already has real investment in Azure Boards for work tracking or needs the Classic (non-YAML) pipeline UI, or has regulated compliance requirements (certain FedRAMP scenarios) it specifically supports.


GitHub Actions for Azure#

name: Deploy Infrastructure
on:
  push:
    branches: [main]
permissions:
  id-token: write
  contents: read
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      - run: az deployment group create --resource-group rg-shipment-api-prod --template-file main.bicep

Worth stating precisely: the azure/login action with id-token: write permission uses OIDC-based workload identity federation (Part 2) — no client secret stored in GitHub at all, the same secretless authentication pattern Part 2 recommended, applied concretely here to the actual deployment pipeline.


Choosing Between Azure DevOps and GitHub Actions#

NeedRecommendation
Code already lives on GitHub, greenfield projectGitHub Actions
Existing deep investment in Azure Boards + Repos + Test PlansAzure DevOps Pipelines
Need the largest possible marketplace of pre-built actions/tasksGitHub Actions (20,000+ community actions vs. a meaningfully smaller Azure DevOps extension marketplace)
Formal, structured release orchestration with complex approval chainsAzure DevOps Pipelines' release management tends to be the stronger fit

Workload Identity Federation for CI/CD, Revisited#

Part 2 introduced workload identity federation generally; worth confirming precisely for both platforms' current state: GitHub Actions has supported it since 2021; Azure DevOps service connections reached general availability with it in February 2024 — both platforms, and Terraform's azurerm provider (since v3.7), now fully support secretless authentication, meaning there is no remaining excuse to fall back to a stored client secret for either CI/CD platform's Azure deployment pipeline in a new setup.


Deployment Strategies for Application Code#

Recapping and applying Part 6's deployment slots concretely within a pipeline:

  - task: AzureWebApp@1
    inputs:
      azureSubscription: 'meridian-prod-connection'
      appName: 'shipment-api-legacy'
      deployToSlotOrASE: true
      slotName: 'staging'
  - task: AzureAppServiceManage@0
    inputs:
      action: 'Swap Slots'
      slot: 'staging'

Deploying to staging, running smoke tests against it, THEN swapping — automated as pipeline steps rather than a manual runbook — is what actually operationalizes the blue-green pattern Part 6 introduced conceptually.


Environment Promotion and Approval Gates#

environments:
  - name: production
    approvals:
      requiredApprovers: ["platform-team"]

A genuinely important practice worth stating explicitly: a change should flow through progressively stricter environments (dev → staging → production), with production specifically requiring an explicit human approval gate — automating the deployment MECHANISM doesn't mean removing human judgment from the decision of WHEN a specific change actually reaches production.


Drift Detection#

az deployment group what-if --resource-group rg-shipment-api-prod --template-file main.bicep

Configuration drift — a resource manually modified outside the IaC pipeline — is worth detecting proactively, not discovering reactively during the next deployment's unexpected diff. Running what-if (Part 1) on a schedule, independent of any actual deployment, surfaces drift as soon as it happens rather than waiting for the next real change to reveal it.


A Full Worked CI/CD Bootstrap for Meridian Freight#

# 1. Template Spec for the platform team's standard VNet pattern
az ts create --name ts-standard-vnet --version "1.0" --resource-group rg-platform \
  --template-file vnet-template.bicep

# 2. Deployment stack for shipment-api's infrastructure, with deny-delete protection
az stack group create --name stack-shipment-api-prod --resource-group rg-shipment-api-prod \
  --template-file main.bicep --deny-settings-mode denyDelete

# 3. GitHub Actions workflow using OIDC-based workload identity federation
# (workflow YAML committed to the repo, referencing azure/login with id-token: write)

# 4. A scheduled drift-detection job running what-if independent of real deployments
az deployment group what-if --resource-group rg-shipment-api-prod --template-file main.bicep

Part 15 CLI Cheat Sheet#

AreaCommandPurpose
Bicepaz deployment group create --template-fileDeploy a Bicep template
Stacksaz stack group create --deny-settings-mode denyDeleteDeploy with lifecycle management and deletion protection
Template Specsaz ts createPublish a versioned, shareable template
What-Ifaz deployment group what-ifPreview changes or detect drift
Terraformterraform plan / terraform applyPreview and apply Terraform changes

Common Mistakes and Interview Traps#

MistakeWhy It's WrongFix
Removing a resource block from a Bicep template and expecting it to be deletedOrdinary deployments are additive — removed resources stay orphaned in AzureUse a deployment stack with action-on-unmanage to actually clean up removed resources
Adopting Terraform purely for its popularity without a multi-cloud requirementAdds real state-management overhead for no corresponding benefitDefault to Bicep for Azure-only infrastructure; adopt Terraform when multi-cloud is a genuine need
Storing a client secret for a CI/CD pipeline's Azure authentication in 2026Both GitHub Actions and Azure DevOps have long supported secretless OIDC-based authenticationUse workload identity federation for any new pipeline setup
Deploying directly to production with no approval gateRemoves human judgment from when a change actually ships, even with a fully automated pipelineRequire an explicit approval gate for production, regardless of automation maturity
Assuming What-If works the same way inside a deployment stack as a standard deploymentWhat-If is not supported within deployment stacksUse standard Bicep deployments (not stacks) for previewing application-level changes with What-If

Worked Practice Problems#

Problem 1: A platform team removes a no-longer-needed NSG resource block from their Bicep template and redeploys, expecting the NSG to be deleted from Azure. A cost review a month later finds the NSG (and its associated public IP) still exist and are still being billed. What's the cause, and what should the team have used instead?

Answer: Ordinary Bicep/ARM deployments are additive by design — they create and update resources present in the template, but never delete a resource that's been removed from it, leaving the NSG orphaned and unmanaged despite no longer appearing in the source code. The team should have deployed through a Deployment Stack instead, with action-on-unmanage configured to delete resources removed from the template — this converts "no longer in the template" into an actual deletion, closing exactly this drift-and-orphan gap that plain Bicep deployments cannot address.

Problem 2: An engineer manually resizes a production VM through the Azure Portal to handle an urgent capacity need, intending to update the Bicep template "later" to match. Three weeks later, a routine deployment reverts the VM back to its original, template-defined size during a business-hours change window, causing a capacity incident. What process gap caused this, and what practice would have caught it earlier?

Answer: This is a textbook configuration drift incident — the actual Azure resource diverged from the IaC template, and the next deployment silently reverted the drift back to the template's stale definition, exactly the pattern of "reactive drift discovery during the next real deployment" this chapter warns against. Running az deployment group what-if on a regular SCHEDULE (not just before actual deployments) would have surfaced this specific drift as soon as it happened — showing a diff between the template and live state days or weeks before the next real deployment accidentally reverted it — giving the team the chance to either update the template to reflect the intentional resize, or consciously resize back down, on their own schedule rather than during an unplanned incident.


Summary and What's Next#

  • Deployment Stacks solve ordinary Bicep/ARM deployments' additive-only limitation — tracking and optionally deleting resources removed from a template, with denyDelete protection against out-of-band manual deletion, though without What-If support.
  • Template Specs let a platform team publish versioned, governed infrastructure templates other teams reference — the same versioned-sharing model Part 3 established for compute images.
  • Bicep is the pragmatic Azure-only default; Terraform earns its adoption specifically when a genuine multi-cloud requirement exists — not from general popularity alone.
  • Both major CI/CD platforms and Terraform's azurerm provider now fully support OIDC-based workload identity federation — no remaining excuse for a stored client secret in a new pipeline.
  • Deployment automation doesn't replace human judgment on production timing — an explicit approval gate remains necessary regardless of pipeline maturity.
  • Scheduled, independent drift detection (via what-if) catches configuration drift proactively, before the next real deployment reverts it unexpectedly during a change window.

Continue to Part 16 (16-multi-region-cost-optimization-and-cheatsheet.md) — the closing chapter bringing multi-region architecture, cost optimization, and a full series cheat sheet together.