CI/CD for Azure Serverless: Automating Deployments with Terraform and Azure DevOps

How a fintech team deployed 50+ Azure Functions across 3 environments using Terraform and Azure DevOps—with zero manual intervention.


Why Terraform + Azure DevOps?

  1. Infrastructure-as-Code (IaC): Terraform’s declarative syntax ensures reproducible environments (dev, staging, prod).

  2. Azure DevOps Pipelines: Enterprise-grade CI/CD with built-in approvals, logging, and integrations.

  3. State Management: Terraform Cloud/Azure Storage for tracking infrastructure state.

  4. Multi-Cloud Ready: Terraform’s provider model supports hybrid/AWS/GCP (unlike Bicep/ARM).


Architecture Overview

[Git Repo]
→ [Azure DevOps Pipeline (Build)]
→ [Terraform Plan/Apply (Infra)]
→ [Azure Functions Deployment (Code)]
→ [Dev/Staging/Prod Environments]


Step 1: Structuring the Terraform Project

Folder Structure

├── modules/  
│   ├── function_app/  
│   │   ├── main.tf  
│   │   ├── variables.tf  
│   ├── storage/  
│   │   ├── main.tf  
├── environments/  
│   ├── dev/  
│   │   ├── main.tf  
│   │   ├── terraform.tfvars  
│   ├── staging/  
│   ├── prod/  
├── pipelines/  
│   ├── azure-devops.yml

Example: Terraform Module for Azure Function (modules/function_app/main.tf)

resource "azurerm_function_app" "example" {  
  name                = "func-${var.env}-payment-api"  
  resource_group_name = var.resource_group  
  location            = var.location  
  app_service_plan_id = var.app_service_plan_id  
  storage_account_name = azurerm_storage_account.example.name  

  app_settings = {  
    "APPINSIGHTS_INSTRUMENTATIONKEY" = azurerm_application_insights.example.instrumentation_key  
    "AzureWebJobsStorage"            = azurerm_storage_account.example.primary_connection_string  
  }  

  identity {  
    type = "SystemAssigned"  
  }  
}

Step 2: Azure DevOps Pipeline Setup

Pipeline Tasks

  1. Install Terraform: Use the Terraform Azure DevOps Extension.

  2. Initialize Terraform: terraform init with backend state in Azure Storage.

  3. Plan/Apply Infrastructure:

    • terraform plan for dry runs.

    • terraform apply to deploy infra.

  4. Deploy Function Code: Use AzureFunctionApp@1 task.

Pipeline YAML (pipelines/azure-devops.yml)

trigger:  
  branches:  
    include:  
      - main  

variables:  
  - name: TF_BACKEND_STORAGE  
    value: "terraformstate"  

stages:  
- stage: Build  
  jobs:  
  - job: Build  
    steps:  
    - task: TerraformInstaller@0  
      inputs:  
        terraformVersion: '1.5.7'  

    - script: |  
        terraform init -backend-config="storage_account_name=$(TF_BACKEND_STORAGE)"  
        terraform validate  
      displayName: 'Initialize Terraform'  

- stage: Deploy_Dev  
  dependsOn: Build  
  jobs:  
  - job: Deploy  
    steps:  
    - task: TerraformTaskV2@2  
      inputs:  
        command: 'plan'  
        environmentServiceName: 'Azure-Dev'  
        commandOptions: '-var-file=environments/dev/terraform.tfvars'  

    - task: TerraformTaskV2@2  
      inputs:  
        command: 'apply'  
        environmentServiceName: 'Azure-Dev'  
        commandOptions: '-auto-approve'  

    - task: AzureFunctionApp@1  
      inputs:  
        azureSubscription: 'Azure-Dev'  
        appType: 'functionApp'  
        appName: 'func-dev-payment-api'  
        package: '$(Pipeline.Workspace)/drop/function.zip'

Step 3: Integrating Function Code Deployment

Build Pipeline for Function Code

- stage: Build_Function  
  jobs:  
  - job: Build  
    steps:  
    - task: UseDotNet@2  
      inputs:  
        version: '6.x'  

    - task: DotNetCoreCLI@2  
      inputs:  
        command: 'publish'  
        projects: 'src/PaymentFunction.csproj'  
        arguments: '-o $(Build.ArtifactStagingDirectory)'  

    - task: ArchiveFiles@2  
      inputs:  
        rootFolderOrFile: '$(Build.ArtifactStagingDirectory)'  
        includeRootFolder: false  
        archiveFile: '$(Build.ArtifactStagingDirectory)/function.zip'  

    - task: PublishPipelineArtifact@1  
      inputs:  
        targetPath: '$(Build.ArtifactStagingDirectory)/function.zip'  
        artifactName: 'function'

Step 4: Securing Terraform State and Secrets

  1. Backend State in Azure Storage:
terraform {  
  backend "azurerm" {  
    resource_group_name  = "tfstate-rg"  
    storage_account_name = "terraformstate"  
    container_name       = "tfstate"  
    key                  = "dev.terraform.tfstate"  
  }  
}
  1. Azure Key Vault Integration:
data "azurerm_key_vault_secret" "stripe_key" {  
  name         = "StripeApiKey"  
  key_vault_id = azurerm_key_vault.example.id  
}  

# Use in Function App settings:  
app_settings = {  
  "StripeApiKey" = "@Microsoft.KeyVault(SecretUri=${data.azurerm_key_vault_secret.stripe_key.id})"  
}

Real-World Use Case: Fintech Deployment Pipeline

Scenario:
A payment gateway needed to deploy 50+ Functions across 3 regions with strict compliance.

Implementation:

  1. Terraform: Defined all resources (Functions, Storage, Key Vault) as code.

  2. Azure DevOps:

    • Multi-Stage Pipelines: Dev → Staging → Prod with manual approvals.

    • Variable Groups: Stored environment-specific settings (e.g., API endpoints).

  3. Security:

    • Terraform state encrypted in Azure Storage.

    • Pipeline permissions scoped to least privilege.

Results:

  • Deployment time reduced from 2 hours to 15 minutes.

  • Zero configuration drift between environments.


Pros and Cons: Terraform vs. Bicep

FactorTerraformBicep
State ManagementRequires backend (e.g., Azure Storage)Native state with Azure
Multi-Cloud Support✅ (AWS, GCP, etc.)❌ (Azure-only)
Learning CurveModerate (HCL syntax)Easy (ARM-like)
Community ModulesExtensive (Terraform Registry)Limited (Azure Quickstart)
CostFree (OSS)Free

Best Practices

  1. Modularize Terraform Code: Reuse modules for Functions, Storage, etc.

  2. Pipeline Approvals: Require manual checks before prod deployments.

  3. Secret Management: Never store secrets in Terraform variables—use Key Vault.

  4. Testing:

    • terraform validate and tflint for static checks.

    • Deploy to a sandbox environment nightly.


Conclusion

Combining Terraform for infrastructure and Azure DevOps for pipelines gives you a robust, auditable, and scalable CI/CD process for Azure serverless apps. While Bicep is great for Azure-only teams, Terraform shines in hybrid/multi-cloud environments.