企业上云已经成为现在的一个大趋势了,而应用上云最好的方式是利用云原生技术。本文介绍基于云原生相关技术的应用CICD实践。
程序员push代码到源码库,持续集成(Continuous Integration)工具从源码库中下载源代码,编译打包源代码,构建镜像,然后推送到镜像仓库,然后持续交付(Continuous Delivery)工具从镜像仓库中下载镜像,并发布到不同的运行环境。
devops思想涉及很多方面,本文的重心在于代码化,实现基础设施即代码、构建即代码、部署即代码。一切代码化后,就能利用CICD工具实现自动化,从而解放以手工方式的重复运维工作。基于代码化、版本化,用户可以在无IT人员的情况下自己根据需要构建、部署和配置服务;可以快速对环境、服务配置做优化、调整实现持续迭代。
自动化
利用各种工具实现来实现自动化流程,减少人为错误
可重复、一致性
流程都可以重复执行,并且每次执行的结果都是一致的
版本化
所有的流程通过脚本或者配置文件的方式和代码类似检入到VCS,当发生任何修改时都能够快速追溯或者回滚
通过jenkins、docker、kubernetes等工具,编写Jenkinsfile、Dockerfile、Kubernetes YAML文件,可以基本满足上述的原则。Jenkinsfile为构建代码,定义了构建流程;Dockerfile为基础设施代码,定义了服务的运行环境;Kubernetes YAML为部署代码,定义了服务的部署配置。
以下具体以一个简单的java项目为例说明如何利用这些工具实现完整的devops流程。
自建arm64版本的kubernetes集群
可以在长城云上申请ecs,自行部署k8s集群,集群部署可以参考https://github.com/toyangdon/k8s_deploy
镜像仓库
自建Harbor私有镜像仓库,或者直接使用docker hub
源代码库
自建svn或者gitlab源代码管理库,或者直接使用github
拷贝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.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
可以通过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自动拉起新容器
配置kubernetes plugin
在Jenkins管理页面中进入 “系统管理”-》“插件管理"-》”可选插件",搜索并安装Kubernetes Plugin。(可安装Kubernetes Cli,用于使用kubectl)
为Jenkins访问kubernetes集群准备访问凭证
kubectl apply -f - <
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”,点击右侧的“连接测试”,确认返回成功后,点击保存
在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等编译、构建、部署相关的配置文件。
流水线支持 两种语法:声明式和脚本式流水线。 两种语法都支持构建持续交付流水线。两种都可以用来在 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放到项目代码根目录下,并提交。
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作为最大堆内存)。
在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
在jenkins页面上选择之前创建的构建任务,点击“立即构建”
构建完成后,执行
kubectl get deploy -n demo-dev -o wide
服务的镜像tag自动更新成了此次构建的BUILD_ID(57)
一切代码化的好处很多,但也有很明显的弊端。一、对团队的技术要求比较高。上述方案中用到的工具虽然都是目前比较主流的产品,但都有其各自的脚本语法,技术人员使用的话需要一定的学习成本;二、对于运维人员来说,搭建完整的一套基于云原生的CICD环境,涉及kubernetes集群、镜像仓库、代码库、监控告警以及日志收集等等,这个代价对于一般企业来说是昂贵的。因此,我的建议是对于小型企业来说,首先技术团队学习DevOps文化是有必要的,但不需要为了实现这些流程去搭建与维护这么复杂的一套环境,直接使用各大云供应商的devops产品可以为企业节省大量成本 。
关注长城云,PK体系线上一站式适配中心https://www.ccyunchina.com/cloud/#/cloud