从一个零碎的上线说起

即便是一个最简略的零碎,从代码到线上,从运维到降级都要经验以下生命周期

  • 前端代码编译(不同线上环境抉择不同配置)
  • 前端代码打包
  • 后端代码编译(不同线上环境抉择不同配置)
  • 后端代码打包
  • 搭建nginx部署前端代码
  • 搭建tomcat部署后端代码
  • 前端利用集群
  • 后端利用集群
  • 配置负载平衡
  • 利用监控
  • 故障重启
  • 滚动降级
  • 利用回滚
  • 弹性伸缩
  • 零碎运维交接

这些步骤如果都是人工来做,每一步都存在危险

  • 代码编译(编译程序版本不统一,遗记批改配置文件,生产跑测试的配置)
  • 环境搭建(不同环境受限网络环境,操作系统版本等差别,同样的程序在不同环境中可能体现不统一)
  • 负载配置(增加节点须要人工操作,节点故障无奈进行故障转移)
  • 利用监控(人工监控不事实)
  • 故障重启(无奈进行人工操作,人工滞后于故障)
  • 滚动降级(无奈进行人工操作)
  • 利用回滚(须要在计划层面上预留回滚机制,比方保留之前版本的部署包)
  • 弹性伸缩(无奈进行人工操作)
  • 运维交接(每个零碎都须要交接,代码仓库,打包办法,部署步骤,重启步骤,降级步骤等)

咱们在运维交接过程中碰到十分多代码与线上运行不统一,存在代码失落问题,有可能是之前运维的共事部署后没有提交最新代码,工夫久了代码就找不到,特地是对于长时间没有更新的零碎,咱们无奈保障目前的代码就是最新的代码,代码不统一对运维是灾难性的,总之一句话就是

人是不牢靠的

现实的状况下,开发提交完代码,所有就与他无关

  • 不必由开发人员个人电脑进行编译,编译应该是对立的平台,对立的环境,对立的规范
  • 环境不能搭建两次,就像你写两边得帆,就算写上几千几万次,你无奈找到两次齐全一样的
  • 集群是自动化的,什么意思呢,集群应该是一个配置项在那,而不是咱们须要针对单机和集群采取齐全不一样的装置形式,部署形式
  • 监控是自动化,多样化,可配置,借助图表可能疾速判断零碎问题
  • 弹性伸缩也要自动化,可能依据零碎负载状况主动进行伸缩
  • 故障重启,利用回滚都是自动化,可能主动重启,部署失败可能主动回滚
  • 交接是标准化,所有零碎的交接都是一个模板

kubernetes解决了什么问题

  • 保障环境完全一致
  • 提供集群配置
  • 故障重启
  • 弹性伸缩

jenkins解决了什么问题

  • 线上编译,打包,部署

prometheus解决了什么问题

  • 监控
  • 预警
  • 告诉

如果能做到以上这些自动化,那么交接的内容就是,如果启动这个流程

  • 某我的项目流水线

所有的扭转源于docker

任何事物的倒退都须要标准化,今人早在两千多年前就意识到标准化的重要性,秦始皇对立六国后履行车同轨书文同文就是一种标准化,自此中华名族才实现了真正意义上的大一统。聪慧的IT人也在标准化上提出了各种计划

  • W3C规范制订了互联网规范,从此互联网进入高速倒退阶段
  • JDBC规范让程序员从适配不同数据库的噩梦中解脱
  • J2EE规范让java成为了最风行的后端开发语言
  • TCP/IP协定奠定了互联网的根底
  • 等等

规范说白了就是对事物的极致形象,那么一个利用的形象是什么呢,利用有太多属性了,拍脑袋就能够列举很多

  • 编程语言不一样
  • 运行环境不一样
  • 依赖不同的第三方零碎
  • 打包编译的形式不一样
  • 部署形式不一样
  • 重启的形式不一样,systemctl想实现这个指标
  • 等等

而且这种差异性都不是小问题,都是刀刀要命的大问题,如果不解决利用都无奈失常运行,如果真的硬要形象那么只会导致利用变得更加的简单,聪慧的程序员从集装箱中找到了灵感,货物和利用有很多共通的中央

  • 大小不一样
  • 体积不一样
  • 运输工具不一样
  • 装载形式不一样
  • 卸载形式不一样

生物的多样性能力让咱们这个星球变得生机勃勃,但多样性也导致了标准化的艰难,人类很聪慧,设计出了集装箱零碎

咱们只运集装箱,你们本人想方法把获取装到集装箱

咱们把利用装到docker中就跟咱们把货物装到集装箱,那么对利用的治理就变为对docker的治理,跟集装箱一样,对货物的治理就变为对集装箱的治理,事件就变得简略多了。

事实上,早在docker之前,咱们就在摸索标准化,比方虚拟机,拷贝虚拟机也能实现相似docker的性能,那为什么这种计划没有流行起来呢,因为虚拟机太重了,货车装羽毛,效率太低了,docker相比虚拟机有很多劣势

  • 轻量级,docker自身占系统资源极少
  • 启动快,指的是docker自身启动很快,相比虚拟机而言
  • 易迁徙,docker实质就是一个dockerfile的文本文件,虚拟机动不动就是好几个G
  • 好治理,只需把握大量命令就能治理利用

对于没有接触过docker的同学,你能够了解为超轻量级虚拟机,尽管docker和虚拟机有实质的区别,但能够让你对docker有个初步的印象。

为什么还有kubernetes

实际上,在没有kubernetes呈现之前,尽管docker热度很高,然而很少有企业在生产中真正用起来,因为docker解决了大问题,但小问题还有很多

  • 官网没有治理界面,不敌对
  • 无奈跨主机通信,导致无奈大规模应用
  • 没有监控机制,无奈监控利用状态
  • 短少下层服务,docker只提供基础设施
  • 短少编排服务,即一个零碎往往须要编写多个dockerfile,尽管前面有composer,但这是后话
  • 短少大公司背书

说到底,docker解决了利用部署,利用运维这两个大问题,然而利用治理,利用编排,利用监控,弹性伸缩等等这些和利用相干的问题还没解决,kubernetes的呈现才真正意义上实现了大一统,自此docker才真正施展出其微小威力。

从这张图能够看出,kubernetes就是一个docker的管理系统,在架构上处于docker之上,利用之下,往上能够为利用赋能,提供利用多种IT资源,往下,能够调度docker,实现利用的对立治理。kubernetes能为利用提供什么呢,其实就是开篇提到的那些利用开发过程中的问题都能在kubernetes中找到解决方案,比方

  • 提供Deployment解决利用部署的问题
  • 提供Service解决利用负载的问题
  • 提供ConfigMap解决不同环境配置的问题
  • 提供Ingress解决利用拜访的问题
  • 提供PersistentVolume解决利用存储问题
  • 等等

根本你能想到在开发中碰到的非业务问题都能在kubernetes中找到解决方案,kubernetes就像一个利用的小镇一样,为利用提供各种所需资源

五分钟搭建一个kubernetes环境

这部分内容次要目标是打消大家对kubernetes的恐怖,不要感觉kubernetes很简单,kubernetes刚公布时的确装置起来比拟麻烦,然而近几年有很多计划让kubernetes装置变得非常简单,只需一行命令就能实现装置

  • 设置主机名
#配置主机名hostnamectl set-hostname k1#增加hosts172.16.8.49 k1172.16.8.50 k2#创立用户groupadd dockeruseradd -g docker rkepasswd rke#配置互信ssh-keygenssh-copy-id rke@k2
  • 装置docker
yum install dockersystemctl start docker
  • 装置kubernetes
# 下载rkehttps://github.com/rancher/rke/releases# cluster.ymlnodes:- address: k1  internal_address: k1  role: [controlplane,etcd]  hostname_override: k1  user: rke- address: k2  internal_address: k2  role: [worker]  hostname_override: k2  user: rkeservices:  kubelet:    extra_args:      max-pods: "10000"  kube-api:    service_node_port_range: "1-65535"authentication:  strategy: x509authorization:  mode: rbac# 执行rke up./rke_linux-amd64 up
  • 装置kubectl
wget https://storage.googleapis.com/kubernetes-release/release/v1.20.5/bin/linux/amd64/kubectlinstall -o rke -g docker -m 0755 kubectl /usr/local/bin/kubectl$ kubectl get nodesNAME   STATUS   ROLES               AGE     VERSIONk1     Ready    controlplane,etcd   6m53s   v1.20.8k2     Ready    worker              6m52s   v1.20.8
  • 装置rancher
docker run --privileged -d --restart=unless-stopped -p 8080:80 -p 4443:443 rancher/rancher

kubernetes架构

总架构

组件

  • master:k8s主节点,负责资源的治理和利用的调度,通常不部署利用
  • node:k8s从节点,负责运行利用
  • API Server:资源管制接口,对k8s所有资源的操作都须要通过API Server,API Server只运行在master节点
  • etcd:集群数据库,集群的数据都保留在etcd中,etcd是一个高性能分布式key-value数据库
  • kubelet:kubelet运行在每一个node上的,负责接管master的调度信息,将利用部署到node上,并且定时上报利用到状态和节点状态为master调度决策提供参考数据
  • kube-proxy:你能够了解为一个nginx(实际上底层实现就是用nginx),负责利用的代理
  • Kube scheduler:调度器,负责决定pod运行在哪个节点上
  • controller manager:保障利用能达到用户冀望程度,比方利用设置正本为2,那么controller确保该利用随时随刻都有2个正本

这些组件确保了利用在kubernetes上

  • 最正当的部署
  • 挂了能重启
  • 资源不够可扩大

罕用资源

POD

  • Kubernetes 治理的最小单元
  • 一个POD就是一个docker的镜像实例
  • 所有的kuberntes操作都是围绕POD进行的
  • 如果把docker治理的利用形容为乌合之众,一群散兵,那么kubernetes就是正规军,有制度,有阶层,有分工,
  • 够把这群散兵治理好,一个POD就是一个散兵,不论分工再怎么简单,命令终归是要达到底层。
  • 能够定义各种策略,比方必须保障同一时间多少个POD运行,保障利用“死不了”,kubernetes会主动部署新的POD以满足要求

Deployment

  • 多个POD汇合
  • 能够了解为一个零碎所需所有POD的汇合
  • 方便管理,能够对一组POD进行操作

Service

  • 对外提供对立地址
  • 作用相似于Nginx,能够作为反向代理

ConfigMap

  • 零碎的配置文件
  • 不同环境间应该仅有配置环境的差别,configmap的设计实现了以上指标
  • configmap最终是给POD应用的

Ingress

  • 使利用能通过域名进行路由,作用相似nginx servername

kubectl根本命令

# 查看所有节点kubectl get nodesNAME   STATUS   ROLES               AGE   VERSIONk1     Ready    controlplane,etcd   22h   v1.20.8k2     Ready    worker              22h   v1.20.8# 查看所有的podkubectl get pods --all-namespaces# 查看指定命名空间下podkubectl get pods -n poc# 获取所有资源kubectl get all --all-namespaces# 获取所有反对的资源类型kubectl api-resources

一个spring boot部署示例

代码

@RestController@RequestMapping("example")public class ExampleController {    @GetMapping("header")    public Map header(HttpServletRequest request) {        Enumeration<String> headerNames = request.getHeaderNames();        Map<String, String> headers = new HashMap<>();        while (headerNames.hasMoreElements()) {            String name = headerNames.nextElement();            headers.put(name, request.getHeader(name));        }        return headers;    }}

代码逻辑很简略,将申请头信息全副打印进去,没有数据库连贯

Dockerfile

FROM openjdk:8-jdk-alpineMAINTAINER definesys.comVOLUME /tmpADD kubernetes-demo-1.0-SNAPSHOT.jar app.jarRUN echo "Asia/Shanghai" > /etc/timezoneEXPOSE 8080ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-Dfile.encoding=UTF-8","-Duser.timezone=Asia/Shanghai", "-jar","app.jar"]

构建镜像

docker build -t 172.16.81.92:8000/poc/kubernetes-example:v1.0 .docker push 172.16.81.92:8000/poc/kubernetes-example:v1.0

Deployment

apiVersion: apps/v1kind: Deploymentmetadata:  labels:    app: kubernetes-example    tier: backend  name: kubernetes-example  namespace: pocspec:  replicas: 1  selector:    matchLabels:      app: kubernetes-example      tier: backend  strategy:    rollingUpdate:      maxSurge: 25%      maxUnavailable: 25%    type: RollingUpdate  template:    metadata:      labels:        app: kubernetes-example        tier: backend    spec:      containers:        - image: 172.16.81.92:8000/poc/kubernetes-example:v2.0          imagePullPolicy: IfNotPresent          name: kubernetes-example          ports:            - containerPort: 8080              protocol: TCP          resources: {}      dnsPolicy: ClusterFirst      schedulerName: default-schedulerstatus: {}

通过kubectl进行部署

kubectl apply -f app-deployment.yml

如果只有deployment的话,只能在容器内进行拜访,咱们能够启动一个busybox的容器,在该容器内用curl命令进行测试

➜ curl  http://10.42.1.30:8080/example/header{"host":"10.42.1.30:8080","user-agent":"curl/7.30.0","accept":"*/*"}
busybox是一些linux工具集的容器

这种拜访形式存在很多问题

  • 在k8s中,ip是变动的,因为k8s会依据环境的变动对利用进行调度,所以就算你不降级,利用也可能会被重新部署
  • 单点问题,如果有多个正本就无奈通过单个利用ip进行拜访
  • 负载问题,如果有多个正本就必然面临负载问题

那么解决下面这些问题的计划就是Service

Service

在传统开发中,咱们如果部署了多个正本,也就是集群,那么咱们会在集群后面部署一个反向代理,比方nginx,不仅能够做代理还能够做负载平衡,在k8s中,咱们不须要独自再去搭建这么一个反向代理服务器和负载平衡,k8s中的Service资源就能够实现需求。

apiVersion: v1kind: Servicemetadata:  name: kubernetes-example-svc  namespace: pocspec:  ports:    - name: app-port      port: 8080      protocol: TCP      targetPort: 8080  selector:    app: kubernetes-example    tier: backend  type: ClusterIP

通过kubectl进行部署

kubectl apply -f app-svc.yaml

留神到Service的类型type: ClusterIP表明这还是一个集群外部的ip,无奈通过内部进行拜访,但这个Service解决了负载平衡和反向代理的问题,在其余利用中能够通过名称service名称进行拜访,比方

➜ curl http://kubernetes-example-svc:8080/example/header{"host":"kubernetes-example-svc:8080","user-agent":"curl/7.30.0","accept":"*/*"}

那如何解决内部拜访的问题呢,有两种计划,咱们先介绍第一种

apiVersion: v1kind: Servicemetadata:  name: kubernetes-example-svc  namespace: pocspec:  ports:    - name: app-port      port: 8080      protocol: TCP      targetPort: 8080      nodePort: 18080  selector:    app: kubernetes-example    tier: backend  type: NodePort

NodePort类型的Service能够在主机上间接开始口,作用相似docker中的-p参数,通过指定nodeport能够在内部通过主机ip:nodePort形式进行拜访

➜ curl http://k2:18080/example/header{"host":"k2:18080","user-agent":"curl/7.29.0","accept":"*/*"}

有同学可能会问,如果有多台主机,是不是还要做一层负载,答案是的,你能够用F5也能够用nginx,上面介绍另外一种形式Ingress

Ingress

通过nodePort裸露服务的形式存在几个重大的问题

  • 端口难以治理,如果服务多,每个服务一个端口,还要自行保护端口和服务的映射关系,十分麻烦
  • 通过在主机上挖孔形式,在实现上就不优雅

为了解决NodePort的问题,于是有了Ingress,Ingress通过域名进行路由,也就是能够给每个service指定域名,通过域名进行拜访就能路由到相应的service上,所有的流量都是通过kube-proxy进行转发,因而无需在主机上开始口

apiVersion: networking.k8s.io/v1beta1kind: Ingressmetadata:  annotations:    nginx.ingress.kubernetes.io/proxy-body-size: 500m    nginx.ingress.kubernetes.io/proxy-connect-timeout: "300"    nginx.ingress.kubernetes.io/proxy-read-timeout: "600"    nginx.ingress.kubernetes.io/proxy-send-timeout: "600"  name: kubernetes-example-svc-ingress  namespace: pocspec:  rules:    - host: kubernetes-example.definesys.com      http:        paths:          - backend:              serviceName: kubernetes-example-svc              servicePort: app-port

kubernetes-example.definesys.com指定了域名,通过serviceName匹配service名称,通过Ingress咱们能够间接用域名拜访,当然,这个域名须要加到dns零碎中,如果有多个节点,能够做dns负载也能够再做一层nginx负载,如果是本人本地测试,能够间接配置hosts文件就能够间接拜访

ConfigMap

ConfigMap是kubernetes中十分蠢才的设计方案,介绍ConfigMap之前咱们想一个问题生产环境的war包和测试环境的war包区别在什么,答案是配置文件,配置文件决定了利用的环境属性,利用能够分为两局部,一部分是程序,一部分是配置文件,依照docker的准则,测试和生产只能是配置文件不一样,程序应该是齐全一样的,也就是说,同一个镜像,在测试环境运行的时候应该用的是测试环境的配置文件,在生产环境运行时应该用的是生产环境的配置文件,如果是docker,咱们能够通过挂载卷实现

#测试docker run -v /data/dev/application.properties:/u01/webapps/application.properties -d app#生产docker run -v /data/prod/application.properties:/u01/webapps/application.properties -d app

在kubernetes中,能够通过ConfigMap实现,ConfigMap也是一个key-value构造的文件

一个jenkins的configmap文件局部内容

apiVersion: v1data:  apply_config.sh: |-    mkdir -p /usr/share/jenkins/ref/secrets/;    echo "false" > /usr/share/jenkins/ref/secrets/slave-to-master-security-kill-switch;    cp -n /var/jenkins_config/config.xml /var/jenkins_home;    cp -n /var/jenkins_config/jenkins.CLI.xml /var/jenkins_home;    cp -n /var/jenkins_config/hudson.model.UpdateCenter.xml /var/jenkins_home;  config.xml: |-    <?xml version='1.0' encoding='UTF-8'?>    <hudson>      <disabledAdministrativeMonitors/>      <version></version>      <numExecutors>0</numExecutors>      <mode>NORMAL</mode>      ....  hudson.model.UpdateCenter.xml: |-    <?xml version='1.1' encoding='UTF-8'?>    <sites>      <site>        <id>default</id>        <url>https://updates.jenkins.io/update-center.json</url>      </site>    </sites>  jenkins.CLI.xml: |-    <?xml version='1.1' encoding='UTF-8'?>    <jenkins.CLI>      <enabled>false</enabled>    </jenkins.CLI>  plugins.txt: ""kind: ConfigMapmetadata:  name: jenkins  namespace: poc

data局部定义的就是ConfigMap的数据局部,留神到这个文件data中的key都是文件名,是的,每个key都能够以文件的模式挂载到容器内,文件的内容就是value,咱们将之前的代码批改下

@RestController@RequestMapping("example")public class ExampleController {    @Value("${kubernetes.demo.env.name:}")    private String envName;    @GetMapping("header")    public Map header(HttpServletRequest request) {        Enumeration<String> headerNames = request.getHeaderNames();        Map<String, String> headers = new HashMap<>();        while (headerNames.hasMoreElements()) {            String name = headerNames.nextElement();            headers.put(name, request.getHeader(name));        }        headers.put("envName", envName);        return headers;    }}

代码注入了配置文件里的配置我的项目kubernetes.demo.env.name

  • 筹备configMap.yaml
apiVersion: v1data:  application.properties: kubernetes.demo.env.name=devkind: ConfigMapmetadata:  name: example-configmap  namespace: poc
  • kubectl导入到kubernetes中
kubectl apply -f configMap.yaml
  • 批改利用的Dockerfile抉择从指定门路下读取配置文件
FROM openjdk:8-jdk-alpineMAINTAINER definesys.comVOLUME /tmpADD kubernetes-demo-1.0-SNAPSHOT.jar app.jarRUN echo "Asia/Shanghai" > /etc/timezoneEXPOSE 8080ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-Dfile.encoding=UTF-8","-Duser.timezone=Asia/Shanghai", "-Dspring.config.location=/app/", "-jar","app.jar"]

减少了--spring.config.location=/u01/config/application.properties启动参数

从新构建镜像

docker build -t 172.16.81.92:8000/poc/kubernetes-example-configmap:v1.0 .
  • 批改Deployment,将ConfigMap挂载到容器内
apiVersion: apps/v1kind: Deploymentmetadata:  labels:    app: kubernetes-example    tier: backend  name: kubernetes-example  namespace: pocspec:  replicas: 1  selector:    matchLabels:      app: kubernetes-example      tier: backend  strategy:    type: RollingUpdate  template:    metadata:      labels:        app: kubernetes-example        tier: backend    spec:      containers:        - image: 172.16.81.92:8000/poc/kubernetes-example-configmap:v1.0          imagePullPolicy: IfNotPresent          name: kubernetes-example          volumeMounts:            - mountPath: /app/              name: configmap-data          ports:            - containerPort: 8080              protocol: TCP          resources: {}      dnsPolicy: ClusterFirst      volumes:        - name: configmap-data          configMap:            name: example-configmap      schedulerName: default-schedulerstatus: {}

Jenkins

一个利用的部署须要至多筹备以下几个yaml配置文件

  • Deployment
  • Service
  • Ingress

如果是手写不仅效率低,易于出错,也难以治理,也不倡议将配置文件放在我的项目中,起因有以下几个

  • 每个我的项目都须要copy一份配置进行批改,繁琐
  • 如果不懂k8s会减少其学习老本
  • 如果k8s降级,配置文件有可能须要跟着变动,就须要批改所有的配置文件

咱们须要一个工具帮忙咱们实现配置文件的生成并且部署到kubernetes环境中,Jenkins能够帮忙咱们实现一系列的自动化操作

#!/bin/bashset -ev_app_name=kubernetes-examplev_module=.v_app_host=kubernetes-example.definesys.comv_k8s_namespace=poc#v_app_name=$appName#v_module=$module#v_app_host=${v_app_name}.fuyaogroup.com#v_k8s_namespace='fone-application'v_app_version=`date +"%Y%m%d%H%M%S"`v_harbor_prefix='172.16.81.92:8000/poc/'if [ "$v_app_host" == "" ]; then  v_app_host=${v_app_name}.definesys.comfiecho "app name    ====>"$v_app_nameecho "app version ====>"$v_app_versionecho "module      ====>"$v_moduleecho "workspace   ====>"$WORKSPACEecho "profile     ====>"$v_profileecho "app host    ====>"$v_app_hostv_workspace=$WORKSPACE/workspacemkdir -p $v_workspacecd $v_modulemvn clean package -Dmaven.test.skip#长期寄存jar包目录v_build_directory_name=buildv_build_directory=$v_workspace/$v_build_directory_namev_app_jar=target/*.jarv_app_jar=`basename target/*.jar`rm -rf $v_build_directorymkdir -p $v_build_directorycp -rf target/$v_app_jar $v_build_directorycd $v_build_directory#v_app_name=${v_app_jar%.*}#v_app_name=${v_app_name%-*}echo "app jar name  =====>"$v_app_jarecho "app name    =====>"$v_app_name#docker镜像构建v_image_tag=$v_harbor_prefix$v_app_name:$v_app_versioncat 1>Dockerfile <<EOFFROM openjdk:8-jdk-alpineMAINTAINER definesys.comVOLUME /tmpADD $v_app_jar app.jarRUN echo "Asia/Shanghai" > /etc/timezoneEXPOSE 8080ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-Dfile.encoding=UTF-8","-Duser.timezone=Asia/Shanghai", "-jar","app.jar"]EOFdocker build -t $v_image_tag . -f Dockerfiledocker push $v_image_tagdocker rmi -f $v_image_tag#部署kubernetescat 1>app-deployment.yaml <<EOFapiVersion: apps/v1kind: Deploymentmetadata:  generation: 1  labels:    app: $v_app_name    tier: backend  name: $v_app_name  namespace: $v_k8s_namespacespec:  progressDeadlineSeconds: 600  replicas: 1  revisionHistoryLimit: 2  selector:    matchLabels:      app: $v_app_name      tier: backend  strategy:    rollingUpdate:      maxSurge: 25%      maxUnavailable: 25%    type: RollingUpdate  template:    metadata:      creationTimestamp: null      labels:        app: $v_app_name        tier: backend    spec:      containers:        - image: $v_image_tag          imagePullPolicy: IfNotPresent          name: $v_app_name          ports:            - containerPort: 8080              protocol: TCP          resources: {}          terminationMessagePath: /dev/termination-log          terminationMessagePolicy: File      dnsPolicy: ClusterFirst#      dnsConfig:#        searches:#          - k2-infrastructure.svc.cluster.local      restartPolicy: Always      imagePullSecrets:      - name: harbor-registry      schedulerName: default-scheduler      securityContext: {}      terminationGracePeriodSeconds: 30status: {}---apiVersion: v1kind: Servicemetadata:  creationTimestamp: null  name: ${v_app_name}-svc  namespace: $v_k8s_namespacespec:  ports:    - name: app-port      port: 8080      protocol: TCP      targetPort: 8080  selector:    app: $v_app_name    tier: backend  sessionAffinity: None  type: ClusterIPstatus:  loadBalancer: {}---apiVersion: networking.k8s.io/v1beta1kind: Ingressmetadata:  annotations:    nginx.ingress.kubernetes.io/proxy-body-size: 500m    nginx.ingress.kubernetes.io/proxy-connect-timeout: "300"    nginx.ingress.kubernetes.io/proxy-read-timeout: "600"    nginx.ingress.kubernetes.io/proxy-send-timeout: "600"  creationTimestamp: null  generation: 1  name: ${v_app_name}-svc-ingress  namespace: $v_k8s_namespacespec:  rules:    - host: $v_app_host      http:        paths:          - backend:              serviceName: ${v_app_name}-svc              servicePort: app-portstatus:  loadBalancer: {}EOFkubectl apply -f app-deployment.yaml --record