K8S基于Jenkins的自动化部署

K8S基于Jenkins的自动化部署

杰子学编程 165 2022-06-02

K8S基于Jenkins的自动化部署

问题

问题分为两种:

方法论的问题:比如团队采用主干开发,主干发布的模式,但是质量得不到保证,这时通过分析讨论决定采用采用主干开发,分支发布的模式来解决,这属于从方法论层面解决问题。

落地执行的问题:已经知道应该采用主干开发,分支发布的模式,但在实际操作的时候,难以执行下去,这属于执行的问题。

在《不断进化的分支和需求管理》一文的最后提到会引入 release 分支和 tag,实际也这么做了,但效果并不理想,原因是执行的不严格,没有做到位,具体原因如下:

  • 发布时是对分支进行构建发布,发布后再在 GitLab 中打上 tag,一忙起来很容易忘记;
  • 镜像的版本也是如此。

解决思路

目的其实很简单,就是让代码的 tag 和镜像的 tag 能够一致,靠人工去做这些事情比想象的要更加困难,所以稍微转换了下思路就能实现自动化,也就可以解决这个问题。

  • 之前提到的 release 分支只做最终的集成测试;

  • 需要发布时就从 release 分支创建 tag,对 tag 来做发布,通过脚本自动创建镜像 tag 进行 push 。

    实际操作

    原来在 jenkins 中对分支进行发布,需要设置特定的分支,现在需要对 tag 进行发布,tag 是不断进行创建的,就需要用到 jenkisn 的参数化功能。

    jenkins 的参数化需要用到 Git Parameter 插件,可以在 jenkins 的插件管理界面中直接安

    装,如果安装失败,可以在这个地址进行下载:http://mirror.xmission.com/jenkins/plugins/git-parameter/latest/,更多插件的使用说明参考官网:https://plugins.jenkins.io/git-parameter/

    具体配置步骤如下:

    安装Git Parameter

    安装Git Parameter

    然后在可选插件中搜索Git Parameter,我这里已经安装了,安装完成后选择自动重启Jenkins。

    Jenkins

    配置完成后,我们新建个发布任务,任务类型选择参数化构建过程

    参数化构建过程

然后在参数化构建过程中配置参数tag,可以添加相应的描述信息。

在源码配置中,由之前的master,调整成${tag};如图:

jenkins指定tag

这里指定分支,改为${tag};

返回我们的项目,可以看到由之前的立即构建变为Build With Parameters

Build With Parameters

按照 tag 进行构建搞定后,剩下就是需要在构建脚本中获取到最新的 tag 名称,并作为参数设置到容器的环境变量和镜像的 tag 中:

  • 首先进入到 jenkins 配置的程序目录,使用 git describe --abbrev=0 --tags 获取 tab 名称;

  • 前端容器使用环境变量的方式将 tag 名称传入,并最终在界面显示;

  • 容器镜像使用参数的方式拼接上 tag 名称。

    #!/bin/bash
    TAGNAME=`git describe --abbrev=0 --tags`
    echo "tag name is:" $TAGNAME
    
    HARBOR=harbor.juxingta.com
    SERVER=jdd-user-center
    DOCKER_TAG=$HARBOR/library/$SERVER:$TAGNAME
    cp -R /home/hongjie/jenkins/images/agent jdd-user-center/
    docker build -t $SERVER -f DockerFile .
    docker tag $SERVER $DOCKER_TAG
    docker push $DOCKER_TAG
    

我们点击Build with Parameters,需要选择gitlab代码中打的tag标签信息。然后点击开始构建。

构建完成后,可以将docker镜像推送到我们自己的Harbor仓库中。

K8S-CIDC

上面文章是基于普通方式,这里我们使用jenkins一键部署到k8s,构建系统的CIDC,这里主要使用jenkins的Pipeline方式进行构建,主要步骤分为以下步骤:

  • 拉取代码
  • build代码
  • 打包成镜像并推送至harbor
  • 发布deply

拉取代码

这里我们从代码仓库git获取代码,我们新建立一个流水线任务jdd-k8s-user,这里以user服务为例子进行讲解。

首先创建个任务

首先创建个任务

然后配置pull代码任务,这里需要注意的是,如果代码仓库是私有的需要配置下credentialsId

pull代码

配置完成后,我们验证下能否成功获取代码。

我们可以查看构建日志,也可以从阶段时图中查看相关信息状态。

build代码

pull代码后,我们Build代码,我们继续配置下一阶段的任务。我们继续完善构建脚本,新增一下信息后重新构建

stage('Maven Build') {
     sh 'mvn -am clean package'
}

可以看到,Maven Build构建成功,这里我们已经将代码打成了jar包,接下来我们构建docker镜像。

打包成镜像并推送至harbor

我们继续将jar包打成docker Image,这里我们编写一个build_image.sh的脚本,我们在jenkins中执行该脚本,进行镜像构建。

首先我们配置下jenkins,执行build_image.sh脚本。

stage('Build DockerImahe') {      
    sh '/var/jenkins/script/build-image.sh'
}

/var/jenkins/script/目录下创建build-image.sh脚本。脚本内容如下:

#!/bin/bash

# 判断构建目录是否存在
if ["${BUILD_DIR}" == ""];then
    echo "env 'BUILD_DIR' is not set"
    exit 1
fi

DOCKER_DIR=${BUILD_DIR}/${JOB_NAME}

# 获取agent
if [ ! -d ${AGENT_DIR}];then
   echo "env 'AGENT_DIR' is not set"
   exit 1
fi


# 如果docker目录不存在则创建

if [ ! -d ${DOCKER_DIR} ];then
    mkdir -p ${DOCKER_DIR}
fi

echo "docker workspace: ${DOCKER_DIR}"

# 获取编译模块
JENKINS_DIR=${WORKSPACE}/${MODULE}

echo "jenkins workspace: ${JENKINS_DIR}"
# p判断jar是否存在
if [ ! -f ${JENKINS_DIR}/target/${MODULE}.jar ];then
    echo "target jar file not found ${JENKINS_DIR}/target/${MODULE}.jar"
    exit 1
fi

cd ${DOCKER_DIR}
pwd
rm -fr *
cp ${AGENT_DIR}/* ${DOCKER_DIR}
cp ${JENKINS_DIR}/target/${MODULE}.jar .
mv ${JENKINS_DIR}/DockerFile .

VERSION=$(date +%Y%m%d%H%M%S)
IMAGE_NAME=harbor.juxingta.com/jdd/${JOB_NAME}:${VERSION}

# 将镜像名称写入本地文件,供下个脚本使用
echo "${IMAGE_NAME}" > ${WORKSPACE}/IMAGE

echo "docker build image name is : ${IMAGE_NAME}"
# 构建docker镜像
docker build -t ${IMAGE_NAME} -f DockerFile .
echo "docker build success !"
docker push ${IMAGE_NAME}
echo "docker push success !"

说明:脚本中涉及的环境变量如下:BUILDDIR{BUILD_DIR}、{AGENT_DIR}、${MODULE},以上环境变量为了构建脚本灵活,这里在构建pipline中进行配置,现在配置如下:

node("118.195.194.75-k8s"){# 这里指定构建的节点
    
        env.BUILD_DIR = "/var/jenkins/build-workspace"
        env.MODULE = "jdd-user-center"
        env.AGENT_DIR = "/var/jenkins/agent"
        # 过去代码
        stage('Pull') {
                // Get some code from a GitHub repository
                git credentialsId: 'xxxx', url: 'https://xxxx/jdd_platform.git'
        }
        # build代码
        stage('Maven Build') {
           
                // Get some code from a GitHub repository
                sh 'mvn -am clean package'
            
        }
       	# 构建docker镜像并推送到harbor
        stage('Build DockerImahe') {
           
                sh '/var/jenkins/script/build-image.sh'
            
        }
}

我们可以看到构建镜像已经成功了,我们去我们的镜像仓库中看下镜像是否已经推送到仓库中。

可以看到,镜像已经推送到镜像仓库中,黑暗即将过去,马上看到胜利的曙光。我们进行下一步操作,将镜像更新到k8s中。

发布deply

这里发布依然采用脚本方式进行发布,我们在/var/jenkins/script/目录下创建delpy.sh脚本。

我们安装自己的需求编写common.yaml文件,这里我安装自己的产品需求编写的,大家可以参考下

# deplay
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{name}}
spec:
  selector:
    matchLabels:
      app: {{name}}
  replicas: 2
  template:
    metadata:
      labels:
        app: {{name}}
    spec:
      imagePullSecrets:
        - name: jdd-harbor
      containers:
        - name: {{name}}
          imagePullPolicy: Always
          image: {{name}}
          resources:
            requests:
              memory: 2048Mi
              cpu: 1000m
            limits:
              memory: 2048Mi
              cpu: 1000m
          ports:
            - name: http-port
              containerPort: 9001
          env:
            - name: NACOS_HOST_URL
              value: "http://nacos-headless"
            - name: NACOS_HOST_PORT
              value: "8848"
          volumeMounts:
            - name: logback-config-volume
              mountPath: /etc/jdd
      volumes:
        - name: logback-config-volume
          configMap:
            name: {{logbackConfig}}
---
#service
apiVersion: v1
kind: Service
metadata:
  name: {{name}}
spec:
  ports:
    - port: 80
      protocol: TCP
      targetPort: 8080
  selector:
    app: {{name}}
  type: ClusterIP

# ConfigMap
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{logbackConfig}}
data:
  logback-spring.xml: |
    <?xml version="1.0" encoding="UTF-8"?>
    <configuration scan="true" scanPeriod="60 seconds" debug="false">

        <include resource="org/springframework/boot/logging/logback/defaults.xml"/>

        <property name="log_pattern" value="%-30thread %d{yyyyMMdd HH:mm:ss.SSS} %-5level traceId=[%X{traceId}] %logger{140} - %msg%n"/>

        <springProperty name="log_path" source="jdd.log.path" scope="context"/>
        <property name="app_name" value="{{appName}}"/>
        <property name="log_file" value="${log_path}/${app_name}"/>

        <contextName>${app_name}</contextName>

        <!--console控制台日志输出-->
        <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
            <layout class="ch.qos.logback.classic.PatternLayout">
                <pattern>${log_pattern}</pattern>
            </layout>
        </appender>

        <!--文件日志输出-->
        <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <fileNamePattern>${log_file}-%d{yyyyMMdd}.log</fileNamePattern>
                <maxHistory>7</maxHistory>
            </rollingPolicy>
            <encoder>
                <pattern>${log_pattern}</pattern>
            </encoder>
        </appender>

        <logger name="org.hibernate" level="INFO"/>
        <logger name="org.springframework" level="INFO"/>
        <logger name="com.netflix" level="WARN"/>
        <logger name="org.apache.http" level="INFO"/>
        <logger name="com.alibaba.nacos" level="INFO"/>
        <logger name="jdbc.connection" level="ERROR"/>
        <logger name="jdbc.resultset" level="ERROR"/>
        <logger name="jdbc.resultsettable" level="INFO"/>
        <logger name="jdbc.audit" level="ERROR"/>
        <logger name="jdbc.sqltiming" level="ERROR"/>
        <logger name="jdbc.sqlonly" level="INFO"/>

        <springProfile name="dev">
            <logger name="org.hibernate.SQL" level="DEBUG"/>
            <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/>
            <root level="debug">
                <appender-ref ref="console"/>
                <appender-ref ref="file"/>
            </root>
        </springProfile>

        <springProfile name="test">
            <logger name="org.hibernate.SQL" level="DEBUG"/>
            <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/>
            <root level="debug">
                <appender-ref ref="console"/>
            </root>
        </springProfile>

        <springProfile name="prod">
            <root level="error">
                <appender-ref ref="file"/>
                <appender-ref ref="console"/>
            </root>
        </springProfile>

    </configuration>

说明:这里名称采用{{name}}进行占位,{ { image } } 镜像名称、{ { app-name } } 日志应用名称、{ { logback-config } }日志配置文件名称。

我们将模版上传到/var/jenkins/script/template目录下,编写delpy.sh。内容如下

#!/bin/bash

name=${JOB_NAME}
image=$(cat ${WORKSPACE}/IMAGE)
logbackConfig="user-abc"
appName="user-server"

echo "deploying ... name : ${name}, image : ${image}"

rm -f common.yaml
cp $(dirname "${BASH_SOURCE[0]}")/template/common.yaml .

sed -i "s,{{name}},${name},g" common.yaml
sed -i "s,{{image}},${image},g" common.yaml
sed -i "s,{{logbackConfig}},${logbackConfig},g" common.yaml
sed -i "s,{{appName}},${appName},g" common.yaml

cat common.yaml

export KUBECONFIG=$KUBECONFIG:/root/.kube/cls-e1eu4py0-config # 切换上下文
kubectl config current-context # 打印当前上下文
kubectl apply -f common.yaml # 发布

我们看到整个构建已经成功了,我们去看下k8s集群的状态,

Service服务已经启动。至此我们的CICD构建基本上完成了,还差最后的一步就是健康检查,这里就先不进行健康检查了。这部分内容后续我们在完善。


# K8S # Jenkins # CICD # DevOps