背景
现在很多公司的项目代码都托管在gitlab上,可以利用gitlab的cicd配置实现项目的持续集成与部署。通过在项目根目录中创建.gitlab-ci.yml文件,当代码提交到远程仓库中时,gitlab可以自动识别该文件,并运行文件中配置的流程。对于已经熟悉ci的配置及运行的人来说,这其实并不是一件难事,但是对于刚接触的人就不一定了。我在刚接触ci时,尽管搜过不少资料,第一次实际操作起来仍然一脸抓瞎。于是在gitlab上创建了一个小项目,进行了各种尝试。
如图,我足足花了周末两天时间,提交了63次,才彻底跑成功了整个流程。本文以此项目为示例,讲解gitlab ci如何配置、运行以及自动部署你的项目。
在此之前,想要了解CICD的工作流、ci配置编写及runner的安装等基础知识,可以参考这篇文章:Gitlab-CI配置及使用全流程总结
当然,本文默认你已经掌握了docker的基本使用。(没有接触过docker的人建议去尝试一下下)
准备工作
运行的项目
首先,需要准备一个用来运行的项目,这个项目可以是任何你喜欢的语言、你喜欢的框架项目,这不是关键。作为示例,本文使用的是go语言写的程序,程序主要是从mysql中查询了一行记录然后返回。我们需要先了解下项目的目录结构,这有助于稍后理解ci文件中的部分内容:
-
.gitlab-ci.yml:这是本文的主角,gitlab会自动识别该文件名,然后运行你定义的工作流程; -
db.sql:里面是sql语句,主要用于在ci中创建测试所需的数据库和测试数据。 -
deploy.sh:这个是拿来自动部署时运行的命令行,主要工作是 拉取最新镜像=>运行最新镜像=>清除tag为none的无用镜像;#!/bin/bash docker-compose down docker-compose pull docker-compose up -d prod docker image prune -f -
docker-compose.yml,Dockerfile,Dockerfile.for_go_build:docker相关的三个文件,deploy.sh中的命令根据docker-compose.yml中的内容运行应用镜像。Dockerfile将项目构建为应用部署的镜像,Dockerfile.for_go_build文件专门用于构建一个额外的镜像,这个镜像通常会安装许多额外的依赖包,主要为编译go程序、测试等工作提供基础依赖环境。这样可以保证在生产环境运行的应用镜像体积小、干净纯粹。# Dockerfile.for_go_build 构建为 registry.cn-chengdu.aliyuncs.com/yangsj/go-build-alpine:latest FROM golang:1.15-alpine RUN sed -i "s/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g" /etc/apk/repositories RUN apk --update --no-cache add mariadb-client g++ rsync openssh RUN go env -w GOPROXY=https://goproxy.cn,direct# Dockerfile 构建为应用程序运行的镜像 registry.cn-chengdu.aliyuncs.com/yangsj/gitlab-cicd:latest FROM registry.cn-chengdu.aliyuncs.com/yangsj/go-build-alpine:latest AS builder WORKDIR /code COPY . /code RUN go fmt && go mod tidy RUN go build main.go FROM alpine:latest WORKDIR /code COPY --from=builder /code/main /code/main CMD [ "/code/main" ] -
其他文件: 不重要,忽略。
运行的工具
现在准备好了可以运行的项目,还需要准备可以运行ci的工具人——gitlab-runner。但是,注意如果你的gitlab中已经存在可以运行的runner了,那么下面的步骤可以省略了。现在假定不存在任何runner,需要我们自己为项目注册一个runner:
首先安装这个工具人:
# linux[ubuntu]
sudo apt install gitlab-runner
# Mac
brew install gitlab-runner
安装好了之后,可以进行两种操作:
-
在本地用runner运行你ci中的job,例如需要运行测试流程,我们指定runner使用docker通过本地镜像来运行test_job:
gitlab-runner exec docker test_job --docker-pull-policy=never -
为你gitlab中的项目注册runner,这样当你在进行代码提交、打tag等操作时,runner就会运行你的ci配置。可以输入如下命令进行注册:
gitlab-runner register具体如何注册runner,这里就没有必要细说了,网上一搜一大把。这里需要说明的是,企业开发注册runner需要通过参数提供安全证书,个人普通用户可以直接进行注册。
工具人注册好了,在你的项目settings->CI/CD中就可以看到该runner:
CI文件说明
此部分详细说明.gitlab-ci.yml中的全部内容。为了便于后文对ci配置的理解,这里截个图(如下)。这是为dev分支定义的两个阶段stages(test、build)和对应的jobs(test_job、build_job), build阶段依赖于test阶段,图中test stage正在运行:
CI阶段
ci中的各个阶段stages具体视项目实际需要而定,你可以定义多个阶段,也可以只定义一个。各阶段可以前后依赖、也可以各自独立,组成了整个工作流程。 示例项目中我定义了三个,分别为测试阶段test、编译阶段build和部署阶段deploy:
stages:
- test
- build
- deploy
这三个阶段意味着我定义的工作流程是:测试阶段运行测试程序=>测试成功后编译打包我的应用=>部署编译打包后的应用程序。这个流程你可以自由发挥。
测试阶段
定义好了stages后,我们需要为各个stage定义具体的工作内容,我定义的测试阶段需要做如下工作:
test_job:
stage: test
image: registry.cn-chengdu.aliyuncs.com/yangsj/go-build-alpine:latest
environment:
name: test_ci
variables:
MYSQL_ROOT_PASSWORD: rootroot
MYSQL_DATABASE: hello
TEST_CICD_ENV: dev
services:
- name: mysql:5.7.17
alias: mysql
before_script:
- mysql --version
- mysql -uroot -p$MYSQL_ROOT_PASSWORD -hmysql < ./db.sql
script:
- go fmt && go mod tidy
- CGO_ENABLED=0 go build main.go
- go test -v
# tags:
# - gitlab-cicd
only:
- dev
-
stage: 我的test_job属于test阶段; -
image: 运行test_job的基础环境由go-build-alpine:latest镜像提供,runner在运行test_job时会去拉取这个镜像作为基础运行环境; -
variables: 指定运行这个job所需要的环境变量为 mysql数据库的密码、mysql使用的数据库以及TEST_CICD_ENV这个变量;TEST_CICD_ENV是main.go运行需要的变量,根据其不同的值读取数据库不同的配置,所以你可以在variables中自由定义你需要的环境变量,然后在程序中读取它; -
services: 定义test_job运行的依赖服务为mysql5.7.17,并取别名为mysql,这其实是runner帮你运行了一个mysql容器; -
before_script:在运行正式脚本前,进行一些预处理,比如显示mysql版本、登录mysql并通过db.sql创建测试数据库及测试数据。这个不是必须的; -
script: 主要的测试命令,示例为编译go程序、并运行程序中所有测试文件*_test.go。script内容是必须的,否则runner会运行失败,并提示你需要有此部分内容; -
tags: 指定运行test_job的runner的tags为gitlab-cicd。runner的tags在注册时输入的。(如果你发现你的runner一直处于pending状态, 那么或许可以注掉这个tags试试。我尝试了好久才成功运行的,百度一大堆都是叫你编辑runner,勾选下面这个选项,然而对我不管用:)
only: 指定这个test_job只在dev分支起作用。
编译阶段
当测试通过后,我们需要编译程序,然后自动构建并上传镜像。build_job主要定义了这些内容:
build_job:
stage: build
dependencies:
- test_job
environment:
name: test_ci
variables:
MAIN_IMAGE: registry.cn-chengdu.aliyuncs.com/yangsj/gitlab-cicd:latest
DOCKER_HOST: tcp://docker:2375
image: docker:latest
services:
- docker:dind
before_script:
- docker login --username=<填你的> --password=<填你的> registry.cn-chengdu.aliyuncs.com
script:
- set -ex
- docker build . -t $MAIN_IMAGE
- docker push $MAIN_IMAGE
- docker image prune -f
only:
- dev
stage: 指定build_job属于build阶段;dependencies: 指定build_job依赖于test_job,只有依赖的test_job运行成功后才会运行build_job;environment: 指定build_job与test_job属于同一个环境,即ci测试阶段,这是自由定义的名称;variables: 指定环境变量,这里MAIN_IMAGE代表了要构建的镜像名,因为太长了,指定为变量后可以通过$MAIN_IMAGE来代替,DOCKER_HOST指定docker daemon监听的服务地址。image、services: 在ci中运行docker,是一个"docker in docker"问题,这两个是必须的,你可以认为这是一个固定写法组合;before_script: 运行docker前,进行登录,这个视具体需要相应更改;script: 构建镜像=>推送镜像=>删除无用镜像only: runner只在dev分支运行。
build_job是一个docker in docker问题, 也就是在docker中运行docker,这是一个并不推荐的做法,网上也有分析各种理由,这里只是作为演示这么做了。实际build_job在这个小示例中确实能够运行成功,但是长时间会处于运行中,有时会达到20-30分钟才会成功。总之,比较迷,因为我在公司的项目中也这么干了,但是却跑不起来。所以,我在实际使用中,采用的是手动构建镜像并推送。
部署阶段
构建镜像并推送至仓库后,我们需要自动在服务器上拉取最新的镜像并运行,示例中采用的是deploy.sh命令行根据docker-compose.yml文件来启动应用。所以,我们需要在部署阶段,将项目中的docker-compose.yml以及deploy.sh两个文件上传至服务器指定目录,然后运行deploy.sh。而上传文件,本文使用rsync通过ssh协议上传至服务器:
deploy_job:
stage: deploy
dependencies:
- build_job
image: registry.cn-chengdu.aliyuncs.com/yangsj/go-build-alpine:latest
environment:
name: deploy_prod
before_script:
- mkdir -p ~/.ssh
- eval $(ssh-agent -s)
- echo "$TEST_KNOWN_HOST" > ~/.ssh/known_hosts
- echo "$TEST_CONFIG" > ~/.ssh/config
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 0600 ~/.ssh/id_rsa
- cat ~/.ssh/id_rsa
- ssh-add
script:
- rsync -av docker-compose.yml deploy.sh root@huaweiyun:/dockers
- ssh huaweiyun "cd /dockers;bash deploy.sh;"
only:
- master
stage:deploy_job属于deploy阶段;dependencies:deploy_job依赖于build_job;image:指定go-build-alpine:latest为基础运行环境;environment: 指定deploy_job的环境名为deploy_prod;before_script: 这部分主要在做ssh配置,以让你的runner能够通过ssh来上传部署需要的文件;script: 通过rsync将docker-compose.yml、deploy.sh上传至华为云服务器的/dockers目录中=>然后再ssh到华为云服务器并执行命令行cd /dockers;bash deploy.sh;only: 指定deploy_job只在master分支运行。
ssh话题
如果你对ssh的流程不是很了解,那么上述部署阶段中before_script的内容,你可能会很懵圈,不知道在做什么。如果是,推荐你去看下阮一峰大佬的ssh教程。这里针对部署阶段的ssh内容做个简要的解释:
假定用户名为root,当我们要通过ssh连接到服务器时,需要输入命令:
ssh [-p 22] root@139.9.240.168
因为每次都要输入用户名@服务器地址,所以可以在~/.ssh/config文件中配置:
# ~/.ssh/config
Host huaweiyun
Port 22
HostName 139.9.240.168
User root
IdentityFile ~/.ssh/id_rsa
然后就可以直接输入如下简短的命令进行登录:
ssh huaweiyun
通过密钥登录的过程有如下四个步骤:
-
首先需要将你的公钥
id_rsa.pub中的内容存入服务器中authorized_keys文件中,这一步你可以手动拷贝,也可以直接运行如下命令自动上传至服务器:ssh-copy-id -i ~/.ssh/id_rsa.pub root@139.9.240.168 # 注意服务器地址换成你自己的。 -
当你通过命令ssh至服务器时,会询问你陌生服务器是否是你需要连接的目标服务器,选择yes会将服务器公钥的指纹存入本地的
known_hosts文件中,下次连接时就不会再次询问; -
服务器会发送随机数据至本地,本地通过私钥
id_rsa进行加密后发送至服务器; -
服务器通过公钥进行解密,通过则连接成功。(图中4画错了,公钥肯定是与本地私钥成对的公钥,不是服务器的公钥,这里不重新画了。)
所以,为了在ci中能够自动通过ssh进行连接,我们需要准备4件事情:
-
将你的公钥存至服务器;
-
将你电脑中服务器的公钥指纹保持至ci的
~/.ssh/known_hosts文件中。查看你电脑中目标服务器指纹的命令是:ssh-keyscan -p 22 139.9.240.168 # 请换成你的服务器端口和ip -
将你的ssh配置保存至ci的
~/.ssh/config文件中。其实你也可以不用config的形式,直接通过username@ip的形式连接,但是这样后续命令中如果需要再次ssh,就会重复编写冗长的代码。推荐config的形式,保密性更好,更简洁。 -
将你的私钥保存至ci的
~/.ssh/id_rsa文件中,同时修改权限为0600, 否则ci会提示你文件权限过大而失败。
关于ssh会话ssh-agent不了解的可以自行去学习,其实就是避免重复输入密码。
ci中整个ssh的操作,其实就是在模拟你本地的ssh环境,因为你本地配置好后可以直接ssh到服务器,所以只需要拷贝相应的值到ci相应的文件中,ci就能够通过这些配置直接ssh到服务器了,然后就可以根据脚本自动上传文件。在实际的开发中,一般不会是你自己工作电脑的ssh配置,万一你跑路了呢?往往是一个公共的配置,大家在ci中都使用一样的配置。
最后,再来看deploy_job中这段脚本, 应该就很简单了:
before_script:
- mkdir -p ~/.ssh
- eval $(ssh-agent -s)
- echo "$TEST_KNOWN_HOST" > ~/.ssh/known_hosts
- echo "$TEST_CONFIG" > ~/.ssh/config
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 0600 ~/.ssh/id_rsa
- cat ~/.ssh/id_rsa
- ssh-add
其中,为了安全性,具体的ssh配置值,如$TEST_CONFIG配置为runner中的变量,因为不可能将ssh的相关配置值直接暴露出来。关于runner变量的配置,可以在gitlab的settings->CI/CD->Variables中配置,如图:
最后
gitlab的cicd体验下来,发现功能其实并不是很强大,docker in docker问题是最失败的,不知道专业的CICD工具如何,例如目前使用最多的jenkins,有时间也捣鼓捣鼓下。水平有限,如有不当之处,还请指正。