Published on

From Code to Cloud: Deploy Azure Functions with Terraform and GitHub Actions

Authors
  • avatar
    Name
    Dinh Nguyen Truong
    Twitter

In today’s world, Serverless Computing is gaining popularity. The main benefit is it helps developers build and deploy apps without worrying about the infrastructure. Besides, it makes the application highly scalable to meet the demand, and cost-effective, as you only pay for the resources you actually use.

But even when you don’t need to worry about the underlying infrastructure, managing complex applications can become a headache as your project grows. That’s how Terraform — a famous Infrastructure as Code tool — was born. It helps you manage all your cloud resources in a single configuration file.

And to demonstrate how simple it is,

We will build a CI/CD Pipeline to deploy an Azure function app using Terraform today

The flow will be simple like this: after pushing the function code to GitHub, GitHub Action workflow will be triggered and execute two main steps:

  • Deploying infrastructure: create the Azure function in Azure, or ignore if it exists.

  • Publish function code to Azure function app.

Prerequisites

  • Basic understanding of Terraform and GitHub Actions.

  • Azure account and subscription. If you don’t have it, register a new account to receive free credit for the first year. Don’t worry about the cost, all resources we use in this post cost you a single cent.

  • Installed tools: Azure CLI, Terraform.

  • Login Azure Cli to your account: az login ( If your account setup tenant, you might have to login to the exact tenant you want to use az login --tenant <tenant-id>)

Step 1: Create new Azure Function app

  • Create a new Azure Function project using Azure Functions Core Tools func init --worker-runtime typescript --model V4

  • Create an index function func new -n index -t HttpTrigger

Here is the src/functions/index.ts file for our Hello World function:

import { app, HttpResponseInit } from "@azure/functions";

app.http("index", {
  methods: ["GET", "POST"],
  authLevel: "anonymous",
  handler: async (): Promise<HttpResponseInit> => {
    return {
      status: 200,
      headers: {
        "Content-Type": "text/html",
      },
      body: "Hello World",
    };
  },
})
  • Test the function locally to ensure it works

Step 2: Setup Terraform Locally

Terraform will have a tfstate file to keep track all of the resources it manages. When we run it locally, terraform will create a local state file for that. But since we plan to run it on GitHub action, we have to setup the state file on a remote location, typically on a storage service, like Azure Blob Storage

Step 2.1 Setup Azure Blob Storage for Terraform backend

You could follow this guide from Microsoft

https://learn.microsoft.com/en-us/azure/developer/terraform/store-state-in-azure-storage

Key point

Your Terraform backend setup will look like this:

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~>3.0"
    }
  }
  backend "azurerm" {
      resource_group_name  = "tfstate"
      storage_account_name = "<storage_account_name>"
      container_name       = "tfstate"
      key                  = "terraform.tfstate"
  }
}

provider "azurerm" {
  features {}
}

And for the storage access key, set up your environment variable ARM_ACCESS_KEY. You always need this variable exporting when performing the terraform command locally. When running in GitHub actions, we’ll keep it on Github Workflow secrets.

export ARM_ACCESS_KEY=$(az storage account keys list --resource-group $RESOURCE_GROUP_NAME --account-name $STORAGE_ACCOUNT_NAME --query '[0].value' -o tsv)

That’s all you need. Run terraform init to initialize your environment and the backend.

Step 2.2 Setup Your Azure resources

When deploying Azure function app, we need 4 resources:

  • Resource Group

  • Storage Account

  • App Service Plan

  • Azure Function App

// .... terraform backend above
// you should create resource group yourself, put its name here instead of my rg's name
data "azurerm_resource_group" "simpleapp_resource_group" {
  name = "simpleapp-resource-group"
}
resource "azurerm_storage_account" "simpleapp_storage" {
  name                     = "simpleappstorageaccount"
  resource_group_name      = data.azurerm_resource_group.simpleapp_resource_group.name
  location                 = data.azurerm_resource_group.simpleapp_resource_group.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}
// we will use consumption plan
resource "azurerm_service_plan" "simpleapp_service_plan" {
  name                = "simpleapp-app-service-plan"
  location            = data.azurerm_resource_group.simpleapp_resource_group.location
  resource_group_name = data.azurerm_resource_group.simpleapp_resource_group.name
  sku_name            = "Y1"
  os_type             = "Linux"
}
resource "azurerm_linux_function_app" "simpleapp_app" {
  name                        = "simpleapp-app"
  location                    = data.azurerm_resource_group.simpleapp_resource_group.location
  resource_group_name         = data.azurerm_resource_group.simpleapp_resource_group.name
  service_plan_id             = azurerm_service_plan.simpleapp_service_plan.id
  storage_account_name        = azurerm_storage_account.simpleapp_storage.name
  storage_account_access_key  = azurerm_storage_account.simpleapp_storage.primary_access_key
  functions_extension_version = "~4"
  https_only                  = true
  identity {
    type = "SystemAssigned"
  }
  site_config {
    application_stack {
      node_version = 20
    }
  }
  // Ignore changes to these app settings to prevent Terraform from overwriting them
  // after the function app code is deployed. These settings reference the code location.
  lifecycle { 
    ignore_changes = [      app_settings["WEBSITE_RUN_FROM_PACKAGE"], 
      app_settings["WEBSITE_ENABLE_SYNC_UPDATE_SITE"]
    ] 
  }
}
output "simpleapp_app_url" {
  value = azurerm_linux_function_app.simpleapp_app.default_hostname
}

Run terraform plan to see the preview.

And confirm by terraform apply --auto-approve.

Step 3: Setup GitHub Actions

We need 2 main jobs for our workflow:

  • Config Terraform

  • Deploy Azure function app

Step 3.1: Config Terraform job

Create new Github workflow file at .github/workflows/terraform_azfunction.yml

name: "Terraform + Azure Function"
env:
  ARM_ACCESS_KEY: ${{ secrets.ARM_ACCESS_KEY }} # For the remote backend
          # You need to setup github action secret ARM_ACCESS_KEY 

jobs:
  terraform:
    name: "Terraform Deploy"
    runs-on: ubuntu-latest
    environment: production
    defaults:
      run:
        shell: bash
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      # Acquire azure credential
      - name: Azure Login
        uses: azure/login@v2
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.9.2"

      # Initialize Terraform by downloading modules, loading any remote state, etc.
      - name: Terraform Init
        run: terraform init
        working-directory: ./terraform # our main.tf located at ./terraform

      # Generates an execution plan for Terraform
      - name: Terraform Plan
        run: terraform plan
        working-directory: ./terraform

      - name: Terraform Apply
        run: terraform apply -auto-approve
        working-directory: ./terraform

This job contains 3 small steps:

  • Login to azure

  • Initialize Terraform module, loading remote state,…

  • Execute Terraform

Why GitHub action need to acquire Azure credentials? To deploy the resources (When we run Terraform locally, Terraform already had the access — we run az login to login to your account)

Login to Azure contains many small steps, so I will leave the documentation link below.

Azure Login · Actions · GitHub Marketplace

There are many ways to login, but I recommend you use the Login With a Service Principal Secret.

Remember to setup Github action secret AZURE_CREDENTIALS for your repo after this.

Step 3.2: Publish your function code job

name: "Terraform + Azure Function"
env:
  ARM_ACCESS_KEY: ${{ secrets.ARM_ACCESS_KEY }}
  AZURE_FUNCTIONAPP_NAME: "simpleapp-app" # set this to your function app name on Azure
  AZURE_FUNCTIONAPP_PACKAGE_PATH: "." # set this to the path to your function app project, defaults to the repository root
  NODE_VERSION: "20.x"

jobs: 
 terraform: # the terrform job above
  azfunction:
    name: "Azure Function Deploy"
    runs-on: ubuntu-latest
    needs: [terraform] # run after terraform job finish
    environment: production
    defaults:
      run:
        shell: bash

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node ${{ env.NODE_VERSION }} Environment
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODE_VERSION }}

      - name: Resolve Project Dependencies Using Npm
        shell: bash
        run: |
          pushd './${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}'
          npm install
          npm run build --if-present
          popd

      - name: Azure Login
        uses: azure/login@v2
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Run Azure Functions Action
        uses: Azure/functions-action@v1
        with:
          app-name: ${{ env.AZURE_FUNCTIONAPP_NAME }}
          package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}

This job is just that simple, we checkout the project, set up node version, build project, login to azure and publish code to our function app.

Step 4: Test

Now, push your repository to GitHub, you should see a new GitHub action running

  • Check the log to confirm it running successfully

  • Check your Azure resources to confirm all resources are deployed

  • Check your Hello World app at <azure function app url>/api/index

Conclusion

You should have a working pipeline now, when you want to add more resources, extend your main.tf file. That’s the beauty of Infrastructure as Code.

I hope you find this post useful, feel free to comment any question you have.

Additional Resources