Sahan Serasinghe

Software Engineer | Data Science Enthusiast

Serverless Go with Azure Functions and GitHub Actions

2022-09-12azure 7 min read

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.

serverless-go-with-azure-functions-github-actions-1

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.

The plan

  1. Clone the repo or create one
  2. Create the Azure function resources
  3. Test out the app
  4. 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 the FUNCTIONS_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 the quotes 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.

serverless-go-with-azure-functions-github-actions-2.png

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:

serverless-go-with-azure-functions-github-actions-3.png

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 and enableForwardingHttpRequest 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.

  1. Authenticate with Azure
  2. Build the project
  3. 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.

  1. 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
  2. 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:

deploy.yml

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.

serverless-go-with-azure-functions-github-actions-4.png

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.

  1. 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
  }
}
  1. 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

Loading...
Sahan Serasinghe - Tech Blog

Sahan Serasinghe Software Engineer at GitHub | Azure Solutions Architect | Master of Data Science at UIUC | CKAD