
之前都在偷懶沒有寫網誌,剛好這次端午連假比較長。 所以想做測試跟實驗的部分都做完了,就來寫一篇關於 GitLab CI 整合的經驗分享。
文章中大致上會涵蓋這些部分:
- GitLab CI 基本使用
- Rancher建置環境
- SonarQube 基本使用
- GitLab CI 整合環境
文章會以我在建構 CI 環境的過程中來講解,一些安裝跟配置的部分會直接跳過。
硬體環境
因為我主要是幫老爸公司建構這個環境的,所以這些配置是基於一個很小的開發團隊(不足十人)的情況去設置的,如果團隊比較大或者有其他需求,不一定會適用。
這個環境會出現兩台機器,不過實際上使用的只有一台。
- Synology DS1512+
- Server 等級 :: i7-3820 3.6GHz / 8G RAM
目前裡面只有使用這兩台 Server 下面那台是自行組的,放在一個小機櫃這樣 XD
內部網路環境用的是 1Gbps 的 Switch 這樣
伺服器因為組了也有三四年,裡面用的是單顆 SSD 原本是 96GB 的前陣子換成 256GB 的 SSD,不過為了避免容量爆炸所以透過 NAS 設定了 ISCSI 給 Server 用,容量是 1TB 這樣。
不過體驗到了 Docker Pull 的慢速,所以 ISCSI 只用在 Container 本身的資料保存。
軟體環境
伺服器上用的是 Ubuntu 14.04 版本,裡面只安裝以下三個套件。
- NGINX - 做反向代理
- Docker
- Rancher - Container 管理
所有的服務都是透過 Docker 架設,對外的 Web 介面則用 NGINX 導出。
Production 不能這樣玩,像是 Database 會有效能貧頸等等問題。
開發環境
一張圖解釋 XD
Fluentd 是拿來玩的,至於配置的話都是使用 docker-compose.yml 上傳設定來處理的。
 1postgresql:
 2  restart: always
 3  image: sameersbn/postgresql:9.4-18
 4  environment:
 5    - DB_USER=gitlab
 6    - DB_PASS=[hidden]
 7    - DB_NAME=gitlabhq_production
 8    - DB_EXTENSION=pg_trgm
 9  volumes:
10    - /srv/gitlab/postgresql:/var/lib/postgresql
11gitlab:
12  restart: always
13  image: sameersbn/gitlab:8.6.4
14  links:
15    - redis:redisio
16    - postgresql:postgresql
17  ports:
18    - "10080:80"
19    - "10022:22"
20  environment:
21    - DEBUG=false
22    - TZ=Asia/Taipei
23    - GITLAB_TIMEZONE=Taipei
24
25    - GITLAB_SECRETS_DB_KEY_BASE=[hidden]
26
27    - GITLAB_HOST=localhost
28    - GITLAB_PORT=10080
29    - GITLAB_SSH_PORT=10022
30    - GITLAB_RELATIVE_URL_ROOT=
31
32    - GITLAB_NOTIFY_ON_BROKEN_BUILDS=true
33    - GITLAB_NOTIFY_PUSHER=false
34
35    - GITLAB_EMAIL=notifications@moho.com.tw
36    - GITLAB_EMAIL_REPLY_TO=noreply@moho.com.tw
37    - GITLAB_INCOMING_EMAIL_ADDRESS=gitlab@moho.com.tw
38
39    - GITLAB_BACKUP_SCHEDULE=daily
40    - GITLAB_BACKUP_TIME=01:00
41
42    - SMTP_ENABLED=true
43    - SMTP_DOMAIN=moho.com.tw
44    - SMTP_HOST=192.168.100.230
45    - SMTP_PORT=587
46    - SMTP_USER=gitlab
47    - SMTP_PASS=[hidden]
48    - SMTP_STARTTLS=true
49    - SMTP_AUTHENTICATION=login
50
51    - IMAP_ENABLED=true
52    - IMAP_HOST=192.168.100.230
53    - IMAP_PORT=993
54    - IMAP_USER=gitlab
55    - IMAP_PASS=[hidden]
56    - IMAP_SSL=true
57    - IMAP_STARTTLS=false
58  volumes:
59    - /srv/gitlab/gitlab:/home/git/data
60redis:
61  restart: always
62  image: sameersbn/redis:latest
63  volumes:
64    - /srv/gitlab/redis:/var/lib/redis
基本上沒什麼特別,就是照 Docker Image 的範例修改環境變數跟設定而已。
用 Rancher 的好處是之後升級可以用 Upgrade 修改 Image 的版本 Tag 就能夠升級了~
SonarQube 的部分也一樣
 1postgresql:
 2  restart: always
 3  image: sameersbn/postgresql:9.4-18
 4  environment:
 5    - DB_USER=sonar
 6    - DB_PASS=[hidden]
 7    - DB_NAME=sonar
 8    - DB_EXTENSION=pg_trgm
 9  volumes:
10    - /srv/sonarqube/postgresql:/var/lib/postgresql
11sonarqube:
12  restart: always
13  image: sonarqube
14  links:
15    - postgresql:postgresql
16  ports:
17    - "10081:9000"
18  environment:
19    - SONARQUBE_JDBC_URL=jdbc:postgresql://postgresql:5432/sonar
20    - SONARQUBE_JDBC_PASSWORD=[hidden]
21  volumes:
22    - /srv/sonarqube/extensions:/opt/sonarqube/extensions
這樣一來就能跑起來了,至於像是 NGINX 的反向代理我就不另外敘述摟~~
GitLab CI 入門
安裝部分就參考官方的文件來安裝,基本上不難。 之後就是把它 Register 到 GitLab 上面,有趣的是他可以登記到多個 GitLab 而不限一個,我自己是開一個 Container 去跑,然後給權限讓他能在 Host 上面開新的 Container (Runner) 這樣。
那麼,先來講幾個關鍵的點吧 XD
NAT 問題
因為路由器設定的問題,所以在 Runner 去 Clone 專案的時候會有些障礙。
假設我的 GitLab Host 是 gitlab.xxx.com.tw 那麼網路設定大概是這樣。
LAN —-> NAT —–> WAN
不過 LAN 裡面又有不同的機器,而老爸公司用的是固定 IP (五組)所以我就透過 IP 去分要導去哪台。
59.x.x.49 —> NAS 59.x.x.50 —> Server
這是透過 Public IP 設定的,但是在 Server 裡面會變成
LAN IP —-> NAT —–> Server
結果 NAT 就不覺得 Server 裡面是走 59.x.x.50 進來的(崩潰)
所以只好對 GitLab Runner 動手腳 XD
GitLab CI 的 Runner 有一個全域的設定檔,我們給他改造一下
 1[[runners]]
 2  name = "ruby-2.1-docker"
 3  url = "https://CI/"
 4  token = "TOKEN"
 5  limit = 0
 6  executor = "docker"
 7  builds_dir = ""
 8  shell = ""
 9  environment = ["ENV=value", "LC_ALL=en_US.UTF-8"]
10  disable_verbose = false
上面是設定為 Docker 模式的 Runner,我們現在要讓 Host 的 gitlab.xxx.com.tw 直接用 Host IP 而不是 Public IP 讓他跳過 NAT 的解析。
1[runners.docker]
2  // 略
3  extra_hosts = ["gitlab.xxx.com.tw:127.10.0.1"]
在 runners.docker 的部分,可以直接告訴他 /etc/hosts 要新增哪幾筆資料。
設定檔
接下來就是學 .gitlab-ci.yml 怎麼寫了,如果用過 Travis CI 之類的服務應該都是可以駕輕就熟拉 XD
 1before_script:
 2  - export GRADLE_USER_HOME=`pwd`/.gradle
 3  - mkdir -p $GRADLE_USER_HOME
 4  - echo "org.gradle.daemon=true" >> $GRADLE_USER_HOME/gradle.properties
 5  - echo "org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8" >> $GRADLE_USER_HOME/gradle.properties
 6  - echo "org.gradle.parallel=true" >> $GRADLE_USER_HOME/gradle.properties
 7  - echo "org.gradle.configureondemand=true" >> $GRADLE_USER_HOME/gradle.properties
 8
 9cache:
10  paths:
11    - .gradle/caches
12    - .gradle/wrapper
13
14junit:
15  image: elct9620/gitlab-android-junit
16  script:
17    - ./gradlew test
18
19sonar:
20  image: elct9620/gitlab-sonar-scanner
21  script:
22    - "/bin/true"
先看一下我目前用的 GitLab CI 設定檔。
基本上分為兩種
- Job
- Config
如果是關鍵字,就會被分類成 Config 像是 cache before_script 這種,如果寫在 Job 外面就是 Global 的,寫在裡面就只對某個 Job 生效。
因為這是 Java 專案,所以我讓所有的 Task 都支援 Gradle 的設定(SonarQube 怎麼上 Cache 還沒找到⋯⋯)
至於該怎麼寫,大致上就是這樣的格式:
1
2job:
3  config:
4    - xxx
5    - xxx
例如我要用 ruby-2.3 然後跑 RSpec 並且快取 cache 目錄
1rspec: # 新增 RSpec 任務
2  image: ruby:2.3 # 設定 Docker Image 沒有則用預設值
3  script:
4    - bundle exec rspec # 執行指令
5  cache:
6    paths:
7    - cache # 快取目錄
這邊基本上不難,但是要注意幾點
- Working Directory 是在 GitLab 設定的目錄
- Script 跟 Entrypoint 不相容,他是真實一段 Shell Script 注入你的 Script
- Cache 只在 Working Directory 中可以運作
關於 Cache 官方的 Issue 上面有人說如果是
/root的形式,可以運作。但是/root/gradle就不行。
其他部分看文件就好了,最基本的使用其實就這樣 XD 然後放到專案的根目錄下就會自動被 GitLab 偵測然後自動運行。
關於 Pipline 等等就等之後有機會再跟大家分享拉 XD
SonarQube
前面已經裝好了,其實不需要再多做設定。 這邊簡單分享一下自製 Docker Image 的心得這樣。
因為只是單純的需要 Docker 環境,所以只要使用官方的 java:8-jre-alpine 版本就可以了!
 1FROM java:8-jre-alpine
 2MAINTAINER 蒼時弦也 docker@frost.tw
 3
 4ENV SONAR_SCANNER_VERSION 2.6.1
 5ENV SONAR_SCANNER_HOME /opt/sonar-scanner-${SONAR_SCANNER_VERSION}
 6ENV SONAR_SCANNER_PACKAGE sonar-scanner-${SONAR_SCANNER_VERSION}.zip
 7ENV HOME ${SONAR_SCANNER_HOME}
 8
 9WORKDIR /opt
10
11RUN apk update \
12  && apk add bash wget ca-certificates unzip \
13  && wget https://sonarsource.bintray.com/Distribution/sonar-scanner-cli/${SONAR_SCANNER_PACKAGE} \
14  && unzip ${SONAR_SCANNER_PACKAGE} \
15  && rm ${SONAR_SCANNER_PACKAGE}
16
17RUN addgroup sonar \
18  && adduser -D -s /usr/sbin/nologin -h ${SONAR_SCANNER_HOME} -G sonar sonar \
19  && chown -R sonar:sonar ${SONAR_SCANNER_HOME} \
20  && mkdir -p /data \
21  && chown -R sonar:sonar /data
22
23USER sonar
24WORKDIR /data
25
26VOLUME /data
27
28ADD entrypoint.sh /entrypoint.sh
29
30ENTRYPOINT ["/entrypoint.sh"]
基本上就只是把 Sonar Scanner 抓下來,然後放到指定的目錄。
不過真正困難的點就在額外的設定 entrypoint.sh 了!
如果是一般使用,直接將 ENTRYPOINT 設定成 Sonar Scanner 就可以。
但是會暴露在外部網路的環境,一定需要使用者驗證。
Sonar Qube 如果開啟
Force Authentication就一定要用帳號密碼或者 Token 才能透過 API 上傳需要分析的程式碼。
所以我弄了一個 Shell Script 當 Entrypoint 來解決這個問題
 1#!/bin/bash
 2
 3set -e
 4
 5VERSION=${CI_BUILD_TAG:-"${CI_BUILD_REF_NAME}"}
 6
 7OPTS="-Dsonar.projectVersion=${VERSION} -Dsonar.gitlab.project_id=${CI_PROJECT_ID} -Dsonar.gitlab.commit_sha=${CI_BUILD_REF} -Dsonar.gitlab.ref_name=${CI_BUILD_REF_NAME} -Dsonar.issuesReport.console.enable=true"
 8
 9# TODO: Improve entrypoint to support gitlab-runner
10cd ${CI_PROJECT_DIR}
11if [[ ! -z $SONAR_TOKEN ]]; then
12  ${SONAR_SCANNER_HOME}/bin/sonar-scanner -Dsonar.login=${SONAR_TOKEN} ${OPTS}
13else
14  ${SONAR_SCANNER_HOME}/bin/sonar-scanner ${OPTS}
15fi
基本上很簡單,利用 Scanner 的 -D 補足缺少的參數,跟自動填入。
1VERSION=${CI_BUILD_TAG:-"${CI_BUILD_REF_NAME}"}
因為會需要給版本,但是照我的習慣會忘記是哪個版本,不如就自動用 Tag / Branch Name 來當版本 XD
但是似乎會被蓋掉拉,所以有實用性有待商榷(但是這是必填項目)
剩下的 OPTS 則是跟 GitLab 整合的部分,目前還沒有成功。
正常運作的話會自動到 GitLab 當次 Commit 留言說有 Bad Semll 之類的 XD
最後是 Token 了,因為不可能直接把 Token 寫在 .gitlab-ci.yml 當環境變數放進去,所以我是在 GitLab 的專案設定中寫進去。
也許你會想說這樣做:
1sonar:
2  script:
3    - -Dsonar.logn=${SONAR_TOKEN}
但是前面有提到,因為是 GitLab 自己的 Shell Script 所以不太可能這樣做。 所以就只好給個 Shell Script 來自己處理這個問題摟~ XD
寫這篇文章時想到我好像可以設定 $PATH 然後直接跑
sonar-scanner -Dsonar.login就好了!?
最後,在專案目錄設定一下 Sonar 的設定檔,扣掉 Token 不適合放到 VCS 之外,其他都可以安心寫在裡面。
 1sonar.host.url=https://sonarqube.xxx.com.tw
 2# must be unique in a given SonarQube instance
 3sonar.projectKey=storemap:Android
 4# this is the name displayed in the SonarQube UI
 5sonar.projectName=StoreMap Android
 6
 7# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
 8# Since SonarQube 4.2, this property is optional if sonar.modules is set.
 9# If not set, SonarQube starts looking for source code from the directory containing
10# the sonar-project.properties file.
11sonar.sources=app/src
12
13# Encoding of the source code. Default is default system encoding
14#sonar.sourceEncoding=UTF-8
預設的 sonar.host.url 是 https://localhost:9000 為了要正確上傳,記得加上這行設定到對應的網址上。
Android SDK & JUnit
網路上 Google 了一下,都沒有滿意的 SDK (誤)所以只好自己包一份 XD
 1FROM java:8-jdk
 2MAINTAINER 蒼時弦也 docker@frost.tw
 3
 4ENV ANDROID_SDK_VERSION r24.4.1
 5ENV ANDROID_SDK_SOURCE https://dl.google.com/android/android-sdk_${ANDROID_SDK_VERSION}-linux.tgz
 6
 7RUN  apt-get update \
 8  && apt-get install -y ca-certificates lib32stdc++6 lib32z1 lib32z1-dev \
 9  && mkdir -p /opt
10
11RUN curl -L ${ANDROID_SDK_SOURCE} | tar zxv -C /opt
12
13ENV ANDROID_HOME /opt/android-sdk-linux
14
15ENV PATH $PATH:$ANDROID_HOME/tools
16ENV PATH $PATH:$ANDROID_HOME/platform-tools
17
18RUN  echo "y" | android update sdk -u -a --filter tools \
19  && echo "y" | android update sdk -u -a --filter platform-tools \
20  && echo "y" | android update sdk -u -a --filter extra-android-support \
21  && echo "y" | android update sdk -u -a --filter extra-android-m2repository \
22  && echo "y" | android update sdk -u -a --filter extra-google-google_play_services \
23  && echo "y" | android update sdk -u -a --filter extra-google-m2repository
24
25RUN  echo "y" | android update sdk -u -a --filter android-23 \
26  && echo "y" | android update sdk -u -a --filter build-tools-23.0.2 \
27  && echo "y" | android update sdk -u -a --filter build-tools-23.0.1 \
28  && echo "y" | android update sdk -u -a --filter build-tools-23.0.0
29
30RUN  mkdir ~/.gradle \
31	&& echo "org.gradle.daemon=true" >> ~/.gradle/gradle.properties \
32	&& echo "org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8" >> ~/.gradle/gradle.properties \
33	&& echo "org.gradle.parallel=true" >> ~/.gradle/gradle.properties \
34	&& echo "org.gradle.configureondemand=true" >> ~/.gradle/gradle.properties
原本是想用 alpine 的版本,不過在跑的時候會碰到因為缺少 lib32stdc++ 跟 lib32z1 這兩個套件而無法正常運作。
但是 Alpine 似乎沒有 lib32z1 只有 zlib-dev 可以用,總之因為出問題就只好放棄了 XD
目前用 API v23 開發,沒有要向下相容的需求就只先把 23 版的部分放進去。 至於上面一對 Support 部分裡面還包含了 Android Uint Test 的部分,所以都是得乖乖放進去(也包含了 Firebase 套件⋯⋯)
最後針對 Gradle 做一些額外設定,像是開啟 Parallel 之類的可以讓 Gradle 跑得比較快些。
剩下的 Cache 就參考前面我的 GitLab 設定檔摟
 1before_script:
 2  - export GRADLE_USER_HOME=`pwd`/.gradle
 3  - mkdir -p $GRADLE_USER_HOME
 4  - echo "org.gradle.daemon=true" >> $GRADLE_USER_HOME/gradle.properties
 5  - echo "org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8" >> $GRADLE_USER_HOME/gradle.properties
 6  - echo "org.gradle.parallel=true" >> $GRADLE_USER_HOME/gradle.properties
 7  - echo "org.gradle.configureondemand=true" >> $GRADLE_USER_HOME/gradle.properties
 8
 9cache:
10  paths:
11    - .gradle/caches
12    - .gradle/wrapper
13
14junit:
15  image: elct9620/gitlab-android-junit
16  script:
17    - ./gradlew test
這邊比較特別的是我將 Gradle 的目錄改到目前 GitLab 運行的目錄。 前面有提到因為 Cache 只對當前目錄(專案目錄)有效果,所以必須這樣設定才能正確的快取到。
不過原本做的設定也會一併消失,所以要在寫入一次設定。
Image 會叫
gitlab-android-junit是因為我沒想到 Gradle 會裝好 JUnit 的關係,之後應該是會改名拉 XD
開始寫文章的時候離準備收假只剩下一個多小時,可能不是很詳細,請大家見諒。
