Compilation environment
architecture microservice golang

How I write a micro-service (part 4)

Code must be maintainable but something we forget is that the building environment must be too. I have tested many features (including Bazel …), to have a most easy setup (so not Bazel definitively …) and really understandable for each newcomers on the project.

For me maintenability is not part of the do it later philosophy. The maintenability is a part of RAMS and must be integrated in your way of thinking when you are engineering stuff. It should help you to formalize and organize your code, but, I know, it needs time to manage them.

I know, and probably you know, that we have to work faster every day, and produce more and more code in shorter time, but your life is not a proof of concept, you don’t have the ability to get a new life to be “production-ready”, so why do you consider creating a project as a PoC with the idea of killing it to rebuild it later before his birth.

Good things takes times to do !

Let’s setup a common building environment.

Settings

EditorConfig

EditorConfig is a convention stored as .editorconfig to describe the editor configuration (using space or tab indentation, etc.). It helps developers to have a common editor settings, in order to prevent merge errors due to editor settings (space vs tabs, parenthesis positions, etc.).

root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true

[*.sh]
indent_size = 2

[Makefile]
indent_style = tab

The case of Golang simplify code edition because Golang doesn’t give the developer the choice of code syntax. Code must be formatted before compilation.

Ignore files

Git

.gitignore

This file is used to exclude files from Git.

# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, build with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out
*.gen.go

# Exclude bin folder containing compiled artifacts
bin
# Exclude vendor (maybe including according your needs)
vendor
# JUnit report for CI integration
unit-tests.xml
# Exclude tool binaries
tools/bin
# Exclude test-results folder for CircleCI
test-results

Golang Vendor Case

When you want to freeze dependencies on your project, you should vendor them with your code.

$ go mod vendor
$ git add vendor
$ git commit -a -m "chore(dep): Freeze dependencies."

This is not the philosophy of go modules, but for many reasons, it makes your build reproductible even when the dependency repository disappeared (or “gently” usurpatedgo-bindata).

I have revived some projects 4 years after their creation, and built them without any problems.

It is always a matter of responsability, you could :

My security hat says (because HE can talk …) that I would prefer to commit vendor to be sure that you could build artifacts when you will really needs to (Murphy’s law).

Docker

.dockerignore

Artifacts are not built on your host, it must be build in a Multi Stage Build docker image. This file is used to ignore each entry during the docker context creation step, all ignored files are not copied to the context.

# Don't include compiled artifacts in docker, it should be build in a docker container
bin
tools/bin
# Exclude vendor dependencies
vendor
tools/vendor

Linter settings

GolangCI

.golangci.yml

You MUST start to work with golangci, I can tell you that it will teach you Go ^^ by blood and violence but you will become a better Golang developer on each finding.

https://raw.githubusercontent.com/Zenithar/go-spotigraph/master/.golangci.yml

Tools

Mage

magefile.go

When I started learning Go, I was used to create some Makefile to orchestrate the compilation process directly or by using CMake. I have used Makefile for 5 years, but the major problem was the Docker image used to build my artifacts. I had to prepare and maintain a prepared docker image containing all tools.

I decided to switch to magefile, which is litteraly Makefile in Golang when Go modules appears. You will be able to pull a build script in Go, via go get and run it without any additionnal binaries in your docker container than the Golang compiler.

mage.go

// +build ignore

package main

import (
	"os"

	"github.com/magefile/mage/mage"
)

func main() { os.Exit(mage.Main()) }

When you need to invoke a magefile.go, you just have to run the following command:

$ go run mage.go

You must be able to run the build pipeline on your computer as it could be executed remotely on your CI platform. So never design your CI pipeline to run on your CI platform first, and adapt it to run locally. Consider that your CI runner is just an invoker of your magefile target.

Gex

Gex is a external tool used to vendorize the tools used in build process. It’s not an official tool, but It seems to fit my needs.

// Code generated by github.com/izumin5210/gex. DO NOT EDIT.

// +build tools

package tools

// tool dependencies
import (
	_ "github.com/99designs/gqlgen"
	_ "github.com/CircleCI-Public/circleci-cli"
	_ "github.com/envoyproxy/protoc-gen-validate"
	_ "github.com/frapposelli/wwhrd"
	_ "github.com/gobuffalo/packr/packr"
	_ "github.com/gogo/protobuf/protoc-gen-gogo"
	_ "github.com/golang/mock/mockgen"
	_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
	_ "github.com/google/wire/cmd/wire"
	_ "github.com/hexdigest/gowrap/cmd/gowrap"
	_ "github.com/sqs/goreturns"
	_ "github.com/srikrsna/protoc-gen-mock"
	_ "github.com/uber/prototool/cmd/prototool"
	_ "go.zenithar.org/protoc-gen-cobra"
	_ "golang.org/x/tools/cmd/goimports"
	_ "gotest.tools/gotestsum"
	_ "mvdan.cc/gofumpt"
)

You have to use blank import to say that you need the tools, and use an go-submodule to control which version of this tool do you really need.

module go.zenithar.org/spotigraph/tools

go 1.12

require (
  github.com/99designs/gqlgen v0.9.0
  ...
)

This is managed by gex by using following commands :

$ gex --add github.com/99designs/gqlgen
$ gex --build

The build switch compile and install tools in the tools/bin path.

License Checker

.wwhrd.yml

When you build corporate softwares, you are not allowed to add as many libraries with viral licenses as you want for legal reasons, you must be able to control and maintain the package and license association list, in order to take actions in case of problematic license changes.

wwhrd (What Would Henry Rollins Do) is a tool, discovered during the dotGo Paris 2017, whose role is to maintain whitelist and blacklist software licenses, and also check them.

You could check also go-license-detector, golicense

---
blacklist:
  - GPL-2.0

whitelist:
  - Apache-2.0
  - BSD
  - FreeBSD
  - CC
  - ISC
  - MIT
  - MPL-2.0
  - NewBSD

exceptions:
  - github.com/davecgh/go-spew/spew/...
  - github.com/dchest/uniuri
  - github.com/opencontainers/go-digest
  - github.com/certifi/gocertifi

Prototool

prototool.yml

Prototool is Your Swiss Army Knife for Protocol Buffers.

# Folders to excludes for protobuf discovery
excludes:
    - ./vendor
    - ./tools/vendor

protoc:
  # Protoc version to use
  version: 3.7.1

  # Additional paths to include with -I to protoc.
  # By default, the directory of the config file is included,
  # or the current directory if there is no config file.
  includes:
    - ./vendor
    - ./tools/vendor

  # If not set, compile will fail if there are unused imports.
  # Setting this will ignore unused imports.
  allow_unused_imports: false

lint:
  # Google ruleset for Protobuf
  group: google
  file_header:
    # Force license header
    path: LICENSE

generate:
  go_options:
    import_path: go.zenithar.org/spotigraph
  plugins:
    # Call protoc-gen-gogo with grpc plugin 
    - name: gogo
      type: gogo
      flags: plugins=grpc
      output: .
    # protoc-gen-validate to generate validator from protobuf descriptors
    - name: validate
      type: gogo
      flags: lang=gogo
      output: .
    # protoc-gen-cobra to generate grpc client command for cobra
    - name: cobra
      type: gogo
      output: .
    # protoc-gen-mock to generate Client and Server mocks for testing
    - name: mock
      type: gogo
      output: .

Same problems with protoc (Protobuf Compiler) as I had with make for Makefile, this tool made by uber is able to orchestrate protobuf generation using protoc. If not present in the filesystem, it will be downloaded according a given version specified in the prototool.yaml or use the matching tool version.

Most important features of prototool are :

Next parts of the series will be focus on protobuf code generation, so wait for it, it’s gonna be legend….ary !

Building Pipeline

I don’t start speaking about CI/CD integration, but how my artifact are built. CI platform only have to call locally executable pipeline.

Never split local and remote pipelines !

On Host

$ go run mage.go

Docker

$ go run mage.go docker:build

deployment/docker/Dockerfile

It is used to produce a final usable docker image.

# Arguments passed via magefile
ARG BUILD_DATE
ARG VERSION
ARG VCS_REF

## -------------------------------------------------------------------------------------------------

FROM golang:1.12 as builder

RUN set -eux; \
    apt-get update -y && \
    apt-get install -y apt-utils upx zip unzip;

# Create a non-root privilege account to build
RUN adduser --disabled-password --gecos "" -u 1000 golang && \
    mkdir -p $GOPATH/src/workspace && \
    chown -R golang:golang $GOPATH/src/workspace;

# Force go modules
ENV GO111MODULE=on

WORKDIR $GOPATH/src/workspace

# Prepare an unprivilegied user for run
RUN set -eux; \
    echo 'nobody:x:65534:65534:nobody:/:' > /tmp/passwd && \
    echo 'nobody:x:65534:' > /tmp/group && \
    mkdir /tmp/.config && \
    chown 65534:65534 /tmp/.config

# Drop privileges to build
USER golang
COPY --chown=golang:golang . .

# Install dependencies
RUN set -eux; \
    # Install tools
    go run mage.go -d tools && \
    # Freeze dependencies
    go run mage.go go:deps

# Build final target
RUN set -eux; \
    # Build pipeline
    go run mage.go

# Compress binaries
RUN set -eux; \
    # Compress all binaries (may produce buggy artifacts)
    upx -9 bin/* && \
    chmod +x bin/*

## -------------------------------------------------------------------------------------------------

FROM gcr.io/distroless/static:latest

# Build Date (RFC3339)
ARG BUILD_DATE
# Version string
ARG VERSION
# Git commit reference
ARG VCS_REF

# Metadata (http://label-schema.org/rc1/)
LABEL \
    org.label-schema.build-date=$BUILD_DATE \
    org.label-schema.name="Spotigraph" \
    org.label-schema.description="Spotify Agile model mapping microservice" \
    org.label-schema.url="https://go.zenithar.org/spotigraph" \
    org.label-schema.vcs-url="https://github.com/Zenithar/go-spotigraph.git" \
    org.label-schema.vcs-ref=$VCS_REF \
    org.label-schema.vendor="Thibault NORMAND" \
    org.label-schema.version=$VERSION \
    org.label-schema.schema-version="1.0" \
    org.zenithar.licence="MIT"

# Copy artifact generated from previous stages
COPY --from=builder /go/src/workspace/bin/spotigraph /usr/bin/spotigraph
# Copy shortened group and passwd definition in order to create nobody:nobody identity
COPY --from=builder /tmp/group /tmp/passwd /etc/
COPY --from=builder --chown=65534:65534 /tmp/.config /

# Drop all privileges for running service
USER nobody:nobody
WORKDIR /

# Set service as entrypoint (no shell in container)
ENTRYPOINT [ "/usr/bin/spotigraph" ]
CMD ["--help"]

This Dockerfile executes the complete building pipeline in a docker stage and reduce the runnable image to its minimum surface no shell, no root user and run as unprivilegied user.

A paranoid, I am ;). Don’t forget to add Security Context like apparmor, seccomp to be a real paranoid. But sometimes this settings could makes your container too secure, not runnable, for example I had many issues with istio when using unpriviligied user in my containers, due to iptables manipulation on pod start on Kubernetes.

Conclusion

These tools are used according the requirements and focused on maintenability to generate code, lint it, format it, etc. It helps you to keep a code clean from easy bugs (missing error checking), but also some difficult code behavior (scope erasure). The more linter you add, the more confident you could be, yes you can say that, but don’t forget they are also code maintained by human ^^

Trust them to notify you, and use your experience to fix the problem, don’t fall in the easy nolint annotation over-uses. Yes you will be frustrated, you will be stuck but that’s the learning process !

In the next post, we will start to discuss how to prepare the business services protocol, and how do I use these tools for this purpose.

References

Persistence adapter implementations
architecture microservice golang

We are about to prepare a Golang project according to Clean and Hexagonal Architecture principles.
architecture microservice golang

During my software developer experience, I have seen many bad practices as a code reviewer and collected a lot of tips to try to build better products.
architecture microservice golang