Better by December

At the end of 2022 I had scheduled vacation days, but plans fell through and I found myself with extra time to kill. I generally do better when I have something to keep me out of trouble, so I built a web application.

With the new year rapidly approaching I was starting to think about goals that I had for 2023 and how to track them. I again wanted to do 10,000 sit ups and 10,000 pushups over the year. Also, my Wife and I wanted to go on 150 walks together. In addition to goals, I also wanted track other things such as the books I read and the number of days I played guitar. The hope was by doing those things I’d be a better person by the next December.

For those who might be interested in the basic architecture, here’s a summary of the various pieces I built and/or assembled:

  • I have a “december-back” repository on Github (currently the repository is private) which does most of the work for the service. I wrote that in Golang.
  • I have a “december-front” repository on Github (currently the repository is private) which provides the web user interface for the service. I wrote that in JavaScript/TypeScript using React.
  • The “december-front” project is built using npm and the resulting artifacts are stored in an AWS S3 bucket and delivered via Cloudfront.
  • The “december-back” project is packaged into a Docker image and published to a registry via a Github action.
  • Docker containers are spun up in AWS ECS (Elastic Container Service) on Fargate.
  • Route53 on AWS takes care of domain registration for betterbydecember.com and also provides all the DNS goodness and ensures requests are correctly directed to the front or back end.
  • An EC2 Load Application Load Balancer can direct traffic to one of two different regions and also takes care of all the https certificate goodness.
  • All the data is stored in a DataStax Astra database since it’s “a database I want to use“.

The web app is available at https://www.betterbydecember.com/ and anybody can try it out. It’s been over a year since the basic functionality was complete and I started using it to track my progress. I met my 2023 goals and feel like I became Better by December. Now that 2024 is in full swing, I’ve set new goals and am actively working towards becoming Better by December.

Getting local address in Go

Awhile ago I had some tests that would spin up a server and then hit some “localhost:8080” endpoints and verify the responses. The tests worked great in my IDE, but when the tests were invoked as a step during a Docker build, they could not find the server. This post is about how I got my tests to run within a Docker container.

There are a variety of ways to get the local address in a Docker container, but many of the solutions I found online involved running one or more command line utilities and I wanted something that “just worked” in Go. After various kicking and swearing (and searching), this is what I came up with:


package main

import (
    "fmt"
    "net"
    "os"
)

func main() {
    fmt.Println(getLocalAddress())
}

func getLocalAddress() (string, error) {
    var address string
    _, err := os.Stat("/.dockerenv") // 1
    if err == nil {
        conn, err := net.Dial("udp", "8.8.8.8:80") // 2
        if err != nil {
            return address, err
        }
        defer conn.Close()
        address = conn.LocalAddr().(*net.UDPAddr).IP.String() // 3
    } else {
        address = "localhost" // 4
    }
    return address, nil
}

The above is a complete main.go which can be compiled and ran, but the most important piece is the getLocalAddress() function which returns the local address (or an error if something goes wrong, but I haven’t had problems). Here are some of the key points of the code:

  1. Checking if the “/.dockerenv” file exists is an easy way to check if the code is running in a Docker container.
  2. We create a net.Conn instance
  3. I first tried using
    address = conn.LocalAddr().String()
    to get the local IP address, but it included a port that I didn’t want. Instead of doing string manipulation, I found the returned LocalAddr was a pointer to a net.UDPAddr and so a simple casting provided easy access to the IP.
  4. If not running in Docker, “localhost” is good enough.

And that’s about it. When I have checked, the local IP address of the container has been in the 172.17.0.x range which I believe is a Docker default. Since doing this, there haven’t been any problems running the test, but I’m not sure if this is an industrial strength solution suitable for production environments.

Multistage Docker Builds For Go Services

One thing I like about using Go for services is that it can have a much smaller footprint than other languages. This is particularly useful when building Docker images to run since a Docker container contains everything needed to run a service.

But images sizes can grow quickly. As Go code gets more complicated you can and up with a large number of dependencies and tests. Sometimes additional tools are needed as part of the build process such as for code generation. I also like to have various tools available when debugging build problems. But you can have all of that and still end up with small image by using multistage builds in Docker.

Multistage builds were added to Docker a couple of years ago and work very well. I will refer interested parties to the official documentation, but one issue I have encountered as I have used multistage builds is that sometimes the Go executable I create in a penultimate stage does not execute properly in the image created in the final stage. Typically the reason for this is because I use Ubuntu based images for building (Ubuntu is the Linux flavor I’m most comfortable using) and prefer Alpine based images for the final package to reduce size. So this is perhaps a problem of my own making, but I have found solutions to the common errors I see.

FROM golang:latest as builder 

# Generic code to copy, compile and run unit tests
WORKDIR /src
COPY src/ ./
RUN go test -v


FROM alpine:latest

WORKDIR /root/

# If you don't adde the certificates you get an error like:
# x509: failed to load system roots and no roots provided
RUN apk --no-cache add ca-certificates

# Because musl and glibc are compatible we can link and fix
# the missing dependencies.  Without this we get an error like:
# sh: ./main: not found
RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2

# Copy the go exec from the builder
COPY --from=builder /go/bin/main .

CMD [ "./main" ]

The above Dockerfile is a generic multistage build of a “main” go application. Here are couple things of note:
– The “RUN apk –no-cache add ca-certificates” snippet is necessary to avoid “x509” errors.
– The “RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2” snippet is a bit of a hack to get around the glibc vs musl in Ubuntu vs Alpine. Without it, you get unhelpful, difficult-to-debug errors like “sh: ./main: not found”.

An alternative to the library linking above is to instead set flags when compiling in Ubuntu like “CGO_ENABLED=0 GOOS=linux”.

When using multistage Docker builds for Go services, I’ve found the image for the first stage to be several hundred megabytes, but the image produced by the final stage will often be around a dozen megabytes. You can’t even fit a JVM in an image that small let alone a Java based service.

Redirecting stdout to /dev/null in Go

Yesterday was the six month anniversary since I started my new job.  It’s been busy and I’ve learned a lot.  One thing I had to start learning was the Go programming language (aka Golang).  Today I wanted to temporarily suppress the standard out, but searching the Internet didn’t come up with any concise solutions.  What I did was pretty simple, but seems to work.

Basically, I’m using a third party package to parse an ini file.  The code works fine, but it is very verbose and prints out several lines of information that end up cluttering my logs.  My solution was to simply redirect the standard out to /dev/null.  Here’s what my code ended up looking like:

func unmarshal(bytes []byte, info *InfoStruct) error {

    // Redirect standard out to null
    stdout := os.Stdout
    defer func() { os.Stdout = stdout }()
    os.Stdout = os.NewFile(0, os.DevNull)

    return ini.Unmarshal(bytes, info)
}

Hopefully the code is easy to follow.  The “InfoStruct” is just some struct for data that is in the ini file.  The “ini” prefix is for the third party package that I am using.

In my quick searching, I found various information about redirecting logs, redirecting command output, etc.  There were even some examples of redirecting the stdout, but none were exactly what I wanted and most were less concise.