一文搞懂 K3D

2022-03-25 14:22:33 浏览数 (1)

Hello folks,作为一款由 Google 开发的开源平台,Kubernetes 主要用于自动部署、资源扩展、管理以及编排容器化应用程序。其不仅是提供了一个简单的系统,用于管理跨多个服务器的容器,同时,具备出色的负载平衡和资源分配能力,以确保每个应用程序能够以最佳性能运行。

尽管 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/

0 人点赞