Short and simple HTTP redirects

This week happy circumstances made me the proud owner of the rather short domain X.GL. Not yet sure what to make out of it I started with what seemed obviously cool to me: Craft myself one of the shortest possible email addresses. From now on you can save yourself some typing when addressing a message to me. Just send your love letters to d@x.gl.

And then it struck me that a short domain asks to be used for URL shortening. Since I was looking for something just for myself I wanted to keep the design simple. Here’s the idea:

  1. A small Go program runs a web server serving a static page to anyone visiting the domain directly.
  2. Requests to certain keyword paths, such as x.gl/foo, shall be redirected to a hardcoded destination. This is the core functionality of the URL shortening service.
  3. Running the service should be low-cost and low-maintenance.
  4. Having some statistics would be nice, for example finding out which ones are the most popular short URLs.
  5. The code would be stored in a Github repository and adding short URLs would be as user friendly as adding an item to the hardcoded list.
  6. Consequently, whenever the code changes a deployment pipeline would need to take care of updating the service.

The code was rather simple. I started off with a map of paths and their redirect targets. Using a map allows for fast lookups. Then I defined a handler for all paths that start with a forward slash /. The root path serves a static file index.html, all paths existing in the map redirect to their respective targets, and all other paths redirect to the root page. Nothing fancy here.

package main

import (
   "net/http"
)

var targets = map[string]string{
   "dan": "https://danrl.com/whois",
   "t":   "https://twitter.com/danrl_com",
   "omw": "https://OnMyWayToSpace.com",
}

func main() {
   http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
       if req.URL.Path == "/" {
           http.ServeFile(w, req, "index.html")
       } else if target, ok := targets[req.URL.Path[1:]]; ok {
           http.Redirect(w, req, target, http.StatusTemporaryRedirect)
       } else {
           http.Redirect(w, req, "/", http.StatusTemporaryRedirect)
       }
   })
   http.ListenAndServe(":8080", nil)
}

The code was simple and elegant. Of course this is a fanboy speaking, but Go really is a great language for exactly this scenario. Next up was building the binary:

FROM golang:alpine AS build
RUN apk add build-base
WORKDIR /src
COPY . .
RUN go build \
   -mod vendor \
   -ldflags "-linkmode external -extldflags -static" \
   -o /app \
   .

Note the command line flags -ldflags "-linkmode external -extldflags -static" which tell Go to build a statically linked binary. Thus it can be run without any userspace libraries present in the production environment.

Due to the lack of outbound, TLS-secured connections in the server binary, it does not need the usual set of certificate authority (CA) certificates. Neither needing userspace libraries nor certificates we can wrap the binary in an empty (scratch) docker image:

FROM scratch
WORKDIR /
COPY index.html index.html
COPY --from=build /app .
CMD ["/app"]

Once I had a working image I looked for a simple way to run it. Cloud Run on Google Cloud was my friend. Just when I wanted to head to Cloud Build to connect my Github repository and configure build triggers I spotted a new option in the console. There’s now the option for inline configurations which allow you to directly link a git branch to Cloud Run. No need for manual setup of triggers anymore.

build config github,small

In the background a regular build trigger was created and built images were stored in Container Registry like always.

build trigger,small

Finally, I mapped the domain to the service and added DNS records to my new favorite domain.

domain config

For statistics I can just graph a log based metric:

log based metrics,small

And this is how I built a very simple URL shortening HTTP redirect service. Try it out: x.gl/d.

Going forward, adding a new HTTP redirect is as simple as adding an element to the hardcoded map in main.go and committing to the master branch. The rest is taken care of by the deployment pipeline.



Subscribe here to receive new articles via email!