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
}