探索在Windows Server上跑起Kubeedge边缘节点

这里我将详细介绍在 Windows Server 上跑起 Kubeedge 边缘节点的艰难历程,我评价为举步维艰,一步一个坑,但是最终还是成功了(指启动最基本的Edgecore,调度一个Nginx容器到Windows Server并成功从Windows宿主机通过Pod IP访问,不包含云边通信)

和本次实验有关的一些软件/技术

Windows 容器技术

官方文档,讲的真的很好。

总的来说,Windows有两种运行容器的方式:
一种是基于Hyper-V的虚拟机容器,这种容器和Linux的容器不同,是基于虚拟机的,所以性能会比较差,但是可以运行不支持Windows的应用。
另一种是基于进程隔离的容器,这种容器和Linux的容器类似,是基于进程隔离的,性能会比较好,但是只能运行支持Windows的应用。

比如说我们最常用的 Docker Desktop,就是基于Hyper-V的虚拟机容器(运行在WSL2的Linux容器中),此时Windows上运行的容器,都是运行在Hyper-V虚拟机里面的,也正因为这样,我们运行的容器都是linux容器。

基于进程隔离的容器(Host Process Container),这个大家平时在Windows中不太会用到,因为这只有Windows Server支持这种容器。他的实现方式和Linux的容器类似,通过Windows 内核的命名空间功能来提供进程级别的隔离,并使用 Windows Server 上的资源管理器来限制容器的资源使用。性能会比较好,但是只能运行支持Windows的应用(linux/amd64之类的镜像就不能用咯)。

containerd、runc、runhcs、cni、sdn、kubelet、edgecore(edged)

runc是一个用于创建和运行容器的命令行工具。它是Open Container Initiative(OCI)的一部分,OCI是一个由多家公司共同推动的开放标准组织,旨在定义容器格式和运行时的标准。runc实现了OCI Runtime Specification,该规范定义了容器运行时的接口和行为。它负责启动和管理容器,处理容器的生命周期,包括创建、启动、停止和销毁容器等操作。runc本身是一个轻量级的工具,它依赖于Linux内核的容器特性(如cgroups和命名空间)来实现容器的隔离和资源管理。

containerd是一个面向生产环境的容器运行时。它是一个守护进程(daemon),负责管理和运行容器。containerd提供了一组API,用于创建、启动、停止和销毁容器,以及管理容器的镜像和存储等。它的设计目标是提供一个稳定、可靠、高性能的容器运行时,适用于各种容器管理工具和平台。containerd还支持插件机制,可以通过插件扩展其功能,例如支持不同的容器镜像格式、网络插件等。

(linux)runc和containerd的关系是容器运行时和容器管理器之间的关系。runc负责实际的容器运行时功能,而containerd则是一个容器管理器,使用runc来创建和管理容器。containerd通过调用runc提供的接口来操作容器,同时提供了更高级的功能,如镜像管理、存储管理、网络管理等。containerd还可以与其他工具和平台集成,例如Kubernetes,作为其底层容器运行时。

(Windows Server)runhcs是runc的Windows版本,实现的功能和runc一致,在Windows Server中containerd就是调用的runhcs来创建和管理容器。

cni是一个用于容器网络的规范和接口,它定义了容器运行时(如containerd)和网络插件之间的通信方式。CNI的目标是提供一个统一的、可插拔的网络接口,使容器运行时能够与各种网络插件进行交互,从而实现容器的网络配置和连接。containerd如果没有cni网络插件的支持,创建出来的容器就是个玩具,没有任何网络功能。在containerd中可以通过配置cni插件来管理容器的网络,CNI插件可以负责为容器分配IP地址、配置网络路由和防火墙规则等。

sdn是Software Defined Network的缩写,即软件定义网络。它是一种网络架构,通过软件来实现网络的配置、管理和控制,而不是通过硬件设备来实现。在本次实验中,我会使用nat作为节点sdn网络cni插件,而nat的工作方式则是在Windows宿主机上创建一个虚拟网卡,然后将容器的网络流量转发到这个虚拟网卡上,再通过Windows宿主机的网络设备转发到外网。

kubelet是Kubernetes集群中的一个组件,它负责管理节点上的容器,包括创建、启动、停止和销毁容器等操作。kubelet通过调用容器运行时(如containerd)的接口来管理容器。

edgecore(edged)是Kubeedge边缘节点的核心组件,edged是一个裁剪过的kubelet。它的运行方式和k8s上kubelet最大的区别是,kubelet是守护进程,但是edged不是守护进程,它随edgecore启停。

ctr和crictl

ctr和crictl是与容器运行时相关的工具,用于管理容器和容器镜像。ctr是和containerd一起分发的,它是containerd的客户端工具。
crictl是一个用于与cri兼容的容器运行时进行交互的命令行工具。crictl需要自己下载。

假如你不懂内在区别,我们可以理解crictl为给k8s用的命令行工具(最直观的感受就是,crictl的命令格式和kubectl十分相似。同时,ctr有namespace的概念,但是crictl没有,因为通过crictl执行的操作都在k8s.io里)。

20230809023232

为什么ctr和crictl彼此看不见对方拉的镜像?命名空间的问题咯,直接crictl pull的镜像,是在k8s.io的命名空间里,而ctr pull的镜像,是在default命名空间里。

操作环境

一台云服务器,在境外,系统是Ubuntu22,将作为k8s集群的master节点,并部署 Kubeedge1.14.1 Cloudcore。

一台 Windows11 电脑,装有VMWare Workstation Pro 16,创建立一个 Windows Server 2019 的虚拟机,将作为Kubeedge windows node。

探索过程

部署云端

这里不是重点,粗略带过。因为我用了境外主机,所以不存在什么网络问题,直接按照k8s和kubeedge官方文档部署即可。

  1. 配置内核参数 https://kubernetes.io/zh-cn/docs/setup/production-environment/container-runtimes/#install-and-configure-prerequisites
    此网页会要求你选择cgroup driver,我选择的是systemd(即文档中的systemd cgroup),因为我用的是Ubuntu22,而Ubuntu22默认使用systemd初始化系统。
  2. 安装容器运行时 https://github.com/containerd/containerd/blob/main/docs/getting-started.md
    值得注意的是,本次实验请使用1.6.20版本的containerd(为了和边缘统一,虽然没啥意义,你用最新版也无妨,但是至于为什么边缘要用1.6.20,后文会详细介绍)。具体步骤跟着文档走,即下载release包(注意这里不要下载包名中含有cni和cri的包,因为跟着文档走会发现cri特性已经包含在基础包中,而cni我们会单独安装),解压到/usr/local(这里可以看见包里有之前介绍的ctr),然后下载service文件并添加到systemd,接着安装runc,最后安装cni
    containerd安装包内的内容
    cni包中给出的一些基本网络工具
  3. 配置crictl,crictl config runtime-endpoint unix:///run/containerd/containerd.sock。如果你要用crictl调试k8s,那么必须这样配一下,否则crictl会报错找不到runtime
  4. 关闭防火墙(这里我直接关了,因为毕竟是做实验,免得少开了啥端口出问题debug)
  5. 安装指定版本的kubeadm、kubelet和kubectl https://kubernetes.io/zh-cn/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#installing-kubeadm-kubelet-and-kubectl
    这里安装命令请替换成apt-get install -y kubelet=1.24.14-00 kubeadm=1.24.14-00 kubectl=1.24.14-00,因为kubeedge当前最新版本已经支持k8s1.24.14,所以我们就用1.24.14版本的k8s,这样可以避免一些版本兼容性问题。
    注意:如果你看着v1.24的文档,那么该就文档中添加google源的过程是有过时的,按照最新版的文档走就好了
  6. 使用kubeadm初始化master节点 https://kubernetes.io/zh-cn/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/
    本实验我不需要复杂的配置,kubeadm init --control-plane-endpoint=<公网IP>。至于为什么要指定这个参数,这不得不扯一下国内云厂商的这个弹性公网IP了,总之就是,你拥有的公网IP不会直接出现在机器的网卡上,而是通过一种叫做弹性公网IP的技术,将你的公网IP映射到机器的网卡上,直接ifcongig,你会看见自己的ip是一个虚拟内网的IP,这会影响到k8s的初始化。如果你要跨虚拟网络部署集群,那么这里的配置会很麻烦,甚至包括手动修改etcd的yaml,配不好的话网络工具像flannel会直接烂掉,这里先不详细介绍了
    当然还推荐使用--pod-network-cidr指定当前节点的子网如10.244.0.0/16,不然后面要手动执行kubectl patch node [node name] -p '{"spec":{"podCIDR":"10.244.0.0/16"}}'来完成分配(10.244.0.0/16是fannel默认的子网,当然你也可以换了它,但是在部署flannel的时候需要修改配置)
  7. 部署网络插件flannel,当然也可以使用其他网络插件像calico,cilium,或是直接使用k8s自带的网络插件。但是我这里就用flannel了,因为我用过,而且我觉得它的配置最简单。https://github.com/flannel-io/flannel
    直接下载yaml并apply即可,注意修改配置文件中的子网。部署成功以后,会看见master节点的状态变成ready。
    master安装成功
  8. 去掉控制面的污点。由于要运行cloudcore,但我们的集群没有node,所以必须让master可调度,否则cloudcore会一直处于pending状态。https://kubernetes.io/zh-cn/docs/concepts/scheduling-eviction/taint-and-toleration/
    执行kubectl taint node [node name] node-role.kubernetes.io/control-plane-kubectl taint node [node name] node-role.kubernetes.io/master:NoSchedule-即可
  9. 使用keadm部署kubeedge。https://kubeedge.io/zh/docs/setup/install-with-keadm
    先去github下载keadm的release包,这里版本我们选择1.14.1,然后解压,最后直接keadm init --advertise-address=<公网IP>,这里显式指定IP还是因为弹性网卡的问题
    成功运行起来的cloudcore
  10. 运行keadm gettoken拿到密钥,边缘加入做准备

Windows Server上的工作

我们要做的工作就是,安装containerd,安装cni,运行edgecore。命令行我们使用Powershell

1. 安装containerd

版本暂时请用1.6.20,因为我在使用1.6.22的时候踩了莫名其妙的坑(containerd死都加载不了cni插件,containerd始终报错说no network config found in /etc/cni/net.d: cni plugin not initialized),同时也请先不要用1.7.x,和1.24.x的k8s windows不匹配。至于安装过程,微软有给出一套安装脚本文档地址,containerd仓库里也有一套Github地址,以及k8s的sig-windows-node也有一套Github。经过我的多次踩坑,我推荐最后一种,因为这个脚本里的安装路径能很好配合后面cni和kubelet等程序的运行(不然大概率跑步起来)。下载Install-Containerd.ps1后,执行.\Install-Containerd.ps1 -ContainerDVersion 1.6.20 -skipHypervisorSupportCheck,根据提示重启后再执行一遍这个命令即可(因为启用了一些系统功能,需要重启)。此脚本会帮我们装好containerd(不包含cni)和crictl

2. 安装cni插件

containerd的仓库里有个install-cni的脚本,里面包含了下载cni并生成默认配置文件,但是事情抽象在是个sh脚本,里面还夹杂舍powershell脚本,总之我是跑不起来。所以这边我们自己装https://github.com/microsoft/windows-container-networking 这是仓库地址,但是我暂时不建议直接从这里下载release,因为踩坑了x,并且他没有给默认配置,指下载这个仓库中的release可能会一脸懵逼(网上零零散散的配置事例很乱,甚至还有issue里教你自己创建一个SDN网络给cni用,但是实际上完全是多余的)。
windows-container-networking中也有一个配置样例,但是先别用!
这里我推荐直接从containerd仓库的release中下载和containerd版本相匹配的带有cni的包,从中解压出已经打包好的cni二进制文件和配置。虽然containerd文档中提到不需要下载cri-containerd-(cni-)<VERSION>-<OS-<ARCH>.tar.gz,并且还说「The cri-containerd-… archives are deprecated, do not work on old Linux distributions, and will be removed in containerd 2.0.」,但是由于我在自行安装cni的过程中踩了坑,所以这里还是极力建议本实验选用1.6.20带有cni的包下载地址
解压后cni文件夹中存在cni/bincni/conf,把他们放到c:\Program Files\containerd\binc:\Program Files\containerd\conf下即可(如果使用sig-windows-node的脚本,那么就是这个路径,但是如果使用其他安装containerd的脚本,配置文件里的cni路径可能不是这个,但是在代码里却写死了地址是c:\Program Files\containerd,这里是大坑)。
有一个要修改的地方,release包中给出的配置文件,gw不在子网里,潜在的结果可能是子后面部署Pod后,主机ping不通PodIP
默认cni配置文件

还有一个要改的地方是dns配置,样例中capabilities.dns=true,即将使用运行时的DNS配置覆盖其他设置,但是我们的运行时并没有配置dns,导致的结果就是在容器里无法使用dns解析,能ping通外部ip但是无法实现域名解析,解决方法是把这行删了,并手动给出Nameservers(这可能不是常规行为),我直接写了宿主机IP,当然也可以写8.8.8.8或者114.114.114.114这种公共DNS。
最终配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"cniVersion": "0.2.0",
"name": "nat",
"type": "nat",
"master": "Ethernet",
"ipam": {
"subnet": "172.16.0.0/24",
"routes": [
{
"GW": "172.16.0.1"
}
]
},
"capabilities": {
"portMappings": true
},
"dns": {
"Nameservers": [ "192.168.91.2" ]
}
}

最后记得重启一下containerd。
此时输入ipconfig不会有任何新的虚拟网络出现,不用担心,在containerd调用cni插件创建容器的时候,会自动创建虚拟网卡并绑定到容器上。
如何测试安装成功?使用ctr工具创建一个pod出来试试。

1
2
ctr images pull mcr.microsoft.com/windows/nanoserver:ltsc2019
ctr run --cni --rm -t mcr.microsoft.com/windows/nanoserver:ltsc2019 test1 cmd /c ipconfig

容器的输出
如果报错cni not initialized,那么就是cni没装好
如果有输出,可以看见ipconfig会输出一个他的IP(在我们分配的子网下),那么就是cni装好了,并成功运行,如上图所示。
现在在宿主机运行ipconfig,会看见多了一个虚拟网卡,这就是cni创建的,名字叫vEthernet (nat)
宿主机由cni创建的SDN网络
同时,在Powershell中执行Get-HnsNetwork,也可以看见更详细的虚拟网络信息(如果命令不存在,从Github Microsoft SDN仓库下载脚本安装一下):
Get-HnsNetworkd的输出
此时可以看看路由表,cni已经为SDN网络创建了路由:
路由表

3. 运行edgecore

这一步是本次操作的核心。
先是准备环境,goland+golang1.20,拉源码。然后建议配置Goland忽略vender,使用module
开发环境准备

然后修改egde/pkg/edged/config/config.goConvertConfigEdgedFlagToKubletFlag函数,增加两行:

1
2
out.WindowsPriorityClass = in.WindowsPriorityClass
out.WindowsService = in.WindowsService // 这一行可有可无,一定是false,因为我们的edged并不是以service的形式运行,而是随着edgecore启停,当这个选项是true的时候,会注册到系统服务,这不是我们想要的结果,当然我踩坑的时候试过,也注册不成功

WindowsPriorityClass参数是windows上运行Kubelet必须的,不填会Panic,可填写类型看Windows 文档

还有一个要临时修改的地方,位于egde/cmd/edgecore/app/server.go:179:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func environmentCheck() error {
processes, err := ps.Processes()
if err != nil {
return err
}

for _, process := range processes {
processName, err := process.Name()
if err != nil {
continue // 这里原本是return err,但是有些进程没有名字,会报错,所以改成continue
}
switch processName {
case "kubelet": // if kubelet is running, return error
return errors.New("kubelet should not running on edge node when running edgecore")
case "kube-proxy": // if kube-proxy is running, return error
return errors.New("kube-proxy should not running on edge node when running edgecore")
}
}

return nil
}

当我们不开metaserver的时候会检查宿主机是否运行了kubelet和kube-proxy,但是这里的检查是有问题的,这里的process.Name()很可能会出错,有些进程没有名字。暂时把return err改成continue(前提是我们必须保证我们确实没在宿主机装这些东西)。

编译前需要准备环境,安装要mingw-64,不然编译不了sqlite相关(一定是64位,不能是32位,否则运行的时候sqlite相关会无法启动)。
接下来编译成可执行文件,在项目根目录执行:

1
go build -o egdecore.exe github.com/kubeedge/kubeedge/edge/cmd/edgecore

可能需要一些别的环境变量,我不太清楚,我是直接用goland的编译工具编译的,可能要开CGO,因为sqlite需要CGO。

编译成功后,执行.\egdecore.exe --minconfig生成一份最小配置文件写入到文件config.yaml。我这里提供一个运行过后edgecore重新生成的完整配置(大部分模块都没有开,核心是测试edged):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
apiVersion: edgecore.config.kubeedge.io/v1alpha2
database:
aliasName: default
dataSource: C:\var\lib\kubeedge\edgecore.db
driverName: sqlite3
kind: EdgeCore
modules:
dbTest:
enable: false
deviceTwin:
enable: true
edgeHub:
enable: true
heartbeat: 15
httpServer: https://<控制面公网IP>:10002
messageBurst: 60
messageQPS: 30
quic:
enable: false
handshakeTimeout: 30
readDeadline: 15
server: 198.18.0.1:10001
writeDeadline: 15
rotateCertificates: true
tlsCaFile: C:\etc\kubeedge\ca\rootCA.crt
tlsCertFile: C:\etc\kubeedge\certs\server.crt
tlsPrivateKeyFile: C:\etc\kubeedge\certs\server.key
token: <token>
websocket:
enable: true
handshakeTimeout: 30
readDeadline: 15
server: <控制面公网IP>:10000
writeDeadline: 15
edgeStream:
enable: false
handshakeTimeout: 30
readDeadline: 15
server: 127.0.0.1:10004
tlsTunnelCAFile: C:\etc\kubeedge\ca\rootCA.crt
tlsTunnelCertFile: C:\etc\kubeedge\certs\server.crt
tlsTunnelPrivateKeyFile: C:\etc\kubeedge\certs\server.key
writeDeadline: 15
edged:
containerRuntime: remote
enable: true
hostnameOverride: default-edge-node
masterServiceNamespace: default
maxContainerCount: -1
maxPerPodContainerCount: 1
minimumGCAge: 0s
podSandboxImage: mcr.microsoft.com/k8s/core/pause:1.2.0
registerNode: true
registerNodeNamespace: default
registerSchedulable: true
remoteImageEndpoint: npipe://./pipe/containerd-containerd
remoteRuntimeEndpoint: npipe://./pipe/containerd-containerd
rootDirectory: C:\var\lib\edged
tailoredKubeletConfig:
address: 0.0.0.0
cgroupDriver: cgroupfs
cgroupsPerQOS: false
clusterDomain: cluster.local
configMapAndSecretChangeDetectionStrategy: Get
containerLogMaxFiles: 5
containerLogMaxSize: 10Mi
contentType: application/json
cpuCFSQuota: false
cpuCFSQuotaPeriod: 100ms
cpuManagerPolicy: none
cpuManagerReconcilePeriod: 10s
enableControllerAttachDetach: true
enableDebugFlagsHandler: true
enableDebuggingHandlers: true
enableProfilingHandler: true
enableSystemLogHandler: true
evictionHard:
imagefs.available: 15%
memory.available: 100Mi
nodefs.available: 10%
evictionPressureTransitionPeriod: 5m0s
failSwapOn: false
hairpinMode: promiscuous-bridge
imageGCHighThresholdPercent: 80
imageGCLowThresholdPercent: 40
imageMinimumGCAge: 2m0s
iptablesDropBit: 15
iptablesMasqueradeBit: 14
enforceNodeAllocatable:
logging:
flushFrequency: 5000000000
format: text
options:
json:
infoBufferSize: "0"
verbosity: 0
makeIPTablesUtilChains: true
maxOpenFiles: 1000000
maxPods: 110
memoryManagerPolicy: None
memorySwap: {}
memoryThrottlingFactor: 0.8
nodeLeaseDurationSeconds: 40
nodeStatusMaxImages: 0
nodeStatusReportFrequency: 5m0s
nodeStatusUpdateFrequency: 10s
oomScoreAdj: -999
podPidsLimit: -1
readOnlyPort: 10350
registerNode: true
registryBurst: 10
registryPullQPS: 5
resolvConf: ""
runtimeRequestTimeout: 2m0s
seccompDefault: false
serializeImagePulls: true
shutdownGracePeriod: 0s
shutdownGracePeriodCriticalPods: 0s
streamingConnectionIdleTimeout: 4h0m0s
syncFrequency: 1m0s
topologyManagerPolicy: none
topologyManagerScope: container
volumePluginDir: C:\usr\libexec\kubernetes\kubelet-plugins\volume\exec\
volumeStatsAggPeriod: 1m0s
windowsPriorityClass: NORMAL_PRIORITY_CLASS
eventBus:
enable: false
eventBusTLS:
enable: false
tlsMqttCAFile: /etc/kubeedge/ca/rootCA.crt
tlsMqttCertFile: /etc/kubeedge/certs/server.crt
tlsMqttPrivateKeyFile: /etc/kubeedge/certs/server.key
mqttMode: 2
mqttPassword: ""
mqttPubClientID: ""
mqttQOS: 0
mqttRetain: false
mqttServerExternal: tcp://127.0.0.1:1883
mqttServerInternal: tcp://127.0.0.1:1884
mqttSessionQueueSize: 100
mqttSubClientID: ""
mqttUsername: ""
metaManager:
contextSendGroup: hub
contextSendModule: websocket
enable: true
metaServer:
apiAudiences: null
enable: false
server: 0.0.0.0:10550
serviceAccountIssuers:
- https://kubernetes.default.svc.cluster.local
serviceAccountKeyFiles: null
tlsCaFile: C:\etc\kubeedge\ca\rootCA.crt
tlsCertFile: C:\etc\kubeedge\certs\server.crt
tlsPrivateKeyFile: C:\etc\kubeedge\certs\server.key
remoteQueryTimeout: 60
serviceBus:
enable: false
port: 9060
server: 127.0.0.1
timeout: 60

由于大部分参数还没有摸透,我是改了几个地方让它能跑,所以不排除此配置文件中部分参数是处于不生效的状态。
和默认配置相比,需要改的不仅是IP地址,还有几个比较关键的地方:

  • 文件路径前缀一律改成C:\,并修改分隔符为\,因为在windows上,/是不被允许的,会报错。(文件中有还是/etc的,但是该模块都没开,所以不影响,以后要用的时候还是要改的)
  • 增加windowsPriorityClass参数,我给的值是NORMAL_PRIORITY_CLASS,具体可选项可以看上文给出的微软文档
  • 关闭edged的cgroupsPerQOS和cpuCFSQuota,这两个Windows不支持,会报错
  • 清空enforceNodeAllocatable数组,此选项在windows上不生效,并且会报错
  • 修改容器运行时接口为npipe://./pipe/containerd-containerd
  • 修改Sandbox为mcr.microsoft.com/k8s/core/pause:1.2.0,理论上保持不变也不会有问题,但我这里出问题了
  • resolvConf留空,因为windows上没有这个文件,但不能删掉这行,否则会使用默认值/etc/resolv.conf,这个文件在windows上也是不存在的
  • <token>改成keadm拿到的token

然后就可以运行了,.\egdecore.exe --config config.yaml,如果一切正常,会看见如下输出
edgecore运行成功

此时在云端执行kubectl get nodes -o wide可以看见:

1
2
3
NAME                STATUS   ROLES           AGE    VERSION
default-edge-node Ready agent,edge 45h v1.24.14-kubeedge-v0.0.0-master+$Format:%h$
vm-4-2-ubuntu Ready control-plane 2d2h v1.24.14

边缘节点加入成功
由于网络插件nat早已安装成功,所以node直接变为Ready状态

部署一个Nginx

这一步我有被折磨到,很难想象这个镜像有多难打

我试图去网上找现成的镜像,微软官方在Github给出了Dockerfile,但是亲测跑不起来,所以无奈只能自己造了。

Windows镜像和Linux镜像的区别在于,Windows容器必须以Windows系统作为基础镜像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
FROM --platform=linux/amd64 curlimages/curl as bins
ARG nginxVersion="1.25.1"

WORKDIR /nginx
RUN curl -LO https://nginx.org/download/nginx-${nginxVersion}.zip
RUN unzip nginx-${nginxVersion}.zip
RUN ls nginx-${nginxVersion}



FROM mcr.microsoft.com/windows/servercore:ltsc2019
ARG nginxVersion="1.25.1"

WORKDIR "C:\nginx"

# 复制文件
COPY --from=bins /nginx .
COPY sleep.bat "C:\nginx\nginx-${nginxVersion}\sleep.bat"

WORKDIR "C:\nginx\nginx-${nginxVersion}"
# 暴露端口
EXPOSE 80

Dockerfile中没有写CMD了,因为这边有大坑,一旦没写好,就会看见这样的报错:

1
E0808 remote_runtime.go:453] "StartContainer from runtime service failed" err="rpc error: code = Unknown desc = failed to start containerd task \"7e5102efe31c1737f1ed2fed5625905b9915dcec6d0367b156cc77b58e313203\": hcs::System::CreateProcess 7e5102efe31c1737f1ed2fed5625905b9915dcec6d0367b156cc77b58e313203: The system cannot find the file specified.: unknown" cont

奇怪的报错
然后陷入CrashLoopBackOff,无法启动,从各个Issue来看,原因尽然CMD写错了,系统找不到你的启动命令,类似于linux容器没有找到/bin/sh

这里我复制了一个Bat文件进去(这不是一个优雅的做法,还在找解决方案):

1
2
3
4
nginx.exe
@echo off
:loop
goto loop

为什么要这样,为什么不直接运行nginx.exe?这我难说,我自己在宿主机测试nginx的时候,批处理会阻塞那,但是一旦在容器里,直接就退出了!所以无奈只能临时写个死循环,让容器不退出。

现在制作镜像,我们 需要使用buildx交叉编译出windows/amd64版本的镜像,在一个装有docker和buildx的机子上执行:

1
2
3
4
repository=${repository:-"wujunyi792/nginx"}
nginxVersion=${nginxVersion:-"1.25.1"}
docker buildx create --name img-builder --use --platform windows/amd64
docker buildx build --platform windows/amd64 --output=type=registry --pull --build-arg=nginxVersion=$nginxVersion -f Dockerfile -t $repository:$nginxVersion-hostprocess .

记得先登录docker,这样打好的镜像就能直接推到仓库了。
ps:其实tag不应该加hostprocess,hostprocess指的是类似linux特权容器的东西,可以到宿主机命名空间运行,具体文档看 https://kubernetes.io/zh-cn/docs/tasks/configure-pod-container/create-hostprocess-pod/。但我最开始没了解这个,想当然加了这个,其实是错误的x。
镜像打好啦

最后编写nginx.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
spec:
containers:
- name: nginx-container
image: wujunyi792/nginx:1.25.1-hostprocess
command: ["cmd", "/c", "sleep.bat"]
ports:
- containerPort: 80
hostPort: 9002
nodeSelector:
kubernetes.io/os: windows

CMD命令请严格按照["cmd", "/c"]开始,由于没有在Dockerfile中没有把powershell程序的位置c:\windows\system32\WindowsPowerShell\v1.0加入环境变量,所以不能直接运行powershell,只能用cmd来启动容器。

最后在Master执行kubectl apply -f nginx.yaml,等待长时间的Pending状态(在拉镜像),然后在云端执行kubectl get pods -o wide,可以看见Pod已经开始运行,并且已经成功分配了IP:
nginx处于Running状态

当然这个IP从云端是ping不通的,因为我们还没有部署Edgemesh,但是可以在Windows server宿主机上ping通:
Ping一下Pod

在主机访问映射的主机端口9002,可以看见nginx的欢迎页面:
nginx欢迎页面

至此,测试成功!🎉

一些可能会用到的网址


探索在Windows Server上跑起Kubeedge边缘节点
https://wujunyi792.github.io/2023/08/09/探索在Windows Server上跑起Kubeedge边缘节点/
作者
Wujunyi
发布于
2023年8月9日
许可协议