尽管 Kubernetes 是为在云中运行而构建的,然而,在实际的业务场景中,开发人员出于各种原因需要在其本地计算机上部署及运行它。毕竟,在本地运行往往是一种使用容器编排平台的最为简单模式。基于本地开发环境,能够尽可能以减轻与生产环境的差异,并确保应用程序在生产中有效运行。
但是,在本地设置 Kubernetes 往往需要一个工具来帮助我们在本地计算机上创建环境。有许多 Kubernetes 开发环境可以帮助开发和测试为 Kubernetes 创建的应用程序,但它们中的每一个都存在一些问题。以笔者的浅薄经验总结:一个良好的开发环境,往往具备以下相关特性:
1、快速启动
2、资源轻量级
3、重启时保持状态
4、易于重置
5、支持跨平台
在本篇文章中,我们将讨论 K3d 以及它如何让开发人员轻松配置 Kubernetes 开发环境。
K3d,顾名思义,就其名称本身而言,可以表达为 “K3s-in-docker”,其是 K3s 的一个包装器——在 Docker 中运行它的轻量级 Kubernetes。K3d 能够以快速地创建及操作集群而闻名,得到了众多开发者、组织及相关社区的高度认可,广泛用于本地 Kubernetes 集群规模项目活动开发。
通过前面的简要解析,我们知道,K3d 是一个旨在轻松在 Docker 中运行 K3s 的实用程序,基于其所提供的一个简单的 CLI 来创建、运行、删除具有 1 到 N 个节点的完全合规的 Kubernetes 集群,是本地容器编排不可或缺的一款平台。那么,K3d 都具备哪些功能呢?
如官网所述,K3s 附带了较多的内置功能和服务,由于 K3s 在容器中运行,其中一些可能只能在 K3d 中以“非正常”方式使用。故此,K3d 囊括了 K3s 所具备的相关功能组件,具体如下所示:
CoreDNS
关于集群 DNS 服务,K3s 相关资源配置列表信息,如下文件所示:
代码语言:javascript复制apiVersion: v1
kind: ServiceAccount
metadata:
name: coredns
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
kubernetes.io/bootstrapping: rbac-defaults
name: system:coredns
rules:
- apiGroups:
- ""
resources:
- endpoints
- services
- pods
- namespaces
verbs:
- list
- watch
- apiGroups:
- discovery.k8s.io
resources:
- endpointslices
verbs:
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
annotations:
rbac.authorization.kubernetes.io/autoupdate: "true"
labels:
kubernetes.io/bootstrapping: rbac-defaults
name: system:coredns
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:coredns
subjects:
- kind: ServiceAccount
name: coredns
namespace: kube-system
---
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
.:53 {
errors
health
ready
kubernetes %{CLUSTER_DOMAIN}% in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
hosts /etc/coredns/NodeHosts {
ttl 60
reload 15s
fallthrough
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
import /etc/coredns/custom/*.server
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: coredns
namespace: kube-system
labels:
k8s-app: kube-dns
kubernetes.io/name: "CoreDNS"
spec:
#replicas: 1
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
selector:
matchLabels:
k8s-app: kube-dns
template:
metadata:
labels:
k8s-app: kube-dns
spec:
priorityClassName: "system-cluster-critical"
serviceAccountName: coredns
tolerations:
- key: "CriticalAddonsOnly"
operator: "Exists"
- key: "node-role.kubernetes.io/control-plane"
operator: "Exists"
effect: "NoSchedule"
- key: "node-role.kubernetes.io/master"
operator: "Exists"
effect: "NoSchedule"
nodeSelector:
beta.kubernetes.io/os: linux
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
k8s-app: kube-dns
containers:
- name: coredns
image: %{SYSTEM_DEFAULT_REGISTRY}%rancher/mirrored-coredns-coredns:1.8.6
imagePullPolicy: IfNotPresent
resources:
limits:
memory: 170Mi
requests:
cpu: 100m
memory: 70Mi
args: [ "-conf", "/etc/coredns/Corefile" ]
volumeMounts:
- name: config-volume
mountPath: /etc/coredns
readOnly: true
- name: custom-config-volume
mountPath: /etc/coredns/custom
readOnly: true
ports:
- containerPort: 53
name: dns
protocol: UDP
- containerPort: 53
name: dns-tcp
protocol: TCP
- containerPort: 9153
name: metrics
protocol: TCP
securityContext:
allowPrivilegeEscalation: false
capabilities:
add:
- NET_BIND_SERVICE
drop:
- all
readOnlyRootFilesystem: true
livenessProbe:
httpGet:
path: /health
port: 8080
scheme: HTTP
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 1
successThreshold: 1
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8181
scheme: HTTP
initialDelaySeconds: 0
periodSeconds: 2
timeoutSeconds: 1
successThreshold: 1
failureThreshold: 3
dnsPolicy: Default
volumes:
- name: config-volume
configMap:
name: coredns
items:
- key: Corefile
path: Corefile
- key: NodeHosts
path: NodeHosts
- name: custom-config-volume
configMap:
name: coredns-custom
optional: true
---
apiVersion: v1
kind: Service
metadata:
name: kube-dns
namespace: kube-system
annotations:
prometheus.io/port: "9153"
prometheus.io/scrape: "true"
labels:
k8s-app: kube-dns
kubernetes.io/cluster-service: "true"
kubernetes.io/name: "CoreDNS"
spec:
selector:
k8s-app: kube-dns
clusterIP: %{CLUSTER_DNS}%
ports:
- name: dns
port: 53
protocol: UDP
- name: dns-tcp
port: 53
protocol: TCP
- name: metrics
port: 9153
protocol: TCP
备注:所涉及的模板变量(如 %{CLUSTER_DOMAIN}%),在将文件写入文件系统之前将被 K3s 替换。对于 K3d 而言,CoreDNS 工作方式与在其他集群中的工作方式基本上是相同的。不过需要注意的是, Corefile 中配置的 /etc/resolv.conf 不能正常工作,因为 K3s 节点容器中的 /etc/resolv.conf 文件与本地机器上的不同。
从 K3d v5.x 开始,K3d 将条目注入到 NodeHosts 以使集群中的 Pod 能够解析同一 Docker 中其他容器的名称网络(集群网络)和一个名为 host.k3d.internal 的特殊条目,它解析为网络网关的 IP(可用于例如使用本地解析器解析 DNS 查询)。
Local-Path-Provisione
基于 Kubernetes 动态配置持久本地存储,K3s 相关资源配置列表信息,如下文件所示:
代码语言:javascript复制apiVersion: v1
kind: ServiceAccount
metadata:
name: local-path-provisioner-service-account
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: local-path-provisioner-role
rules:
- apiGroups: [""]
resources: ["nodes", "persistentvolumeclaims", "configmaps"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["endpoints", "persistentvolumes", "pods"]
verbs: ["*"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "patch"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: local-path-provisioner-bind
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: local-path-provisioner-role
subjects:
- kind: ServiceAccount
name: local-path-provisioner-service-account
namespace: kube-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: local-path-provisioner
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app: local-path-provisioner
template:
metadata:
labels:
app: local-path-provisioner
spec:
priorityClassName: "system-node-critical"
serviceAccountName: local-path-provisioner-service-account
tolerations:
- key: "CriticalAddonsOnly"
operator: "Exists"
- key: "node-role.kubernetes.io/control-plane"
operator: "Exists"
effect: "NoSchedule"
- key: "node-role.kubernetes.io/master"
operator: "Exists"
effect: "NoSchedule"
containers:
- name: local-path-provisioner
image: %{SYSTEM_DEFAULT_REGISTRY}%rancher/local-path-provisioner:v0.0.21
imagePullPolicy: IfNotPresent
command:
- local-path-provisioner
- start
- --config
- /etc/config/config.json
volumeMounts:
- name: config-volume
mountPath: /etc/config/
env:
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
volumes:
- name: config-volume
configMap:
name: local-path-config
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-path
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete
---
kind: ConfigMap
apiVersion: v1
metadata:
name: local-path-config
namespace: kube-system
data:
config.json: |-
{
"nodePathMap":[
{
"node":"DEFAULT_PATH_FOR_NON_LISTED_NODES",
"paths":["%{DEFAULT_LOCAL_STORAGE_PATH}%"]
}
]
}
setup: |-
#!/bin/sh
while getopts "m:s:p:" opt
do
case $opt in
p)
absolutePath=$OPTARG
;;
s)
sizeInBytes=$OPTARG
;;
m)
volMode=$OPTARG
;;
esac
done
mkdir -m 0777 -p ${absolutePath}
chmod 701 ${absolutePath}/..
teardown: |-
#!/bin/sh
while getopts "m:s:p:" opt
do
case $opt in
p)
absolutePath=$OPTARG
;;
s)
sizeInBytes=$OPTARG
;;
m)
volMode=$OPTARG
;;
esac
done
rm -rf ${absolutePath}
helperPod.yaml: |-
apiVersion: v1
kind: Pod
metadata:
name: helper-pod
spec:
containers:
- name: helper-pod
image: %{SYSTEM_DEFAULT_REGISTRY}%rancher/mirrored-library-busybox:1.34.1
在 K3d 中,Local-Path-Provisioner 使用的是位于容器的文件系统中的本地路径(默认为 /var/lib/rancher/k3s/storage),这意味着默认情况下它不会映射到某个地方,例如,在我们的用户主目录中供使用。
在实际的业务场景中,我们可能需要将一些本地目录映射到该路径以轻松使用此路径中的文件,此时,可借助以下命令行参数:
--volume $HOME/some/directory:/var/lib/rancher/k3s/storage@all 添加到我们的 K3d 集群以进行相关操作命令的创建。
Traefik
在 K3s 中,Kubernetes Ingress Controller 即入口控制器默认使用的是 Traefik 接入层代理,其版本为 1.x。K3d 在容器中运行 K3s,因此我们需要在主机上暴露 Http/Https 端口才能轻松访问集群中的 Ingress 资源。其相关资源配置列表信息,如下文件所示:
代码语言:javascript复制---
apiVersion: helm.cattle.io/v1
kind: HelmChart
metadata:
name: traefik-crd
namespace: kube-system
spec:
chart: https://%{KUBERNETES_API}%/static/charts/traefik-crd-10.14.100.tgz
---
apiVersion: helm.cattle.io/v1
kind: HelmChart
metadata:
name: traefik
namespace: kube-system
spec:
chart: https://%{KUBERNETES_API}%/static/charts/traefik-10.14.100.tgz
set:
global.systemDefaultRegistry: "%{SYSTEM_DEFAULT_REGISTRY_RAW}%"
valuesContent: |-
rbac:
enabled: true
ports:
websecure:
tls:
enabled: true
podAnnotations:
prometheus.io/port: "8082"
prometheus.io/scrape: "true"
providers:
kubernetesIngress:
publishedService:
enabled: true
priorityClassName: "system-cluster-critical"
image:
name: "rancher/mirrored-library-traefik"
tag: "2.6.1"
tolerations:
- key: "CriticalAddonsOnly"
operator: "Exists"
- key: "node-role.kubernetes.io/control-plane"
operator: "Exists"
effect: "NoSchedule"
- key: "node-role.kubernetes.io/master"
operator: "Exists"
effect: "NoSchedule"
通常而言,目前支持以下 2 种模式,具体:
1、入口
此模式为默认推荐的方式。我们通过某种方式创建集群,使内部端口 80(Traefik 入口控制器监听)暴露在主机系统上。
2、节点端口
Servicelb
基于 K3d 中的 Servicelb,klipper-lb 创建新的 Pod,将来自 hostPorts 的流量代理到以下类型的服务端口:LoadBalancer。 在这种情况下,hostPort 是 K3s 容器中的端口,而不是我们的本地主机,因此需要在创建集群时通过 --port 标志添加端口映射。
除了上述功能外,K3d 也具备其他高级特性,诸如使用 Calico 网络策略替代早期的 Flannel、运行 CUDA 工作负载等等。
备注:如果想在 K3s 容器上运行 CUDA 工作负载,我们则需要自定义容器。CUDA 工作负载需要 NVIDIA Container Runtime,因此需要将 containerd 配置为使用此运行时。K3s 容器本身也需要与此运行时一起运行。如果使用的是 Docker,则可以安装 NVIDIA Container Toolkit。
接下来,我们了解一下 K3d 的安装部署以及所映射的相关网络模型。为了尽可能地融入社区,K3d 使用 “Server” 和 “Agent” 两个词来设计 “Master” 和 “Worker” 节点。通常,基于 K3d 所构建的本地 Kubernetes 集群环境,主要涉及以下:
1、所创建的每个集群现在将生成至少 2 个容器:1 个负载均衡器和 1 个“服务器”节点。
负载均衡器将成为 Kubernetes API 的接入点,因此即使对于多服务器集群,我们也只需要公开一个 Api 端口,然后负载均衡器将负责将我们的请求代理到正确的服务器节点。(当然,若不使用此项,可以使用 --no-lb 标志进行禁用)
2、采用“名词动词”句法。这一重大更改使得添加新名词(即 K3d 托管对象)变得更加容易,并且与许多其他云原生 CLI(例如 Gcloud、AWScli、AZURE cli、...)类似,并且还提供了更清晰的 CLI 层次结构。
3、当一个新的服务器节点被添加至集群时,支持多服务器集群(dqlite)和热重载配置。
4、独立的集群处理节点。
5、基本的插件支持系统及丰富的命令行操作。
现在创建一个带有1个Loadbalancer 和1节点(具有服务器和代理的角色)的简单群集,名称为“Devops Cluster”,具体命令行操作如下所示:
代码语言:javascript复制[leonli@192 ~] % k3d version
k3d version v5.3.0
k3s version v1.22.6-k3s1 (default)
[leonli@192 ~] % docker version
Client:
Cloud integration: v1.0.22
Version: 20.10.12
API version: 1.41
Go version: go1.16.12
Git commit: e91ed57
Built: Mon Dec 13 11:46:56 2021
OS/Arch: darwin/arm64
Context: default
Experimental: true
Server: Docker Desktop 4.5.0 (74594)
Engine:
Version: 20.10.12
API version: 1.41 (minimum version 1.12)
Go version: go1.16.12
Git commit: 459d0df
Built: Mon Dec 13 11:43:07 2021
OS/Arch: linux/arm64
Experimental: false
containerd:
Version: 1.4.12
GitCommit: 7b11cfaabd73bb80907dd23182b9347b4245eb5d
runc:
Version: 1.0.2
GitCommit: v1.0.2-0-g52b36a2
docker-init:
Version: 0.19.0
GitCommit: de40ad0
代码语言:javascript复制[leonli@192 ~] % k3d cluster create devops-cluster --port 8080:80@loadbalancer --port 8443:443@loadbalancer --api-port 6443 --servers 1 --agents 1
INFO[0000] portmapping '8080:80' targets the loadbalancer: defaulting to [servers:*:proxy agents:*:proxy]
INFO[0000] portmapping '8443:443' targets the loadbalancer: defaulting to [servers:*:proxy agents:*:proxy]
INFO[0000] Prep: Network
INFO[0000] Created network 'k3d-devops-cluster'
INFO[0000] Created image volume k3d-devops-cluster-images
INFO[0000] Starting new tools node...
INFO[0004] Pulling image 'docker.io/rancher/k3d-tools:5.3.0'
INFO[0006] Creating node 'k3d-devops-cluster-server-0'
INFO[0010] Pulling image 'docker.io/rancher/k3s:v1.22.6-k3s1'
INFO[0010] Starting Node 'k3d-devops-cluster-tools'
INFO[0018] Creating node 'k3d-devops-cluster-agent-0'
INFO[0018] Creating LoadBalancer 'k3d-devops-cluster-serverlb'
INFO[0023] Pulling image 'docker.io/rancher/k3d-proxy:5.3.0'
INFO[0033] Using the k3d-tools node to gather environment information
INFO[0034] Starting cluster 'devops-cluster'
INFO[0034] Starting servers...
INFO[0034] Starting Node 'k3d-devops-cluster-server-0'
INFO[0038] Starting agents...
INFO[0038] Starting Node 'k3d-devops-cluster-agent-0'
INFO[0045] Starting helpers...
INFO[0045] Starting Node 'k3d-devops-cluster-serverlb'
INFO[0052] Injecting records for hostAliases (incl. host.k3d.internal) and for 3 network members into CoreDNS configmap...
INFO[0054] Cluster 'devops-cluster' created successfully!
INFO[0054] You can now use it like this:
kubectl cluster-info
此时,依据日志输出提示,运行 kubectl cluster-info 查看下当前集群的信息,如下所示:
代码语言:javascript复制[leonli@192 ~] % kubectl cluster-info
Kubernetes control plane is running at https://0.0.0.0:6443
CoreDNS is running at https://0.0.0.0:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
Metrics-server is running at https://0.0.0.0:6443/api/v1/namespaces/kube-system/services/https:metrics-server:https/proxy
然后,我们借助 docker ps 命令来看一下创建的容器底层相关信息,具体如下所示:
代码语言:javascript复制[leonli@192 ~] % docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
52ae7eedb2f6 rancher/k3d-proxy:5.3.0 "/bin/sh -c nginx-pr…" 6 minutes ago Up 6 minutes 0.0.0.0:6443->6443/tcp, 0.0.0.0:8080->80/tcp, 0.0.0.0:8443->443/tcp k3d-devops-cluster-serverlb
c147f033af88 rancher/k3s:v1.22.6-k3s1 "/bin/k3d-entrypoint…" 7 minutes ago Up 6 minutes k3d-devops-cluster-agent-0
3e1e97081296 rancher/k3s:v1.22.6-k3s1 "/bin/k3d-entrypoint…" 7 minutes ago Up 6 minutes k3d-devops-cluster-server-0
接下来,我们基于所创建的集群信息,来梳理一下所配置的端口映射逻辑关系:
--port 8080:80@loadbalancer 会将本地的 8080 端口映射到 Loadbalancer 的 80 端口,然后 Loadbalancer 接收到 80 端口的请求后,会代理到所有的 K8s 节点。
--api-port 6443 默认提供的端口号,K3s 的 Api-Server 会监听 6443 端口,主要是用来操作 Kubernetes API 的,即使创建多个 Master 节点,也只需要暴露一个 6443 端口,Loadbalancer 会将请求代理分发给多个 Master 节点。
如果我们期望通过 NodePort 的形式暴露服务,也可以基于实际的业务场景来自定义一些端口号映射到 Loadbalancer 来暴露 K8s 的服务,当然,前提是如果不想使用 Ingress Controller 的话。相关命令行可参考如下命令行:
代码语言:javascript复制-p 20080-30080:20080-30080@loadbalancer
此时,基于 K3d 所创建的名称为 Devops Cluster 的本地集群网络拓扑如下所示:
现在,一个完整的本地 K8s 集群已部署 Ok,接下来,我们通过创建一个简单的 Nginx 实例进行验证,具体如下所示:
代码语言:javascript复制[leonli@192 ~] % kubectl create deployment nginx --image=nginx
deployment.apps/nginx created
[leonli@192 ~] % kubectl create service clusterip nginx --tcp=80:80
service/nginx created
[leonli@192 ~] % cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: nginx
annotations:
ingress.kubernetes.io/ssl-redirect: "false"
spec:
rules:
- http:
paths:
- path: /
backend:
serviceName: nginx
servicePort: 80
EOF
创建一个默认由 Traefik 1.x 作为 Ingress Controller 的 Ingress,我们可以直接访问 http://localhost:8080/,即可看到 Nginx 相关信息。
其实,从本质而言,K3d 是一款出色的工具,其不仅结合了简单、极轻、模块化和功能,同时也解决了更为复杂的需求。除此之外,Rancher 团队再次出色地重写了 K3d,使得在一台机器上运行具有不同拓扑的 K3s Kubernetes 集群的多个实例变得非常容易、模块化、简单和高效。
# 参考资料
- https://k3d.io/