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
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
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