# Azure Cloud Architecture — Part 15: CI/CD & Infrastructure as Code

> **Series:** Azure Cloud Architecture (15 of 16)
> **Part 1:** `01-fundamentals-and-governance.md` — Fundamentals & Governance
> **Part 2:** `02-identity-and-access.md` — Identity & Access
> **Part 3:** `03-compute-vms-and-scale-sets.md` — Compute: Virtual Machines & Scale Sets
> **Part 4:** `04-networking-foundations-vnets-ip-and-dns.md` — Networking Foundations: VNets, IP & DNS
> **Part 5:** `05-networking-hybrid-connectivity.md` — Networking: Hybrid Connectivity
> **Part 6:** `06-networking-application-delivery.md` — Networking: Application Delivery
> **Part 7:** `07-networking-private-access-and-security.md` — Networking: Private Access & Security
> **Part 8:** `08-storage-blob-files-and-disks.md` — Storage: Blob, Files & Disks
> **Part 9:** `09-databases-and-data-services.md` — Databases & Data Services
> **Part 10:** `10-containers-and-serverless.md` — Containers & Serverless
> **Part 11:** `11-application-architecture-and-messaging.md` — Application Architecture & Messaging
> **Part 12:** `12-security-and-compliance.md` — Security & Compliance
> **Part 13:** `13-monitoring-logging-and-observability.md` — Monitoring, Logging & Observability
> **Part 14:** `14-business-continuity-backup-dr-and-migration.md` — Business Continuity: Backup, DR & Migration
> **Part 15:** This file — CI/CD & Infrastructure as Code
> **Part 16:** `16-multi-region-cost-optimization-and-cheatsheet.md` — Multi-Region, Cost Optimization & Cheat Sheet
> **Questions:** `questions.md`

## Table of Contents

1. [Deploying Everything This Series Has Built](#deploying-everything-this-series-has-built)
2. [Bicep — Azure's Native IaC Language, Revisited](#bicep--azures-native-iac-language-revisited)
3. [Bicep Modules](#bicep-modules)
4. [Deployment Stacks — Lifecycle Management](#deployment-stacks--lifecycle-management)
5. [Template Specs — Reusable, Versioned Templates](#template-specs--reusable-versioned-templates)
6. [Terraform on Azure — the azurerm Provider](#terraform-on-azure--the-azurerm-provider)
7. [Choosing Between Bicep and Terraform](#choosing-between-bicep-and-terraform)
8. [Azure DevOps Pipelines](#azure-devops-pipelines)
9. [GitHub Actions for Azure](#github-actions-for-azure)
10. [Choosing Between Azure DevOps and GitHub Actions](#choosing-between-azure-devops-and-github-actions)
11. [Workload Identity Federation for CI/CD, Revisited](#workload-identity-federation-for-cicd-revisited)
12. [Deployment Strategies for Application Code](#deployment-strategies-for-application-code)
13. [Environment Promotion and Approval Gates](#environment-promotion-and-approval-gates)
14. [Drift Detection](#drift-detection)
15. [A Full Worked CI/CD Bootstrap for Meridian Freight](#a-full-worked-cicd-bootstrap-for-meridian-freight)
16. [Part 15 CLI Cheat Sheet](#part-15-cli-cheat-sheet)
17. [Common Mistakes and Interview Traps](#common-mistakes-and-interview-traps)
18. [Worked Practice Problems](#worked-practice-problems)
19. [Summary and What's Next](#summary-and-whats-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.

```mermaid
graph LR
    Code["Bicep/Terraform code\nin source control"] --> Pipeline["CI/CD pipeline\n(Azure DevOps or GitHub Actions)"]
    Pipeline --> Deploy["Deployed via\nworkload identity federation\n(Part 2), no stored secrets"]
```

---

## 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.

```bicep
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'
}
```

```bash
az deployment group create --resource-group rg-shipment-api-prod \
  --template-file main.bicep --parameters environmentName=prod
```

---

## Bicep Modules

```bicep
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.

```bash
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

```bash
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

```hcl
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

| Factor | Bicep | Terraform |
|---|---|---|
| Scope | Azure-only | Multi-cloud, hundreds of providers |
| State management | None needed — Azure Resource Manager tracks state natively | Requires explicit state file management (typically a remote backend) |
| Native platform integration | First-party, always current with new Azure features immediately | Provider updates can lag behind brand-new Azure features |
| Team familiarity | Azure-specific skill | Broadly 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

```yaml
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

```yaml
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

| Need | Recommendation |
|---|---|
| Code already lives on GitHub, greenfield project | GitHub Actions |
| Existing deep investment in Azure Boards + Repos + Test Plans | Azure DevOps Pipelines |
| Need the largest possible marketplace of pre-built actions/tasks | GitHub Actions (20,000+ community actions vs. a meaningfully smaller Azure DevOps extension marketplace) |
| Formal, structured release orchestration with complex approval chains | Azure 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:

```yaml
  - 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

```yaml
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

```bash
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

```bash
# 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

| Area | Command | Purpose |
|---|---|---|
| Bicep | `az deployment group create --template-file` | Deploy a Bicep template |
| Stacks | `az stack group create --deny-settings-mode denyDelete` | Deploy with lifecycle management and deletion protection |
| Template Specs | `az ts create` | Publish a versioned, shareable template |
| What-If | `az deployment group what-if` | Preview changes or detect drift |
| Terraform | `terraform plan` / `terraform apply` | Preview and apply Terraform changes |

---

## Common Mistakes and Interview Traps

| Mistake | Why It's Wrong | Fix |
|---|---|---|
| Removing a resource block from a Bicep template and expecting it to be deleted | Ordinary deployments are additive — removed resources stay orphaned in Azure | Use a deployment stack with `action-on-unmanage` to actually clean up removed resources |
| Adopting Terraform purely for its popularity without a multi-cloud requirement | Adds real state-management overhead for no corresponding benefit | Default 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 2026 | Both GitHub Actions and Azure DevOps have long supported secretless OIDC-based authentication | Use workload identity federation for any new pipeline setup |
| Deploying directly to production with no approval gate | Removes human judgment from when a change actually ships, even with a fully automated pipeline | Require an explicit approval gate for production, regardless of automation maturity |
| Assuming What-If works the same way inside a deployment stack as a standard deployment | What-If is not supported within deployment stacks | Use 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.
