通过jenkins在kubernetes上配置CI/CD pipeline(arm64环境)

概述

企业上云已经成为现在的一个大趋势了,而应用上云最好的方式是利用云原生技术。本文介绍基于云原生相关技术的应用CICD实践。

什么是DevOps

  • DevOps是一种软件工程文化和实践,旨在统一融合软件开发和软件 运维 、加速软件交付效率、提高软件交付质量。
  • DevOps的关键原则之一是自动化,它减少了人为错误,提供了一致的结果,甚至降低了风险。
  • DevOps本质是为运维服务的,通过一系列的工具让研发更接近生产环境,部分工作向研发倾斜。
    在这里插入图片描述

程序员push代码到源码库,持续集成(Continuous Integration)工具从源码库中下载源代码,编译打包源代码,构建镜像,然后推送到镜像仓库,然后持续交付(Continuous Delivery)工具从镜像仓库中下载镜像,并发布到不同的运行环境。

原则

devops思想涉及很多方面,本文的重心在于代码化,实现基础设施即代码、构建即代码、部署即代码。一切代码化后,就能利用CICD工具实现自动化,从而解放以手工方式的重复运维工作。基于代码化、版本化,用户可以在无IT人员的情况下自己根据需要构建、部署和配置服务;可以快速对环境、服务配置做优化、调整实现持续迭代。

  1. 自动化

    利用各种工具实现来实现自动化流程,减少人为错误

  2. 可重复、一致性

    流程都可以重复执行,并且每次执行的结果都是一致的

  3. 版本化

    所有的流程通过脚本或者配置文件的方式和代码类似检入到VCS,当发生任何修改时都能够快速追溯或者回滚

实践

通过jenkins、docker、kubernetes等工具,编写Jenkinsfile、Dockerfile、Kubernetes YAML文件,可以基本满足上述的原则。Jenkinsfile为构建代码,定义了构建流程;Dockerfile为基础设施代码,定义了服务的运行环境;Kubernetes YAML为部署代码,定义了服务的部署配置。

以下具体以一个简单的java项目为例说明如何利用这些工具实现完整的devops流程。

准备

  1. 自建arm64版本的kubernetes集群

    可以在长城云上申请ecs,自行部署k8s集群,集群部署可以参考https://github.com/toyangdon/k8s_deploy

  2. 镜像仓库

    自建Harbor私有镜像仓库,或者直接使用docker hub

  3. 源代码库

    自建svn或者gitlab源代码管理库,或者直接使用github

Step 1. 在kubernetes集群上部署jenkins

拷贝jenkins.yaml文件,并执行以下命令

kubectl apply -f jenkins.yaml -n cicd

执行完成后,查看jenkins容器日志,等待jenkins容器启动完成(jenkins完成启动的时间比较长)。

kubectl logs $(kubectl get pods --selector=app=jenkins -n cicd -o=jsonpath='{.items[0].metadata.name}') jenkins -n cicd

jenkins.yaml

# Source: jenkins/templates/pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: jenkins
  labels:
    app: jenkins
spec:
  accessModes:
    - "ReadWriteMany"
  resources:
    requests:
      storage: "50Gi"
---
# Source: jenkins/templates/svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: jenkins
  labels:
    app: jenkins
spec:
  type: LoadBalancer
  externalTrafficPolicy: "Cluster"
  ports:
    - name: http
      port: 80
      targetPort: http
    - name: https
      port: 443
      targetPort: https
  selector:
    app: jenkins
---
# Source: jenkins/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jenkins
  labels:
    app: jenkins
spec:
  selector:
    matchLabels:
      app: jenkins
  template:
    metadata:
      labels:
        app: jenkins
    spec:      
      containers:
        - name: jenkins
          image: toyangdon/jenkins:2.1901-arm64
          env:
            - name: JAVA_OPTS
              value: "-Xmx3072m -Xms512m -XX:ErrorFile=/var/jenkins_home/hs_err_pid<pid>.log"
          ports:
            - name: http
              containerPort: 8080
            - name: https
              containerPort: 8443
          livenessProbe:
            httpGet:
              path: /login
              port: http
            initialDelaySeconds: 300
            periodSeconds: 10
            timeoutSeconds: 5
            successThreshold: 1
            failureThreshold: 10
          readinessProbe:
            httpGet:
              path: /login
              port: http
            initialDelaySeconds: 30
            periodSeconds: 5
            timeoutSeconds: 3
            successThreshold: 1
            failureThreshold: 3
          resources:
            limits:
              cpu: 2
              memory: 4Gi 
            requests:
              cpu: 300m
              memory: 512Mi
          volumeMounts:
            - name: jenkins-data
              mountPath: /var/jenkins_home
      volumes:
        - name: jenkins-data
          persistentVolumeClaim:
            claimName: jenkins

Step 2. 完成Jenkins相关配置

  1. 可以通过nodePort方式在浏览器上访问jenkins页面,按提示输入初始管理密码,选择安装推荐插件,设置管理员账号。

    初始管理密码查看

    kubectl exec -n cicd $(kubectl get pods --selector=app=jenkins -n cicd -o=jsonpath='{.items[0].metadata.name}') cat /var/jenkins_home/secrets/initialAdminPassword

    如果Jenkins 插件下载速度太慢,可以配置国内插件源

    kubectl exec -n cicd $(kubectl get pods --selector=app=jenkins -n cicd -o=jsonpath='{.items[0].metadata.name}') -- sh -c "sed -i 's/http:\/\/updates.jenkins-ci.org\/download/https:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g' /var/jenkins_home/updates/default.json && sed -i 's/http:\/\/www.google.com/https:\/\/www.baidu.com/g' /var/jenkins_home/updates/default.json"

    修改配置后,需要重启Jenkins容器生效

    kubectl delete pod -n cicd $(kubectl get pods --selector=app=jenkins -n cicd -o=jsonpath='{.items[0].metadata.name}') #删除旧容器,由k8s自动拉起新容器

  2. 配置kubernetes plugin

    在Jenkins管理页面中进入 “系统管理”-》“插件管理"-》”可选插件",搜索并安装Kubernetes Plugin。(可安装Kubernetes Cli,用于使用kubectl)

    为Jenkins访问kubernetes集群准备访问凭证

    kubectl apply -f - <<EOF
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: jenkins-builder
      namespace: cicd
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: jenkins-builder
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: cluster-admin
    subjects:
    - kind: ServiceAccount
      name: jenkins-builder
      namespace: cicd
    EOF
    
    kubectl get secret `kubectl get secret -n cicd|grep jenkins-builder|awk '{print $1}'` -n cicd -o=jsonpath='{.data.token}' |base64 -d
    

    返回Jenkins页面,点击“系统管理",进入"系统配置",在”云“板块中,点击”新增一个云“,选择kubernetes,点击Kubernetes Cloud details

    Kubernetes 地址填写为"https://kubernetes.default"

    凭据下拉框旁边点击”添加“,弹出添加凭据对话框,类型选择”Secret text“,Secret输入刚刚复制的jenkins-builder用户的token,ID和描述都填为“kubernetes-builder”
    在这里插入图片描述
    添加凭据成功后,在凭据下拉框中选择刚刚创建的“kubernetes-builder”,点击右侧的“连接测试”,确认返回成功后,点击保存
    在这里插入图片描述

Step 3. 创建一个构建流水线任务

在Jenkins页面中,点击左侧“新建任务”,输入任务名,选择“流水线”,点击确定。

在弹出的任务配置页面中,找到流水线配置模块,“定义”选项选择“Pipeline script from SCM”,表示Pipeline脚本从SCM中(遵循原则3 版本化),然后配置SCM相关信息,下图配置示例表示Pipeline脚本从github上URL为“https://github.com/toyangdon/demo.git”的项目中取,Pipeline脚本为项目根路径下文件名为Jenkinsfile的文件。
在这里插入图片描述
点击保存,即创建好一个构建流水线任务。在Jenkins的配置中我们尽量保持简单,具体的构建流程、部署流程均配置在流水线脚本文件中。

图中github项目是一个简单的springboot工程,项目中除了Java源码还需要提供Jenkinsfile、Dockerfile、kustomize yaml等编译、构建、部署相关的配置文件。

Step 4. 编写Jenkinsfile流水线文件

流水线支持 两种语法:声明式和脚本式流水线。 两种语法都支持构建持续交付流水线。两种都可以用来在 web UI 或 Jenkinsfile 中定义流水线,不过通常认为创建一个 Jenkinsfile 并将其检入源代码控制仓库是最佳实践。

本文件示例使用脚本式语法编写Jenkinsfile,同时将Jenkinsfile文件与代码一起存放github代码库中。

​ Jenkinsfile

def POD_LABEL = "java-builder"
podTemplate(label: POD_LABEL, cloud: 'dev', containers: [
    containerTemplate(name: 'jnlp', image: 'toyangdon/jnlp-slave-maven-arm64:4.3-v2')  
  ],
  volumes: [
    hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'), //实现dockerInDocker
	persistentVolumeClaim(mountPath: '/root/.m2', claimName: 'jenkins-build-maven-m2'), //如果想避免每次构建重复下载maven依赖包,可以将.m2目录持久化
    hostPathVolume(mountPath: '/usr/local/sbin', hostPath: '/opt/k8s/bin')  //使用宿主机的docker\kubectl等二进制文件
  ]
  ) {

    node(POD_LABEL) {
        stage('下载代码') {
            git  'https://github.com/toyangdon/demo.git'
        }
		stage('编译源码'){	
			 sh 'mvn install'
		}
	   stage('镜像'){	
			 sh "docker build -t  toyangdon/demo:${BUILD_ID} ."
		}
		stage('推送镜像'){	
			 withCredentials([usernamePassword(credentialsId: 'docker_hub', passwordVariable: 'DOCKER_HUB_PASSWORD', usernameVariable: 'DOCKER_HUB_USERNAME')]) {
				sh "docker login -u ${DOCKER_HUB_USERNAME} -p${DOCKER_HUB_PASSWORD}"
				sh "docker push toyangdon/demo:${BUILD_ID}"
			}
		}
		stage('部署服务'){
			withKubeConfig( credentialsId: 'kubernetes-builder', serverUrl: 'https://kubernetes.default') {
				dir('kustomize/overlays/dev'){
					sh "kustomize edit set image toyangdon/demo:${BUILD_ID}"
					sh "kustomize build | kubectl apply -f -"
				}
			}
		}
    }
}

这个Jenkinsfile的开头声明了一个podTemplate,该podTemplate为执行构建任务时创建的Pod模板。

label表示jenkins构建节点的标签,后面的构建流程中需要指定该标签。

cloud属性为第2步中配置的kubernetes集群 的名称。

containers中定义了该pod下的容器模板。默认情况下每个构建Pod中都有一个名为jnlp容器,用于jenkins master的通信。由于kubernetes plugin默认的jnlp容器使用的x86镜像,此处改成自己编译构建的arm64版本的镜像(编译参考https://github.com/jenkinsci/docker-slave/,https://github.com/jenkinsci/docker-jnlp-slave),此处为了简单,直接使用jnlp镜像容器作为构建环境(可以选择再定义一个containerTemplate,将其作为构建容器),示例中使用了 toyangdon/jnlp-slave-mvn-arm64:4.3-v2镜像,这个自建镜像在默认的jnlp的基础上另外安装了maven,因此这个镜像中除了jnlp salve之外装有openjdk1.8、git、maven,这些工具可用后续的构建流程。

toyangdon/jnlp-slave-mvn-arm64:4.3-1镜像的dockerfile

FROM toyangdon/jnlp-slave-arm64:4.3-v2

USER root

RUN USER_HOME_DIR=/root  &&mkdir -p /usr/share/maven /usr/share/maven/ref   && curl -fsSL -o /tmp/apache-maven.tar.gz https://mirrors.tuna.tsinghua.edu.cn/apache/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz   && tar -xzf /tmp/apache-maven.tar.gz -C /usr/share/maven --strip-components=1   && rm -f /tmp/apache-maven.tar.gz   && ln -s /usr/share/maven/bin/mvn /usr/bin/mvn

ENV MAVEN_HOME=/usr/share/maven

ENV MAVEN_CONFIG=/root/.m2

volumes属性定义了运行容器时所需要挂载的持久化卷,使用了两个hostPathVolume,将宿主机上的/var/run/docker.sock文件/opt/k8s/bin目录挂载到了容器中,因为作者的k8s节点中/opt/k8s/bin目录下有docker、kubectl、kustomize等二进制文件,构建过程需要使用这些文件,故将其挂载。

Jenkinsfile后面部分共包含了5个stage,第一个stage中执行了git命令下载源码;第二个stage中执行mvn命令编译打包源码;第三个stage中执行了docker build命令构建镜像,构建时镜像的tag使用jenkins的全局变量BUILD_ID,构建时使用了当前目录下的Dockerfile文件,该文件在下文Step 5中详细说明;第四个stage中执行docker push推送镜像,stage中使用的withCredentials需要自己事先创建ID为“docker_hub”的凭证;第五个stage中执行kubectl命令完成服务的部署,先使用了kustomize修改dev环境的部署文件,将镜像的tag更新为${BUIlD_ID},再使用kubectl部署服务。

将编写好的Jenkinsfiles放到项目代码根目录下,并提交。

Step 5. 编写Dockerfile文件

Dockerfile 是一个用来构建镜像的文本文件,文件中内容包含了一条条构建镜像所需的指令和说明。docker通过读取指令来完成构建。

构建用的Dockerfile

FROM openjdk:8-alpine

COPY target/*.jar /root/demo.jar

ENTRYPOINT java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=2 -jar demo.jar

该Dockerfile中的指令很简单,拷贝构建流程的stage 2中编译的jar到镜像中,然后设置镜像的入口命令为运行编译的jar。

-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=2为jdk1.8支持的jvm参数,表示让jvm感知容器部署时设置的内存限制值,并设置最大堆内存为限制值的一半(如果不设置这些参数,默认情况下jvm会使用宿主机总内存的4分之1作为最大堆内存)。

Step 6. 编写Kustomize 部署编排文件

在kubernetes上部署服务,通常情况下是通过kubectl工具去执行kubernetes 应用描述文件 (YAML文件)。应用描述文件中定义好了服务部署的相关配置,包括镜像名、副本数、端口号、资源配额、环境变量等等。

而在CICD的场景下,一般服务是需要部署在多个环境下的,而不同环境下的部署配置会存在细微的差异,针对这种场景,kubernetes提供了kustomize工具,以结构化的方式管理应用描述文件,允许用户以一个应用描述文件 (YAML 文件)为基础(Base YAML),然后通过 Overlay(覆盖) 的方式生成最终部署应用所需的描述文件。通过Overlay的方式可以基于Base YAML衍生出不同环境的YAML文件。

在项目代码根据目录中创建kustomize目录,该目录下编写以下文件

kustomize/
├── base
│   ├── deployment.yaml
│   ├── kustomization.yaml
│   └── service.yaml
└── overlays
    ├── dev
    │   ├── env_patch.yaml
    │   ├── kustomization.yaml
    │   └── memorylimit_patch.yaml
    └── prod
        ├── env_patch.yaml
        ├── kustomization.yaml
        └── memorylimit_patch.yaml

base目录下定义了基础YAML文件,overlays目录分为dev和prod两个目录,而这个两目录分别定义开发环境和生产环境的patch YAML文件,env_patch.yaml中声明了不同的环境变量,memorylimit_patch.yaml中声明了不同的资源请求和限制值。

由于相关yaml文件内容过多,此处只贴出memorylimit_patch.yaml文件,全部文件可以到https://github.com/toyangdon/demo.git中查看

dev中memorylimit_patch.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
spec:
  replicas: 1
  template:
    spec:
      containers:
        - name: demo
          resources:
            limits:
              cpu: 300m
              memory: 500Mi
            requests:
              cpu: 300m
              memory: 500Mi

prod中memorylimit_patch.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: demo
          resources:
            limits:
              cpu: 1
              memory: 1024Mi
            requests:
              cpu: 500m
              memory: 600Mi

prod环境中的replicas(副本数)、limits(限制值)、request(请求值)均设置不同于dev环境中的。这两个patch文件中的字段通过kustomize命令根据实现场景合并到base中的deployment.yaml中的deployment配置中去。

Jenkinsfile文件的stage(“部署服务”)中

dir('kustomize/overlays/dev'){
					sh "kustomize edit set image toyangdon/demo:${BUILD_ID}"
					sh "kubectl apply -k"
				}

dir(kustomize/overlays/dev) 表示进入到kustomize/overlays/dev目录中。

sh "kustomize edit set image toyangdon/demo:${BUILD_ID}"表示修改yaml中的镜像名为toyanagdon/demo:${BUILD_ID}

sh "kustomize build | kubectl apply -f -"表示部署当前目录下kustomize build后的配置文件,kubectl 1.14中集成了kustomize,通过-k表示使用kustomize

Step 7. 执行构建流水线

在jenkins页面上选择之前创建的构建任务,点击“立即构建”
在这里插入图片描述
构建完成后,执行

kubectl get deploy -n demo-dev -o wide
在这里插入图片描述
服务的镜像tag自动更新成了此次构建的BUILD_ID(57)

结论

一切代码化的好处很多,但也有很明显的弊端。一、对团队的技术要求比较高。上述方案中用到的工具虽然都是目前比较主流的产品,但都有其各自的脚本语法,技术人员使用的话需要一定的学习成本;二、对于运维人员来说,搭建完整的一套基于云原生的CICD环境,涉及kubernetes集群、镜像仓库、代码库、监控告警以及日志收集等等,这个代价对于一般企业来说是昂贵的。因此,我的建议是对于小型企业来说,首先技术团队学习DevOps文化是有必要的,但不需要为了实现这些流程去搭建与维护这么复杂的一套环境,直接使用各大云供应商的devops产品可以为企业节省大量成本 。

关注长城云,PK体系线上一站式适配中心https://www.ccyunchina.com/cloud/#/cloud

Logo

开源、云原生的融合云平台

更多推荐