How to create a function in an Azure Function App by Azure DevOps Pipeline?

Khalid Hajjouji 50 Reputation points
2025-08-13T08:14:28.9766667+00:00

I make it work to create a function in an Azure Function App like in the yaml file below. See stage stage "Deploy_FunctionCode". But I make it work with bicep. Can I do the same with Terraform?

trigger:
  - none

pool:
  vmImage: 'ubuntu-latest'

variables:
  environment: 'prd'
  location: 'West Europe'
  functionAppName: 'az-func-app-m365-stproject-$(environment)'
  storageAccountName: 'stprojstoracc$(environment)'
  resourceGroupName: 'rg-m365-stProject-$(environment)-002'
  appServicePlanName: 'stproject-appserviceplan-$(environment)'
  serviceConnectionName: 'M365 Automation PRD Azure Appregistratie'

stages:
- stage: Deploy_Infrastructure
  displayName: 'Deploy Azure Function  Infrastructure'
  jobs:
  - job: DeployBicep
    steps:
    - task: AzureCLI@2
      inputs:
        azureSubscription: $(serviceConnectionName)
        scriptType: 'bash'
        scriptLocation: 'inlineScript'
        inlineScript: |
          az storage account create \
            --name $(storageAccountName) \
            --resource-group $(resourceGroupName) \
            --location westeurope \
            --sku Standard_RAGRS \
            --kind StorageV2 \
            --min-tls-version TLS1_2 \
            --allow-blob-public-access false
          az deployment group create \
            --resource-group $(resourceGroupName) \
            --template-file functionapp.bicep \
            --parameters functionAppName=$(functionAppName) storageAccountName=$(storageAccountName) appServicePlanName=$(appServicePlanName) environment=$(environment)

- stage: Deploy_FunctionCode
  displayName: 'Deploy Function Code'
  dependsOn: Deploy_Infrastructure
  jobs:
  - job: DeployCode
    steps:
    - task: AzureFunctionApp@1
      inputs:
        azureSubscription: $(serviceConnectionName)
        appType: 'functionApp'
        appName: '$(functionAppName)'
        package: '$(System.DefaultWorkingDirectory)'


Bicep project:

User's image

Azure Functions
Azure Functions
An Azure service that provides an event-driven serverless compute platform.
{count} votes

1 answer

Sort by: Most helpful
  1. siva kumar 40 Reputation points
    2025-08-13T09:14:02.81+00:00

    Yes, you can absolutely achieve the same functionality with Terraform!

    
    # ====================================================================================================
    # CI/CD INTEGRATION EXAMPLES FOR TERRAFORM AZURE FUNCTION DEPLOYMENT
    # ====================================================================================================
    
    # EXAMPLE 1: GitHub Actions Workflow
    # File: .github/workflows/deploy-function.yml
    ---
    name: Deploy Azure Function App with Terraform
    
    on:
      push:
        branches: [main]
      workflow_dispatch:
    
    env:
      TERRAFORM_VERSION: '1.5.0'
      ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
      ARM_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
      ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
    
    jobs:
      deploy:
        runs-on: ubuntu-latest
        steps:
        - name: Checkout
          uses: actions/checkout@v3
          
        - name: Setup Terraform
          uses: hashicorp/setup-terraform@v2
          with:
            terraform_version: ${{ env.TERRAFORM_VERSION }}
            
        - name: Azure Login
          uses: azure/login@v1
          with:
            creds: ${{ secrets.AZURE_CREDENTIALS }}
            
        - name: Terraform Init
          run: terraform init
          
        - name: Terraform Plan
          run: |
            terraform plan \
              -var="environment=prd" \
              -var="function_code_path=${{ github.workspace }}/function-code" \
              -var="build_id=${{ github.run_number }}"
              
        - name: Terraform Apply
          run: |
            terraform apply -auto-approve \
              -var="environment=prd" \
              -var="function_code_path=${{ github.workspace }}/function-code" \
              -var="build_id=${{ github.run_number }}"
    
    # ====================================================================================================
    # EXAMPLE 2: Azure DevOps Pipeline (YAML)
    # File: azure-pipelines.yml
    ---
    trigger:
      - main
    
    pool:
      vmImage: 'ubuntu-latest'
    
    variables:
      terraformVersion: '1.5.0'
      environment: 'prd'
      serviceConnection: 'M365 Automation PRD Azure Appregistratie'
    
    stages:
    - stage: DeployInfrastructure
      displayName: 'Deploy Infrastructure with Terraform'
      jobs:
      - job: TerraformDeploy
        steps:
        - task: TerraformInstaller@0
          displayName: 'Install Terraform'
          inputs:
            terraformVersion: $(terraformVersion)
            
        - task: AzureCLI@2
          displayName: 'Terraform Init and Apply'
          inputs:
            azureSubscription: $(serviceConnection)
            scriptType: 'bash'
            scriptLocation: 'inlineScript'
            inlineScript: |
              # Initialize Terraform
              terraform init
              
              # Plan with variables
              terraform plan \
                -var="environment=$(environment)" \
                -var="function_code_path=$(System.DefaultWorkingDirectory)/function-code" \
                -var="build_id=$(Build.BuildId)"
              
              # Apply infrastructure and code
              terraform apply -auto-approve \
                -var="environment=$(environment)" \
                -var="function_code_path=$(System.DefaultWorkingDirectory)/function-code" \
                -var="build_id=$(Build.BuildId)"
    
    # ====================================================================================================
    # EXAMPLE 3: Terraform Variables File for Different Environments
    # File: terraform.tfvars (or environments/prd.tfvars)
    ---
    environment = "prd"
    location = "West Europe"
    
    # Function app configuration
    function_app_name = "az-func-app-m365-stproject-prd"
    storage_account_name = "stprojstoraccprd"
    resource_group_name = "rg-m365-stProject-prd-002"
    app_service_plan_name = "stproject-appserviceplan-prd"
    
    # Code deployment settings
    function_code_path = "./function-code"
    
    # Optional: Additional settings
    function_runtime = "python"
    function_version = "3.9"
    
    # ====================================================================================================
    # EXAMPLE 4: PowerShell Deployment Script
    # File: Deploy-FunctionApp.ps1
    ---
    param(
        [Parameter(Mandatory=$true)]
        [string]$Environment,
        
        [Parameter(Mandatory=$false)]
        [string]$FunctionCodePath = "./function-code",
        
        [Parameter(Mandatory=$false)]
        [switch]$DestroyFirst = $false
    )
    
    Write-Host "Deploying Azure Function App for environment: $Environment"
    
    # Set Terraform variables
    $env:TF_VAR_environment = $Environment
    $env:TF_VAR_function_code_path = $FunctionCodePath
    $env:TF_VAR_build_id = Get-Date -Format "yyyyMMddHHmmss"
    
    try {
        # Initialize Terraform
        Write-Host "Initializing Terraform..."
        terraform init
        
        # Destroy if requested
        if ($DestroyFirst) {
            Write-Host "Destroying existing infrastructure..."
            terraform destroy -auto-approve
        }
        
        # Plan deployment
        Write-Host "Planning deployment..."
        terraform plan
        
        # Apply deployment
        Write-Host "Applying deployment..."
        terraform apply -auto-approve
        
        # Get outputs
        $functionAppName = terraform output -raw function_app_name
        $functionAppUrl = terraform output -raw function_app_url
        
        Write-Host "Deployment completed successfully!"
        Write-Host "Function App Name: $functionAppName"
        Write-Host "Function App URL: $functionAppUrl"
        
    } catch {
        Write-Error "Deployment failed: $($_.Exception.Message)"
        exit 1
    }
    
    
     
        
       
    
    
    terraform {
      required_version = ">= 1.0"
      required_providers {
        azurerm = {
          source  = "hashicorp/azurerm"
          version = "~> 3.0"
        }
        archive = {
          source  = "hashicorp/archive"
          version = "~> 2.0"
        }
      }
    }
    
    provider "azurerm" {
      features {}
    }
    
    # ====================================================================================================
    # VARIABLES (Equivalent to your YAML variables)
    # ====================================================================================================
    
    variable "environment" {
      description = "Environment name"
      type        = string
      default     = "prd"
    }
    
    variable "location" {
      description = "Azure region"
      type        = string
      default     = "West Europe"
    }
    
    variable "function_app_name" {
      description = "Function App name"
      type        = string
      default     = ""
    }
    
    variable "storage_account_name" {
      description = "Storage Account name"
      type        = string
      default     = ""
    }
    
    variable "resource_group_name" {
      description = "Resource Group name"
      type        = string
      default     = ""
    }
    
    variable "app_service_plan_name" {
      description = "App Service Plan name"
      type        = string
      default     = ""
    }
    
    variable "function_code_path" {
      description = "Path to function code directory"
      type        = string
      default     = "./function-code"
    }
    
    # ====================================================================================================
    # LOCAL VALUES (Computed naming like your YAML variables)
    # ====================================================================================================
    
    locals {
      function_app_name      = var.function_app_name != "" ? var.function_app_name : "az-func-app-m365-stproject-${var.environment}"
      storage_account_name   = var.storage_account_name != "" ? var.storage_account_name : "stprojstoracc${var.environment}"
      resource_group_name    = var.resource_group_name != "" ? var.resource_group_name : "rg-m365-stProject-${var.environment}-002"
      app_service_plan_name  = var.app_service_plan_name != "" ? var.app_service_plan_name : "stproject-appserviceplan-${var.environment}"
      
      # Function app settings
      function_app_settings = {
        "FUNCTIONS_EXTENSION_VERSION"     = "~4"
        "FUNCTIONS_WORKER_RUNTIME"        = "python"  # Change as needed: python, dotnet, node, java
        "AzureWebJobsStorage"            = azurerm_storage_account.function_storage.primary_connection_string
        "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING" = azurerm_storage_account.function_storage.primary_connection_string
        "WEBSITE_CONTENTSHARE"           = local.function_app_name
      }
    }
    
    # ====================================================================================================
    # DATA SOURCES
    # ====================================================================================================
    
    # Get existing resource group (assuming it exists)
    data "azurerm_resource_group" "main" {
      name = local.resource_group_name
    }
    
    # ====================================================================================================
    # STORAGE ACCOUNT (Equivalent to your az storage account create command)
    # ====================================================================================================
    
    resource "azurerm_storage_account" "function_storage" {
      name                     = local.storage_account_name
      resource_group_name      = data.azurerm_resource_group.main.name
      location                 = data.azurerm_resource_group.main.location
      account_tier             = "Standard"
      account_replication_type = "RAGRS"
      account_kind             = "StorageV2"
      min_tls_version          = "TLS1_2"
      
      # Security settings (equivalent to --allow-blob-public-access false)
      allow_nested_items_to_be_public = false
      
      tags = {
        Environment = var.environment
        Purpose     = "FunctionApp"
      }
    }
    
    # ====================================================================================================
    # APP SERVICE PLAN (Consumption Plan for Functions)
    # ====================================================================================================
    
    resource "azurerm_service_plan" "function_plan" {
      name                = local.app_service_plan_name
      resource_group_name = data.azurerm_resource_group.main.name
      location            = data.azurerm_resource_group.main.location
      os_type             = "Linux"  # Change to "Windows" if needed
      sku_name            = "Y1"     # Consumption plan
      
      tags = {
        Environment = var.environment
        Purpose     = "FunctionApp"
      }
    }
    
    # ====================================================================================================
    # FUNCTION APP (Equivalent to your Bicep template)
    # ====================================================================================================
    
    resource "azurerm_linux_function_app" "function_app" {
      name                = local.function_app_name
      resource_group_name = data.azurerm_resource_group.main.name
      location            = data.azurerm_resource_group.main.location
      service_plan_id     = azurerm_service_plan.function_plan.id
      
      storage_account_name       = azurerm_storage_account.function_storage.name
      storage_account_access_key = azurerm_storage_account.function_storage.primary_access_key
      
      site_config {
        application_stack {
          python_version = "3.9"  # Adjust based on your runtime
        }
        
        # CORS settings if needed
        cors {
          allowed_origins = ["*"]  # Restrict as needed
        }
      }
      
      app_settings = local.function_app_settings
      
      tags = {
        Environment = var.environment
        Purpose     = "FunctionApp"
      }
      
      lifecycle {
        ignore_changes = [
          app_settings["WEBSITE_RUN_FROM_PACKAGE"],
        ]
      }
    }
    
    # ====================================================================================================
    # FUNCTION CODE DEPLOYMENT (Equivalent to AzureFunctionApp@1 task)
    # ====================================================================================================
    
    # Create ZIP package from function code
    data "archive_file" "function_code" {
      type        = "zip"
      source_dir  = var.function_code_path
      output_path = "${path.module}/function-code.zip"
    }
    
    # Upload function code to storage blob
    resource "azurerm_storage_blob" "function_code" {
      name                   = "function-code-${data.archive_file.function_code.output_md5}.zip"
      storage_account_name   = azurerm_storage_account.function_storage.name
      storage_container_name = azurerm_storage_container.function_code.name
      type                   = "Block"
      source                 = data.archive_file.function_code.output_path
    }
    
    # Storage container for function code
    resource "azurerm_storage_container" "function_code" {
      name                  = "function-releases"
      storage_account_name  = azurerm_storage_account.function_storage.name
      container_access_type = "private"
    }
    
    # Generate SAS URL for the function code blob
    data "azurerm_storage_account_blob_container_sas" "function_code" {
      connection_string = azurerm_storage_account.function_storage.primary_connection_string
      container_name    = azurerm_storage_container.function_code.name
      https_only        = true
    
      start  = "2024-01-01T00:00:00Z"
      expiry = "2025-12-31T23:59:59Z"
    
      permissions {
        read   = true
        add    = false
        create = false
        write  = false
        delete = false
        list   = false
      }
    }
    
    # Update function app to run from package
    resource "azurerm_function_app_function" "update_package_url" {
      name            = "update-package"
      function_app_id = azurerm_linux_function_app.function_app.id
      
      # This is a workaround to update the WEBSITE_RUN_FROM_PACKAGE setting
      depends_on = [azurerm_storage_blob.function_code]
      
      lifecycle {
        replace_triggered_by = [
          azurerm_storage_blob.function_code.url
        ]
      }
    }
    
    # Alternative: Use local-exec provisioner to deploy code
    resource "null_resource" "deploy_function_code" {
      depends_on = [azurerm_linux_function_app.function_app]
      
      triggers = {
        code_hash = data.archive_file.function_code.output_md5
      }
      
      provisioner "local-exec" {
        command = <<-EOT
          # Option 1: Use Azure CLI (requires az cli and authentication)
          az functionapp deployment source config-zip \
            --resource-group ${data.azurerm_resource_group.main.name} \
            --name ${azurerm_linux_function_app.function_app.name} \
            --src ${data.archive_file.function_code.output_path}
          
          # Option 2: Use REST API with curl (alternative)
          # curl -X POST \
          #   -H "Content-Type: application/zip" \
          #   -H "Authorization: Bearer $(az account get-access-token --query accessToken -o tsv)" \
          #   --data-binary @${data.archive_file.function_code.output_path} \
          #   "https://${azurerm_linux_function_app.function_app.name}.scm.azurewebsites.net/api/zipdeploy"
        EOT
      }
    }
    
    # ====================================================================================================
    # OUTPUTS
    # ====================================================================================================
    
    output "function_app_url" {
      description = "Function App URL"
      value       = "https://${azurerm_linux_function_app.function_app.default_hostname}"
    }
    
    output "function_app_name" {
      description = "Function App name"
      value       = azurerm_linux_function_app.function_app.name
    }
    
    output "storage_account_name" {
      description = "Storage Account name"
      value       = azurerm_storage_account.function_storage.name
    }
    
    output "resource_group_name" {
      description = "Resource Group name"
      value       = data.azurerm_resource_group.main.name
    }
    

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.