kubernetes之CI/CD工具jenkins第二篇,helm的使用

1. kubernetes之CI/CD第二篇-jenkins结合helm部署应用:

1. 概述:

   在前期的博文中我已经初步介绍过kubernetes环境下的CI/CD的使用。主要是jenkins slave pod自动创建和销毁,当有jenkins job任务执行的时候,就会自动创建一个jenkins slave pod。在本篇博文中,我们将介绍jenkins生成slave pod的另外一种方法,就是在pipeline脚本里面定义slave pod的镜像等,同时将Dockerfile、Jenkinsfile、YAML清单文件全部放到gitlab上面,通过jenkins的插件Blue Ocean自动创建多任务的Job。主要的流程包括如下:

  1. Jenkins服务器安装Blue Ocean插件;
  2. 开发人员提交代码到 Gitlab 代码私有仓库;
  3. 通过 Gitlab 配置的 Jenkins Webhook 触发 Pipeline 自动构建;
  4. 配置Blue Ocean,添加git地址和认证信息,创建多分支流水线任务;
  5. Jenkins 触发构建构建任务,根据 Pipeline 脚本定义分步骤构建;
  6. 先进行代码静态分析,单元测试;
  7. 然后进行 Maven 构建(Java 项目);
  8. 根据构建结果构建 Docker 镜像;
  9. 推送 Docker 镜像到 test 仓库;
  10. 触发更新服务阶段,使用 Helm 安装/更新 Release;
  11. 查看服务是否更新成功。

2. 实施过程:

  2.1 安装插件Blue Ocean:

  选择系统管理---插件管理---Available---输入Blue Ocean搜素插件名称---选择安装插件并重启服务生效

  2.2 编写DockerFile:

  这里的DockerFile主要用途是产生应用的镜像,并上传到私有仓库。一般一个微服务模块就是一个Dockerfile,也就是一个独立的镜像;我这边演示的一个demo程序,有两个微服务模块,所以就需要两个Dockerfile,文件分别放置到微服务pom.xml打包文件的同级目录;

  这个是后端应用的Dockerfile

# Start with a base image containing Java runtime
FROM openjdk:8-jdk-alpine

# Add Maintainer Info
LABEL maintainer="[email protected]"

ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
ENV TZ=Asia/Shanghai
RUN mkdir /app
WORKDIR /app
COPY target/polls-0.0.1-SNAPSHOT.jar /app/polls.jar

# Make port 8080 available to the world outside this container
EXPOSE 8080

# Run the jar file
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app/polls.jar"]

  Dockerfile解释如下:
  1. 定义基础镜像,一般是alpine镜像;这种镜像比较小,此处使用jdk1.8的镜像;
  2. 定义环境变量,创建应用目录,拷贝maven编译之后的jar包到容器内的应用目录;
  3. 定义暴露端口,此处并不是真的暴露,只是描述;真正暴露的端口在yaml清单文件里面定义;
  4. 定义入口命令,也就是程序启动的命令;

  这个是前端应用的Dockerfile,前端应用是一个nginx服务器,将前端的html文件放在nginx根目录

FROM nginx:1.15.10-alpine
ADD build /usr/share/nginx/html
ADD nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
ENTRYPOINT ["nginx","-g","daemon off;"]

  Dockerfile解释如下:

  1. 定义基础镜像,一般是alpine镜像;这种镜像比较小;此处使用nginx镜像作为前端的web服务器;
  2. 添加网页html文件到nginx网页根目录;
  3. 定义暴露的端口,此处并不是真的暴露,只是描述;真正暴露的端口在yaml清单文件里面定义;
  4. 定义nginx的启动命令;

  nginx的配置文件如下:

server {
    gzip on;
    listen       80;
    server_name  localhost;

    root   /usr/share/nginx/html;
    location / {
        try_files $uri /index.html;
        expires 1h;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

  2.3 编写YAML清单文件:
  如果是用helm部署应用的话,实际上是可以不用定义Deployment、service等资源清单文件的。只需要配置好helm的value.yaml就可以。但是helm的chart一般是别人写好的,要自己针对应用写一个chart,一般要熟悉go语言模板,学习成本比较高;

  后端的服务k8s配置清单文件:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: polling-server
  namespace: sit
  labels:
    app: polling-server
spec:
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: polling-server
    spec:
      restartPolicy: Always
      imagePullSecrets:
        - name: myreg
      containers:
      - image: <IMAGE>:<IMAGE_TAG>
        name: polling-server
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8080
          name: api
        env:
        - name: DB_HOST
          value: my-mysql.kube-system
        - name: DB_PORT
          value: "3306"
        - name: DB_NAME
          value: polling_app
        - name: DB_USER
          value: polling
        - name: DB_PASSWORD
          value: polling321

---
kind: Service
apiVersion: v1
metadata:
  name: polling-server
  namespace: sit
spec:
  selector:
    app: polling-server
  type: ClusterIP
  ports:
  - name: api-port
    port: 8080
    targetPort: api

  yaml清单文件解释如下:

   1. 注意这里的镜像名称和镜像版本是变量的形式,是通过前面的Dockerfile build的镜像,每次的镜像版本不一样。

   2. 定义滚动更新策略,每次只能有一个pod副本不可用,每次只能升级一个pod副本;

   3. 定义镜像下载策略,这里是一直下载,也可以使用IfNotpresent,如果本地有镜像就不下载;

   4. 创建一个myreg的secrets,用于镜像认证的imagePullsecrets;

   5. 定义容器的镜像,端口,环境变量;

   6. 定义service,包括类型为ClusterIP,只能集群内访问,如果需要外部访问就要用到Ingress.定义集群端口和容器端口;

  2.4 编写Jenkinsfile:

def label = "slave-${UUID.randomUUID().toString()}"

def helmLint(String chartDir) {
    println "校验 chart 模板"
    sh "helm lint ${chartDir}"
}

def helmInit() {
    println "初始化 helm client"
    sh "helm init --client-only --stable-repo-url https://mirror.azure.cn/kubernetes/charts/"
}

def helmRepo(Map args) {
    println "添加 myrepo repo"
    sh "helm repo add --username ${args.username} --password ${args.password} myrepo http://k8s.harbor.test.site/chartrepo/system"

    println "update repo"
    sh "helm repo update"

    println "fetch chart package"
    sh """
      helm fetch myrepo/polling
      tar xzvf polling-0.1.0.tgz
    """
}

def helmDeploy(Map args) {
    helmInit()
    helmRepo(args)

    if (args.dry_run) {
        println "Debug 应用"
    sh "helm upgrade --dry-run --debug --install ${args.name} ${args.chartDir} --set persistence.persistentVolumeClaim.database.storageClass=dynamic --set database.type=external --set database.external.database=polling_app --set database.external.username=polling --set database.external.password=polling321 --set api.image.repository=${args.image} --set api.image.tag=${args.tag} --set imagePullSecrets[0].name=myreg --namespace=${args.namespace}"
    } else {
        println "部署应用"
    sh "helm upgrade --install ${args.name} ${args.chartDir} --set persistence.persistentVolumeClaim.database.storageClass=dynamic --set database.type=external --set database.external.database=polling_app --set database.external.username=polling --set database.external.password=polling321 --set api.image.repository=${args.image} --set api.image.tag=${args.tag} --set imagePullSecrets[0].name=myreg --namespace=${args.namespace}"
        echo "应用 ${args.name} 部署成功. 可以使用 helm status ${args.name} 查看应用状态"
    }
}

podTemplate(label: label, containers: [
  containerTemplate(name: 'maven', image: 'maven:3.6-alpine', command: 'cat', ttyEnabled: true),
  containerTemplate(name: 'docker', image: 'docker', command: 'cat', ttyEnabled: true),
  containerTemplate(name: 'kubectl', image: 'cnych/kubectl', command: 'cat', ttyEnabled: true),
  containerTemplate(name: 'helm', image: 'cnych/helm', command: 'cat', ttyEnabled: true)
], volumes: [
  hostPathVolume(mountPath: '/root/.m2', hostPath: '/var/run/m2'),
  hostPathVolume(mountPath: '/home/jenkins/.kube', hostPath: '/root/.kube'),
  hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock')
]) {
  node(label) {
    def myRepo = checkout scm
    def gitCommit = myRepo.GIT_COMMIT
    def gitBranch = myRepo.GIT_BRANCH

    def imageTag = sh(script: "git rev-parse --short HEAD",returnStdout:true).trim()
    def dockerRegistryUrl = "k8s.harbor.test.site"
    def imageEndpoint = "system/polling-app-server"
    def image = "${dockerRegistryUrl}/${imageEndpoint}"

    stage('单元测试') {
     input id: 'ncpprd', message: '发布生产请找-张三--批准?', ok: '确认', submitter: 'admin'
     echo "1.测试阶段"
    }
    stage('代码编译打包') {
     try {
        container('maven') {
          echo "2. 代码编译打包阶段"
          sh "cd polling-app-server && mvn clean package -Dmaven.test.skip=true"
      }
    }   catch (exc) {
        println "构建失败 - ${currentBuild.fullDisplayName}"
        throw (exc)
    }
  }
    stage('构建 Docker 镜像') {
      withCredentials ([[$class: 'UsernamePasswordMultiBinding',
        credentialsId: 'k8sharbor',
        usernameVariable: 'DOCKER_HUB_USER',
        passwordVariable: 'DOCKER_HUB_PASSWORD']]) {
          container('docker') {
           echo "3. 构建 Docker 镜像阶段"
           sh """
              docker login ${dockerRegistryUrl} -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASSWORD}
              cd polling-app-server && docker build -t ${image}:${imageTag} .
              docker push ${image}:${imageTag}
          """
      }
  }
}

    // stage('运行 Kubectl') {
    // container('kubectl') {
    //    echo "查看 K8S 集群 Pod 列表"
    //    sh "kubectl get pods"
    //    sh """
    //       sed -i "s#<IMAGE>#${image}#g" polling-app-server/polling-app-server.yaml
    //       sed -i "s#<IMAGE_TAG>#${imageTag}#g" polling-app-server/polling-app-server.yaml
    //       kubectl apply -f polling-app-server/polling-app-server.yaml
    //    """
    //  }
    //}

    stage('运行 Helm') {
       withCredentials([[$class: 'UsernamePasswordMultiBinding',
       credentialsId: 'k8sharbor',
       usernameVariable: 'DOCKER_HUB_USER',
       passwordVariable: 'DOCKER_HUB_PASSWORD']]) {
         container('helm') {
        // todo,也可以做一些其他的分支判断是否要直接部署
        echo "4. [INFO] 开始 Helm 部署"
        helmDeploy(
            dry_run     : false,
            name        : "polling",
            chartDir    : "polling",
            namespace   : "prd",
            tag         : "${imageTag}",
            image       : "${image}",
            username    : "${DOCKER_HUB_USER}",
            password    : "${DOCKER_HUB_PASSWORD}"

        )
        echo "[INFO] Helm 部署应用成功..."
       }
     }
   }
 }
}

  注意这里的Jenkinsfile文件名称,并且这个文件一定要放置在gitlab项目目录的根目录下面;关于Jenkinsfile的配置解释如下:

  1. 第一行定义jenkins slave pod的名称是随机生成的;
  2. 紧接着定义一个helmLint的函数,用途是检查helm模板的语法是否正确;
  3. 紧接着定义helmInit函数,用途是在jenkins slave pod里面安装一个helm的命令,并且使用微软的azure仓库;
  4. 紧接着定义helmRepo函数,此函数执行的时候需要传参数进去,函数用到了用途是添加内部的helm仓库,并且下载helm包。前提是必须定义好helm包里面的value.yaml文件,并且将helm包打包上传到私有仓库;
  5. 紧接着定义helmDeploy函数,此函数执行的时候需要传参数进去,如果传了dry_run干跑的参数,就是不实际部署helm,只是运行检查;同时helm命令可以通过--set来设置value.yaml文件的内容;
  6. 定义Pod模板,这里的slave pod定义就和上次在Jenkins系统管理---kubernetes云接入---Pod模板的方法不一样,这里的salve pod定义更灵活。可以定义一个pod模板,定义需要的镜像。比如JAVA项目需要maven打包,就需要maven镜像;同样将kubelet、docker命令的镜像和挂载的配置文件和socket文件;
  7. 从node(label)开始jenkins pipeline流程,定义git的使用,这里用到了插件 checkout。定义变量imageTag,获取git的提交编号;定义docker仓库,定义镜像的名称;
  8. 开始单元测试,生产环境使用了批准的机制;代码编译,需要进入到代码模块目录打包;
  9. 构建docker镜像,使用插件来完成credentialsId用户名和密码的定义,构建镜像和推送到私有仓库;
    10.可以选择使用kubectl命令部署应用,前期是有yaml清单文件;
    11.也可以选择使用helm部署,执行helmDepoly函数的时候,可以传入参数进去;
    12.关于helm的仓库,harbor自带了helm仓库,需要将chart包上传到helm仓库;
helm repo add --username=admin --password=Harbor12345 myrepo http://k8s.harbor.test.site/chartrepo/system
helm push polling-helm/ myreop

  2.5 GitLab和jenkins结合web-hook配置

  2.6 Blue Ocean创建多分支JOB

  2.7 GitLab和jenkins结合web-hook配置

  2.8 代码引用
  本文档中的代码引用地址如下:
  helm chart: https://github.com/cnych/polling-helm
  代码引用:https://github.com/callicoder/spring-security-react-ant-design-polls-app

博文的更详细内容请关注我的个人微信公众号 “云时代IT运维”,本公众号旨在共享互联网运维新技术,新趋势; 包括IT运维行业的咨询,运维技术文档分享。重点关注devops、jenkins、zabbix监控、kubernetes、ELK、各种中间件的使用,比如redis、MQ等;shell和python等运维编程语言;本人从事IT运维相关的工作有十多年。2008年开始专职从事Linux/Unix系统运维工作;对运维相关技术有一定程度的理解。本公众号所有博文均是我的实际工作经验总结,基本都是原创博文。我很乐意将我积累的经验、心得、技术与大家分享交流!希望和大家在IT运维职业道路上一起成长和进步;

kubernetes之CI/CD工具jenkins第二篇,helm的使用

猜你喜欢

转载自blog.51cto.com/zgui2000/2396800