- Published on
From Code to Cloud: Deploy Azure Functions with Terraform and GitHub Actions
- Authors
- Name
- Dinh Nguyen Truong
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 useaz 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
- My complete code in this post: compimprove/deploy-azfunction-terraform-githubaction