当初这家单位的 CICD 比拟的凌乱,而后突发奇想,想革新下,于是就用 pipeline
做了一个简略的流水线,上面是对于它的一些介绍
写一个简略的流水线
大略就是这么个流程简略来说就是:拉代码 —》编译 —》打镜像 —》推镜像 —》部署到 k8s 中,上面的 pipeline 就是在这条主线上进行,依据状况进行减少
pipeline {agent { label 'pdc&&jdk8'}
environment {
git_addr = "代码仓库地址"
git_auth = "拉代码时的认证 ID"
pom_dir = "pom 文件的目录地位(相对路径)"
server_name = "服务名"
namespace_name = "服务所在的命名空间"
img_domain = "镜像地址"
img_addr = "${img_domain}/cloudt-safe/${server_name}"
// cluster_name = "集群名"
}
stages {stage('Clear dir') {
steps {deleteDir()
}
}
stage('Pull server code and ops code') {
parallel {stage('Pull server code') {
steps {
script {
checkout(
[
$class: 'GitSCM',
branches: [[name: '${Branch}']],
userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_addr}"]]
]
)
}
}
}
stage('Pull ops code') {
steps {
script {
checkout(
[
$class: 'GitSCM',
branches: [[name: 'pipeline-0.0.1']], // 拉取的构建脚本的分支
doGenerateSubmoduleConfigurations: false,
extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'DEPLOYJAVA']], //DEPLOYJAVA: 把代码寄存到此目录中
userRemoteConfigs: [[credentialsId: 'chenf-o', url: '构建脚本的仓库地址']]
]
)
}
}
}
}
}
stage('Set Env') {
steps {
script {date_time = sh(script: "date +%Y%m%d%H%M", returnStdout: true).trim()
git_cm_id = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim()
whole_img_addr = "${img_addr}:${date_time}_${git_cm_id}"
}
}
}
stage('Complie Code') {
steps {
script {withMaven(maven: 'maven_latest_linux') {sh "mvn -U package -am -amd -P${env_name} -pl ${pom_dir}"
}
}
}
}
stage('Build image') {
steps {
script {dir("${env.WORKSPACE}/${pom_dir}") {
sh """echo'FROM 根底镜像地址 '> Dockerfile // 因为我这里进行了镜像的优化,只指定一个根底镜像地址即可,前面会具体的阐明"""
withCredentials([usernamePassword(credentialsId: 'faabc5e8-9587-4679-8c7e-54713ab5cd51', passwordVariable: 'img_pwd', usernameVariable: 'img_user')]) {
sh """
docker login -u ${img_user} -p ${img_pwd} ${img_domain}
docker build -t ${img_addr}:${date_time}_${git_cm_id} .
docker push ${whole_img_addr}
"""
}
}
}
}
}
stage('Deploy img to K8S') {
steps {
script {dir('DEPLOYJAVA/deploy') {
// 执行构建脚本
sh """/usr/local/python3/bin/python3 deploy.py -n ${server_name} -s ${namespace_name} -i ${whole_img_addr} -c ${cluster_name}"""
}
}
}
// 做了下判断如果下面脚本执行失败,会把下面阶段打的镜像删除掉
post {
failure {sh "docker rmi -f ${whole_img_addr}"
}
}
}
stage('Clear somethings') {
steps {
script {
// 删除打的镜像
sh "docker rmi -f ${whole_img_addr}"
}
}
post {
success {
// 如果下面阶段执行胜利,将把当前目录删掉
deleteDir()}
}
}
}
}
优化构建镜像
下面的 pipeline 中有一条命令是生成 Dockerfile
的,在这里做了很多优化,尽管我的 Dockerfile
就写了一个 FROM
,然而在这之后又会执行一系列的操作,上面咱们比照下没有做优化的Dockerfile
未优化
FROM 根底镜像地址
RUN mkdir xxxxx
COPY *.jar /usr/app/app.jar
ENTRYPOINT java -jar app.jar
优化后的
FROM 根底镜像地址
优化后的 Dockerfile
就这一行就完了。。。。。上面简略介绍下这个 ONBUILD
ONBUILD 能够这样了解,就比方咱们这里应用的镜像,是基于 java 语言做的一个镜像,这个镜像有两局部,一个是蕴含 JDK 的根底镜像 A,另一个是蕴含 jar 包的镜像 B,关系是先有 A 再有 B,也就是说 B 依赖于 A。
假如一个残缺的基于 Java 的 CICD 场景,咱们须要拉代码,编译,打镜像,推镜像,更新 pod 这一系列的步骤,而在打镜像这个过程中,咱们须要把编译后的产物 jar 包 COPY 到根底镜像中,这就造成了,咱们还得写一个 Dockerfile,用来 COPY jar 包,就像上面这个样子:
FROM jdk 根底镜像
COPY xxx.jar /usr/bin/app.jar
ENTRYPOINT java -jar app.jar
这样看起来也还好,基本上三行就解决了,然而能用一行就解决为什么要用三行呢?
FROM jdk 根底镜像
ONBUILD COPY target/*.jar /usr/bin/app.jar
CMD ["/start.sh"]
打成一个镜像,比方镜像名是:java-service:jdk1.8
,在打镜像的时候,ONBUILD
前面的在本地打镜像的过程中不会执行,而是在下次援用时执行的
FROM java-service:jdk1.8
只须要这一行就能够了,并且这样看起来更加简洁,pipeline
看起来也很标准,这样的话,咱们每一个 java 的服务都能够应用这一行 Dockerfile 了。
应用凭据
有时候应用 docker 进行 push 镜像时须要进行认证,如果咱们间接在 pipeline 里写的话不太平安,所以得进行脱敏,这样的话咱们就须要用到凭据了,增加凭据也是非常简单,因为咱们只是保留咱们的用户名和明码,所以用 Username with password
类型的凭据就能够了,如下所示
比如说:拉取 git 仓库的代码须要用到,而后这里就增加一个凭据,对应与 pipeline 里的上面这段内容:
stage('Pull server code') {
steps {
script {
checkout(
[
$class: 'GitSCM',
branches: [[name: '${Branch}']],
userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_addr}"]]
]
)
}
}
}
这里的变量 ${git_auth}
就是增加凭据时设置的 ID
,如果不设置ID
会随机生成一个ID
而后 docker push
时会须要进行认证,也须要增加凭据,增加形式和下面是一样的,不过咱们能够用 pipeline 的语法来生成一个,形式如下:
点击Pipeline Syntax
抉择withCredentials: Bind credentials to variables
而后和之前增加的凭据进行绑定,这里抉择类型为:Username and password (separated)
设置用户名和明码的变量名,而后抉择方才增加好的凭据
点击生成即可,就是下面 pipeline 里的上面这段:
withCredentials([usernamePassword(credentialsId: 'faabc5e8-9587-4679-8c7e-54713ab5cd51', passwordVariable: 'img_pwd', usernameVariable: 'img_user')]) {
sh """
docker login -u ${img_user} -p ${img_pwd} ${img_domain}
docker build -t ${img_addr}:${date_time}_${git_cm_id} .
docker push ${whole_img_addr}
"""
}
credentialsId
: 这个 ID 就是随机生成的 ID
执行脚本进行更新镜像
这里是应用 python 写了一个小脚本,来调用 kubernetes 的接口做了一个 patch
的操作实现的。先来看下此脚本的目录构造
外围代码:deploy.py
外围文件:config.yaml
寄存的是 kubeconfig 文件,用于和 kubernetes 的认证
上面贴一下 deploy.py
的脚本内容,能够参考下:
import os
import argparse
from kubernetes import client, config
class deployServer:
def __init__(self, kubeconfig):
self.kubeconfig = kubeconfig
config.kube_config.load_kube_config(config_file=self.kubeconfig)
self._AppsV1Api = client.AppsV1Api()
self._CoreV1Api = client.CoreV1Api()
self._ExtensionsV1beta1Api = client.ExtensionsV1beta1Api()
def deploy_deploy(self, deploy_namespace, deploy_name, deploy_img=None, deploy_which=1):
try:
old_deploy = self._AppsV1Api.read_namespaced_deployment(
name=deploy_name,
namespace=deploy_namespace,
)
old_deploy_container = old_deploy.spec.template.spec.containers
pod_num = len(old_deploy_container)
if deploy_which == 1:
pod_name = old_deploy_container[0].name
old_img = old_deploy_container[0].image
print("获取上一个版本的信息 \n")
print("以后 Deployment 有 {} 个 pod, 为: {}\n".format(pod_num, pod_name))
print("上一个版本的镜像地址为: {}\n".format(old_img))
print("此次构建的镜像地址为: {}\n".format(deploy_img))
print("正在替换以后服务的镜像地址....\n")
old_deploy_container[deploy_which - 1].image = deploy_img
else:
print("只反对替换一个镜像地址")
exit(-1)
new_deploy = self._AppsV1Api.patch_namespaced_deployment(
name=deploy_name,
namespace=deploy_namespace,
body=old_deploy
)
print("镜像地址曾经替换实现 \n")
return new_deploy
except Exception as e:
print(e)
def run():
parser = argparse.ArgumentParser()
parser.add_argument('-n', '--name', help="构建的服务名")
parser.add_argument('-s', '--namespace', help="要构建的服务所处在的命名空间")
parser.add_argument('-i', '--img', help="此次构建的镜像地址")
parser.add_argument('-c', '--cluster',
help="rancher 中以后服务所处的集群名称")
args = parser.parse_args()
if not os.path.exists('../config/' + args.cluster):
print("以后集群名未设置或名称不正确: {}".format(args.cluster), 'red')
exit(-1)
else:
kubeconfig_file = '../config/' + args.cluster + '/' + 'config.yaml'
if os.path.exists(kubeconfig_file):
cli = deployServer(kubeconfig_file)
cli.deploy_deploy(
deploy_namespace=args.namespace,
deploy_name=args.name,
deploy_img=args.img
)
else:
print("以后集群的 kubeconfig 不存在,请进行配置,地位为 {} 下的 config.yaml.(留神:config.yaml 名称写死,不须要改到)".format(args.cluster),
'red')
exit(-1)
if __name__ == '__main__':
run()
写的比较简单,没有难懂的中央,要害的中央是:
new_deploy = self._AppsV1Api.patch_namespaced_deployment(
name=deploy_name,
namespace=deploy_namespace,
body=old_deploy
)
这一句是执行的 patch 操作,把替换好新的镜像地址的内容进行 patch。
而后就是执行就能够了。
其余
这里有一个须要留神的中央是 pipeline
里加了一个异样捕捉, 如下所示:
post {
success {
// 如果下面阶段执行胜利,将把当前目录删掉
deleteDir()}
}
生命式的 pipeline 和脚本式的 pipeline 的异样捕捉的写法是有区别的,申明式写法是用的 post
来进行判断,比较简单,能够参考下官网文档
另外还有一个中央应用了并行执行,同时拉了服务的代码,和构建脚本的代码,这样能够进步执行整个流水线的速度,如下所示:
parallel {stage('Pull server code') {
steps {
script {
checkout(
[
$class: 'GitSCM',
branches: [[name: '${Branch}']],
userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_addr}"]]
]
)
}
}
}
stage('Pull ops code') {
steps {
script {
checkout(
[
$class: 'GitSCM',
branches: [[name: 'pipeline-0.0.1']], // 拉取的构建脚本的分支
doGenerateSubmoduleConfigurations: false,
extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'DEPLOYJAVA']], //DEPLOYJAVA: 把代码寄存到此目录中
userRemoteConfigs: [[credentialsId: 'chenf-o', url: '构建脚本的仓库地址']]
]
)
}
}
}
}
嗯,状况就是这么个状况,一个简简单单的流水线就实现了,如果想疾速应用流水线实现 CICD,能够参考下这篇文章。
欢送各位朋友关注我的公众号,来一起学习提高哦