Hosting a Go SPA on Azure App Service

A Simple Golang web server for a Single Page Application in Azure

I'll keep this brief. There are a lot of articles about hosting Single Page Applications on Azure and possibly even more examples of the trivial setup required for Golang web servers. I couldn't find anything together, though, when I perused the Microsoft Azure Go page. My teammate works in React, and I wanted to try hosting a React SPA on Go as minimally as possible because...well, because.

It worked great on my machine...

Ultimately, a simple implementation worked great locally until I put it in a pipeline and released it to an empty and awaiting Azure App Service that I stood up in our resource group.

Once the page was deployed successfully in Azure, nothing happened. It just spun for a while until I received a strange 503 message. After I dug through Azure's documentation, I realized the last culprit that could cause that behavior was a timeout, and my gut told me the server obviously wasn't spinning up properly for the app service to have any idea of what was what. My suspicion was that the port for the application was not anything usual like 443, which I used locally, so I passed in an Azure-specific environment variable HTTP_PLATFORM_PORT that I vaguely recalled. Once that was used to create the endpoint, everything worked perfectly.

The code hosting my team's website

Now, I do have something fancier running in production that involves dependency injection and whatnot based on the code below, but a basic, functional SPA web server that'll run in an Azure App Service can be copied here:

package main

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

func getAzurePorts()(AzurePorts string) {
	// Gets the open port for the hosted App Serv
	value, ok := os.LookupEnv("HTTP_PLATFORM_PORT")
	if !ok {
		log.Fatal("Cannot find Environment Variable 'HTTP_PLATFORM_PORT'")
	}
	return ":" + value
}

func main() {
	fmt.Println("Starting web server")

	// Main router static pages within our SPA; create multiple for several pages
	http.HandleFunc("/", handler)

	if err := http.ListenAndServe(getAzurePorts(), nil); err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}

// Main HTTP request handler; abstracted as a demo if there were separate routes
func handler(writer http.ResponseWriter, request *http.Request) {

	// Gets the working directory only to serve up the SPA files
	dir, err := os.Getwd()
	if err != nil {
		log.Fatal(err)
	}

	// Serve ONLY the site's working directory (allows rendering the web site file)
	http.FileServer(http.Dir(dir)).ServeHTTP(writer, request)
}
The moral of the story is use Azure's environment variables for values such as the open ports in the app service for the Go web server

This actually could be written with less code. There could be pretty much none of the logging, as I'm already reliant on Go to fail elegantly, but I think this is a healthy medium for a basic, working example.

What's going on here?

Honestly, not much. Go fires up a web server and registers the listening port available for the app service, and the SPA is available by exposing the FileServer. The way I have this working is extremely simple, but it serves its purpose.

Two Important Points:

  1. If I used ServeFile and not FileServer, the directory would not be open to reference and you'd get an empty, non-rendered React.js page.
  2. The secret sauce for Azure is checking for the active, open port with the environment variable `HTTP_PLATFORM_PORT`. The Golang server would host locally jus fine, and you could even assign a port, but that's not what you get functionally when you deploy to an App Service.

And There You Have It

In only a few minutes, we've easily built an extremely light-weight web server in Go and are hosting a React.js page on top of it. Since it's a simple page that's really just hosting some information about our team and links to our resources, no authorization is required, so I authenticated by enabling Azure Active Directory for our organization.

And that's that! The solution, arguably, is overkill, but it really is very lightweight and introduces the opportunity to add functionality since it is a proper server.