准备工作
准备工作包括创建工程、编写辅助功能代码、初始化代码等:
打开《Kubernetes官方java客户端之一:准备 》一文创建的项目kubernetesclient,新增名为patch的子工程,pom.xml内容如下:
<?xml version="1.0" encoding="UTF-8"?>4.0.0com.bolingcavalrykubernetesclient1.0-SNAPSHOT../pom.xmlcom.bolingcavalrypatch0.0.1-SNAPSHOTpatchpatch demojarorg.springframework.bootspring-boot-starter-weborg.springframework.bootspring-boot-starter-jsonorg.springframework.bootspring-boot-starter-actuatororg.projectlomboklomboktrueio.kubernetesclient-javaorg.springframework.bootspring-boot-maven-plugin2.3.0.RELEASE<!--该配置会在jar中增加layer描述文件,以及提取layer的工具-->true
编写一个辅助类ClassPathResourceReader.java,作用是读取json文件的内容作为字符串返回:
packagecom.bolingcavalry.patch;importjava.io.BufferedReader;importjava.io.IOException;importjava.io.InputStreamReader;importjava.util.stream.Collectors;importorg.springframework.core.io.ClassPathResource;publicclassClassPathResourceReader{privatefinalString path;privateString content;publicClassPathResourceReader(String path){this.path = path; }publicStringgetContent(){if(content ==null) {try{ ClassPathResource resource =newClassPathResource(path); BufferedReader reader =newBufferedReader(newInputStreamReader(resource.getInputStream())); content = reader.lines().collect(Collectors.joining("\n")); reader.close(); }catch(IOException ex) {thrownewRuntimeException(ex); } }returncontent; }}
接下来新建本篇文章的核心类PatchExample.java,首先这个类中有main方法,整个应用从这里启动:
publicstaticvoidmain(String[] args){ SpringApplication.run(PatchExample.class, args); }
接下来有两个常量定义,分别是kubernetes环境里用来测试的deployment名称,以及namespace名称:
staticString DEPLOYMENT_NAME ="test123";staticString NAMESPACE ="default";
然后定义几个字符串变量,执行patch操作时用到的json内容都保存到这些字符串变量中:
static String deployStr, jsonStr, mergeStr, strategicStr, applyYamlStr;
在resources文件夹中放入json文件,稍后的初始化代码会将这些文件读取到字符串变量中,如下图,这些json文件的内容稍后会详细说明:
编写初始化代码(通过PostConstruct注解实现),主要是客户端配置,还有将json文件的内容读出来,保存到刚刚准备的字符串变量中:
@PostConstructprivatevoidinit()throwsIOException{// 设置api配置ApiClient client = Config.defaultClient(); Configuration.setDefaultApiClient(client);// 设置超时时间Configuration.getDefaultApiClient().setConnectTimeout(30000);// 部署用的JSON字符串deployStr =newClassPathResourceReader("deploy.json").getContent();// json patch用的JSON字符串jsonStr =newClassPathResourceReader("json.json").getContent();// merge patch用的JSON字符串,和部署的JSON相比:replicas从1变成2,增加一个名为from的label,值为mergemergeStr =newClassPathResourceReader("merge.json").getContent();// strategic merge patch用的JSON字符串strategicStr =newClassPathResourceReader("strategic.json").getContent();// server side apply用的JSON字符串applyYamlStr =newClassPathResourceReader("applyYaml.json").getContent(); }
以上就是准备工作;
创建服务
首先要开发一个部署的接口,通过调用此接口可以在kubernetes环境部署一个deployment:
部署服务的path是/patch/deploy,代码如下,可见部署deployment的代码分为三步:创建api实例、用字符串创建body实例、把body传给api即可:
/** * 通用patch方法 *@parampatchFormat patch类型,一共有四种 *@paramdeploymentName deployment的名称 *@paramnamespace namespace名称 *@paramjsonStr patch的json内容 *@paramfieldManager server side apply用到 *@paramforce server side apply要设置为true *@returnpatch结果对象转成的字符串 *@throwsException */privateStringpatch(String patchFormat, String deploymentName, String namespace, String jsonStr, String fieldManager, Boolean force)throwsException{// 创建api对象,指定格式是patchFormatApiClient patchClient = ClientBuilder .standard() .setOverridePatchFormat(patchFormat) .build(); log.info("start deploy : "+ patchFormat);// 开启debug便于调试,生产环境慎用!!!patchClient.setDebugging(true);// 创建deploymentExtensionsV1beta1Deployment deployment =newExtensionsV1beta1Api(patchClient) .patchNamespacedDeployment( deploymentName, namespace,newV1Patch(jsonStr),null,null, fieldManager, force ); log.info("end deploy : "+ patchFormat);returnnewGsonBuilder().setPrettyPrinting().create().toJson(deployment); }
body实例用到的json字符串来自deploy.json文件,内容如下,很简单,只有nginx的1.18.0版本的pod:
{"kind":"Deployment","apiVersion":"extensions/v1beta1","metadata":{"name":"test123","labels":{"run":"test123"} },"spec":{"replicas":1,"selector":{"matchLabels":{"run":"test123"} },"template":{"metadata":{"creationTimestamp":null,"labels":{"run":"test123"} },"spec":{"terminationGracePeriodSeconds":30,"containers":[ {"name":"test123","image":"nginx:1.18.0","ports":[ {"containerPort":80} ],"resources":{ } } ] } },"strategy":{ } },"status":{ }}
如此一来,web浏览器只要访问/patch/deploy就能创建deployment了;
发起patch请求的通用方法
通过kubernetes的客户端发起不同的patch请求,其大致步骤都是相同的,只是参数有所不同,我这里做了个私有方法,发起几种patch请求的操作都调用此方法实现(只是入参不同而已),可见都是先建好ApiClient实例,将patch类型传入,再创建V1Patch实例,将patch字符串传入,最后执行ExtensionsV1beta1Api实例的patchNamespacedDeployment方法即可发送patch请求:
/** * 通用patch方法 *@parampatchFormat patch类型,一共有四种 *@paramdeploymentName deployment的名称 *@paramnamespace namespace名称 *@paramjsonStr patch的json内容 *@paramfieldManager server side apply用到 *@paramforce server side apply要设置为true *@returnpatch结果对象转成的字符串 *@throwsException */privateStringpatch(String patchFormat, String deploymentName, String namespace, String jsonStr, String fieldManager, Boolean force)throwsException{// 创建api对象,指定格式是patchFormatApiClient patchClient = ClientBuilder .standard() .setOverridePatchFormat(patchFormat) .build(); log.info("start deploy : "+ patchFormat);// 开启debug便于调试,生产环境慎用!!!patchClient.setDebugging(true);// 创建deploymentExtensionsV1beta1Deployment deployment =newExtensionsV1beta1Api(patchClient) .patchNamespacedDeployment( deploymentName, namespace,newV1Patch(jsonStr),null,null, fieldManager, force ); log.info("end deploy : "+ patchFormat);returnnewGsonBuilder().setPrettyPrinting().create().toJson(deployment); }
上述代码中,有一行代码要格外重视,就是patchClient.setDebugging(true)这段,执行了这一行,在log日志中就会将http的请求和响应全部打印出来,是我们调试的利器,但是日志内容过多,生产环境请慎用;
上述patch方法有六个入参,其实除了patch类型和patch内容,其他参数都可以固定下来,于是再做个简化版的patch方法:
/** * 通用patch方法,fieldManager和force都默认为空 *@parampatchFormat patch类型,一共有四种 *@paramjsonStr patch的json内容 *@returnpatch结果对象转成的字符串 *@throwsException */privateStringpatch(String patchFormat, String jsonStr)throwsException{returnpatch(patchFormat, DEPLOYMENT_NAME, NAMESPACE, jsonStr,null,null); }
入参patchFormat的值是四种patch类型的定义,在V1Patch.java中,其值如下所示:
接下来可以轻松的开发各种类型patch的代码了;
执行json patch
首先来看json patch要提交的内容,即json.json文件的内容,这些内容在应用启动时被保存到变量jsonStr,如下所示,非常简单,修改了terminationGracePeriodSeconds属性的值,原来是30,这个属性在停止pod的时候用到,是等待pod的主进程的最长时间:
[ {"op":"replace","path":"/spec/template/spec/terminationGracePeriodSeconds","value":27}]
接下来就是web接口的代码,可见非常简单,仅需调用前面准备好的patch方法:
/** * JSON patch格式的关系 * *@return*@throwsException */@RequestMapping(value = "/patch/json", method = RequestMethod.GET)publicStringjson()throwsException{returnpatch(V1Patch.PATCH_FORMAT_JSON_PATCH, jsonStr); }
merge patch(全量)
先尝试全量的merge patch,也就是准备好完整的deployment内容,修改其中一部分后进行提交,下图是json文件merge.json的内容,其内容前面的deploy.json相比,仅增加了红框处的内容,即增加了一个label:
代码依然很简单:
@RequestMapping(value = "/patch/fullmerge", method = RequestMethod.GET)publicStringfullmerge()throwsException{returnpatch(V1Patch.PATCH_FORMAT_JSON_MERGE_PATCH, mergeStr); }
merge patch(增量)
前面曾提到merge patch和strategic merge patch的区别:merge patch提交一个container时做的是替换,而strategic merge patch提交一个container时做的是合并,为了展示这两种patch的不同,这里我们就用同一个json内容,分别执行merge patch和strategic merge patch,看看结果有什么区别,这是最直观的学习方法;
这个json对应的文件是strategic.json,内容如下:
{"spec":{"template":{"spec":{"containers":[ {"name":"test456","image":"tomcat:7.0.105-jdk8"} ] } } }}
增量merge的代码如下:
@RequestMapping(value = "/patch/partmerge", method = RequestMethod.GET)publicStringpartmerge()throwsException{returnpatch(V1Patch.PATCH_FORMAT_JSON_MERGE_PATCH, strategicStr); }
strategic merge patch
strategic merge patch用的json内容和前面的增量merge patch是同一个,代码如下:
@RequestMapping(value = "/patch/strategic", method = RequestMethod.GET)publicStringstrategic()throwsException{returnpatch(V1Patch.PATCH_FORMAT_STRATEGIC_MERGE_PATCH, strategicStr); }
apply yaml patch
apply yaml patch与其他patch略有不同,调用ExtensionsV1beta1Api的patchNamespacedDeployment方法发请求时,fieldManager和force字段不能像之前那样为空了:
@RequestMapping(value = "/patch/apply", method = RequestMethod.GET)publicStringapply()throwsException{returnpatch(V1Patch.PATCH_FORMAT_APPLY_YAML, DEPLOYMENT_NAME, NAMESPACE, applyYamlStr,"example-field-manager",true); }
上面的代码中,如果force字段不等于true,可能会导致patch失败,在官方文档也有说明,如下图红框:
apply yaml patch的json字符串来自文件applyYaml.json,其内容是从deploy.json直接复制的,然后改了下图两个红框中的内容,红框1修改了nginx的版本号,用来验证patch是否生效(原有版本是1.18),红框2是kubernetes1.16之前的一个问题,protocol字段必填,否则会报错,问题详情请参考:https://github.com/kubernetes-sigs/structured-merge-diff/issues/130
以上就是所有代码和patch的内容了,接下来部署到kubernetes环境实战吧
制作镜像并且部署
在patch工程目录下执行以下命令编译构建:
mvn clean package -U -DskipTests
在patch工程目录下创建Dockerfile文件,内容如下:
# 指定基础镜像,这是分阶段构建的前期阶段FROM openjdk:8u212-jdk-stretch as builder# 执行工作目录WORKDIR application# 配置参数ARG JAR_FILE=target/*.jar# 将编译构建得到的jar文件复制到镜像空间中COPY ${JAR_FILE} application.jar# 通过工具spring-boot-jarmode-layertools从application.jar中提取拆分后的构建结果RUN java -Djarmode=layertools -jar application.jar extract# 正式构建镜像FROM openjdk:8u212-jdk-stretchWORKDIR application# 前一阶段从jar中提取除了多个文件,这里分别执行COPY命令复制到镜像空间中,每次COPY都是一个layerCOPY --from=builder application/dependencies/ ./COPY --from=builder application/spring-boot-loader/ ./COPY --from=builder application/snapshot-dependencies/ ./COPY --from=builder application/application/ ./ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
在patch工程目录下执行以下命令创建docker镜像:
docker build -t 192.168.50.43:5888/common/patch:1.0-SNAPSHOT .
如果您已经配置了docker镜像仓库私服,建议将此镜像推送到私服上去,以便kubernetes上可以使用该镜像,我这边的推送命令如下,仅供参考(涉及到身份验证的话还请执行docker login登录):
docker push 192.168.50.43:5888/common/patch:1.0-SNAPSHOT
SSH登录kubernetes环境,新建patch.yaml文件,内容如下,image那里请按您的镜像情况自行调整:
apiVersion:v1kind:Servicemetadata:name:patchnamespace :kubernetesclientspec:type:NodePortports:-port:8080nodePort:30102selector:name:patch---apiVersion:extensions/v1beta1kind:Deploymentmetadata:namespace :kubernetesclientname:patchspec:replicas:1template:metadata:labels:name:patchspec:serviceAccountName:kubernates-client-service-accountcontainers:-name:patchimage:192.168.50.43:5888/common/patch:1.0-SNAPSHOTtty:truelivenessProbe:httpGet:path:/actuator/health/livenessport:8080initialDelaySeconds:5failureThreshold:10timeoutSeconds:10periodSeconds:5readinessProbe:httpGet:path:/actuator/health/readinessport:8080initialDelaySeconds:5timeoutSeconds:10periodSeconds:5ports:-containerPort:8080resources:requests:memory:"512Mi"cpu:"100m"limits:memory:"1Gi"cpu:"1000m"
执行以下命令即可完成部署:
kubectl apply -f patch.yaml
用于验证patch的deployment名为test123(浏览器访问/patch/deploy就会创建),这个deployment里面是个nginx容器,咱们要给它准备一个NodePort类型的service,以便验证的是否可以通过浏览器访问,该service对应的yaml文件是nginx-service.yaml,内容如下:
apiVersion: v1
kind: Service
metadata:
name: test123
namespace : default
spec:
type: NodePort
ports:
- port: 80
nodePort: 30101
selector:
run: test123
执行以下命令即可完成部署:
kubectl apply -f nginx-service.yaml
亚马逊测评 www.yisuping.cn