基于jenkins构建微服务发布平台
2700字约9分钟
2024-09-29
发布流程设计

准备基础环境:Harbor、Gitlab、Jenkins
Harbor镜像仓库
项目地址:https://github.com/goharbor/harbor
1.安装docker与docker-compose
2.解压离线包部署
# tar zxvf harbor-offline-installer-v2.0.0.tgz
# cd harbor
# cp harbor.yml.tmpl harbor.yml
# vi harbor.yml
hostname: 192.168.0.14
https:   # 先注释https相关配置
harbor_admin_password: Harbor12345
# ./prepare
# ./install.sh --with-chartmuseum
# docker-compose ps- 在Jenkins主机配置Docker可信任,如果是HTTPS需要拷贝证书
由于habor未配置https,还需要在docker配置可信任。
# cat /etc/docker/daemon.json 
{"registry-mirrors": ["https://b9pmyelo.mirror.aliyuncs.com"],
  "insecure-registries": ["192.168.0.12"]
}
# systemctl restart dockerGitlab代码仓库
在Gitlab创建一个项目,然后提交微服务项目代码。
如果没有Gitlab可以使用Docker启动一个:
mkdir /opt/gitlab 
GITLAB_HOME=/opt/gitlab # 数据持久化目录
docker run --detach \
--publish 443:443 \
--publish 88:80 \
--publish 2222:22 \
--name gitlab \
--restart always \
--volume $GITLAB_HOME/config:/etc/gitlab \
--volume $GITLAB_HOME/logs:/var/log/gitlab \
--volume $GITLAB_HOME/data:/var/opt/gitlab \
gitlab/gitlab-ce:latest访问地址:http://IP:88
初次会先设置管理员密码 ,然后登陆,默认管理员用户名 root,密码就是刚设置的。

Jenkins 发布系统

Jenkins是一款开源 CI&CD 系统,用于自动化各种任务,包括构建、测试和部署。
Jenkins官方提供了镜像:https://hub.docker.com/r/jenkins/jenkins
使用Deployment来部署这个镜像,会暴露两个端口:8080 Web访问端口,50000 Slave通 信端口,容器启动后Jenkins数据存储在/var/jenkins_home目录,所以需要将该目录使用 PV持久化存储。
配置PV持久化存储:
1、部署NFS共享服务器
#安装nfs安装包(每个k8s节点都要安装)
yum install nfs-utils2、找一个节点作为NFS共享存储服务器
#创建nfs共享目录
mkdir -p /ifs/kubernetes/jenkins-data
#修改nfs配置文件
vim /etc/exports
/ifs/kubernetes 192.168.0.0/24(rw,no_root_squash)
#启动nfs并加入开机自启
systemctl start nfs
systemctl enable nfs
#在别的节点验证是否能挂载成功
mount -t nfs 192.168.0.13:/ifs/kubernetes /mnt/
umount /mnt/3、为Jenkins准备PV
#vi pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv0001
spec:
  capacity:
    storage: 5Gi
  accessModes: ["ReadWriteOnce"]
  nfs:
    path: /ifs/kubernetes/jenkins-data
    server: 192.168.0.13
    
#kubectl apply -f pv.yaml在k8s中部署jenkins
kubectl apply -f jenkins.yml先安装后面所需的插件:
Jenkins下载插件默认服务器在国外,会比较慢,建议修改国内源:
 # 进入到nfs共享目录
cd /ifs/kubernetes/jenkins-data
sed -i 's/https:\/\/updates.jenkins.io\/download/https:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g' default.json 
sed -i 's/http:\/\/www.google.com/https:\/\/www.baidu.com/g' default.json
# 删除pod重建,pod名称改成你实际的
kubectl delete pod jenkins-d58f4db66-9cthj -n ops管理Jenkins->系统配置-->管理插件-->分别搜索Git Parameter/Git/Pipeline/kubernetes/Config File Provider, 选中点击安装。
- Git:拉取代码 
- Git Parameter:Git参数化构建 
- Pipeline:流水线 
- kubernetes:连接Kubernetes动态创建Slave代理 
- Config File Provider:存储配置文件 
- Extended Choice Parameter:扩展选择框参数,支持多选 
Jenkins在K8s中动态创建代理
Jenkins主从架构介绍

Jenkins Master/Slave架构,Master(Jenkins本身)提供Web页面 让用户来管理项目和从节点(Slave),项目任务可以运行在Master 本机或者分配到从节点运行,一个Master可以关联多个Slave,这样 好处是可以让Slave分担Master工作压力和隔离构建环境。

当触发Jenkins任务时,Jenkins会调用Kubernetes API 创建Slave Pod,Pod启动后会连接Jenkins,接受任务 并处理。
Kubernetes插件配置
Kubernetes插件:用于Jenkins在Kubernetes集群中运行动态代理
插件介绍:https://github.com/jenkinsci/kubernetes-plugin
配置插件:管理Jenkins->管理Nodes和云->管理云->添加Kubernetes

自定义Jenkins Slave镜像

构建salve镜像
unzip jenkins-slave.zip 
cd jenkins-slave/课件目录里涉及六个文件:
- Dockerfile:构建镜像 
- jenkins-slave:shell脚本启动slave.jar,下载地址:https://github.com/jenkinsci/docker-jnlpslave/blob/master/jenkins-slave 
- settings.xml:修改maven官方源为阿里云源 
- slave.jar:agent程序,接受master下发的任务,下载地址:http://jenkinsip:port/jnlpJars/slave.jar 
- helm和kubectl客户端工具 
构建并推送到镜像仓库:
docker build -t 192.168.0.14/library/jenkins-slave-jdk:1.8 .
docker push 192.168.0.14/library/jenkins-slave-jdk:1.8
测试主从架构是否正常
新建项目->流水线->Pipeline脚本(可生成示例)

pipeline {
    agent {
        kubernetes {
            label "jenkins-slave"
            yaml '''
apiVersion: v1
kind: Pod
metadata:
  name: jenkins-slave
spec:
  containers:
  - name: jnlp
    image: "192.168.0.14/library/jenkins-slave-jdk:1.8"
'''
        }
    }
    stages {
        stage('Main') {
            steps {
                sh 'hostname'
            }
        }
    }
}Jenkins Pipeline流水线
Jenkins Pipeline 介绍
Jenkins Pipeline是一套运行工作流框架,将原本独立运行单个或者多个节点的任 务链接起来,实现单个任务难以完成的复杂流程编排和可视化。
- Jenkins Pipeline是一套插件,支持在Jenkins中实现持续集成和持续交付; 
- Pipeline通过特定语法对简单到复杂的传输管道进行建模; 
- Jenkins Pipeline的定义被写入一个文本文件,称为Jenkinsfile。 

Jenkins Pipeline 语法

Jenkins Pipeline 示例
- Stages 是 Pipeline 中最主要的组成部分,Jenkins 将会按照 Stages 中描述的顺序 从上往下的执行。 
- Stage:阶段,一个 Pipeline 可以划分为若干个 Stage,每个 Stage 代表一组操作, 比如:Build、Test、Deploy 
- Steps:步骤,Steps 是最基本的操作单元,可以是打印一句话,也可以是构建一 个 Docker 镜像,由各类 Jenkins 插件提供,比如命令:sh ‘mvn',就相当于我 们平时 shell 终端中执行 mvn命令一样。 

流水线自动发布微服务项目
发布需求
在将微服务项目自动化部署到K8s平台会有这些需求:
- 尽量完全自动化部署,无需过多人工干预 
- 可以选择升级某个、某些微服务 
- 在部署、升级微服务时,可对微服务某些特性做配置,例如命名 空间、副本数量 


实现思路
Pipeline编写思路:
在微服务架构中,会涉及几个、几十个微服务,如果每个服务都创建一个item,势必 给运维维护成本增加很大,因此需要编写一个通用Pipeline脚本,将这些微服务部署 差异化部分使用Jenkins参数化,人工交互确认发布的微服务、环境配置等。 但这只是解决用户交互层面,在K8s实际部署项目用YAML创建对应资源,现在问题是 如何接收用户交互参数,自动化生成YAML文件,这就会用到Helm完成YAML文件高 效复用和微服务部署。
部署一个微服务项目,每个微服务的差异化部分在哪里?
- 服务名称 
- 代码版本 
- 镜像 
- 端口 
- 副本数 
- 标签 
- 域名 


编写Pipeline流水线脚本
对于课件中的Pipeline脚本,重点修改这几个变量:

1、将harbor认证和gitlab认证保存到Jenkins凭据
管理Jenkins->安全-->管理凭据->Jnekins->添加凭据->Username with password
分别添加连接gitlab和harbor的用户名到Jenkins凭据,然后获取该凭据ID替换到脚本中docker_registry_auth和git_auth变量的值。

2、将kubeconfig存储在Jenkins,用于slave镜像里kubectl连接k8s集群
管理Jenkins-> Managed files->Add->Custom file ->Content字段内容是kubeconfig(kubeadm部署k8s默认路径在master节点 /root/.kube/config,如果你是二进制部署,需要自己生成,参考下面),然后复制ID替换上述脚本中k8s_auth变量的值。
说明:将kubectl、helm工具封装到Slave镜像中,并通过Config File Provider插件存储连接K8s集群的kubeconfig认证文件,然后挂载到 Slave容器中,这样就能用kubectl apply deploy.yaml --kubeconfig=config管理K8s应用了,为提高安全性,kubeconfig文件可分配权限。

3.上传 helm chart包到镜像仓库

创建命名空间并部署eureka和MySQL服务(记得还要部署ingress控制器)
kubectl create namespace ms
kubectl apply -f mysql.yaml
#启动部署(修改eureka-service微服务yaml文件中的requests,请求资源设置小一点0.2)
./docker_build.sh eureka-service
kubectl apply -f ingress-controller.yamljenkinsfile
#!/usr/bin/env groovy
// 所需插件: Git Parameter/Git/Pipeline/Config File Provider/kubernetes/Extended Choice Parameter
// 公共
def registry = "192.168.0.14"
// 项目
def operation = "microservice"
def git_url = "http://192.168.0.14:88/root/k8s-microservice.git"
def gateway_domain_name = "gateway.ctnrs.com"
def portal_domain_name = "portal.ctnrs.com"
// 认证
def image_pull_secret = "registry-pull-secret"
def harbor_auth = "dc8877f5-3238-407f-a9fb-96932501a9b0"
def git_auth = "8d022667-0d69-4e5c-a6f8-6d0ac532f596"
// ConfigFileProvider ID
def k8s_auth = "cf3c93bc-dc97-4305-8379-53a669a2b2b7"
pipeline {
  agent {
    kubernetes {
        label "jenkins-slave"
        yaml """
apiVersion: v1
kind: Pod
metadata:
  name: jenkins-slave
spec:
  containers:
  - name: jnlp
    image: "${registry}/library/jenkins-slave-jdk:1.8"
    imagePullPolicy: Always
    volumeMounts:
      - name: docker-cmd
        mountPath: /usr/bin/docker
      - name: docker-sock
        mountPath: /var/run/docker.sock
      - name: maven-cache
        mountPath: /root/.m2
  volumes:
    - name: docker-cmd
      hostPath:
        path: /usr/bin/docker
    - name: docker-sock
      hostPath:
        path: /var/run/docker.sock
    - name: maven-cache
      hostPath:
        path: /tmp/m2
"""
        }
      
      }
    parameters {
        gitParameter branch: '', branchFilter: '.*', defaultValue: 'origin/master', description: '选择发布的分支', name: 'Branch', quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE', tagFilter: '*', type: 'PT_BRANCH'        
        extendedChoice defaultValue: 'none', description: '选择发布的微服务', \
          multiSelectDelimiter: ',', name: 'Service', type: 'PT_CHECKBOX', \
          value: 'gateway-service:9999,portal-service:8080,product-service:8010,order-service:8020,stock-service:8030'
        choice (choices: ['ms', 'demo'], description: '部署模板', name: 'Template')
        choice (choices: ['1', '3', '5', '7'], description: '副本数', name: 'ReplicaCount')
        choice (choices: ['ms'], description: '命名空间', name: 'Namespace')
    }
    stages {
        stage('拉取代码'){
            steps {
                checkout([$class: 'GitSCM', 
                branches: [[name: "${params.Branch}"]], 
                extensions: [], 
                userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]]
                ])
            }
        }
        stage('代码编译') {
            // 编译指定服务
            steps {
                sh """
                  mvn clean package -Dmaven.test.skip=true
                """
            }
        }
        stage('构建镜像') {
          steps {
              withCredentials([usernamePassword(credentialsId: "${harbor_auth}", passwordVariable: 'password', usernameVariable: 'username')]) {
                sh """
                 docker login -u ${username} -p '${password}' ${registry}
                 for service in \$(echo ${Service} |sed 's/,/ /g'); do
                    service_name=\${service%:*}
                    image_name=${registry}/${operation}/\${service_name}:${BUILD_NUMBER}
                    cd \${service_name}
                    if ls |grep biz &>/dev/null; then
                        cd \${service_name}-biz
                    fi
                    docker build -t \${image_name} .
                    docker push \${image_name}
                    cd ${WORKSPACE}
                  done
                """
                configFileProvider([configFile(fileId: "${k8s_auth}", targetLocation: "admin.kubeconfig")]){
                    sh """
                    # 添加镜像拉取认证
                    kubectl create secret docker-registry ${image_pull_secret} --docker-username=${username} --docker-password=${password} --docker-server=${registry} -n ${Namespace} --kubeconfig admin.kubeconfig |true
                    # 添加私有chart仓库
                    helm repo add  --username ${username} --password ${password} myrepo http://${registry}/chartrepo/${operation}
                    """
                }
              }
          }
        }
        stage('Helm部署到K8S') {
          steps {
              sh """
              common_args="-n ${Namespace} --kubeconfig admin.kubeconfig"
              
              for service in  \$(echo ${Service} |sed 's/,/ /g'); do
                service_name=\${service%:*}
                service_port=\${service#*:}
                image=${registry}/${operation}/\${service_name}
                tag=${BUILD_NUMBER}
                helm_args="\${service_name} --set image.repository=\${image} --set image.tag=\${tag} --set replicaCount=${replicaCount} --set imagePullSecrets[0].name=${image_pull_secret} --set service.targetPort=\${service_port} myrepo/${Template}"
                # 判断是否为新部署
                if helm history \${service_name} \${common_args} &>/dev/null;then
                  action=upgrade
                else
                  action=install
                fi
                # 针对服务启用ingress
                if [ \${service_name} == "gateway-service" ]; then
                  helm \${action} \${helm_args} \
                  --set ingress.enabled=true \
                  --set ingress.host=${gateway_domain_name} \
                   \${common_args}
                elif [ \${service_name} == "portal-service" ]; then
                  helm \${action} \${helm_args} \
                  --set ingress.enabled=true \
                  --set ingress.host=${portal_domain_name} \
                   \${common_args}
                else
                  helm \${action} \${helm_args} \${common_args}
                fi
              done
              # 查看Pod状态
              sleep 10
              kubectl get pods \${common_args}
              """
          }
        }
    }
}最终效果图:

流水线脚本与源代码一起版本管理
Jenkinsfile文件建议与源代码一起版本管理,实现流水线即 代码(Pipeline as Code)。
这样做的好处:
- 自动为所有分支创建流水线脚本 
- 方便流水线代码复查、追踪、迭代 
- 可被项目成员查看和编辑 


小结
使用Jenkins的插件
- Git & gitParameter 
- Kubernetes 
- Pipeline 
- Config File Provider 
- Extended Choice Parameter 
CI/CD环境特点
- Slave弹性伸缩 
- 基于镜像隔离构建环境 
- 流水线发布,易维护 
Jenkins参数化构建可帮助你完成更复杂环境CI/CD
回滚思路:
1.使用kubectl rollout ,将资源名称和命名空间等参数化传入
2.每次发布记录发布的镜像版本写到一个历史文件中,Extended choice Parameter从历史文件中作为选择项
