Back

Embedding a Web application in a Golang binary

Some time ago I wrote an article on how to run gRPC through a web application without Envoy. I'm going to use the project in this article…

by Percy Bolmér, December 27, 2020

By [Heliberto Arias] on Unsplash
By [Heliberto Arias] on Unsplash

Some time ago I wrote an article on how to run gRPC through a web application without Envoy. I’m going to use the project in this article, and embed the web application into a binary. If you’re not familiar with the project I suggest reading part one Using gRPC With TLS, Golang, React without a Reverse Proxy (Envoy)

But if you only want to learn how to use the embed package in Go you should be able to follow this article without prior knowledge. Why would we want to include things like an web application in a binary?

For me, it’s all about the deployment. Being able to just send a single binary to a server is a big time saver. Before Golang 1.16 (this is when the embed package is released), we would need to deploy the binary, and the web application, or any other files we needed that wasn’t compiled code.

To solve this I tend to use Docker, I know a lot of people also recommend Packer.

Don’t get me started on docker, I love it, I’m a super fanboy.

But, lets be honest, sometimes it would make much more sense with a binary than a docker.

Lets get started setting the project up

Before we start coding lets grab the project that we will modify.

mkdir embedding && cd embedding
git init
git pull https://github.com/percybolmer/grpcexample

Its a super simple gRPC API that has an Ping function, and a React application that is hosted on localhost:8080 which will call the Ping function each third second.

This is not important for the tutorial, you can use any other web application. If your interested in reading more about the project, see part one of this series.

If you are using the project you need to generate certificates, if you haven’t already. Go into the cert folder and run the certgen.sh script. This will require you to have openssl installed.

cd cert 
bash certgen.sh

We will also need to compile the react application that we are embedding. The application is located inside the ui folder.

Before running the build commands make sure you add two lines to the package.json file. We need to tell the React application that these files will be served without a server.

Add the two lines below to ui/pingpongapp/package.json

“homepage”: “.”,
”proxy”: “https://localhost:8080",

Run the following commands.

npm install
npm run build

npm install will download any needed dependencies, and npm run build will build a static application located inside a new folder called build. This is the folder that we will embed later. You will also need to run at least Golang 1.16 Test your installation by running

go version
You should see an output that mentions the current version
You should see an output that mentions the current version

Lets embed

Once your all setup, open main.go. You can read about the embed package link here

What we need is to embed our build folder and embed that into our binary as an filesystem. I recommend reading the linked package docs, its really short and explains everything very well.

The embed package differs from the rest of the packages I’ve seen before. We will actually use comments to tell the compiler what action to perform.

The syntax is short and concise.

//go:embed $PATH
var content $WANTED_DATA_TYPE

There is 2 lines of code, one comment and one variable declaration. They have to be directly after each other, starting with the comment. See the $PATH and $WANTED_DATA_TYPE?

Those needs to be replace.

$PATH should be replaced with the path to the files or directories you want to include. A side note is that if you use the data type embed.FS, it accepts multiple paths in one comment, and also many comments at the same time.

$WANTED_DATA_TYPE is to be replaced with the any of the available data types:

  • string = Accepts a single file and upon building will read the content of that file into the variable as a string
  • []byte = Same as String, but as a []Byte
  • embed.FS = Now this is where embed gets exciting for me. We are allowed to embed multiple directories and files. And the best part, they are part of the io/FS interface. This means net/http package can use this out of the box.

With this new knowledge, lets update main.go to embed the build folder at ui/pingpongapp/build. In the old main.go the path is hard coded, that needs to be changed into an embedded filesystem.

package main

import (
	"log"
	"net"
	"net/http"
	"time"

	"embed"

	"github.com/improbable-eng/grpc-web/go/grpcweb"
	pingpong "github.com/percybolmer/grpcexample/pingpong"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
)

func main() {

	// We Generate a TLS grpc API
	apiserver, err := GenerateTLSApi("cert/server.crt", "cert/server.key")
	if err != nil {
		log.Fatal(err)
	}
	// Start listening on a TCP Port
	lis, err := net.Listen("tcp", "127.0.0.1:9990")
	if err != nil {
		log.Fatal(err)
	}
	// We need to tell the code WHAT TO do on each request, ie. The business logic.
	// In GRPC cases, the Server is acutally just an Interface
	// So we need a struct which fulfills the server interface
	// see server.go
	s := &Server{}
	// Register the API server as a PingPong Server
	// The register function is a generated piece by protoc.
	pingpong.RegisterPingPongServer(apiserver, s)
	// Start serving in a goroutine to not block
	go func() {
		log.Fatal(apiserver.Serve(lis))
	}()
	// Wrap the GRPC Server in grpc-web and also host the UI
	grpcWebServer := grpcweb.WrapServer(apiserver)
	// Lets put the wrapped grpc server in our multiplexer struct so
	// it can reach the grpc server in its handler
	multiplex := grpcMultiplexer{
		grpcWebServer,
	}

	// We need a http router
	r := http.NewServeMux()
	// Here we embed the buildt web application with the embed comment
	//go:embed ui/pingpongapp/build
	var app embed.FS
	// Convert the embed.FS into a http.FS and serve it
	webapp := http.FileServer(http.FS(app))
	// Host the Web Application at /, and wrap it in the GRPC Multiplexer
	// This allows grpc requests to transfer over HTTP1. then be
	// routed by the multiplexer
	r.Handle("/", multiplex.Handler(webapp))
	// Create a HTTP server and bind the router to it, and set wanted address
	srv := &http.Server{
		Handler:      r,
		Addr:         "localhost:8080",
		WriteTimeout: 15 * time.Second,
		ReadTimeout:  15 * time.Second,
	}
	// Serve the webapp over TLS
	log.Fatal(srv.ListenAndServeTLS("cert/server.crt", "cert/server.key"))

}

// GenerateTLSApi will load TLS certificates and key and create a grpc server with those.
func GenerateTLSApi(pemPath, keyPath string) (*grpc.Server, error) {
	cred, err := credentials.NewServerTLSFromFile(pemPath, keyPath)
	if err != nil {
		return nil, err
	}

	s := grpc.NewServer(
		grpc.Creds(cred),
	)
	return s, nil
}

type grpcMultiplexer struct {
	*grpcweb.WrappedGrpcServer
}

// Handler is used to route requests to either grpc or to regular http
func (m *grpcMultiplexer) Handler(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if m.IsGrpcWebRequest(r) {
			m.ServeHTTP(w, r)
			return
		}
		next.ServeHTTP(w, r)
	})
}

Lets build the binary and run it

go1.16beta1 build -o api . && ./api

Visit localhost:8080 and watch the status change from false to true after 3 seconds!

That’s it! I’ve gotta hand it to the team at Golang, they’ve made this super simple. We have in just 2 new lines of code added a embedded web application to our binary.

If your interested, feel free to read Part 2 about implementing gRPC interceptors.

If you enjoyed my writing, please support future articles by buying me an Coffee

Sign up for my Awesome newsletter