Building a CI for Golang test

I built a CI with Jenkins for Golang test. We run go test on a Docker container and even run Jenkins on a Docker container.


├── docker
│   ├── dockerfiles # Dockerfiles for unit test
│   └── test
│       ├── # This initializes DB before testing
│       └── # Testing script
└── Jenkinsfile # The configuration for Jenkins pipeline

Environment of CI

Our Jenkins server uses an EC2 instance of t2.large, and the server runs on Docker container, and even a unit test run on Docker container on the container Jenkins runs with /var/run/docker.sock.

Jenkins loads Jenkinsfile and then execute it on the Jenkins pipeline.

How to build an execution environment

Create an AWS EC2 instance

We prepare the instance of EC2 installed Docker CE. Please see Get Docker CE for CentOS installation guide.

Create a Docker image for golang unit test



FROM jenkins/jenkins:lts

# Switch to root user
USER root

# Install Docker
RUN apt-get update
RUN apt-get install -y \
     apt-transport-https \
     ca-certificates \
     curl \
     gnupg2 \

RUN curl -fsSL | apt-key add -
RUN add-apt-repository \
   "deb [arch=amd64] \
   $(lsb_release -cs) \
RUN apt-get update
RUN apt-get install -y docker-ce
RUN echo "jenkins ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

# Switch back to jenkins user
USER jenkins

# Set system timezone JST

ENV TZ Asia/Tokyo

Run on a Jenkins host.

$ docker build --rm --tag jenkins-docker:latest .



FROM circleci/golang:1.9

# Install goose
RUN curl | sh
RUN go get

# Set system timezone JST
ENV TZ Asia/Tokyo

Run on our Jenkins host.

$ docker build --rm --tag golang:latest .

Launch Jenkins

$ sudo docker run --env JAVA_OPTS=-Dorg.apache.commons.jelly.tags.fmt.timeZone=Asia/Tokyo -v /var/run/docker.sock:/var/run/docker.sock --name jenkins -d -p 80:8080 -p 50000:50000 -v jenkins_home:/var/jenkins_home jenkins-docker:latest

-v /var/run/docker.sock:/var/run/docker.sock is used to manipulate host’s Docker because we want to launch containers on host-side.

-v jenkins_home:/var/jenkins_home is used to store everything of our Jenkins configurations and build results on our host’s filesystem. If you
move your Jenkins to another host or backup your Jenkins data, read this page.

Add a job to Jenkins

We need to enable Jenkins to hook Pull Requests when some developer does it.

Below settings on Jenkins.

Add a credential of GitHub enterprise

Because we use GitHub enterprise our development.

Credentials -> Jenkins -> Global credentials -> Add Credentials

key value
Kind Username with password
Scope Global
Username ci
Password ****

Add our GitHub enterprise

Configure System -> GitHub Enterprise Servers

key value
API endpoint http://***/api/v3
Name GitHub Enterprise

Create a job

New Item -> GitHub Organization -> OK

Configure the job’s settings

<Job> -> Configure -> Projects

key value
API endpoint GitHub Enterprise (http://***/api/v3)
Credentials ci/****
Owner some-repogitory
Script Path Jenkinsfile

<Job> -> Configure -> Projects -> Behaviours

key value
Filter by name (with regular expression) some-repogitory
Discover pull request from forks – Strategy Merging the pull request with the current target branch revision
Discover pull request from forks – Trust Everyone

Create a webhook in GitHub

To hook PR in Jenkins, We need to create a webhook in GitHub. Note that we must use the user has right permission.

<your repository> -> Settings -> Hooks

key value
Payload URL http://***/github-webhook/
Content type application/json
Which events would you like to trigger this webhook? Send me everything
Active true

Disable Jenkins’ authentication

Because we use Jenkins in a secure place, there are no incoming packets from the internet.

Manage Jenkins -> Configure Global Security -> Access Control -> Authorization -> check Anyone can do anything

Upgrade Jenkins

Since jenkins_home Docker volume has all Jenkins’ setting files, We pull a latest Docker image and relaunch Docker container, that’s it!

$ sudo docker stop jenkins
$ sudo docker rm jenkins
$ sudo docker pull jenkins/jenkins:lts
$ cd app/docker/dockerfiles/jenkins
$ docker build --rm --tag jenkins-docker:latest .
$ sudo docker run --env JAVA_OPTS=-Dorg.apache.commons.jelly.tags.fmt.timeZone=Asia/Tokyo -v /var/run/docker.sock:/var/run/docker.sock --name jenkins -d -p 80:8080 -p 50000:50000 -v jenkins_home:/var/jenkins_home jenkins-docker:latest

Jenkinsfile template

The Jenkinsfile we use, almost same, like this;

pipeline {
    agent any

    stages {
        stage('Checkout') {
            steps {
                step($class: 'GitHubSetCommitStatusBuilder')
                checkout scm

        stage('Start up containers') {
            steps {
                sh "sudo docker network create ci${env.EXECUTOR_NUMBER}"

                sh "sudo docker run -d --name mysql${env.EXECUTOR_NUMBER} --network ci${env.EXECUTOR_NUMBER} -p 3306${env.EXECUTOR_NUMBER}:3306 circleci/mysql:5.7"
                sh "sudo docker run -d --name redis${env.EXECUTOR_NUMBER} --network ci${env.EXECUTOR_NUMBER} redis:4.0"

                script {
                    if (sh (
                            script: "sudo docker create --name golang${env.EXECUTOR_NUMBER} --network ci${env.EXECUTOR_NUMBER} golang:latest bash /go/src/app/docker/test/",
                            returnStatus: true
                    ) == 0) {
                        sh "sudo docker cp ${env.WORKSPACE} golang${env.EXECUTOR_NUMBER}:/go/src/leo-server"

        stage('Initialize containers') {
            steps {
                // Initialize something like DB

        stage('Unit test') {
            steps {

                script {
                    if (sh (
                            script: "sudo docker start -a golang${env.EXECUTOR_NUMBER}",
                            returnStatus: true
                    ) != 0) {
                        currentBuild.result = 'FAILURE'

                // Copy test report and convert it into junit xml report
                sh "sudo docker cp golang${env.EXECUTOR_NUMBER}:/go/src/app/report.xml ."

                step([$class: 'JUnitResultArchiver', testResults: 'report.xml'])

    post {
        always {
            sh script: "sudo docker stop mysql${env.EXECUTOR_NUMBER}", returnStatus: true
            sh script: "sudo docker stop redis${env.EXECUTOR_NUMBER}", returnStatus: true

            sh script: "sudo docker rm mysql${env.EXECUTOR_NUMBER}", returnStatus: true
            sh script: "sudo docker rm redis${env.EXECUTOR_NUMBER}", returnStatus: true
            sh script: "sudo docker rm golang${env.EXECUTOR_NUMBER}", returnStatus: true

            sh script: "sudo docker network rm ci${env.EXECUTOR_NUMBER}", returnStatus: true

Our test script,, like this;


sudo chown -R circleci:circleci /go/src

cd /go/src/leo-server

echo 'Installing go-packages...'
glide i

echo 'Migrating DBs...'
go get
goose -env=ci -path=database/user up

echo 'Installing testing libraries...'
go get -u

echo 'Testing...'
go test -v ./... 2>&1 > tmp
go-junit-report < tmp > report.xml

exit ${status}

How to change Timezone on Docker with Ubuntu 16.04

Ubuntu 16.04 and every other use UTC by default.

I want to change to JST. So I search about this on the web, and some site said to use like this.

Show list of time zone

$ timedatectl list-timezones

But I executed this command, and then above message occurs.

Error about bus happen

$ timedatectl list-timezones
Failed to create bus connection: No such file or directory

It is a problem to use systemd. Ubuntu has used systemd since 16.04.

To use systemd on docker container is pretty tough. So I used tzdata. It is still available to install via apt on Ubuntu 16.04, but if you can also change time zone with systemd. See this document.

I write this config in Dockerfile like this. And build the container.


FROM ubuntu:16.04

  apt-get -y install tzdata && \
  ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

Then I enter the container and check time zone.

root@4dbd2aff2747:~# cat /etc/timezone

root@9dce4012502e:~# strings /etc/localtime

The docker container’s time zone becomes JST!

Docker Swarm modeでservice間のアクセスを可能にする

Docker Swarmのservice間で名前解決して互いにネットワークアクセスできるように設定する方法です。

使用したDocker engineのバージョンは 1.13.0 です。

service discoveryを使用する

service discovery を使用するとservice名で名前解決できるようになります。例えば appdb という2つのserviceを作成した時、app から jdbc:mysql://db:3306/mydatabase のように db に接続できるようになります。このように名前解決できる機能のことを service discovery と言います。

service discoveryは、例えばWebアプリケーションでHTTPSを使いため、WebアプリケーションのserviceとNginxのserviceを立ち上げて、Nginxをリバースプロキシとして使いたい場合を考えてみます。

Webアプリケーションのservice名は myapp、 Nginxのservice名は proxy とします。
Webアプリケーションはport 9000で動作していて、Nginxは外部のHTTPSのport 443を受け取ってport 9000にリバースプロキシします。

myapp(9000) <- (9000)proxy(443) <- Webブラウザ


upstream my-backend {
  server myapp:9000;

server myapp:9000の部分でmyappのポート9000に対してリクエストを転送していることがわかります。

このservice discoveryを使用するには、新規にoverray networkを作成する必要があります。
Attach services to an overlay network

overlay networkを作成する

以下はoverlay networkを作成する例です。

$ docker network create --driver overlay my-network



$ docker service create --replicas 1 -p 80:80 --name nginx --network my-network my-nginx


$ docker service create --replicas 1 --name app --network my-network my-app

このように同じoverlay networkを設定してserviceを作成します。同じネットワークに存在するserviceはVIPとDNSが設定されて、service名に名前解決できるようになります。

By default, when you create a service attached to a network, the swarm assigns the service a VIP. The VIP maps to a DNS alias based upon the service name. Containers on the network share DNS mappings for the service via gossip so any container on the network can access the service via its service name.

Use swarm mode service discovery

実際に app serviceで作成したコンテナに入ってdig nginxなどすると名前解決されていることがわかります。

service discoveryのハマりどころ


service discoveryを使用するにはPort 7946 TCP/UDP とPort 4789 UDP が開いていることが必要です。
もし同じoverlay networkを設定しているのに名前解決できない場合は、netstat -antupしてみます。上記のポートが見つからなければdocker daemonをrestartしてみると直ることがあります。
このportがlistensされていなくてもDocker Swarmは警告を出さない(たぶん)ので気づきにくいです。

また、portは問題ないのにうまくservice discoveryできない場合はホストマシン間のportが拒否されていないか確認します。もしES2のようなserviceを使用している場合は、そちらのポートの設定も確認します。



先の例だと、myapp を立ち上げる前に proxy を立ち上げてしまうと、myapp がまだservice discoveryに登録されていないので、 proxy service起動時に名前解決されずにエラーで起動できません。先に myapp serivceを立ち上げてから proxy serviceを立ち上げる必要があります。


また、Nginxの場合だとservice名を変数にして、service discoveryのDNSで名前解決する方法があります。この時resolver valid=2s;でDNSのTTLに関わらず、2秒で更新するようにします。


resolver valid=2s;
set $upstream "myapp";

upstream my-backend {
  server myapp:9000;

HTTPを使ったDocker Registryを作る

基本的にドキュメントに書いてありまが、Docker RegistryでHTTPを使おうとすると少し面倒だったので、まとめておきます。


Docker Registryを起動する

Docker Registryを起動すにはDeploy a registry serverに書かれている通り、以下のコマンドを実行します。
Docker Registryはこれで終わりです。

$ docker run -d -p 5000:5000 --restart=always --name registry registry:2

Docker Registryに接続するクライアントの設定


Docker Registry が10.0.0.2:5000でLISTENしている例です。


{ "insecure-registries":[""] }




あとはDocker daemonをrestartします。

# systemdの場合
$ sudo systemctl restart docker.service



Docker Swarm内の分散したログをFluentdでまとめてElasticsearchとKibanaでログを確認する


Docker Swarmで複数のコンテナがあると、コンテナが起動している複数のnodeに入ってdocker logsは大変です。2台や3台程度であればdocker logsで十分かもしれませんが、5台やそれ以上になると大変です。
また、一番大きい問題としてコンテナのログはapplication imageを更新するたびに揮発しまうことがあります。

そのため、Docker Swarmで複数のコンテナのログを一箇所に収集するようにします。



Docker container -> Fluentd -> Elasticsearch -> Kibana

  1. Docker containerからFluentdにログを送信
  2. FluentdでElasticsearchにログを送信するようにする。ここでElasticsearchのフォーマットに合わせてログを変換
  3. Kibanaでログを確認

Docker containerからFluentdにログを送信する

Dockerサービスを作成時に--log-driver=fluentdでFluentdに出力するように指定します。Dockerは標準でFluentdをサポートしています。次に--log-opt fluentd-address=で送信先とポートを指定します。--log-opt tag='developer_center.clientでFluentdのタグを指定します。

docker service create --replicas 1 --name api --log-driver=fluentd --log-opt fluentd-address= --log-opt tag='developer_center.api' api




Docker containerから送信されたログは以下のようにJSON形式になっています。

2017-07-19 11:54:27,165 [application-package-actor-7] INFO  application - make directories: mkdir -p /myapp/example/124223/50001


<filter application>
  @type parser
  format /^(?<time>.*) \[(?<thread>.*)\] (?<level>[\w]*)\s+(?<class>[\w.]*) - (?<message>.*)|\s*(?<system_message>.*)$/
  time_format %Y-%m-%d %H:%M:%S,%L
  key_name log
  keep_time_key true
  reserve_data true

<filter application>
  @type record_transformer
  enable_ruby true
    timestamp ${ require 'time'; }

<match application>
  @type copy
    @type elasticsearch
    port 80
    logstash_format true
    logstash_prefix application
    time_key timestamp
    buffer_type file
    buffer_path /dev/shm/fluentd_buffer/application.buf
    buffer_queue_limit 128
    buffer_chunk_limit 32m
    flush_interval 1s
    @type file_alternative
    path /home/fluentd/logs/application.log
    time_slice_format %Y%m%d%H00
    time_format %Y-%m-%dT%H:%M:%SZ
    buffer_type file
    buffer_path /dev/shm/fluentd_buffer/application.buf
    buffer_queue_limit 64
    buffer_chunk_limit 32m
    flush_interval 1s
    add_newline true

最初のfilterではアプリケーションのログをパースします。formatはパースするフォーマットです。ここではアプリケーションのログフォーマットに合わせています。time_formatも同じように時刻のフォーマットを合わせています。key_name logはどのJSONのkeyをパース対象にするか指定します。ここでは log を対象にしています。reserve_data truekey_name logで指定した元のログをパース後も保持するか指定します。 false にするとkeyが log のレコードが削除されます。

2番目のfilterではパースしたレコードにElasticsearchで使用するタイムスタンプを追加しています。タイムスタンプはすでにあるのですが、Fluentd 0.12はミリ秒に対応していないので、ここでrubyのTimeを使ってログを処理した時刻にミリ秒を含めたタイプムスタンプをレコードに追加しています。


1番目の<store>はElasticsearchに送信する設定です。ここでElasticsearchに送信するための設定としてhostport指定しています。次にlogstash_formatlogstash_prefixを指定しています。logstash_formatはKibanaで加工しやすいようにインデックスを付けます。logstash_prefixではインデックスのprefixを指定します。Kibanaでインデックスを指定してログを見ることになるのでわかりやすい名前をつけます。上記の設定ではclient-2017.01.01のようなインデックスが作成されます。また、time_key timestampfilterで追加したタイムスタンプを使用するようにkeyを指定しています。



ElasticsearchとKibanaを使用するにはAWSで提供されているAmazon Elasticsearch Serviceを使用しています。インスタンスを立ち上げるだけで使用できるようになるのでとても簡単です。もちろん、Elasticsearchにログを流す前にインスタンスを立ち上げておく必要があります。


Kibana上で、Management -> Index Patterns -> [Add New]でindex patternを指定します。

Index name or patternにはlogstash_prefixで指定した値を入力します。
正しくログが入っているとTime-field name@taimestampを選択できるようになるので、選択して[Create]します。

あとは、Discover から先ほど登録したindex patternを指定すればログを確認できるようになります。




Docker containerから外部へのアクセスができなくなった時の原因と直し方です。


centosをリスタートかsystemctl restart network.serviceすると net.ipv4.conf.all.forwarding0 になってしまいます。

$ sysctl net.ipv4.conf.all.forwarding
net.ipv4.conf.all.forwarding = 0

net.ipv4.conf.all.forwarding が0になってしまうとDockerコンテナと外のネットワークのパケットのforwardingができなくなり、外部とのネットワークが繋がらない状態になります。

これはDocker imageをbuildするときも同じです。

CIでこれをやってしまい、Docker imageのbuild時にyum installしている部分でネットワークに接続されず、かつ必要なパッケージがインストールされずにDocker imageがbuildされてしまいました。
ログを見ると warning は出ているのですが error ではないためbuildは継続され、正常にDocker imageが作成されます。アプリケーションを実行すると実行に必要なパッケージがインストールされていないため、アプリケーション側でエラーが出ます。

下記はnet.ipv4.conf.all.forwarding が0の状態でDocker buildをした出力の一部です。

3行目で---> [Warning] IPv4 forwarding is disabled. Networking will not work.が出力されていることから、IPv4 forwardingが無効になっていることがわかります。また、 Removing intermediate container c0b08184ced5でもわかるようにbuildは続行されています。

ネットワークエラーでパッケージはインストールされないがDocker buildは続行される

Step 3/5 : RUN yum -y update; yum -y install rsync; yum clean all; yum install -y openssh-clients
---> [Warning] IPv4 forwarding is disabled. Networking will not work.
---> Running in c0b08184ced5
Loaded plugins: fastestmirror, ovl

One of the configured repositories failed (Unknown),
and yum doesn't have enough cached data to continue. At this point the only
safe thing yum can do is fail. There are a few ways to work "fix" this:

1. Contact the upstream for the repository and get them to fix the problem.

2. Reconfigure the baseurl/etc. for the repository, to point to a working
upstream. This is most often useful if you are using a newer
distribution release than is supported by the repository (and the
packages for the previous distribution release still work).

3. Run the command with the repository temporarily disabled
yum --disablerepo= ...

4. Disable the repository permanently, so yum won't use it by default. Yum
will then just ignore the repository until you permanently enable it
again or use --enablerepo for temporary usage:

yum-config-manager --disable
subscription-manager repos --disable=

5. Configure the failing repository to be skipped, if it is unavailable.
Note that yum will try to contact the repo. when it runs most commands,
so will have to try and fail each time (and thus. yum will be be much
slower). If it is a very temporary problem though, this is often a nice

yum-config-manager --save --setopt=.skip_if_unavailable=true

Cannot find a valid baseurl for repo: base/7/x86_64
Could not retrieve mirrorlist error was
12: Timeout on (28, 'Resolving timed out after 30544 milliseconds')
Loaded plugins: fastestmirror, ovl

---> d6d1bd8a8271
Removing intermediate container c0b08184ced5



network serviceのscript



sysctl -w net.ipv4.ip_forward=0 > /dev/null 2>&1

networkをリスタートした時sysctl -w net.ipv4.ip_forward=0が実行されます。


$ sudo sysctl net.ipv4.conf.all.forwarding=1
net.ipv4.conf.all.forwarding = 1
$ sudo systemctl restart network.service
$ sysctl net.ipv4.conf.all.forwarding
net.ipv4.conf.all.forwarding = 0






$ sudo sysctl net.ipv4.conf.all.forwarding=1
net.ipv4.conf.all.forwarding = 1
$ sudo systemctl restart network.service
$ sysctl net.ipv4.conf.all.forwarding
net.ipv4.conf.all.forwarding = 0





Docker buildでも問題なくビルドされるので、少し気づきにくい不具合でした。
ホストOSをリスタートしたりnetwork serviceをリスタートするときは設定を確認すると良いかもしれません。