Skip to main content

· 阅读需要 1 分钟

介绍

Sidecar 北京土话叫三蹦子,通俗叫就是带棚子的三轮摩托车。今天我们要聊的 K8S 中三蹦子也称为边三轮车:边三轮车是在摩托车边上挂靠一个拖斗,云原生中的叫法是主容器和边容器。本系列文章将展示 Sidecar 模式的用法,以及如何通过 KCL 等面向配置的编程语言来简化 YAML 的编写。

一个最简单的云原生 Web 服务

首先以最简方式在 Kubernetes 环境启动一个 Web 服务。在下面 pod.yaml 文件中定义一个 Pod,其中只有一个 Nginx 服务,在 80 端口启动一个 web 服务。

apiVersion: v1
kind: Pod
metadata:
name: web-app
spec:
containers:
- image: nginx
name: main-container
ports:
- containerPort: 80

Pod 是云原生中的一个基础原语。Pod 将多个容器包装为一个逻辑单元,Kubernetes 运行时确保 Pod 中的容器运行在一个机器上。因此 Pod 中的所有容器都共享生命周期、共享磁盘卷、共享网络环境等。Sidecar 模式就是在 Pod 中增加其他容器来扩展和增强主容器的能力。

然后通过 kubectl create 命令行工具创建 Pod,然后通过 kubectl get po 查看 Pod 执行状态:

$ kubectl create -f pod.yaml
$ kubectl get po
NAME READY STATUS RESTARTS AGE
web-app 1/1 Running 0 45m

可以看到一个名为 web-appPod 已经正常启动并运行,其中包含 Nginx 服务。为了便于外部访问配置端口转发,将宿主的 3999 端口对应到主容器的 80 端口:

$ kubectl port-forward web-app 3999:80
Forwarding from 127.0.0.1:3999 ->80
Forwarding from [::1]:3999 -> 80

端口转发是一个阻塞程序,保持命令行窗口打开。然后在浏览器打开测试页面:

通过 Sidecar 定扩展页面内容

现在我们尝试在不修改原始 Nginx 容器镜像的前提下,通过 Sidecar 模式为 Nginx 服务增加定制 Web 页面的能力。在开始前先删除之前启动的 Pod

$ kubectl delete po web-app
pod "web-app" deleted

然后在 Pod 中增加第二个 Busybox Sidecar 容器,完整的 pod.yaml 文件如下:

apiVersion: v1
kind: Pod
metadata:
name: web-app
spec:
containers:
- image: nginx
name: main-container
ports:
- containerPort: 80

# --- 以下是新添加的内容 ---

# 和 Sidecar 通过 磁盘卷共享要发布的文件目录
volumeMounts:
- name: var-logs
mountPath: /usr/share/nginx/html

# Sidecar 容器
- image: busybox
command: ["/bin/sh"]
args: ["-c", "while true; do echo $(date -u) 'Hi I am from Sidecar container' > /var/log/index.html; sleep 5;done"]
name: Sidecar-container
volumeMounts: var-logs
mountPath: /var/log

# Pod 中全部容器共享磁盘卷
volumes:
- name: var-logs
emptyDir: {}

Busybox Sidecar 容器执行的命令对应以下 Shell 脚本:

while true; do
echo $(date -u) 'Hi I am from Sidecar container' > /var/log/index.html;
sleep 5;
done

Sidecar 容器只有一个功能:每隔 5 秒钟覆盖一次 /var/log/index.html 文件,这个文件刚好对应 Nginx 的服务的首页页面文件。

然后重新启动 Pod,并重新映射本地宿主机端口到容器端口:

$ kubectl create -f pod.yaml
pod/web-app created
$ kubectl port-forward web-app 3999:80
Forwarding from 127.0.0.1:3999 -> 80
Forwarding from [::1]:3999 -> 80

重新打开浏览器后将看到以下页面:

Sidecar 模式的工作原理

简单来说,BusyboxSidecar 容器角色,负责生产首页数据;而 Nginx 是主容器,负责消费 Busybox 生产的主页数据;两个容器通过 var-logs 磁盘卷共享空间。如果以 Go 语言的术语类比,Nginx 是主 GoroutineBusybox 是后台干脏活的 Goroutine,而共享的磁盘卷类似 Channel 的作用。

在这个例子中 Nginx 依然是主容器,Sidecar 容器是 BusyBox。我们还可以挂更多 Sidecar 容器,比如网络、监控、日志等等。

这样就通过 Sidecar 模式,在不修改 Nginx 主容器的前提下,扩展出了网络、监控、日志等能力。

Sidecar 模式的优点

现在容器已经成为一种流行的打包技术,各种不同角色的同学可以通过容器以统一的方式构建、发布和运行程序,甚至管理各种资源。因此容器更像一个功能明确的产品,它有自己的运行时、发布周期、文档和 API 等。好的容器/产品只负责解决一个问题,保持了 KISS 原则可以让容器本身具有极高的重用性和可被替代性。正是因为可重用才使得现代化的构建程序的流程更加敏捷和高效。但是可复用的容器一般都功能单一,我们常常需要通过各种手段扩展容器的功能,以及需要更多的容器之间的协同。

三蹦子 Sidecar 可以在不改造主摩托车的前提下增加 N 个拖车功能,相应地云原生 Sidecar 模式可以在无需修改主容器的前提下扩展并增强已有主容器功能。如果将云原生的玩法和面向对象编程联系起来,容器镜像就是 Java 中的 class,而执行中的容器就是 class 的实例。而面向对象的 class 继承就是基于已有的容器镜像做扩展,Sidecar 则是通过类似组合的模式扩展 class 的能力。

面向对象编程中有一个“组合优于继承,多用组合少用继承”的规则,因此 Sidecar 也是推荐使用的模式。正是因为三蹦子模式的优点,最近在云原生场景也被大量使用:比如在边车上架一些类似机关枪的网络服务、监控、跟踪等功能。

总结

这一篇文章我们简要介绍并在 Kubernetes 环境展示了 Sidecar 模式,同时结合传统的面向对象编程思想对比了 Sidecar 和组合编程模式的关系。Sidecar 模式的优势不仅仅体现在无害增强主容器,更灵活的是可以在 apply 时动态调整 Sidecar 能力。

在后面的文章中,我们将尝试结合 KCL 等现代化的云原生配置语言来简化 Sidecar 配置的编写。通过尝试探索通过 KCL 动态注入和修改 Sidecar 来扩展基于已有配置的能力。

· 阅读需要 1 分钟

简介

Helm 是一个为 Kubernetes 对象生成可部署清单的工具,它承担了以两种不同形式生成最终清单的任务。Helm 是一个管理 Kubernetes 包(称为 charts)的必备模板工具。图表是 YAML 清单的模板化版本,其中混合了 Go template 的子集,它也是 Kubernetes 的包管理器,可以打包、配置和部署/应用 Helm 图表到 Kubernetes 集群。

在 KCL 中,用户可以使用更多的工具和 IDE 插件支持直接编写配置代码文件,而不是模板文件,这些工具和插件支持需要在相应位置的代码中进行修改,从而消除了读取 YAML 的成本。同时,用户可以通过代码重用配置片段,避免了YAML 配置的大量复制和粘贴。信息密度更高,更不容易出错。

下面以一个经典的 Helm Chart 配置管理的例子详细说明 Kustomize 和 KCL 在 Kubernetes 资源配置管理上的区别。

Helm

Helm 具备 values.yamltemplate 的概念, 通常一个 Helm Chart 由一个包含 Chart.yaml 的路径组成。我们可以执行如下命令获得一个典型的 Helm Chart 工程

  • 创建 workload-helm 目录来保存 chart 工程
# Create a directory to hold the chart project
mkdir workload-helm
# Create a workload-helm/Chart.yaml
cat <<EOF > workload-helm/Chart.yaml
apiVersion: v2
appVersion: 0.3.0
description: A helm chart to provision standard workloads.
name: workload
type: application
version: 0.3.0
EOF
# Create a workload-helm/values.yaml
cat <<EOF > workload-helm/values.yaml
service:
type: ClusterIP
ports:
- name: www
protocol: TCP
port: 80
targetPort: 80

containers:
my-container:
image:
name: busybox:latest
command: ["/bin/echo"]
args:
- "-c"
- "Hello World!"
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
EOF
  • 创建模版文件夹
# Create a directory to hold templates
mkdir workload-helm/templates
# Create a workload-helm/templates/helpers.tpl
cat <<EOF > workload-helm/templates/helpers.tpl
{{/*
Expand the name of the chart.
*/}}
{{- define "workload.name" -}}
{{- default .Release.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "workload.fullname" -}}
{{- \$name := default .Chart.Name .Values.nameOverride }}
{{- if contains \$name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name \$name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "workload.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "workload.labels" -}}
helm.sh/chart: {{ include "workload.chart" . }}
{{ include "workload.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "workload.selectorLabels" -}}
app.kubernetes.io/name: {{ include "workload.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
EOF
cat <<EOF > workload-helm/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "workload.name" . }}
labels:
{{- include "workload.labels" . | nindent 4 }}
spec:
selector:
matchLabels:
{{- include "workload.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "workload.selectorLabels" . | nindent 8 }}
spec:
containers:
{{- range \$name, \$container := .Values.containers }}
- name: {{ \$name }}
image: "{{ $container.image.name }}"
{{- with \$container.command }}
command:
{{- toYaml \$container.command | nindent 12 }}
{{- end }}
{{- with \$container.args }}
args:
{{- toYaml \$container.args | nindent 12 }}
{{- end }}
{{- with \$container.env }}
env:
{{- toYaml \$container.env | nindent 12 }}
{{- end }}
{{- with \$container.volumeMounts }}
volumeMounts:
{{- toYaml \$container.volumeMounts | nindent 12 }}
{{- end }}
{{- with \$container.livenessProbe }}
livenessProbe:
{{- toYaml \$container.livenessProbe | nindent 12 }}
{{- end }}
{{- with \$container.readinessProbe }}
readinessProbe:
{{- toYaml \$container.readinessProbe | nindent 12 }}
{{- end }}
{{- with \$container.resources }}
resources:
{{- toYaml \$container.resources | nindent 12 }}
{{- end }}
{{- end }}
EOF
cat <<EOF > workload-helm/templates/service.yaml
{{ if .Values.service }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "workload.name" . }}
labels:
{{- include "workload.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
selector:
{{- include "workload.selectorLabels" . | nindent 4 }}
{{- with .Values.service.ports }}
ports:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
EOF

可以得到如下的 Helm chart 工程

.
├── Chart.yaml
├── templates
│ ├── _helpers.tpl
│ ├── deployment.yaml
│ └── service.yaml
└── values.yaml

我们可以通过如下的命令渲染真实的部署配置

helm template workload-helm

可以得到如下 YAML 输出

---
# Source: workload-helm/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: release-name
labels:
helm.sh/chart: workload-0.3.0
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
app.kubernetes.io/version: "0.3.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
ports:
- name: www
port: 80
protocol: TCP
targetPort: 80
---
# Source: workload-helm/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: release-name
labels:
helm.sh/chart: workload-0.3.0
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
app.kubernetes.io/version: "0.3.0"
app.kubernetes.io/managed-by: Helm
spec:
selector:
matchLabels:
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
template:
metadata:
labels:
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
spec:
containers:
- name: my-container
image: "busybox:latest"
command:
- /bin/echo
args:
- -c
- Hello World!
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi

KCL

在 KCL 中,我们提供了与 Helm values.yaml 相似的动态配置参数 kcl.yaml 文件,我们可以执行如下的命令获得一个典型的 KCL 工程。

  • 创建 workload-kcl 目录来保存 KCL 工程
# Create a directory to hold the KCL project
mkdir workload-kcl
# Create a workload-kcl/kcl.yaml
cat <<EOF > workload-kcl/kcl.yaml
kcl_options:
- key: containers
value:
my-container:
image:
name: busybox:latest
command: ["/bin/echo"]
args:
- "-c"
- "Hello World!"
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi

- key: service
value:
type: ClusterIP
ports:
- name: www
protocol: TCP
port: 80
targetPort: 80
EOF
  • 创建如下 KCL 文件来保存 kubernetes 资源
# Create a workload-kcl/deployment.k
cat <<EOF > workload-kcl/deployment.k
apiVersion = "apps/v1"
kind = "Deployment"
metadata = {
name = "release-name"
labels = {
"app.kubernetes.io/name" = "release-name"
"app.kubernetes.io/instance" = "release-name"
}
}
spec = {
selector.matchLabels = metadata.labels
template.metadata.labels = metadata.labels
template.spec.containers = [
{
name = n
image = container.image.name
command = container.command
command = container.args
env = container.env
resources = container.resources
} for n, container in option("containers") or {}
]
}
EOF
cat <<EOF > workload-kcl/service.k
apiVersion = "v1"
kind = "Service"
metadata = {
name = "release-name"
labels = {
"app.kubernetes.io/name" = "release-name"
"app.kubernetes.io/instance" = "release-name"
}
}
spec = {
selector.matchLabels = metadata.labels
type = option("service", default={})?.type
ports = option("service", default={})?.ports
}
EOF

上述 KCL 代码中我们分别声明了一个 Kubernetes DeploymentService 资源的 apiVersionkindmetadataspec 等变量,并分别赋值了相应的内容,特别地,我们将 metadata.labels 字段分别重用在 spec.selector.matchLabelsspec.template.metadata.labels 字段。可以看出,相比于 Helm 模版 或者 YAML,KCL 定义的数据结构更加紧凑,而且可以通过定义局部变量实现配置重用。

在 KCL 中,我们可以通过条件语句和 option 内置函数接收动态参数,并设置不同的配置值以生成资源。

可以通过如下的命令得到 DeploymentService YAML 输出:

  • Deployment
$ kcl workload-kcl/deployment.k -Y workload-kcl/kcl.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: release-name
labels:
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
spec:
selector:
matchLabels:
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
template:
metadata:
labels:
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
spec:
containers:
- name: my-container
image: busybox:latest
command:
- -c
- Hello World!
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
  • Service
$ kcl workload-kcl/service.k -Y workload-kcl/kcl.yaml
apiVersion: v1
kind: Service
metadata:
name: release-name
labels:
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
spec:
selector:
matchLabels:
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
type: ClusterIP
ports:
- name: www
protocol: TCP
port: 80
targetPort: 80

此外我们可以通过 -D 标志设置额外的参数并覆盖 kcl.yaml 文件的配置值

$ kcl workload-kcl/service.k -Y workload-kcl/kcl.yaml -D service=None
apiVersion: v1
kind: Service
metadata:
name: release-name
labels:
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
spec:
selector:
matchLabels:
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
type: null
ports: null

小结

可以看出,与 Helm 相比,KCL 通过在配置重用和覆盖的基础上生成代码,减少了配置文件和代码行的数量。与 Helm 一样,它是一个纯客户端解决方案,可以将配置和策略验证尽可能地左移,而不会对集群造成额外的依赖或负担,或者甚至没有 Kubernetes 集群时也可以通过 KCL Schema 等特性对 YAML 进行充分验证和测试。

Helm 可以在 .tpl 文件中定义可重用模板,并支持其他模板引用它。但是,只有模板定义才能重用。在一个复杂的 Helm 图表项目中,我们需要定义许多附加的基本模板。与 Helm 繁琐的写作方法相比,KCL 中的所有内容都是变量。指定模板不需要其他语法。任何变量都可以相互引用。

此外,Helm 中还有大量与实际逻辑无关的 {{- include }}, nindenttoYaml 标记字符,我们需要计算每个 Helm 引用处的空格和缩进。在 KCL 中,无用代码更少,并且不需要很多的 {{*}} 来标记代码块,信息密度更高。

事实上,KCL 和 Helm Chart 并不对立。我们甚至可以使用 KCL 编写 Helm 模板或者使用 KCL 来生成 values.yaml,或者为现有的 Helm 图表提供可编程扩展功能,比如为 Helm 开发可选的 KCL Schema 插件来验证已有的 Helm 图表或者为 Helm Chart 编写额外的 Transformer 来 Patch 已有的 Helm Chart。

未来计划

我们后续计划 KCL 的模型和约束可以作为一个包来管理(这个包只有 KCL 文件)。例如,Kubernetes 的模型和约束可以开箱即用。用户可以通过已有的模型生成配置或验证现有配置,并且可以通过 KCL 继承手段简单地扩展用户想要的模型和约束。

在此阶段,您可以使用 Git 或 OCI Registry as Storage(ORAS) 等工具来管理 KCL 配置版本。