98°

基于GitLab的CICD流程

最近想把公司基于Jenkins的自动化构建修改到GitLab上,主要原因是在Jenkins上没有做权限控制,大家使用同一个账号,造成不同项目组的源码泄漏问题;另外还有一个使用Jenkins的独立服务器,感觉还是资源浪费了点。

配置GitLab的runner

从gitlab上获取Runner注册的token。有三种方式

  • 按项目
    项目->设置->CI/CD->Runner
  • 按分组
    分组->设置->CI/CD->Runner
  • 全局
    管理中心->概览->Runner

运行Runner

  • 从gitee上下载对应的的helm包,地址
  • 修改values文件的gitlabUrlrunnerRegistrationTokentags
  • 执行以下命令
helm package .
helm install --namespace gitlab --name gitlab-runner *.tgz

说明:

  • gitlabUrl 就是你gitlab私服地址
  • runnerRegistrationToken 就是从第1步获取的runner注册的token值
  • tags 标识这个runner,stage的job就是根据该tag选择对应的runner的
  • Chart.yaml定义该helm的基本信息,注意里面的name字段必须和当前目录的名称一致
  • templates/pvc.yaml 定义了一个Dynamic Provisioning的PVC,使用的storageClassName是阿里云的alicloud-disk-efficiency(我们的k8s是阿里云的容器服务),它会自动创建对应的PV,并创建一个云盘实例。这个可以用来处理多个Job之间的缓存文件
  • templates/configmap.yaml文件的runners.kubernetes.volumes.pvc定义了PVC的名称,即上一步定义的PVC名称,如果有修改注意同步

至此,一个gitlab-runner应该就可以注册到对应的gitlab server上去了。

配置环境变量

对于在构建过程使用到比较私密的信息,应该直接配置到gitlab server的环境变量上,这里我们主要配置一下三个参数:

  • REGISTRY_PASSWORD Harbor私服的密码
  • REGISTRY_USERNAME Harbor私服的用户名
  • KUBE_CONFIG kubectl执行所需的账号、证书等信息,该字符串可以使用以下命令获取
echo $(cat ~/.kube/config | base64) | tr -d " "

这里的环境变量是配置在group下的CI/CD页面的环境变量

对于以下容器的配置为了纯手工配置,更好的方式应该是使用Dockerfile进行编写。这里只是为了更好的说明基础镜像的制作过程。

配置Node容器

Node容器主要用于编译前端项目,一般主要使用yarn下载依赖,npm编译打包。所以Node容器需要包含这两个命令。

$ docker pull node //拉取最新的node镜像
$ docker run -it --rm --name node node /bin/sh //运行node镜像,并且进入
$ yarn config set registry https://registry.npm.taobao.org //配置yarn的源为淘宝源
$ yarn config list //查看配置
--------------------------------
info yarn config
{
  'version-tag-prefix': 'v',
  'version-git-tag': true,
  'version-commit-hooks': true,
  'version-git-sign': false,
  'version-git-message': 'v%s',
  'init-version': '1.0.0',
  'init-license': 'MIT',
  'save-prefix': '^',
  'bin-links': true,
  'ignore-scripts': false,
  'ignore-optional': false,
  registry: 'https://registry.yarnpkg.com',
  'strict-ssl': true,
  'user-agent': 'yarn/1.16.0 npm/? node/v12.5.0 linux x64',
  version: '1.16.0'
}
info npm config
{
  version: '1.16.0'
}
Done in 0.07s.
--------------------------------
$ docker commit node harbor_url/tools/node-taobao //另外打开一个窗口,提交修改后的node镜像
$ docker push harbor_url/tools/node-taobao //推送镜像到Harbor私服

配置Java容器

Java容器主要用于编译Java项目,主要用到JDKMAVEN

$ docker pull alpine // 拉取最新的alpine镜像
$ docker run -it --rm --name java alpine //进入镜像
$ mkdir -p /opt/java // 创建java目录
$ mkdir -p /opt/maven //创建maven目录
$ docker cp jdk-8u211-linux-x64.tar.gz java:/opt/java/    //从主机上拷贝JDK到容器内部 
$ docker cp apache-maven-3.6.1-bin.tar.gz java:/opt/maven/   //从主机上拷贝MAVEN到容器内部
/opt/maven $ tar -xzvf apache-maven-3.6.1-bin.tar.gz  //在容器内解压MAVEN
/opt/maven $ rm -rf apache-maven-3.6.1-bin.tar.gz     //删除MAVEN压缩包
/opt/java $ tar -xzvf jdk-8u211-linux-x64.tar.gz      //在容器内解压JDK
/opt/java $ rm -rf jdk-8u211-linux-x64.tar.gz         //删除JDK压缩包
$ vi /etc/profile	//配置环境变量
--------------------------------
export JAVA_HOME=/opt/java/jdk1.8.0_211
export M2_HOME=/opt/maven/apache-maven-3.6.1
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$JAVA_HOME/bin:$M2_HOME/bin
--------------------------------
//Java是基于GUN Standard C library(glibc),Alpine是基于MUSL libc(mini libc),所以需要安装glibc库
// 参考地址:https://blog.csdn.net/Dannyvon/article/details/80092834
$ echo http://mirrors.ustc.edu.cn/alpine/v3.10/main > /etc/apk/repositories
$ echo http://mirrors.ustc.edu.cn/alpine/v3.10/community >> /etc/apk/repositories
$ apk update
$ apk --no-cache add ca-certificates
$ wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.27-r0/glibc-2.27-r0.apk
$ wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
$ apk add glibc-2.27-r0.apk  // 至此glibc库安装完毕
$ source /etc/profile  //生效环境变量
$ java -version  //查看JAVA版本
--------------------------------
java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)
--------------------------------
$ mvn -v  //查看MAVEN版本
--------------------------------
Apache Maven 3.6.1 (d66c9c0b3152b2e69ee9bac180bb8fcc8e6af555; 2019-04-04T19:00:29Z)
Maven home: /opt/maven/apache-maven-3.6.1
Java version: 1.8.0_211, vendor: Oracle Corporation, runtime: /opt/java/jdk1.8.0_211/jre
Default locale: en_US, platform encoding: ANSI_X3.4-1968
OS name: "linux", version: "4.9.125-linuxkit", arch: "amd64", family: "unix"
--------------------------------
$ docker commit java harbor_url/tools/java:1.8  //在另外一个窗口,提交镜像
$ docker push harbor_url/tools/java:1.8   //将镜像推送到Harbor私服

注意source /etc/profile这个命令只是当前有效,在提交镜像后,使用镜像重新运行容器还需要在执行一次该命令。具体如何永久生效,还不知道。如果大佬知道可以告知下。

配置Curl&Git容器

该容器只是适应我们公司的状况,我们公司的前端打包其实并不需要Node容器。前端打包过程是在本地打包编译后生成dist目录下的文件,然后压缩上传到内网的oss上。接着在Jenkins上执行脚本,其实是从内网的oss下载并解压,然后根据Dockerfile在制作业务镜像。这个过程就需要使用到Curl和Git命令。整个过程是为了解决线上打包环境可能跟开发本地不一致,以及前端打包需要下载依赖和编译耗时的问题,通过这样的一个流程,前端每次构建的时间就非常短,几秒钟就能搞定。

$ docker run -it --rm --name curl-git alpine
$ echo http://mirrors.ustc.edu.cn/alpine/v3.10/main > /etc/apk/repositories
$ echo http://mirrors.ustc.edu.cn/alpine/v3.10/community >> /etc/apk/repositories
$ apk update
$ apk add curl
$ apk add git
$ curl -V
--------------------------------
curl 7.61.1 (x86_64-alpine-linux-musl) libcurl/7.61.1 LibreSSL/2.6.5 zlib/1.2.11 libssh2/1.8.2
Release-Date: 2018-09-05
Protocols: dict file ftp ftps gopher http https imap imaps pop3 pop3s rtsp scp sftp smb smbs smtp smtps telnet tftp
Features: AsynchDNS IPv6 Largefile NTLM NTLM_WB SSL libz UnixSockets HTTPS-proxy
--------------------------------
$ git version
--------------------------------
git version 2.15.3
--------------------------------
$ docker commit curl-git harbor_url/tools/curl-git  //在另外一个窗口,提交镜像
$ docker push harbor_url/tools/curl-git   //将镜像推送到Harbor私服

配置kubectl容器

kubectl容器是用于部署应用到k8s集群,需要用到config的配置信息。但是这个信息一般比较私密,不会直接打到容器里面,容器里面只会放一个kubectl客户端,然后具体的配置文件在运行期间在放到~/.kube/config文件内。

$ docker run -it --rm --name kubectl alpine  //运行并且进入容器
$ docker cp kubectl kubectl:/usr/bin/  //拷贝kubectl客户端到容器内部,kubectl客户端可以直接在k8s的master节点找到,或者直接在github上下载到对应版本的客户端
$ chmod +x /usr/bin/kubectl //设置为可执行文件
$ kubectl version //查看版本,因为没有配置服务的证书,所以服务端的信息打印不出来
--------------------------------
Client Version: version.Info{Major:"1", Minor:"12+", GitVersion:"v1.12.6-aliyun.1", GitCommit:"4304b26", GitTreeState:"", BuildDate:"2019-04-08T08:50:29Z", GoVersion:"go1.10.8", Compiler:"gc", Platform:"linux/amd64"}
The connection to the server localhost:8080 was refused - did you specify the right host or port?
--------------------------------
$ docker commit kubectl harbor_url/tools/kubectl:1.12.6  //在另外一个窗口,提交镜像
$ docker push  harbor_url/tools/kubectl:1.12.6   //将镜像推送到Harbor私服

配置.gitlab-ci.yml

# 如果各个stage没有使用镜像,则使用默认镜像
image: $HARBOR_URL/tools/alpine
stages:
  - build
  - deploy
# 全局变量定义
variables:
  # 镜像名,tag默认使用pipeline_id
  IMAGE_NAME: <harbor_url>/<image_path>/<image_name>
  # 定义应用名称,即deployment_name、container_name、service_name等
  APP_NAME: <application_name>
  # 应用端口,一般应用端口和service的端口一致
  APP_PORT: 80
  # 定义命名空间,需要根据不同的打包命令替换为真实命名空间
  NAMESPACE: dev
  # HARBOR_URL
  HARBOR_URL: <harbor_url>
docker_build_job:
  # 包含curl 和 git 工具的镜像
  image: $HARBOR_URL/tools/curl-git
  stage: build
  # 定义只有dev分支push或者merge事件触发Job
  only:
    refs:
      - dev
  tags:
    - <runner_tag>
  # 使用dind模式,连接到一个有启动docker的容器
  services:
    - $HARBOR_URL/tools/docker-dind:18.09.7
  variables:
    # docker daemon 启动的参数
    DOCKER_DRIVER: overlay
    DOCKER_HOST: tcp://localhost:2375
  # 该 stage 执行的脚本命令
  # 1. 执行构建脚本build.sh
  # 2. 登录Harbor私服
  # 3. 根据Dockerfile构建image
  # 4. push image 到Harbor
  script:
  	# build.sh脚本为公司内部脚本,根据后面的参数配置不同的环境
    # 主要流程是下载开发打包好的压缩文件,然后解压,接着根据不同环境替换变量
    - sh ./build.sh -b development
    - docker login -u $REGISTRY_USERNAME -p $REGISTRY_PASSWORD $HARBOR_URL
    - docker build -t $IMAGE_NAME:$CI_PIPELINE_ID .
    - docker push $IMAGE_NAME:$CI_PIPELINE_ID
k8s_deploy_job:
  # 包含kubectl 工具的镜像
  image: $HARBOR_URL/tools/kubectl:1.12.6
  stage: deploy
  only:
    refs:
      - dev
  tags:
    - <runner_tag>
  # 该 stage 执行的脚本命令
  # 1. 创建/etc/deploy/config文件
  # 2. 将k8s的证书信息写入/etc/deploy/config
  # 3. 替换deployment.yaml文件中的各个变量
  # 4. 部署到k8s集群环境中
  script:
    - mkdir -p ~/.kube
    - touch ~/.kube/config
    # KUBE_CONFIG这个环境变量是在gitlab server中配置的
    - echo $KUBE_CONFIG | base64 -d > ~/.kube/config
    # 对deployment.yaml进行对应的变量替换
    - sed -i "s?IMAGE_TAG?$CI_PIPELINE_ID?g" deployment.yaml
    - sed -i "s?IMAGE_NAME?$IMAGE_NAME?g" deployment.yaml
    - sed -i "s?APP_NAME?$APP_NAME?g" deployment.yaml
    - sed -i "s?NAMESPACE?$NAMESPACE?g" deployment.yaml
    - sed -i "s?APP_PORT?$APP_PORT?g" deployment.yaml
    - kubectl apply -f deployment.yaml

这个.gitlab-ci.yml文件少了基于node和java镜像的配置。不过都是大同小异,就不在复述。对于node镜像打包后一般会生成dist目录,这个时候可以加个步骤把dist目录压缩,然后定义artifacts,这样当前的stage执行完后就会上传该压缩包到gitlab-server。接着下一个stage就会自动下载这个压缩包,这样我们就可以解压这个压缩包,然后再根据Dockerfile进行打包了,同样是使用dind的模式。对于java镜像同样可以使用这个原理,mvn编译打包出一个jar包或者war包,然后传递到下一个stage,再进行构建镜像;不过如果使用maven的docker插件的话,那就不用分两个stage了,直接在java的那个镜像加个services的定义,这样就可以使用mvn docker:build docker:push命令了。不过要注意,使用maven的docker插件,镜像是定义在pom.xml文件的,这个需要和外部的.gitlab-ci.yml文件定义的镜像名称同步

node镜像部分定义
node_build_job:
  image: $HARBOR_URL/tools/node-taobao
  stage: package
  only:
    refs:
      - dev
  tags:
    - <runner_tag>
  script:
  	# 下载依赖
    - yarn
    # 编译打包
    - npm run build --qa
    # 压缩dist目录
    - tar -czvf dist.tar.gz dist/
  # 定义artifacts,会上传到gitlab-server
  artifacts:
    paths:
    - dist.tar.gz
java镜像部分定义
java_build_job:
  image: $HARBOR_URL/tools/java:1.8
  stage: package
  only:
    refs:
      - dev
  # services定义,使用dind模式,其实就是通过link指令把docker容器链接到java镜像,使得java镜像可以使用docker命令
  services:
    - $HARBOR_URL/tools/docker-dind:18.09.7
  variables:
    DOCKER_DRIVER: overlay
    DOCKER_HOST: tcp://localhost:2375
  tags:
    - <runner_tag>
  script:
    # 重新使得JAVA_HOME、M2_HOME环境变量生效
    - source /etc/profile
    - mvn -P$NAMESPACE clean package
    - cd <module_dir>
    - mvn -P$NAMESPACE docker:build docker:push

配置deployment.yaml

deployment.yaml文件中包含2部分,k8s的deployment对象和service对象。

apiVersion: apps/v1beta2
kind: Deployment
metadata:
  labels:
    app: APP_NAME
  name: APP_NAME
  namespace: NAMESPACE
spec:
  progressDeadlineSeconds: 600
  replicas: 1
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      app: APP_NAME
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: APP_NAME
    spec:
      affinity: {}
      containers:
        - image: 'IMAGE_NAME:IMAGE_TAG'
          imagePullPolicy: Always
          name: APP_NAME
          # 前端项目,使用的是nginx基础镜像,一般使用内存都比较低
          resources:
            limits:
              cpu: '1'
              memory: 64Mi
            requests:
              cpu: '0'
              memory: 32Mi
          terminationMessagePath: /dev/termination-log
          terminationMessagePolicy: File
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      terminationGracePeriodSeconds: 30

apiVersion: v1 kind: Service metadata: name: APP_NAME namespace: NAMESPACE spec: ports: - port: APP_PORT protocol: TCP targetPort: APP_PORT selector: app: APP_NAME sessionAffinity: None type: ClusterIP

结束

至此,一个完整的基于gitlab的CICD流程可以跑起来了。因为是配合k8s运行的,在整个搭建的过程还是坎坷。例如使用javaj镜像但是需要运行docker命令,services那块的定义如果不去看文档就稀里糊涂的;然后前端的yarnmvn install等命令都会涉及到从公网下载依赖包,这些依赖包如何缓存才能使得下一次构建可以直接使用,这个就涉及到k8s的PV和PVC的相关概念和使用。

另外,对于.gitlab-ci.yml的变量还是有写死的内容,例如namespace,还需要另外一个脚本根据打包命令来替换对应的变量。还有待优化。

这个整套流程跑下来,感觉又学到了一些东西。


一个吃饱了努力减肥的小胖子。。。

本文由【暗音灬】发布于开源中国,原文链接:https://my.oschina.net/u/2303182/blog/3072694

全部评论: 0

    我有话说: