Go on Azure Functions with Custom Handlers

Go on Azure Functions with Custom Handlers

Overview

I recently worked on a project where we wanted to port an existing API to Azure. Problem was all the code was written in Go and Azure is not known for its Go support. Enter Azure Functions custom handlers which allows you to bring your own HTTP server to receive and respond to Azure Functions requests. I was impressed by this preview feature and how easily it enabled us to bring our existing code to Azure. In this post, I’m going to walk through creating some simple Go based Azure Functions to demonstrate how it works.
 


 

At a high level, custom handlers enables the Azure Function host to proxy requests to a HTTP server written in any language. The server application must run on the Azure Function host (meaning you may need to cross-compile the application to work on the Function Host’s operating system). The only requirements are the server must be able to receive and return HTTP requests in a specific, Azure Function centric format and the server needs to startup and respond in 60 seconds or less.
 
 

What We’re Building

The problem we’re going to solve is the automated creation of developer environments on Azure, called “playgrounds”. The goal is to add some automation around the creation of resource groups our developers need to create and test their applications. The requirements our functions needs to meet are:

 

  • Creation of “Playgrounds” (a.k.a. resource groups) by an administrator and assignment of access to a developer
  • Getting list and details of existing “Playgrounds” to help with management
  • Deletion of “Playgrounds” once the developer’s work has completed

 
 

 
We also have some requirements for developer experience:

  • Short inner loop for local development (more info)
  • Automated deployment of infrastructure and code checked into the master branch
  • Logging and monitoring for the functions

 
 

Testing Go Custom Handlers

 
NOTE: Full code for this section is here.
 

The Azure Functions team has provided a set of basic samples on Github that walk through some basic input and output binding scenarios. The official documentation is also quite good in describing the request and response payload required for writing a custom handler. Let’s start with a basic, modified version of the samples repository to see if we can get a simple HTTP function working.

 
Let’s start with an empty directory and copy over the following files from the samples repository linked above:

 
We’ll need to make some changes in these files to get our basic sample working. Below is the new host.json code. The executable name needs to change for the Go HTTP server we’ll compile soon. Also, set enableForwardingHttpRequest to false so we’re able to handle other output bindings in the future (like blobs and queues).

 
host.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
	"version": "2.0",
	"extensionBundle": {
		"id": "Microsoft.Azure.Functions.ExtensionBundle",
		"version": "[1.*, 2.0.0)"
	},
	"customHandler": {	    
		"description": {
			"defaultExecutablePath": "azure-playground-generator"
		},
		"enableForwardingHttpRequest": true
	}
}

 

We’ll need to pair down and modify our Go code quite a bit to isolate our sample and properly instruct our output HTTP binding to return the correct headers, status code, and body.
 

GoCustomHandlers.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
)

type InvokeResponse struct {
	Outputs     map[string]interface{}
	Logs        []string
	ReturnValue interface{}
}

func httpTriggerHandler(w http.ResponseWriter, r *http.Request) {
	headers := make(map[string]interface{})
	headers["Content-Type"] = "application/json"

	res := make(map[string]interface{})
	res["statusCode"] = http.StatusOK
	res["headers"] = headers
	res["body"] = "{\"value\": \"test return value\"}"

	outputs := make(map[string]interface{})
	outputs["res"] = res
	invokeResponse := InvokeResponse{outputs, nil, nil}

	js, err := json.Marshal(invokeResponse)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	w.Write(js)
}

func main() {
	customHandlerPort, exists := os.LookupEnv("FUNCTIONS_CUSTOMHANDLER_PORT")
	if exists {
		fmt.Println("FUNCTIONS_CUSTOMHANDLER_PORT: " + customHandlerPort)
	}
	mux := http.NewServeMux()
	mux.HandleFunc("/HttpTriggerWithOutputs", httpTriggerHandler)
	fmt.Println("Go server Listening...on FUNCTIONS_CUSTOMHANDLER_PORT:", customHandlerPort)
	log.Fatal(http.ListenAndServe(":"+customHandlerPort, mux))
}

 

And finally, we’ll need to setup our function bindings. Note the modification of $return to a http output - this is required to properly set the res output (but may be a bug that will be fixed).
 

HttpTriggerWithOutputs/function.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
    "bindings": [
      {
        "type": "httpTrigger",
        "direction": "in",
        "name": "req",
        "methods": [
          "get",
          "post"
        ]
      },
      {
        "type": "http",
        "direction": "out",
        "name": "res"
      },
      {
        "name": "$return",
        "type": "http",
        "direction": "out"
      }
    ]
  }

 

Now we just need to compile our Go HTTP server and run our function.

1# Compile the server
2go build -o azure-playground-generator
3chmod +x azure-playground-generator
4
5# Start Azure Function locally
6func host start

 

This will expose http://localhost:7071/api/HttpTriggerWithOutputs which we can test to ensure we are receiving the body, content type, and response code we specified.

 
 

Structuring The Project

 
NOTE: Full code for this section is here.
 

Before adding any more functionality to our code, let’s spend some time maturing the project structure. I’ll be leaning heavily on the project layout pattern as defined in this repository, especially since I’m a Golang newcomer. If you are creating your own Go on Azure project, I recommend reviewing this repository for pointers. Now, we’re going to start with the below project structure. The goal is to split out separate responsibilities into separate packages of our project (config, API helpers, standard errors, and a shell for the playground functionality) and isolate the Azure Function specific configuration. We’re also adding some automation around building and running the code to reduce our inner loop.
 

 1.
 2├── functions
 3│   ├── HttpTriggerWithOutputs
 4│   │   └── function.json
 5│   └── host.json
 6├── internal
 7│   └── config
 8│       ├── config.go
 9│       └── env.go
10├── pkg
11│   ├── api
12│   │   ├── request.go
13│   │   └── response.go
14│   ├── errors
15│   │   └── error.go
16│   └── playground
17│       └── test.go
18├── scripts
19│   ├── build.sh
20│   └── run-function.sh
21├── Makefile
22├── go.mod
23└── main.go

 

First, create a functions folder at the root of the repository and copy the HttpTriggerWithOutputs folder and the host.json file into this folder. While this doesn’t follow the guide mentioned above, these files are special enough to warrant their own directory. No changes are required for these files.
 

Next, create the config.go and env.go files inside of internal/config. We only have one value for configuration at the moment (the Go server port), but this isolation is useful when adding more configuration. This setup will allow setting our configuration by passing environment variables.
 

internal/config/config.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Package config manages loading configuration from environment
package config

// Source:
// https://github.com/Azure-Samples/azure-sdk-for-go-samples/blob/3d51ac8a1a5097b8881a8cf29888d4a44f7205f5/internal/config/config.go

var (
	functionHTTPWorkerPort int = 8082
)

// FunctionHTTPWorkerPort is the port the go server will run on and Azure Function will send requests to
func FunctionHTTPWorkerPort() int {
	return functionHTTPWorkerPort
}

 

internal/config/env.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Package config manages loading configuration from environment
package config

// Source:
// https://github.com/Azure-Samples/azure-sdk-for-go-samples/blob/3d51ac8a1a5097b8881a8cf29888d4a44f7205f5/internal/config/config.go

var (
	functionHTTPWorkerPort int = 8082
)

// FunctionHTTPWorkerPort is the port the go server will run on and Azure Function will send requests to
func FunctionHTTPWorkerPort() int {
	return functionHTTPWorkerPort
}

 

Next, let’s isolate API handling specific code into pkg/api. Create two files here: request.go for helping handle requests, and response.go for help with building response objects. As we add more Azure Functions, isolating this code will be useful to standardize how we handle requests and responses.
 

pkg/api/request.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package api

import (
	"encoding/json"
	"net/http"

	"azure-playground-generator/pkg/errors"
	"azure-playground-generator/pkg/playground"
)

// InvokeRequest is a struct that represents an Azure Function input
type InvokeRequest struct {
	Data     map[string]interface{}
	Metadata map[string]interface{}
}

// InvokeHTTPRequest is a struct that represents an Azure Function input with HTTP input
type InvokeHTTPRequest struct {
	Data     map[string]HTTPInput
	Metadata map[string]interface{}
}

// HTTPInput is a struct that represents the data of a HTTP binding for Azure Functions
type HTTPInput struct {
	Body    string
	Headers map[string][]string
	//Identities string
	Method string
	Params map[string]string
	Query  interface{}
	URL    string
}

func requestDecoder(r *http.Request) (*HTTPInput, error) {

	// Decode Azure Function Request
	var invokeReq InvokeHTTPRequest
	d := json.NewDecoder(r.Body)
	decodeErr := d.Decode(&invokeReq)
	if decodeErr != nil {
		return nil, errors.NewBadRequest("Invalid format from function")
	}

	// Pull out request
	req, ok := invokeReq.Data["req"]
	if !ok {
		return nil, errors.NewBadRequest("Function input req not found or improperly constructed")
	}

	return &req, nil
}

// HTTPTriggerHandler returns test data from server request
func HTTPTriggerHandler(w http.ResponseWriter, r *http.Request) {

	// Decode request object
	req, err := requestDecoder(r)
	if err != nil {
		WriteHTTPErrorResponse(w, err)
		return
	}

	// Get data from playground package
	test, err := playground.Test(r.Context(), req.Body)
	if err != nil {
		WriteHTTPErrorResponse(w, err)
		return
	}

	WriteHTTPResponse(w, http.StatusOK, test)
}

 

pkg/api/response.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package api

import (
	"encoding/json"
	"net/http"

	"azure-playground-generator/pkg/errors"
)

// Source:
// https://github.com/Azure-Samples/functions-custom-handlers/blob/3bd2a534130992af6f8af6608a3dc6007fd31161/go/GoCustomHandlers.go
// https://github.com/Optum/dce/blob/790404bd0b336994d627add7d3a176d90d4d6156/pkg/api/error.go

// InvokeResponse is an Azure Function construct showing how the function expects a return from the Go server
type InvokeResponse struct {
	Outputs     map[string]interface{}
	Logs        []string
	ReturnValue interface{}
}

// HTTPBindingResponse is a output of InvokeResponse used to pass data back to a HTTP Output for an Azure Function
type HTTPBindingResponse struct {
	statusCode int
	body       interface{}
	headers    map[string]string
}

// WriteHTTPResponse takes data and writes it in a standard way to a http.ResponseWriter
func WriteHTTPResponse(w http.ResponseWriter, status int, body interface{}) {
	outputs := make(map[string]interface{})
	headers := make(map[string]string)
	headers["System"] = "Azure Playground Generator"
	headers["Content-Type"] = "application/json"

	// Serialize body
	jsonData, err := json.Marshal(body)
	if err != nil {
		WriteHTTPErrorResponse(w, errors.NewInternalServer("error serializing body", err))
		return
	}

	// Create response object
	res := make(map[string]interface{})
	res["statusCode"] = status
	res["body"] = string(jsonData)
	res["headers"] = headers
	outputs["res"] = res
	invokeResponse := InvokeResponse{Outputs: outputs}
	// Serialize response object
	j, err := json.Marshal(invokeResponse)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	w.Write(j)
}

// WriteHTTPErrorResponse takes an error and writes it in a standard way to a http.ResponseWriter
func WriteHTTPErrorResponse(w http.ResponseWriter, err error) {
	// If custom error object
	switch t := err.(type) {
	case errors.HTTPCode:
		WriteHTTPResponse(w, t.HTTPCode(), err)
		return
	}

	// If standard error
	WriteHTTPResponse(
		w,
		http.StatusInternalServerError,
		errors.NewInternalServer("unknown error", err),
	)
}

 

Now, lets add a couple standard errors to make creating errors in our application simpler. Create pkg/errors/error.go and add the below code.
 

pkg/errors/error.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package errors

import "net/http"

// Source:
//https://github.com/Optum/dce/blob/82521f9b906194df4b69ea8e852d9b3f763e4c89/pkg/errors/error.go

// StatusError is the custom error type we are using.
// Should satisfy errors interface
type StatusError struct {
	httpCode int
	cause    error
	message  string
}

// Error allows conversion to standard error object
func (se *StatusError) Error() string {
	return se.message
}

// HTTPCode returns the http code
func (se StatusError) HTTPCode() int { return se.httpCode }

// HTTPCode returns the API Code
type HTTPCode interface {
	HTTPCode() int
}

// NewBadRequest returns a new error representing a bad request
func NewBadRequest(m string) *StatusError {
	return &StatusError{
		httpCode: http.StatusBadRequest,
		cause:    nil,
		message:  m,
	}
}

// NewInternalServer returns an error for Internal Server Errors
func NewInternalServer(m string, err error) *StatusError {
	return &StatusError{
		httpCode: http.StatusInternalServerError,
		cause:    err,
		message:  m,
	}
}

 

For the final package in our project, let’s stub out the playground package and add a simple test method which will return some test data. We’re finally making use of the defined POST method of our function - here we will simply return the data sent in the request body to verify we are correctly parsing the object from Azure Functions.
 

pkg/playground/test.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package playground

import (
	"context"
)

// Test returns a Test string
func Test(ctx context.Context, reqBody interface{}) (interface{}, error) {
	// Build Response
	resp := make(map[string]interface{})
	resp["requestBody"] = reqBody
	resp["value"] = "test return value"

	return resp, nil
}

 

To wire all of this together, we need to create two files at the root of the project: main.go (which replaces GoCustomHandlers.go) and go.mod which will allow our project to reference its internal packages.
 

main.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
	"fmt"
	"log"
	"net/http"

	"azure-playground-generator/internal/config"
	"azure-playground-generator/pkg/api"
)

func main() {
	// Setup Config
	err := config.ParseEnvironment()
	if err != nil {
		log.Fatalf("Failed to parse env: %v", err)
	}

	// Setup Server
	mux := http.NewServeMux()
	mux.HandleFunc("/HttpTriggerWithOutputs", api.HTTPTriggerHandler)

	// Start Server
	fmt.Println("Go server Listening...on FUNCTIONS_CUSTOMHANDLER_PORT:", config.FunctionHTTPWorkerPort())
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", config.FunctionHTTPWorkerPort()), mux))
}

 

go.mod

1
2
3
4
5
6
7
module azure-playground-generator

go 1.14

require (
	github.com/gobuffalo/envy v1.9.0
)

 

Let’s also improve our development inner loop (the time it takes to build and run our application) to make it easier to test our changes. Create build.sh and run-function.sh under the scripts directory. Once you have done that, create a file called Makefile in the root of the repository that we’ll use for simple build and run commands.
 

scripts/build.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/bash

set -eou pipefail

SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"

# Build go code
cd ${SCRIPT_DIR}/../
go build -o functions/azure-playground-generator
chmod +x functions/azure-playground-generator

echo "Build complete!"

 

scripts/run-function.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/bash

set -eou pipefail

SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
cd $SCRIPT_DIR/../

# First ensure you have a proper .env file (in the same format as .env.sample) with the deployment values filled out
# Load settings from .env file
if [[ -f ".env" ]]; then
    export $(grep -v '^#' .env | xargs)
fi

cd $SCRIPT_DIR/../functions/
func host start

 

Makefile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
define PROJECT_HELP_MSG
Usage:
    make help               show this message
    make build              build the golang components of the function
    make run                run the solution locally
endef
export PROJECT_HELP_MSG

help:
	@echo "$$PROJECT_HELP_MSG" | less

build:
	bash ./scripts/build.sh

run:
	bash ./scripts/build.sh
	bash ./scripts/run-function.sh

 

Now let’s test! Fire up the function host with the make run command from the root of the repository. This will use the Makefile we just created to build our Go HTTP server and then launch the Azure Functions host for local execution. The function now will handle both GET requests and POST requests with a payload.
 


 


 

Adding “Playground” Functionality

 
NOTE: Full code for this section is here.
 

The goal of this sample is some light automation around the management of Azure resource groups. In this section, we’ll add code to interact with the Azure Management APIs to list, create, get, and delete resource groups, which maps to Playgrounds in our application. We’ll be adding actual functionality to our API to accomplish our basic use case to create “Playgrounds” or resource groups for developers to test out their applications.
 

Authenticating to Azure

 
Since the code will be accessing Azure and creating resource groups on the user’s behalf, we need to create an Azure service principal that can create resource groups on our behalf. Follow the instructions here to create a service principal with the Azure CLI and then make sure to give it “Contributor” rights to it can create the resource groups and “User Access Manager” rights to it can assign users to resource groups. Create a file named “.env” at the root of the repository in the following format which our Go application will load.
 

.env

1
2
3
4
5
6
# Needed for running Go sample
AZURE_SUBSCRIPTION_ID=00000000-0000-0000-0000-000000000000
AZURE_TENANT_ID=00000000-0000-0000-0000-000000000000
AZURE_CLIENT_ID=00000000-0000-0000-0000-000000000000
AZURE_CLIENT_SECRET=00000000-0000-0000-0000-000000000000
FUNCTIONS_HTTPWORKER_PORT=8082

 

Now let’s change our configuration loaded to pull these values from our application. I’ll be borrowing heavily from the Azure SDK for Go (link here and here). We need to add the values required by the SDK to access Azure to config.go and the appropriate loader to env.go.
 

internal/config/config.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package config

// Source:
// https://github.com/Azure-Samples/azure-sdk-for-go-samples/blob/3d51ac8a1a5097b8881a8cf29888d4a44f7205f5/internal/config/config.go

var (
	functionHTTPWorkerPort int = 8082
	subscriptionID         string
	userAgent              string
)

// FunctionHTTPWorkerPort is the port the go server will run on and Azure Function will send requests to
func FunctionHTTPWorkerPort() int {
	return functionHTTPWorkerPort
}

// SubscriptionID is a target subscription for Azure resources.
func SubscriptionID() string {
	return subscriptionID
}

// UserAgent specifies a string to append to the agent identifier.
func UserAgent() string {
	if len(userAgent) > 0 {
		return userAgent
	}
	return "playground-manager"
}

 

internal/config/env.go

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
	var err error

	// these must be provided by environment
	functionHTTPWorkerPortString, err := envy.MustGet("FUNCTIONS_HTTPWORKER_PORT")
	if err != nil {
		return fmt.Errorf("Expected env vars not provided: %s", err)
	}
	functionHTTPWorkerPort, err = strconv.Atoi(functionHTTPWorkerPortString)
	if err != nil {
		return fmt.Errorf("Expected env vars must be an integer: %s", err)
	}

	subscriptionID, err = envy.MustGet("AZURE_SUBSCRIPTION_ID")
	if err != nil {
		return fmt.Errorf("Expected env vars not provided: %s", err)
	}

	return nil
}

 

Accessing Azure with the Go SDK

 
Now that we can authenticate to Azure, let’s write the code to actually access Azure and translate from the Azure concept of resource groups to our application’s concept of Playgrounds. We’re going to focus on adding the list, get, create, and delete operations for Playgrounds in this example. But first, let’s add some common code to get started.
 

First, let’s add a model for our “Playground” object. This is almost a direct map from the Azure resource group object, except we have an owner field.
 

pkg/playground/model.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package playground

import (
	"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources"
)

// Playground is a resource group for our system
type Playground struct {
	ID       *string            `json:"id,omitempty"`
	Name     *string            `json:"name,omitempty"`
	Location *string            `json:"location,omitempty"`
	OwnerID  *string            `json:"ownerId,omitempty"`
	Tags     map[string]*string `json:"tags"`
}

func groupToPlayground(g resources.Group) Playground {
	var p Playground
	p.ID = g.ID
	p.Name = g.Name
	p.Location = g.Location
	p.Tags = g.Tags

	if val, ok := g.Tags["OwnerId"]; ok {
		p.OwnerID = val
	}

	return p
}

 

The common.go file is meant for code that may be used across the package. Here we have the code for the necessary Azure Go SDK clients.
 

pkg/playground/common.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package playground

import (
	"azure-playground-generator/internal/config"

	"github.com/Azure/azure-sdk-for-go/profiles/latest/authorization/mgmt/authorization"
	"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources"
	"github.com/Azure/go-autorest/autorest/azure/auth"
)

func getResourceGroupsClient() (resources.GroupsClient, error) {
	var groupsClient resources.GroupsClient
	var err error

	groupsClient = resources.NewGroupsClient(config.SubscriptionID())
	groupsClient.AddToUserAgent(config.UserAgent())
	authorizer, err := auth.NewAuthorizerFromEnvironment()

	if err == nil {
		groupsClient.Authorizer = authorizer
		return groupsClient, nil
	}

	return groupsClient, err
}

func getRoleAssignmentsClient() (authorization.RoleAssignmentsClient, error) {
	var roleClient authorization.RoleAssignmentsClient
	var err error

	roleClient = authorization.NewRoleAssignmentsClient(config.SubscriptionID())
	roleClient.AddToUserAgent(config.UserAgent())
	authorizer, err := auth.NewAuthorizerFromEnvironment()

	if err == nil {
		roleClient.Authorizer = authorizer
		return roleClient, nil
	}

	return roleClient, err
}

 
Before we forget, we will be needing two more custom errors in our error.go file. Add them now so we have something to reference!
 

pkg/errors/error.go

46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
		message:  m,
	}
}

// NewAlreadyExists returns a new error representing an already exists error
func NewAlreadyExists(name string) *StatusError {
	return &StatusError{
		httpCode: http.StatusConflict,
		cause:    nil,
		message:  fmt.Sprintf("%s already exists", name),
	}
}

// NewNotFound returns an a NotFound error with standard messaging
func NewNotFound(name string) *StatusError {
	return &StatusError{
		httpCode: http.StatusNotFound,
		cause:    nil,
		message:  fmt.Sprintf("%s not found", name),
	}
}

 
Now let’s start with our CRUD like operations. This code isn’t complete - there are edge cases that were not considered in the spirit of this sample. First we’ll retrieve a list of Playgrounds, converted from Azure resource groups. We can identify which resource groups mag to Playgrounds via the System tag.
 

pkg/playground/list.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package playground

import "context"

// ListPlaygrounds returns a list of all active playgrounds in subscription
func ListPlaygrounds(ctx context.Context) ([]Playground, error) {
	groupsClient, err := getResourceGroupsClient()
	if err != nil {
		return nil, err
	}

	// Get resource groups created by our Playground System
	groups, err := groupsClient.ListComplete(ctx, "tagName eq 'System' and tagValue eq 'Playground'", nil)
	if err != nil {
		return nil, err
	}

	playgrounds := make([]Playground, 0)
	for _, v := range *groups.Response().Value {
		playgrounds = append(playgrounds, groupToPlayground(v))
	}

	return playgrounds, nil
}

 
Next, to get a specific Playground, we look for a resource group with the same name. We also will throw one of our custom errors if the resource group is not found.
 

pkg/playground/get.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package playground

import (
	"azure-playground-generator/pkg/errors"
	"context"
	"net/http"
)

// GetPlayground returns a playground with a given name
func GetPlayground(ctx context.Context, name string) (*Playground, error) {

	groupsClient, err := getResourceGroupsClient()
	if err != nil {
		return nil, err
	}

	// See if playground exists (throw error if not)
	existResp, err := groupsClient.CheckExistence(ctx, name)
	if err != nil {
		return nil, err
	}
	if existResp.StatusCode == http.StatusNotFound {
		return nil, errors.NewNotFound(name)
	}

	group, err := groupsClient.Get(ctx, name)
	if err != nil {
		return nil, err
	}

	playground := groupToPlayground(group)

	return &playground, nil
}

 
Creating a Playground is our most complex function. Here we need to create a resource group and assign the correct permissions to the owner provided. We also check and see if the Playground already exists and return a custom error if so.
 

pkg/playground/create.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package playground

import (
	"azure-playground-generator/internal/config"
	"azure-playground-generator/pkg/errors"
	"context"
	"fmt"
	"net/http"

	"github.com/Azure/azure-sdk-for-go/profiles/latest/authorization/mgmt/authorization"
	"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources"
	"github.com/Azure/go-autorest/autorest/to"
	uuid "github.com/satori/go.uuid"
)

// CreatePlayground creates a specific resource group with playground attributes
func CreatePlayground(ctx context.Context, name string, location string, ownerPrincipalID string) (*Playground, error) {

	// Get Client
	groupsClient, err := getResourceGroupsClient()
	if err != nil {
		return nil, err
	}

	// See if group already exists (throw error if so)
	existResp, err := groupsClient.CheckExistence(ctx, name)
	if err != nil {
		return nil, err
	}
	if existResp.StatusCode != http.StatusNotFound {
		return nil, errors.NewAlreadyExists(name)
	}

	var parameters resources.Group
	parameters.Location = to.StringPtr(location)
	parameters.Tags = make(map[string]*string)
	parameters.Tags["System"] = to.StringPtr("Playground")
	parameters.Tags["OwnerId"] = to.StringPtr(ownerPrincipalID)

	group, err := groupsClient.CreateOrUpdate(ctx, name, parameters)
	if err != nil {
		return nil, err
	}

	// Assign Contributor Role
	err = assignPlaygroundRole(ctx, *group.ID, ownerPrincipalID, "b24988ac-6180-42a0-ab88-20f7382dd24c")
	if err != nil {
		return nil, err
	}
	// Assign User Access Administrator Role
	err = assignPlaygroundRole(ctx, *group.ID, ownerPrincipalID, "18d7d88d-d35e-4fb5-a5c3-7773c20a72d9")
	if err != nil {
		return nil, err
	}

	playground := groupToPlayground(group)
	return &playground, nil
}

func assignPlaygroundRole(ctx context.Context, scope string, ownerPrincipalID string, roleID string) (err error) {

	roleAssignmentClient, err := getRoleAssignmentsClient()
	if err != nil {
		return err
	}

	// Assign Contributor Role
	roleDefID := fmt.Sprintf("/subscriptions/%s/providers/Microsoft.Authorization/roleDefinitions/%s", config.SubscriptionID(), roleID)
	_, err = roleAssignmentClient.Create(
		ctx,
		scope,
		uuid.NewV1().String(),
		authorization.RoleAssignmentCreateParameters{
			Properties: &authorization.RoleAssignmentProperties{
				PrincipalID:      to.StringPtr(ownerPrincipalID),
				RoleDefinitionID: to.StringPtr(roleDefID),
			},
		},
	)

	if err != nil {
		return err
	}

	return nil
}

 
Here we enable deleting of a Playground given it’s name. If the Playground is not found, then we return a custom error again.
 

pkg/playground/delete.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package playground

import (
	"azure-playground-generator/pkg/errors"
	"context"
	"net/http"
)

// DeletePlayground deletes a playground with a given name
func DeletePlayground(ctx context.Context, name string) (interface{}, error) {

	groupsClient, err := getResourceGroupsClient()
	if err != nil {
		return nil, err
	}

	// See if playground exists (throw error if not)
	existResp, err := groupsClient.CheckExistence(ctx, name)
	if err != nil {
		return nil, err
	}
	if existResp.StatusCode == http.StatusNotFound {
		return nil, errors.NewNotFound(name)
	}

	response, err := groupsClient.Delete(ctx, name)
	if err != nil {
		return nil, err
	}

	return response, nil
}

 

Wiring up the HTTP server

 
We are making progress! Now we need to enable our Playground package to be called from a HTTP request. I’ve decided to do all of this in the request.go file. As you
 

pkg/api/request.go

 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
	return &req, nil
}

// PlaygroundListHandler returns playgrounds from a Gin server request
func PlaygroundListHandler(w http.ResponseWriter, r *http.Request) {

	playgrounds, err := playground.ListPlaygrounds(r.Context())
	if err != nil {
		WriteHTTPErrorResponse(w, err)
		return
	}

	WriteHTTPResponse(w, http.StatusOK, playgrounds)
}

// PlaygroundCreateHandler creates a playground from a Gin server request
func PlaygroundCreateHandler(w http.ResponseWriter, r *http.Request) {

	// Decode request object
	req, err := requestDecoder(r)
	if err != nil {
		WriteHTTPErrorResponse(w, err)
		return
	}

	// Decode Body
	var playgroundReq playground.Playground
	err = json.Unmarshal([]byte(req.Body), &playgroundReq)
	if err != nil {
		WriteHTTPErrorResponse(w, errors.NewBadRequest("invalid input"))
		return
	}

	// Test inputs
	if playgroundReq.Name == nil || playgroundReq.Location == nil || playgroundReq.OwnerID == nil {
		WriteHTTPErrorResponse(w, errors.NewBadRequest("Please provide a name, location, and ownerId for this request"))
		return
	}

	// Create playground
	resp, err := playground.CreatePlayground(r.Context(), *playgroundReq.Name, *playgroundReq.Location, *playgroundReq.OwnerID)
	if err != nil {
		WriteHTTPErrorResponse(w, err)
		return
	}

	WriteHTTPResponse(w, http.StatusCreated, resp)
}

// PlaygroundGetHandler gets a specific playground (given the name) from a Gin server request
func PlaygroundGetHandler(w http.ResponseWriter, r *http.Request) {
	// Decode request object
	req, err := requestDecoder(r)
	if err != nil {
		WriteHTTPErrorResponse(w, err)
		return
	}

	// Get name parameter
	name, ok := req.Params["name"]
	if !ok {
		WriteHTTPErrorResponse(w, errors.NewBadRequest("Get Playground requires the name URL parameter"))
		return
	}

	// Get and return playground
	playgroundRtn, err := playground.GetPlayground(r.Context(), name)
	if err != nil {
		WriteHTTPErrorResponse(w, err)
		return
	}

	WriteHTTPResponse(w, http.StatusOK, playgroundRtn)
}

// PlaygroundDeleteHandler deletes a specific playground (given the name) from a Gin server request
func PlaygroundDeleteHandler(w http.ResponseWriter, r *http.Request) {

	// Decode request object
	req, err := requestDecoder(r)
	if err != nil {
		WriteHTTPErrorResponse(w, err)
		return
	}

	// Get name parameter
	name, ok := req.Params["name"]
	if !ok {
		WriteHTTPErrorResponse(w, errors.NewBadRequest("Delete Playground requires the name URL parameter"))
		return
	}

	// Delete Playground
	playgroundRtn, err := playground.DeletePlayground(r.Context(), name)
	if err != nil {
		WriteHTTPErrorResponse(w, err)
		return
	}

	WriteHTTPResponse(w, http.StatusAccepted, playgroundRtn)
}

 

main.go

19
20
21
22
23
24
25
26
27
28
29
	// Setup Server
	mux := http.NewServeMux()
	mux.HandleFunc("/playground-list", api.PlaygroundListHandler)
	mux.HandleFunc("/playground-create", api.PlaygroundCreateHandler)
	mux.HandleFunc("/playground-get", api.PlaygroundGetHandler)
	mux.HandleFunc("/playground-delete", api.PlaygroundDeleteHandler)

	// Start Server
	fmt.Println("Go server Listening...on FUNCTIONS_CUSTOMHANDLER_PORT:", config.FunctionHTTPWorkerPort())
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", config.FunctionHTTPWorkerPort()), mux))
}

 

Adding Function Configuration

We need to add four new function configurations for the four operations we just added so we can expose them to the Azure Function host. Add these
 

functions/playground-list/function.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
    "bindings": [
        {
            "authLevel": "function",
            "type": "httpTrigger",
            "direction": "in",
            "name": "req",
            "methods": [
                "get"
            ],
            "route": "playground"
        },
        {
            "type": "http",
            "direction": "out",
            "name": "res"
        },
        {
            "name": "$return",
            "type": "http",
            "direction": "out"
        }
    ]
}

 

functions/playground-get/function.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
    "bindings": [
        {
            "authLevel": "function",
            "type": "httpTrigger",
            "direction": "in",
            "name": "req",
            "methods": [
                "get"
            ],
            "route": "playground/{name}"
        },
        {
            "type": "http",
            "direction": "out",
            "name": "res"
        },
        {
            "name": "$return",
            "type": "http",
            "direction": "out"
        }
    ]
}

 

functions/playground-create/function.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
    "bindings": [
        {
            "authLevel": "function",
            "type": "httpTrigger",
            "direction": "in",
            "name": "req",
            "methods": [
                "post"
            ],
            "route": "playground"
        },
        {
            "type": "http",
            "direction": "out",
            "name": "res"
        },
        {
            "name": "$return",
            "type": "http",
            "direction": "out"
        }
    ]
}

 

functions/playground-delete/function.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
    "bindings": [
        {
            "authLevel": "function",
            "type": "httpTrigger",
            "direction": "in",
            "name": "req",
            "methods": [
                "delete"
            ],
            "route": "playground/{name}"
        },
        {
            "type": "http",
            "direction": "out",
            "name": "res"
        },
        {
            "name": "$return",
            "type": "http",
            "direction": "out"
        }
    ]
}

 
 

Automated Deployment

 
Coming Soon!
 
 

Logging and Monitoring

 
Coming Soon!
 
 

More Resources

For more information on Azure Function custom handlers, check out the following resources:

 
 

Disclaimer - While I work at Microsoft, I do not work on Azure Functions or with the Azure Functions team. This post is a analysis of my work with the technology as an end-user. Microsoft and Azure are registered trademarks ot Microsoft Corporation