Serverless Go with Azure Functions and GitHub Actions
In this article, we are going to learn how to host a serverless Go web service with Azure Functions leveraging custom handlers in Azure functions. We will also automate the deployments by using GitHub Actions 🚀 You can even apply the same concepts to other languages Rust as long as you can self-contained binary.
Walkthrough video
If you prefer to watch a video, I have created a walkthrough video on my YouTube channel 😊
Introduction
Azure Functions is Microsoft’s offering for serverless computing that enables you to run code on-demand without having to explicitly provision or manage infrastructure. Azure Functions is a great way to run your code in response to events, such as HTTP requests, timers, or messages from Azure services.
As of today, they support multiple runtimes such as .NET, Java, JavaScript, Python, TypeScript. But what if we want to write our app in Go? Well, now you can do that too - by using Custom Handlers.
Before we move on, so what really is a custom handler and how does it work? Custom handlers let your Function app to accept events (eg. HTTP requests) from the Global host (aka Function host - that powers your Function apps) - as long as your chosen language supports HTTP primitives.
Here’s a great overview from Microsoft on how this is achieved.
Source: Microsoft Docs
So, in our case, we are going to wrap a Go binary as a Function app and deploy it to Azure. Sounds good? Let’s jump right into it.
Prerequisites
Make sure you have the following setup locally.
- Go SDK
- Azure Functions Core Tools
- Prefererrably, Azure Functions VS Code extension
The plan
- Clone the repo or create one
- Create the Azure function resources
- Test out the app
- Deploy to Azure
1. Clone the repo or create one
Our code is going to be pretty simple. All it does is, whenever we make a request, it will return a random quote on programming.
💡 You can clone the repo I have created from here.
package main
// Removed for brevity
var quotes = []string{
"Talk is cheap. Show me the code.",
"First, solve the problem. Then, write the code.",
"Experience is the name everyone gives to their mistakes.",
"Any fool can write code that a computer can understand. Good programmers write code that humans can understand.",
}
func quotesHandler(w http.ResponseWriter, r *http.Request) {
// Get a random quote
message := quotes[rand.Intn(len(quotes))]
// Write the response
fmt.Fprint(w, message)
}
func main() {
listenAddr := ":8080"
if val, ok := os.LookupEnv("FUNCTIONS_CUSTOMHANDLER_PORT"); ok {
listenAddr = ":" + val
}
http.HandleFunc("/api/GetQuotes", quotesHandler)
log.Printf("About to listen on %s. Go to https://127.0.0.1%s/", listenAddr, listenAddr)
log.Fatal(http.ListenAndServe(listenAddr, nil))
}
Explanation
- We setting port
8080
as the default port for our app. But if we are running this in Azure Functions, we will be using a different port. So, we are checking if theFUNCTIONS_CUSTOMHANDLER_PORT
environment variable is set. If it is, we will use that port instead. - We are registering a handler for the
/api/GetQuotes
endpoint. This is the endpoint that we will be using to make requests to our app. quotesHandler
is a simple function that returns a random quote from thequotes
array.
2. Create the Azure function resources
You can create your own Azure function from portal.azure.com or using ARM templates. Below are the configuration I chose.
- Publish: Code
- Runtime Stack: Custom Handler
- Operating System: Linux
- Plan type: Consumption (Serverless)
3. Test out the app
If you have cloned the project you should see a folder structure similar to what’s shown below.
At the root of the project folder let’s run the following commands.
go build handler.go # To build a binary
func start # Start the Function app service
You should see the output like so:
Let’s run through each file now.
- GetQuotes/function.json: This file defines what happens when a request comes in and what should go out from the function. These are known as bindings. Our function is triggered by HTTP requests and we will return a response
- handler.go: This is our Go web service where our main logic lives in. We listen on port 8080 and expose an HTTP endpoint called
/api/GetQuotes
- host.json: Take note under
customHandler.description.defaultExecutablePath
is set to handler which says where to find the compiled binary of our Go app andenableForwardingHttpRequest
where we tell the function host to forward our traffic
4. Deploy to Azure with GitHub Actions
Now that we have everything ready to go let’s deploy this to Azure! 🚀 To get this done, we are going to use the Azure Functions Action from the GH Actions Marketplace.
From a high-level this is what we need to do in order to deploy this.
- Authenticate with Azure
- Build the project
- Deploy
Authenticate with Azure
Since our app is written in “Go” which is not really supported out of the box, we won’t be able to use the Publish Profile method for this. So we are going to focus on using an Azure Service Principal for RBAC.
💡 Remember to follow the steps according to this guide to create an SP.
-
Once you have created the service principal, we need to add that as a secret in our repo so that it can be used for authenticating with Azure Resource Manager during the deployment step. Head over to your repo → Settings → Secrets → Actions
-
Create a secret a called
AZURE_RBAC_CREDENTIALS
and update its content with what you got when you created the service principal.
Now the that the credentials are in place we can refer to that from the GitHub Action workflow file.
Build the project
This step is pretty straightforward as we are using the Go SDK to build the project with GOOS=linux GOARCH=amd64
config.
This is what the final workflow file should look like:
name: CI/CD
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
AZURE_FUNCTIONAPP_NAME: azgofuncapp # set this to your application's name
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 'Login via Azure CLI'
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_RBAC_CREDENTIALS }}
- name: 'Set up Go'
uses: actions/setup-go@v3
with:
go-version: 1.18
- name: Build
run: GOOS=linux GOARCH=amd64 go build handler.go
- name: 'Deploy to Azure'
uses: Azure/functions-action@v1
with:
app-name: ${{ env.AZURE_FUNCTIONAPP_NAME }}
Once everything is done, head over to the following URL and refresh a couple of times to see our programming quotes! 😀
https://<your_app_URL>.azurewebsites.net/api/GetQuotes
Here’s the an example.
Conclusion
Well, that’s it folks! Today we built a small Go web service, wrapped it in an Azure Function and deployed it to Azure by using GitHub Actions!
Troubleshooting
I ran into couple of issues when creating this project.
- Are you getting the
Value cannot be null. (Parameter 'provider')
error?
I was able to resolve it by following the exact config as below.
{
"version": "2.0",
"logging": {...},
"customHandler": {
"description": {
"defaultExecutablePath": "handler",
"workingDirectory": "",
"arguments": []
},
"enableForwardingHttpRequest": true
}
}
- Are you getting the
Azure Functions Runtime is unreachable
error?
For me, this went away when I did the first deployment. If it still doesn’t go away, check out this link for more info.
References
- https://docs.microsoft.com/en-us/azure/azure-functions/create-first-function-vs-code-other?tabs=go%2Cmacos
- https://docs.microsoft.com/en-us/azure/azure-functions/event-driven-scaling
- https://github.com/Azure/azure-functions-dotnet-worker/issues/810
- https://github.com/marketplace/actions/azure-functions-action#using-azure-service-principal-for-rbac-as-deployment-credential