DEV Community

Cover image for How to make friends with Golang, Docker and GitLab CI
Igor Zibarev
Igor Zibarev

Posted on

How to make friends with Golang, Docker and GitLab CI

Let's start by demonstrating the simple app we will use as an example:

package main

import (
    "log"
    "net/http"

    "github.com/gorilla/mux"
)

var port = "8080"

func main() {
    log.Fatal(http.ListenAndServe(":"+port, router()))
}

func router() http.Handler {
    r := mux.NewRouter()
    r.Path("/greeting").Methods(http.MethodGet).HandlerFunc(greet)
    return r
}

func greet(w http.ResponseWriter, req *http.Request) {
    _, _ = w.Write([]byte("Hello, world!"))
}
Enter fullscreen mode Exit fullscreen mode

Also, let's add a small test:

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestRouter(t *testing.T) {
    w := httptest.NewRecorder()
    req := httptest.NewRequest(http.MethodGet, "/greeting", nil)
    router().ServeHTTP(w, req)

    expected := "Hello, world!"
    actual := w.Body.String()
    if expected != actual {
        t.Fatalf("Expected %s but got %s", expected, actual)
    }
}
Enter fullscreen mode Exit fullscreen mode

This is pretty straightforward. One thing I want to emphasize is that we have an external dependency. To manage it (and future dependencies), we are going to use dep:

dep init -v -no-examples
Enter fullscreen mode Exit fullscreen mode

Just to show everything works fine, we run tests, build and try our app locally:

$ go test ./...
ok      gitlab.com/hypnoglow/example-go-docker-gitlab   0.016s

$ go build -o app .
$ ./app

# and in another terminal:
$ curl http://localhost:8080/greeting
Hello, world!
Enter fullscreen mode Exit fullscreen mode

Seems simple. Now we want to dockerize our app.

Question: how we will pass dependencies to the docker build process?

  • Option 1: pass vendor directory along with the source code using COPY command.

  • Option 2: install dependencies on build time using RUN dep ensure.

The first option has a few benefits over second:

  • We (usually) already have our vendors installed locally, why bother installing them again at the build time? This way we speed up the docker build process.
  • In case of private dependencies, like your package in another private repository, you need to pass git credentials to the docker build process to make go tool able to fetch those dependencies. In 1st option, you don't have such problem.

Thus, we create the following Dockerfile:

FROM golang:1.10-alpine3.7 as build

WORKDIR /go/src/app

COPY . .

RUN go build -o app

FROM alpine:3.7

COPY --from=build /go/src/app/app /usr/local/bin/app

ENTRYPOINT ["/usr/local/bin/app"]
Enter fullscreen mode Exit fullscreen mode

Next, we need a .gitlab-ci.yml file to run tests and build our image on push. How are we going to accomplish that tasks?

Well, we need a job to install dependencies, because above we decided not to install them in Dockerfile. Even if we did, we need them installed to run our test. So, we create a job dep to install dependencies and store vendor directory as a GitLab artifact. In other jobs, we add dep job as a dependency, and GitLab will extract previously stored vendor right into our project directory.

variables:
  PACKAGE_PATH: /go/src/gitlab.com/hypnoglow/example-go-docker-gitlab

stages:
  - dep
  - test
  - build

# A hack to make Golang-in-Gitlab happy
.anchors:
  - &inject-gopath
      mkdir -p $(dirname ${PACKAGE_PATH})
      && ln -s ${CI_PROJECT_DIR} ${PACKAGE_PATH}
      && cd ${PACKAGE_PATH}

dep:
  stage: dep
  image: golang:1.10-alpine3.7
  before_script:
    - apk add --no-cache curl git
    - curl -sSL https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64 -o /go/bin/dep
    - chmod +x /go/bin/dep
    - *inject-gopath
  script:
    - dep ensure -v -vendor-only
  artifacts:
    name: "vendor-$CI_PIPELINE_ID"
    paths:
      - vendor/
    expire_in: 1 hour

test:
  stage: test
  dependencies:
    - dep
  image: golang:1.10-alpine3.7
  before_script:
    - *inject-gopath
  script:
    - go test ./...

build:
  stage: build
  dependencies:
    - dep
  image: docker:17
  services:
    - docker:dind
  script:
    - docker build -t app .
Enter fullscreen mode Exit fullscreen mode

Finally, we check our pipeline:

pipeline

We're done! Next steps, like pushing the built image to the docker registry, are left as an exercise for the reader. ๐Ÿ™‚

The full example is available in the GitLab repository.

Thanks! This was my 1st article on dev.to, I hope you enjoyed.

I apologize for any grammatical and linguistic mistakes, as English is not my native language. Please fix me in comments if you spot a problem! ๐Ÿค“

Top comments (6)

Collapse
 
hypnoglow profile image
Igor Zibarev

Absolutely valid points! Thanks!

I install ca-certificates in almost every Dockerfile too. As for cgo, I tend to use apline as a base instead of scratch, so this is not required in my case.

Collapse
 
herla97 profile image
HL Salvador

Hi Igor, Nice job man.

I have once issue:

$ go test ./...
# runtime/cgo
exec: "gcc": executable file not found in $PATH
FAIL    gitlab.com/go-pipeline-testing [build failed]
Enter fullscreen mode Exit fullscreen mode

I can solution this:

test:
  stage: test
  dependencies:
    - dep
  image: golang:1.13-alpine3.11
  before_script:
    - *inject-gopath
  script:
    - CGO_ENABLED=0 GOOS=linux go test ./...
Enter fullscreen mode Exit fullscreen mode

Thanks for all !!!

Collapse
 
jaihind213 profile image
vishnu rao

hi

its a nice post. i had a qns.

i noticed almost everywhere people use the 'image: docker:latest' in build along with dind service.

whats the purpose of using docker:17 image , cant i use alpine image with dind service and build image? , as dind launches the docker daemon ?

appreciate if you can help me understand . thanks

Collapse
 
hypnoglow profile image
Igor Zibarev

Hi Vishnu,

When building Docker images, we need both Docker client and Docker daemon. That's why we:

  • use docker:17 as a base image for the job to call Docker client;
  • add docker:dind to the job as a service for the daemon.

There is a simple example of this interaction on Docker hub hub.docker.com/_/docker without involving GitLab CI.

p.s. FYI there are alternative solutions that allow building docker images without docker client, but these are out of scope and require more effort.

Collapse
 
atthoriq profile image
At Thoriq

thank you!

Collapse
 
paltamadura profile image
David Good

Thanks. This is great. Any chance you could make an update for Go modules?