Migrating to Go Modules

I spent yesterday figuring out how to port our workflow here at Modular Finance to a more modern one. Specifically trying to make go modules play nice with our docker containers.

We have successfully tried this out before but only for trivial cases. This time I tried to take on one of our bigger projects and it has proven to be a bit more challenging.

Background

At Modular Finance we use a lot of Go and Docker for most of our projects and products. We both develop, test and deploy all of our code in a docker environment. This gives us a lot of benefits, a consistent environment across the applications life cycle, easy to get started for new developers, standardized builds and so on. Nothing is however without its downside and some work has to be put in to get things running.

We run and build multiple products. Each product we have is a separate git repo which may consist of many micro services, gateways, frontends, databases and so on. These products share generic libraries which we keep in a separate (private) git repo, kit. This gives us a paradigm where we have mono repo for each product and a shared repo between them.

To develop our applications we use docker compose which is easily then translated to a kubernetes configuration when deployed to our integration, test and production environments. A typical structure for a product might look as follows

 a-project/
 |- go/
 |  +- src/
 |     +- a-project/
 |        |- service1/
 |        |- service2/
 |        |  |- cmd/
 |        |  |- internal/
 |        |  |- modelfiles.go
 |        |  +- Dockerfile
 |        +- vendor/
 |- frontend1/
 |- frontend2/
 |- databases/
 +- docker-compose.yml

Each service becomes its own docker container, we mount a-project/go/src/a-project into /go/src/ on each go docker container. In our IDE we now add the a-project/go as our $GOPATH. Until now we have been using govendor for dependency management placing all dependencies in a-project/go/src/a-project/vendor.

This structure is the last one in a long line of different structures/workflows we have tried and it has been serving us very well during the last year or so. But since Go modules are now available we want to address a couple of things that has been hard to solve with govendor.

The basics

Let’s start with a trivial case. There are already a lot of resources around the web for this. I found https://github.com/donvito/hellomod to be a helpful starting point.

This tells us to create a main.go looking something like this:

package main

import "github.com/donvito/hellomod"

func main() {
	hellomod.SayHello()
}

Then:

go mod init hello # -> go: creating new go.mod: module hello

Which gives us a new file go.mod with one line: module hello.

So far so good, let’s wrap this in a docker container since we use docker-compose:

# dockerfile

FROM golang:1.11-alpine

# go get uses git to fetch modules
RUN apk add --no-cache git

RUN mkdir -p /go/src/hello
WORKDIR /go/src/hello

ENV GO111MODULE=on

CMD go run main.go
# docker-compose.yml

version: '3.0'

services:
  hello-service:
    build:
      context: .
      dockerfile: ./dockerfile
    volumes:
      - .:/go/src/hello/
      - ./pkg:/go/pkg

Spinning this up with docker-compose up downloads the missing github.com/donvito/hellomod go module we specified in main.go above. We can see the module in the pkg folder that we mounted: pkg/mod/github.com/donvito/hellomod@v1.0.1/

Permissions

Using this setup we can continue to develop our app and let docker download modules for us when needed. However one slightly annoying part is that since we run as root inside the container our pkg/ folder will be locked behind sudo.

This is nothing new, govendor and package managers in other languages have the same behavior. To make our dev environments more smooth we typically add the following to our docker configs:

# ...

# mirror dev user
ARG UID

RUN useradd --uid $UID --create-home $USER

RUN chown -R $UID /go

USER $USER

CMD go run main.go

# ...

  hello-service:
    build:
      context: .
      dockerfile: ./dockerfile
      args:
        UID: ${UID}
        USER: ${USER}
# ...

Let now restart everything with the correct uid

echo UID=$(id -u) >> .env # <- docker-compose default env config
docker-compose up

Private git repos as Go Modules

Cool, we’re done with the basics. On to more complex stuff.

As many other companies we try to dogfood as much as we can. This was the first roadblock I encountered when trying to convert to go modules. Previously we (a bit reluctantly) imported private packages using git submodules. This works but since it’s possible to import stuff with go get directly from git it adds complexity.

The method I went with is to pass in my ssh socket to the docker container. I’m not passing in any ssh keys but for safety I’m just recommending this method for dev configs.

To access private repos with go get we’ll first configure git to use ssh instead of the https. In normal git repos, git figures out what protocol to use by itself (depending on how the “remote” is configured). In this case we need to pre-empt the default https setting by overriding it in the .gitconfig inside the container. We’re also silencing the “authenticity of host … (yes/no)” prompt by adding the “… StrictHostKeyChecking …” line below.

Final dockerfile:

#dockerfile

FROM golang:1.11-alpine

RUN apk add --no-cache git curl build-base bash openssh-client shadow

RUN mkdir -p /go/src/hello
WORKDIR /go/src/hello

ENV GO111MODULE=on

# mirror dev user
ARG UID
ARG USER

RUN useradd --uid $UID --create-home $USER
RUN chown -R $UID /go

RUN printf "[url \"git@bitbucket.org:\"]\n\tinsteadOf = https://bitbucket.org/\n" >> /home/$USER/.gitconfig
RUN mkdir /home/$USER/.ssh && echo "StrictHostKeyChecking no " > /home/$USER/.ssh/config

USER $USER

CMD go run main.go

Okay, almost there, now just share the ssh socket

Final docker-compose.yml:

# docker-compose.yml

version: '3.0'

services:
  hello-service:
    build:
      context: .
      dockerfile: ./dockerfile
      args:
        UID: ${UID}
        USER: ${USER}
    environment:
      SSH_AUTH_SOCK: /run/ssh_agent
    volumes:
      - $SSH_AUTH_SOCK:/run/ssh_agent
      - .:/go/src/hello/
      - ./pkg:/go/pkg

Add a private repo as import in main.go and spin up the container. It should download everything as go modules to pkg/, both public and private dependencies.

GoLand configuration

Last bit of setup. We typically use GoLand for developing Go. To make GoLand recognize the pkg/ folder as modules (for IntelliSense etc.) open settings (ctrl + alt + s on ubuntu) then:

  • Go > Go Modules (vgo) > Enable Go Modules (vgo integration)
  • Go > GOPATH > Project GOPATH: add parent folder for pkg/

I ran in to an annoying issue here with the yaml.v2 module. I even found another user with the exact same problem. Unfortunately the trail ends there in a striking resemblance to a relevant xkcd: Wisdom of the Ancients

I have some suspicions about what’s wrong here but nothing concrete.

My solution so far has been to hack around this using chmod from inside the container

# somewhere after fetching go modules in the dockerfile
chmod +w /go/pkg/mod/gopkg.in/yaml.v2@v2.2.2 /go/pkg/mod/gopkg.in/yaml.v2@v2.2.2/go.mod

With that we have a really nice (apart from that last thing), encapsulated, environment for developing Go projects with Docker and private go modules.


Author:
Jonas Zeitler, Developer