最近在改进公司的研发流程,发现数据库脚本这块确实头疼,虽然与代码一样在gitlab上管理,但是数据库的运行状态确实失控的。应用版本的回退总是收到数据库结构的限制,不能按需回退。

        在github上发现flyway这个开源项目可以对数据库的脚本的变更进行管理,并且记录变化,动态执行DDL,实现脚本状态与数据库结构联动。Flyway的具体使用方法参见github的官方说明:https://github.com/flyway/flyway        

kubesphere 流水线引用凭证 流水线数据库_jenkins

        突然想到可以将Flyway纳入到pipeline中,通过与代码的CI/CD流水线关联,岂不是可以实现数据库与应用版本的同步更新。

        实现原理:

kubesphere 流水线引用凭证 流水线数据库_kubesphere 流水线引用凭证_02

一、环境准备

  • 代码托管:GitLab
  • 流水线工具:Jenkins
  • 镜像构建:docker
  • 数据库:mysql 5.7
  • 容器运行平台:K8S 1.20.1(在k8s中运行jenkins代理服务,用于运行flyway的容器。非必须,也可以直接在jenkins服务器上运行)

二、数据准备

1、创建数据库脚本仓库

        在gitlab中创建数据库脚本的仓库“db_script”,其目录结构只有一个“sql”目录,用于存放不通阶段的sql脚本,如下: 

kubesphere 流水线引用凭证 流水线数据库_docker_03

2、创建流水线脚本

2.1  在gitlab中创建jenkinsfile的仓库“jenkinsfile”,用来存放执行的pipeline脚本。如下:

kubesphere 流水线引用凭证 流水线数据库_Jenkins_04

2.2  编写Jenkinsfile文件,这是pipeline脚本,这里启用的是jenkins master/salve模式的声明式流水线,savle在k8s集群中运行。里面jenkins的Credentials请替换为自己环境的,k8s环境也替换为自己环境的。

这段流水线脚本中,在k8s容器中启动了一个pod,其中运行了4个容器,一个是jnlp用于和jenkins master节点通信;一个是migrate,用于执行sql脚本;其他两个是docker构建和kubectl操作的,这里没有用,其他项目使用到,没有删除。

pipeline {
  agent {
    kubernetes {
      cloud 'kubernetes-default'
      slaveConnectTimeout 120
      yaml '''
apiVersion: v1
kind: Pod
spec:
  containers:
    - args: [\'$(JENKINS_SECRET)\', \'$(JENKINS_NAME)\']
      image: \'registry.cn-hangzhou.aliyuncs.com/luanyang_citools/jnlp:alpine\'
      name: jnlp
      imagePullPolicy: IfNotPresent
      volumeMounts:
        - mountPath: "/etc/localtime"
          name: "volume-2"
          readOnly: false
        - mountPath: "/etc/hosts"
          name: "volume-hosts"
          readOnly: false        
    - command:
        - "cat"
      env:
        - name: "LANGUAGE"
          value: "en_US:en"
        - name: "LC_ALL"
          value: "en_US.UTF-8"
        - name: "LANG"
          value: "en_US.UTF-8"
      image: "registry.cn-hangzhou.aliyuncs.com/luanyang_citools/flyway:8.0.5-alpine-su"
      imagePullPolicy: "IfNotPresent"
      name: "migrate"
      tty: true
      volumeMounts:
        - mountPath: "/etc/localtime"
          name: "volume-2"
          readOnly: false
        - mountPath: "/etc/hosts"
          name: "volume-hosts"
          readOnly: false
    - command:
        - "cat"
      env:
        - name: "LANGUAGE"
          value: "en_US:en"
        - name: "LC_ALL"
          value: "en_US.UTF-8"
        - name: "LANG"
          value: "en_US.UTF-8"
      image: "registry.cn-hangzhou.aliyuncs.com/luanyang_citools/kubectl:self-1.17"
      imagePullPolicy: "IfNotPresent"
      name: "kubectl"
      tty: true
      volumeMounts:
        - mountPath: "/etc/localtime"
          name: "volume-2"
          readOnly: false
        - mountPath: "/var/run/docker.sock"
          name: "volume-docker"
          readOnly: false
        - mountPath: "/mnt/.kube/"
          name: "volume-kubeconfig"
          readOnly: false
        - mountPath: "/etc/hosts"
          name: "volume-hosts"
          readOnly: false
    - command:
        - "cat"
      env:
        - name: "LANGUAGE"
          value: "en_US:en"
        - name: "LC_ALL"
          value: "en_US.UTF-8"
        - name: "LANG"
          value: "en_US.UTF-8"
      image: "registry.cn-hangzhou.aliyuncs.com/luanyang_citools/docker:19.03.9-git"
      imagePullPolicy: "IfNotPresent"
      name: "docker"
      tty: true
      volumeMounts:
        - mountPath: "/etc/localtime"
          name: "volume-2"
          readOnly: false
        - mountPath: "/var/run/docker.sock"
          name: "volume-docker"
          readOnly: false
        - mountPath: "/etc/hosts"
          name: "volume-hosts"
          readOnly: false
          value: "en_US.UTF-8"      
  restartPolicy: "Never"
  nodeSelector:
    build: "true"
  securityContext: {}
  volumes:
    - hostPath:
        path: "/var/run/docker.sock"
      name: "volume-docker"
    - hostPath:
        path: "/usr/share/zoneinfo/Asia/Shanghai"
      name: "volume-2"
    - hostPath:
        path: "/etc/hosts"
      name: "volume-hosts"
    - name: volume-maven-repo
      persistentVolumeClaim:
        claimName: dockercicd-data
    - name: "volume-kubeconfig"
      secret:
        secretName: "multi-kube-config"
'''
    }

  }
  stages {
    stage('pulling Code') {
      parallel {
        stage('pulling Code') {
          when {
            expression {
              env.gitlabBranch == null
            }

          }
          steps {
            git(branch: "${BRANCH}", credentialsId: 'c642514f-e10f-477c-a066-3711f9fe44d7', url: "${REPO_URL}")
          }
        }

        stage('pulling Code by trigger') {
          when {
            expression {
              env.gitlabBranch != null
            }

          }
          steps {
            git(url: "${REPO_URL}", branch: env.gitlabBranch, credentialsId: 'c642514f-e10f-477c-a066-3711f9fe44d7')
          }
        }

      }
    }

    stage('migrate-dbconfig') {
      steps {
        withCredentials(bindings: [usernamePassword(credentialsId: 'DATABASE_USER', passwordVariable: 'Password', usernameVariable: 'Username')]){
          container(name: 'migrate') {
                sh """                          
                            flyway -user=${Username} -password=${Password} -url="${DBURL}" -connectRetries=60 migrate                            
                  """
          }
        }
      }
    }


  }

}

3、配置Jenkins的基础环境

3.1 需要安装的插件,详见列表。插件安装请自行百度,这里不在详述。

  • Hidden Parameter
  • List Git Branches (and more)
  • String Parameter
  • kubernetes

3.2 创建Jenkins的管理集群,“Manage Jenkins”—>“Configure System”—>“Cloud”(最下面)—>“Configure Clouds”。

3.2.1 集群命名,此处命名与pipeline流水线中集群名称一致。

kubesphere 流水线引用凭证 流水线数据库_docker_05

 3.2.2 编辑详情

        如果jenkins与应用在同一个k8s集群中,只要配置“kubernetes地址”与“Jenkins地址”两个参数即可。 

kubesphere 流水线引用凭证 流水线数据库_git_06

         如果jenkins与应用不在在同一个k8s集群中,或者要管理多个k8s集群,则还需要完善“Kubernetes server certificate key(服务证书)”、“Credentials(凭据)”两个参数。两个参数的配置详见博客Jenkins基于https的k8s配置

3.2.3 添加凭据

  • 登录gitlab服务器的凭据(类型:SSH Username with private key)

kubesphere 流水线引用凭证 流水线数据库_Jenkins_07

  • 登录数据库服务器的凭据(类型:Username with password)

kubesphere 流水线引用凭证 流水线数据库_Jenkins_08

三、流水线任务

1、制作镜像

        经过测试,DockerHub上的官方镜像使用的flyway用户运行command,在流水线启动容器的时候执行sh脚本,会卡主不动,替换为root用户,则可以正常执行sh。

1.1 修改Dockerfile

        那么就不能直接使用flyway/flyway:8.0.0-alpine镜像,从github上下载https://github.com/flyway/flyway-docker,修改“flyway-docker/alpine/Dockerfile”,去掉添加用户部分:

FROM eclipse-temurin:11-alpine

RUN apk --no-cache add --update bash openssl

# Add the flyway user and step in the directory
# RUN addgroup flyway \  #注释掉的
#    && adduser -S -h /flyway -D -G flyway flyway #注释掉的

RUN mkdir /flyway #新增的
WORKDIR /flyway

# Change to the flyway user
# USER flyway #注释掉的

ENV FLYWAY_VERSION 8.0.5

RUN wget https://repo1.maven.org/maven2/org/flywaydb/flyway-commandline/${FLYWAY_VERSION}/flyway-commandline-${FLYWAY_VERSION}.tar.gz \
  && tar -xzf flyway-commandline-${FLYWAY_VERSION}.tar.gz \
  && mv flyway-${FLYWAY_VERSION}/* . \
  && rm flyway-commandline-${FLYWAY_VERSION}.tar.gz

ENV PATH="/flyway:${PATH}"

ENTRYPOINT ["flyway"]
CMD ["-?"]

 1.2 运行docker build:

docker build -t registry.cn-hangzhou.aliyuncs.com/<自己的命名空间>/flyway:8.0.5-alpine-su .

 编译完成:

Sending build context to Docker daemon  15.36kB
Step 1/9 : FROM eclipse-temurin:11-alpine
11-alpine: Pulling from library/eclipse-temurin
97518928ae5f: Already exists 
56981b1bb25b: Already exists 
b97623027cff: Already exists 
c6fc9c4c0237: Already exists 
Digest: sha256:7e9507850e2b4a4bd50151bcd63a3d32c8d2a6249c0cb3665c8fa9f8d4aba72e
Status: Downloaded newer image for eclipse-temurin:11-alpine
 ---> 7a2d72829277
Step 2/9 : RUN apk --no-cache add --update bash openssl
 ---> Running in 19b82c0f9d2a
fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/community/x86_64/APKINDEX.tar.gz
(1/5) Installing ncurses-terminfo-base (6.2_p20210612-r0)
(2/5) Installing ncurses-libs (6.2_p20210612-r0)
(3/5) Installing readline (8.1.0-r0)
(4/5) Installing bash (5.1.4-r0)
Executing bash-5.1.4-r0.post-install
(5/5) Installing openssl (1.1.1l-r0)
Executing busybox-1.33.1-r6.trigger
OK: 12 MiB in 22 packages
Removing intermediate container 19b82c0f9d2a
 ---> 3c511a5d3bef
Step 3/9 : RUN mkdir /flyway
 ---> Running in 1a7b0d6f0c19
Removing intermediate container 1a7b0d6f0c19
 ---> df6257bbb491
Step 4/9 : WORKDIR /flyway
 ---> Running in 7b3945b16420
Removing intermediate container 7b3945b16420
 ---> 4d81195e7589
Step 5/9 : ENV FLYWAY_VERSION 8.0.5
 ---> Running in 5f2186eb599b
Removing intermediate container 5f2186eb599b
 ---> ba92f02ab9e8
Step 6/9 : RUN wget https://repo1.maven.org/maven2/org/flywaydb/flyway-commandline/${FLYWAY_VERSION}/flyway-commandline-${FLYWAY_VERSION}.tar.gz   && tar -xzf flyway-commandline-${FLYWAY_VERSION}.tar.gz   && mv flyway-${FLYWAY_VERSION}/* .   && rm flyway-commandline-${FLYWAY_VERSION}.tar.gz
 ---> Running in c22a1df47872
Connecting to repo1.maven.org (151.101.24.209:443)
saving to 'flyway-commandline-8.0.5.tar.gz'
flyway-commandline-8   0% |                                |  176k  0:07:45 ETA
flyway-commandline-8   1% |                                |  882k  0:03:04 ETA
flyway-commandline-8   3% |                                | 2494k  0:01:35 ETA
flyway-commandline-8   6% |**                              | 5182k  0:00:59 ETA
flyway-commandline-8  14% |****                            | 11.5M  0:00:29 ETA
flyway-commandline-8  14% |****                            | 11.5M  0:00:35 ETA
flyway-commandline-8  24% |*******                         | 19.3M  0:00:22 ETA
flyway-commandline-8  32% |**********                      | 26.3M  0:00:16 ETA
flyway-commandline-8  39% |************                    | 32.1M  0:00:13 ETA
flyway-commandline-8  43% |**************                  | 35.2M  0:00:12 ETA
flyway-commandline-8  49% |***************                 | 40.0M  0:00:11 ETA
flyway-commandline-8  57% |******************              | 46.2M  0:00:08 ETA
flyway-commandline-8  65% |********************            | 52.2M  0:00:06 ETA
flyway-commandline-8  73% |***********************         | 58.8M  0:00:05 ETA
flyway-commandline-8  74% |***********************         | 60.0M  0:00:05 ETA
flyway-commandline-8  79% |*************************       | 64.1M  0:00:04 ETA
flyway-commandline-8  89% |****************************    | 72.2M  0:00:01 ETA
flyway-commandline-8  99% |******************************* | 79.8M  0:00:00 ETA
flyway-commandline-8 100% |********************************| 80.3M  0:00:00 ETA
'flyway-commandline-8.0.5.tar.gz' saved
Removing intermediate container c22a1df47872
 ---> 2a48fd38d034
Step 7/9 : ENV PATH="/flyway:${PATH}"
 ---> Running in 220c1f982e72
Removing intermediate container 220c1f982e72
 ---> eb460c347546
Step 8/9 : ENTRYPOINT ["flyway"]
 ---> Running in f725e59d8465
Removing intermediate container f725e59d8465
 ---> 20d54593b3ae
Step 9/9 : CMD ["-?"]
 ---> Running in bd26e146fdc4
Removing intermediate container bd26e146fdc4
 ---> c2ec13a36110
Successfully built c2ec13a36110

查询制作好的镜像:

kubesphere 流水线引用凭证 流水线数据库_git_09

 1.3 推送到自己的镜像仓库:

docker push registry.cn-hangzhou.aliyuncs.com/<自己的命名空间>/flyway:8.0.5-alpine-su

 推送成功:

The push refers to repository [registry.cn-hangzhou.aliyuncs.com/<自己的命名空间>/flyway]
e413cc4bca80: Pushed 
492e18b869df: Pushed 
c9a8fbe9621f: Pushed 
c3fb54c0d681: Layer already exists 
b3120221672a: Layer already exists 
d4c9267a80dd: Layer already exists 
1a058d5342cc: Layer already exists 
8.0.5-alpine-su: digest: sha256:95881430e69aacac6ef62267134080a4f9242e3e50f87d7f5d782e50189658c2 size: 1788

2、创建jenkins的任务

 2.1 创建jenkins的pipeline任务

kubesphere 流水线引用凭证 流水线数据库_Jenkins_10

 2.2 编辑参数

2.2.1 添加“List Git Branch (and more)”

        添加该参数用于获取sql的分支或者tag(按照业务需求获取),这里演示的按照分支获取sql脚本文件分支号。

kubesphere 流水线引用凭证 流水线数据库_jenkins_11

 2.2.2 添加 git仓库地址配置

        添加该参数用于获取git仓库地址。

kubesphere 流水线引用凭证 流水线数据库_git_12

2.2.3 添加数据库连接配置

kubesphere 流水线引用凭证 流水线数据库_jenkins_13

2.2.4 pipeline脚本编辑

        因为之前已经在gitlab上托管了pipeline脚本,所以这里不在jenkins中编写脚本,而是直接从gitlab上下载。

kubesphere 流水线引用凭证 流水线数据库_kubesphere 流水线引用凭证_14

  

3、编辑脚本测试

3.1 准备sql

3.1.1 sql/V1.0.0__init.sql

CREATE TABLE MyTable (
    MyColumn VARCHAR(100) NOT NULL
);

3.1.2 sql/V1.1.0__addTables.sql

CREATE TABLE students (
    id  VARCHAR(100) NOT NULL,
    name VARCHAR(100) NOT NULL,
    sex  char(1) NOT NULL,
    address VARCHAR(500)
);

 3.1.3 sql/V1.2.0__addTables.sql

alter table students MODIFY COLUMN sex varchar(10) NOT NULL;

insert into students values('1','zhangsan','male','sdasdada');
insert into students values('2','lisi','fmale','222222222222');

insert into MyTable values('testTable');

3.2 准备数据库

        创建数据库连接中对应的数据库。

kubesphere 流水线引用凭证 流水线数据库_docker_15

3.3 运行流水线

3.3.1 执行流水线

kubesphere 流水线引用凭证 流水线数据库_Jenkins_16

3.3.2 执行结果

kubesphere 流水线引用凭证 流水线数据库_docker_17

kubesphere 流水线引用凭证 流水线数据库_jenkins_18

kubesphere 流水线引用凭证 流水线数据库_kubesphere 流水线引用凭证_19