开源工具集Carvel在CI/CD流水线中的集成
CI/CD流水线中使用VMware开源工具集Carvel
Carvel项目简介
Carvel项目的主页: https://carvel.dev/
简介
一套适用于开发者和平台运维者构建;分发;安装;管理K8s上容器化应用的工具集. 遵循Unix哲学[1] Make each program do one thing well.
工具集中包含但不限于下图几款软件:
它们分别在应用发布的生命周期中发挥着不同的作用:
Carvel每个工具执行一项特定的功能, 这样使用者就可以决定在哪个阶段用哪个工具做哪个任务. 而不是做成一个单体式多功能的工具(如Helm). 这里对工具的使用并没有好坏之分, 只要适用于用户的环境和用户的DevSecOps文化的就是好的工具.
下文中我会对Carvel中的ytt; kbld以及kapp做进一步的介绍, 分别展示其功能及特性. 最后在一个Gitlab CI中将这些工具串联起来, 打包一个Java Spring程序源码成容器镜像, 部署进Tanzu K8s集群.
YTT
简介 这工具名字取得...emmmm, 我心想就工程师就这么直男吗, 八成是什么Yaml Template Tool啥的. 结果在一次Tanzu总工的视频上听到这ytt原意来自钇[2]元素(自以为不直男的一次知识点的炫技).
官方文档对ytt[3]有详细的介绍, 我在这收敛一下重点, 然后再通过几个列子演示一下.
ytt识别近乎所有主流的Yaml配置(K8s Configuration, Concourse Pipeline, Docker Compose, GitHub Action workflow...) Yaml进, Yaml出 类Python语法
一张ytt如何工作的示意图, 感觉不是很直观, 上用例.
用例 我们用一个例子来展示ytt的多种功能, 内置变量, 外置变量, 简单数学公式, 覆盖变量. 首先我们制作一个K8s Deployment和Service的Yaml模版, 起名Deployment.yml:
#@ load("@ytt:data", "data")
#@ def labels():
app: "spring-demo"
team: "dev"
#@ end
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-sample-demo
labels: #@ labels()
spec:
replicas: #@ data.values.replicas / 2
selector:
matchLabels: #@ data.values.labels1
template:
metadata:
labels: #@ data.values.labels1
spec:
containers:
- image: #@ data.values.image1
name: spring-sample-app
ports:
- containerPort: #@ data.values.app_port
name: http
---
apiVersion: v1
kind: Service
metadata:
name: spring-sample-demo-svc
spec:
selector:
app: spring-demo
type: NodePort
ports:
- port: #@ data.values.svc_port
targetPort: #@ data.values.app_port
nodePort: 30028
#@ load("@ytt:data", "data")
是告诉ytt加载外置变量来自values.yml.#@ def labels():
是Yaml模版内变量, 定义了两个标签app: "spring-demo", team: "dev"
在有效K8s字段中,我们让Deployment的标签来自内置变量labels(), 而ReplicaSet和Selector标签从外置变量values.yml中读取labels1. 副本数量replicas: #@ data.values.replicas / 2
做一个简单的除法. 其他的变量的Patch原则如出一辙.
再看values.yml:
以#@data/values
, 告诉ytt渲染成values. 类似Helm中的values, 作为运维者或者流水线制定者在该文件中通过注释让开发者填写与应用相关的参数. 真正部署到K8s里的配置文件通过ytt渲染生成.
#@data/values
---
replicas: 8 #需要部署几个实例
svc_port: 80 #集群内暴露的服务端口
app_port: 8080 #应用监听的端口
image1: "" #实例的容器镜像名字
# 一些需要的标签
labels1:
app: "spring-demo"
cluster: "tce-mc"
这里我故意把values中image1的值设为空, 因为我希望通过覆盖方式填写进去, 比如这个镜像名字是来自CI流水线中的内置变量之类的.
执行 接下来我们在命令行中执行:
# 演示用途, 设置一个环境变量IMAGE1
export IMAGE1=docker.io/rock981119/spring-sample:ci-main
# 执行ytt
ytt -f Deployment.yml -f values.yml -v image1=$IMAGE1
通过-v image1=$IMAGE1
覆盖掉values.yml里的image1的值, 那么总体的输出结果为:
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-sample-demo
labels:
app: spring-demo
team: dev
spec:
replicas: 4
selector:
matchLabels:
app: spring-demo
cluster: tce-mc
template:
metadata:
labels:
app: spring-demo
cluster: tce-mc
spec:
containers:
- image: docker.io/rock981119/spring-sample:ci-main
name: spring-sample-app
ports:
- containerPort: 8080
name: http
---
apiVersion: v1
kind: Service
metadata:
name: spring-sample-demo-svc
spec:
selector:
app: spring-demo
type: NodePort
ports:
- port: 80
targetPort: 8080
nodePort: 30028
这样不管是输出成文件还是通过管道符交给Kubectl apply -f-
执行都是可以的. 通过这个例子我们简单小结一下, ytt懂得Yaml格式, 不同于Shell中使用sed或其他模版语言只能替换固定位置的变量. 语法也比较精简, 有Python基础即可.
Python语法渲染这个点是ytt重点优点里我最喜欢的, 让我这种只会点Python的人很快就能用起来. 其次就是现在的工具实在是太多了, 考虑学习成本迫使我学习心态变得更功利. 例如之前学习的Terraform的时候, 我就很反感HashiCorp非要再搞出个HCL的语法. 学OPA的时候要学习Rego语法... 而这些语法除了这些独立的场景外别的地方都用不着. 官网有ytt对比其他模版工具的观点[4], 欢迎查阅.
如果是自定义的K8s CRD, ytt也可以通过写Schema来渲染你要的Yaml层级, 篇幅问题就不演示更多的例子了, 更多更详细的说明在官网的文档中都有用例.
KBLD
简介kbld[5] (pronounced: kei·bild):
镜像构建(委派 Docker, pack, kubectl-buildkit等工具)和推送;搬迁的编排工具 解析Yaml中镜像的摘要, 渲染出新的Yaml将镜像的值替换成该镜像的摘要(而不是某个tag或latest), 确保调用的镜像是不可变的
用例1 在Kubernetes安全原则中经常提及到: 永远不要使用image:latest. 其实就算你使用的是image:tag, 你也不能保证相同的tag的镜像已经被更改. 主流镜像仓库(如DockerHub; Harbor)支持引用镜像时采用哈希值摘要.
即便镜像的便签不变, 只要构建时任何一层发生变化, 其哈希摘要都会发生变化. 刚才ytt渲染后的Yaml已经是我在Docker Hub上真实上传的镜像名+Tag. 我们看看ytt渲染后的Yaml再交给kbld渲染一次会如何.
执行: ytt -f Deployment.yml -f values.yml -v image1=$IMAGE1 | kbld -f -
resolve | final: docker.io/rock981119/spring-sample:ci-main -> index.docker.io/rock981119/spring-sample@sha256:e901302da31edf61cbaec68df230e8b0f5cc33932d43337f9dac8a548ff23b7e
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kbld.k14s.io/images: |
- origins:
- resolved:
tag: ci-main
url: docker.io/rock981119/spring-sample:ci-main
url: index.docker.io/rock981119/spring-sample@sha256:e901302da31edf61cbaec68df230e8b0f5cc33932d43337f9dac8a548ff23b7e
# 省略
spec:
containers:
- image: index.docker.io/rock981119/spring-sample@sha256:e901302da31edf61cbaec68df230e8b0f5cc33932d43337f9dac8a548ff23b7e
#省略
---
apiVersion: v1
kind: Service
#省略
Succeeded
kbld只能解析到镜像仓库可达并正确的镜像名, 比如你自己瞎写一个或者只是本地的镜像它则不能解析了. 除了解析镜像的哈希摘要外, kbld可以同时委派Docker或者Pack来构建镜像, 你也可以一步把构建的镜像再推到仓库里.
用例2 构建镜像并推送至仓库, 文件名kbld.yml
---
apiVersion: kbld.k14s.io/v1alpha1
kind: Config
sources:
- image: docker.io/rock981119/spring-sample:ci-main
path: apps/java-maven/
pack:
build:
builder: paketobuildpacks/builder:tiny
---
apiVersion: kbld.k14s.io/v1alpha1
kind: Config
destinations:
- image: docker.io/rock981119/spring-sample:ci-main
newImage: docker.io/rock981119/spring-sample
tags: [latest, ci-main]
sources
配置就是告诉kbld委派Pack构建本地镜像docker.io/rock981119/spring-sample:ci-main
, path
告诉Pack源码的位置, 你可以单独指定builder
. 如果你的Pack设置了默认的builder也可以不指定. 关于kbld委派其他的构建工具可以参考官网文档[6].
destinations
告诉kbld将哪个本地镜像重命名并推送至仓库, 推送时可以加若干标签, 他们的哈希摘要都是一致的.
kbld不能直接对kbld.yml生效, 因为它的目的本身并不在于构建镜像和推送, 而是摄取正确的镜像哈希摘要填入到需要部署的Yaml中去.
承接上一个ytt的用例, 我们继续联合两个工具一起工作, ytt渲染配置, kbld委派构建镜像并推送至仓库, 最终渲染出部署Yaml
ytt -f Deployment.yml -f values.yml -v image1=$IMAGE1 | kbld -f kbld.yml -f -
最终输出的Yaml跟kbld的第一个用例是类似的, 但在你的Console中它已经完成了委派镜像构建和推送.
KAPP
ytt和kbld的实质任务都是渲染出最终可用于部署的Yaml配置, 是时候该找个工具部署了. 事实上渲染后的Yaml文件可以直接用kubectl
或者helm
来部署了. 那我们再看看Carvel中的kapp又有什么不同.
这个官网[7]的一句话定义:
Deploy and view groups of Kubernetes resources as "applications". Apply changes safely and predictably, watching resources as they converge.
简单来说当你部署一个Yaml Bundle的时候, 内容里包含了若干CRD, Deployment, Service, Ingress等, 其实它们是同一个应用. 通过kapp部署这个Yaml Bundle, kapp会将它实例化为一个app(自定义名字).
apply阶段识别Yaml中资源的部署先后顺序 apply阶段告知创建了何种配置 diff阶段以git格式告知产生了何种变化 树状展示资源
用例 我们用kapp将ytt和kbld渲染后的Yaml部署到已建好的Tanzu Kubernetes集群当中去:
ytt -f Deployment.yml -f values.yml -v image1=$IMAGE1 | kbld -f kbld.yml -f - | kapp deploy -a spring-demo -c -y -f -
#输出省略
@@ update deployment/spring-sample-demo (apps/v1) namespace: default @@
...
16, 16 - ci-main
17 - url: index.docker.io/rock981119/spring-sample@sha256:feb86d80c50f95d269c6cb331ae3a36f6e3585b04ded0ff9aff1ad65edc974f7
17 + url: index.docker.io/rock981119/spring-sample@sha256:f8fc4021d58af607f5ef15a31091cdc759a2b958981a8fe9792654376e3e39c8
18, 18 creationTimestamp: "2022-01-29T06:28:57Z"
19, 19 generation: 4
...
138,138 containers:
139 - - image: index.docker.io/rock981119/spring-sample@sha256:feb86d80c50f95d269c6cb331ae3a36f6e3585b04ded0ff9aff1ad65edc974f7
139 + - image: index.docker.io/rock981119/spring-sample@sha256:f8fc4021d58af607f5ef15a31091cdc759a2b958981a8fe9792654376e3e39c8
140,140 name: spring-sample-app
141,141 ports:
Changes
Namespace Name Kind Conds. Age Op Op st. Wait to Rs Ri
default spring-sample-demo Deployment 2/2 t 14m update - reconcile ok -
Op: 0 create, 0 delete, 1 update, 0 noop, 0 exists
Wait to: 1 reconcile, 0 delete, 0 noop
#输出省略
因为演示, kapp部署时, -a
为应用实例起名spring-demo
, 我apply了多次, 用例中kapp输出了与上次变更了的内容. kapp ls
罗列当前通过kapp部署的实例. kapp inspect -a spring-demo
树状展示spring-demo
实例的资源关系以及同步状态.
kapp inspect -a spring-demo
Target cluster 'https://192.168.31.100:6443' (nodes: tce-management-control-plane-86792, 1+)
#省略部分告警
Resources in app 'spring-demo'
Namespace Name Kind Owner Conds. Rs Ri Age
default spring-sample-demo Deployment kapp 2/2 t ok - 19m
^ spring-sample-demo-65b8d65957 ReplicaSet cluster - ok - 4m
^ spring-sample-demo-65b8d65957-brmpg Pod cluster 4/4 t ok - 4m
^ spring-sample-demo-65b8d65957-jx2gw Pod cluster 4/4 t ok - 4m
^ spring-sample-demo-65b8d65957-ls2nt Pod cluster 4/4 t ok - 4m
^ spring-sample-demo-65b8d65957-vzn6b Pod cluster 4/4 t ok - 4m
^ spring-sample-demo-667859679 ReplicaSet cluster - ok - 19m
^ spring-sample-demo-6774567466 ReplicaSet cluster - ok - 15m
^ spring-sample-demo-svc Endpoints cluster - ok - 19m
^ spring-sample-demo-svc Service kapp - ok - 19m
^ spring-sample-demo-svc-ncrlx EndpointSlice cluster - ok - 19m
Rs: Reconcile state
Ri: Reconcile information
11 resources
kapp删除实例, 在Op列中可以看到, 实际删除的就是Deployment和Service.
kapp delete -a spring-demo
Changes
Namespace Name Kind Conds. Age Op Op st. Wait to Rs Ri
default spring-sample-demo Deployment 2/2 t 21m delete - delete ok -
^ spring-sample-demo-65b8d65957 ReplicaSet - 7m - - delete ok -
^ spring-sample-demo-65b8d65957-brmpg Pod 4/4 t 7m - - delete ok -
^ spring-sample-demo-65b8d65957-jx2gw Pod 4/4 t 7m - - delete ok -
^ spring-sample-demo-65b8d65957-ls2nt Pod 4/4 t 7m - - delete ok -
^ spring-sample-demo-65b8d65957-vzn6b Pod 4/4 t 7m - - delete ok -
^ spring-sample-demo-667859679 ReplicaSet - 21m - - delete ok -
^ spring-sample-demo-6774567466 ReplicaSet - 17m - - delete ok -
^ spring-sample-demo-svc Endpoints - 21m - - delete ok -
^ spring-sample-demo-svc Service - 21m delete - delete ok -
^ spring-sample-demo-svc-ncrlx EndpointSlice - 21m - - delete ok -
Op: 0 create, 2 delete, 0 update, 9 noop, 0 exists
Wait to: 0 reconcile, 11 delete, 0 noop
Carvel in CI/CD
简介完这三个Carvel的工具后, 我们基本就可以用它们完成一个源码到镜像, 生成配置再到实例化部署的一个过程了. 那么我们把这个过程在CI/CD流水线中实践一下.
源码用的是Buildpack官方的例子Buildpack官方的例子源码
上传本地Gitlab Server. 如果你想安装Gitlab CE私有环境可以考虑我司在Bitnami上打包好的虚拟机镜像Bitnami Gitlab CE OVA[8], 还附有设置文档. 制作Deplpoyment.yml模版, values.yml, kbld.yml 为该项目注册一个Runner, 为了方便选用了Photon Linux, Shell执行模式 构建Gitlab Pipeline文件 验证 拓扑与流程如上图, 下方是我的Gitlab Pipeline配置文件, .gitlab-ci.yml
:
# This file is a template, and might need editing before it works on your project.
# To contribute improvements to CI/CD templates, please follow the Development guide at:
# https://docs.gitlab.com/ee/development/cicd/templates.html
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Getting-Started.gitlab-ci.yml
# This is a sample GitLab CI/CD configuration file that should run without any modifications.
# It demonstrates a basic 3 stage CI/CD pipeline. Instead of real tests or scripts,
# it uses echo commands to simulate the pipeline execution.
#
# A pipeline is composed of independent jobs that run scripts, grouped into stages.
# Stages run in sequential order, but jobs within stages run in parallel.
#
# For more information, see: https://docs.gitlab.com/ee/ci/yaml/index.html#stages
stages: # List of stages for jobs, and their order of execution
- test
- build
- deploy
test:
stage: test
tags:
- linux
script:
- echo "测了, 但没完全测"
build:
stage: build
needs: ["test"]
variables:
IMAGE1: "docker.io/rock981119/spring-sample:ci-main"
tags:
- linux
before_script:
- echo "Before Script For Docker Hub Login"
- echo $DOCKER_PWD | docker login --username $DOCKER_USER --password-stdin
script:
- echo " ----> 使用ytt和kbld渲染Yaml配置"
- echo " kbld委派Pack构建镜像并推送至仓库"
- ytt -f Deployment.yml -f values.yml -v image1=$IMAGE1 | kbld -f kbld.yml -f - >> app.yml
artifacts:
paths:
- app.yml
deploy2tanzu:
stage: deploy
needs: ["build"]
tags:
- linux
when: manual
script:
- echo "使用kapp部署应用实例"
- kapp deploy -a $CI_PROJECT_NAME -c -y -f app.yml
查看一下结果吧
ci-with-carvel [main] kapp ls
Target cluster 'https://192.168.31.100:6443' (nodes: tce-management-control-plane-86792, 1+)
Apps in namespace 'default'
Name Namespaces Lcs Lca
ci-with-carvel default true 1m
tce-repo-ctrl default true 1m
Lcs: Last Change Successful
Lca: Last Change Age
2 apps
Succeeded
ci-with-carvel [main] kapp inspect -a ci-with-carvel
Resources in app 'ci-with-carvel'
Namespace Name Kind Owner Conds. Rs Ri Age
default spring-sample-demo Deployment kapp 2/2 t ok - 34m
^ spring-sample-demo-6798f54d9c ReplicaSet cluster - ok - 2m
^ spring-sample-demo-6798f54d9c-grm5k Pod cluster 4/4 t ok - 1m
^ spring-sample-demo-6798f54d9c-q88tn Pod cluster 4/4 t ok - 1m
^ spring-sample-demo-6798f54d9c-rxr5w Pod cluster 4/4 t ok - 2m
^ spring-sample-demo-6798f54d9c-smf6j Pod cluster 4/4 t ok - 2m
^ spring-sample-demo-746b764476 ReplicaSet cluster - ok - 34m
^ spring-sample-demo-svc Endpoints cluster - ok - 34m
^ spring-sample-demo-svc Service kapp - ok - 34m
^ spring-sample-demo-svc-8md44 EndpointSlice cluster - ok - 34m
Rs: Reconcile state
Ri: Reconcile information
10 resources
kubectl get all
NAME READY STATUS RESTARTS AGE
pod/spring-sample-demo-6798f54d9c-grm5k 1/1 Running 0 2m26s
pod/spring-sample-demo-6798f54d9c-q88tn 1/1 Running 0 2m28s
pod/spring-sample-demo-6798f54d9c-rxr5w 1/1 Running 0 2m42s
pod/spring-sample-demo-6798f54d9c-smf6j 1/1 Running 0 2m43s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 100.64.0.1 443/TCP 4d2h
service/spring-sample-demo-svc NodePort 100.65.232.184 80:30028/TCP 35m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/spring-sample-demo 4/4 4 4 35m
NAME DESIRED CURRENT READY AGE
replicaset.apps/spring-sample-demo-6798f54d9c 4 4 4 2m43s
replicaset.apps/spring-sample-demo-746b764476 0 0 0 35m
目标达到了,看起来不错.
才疏学浅,只把玩了一下Carvel Toolset的小功能, 希望对大家DevSecOps的日常有帮助, 多多支持与关注我们VMware的开源项目.
结语
结语就不上什么价值观了, 这一年比较懒, 没输出啥内容, 希望明年能更努力. 最后祝大家虎年行大运, 走出一个虎虎生风.
参考资料
Unix philosophy: https://en.wikipedia.org/wiki/Unix_philosophy
[2]Yttrium: https://en.wikipedia.org/wiki/Yttrium
[3]About ytt: https://carvel.dev/ytt/docs/latest/
[4]ytt-vs-x: https://carvel.dev/ytt/docs/v0.38.0/ytt-vs-x/
[5]kbld: https://carvel.dev/kbld/docs/latest/
[6]kbld委派其他的构建工具: https://carvel.dev/kbld/docs/v0.32.0/config/
[7]kapp: https://carvel.dev/kapp/
[8]Bitnami Gitlab CE OVA: https://bitnami.com/stack/gitlab