Golang基於GitLab CI/CD部署方案
持續整合(Continuous Integration)是一種軟體開發實踐,即團隊開發成員經常整合它們的工作,通過每個成員每天至少整合一次,也就意味著每天可能會發生多次整合。每次整合都通過自動化的構建(包括編譯,釋出,自動化測試)來驗證,從而儘早地發現整合錯誤。
持續部署(Continuous Deployment)是通過自動化的構建、測試和部署迴圈來快速交付高質量的產品。某種程度上代表了一個開發團隊工程化的程度,畢竟快速運轉的網際網路公司人力成本會高於機器,投資機器優化開發流程化相對也提高了人的效率,讓 engineering productivity 最大化。
1. 環境準備
本次試驗是基於CentOS 7.3,Docker 17.03.2-ce環境下的。Docker的安裝這裡就不贅述了,提供官方連結: ofollow,noindex" target="_blank">Get Docker CE for CentOS 。
1.1. Docker啟動GitLab
啟動命令如下:
docker run --detach \ --hostname gitlab.chain.cn \ --publish 8443:443 --publish 8080:80 --publish 2222:22 \ --name gitlab \ --restart always \ --volume /Users/zhangzc/gitlab/config:/etc/gitlab \ --volume /Users/zhangzc/gitlab/logs:/var/log/gitlab \ --volume /Users/zhangzc/gitlab/data:/var/opt/gitlab \ gitlab/gitlab-ce
port,hostname,volume根據具體情況具體設定。
1.2. Docker啟動gitlab-runner
啟動命令如下:
sudo docker run -d / --name gitlab-runner / --restart always / -v /Users/zhangzc/gitlab-runner/config:/etc/gitlab-runner / -v /Users/zhangzc/gitlab-runner/run/docker.sock:/var/run/docker.sock / gitlab/gitlab-runner:latest
volume根據具體情況具體設定。
1.3. 用於整合部署的映象製作
我們的整合和部署都需要放在一個容器裡面進行,所以,需要製作一個映象並安裝一些必要的工具,用於整合和部署相關操作。目前我們的專案都是基於Golang 1.9.2的,這裡也就基於Golang 1.9.2的映象制定一個特定的映象。
Dockerfile內容如下:
# Base image: https://hub.docker.com/_/golang/ FROM golang:1.9.2 USER root
Install golint
ENV GOPATH /go
ENV PATH ${GOPATH}/bin:$PATH
RUN mkdir -p /go/src/golang.org/x
RUN mkdir -p /go/src/github.com/golang
COPY source/golang.org /go/src/golang.org/x/
COPY source/github.com /go/src/github.com/golang/
RUN go install github.com/golang/lint/golint
install docker
RUN curl -O https://get.docker.com/builds/Linux/x86_64/docker-latest.tgz \
&& tar zxvf docker-latest.tgz \
&& cp docker/docker /usr/local/bin/ \
&& rm -rf docker docker-latest.tgz
install expect
RUN apt-get update
RUN apt-get -y install tcl tk expect
其中Golint是用於Golang程式碼風格檢查的工具。
Docker是由於需要在容器裡面使用宿主的Docker命令,這裡就需要安裝一個Docker的可執行檔案,然後在啟動容器的時候,將宿主的 /var/run/docker.sock 檔案掛載到容器內的同樣位置。
expect是用於SSH自動登入遠端伺服器的工具,這裡安裝改工具是為了可以實現遠端伺服器端部署應用。
另外,在安裝Golint的時候,是需要去golang.org下載原始碼的,由於牆的關係,go get命令是執行不了的。為了處理這個問題,首先通過其他渠道先下載好相關原始碼,放到指定的路徑下,然後copy到映象裡,並執行安裝即可。
下面有段指令碼是用於生成映象的:
#!/bin/bash echo "提取構建映象時需要的檔案" source_path="source" mkdir -p $source_path/golang.org mkdir -p $source_path/github.com cp -rf $GOPATH/src/golang.org/x/lint $source_path/golang.org/ cp -rf $GOPATH/src/golang.org/x/tools $source_path/golang.org/ cp -rf $GOPATH/src/github.com/golang/lint $source_path/github.com echo "構建映象" docker build -t go-tools:1.9.2 . echo "刪除構建映象時需要的檔案" rm -rf $source_path
生成映象後,推送到映象倉庫,並在gitlab-runner的伺服器上拉取該映象。
本次試驗的GitLab和gitlab-runner是執行在同一伺服器的Docker下的 。
2. Runner註冊及配置
2.1. 註冊
環境準備好後,在伺服器上執行以下命令,註冊Runner:
docker exec -it gitlab-runner gitlab-ci-multi-runner register
按照提示輸入相關資訊:
Please enter the gitlab-ci coordinator URL: # gitlab的url, 如:https://gitlab.chain.cn/ Please enter the gitlab-ci token for this runner: # gitlab->你的專案->settings -> CI/CD ->Runners settings Please enter the gitlab-ci description for this runner: # 示例:demo-test Please enter the gitlab-ci tags for this runner (comma separated): # 示例:demo Whether to run untagged builds [true/false]: # true Please enter the executor: docker, parallels, shell, kubernetes, docker-ssh, ssh, virtualbox, docker+machine, docker-ssh+machine: # docker Please enter the default Docker image (e.g. ruby:2.1): # go-tools:1.9.2(之前自己製作的映象)
成功後,可以看到GitL ab->你的專案->Settings -> CI/CD ->Runners settings頁面下面有以下內容:
2.2. 配置
註冊成功之後,還需要在原有的配置上做一些特定的配置,如下:
[[runners]] name = "demo-test" url = "https://gitlab.chain.cn/" token = "c771fc5feb1734a9d4df4c8108cd4e" executor = "docker" [runners.docker] tls_verify = false image = "go-tools:1.9.2" privileged = false disable_cache = false volumes = ["/var/run/docker.sock:/var/run/docker.sock"] extra_hosts = ["gitlab.chain.cn:127.0.0.1"] network_mode = "host" pull_policy = "if-not-present" shm_size = 0 [runners.cache]
這裡先解釋下gitlab-runner的流程吧,gitlab-runner在執行的時候,會根據上面的配置啟動一個容器,即配置中的go-tools:1.9.2,其中所有的啟動引數都會在[runners.docker]節點下配置好,包括掛載啊,網路啊之類的。容器啟動成功之後,會使用這個容器去GitLab上pull程式碼,然後根據自己定義的規則進行檢驗,全部檢測成功之後便是部署了。
volumes:是為了在容器中可以執行宿主機的Docker命令。
extra_hosts:給GitLab添加個host對映,對映到127.0.0.1
network_mode:令容器的網路與宿主機一致,只有這樣才能通過127.0.0.1訪問到GitLab。
pull_policy:當指定的映象不存在的話,則通過docker pull拉取。
3. 定義規則
在GitLab專案根目錄建立.gitlab-ci.yml檔案,填寫Runner規則,具體語法課參考官方文件: https://docs.gitlab.com/ee/ci/yaml/ 。
3.1. Go整合命令
下面介紹幾個Golang常見的整合命令。
包列表,正如在官方文件中所描述的那樣,Go專案是包的集合。下面介紹的大多數工具都將使用這些包,因此我們需要的第一個命令是列出包的方法。我們可以用go list子命令來完成:
go list ./...
請注意,如果我們要避免將我們的工具應用於外部資源,並將其限制在我們的程式碼中。 那麼我們需要去除vendor目錄,命令如下:
go list ./... | grep -v /vendor/
單元測試,這些是您可以在程式碼中執行的最常見的測試。每個.go檔案需要一個能支援單元測試的_test.go檔案。可以使用以下命令執行所有包的測試:
go test -short $(go list ./... | grep -v /vendor/)
資料競爭,這通常是一個難以逃避解決的問題,Go工具預設具有(但只能在Linux/amd64、FreeBSD/amd64、Darwin/amd64和Windows/amd64上使用):
go test -race -short $(go list . /…| grep - v /vendor/)
程式碼覆蓋,這是評估程式碼的質量的必備工具,並能顯示哪部分程式碼進行了單元測試,哪部分沒有。
要計算程式碼覆蓋率,需要執行以下指令碼:
PKG_LIST=$(go list ./... | grep -v /vendor/) for package in ${PKG_LIST}; do go test -covermode=count -coverprofile "cover/${package##*/}.cov" "$package" ; done tail -q -n +2 cover/*.cov >> cover/coverage.cov go tool cover -func=cover/coverage.cov
如果我們想要獲得HTML格式的覆蓋率報告,我們需要新增以下命令:
go tool cover -html=cover/coverage.cov -o coverage.html
構建,最後一旦程式碼經過了完全測試,我們要對程式碼進行編譯,從而構建可以執行的二進位制檔案。
go build .
linter,這是我們在程式碼中使用的第一個工具:linter。它的作用是檢查程式碼風格/錯誤。這聽起來像是一個可選的工具,或者至少是一個“不錯”的工具,但它確實有助於在專案上保持一致的程式碼風格。
linter並不是Go本身的一部分,所以如果要使用,你需要手動安裝它(之前的go-tools映象我們已經安裝過了)。
使用方法相當簡單:只需在程式碼包上執行它(也可以指向. go檔案):
$ golint -set_exit_status $(go list ./... | grep -v /vendor/)
注意-set_exit_status選項。 預設情況下,Golint僅輸出樣式問題,並帶有返回值(帶有0返回碼),所以CI不認為是出錯。 如果指定了-set_exit_status,則在遇到任何樣式問題時,Golint的返回碼將不為0 。
3.2. Makefile
如果我們不想在.gitlab-ci.yml檔案中寫的太複雜,那麼我們可以把持續整合環境中使用的所有工具,全部打包在Makefile中,並用統一的方式呼叫它們。
這樣的話,.gitlab-ci.yml檔案就會更加簡潔了。當然了,Makefile同樣也可以呼叫*.sh指令碼檔案。
3.3. 配置示例
3.3.1. .gitlab-ci.yml
image: go-tools:1.9.2 stages: - build - test - deploy before_script: - mkdir -p /go/src/gitlab.chain.cn/ZhangZhongcheng /go/src/_/builds - cp -r $CI_PROJECT_DIR /go/src/gitlab.chain.cn/ZhangZhongcheng/demo - ln -s /go/src/gitlab.chain.cn/ZhangZhongcheng /go/src/_/builds/ZhangZhongcheng - cd /go/src/_/builds/ZhangZhongcheng/demo unit_tests: stage: test script: - make test tags: - demo race_detector: stage: test script: - make race tags: - demo code_coverage: stage: test script: - make coverage tags: - demo code_coverage_report: stage: test script: - make coverhtml only: - master tags: - demo lint_code: stage: test script: - make lint build: stage: build script: - pwd - go build . tags: - demo build_image: stage: deploy script: - make build_image tags: - demo
3.3.2. Makefile
PROJECT_NAME := "demo" PKG := "gitlab.chain.cn/ZhangZhongcheng/$(PROJECT_NAME)" PKG_LIST := $(shell go list ./... | grep -v /vendor/) GO_FILES := $(shell find . -name '*.go' | grep -v /vendor/ | grep -v _test.go) test: ## Run unittests @go test -v ${PKG_LIST} lint: ## Lint the files @golint ${PKG_LIST} race: ## Run data race detector @go test -race -short ${PKG_LIST} coverage: ## Generate global code coverage report ./scripts/coverage.sh; coverhtml: ## Generate global code coverage report in HTML ./scripts/coverage.sh html; build_image: ./scripts/buildDockerImage.sh
3.3.3. coverage.sh
#!/bin/bash # # Code coverage generation COVERAGE_DIR="${COVERAGE_DIR:-coverage}" PKG_LIST=$(go list ./... | grep -v /vendor/) # Create the coverage files directory mkdir -p "$COVERAGE_DIR"; # Create a coverage file for each package for package in ${PKG_LIST}; do go test -covermode=count -coverprofile "${COVERAGE_DIR}/${package##*/}.cov" "$package" ; done ; # Merge the coverage profile files echo 'mode: count' > "${COVERAGE_DIR}"/coverage.cov ; tail -q -n +2 "${COVERAGE_DIR}"/*.cov >> "${COVERAGE_DIR}"/coverage.cov ; # Display the global code coverage go tool cover -func="${COVERAGE_DIR}"/coverage.cov ; # If needed, generate HTML report if [ "$1" == "html" ]; then go tool cover -html="${COVERAGE_DIR}"/coverage.cov -o coverage.html ; fi # Remove the coverage files directory rm -rf "$COVERAGE_DIR";
3.3.4. buildDockerImage.sh
#!/bin/bash #檢測GOPATH echo "檢測GOPATH" if [ -z "$GOPATH" ];then echo "GOPATH 未設定" exit 1 else echo "GOPATH=$GOPATH" fi #初始化資料 echo "初始化資料" new_version="1.0.0" old_version="1.0.0" golang_version="1.9.2" app_name="application" projust_root="demo" DOCKER_IMAGE_NAME="demo" REGISTRY_HOST="xxx.xxx.xxx.xxx:5000" path="/go/src/_/builds/ZhangZhongcheng/demo" #當前容器更換為舊標籤 echo "當前容器更換為舊標籤" docker rmi $REGISTRY_HOST/$DOCKER_IMAGE_NAME:$old_version # 基於golang:1.9.2映象啟動的容器例項,編譯本專案的二進位制可執行程式 echo "基於golang:1.9.2映象啟動的容器例項,編譯本專案的二進位制可執行程式" cd $path go build -o $app_name echo "檢測 $app_name 應用" FILE="$path/$app_name" if [ -f "$FILE" ];then echo "$FILE 已就緒" else echo "$FILE 應用不存在" exit 1 fi #docker構建映象 禁止在構建上下文之外的路徑 新增複製檔案 #所以在此可以用命令把需要的檔案cp到 dockerfile 同目錄內 ,構建完成後再用命令刪除 cd $path/scripts echo "提取構建時需要的檔案" cp ../$app_name $app_name # 基於當前目錄下的Dockerfile構建映象 echo "基於當前目錄下的Dockerfile構建映象" echo "docker build -t $REGISTRY_HOST/$DOCKER_IMAGE_NAME:$new_version ." docker build -t $REGISTRY_HOST/$DOCKER_IMAGE_NAME:$new_version . # 刪除本次生成的可執行檔案 以及構建所需要的檔案 echo "刪除本次生成的可執行檔案 以及構建所需要的檔案" rm -rf $app_name rm -rf ../$app_name #檢視映象 echo "檢視映象" docker images | grep $DOCKER_IMAGE_NAME #推送映象 echo "推送映象" echo "docker push $REGISTRY_HOST/$DOCKER_IMAGE_NAME:$new_version" docker push $REGISTRY_HOST/$DOCKER_IMAGE_NAME:$new_version echo "auto deploy" ./automationDeployment.sh $new_version $old_version
3.3.5. automationDeployment.sh
#!/usr/bin/expect #指定shebang #設定超時時間為3秒 set ip xxx.xxx.xxx.xxx set password "xxxxxxx" set new_version [lindex $argv 0] set old_version [lindex $argv 1] spawn ssh root@$ip expect { "*yes/no" { send "yes\r"; exp_continue} "*password:" { send "$password\r" } } expect "#*" send "cd /root/demo/\r" send "./docker_run_demo.sh $new_version $old_version\r" expect eof 3.3.6. Dockerfile FROM golang:1.9.2 #定義環境變數 alpine專用 #ENV TIME_ZONE Asia/Shanghai ADD application /go/src/demo/ WORKDIR /go/src/demo ADD run_application.sh /root/ RUN chmod 755 /root/run_application.sh CMD sh /root/run_application.sh EXPOSE 8080
3.3.7. run_application.sh
#!/bin/bash #對映ip cp /usr/share/zoneinfo/Asia/Shanghai/etc/localtime cd /go/src/demo/ ./application
4. 結果
以下為部署成功後的截圖: