持续集成(Continuous Integration)是一种软件开发实践,即团队开发成员经常集成它们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。
持续部署(Continuous Deployment)是通过自动化的构建、测试和部署循环来快速交付高质量的产品。某种程度上代表了一个开发团队工程化的程度,毕竟快速运转的互联网公司人力成本会高于机器,投资机器优化开发流程化相对也提高了人的效率,让 engineering productivity 最大化。
1. 环境准备
本次试验是基于 CentOS 7.3,Docker 17.03.2-ce 环境下的。Docker 的安装这里就不赘述了,提供官方链接:https://docs.docker.com/install/linux/docker-ce/centos/ 。
1.1 Docker 启动 GitLab
启动命令如下:
1 2 3 4 5 6 7 8 9 10 |
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
启动命令如下:
1 2 3 4 5 6 7 |
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 内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# 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 到镜像里,并执行安装即可。
下面有段脚本是用于生成镜像的:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#!/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:
1 2 |
docker exec -it gitlab-runner gitlab-ci-multi-runner register |
按照提示输入相关信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 配置
注册成功之后,还需要在原有的配置上做一些特定的配置,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
[[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 子命令来完成:
1 2 |
go list ./... |
请注意,如果我们要避免将我们的工具应用于外部资源,并将其限制在我们的代码中。 那么我们需要去除 vendor 目录,命令如下:
1 2 |
go list ./... | grep -v /vendor/ |
单元测试 这些是您可以在代码中运行的最常见的测试。每个 .go
文件需要一个能支持单元测试的 _ test.go
文件。可以使用以下命令运行所有包的测试:
1 2 |
go test -short $(go list ./... | grep -v /vendor/) |
数据竞争 这通常是一个难以逃避解决的问题,Go 工具默认具有(但只能在 Linux/amd64、FreeBSD/amd64、Darwin/amd64 和 Windows/amd64 上使用):
1 2 |
go test -race -short $(go list . /…| grep - v /vendor/) |
代码覆盖 这是评估代码的质量的必备工具,并能显示哪部分代码进行了单元测试,哪部分没有。
要计算代码覆盖率,需要运行以下脚本:
1 2 3 4 5 6 7 |
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 格式的覆盖率报告,我们需要添加以下命令:
1 2 |
go tool cover -html=cover/coverage.cov -o coverage.html |
构建 最后一旦代码经过了完全测试,我们要对代码进行编译,从而构建可以执行的二进制文件。
1 2 |
go build . |
linter 这是我们在代码中使用的第一个工具:linter。它的作用是检查代码风格/错误。这听起来像是一个可选的工具,或者至少是一个“不错”的工具,但它确实有助于在项目上保持一致的代码风格。
linter 并不是 Go 本身的一部分,所以如果要使用,你需要手动安装它(之前的 go-tools 镜像我们已经安装过了)。
使用方法相当简单:只需在代码包上运行它(也可以指向 . go
文件):
1 2 |
$ 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#!/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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
#!/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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#!/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
1 2 3 4 5 6 7 8 9 10 |
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
1 2 3 4 5 6 |
#!/bin/bash #映射ip cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime cd /go/src/demo/ ./application |
4. 结果
以下为部署成功后的截图: