基于amazonlinux2023的EKS优化AMI相关更新如下
-
引入yaml文件进行节点的初始化配置
-
需要VPC CNI v1.16.2及以上版本
-
可配置实例存储将自动挂载为
raid0
-
需要IMDSv2
-
使用cgroupv2
从userdata到nodeadm
AL2中节点需要通过bootstrap.sh
脚本执行节点的初始化逻辑,在AL2023中userdata中的内容变更为MIME格式的yaml文件,该文件的解析和配置是通过nodeadm完成的
在**amazon-eks-ami**构建仓库中的al2023文件中,从install-worker.sh和install-nodeadm.sh脚本中可以看到AMI中内置的组件和systemd service。节点启动运行的service包括以下两个和nodeadm相关的service,分别是nodeadm-config和nodeadm-run
# enable nodeadm bootstrap systemd units
sudo systemctl enable nodeadm-config nodeadm-run
从以下service中可以,nodeadm主要包括config和run两部分逻辑
# Before=cloud-init.service
/usr/bin/nodeadm init --skip run
# After=nodeadm-config.service cloud-final.service
# Requires=nodeadm-config.service
/usr/bin/nodeadm init --skip config
手动执行node init命令结果如下,脚本必须以root用户运行
基于以上的日志信息,我们需要明确以下几个问题
- MIME文件是如何被读取和解析的?
- nodeadm如何替代bootstrap脚本完成节点的初始化工作(包括配置文件处理,进程的启动和系统配置)?
- nodeadm如何进行troubleshooting?
nodeadm代码分析
接下来通过查看nodeadm代码解答上述问题
nodeadm入口文件为cmd/nodeadm/main.go
使用了flaggy作为cmd命令解析库,在入口中初始化config和init配置项,之后遍历并分别运行子命令
cmds := []cli.Command{
config.NewConfigCommand(),
initcmd.NewInitCommand(),
}
for _, cmd := range cmds {
if cmd.Flaggy().Used {
err := cmd.Run(log, opts)
...
}
}
在Run方法中,统一通过BuildConfigProvider获取nodeconfig
provider, err := configprovider.BuildConfigProvider(opts.ConfigSource)
之后根据配置的source URL获取raw schema,默认走imds
获取imds://user-data
的数据
switch parsedURL.Scheme {
case "imds":
return NewUserDataConfigProvider(), nil
case "file":
source := getURLWithoutScheme(parsedURL)
return NewFileConfigProvider(source), nil
default:
return nil, fmt.Errorf("unsupported scheme: %s", parsedURL.Scheme)
}
初始化imds客户端后,按照以下顺序获取数据,如果无法将MIME解析为multipart document,则整个userdata会被直接解析为node config,所以逻辑上我们可以直接写nodeconfig yaml文件
func (ics *userDataConfigProvider) Provide() (*internalapi.NodeConfig, error) {
userData, err := ics.getUserData() // 获取userdata
if multipartReader, err := getMIMEMultipartReader(userData); err == nil {
config, err := parseMultipart(multipartReader) // 解码MIME文件
...
} else {
config, err := apibridge.DecodeNodeConfig(userData)
}
}
在解码函数中,使用了DecodeNodeConfig函数,这里会比较MIME文件的类型是否为"application/" + “node.eks.aws”,从这里我们得知MIME文件中cloud-init,bash脚本和nodeconfig可以共存的原因,前两者并不会被nodeadm解析和处理,并且顺序在cloud-init之后
if mediaType == nodeConfigMediaType {
nodeConfigPart, err := io.ReadAll(part)
// DecodeNodeConfig unmarshals the given data into an internal NodeConfig object. The data may be JSON or YAML.
decodedConfig, err := apibridge.DecodeNodeConfig(nodeConfigPart) // 解析为golang对象,
nodeConfigs = append(nodeConfigs, decodedConfig)
}
}
在将yaml文件解码并最终返回NodeConfig对象,从doc中我们可以从api中查看到能够填写的参数,和官方配置清单基本是一致的,例如ClusterDetails ¶
type ClusterDetails struct {
Name string `json:"name,omitempty"`
APIServerEndpoint string `json:"apiServerEndpoint,omitempty"`
CertificateAuthority []byte `json:"certificateAuthority,omitempty"`
CIDR string `json:"cidr,omitempty"`
EnableOutpost *bool `json:"enableOutpost,omitempty"`
ID string `json:"id,omitempty"`
}
获取nodeconfig之后,开始进行如下两部分操作
- containerd和kubelet相关进程配置和启动
- 系统方面的配置和启动
在containerd和kubelet部分入口如下,实现了4个方法意思很容易明白,主要配置逻辑在Configure中
daemons := []daemon.Daemon{
containerd.NewContainerdDaemon(daemonManager),
kubelet.NewKubeletDaemon(daemonManager),
}
func (cd *containerd) Configure(c *api.NodeConfig) error {
return writeContainerdConfig(c)
}
func (cd *containerd) EnsureRunning() error {
return cd.daemonManager.StartDaemon(ContainerdDaemonName)
}
func (cd *containerd) PostLaunch(c *api.NodeConfig) error {
return cacheSandboxImage(c)
}
func (cd *containerd) Name() string {
return ContainerdDaemonName
}
对于containerd
的configure
,主要为加载cfg并写入/etc/containerd/config.toml
#internal/containerd/config.go
containerdConfig, err := generateContainerdConfig(cfg)
if err := util.WriteFileWithDir(containerdConfigFile, containerdConfig, containerdConfigPerm);
对于kubelet
的configure
,主要为如下配置,执行逻辑和al2中的bootstrap基本一致,即基于原始模板做变量替换并写入指定路径
if err := k.writeKubeletConfig(cfg); err != nil {
if err := k.writeKubeconfig(cfg); err != nil {
if err := k.writeImageCredentialProviderConfig(cfg); err != nil {
if err := writeClusterCaCert(cfg.Spec.Cluster.CertificateAuthority); err != nil {
if err := k.writeKubeletEnvironment(cfg); err != nil {
配置完毕后通过以下代码启动daemon,例如kubelet
const KubeletDaemonName = "kubelet"
if err := daemon.EnsureRunning();
func (k *kubelet) EnsureRunning() error {
return k.daemonManager.StartDaemon(KubeletDaemonName)
}
func (m *systemdDaemonManager) StartDaemon(name string) error {
unitName := getServiceUnitName(name)
_, err := m.conn.StartUnitContext(context.TODO(), unitName, ModeReplace, nil)
return err
}
在系统配置下如果指定了localstorage
相关配置就会执行命令,setup-local-disks
这个命令是预置的shell脚本$ cat /usr/bin/setup-local-disks
cmd := exec.Command("setup-local-disks", strategy)
nodeconfig配置实践
按照al2中的启动逻辑,如果指定启动模板但不指定ami,则最终的userdata中会出现2部分MIME文件。使用如下userdata的启动模板创建节点组
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="//"
--//
Content-Type: application/node.eks.aws
---
apiVersion: node.eks.aws/v1alpha1
kind: NodeConfig
spec:
cluster:
name: test127
--//--
最终生效的模板如下,说明不选择ami时最终还是会拼接
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="//"
--//
Content-Type: application/node.eks.aws
---
apiVersion: node.eks.aws/v1alpha1
kind: NodeConfig
spec:
cluster:
apiServerEndpoint: https://F4B054xxxxxxxxx9A8CA3EE4.yl4.cn-north-1.eks.amazonaws.com.cn
certificateAuthority: LS0tLS1CRUdJTiBS0K
cidr: 10.100.0.0/16
name: test127
kubelet:
config:
maxPods: 17
clusterDNS:
- 10.100.0.10
flags:
- "--node-labels=eks.amazonaws.com/sourceLaunchTemplateVersion=7,eks.amazonaws.com/nodegroup-image=ami-0efa79cf5795c3dfa,eks.amazonaws.com/capacityType=ON_DEMAND,eks.amazonaws.com/nodegroup=tmp2,eks.amazonaws.com/sourceLaunchTemplateId=lt-0cf3f3c96601398d5"
--//
Content-Type: application/node.eks.aws
---
apiVersion: node.eks.aws/v1alpha1
kind: NodeConfig
spec:
cluster:
name: test127
--//--
如果选择ami呢?显然没有拼接,但是实例没有加入集群,查看日志发现报错
Mar 06 04:33:30 localhost nodeadm[1445]: {"level":"fatal","ts":1709699610.6430151,"caller":"nodeadm/main.go:36","msg":"Command failed","error":"Apiserver endpoint is missing in cluster configuration","stacktrace":"main.main\n\t/workdir/cmd/nodeadm/mai>
Mar 06 04:33:30 localhost systemd[1]: nodeadm-config.service: Main process exited, code=exited, status=1/FAILURE
Mar 06 04:33:30 localhost systemd[1]: nodeadm-config.service: Failed with result 'exit-code'.
Mar 06 04:33:30 localhost systemd[1]: Failed to start nodeadm-config.service - EKS Nodeadm Config.
这是由于没有配置apiserver终端节点导致配置校验失败导致的,和bootstrap不同,仅仅指定cluster name是不够的,参考nodeadm文档中nodeconfig的最小参数要求应当如下
---
apiVersion: node.eks.aws/v1alpha1
kind: NodeConfig
spec:
cluster:
name: my-cluster
apiServerEndpoint: https://example.com
certificateAuthority: Y2VydGlmaWNhdGVBdXRob3JpdHk=
cidr: 10.100.0.0/16
我们尝试将启动模板的userdata部分修改如下,使其直接退化为不解析MIME格式文件模式,测试也能够顺利加入集群
apiVersion: node.eks.aws/v1alpha1
kind: NodeConfig
spec:
cluster:
apiServerEndpoint: https://F4B054xxxxxxxxx9A8CA3EE4.yl4.cn-north-1.eks.amazonaws.com.cn
certificateAuthority: LS0tLS1CRUdJTiBDRVJUSUZJQ0RFLS0tLS0K
cidr: 10.100.0.0/16
name: test127
既然如此我们可以不从imds而是从file中读取配置文件(此时已经不需要解码),而是手动指定sudo nodeadm init -c file://nodeconfig.yaml
,节点成功能够加入集群
# nodeconfig.yaml
apiVersion: node.eks.aws/v1alpha1
kind: NodeConfig
spec:
cluster:
apiServerEndpoint: https://F4B054xxxxxxxxx9A8CA3EE4.yl4.cn-north-1.eks.amazonaws.com.cn
certificateAuthority: LS0tLS1CRUdJTiBDRVJUSxkFRFLS0tLS0K
cidr: 10.100.0.0/16
name: test127
如果加入自定义的bash脚本呢?按照之前的配置方式,取消ami的选择,然后加入以下userdata
Content-Type: multipart/mixed; boundary="//"
MIME-Version: 1.0
--//
Content-Type: text/x-shellscript; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="userdata.txt"
#!/bin/bash
/bin/echo "Hello World" >> /tmp/testfile.txt
--//--
最终的userdata生效如下,节点顺利加入集群,并且脚本顺利执行。需要注意的是两者的执行现后顺序
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="//"
--//
Content-Type: application/node.eks.aws
---
apiVersion: node.eks.aws/v1alpha1
kind: NodeConfig
spec:
cluster:
apiServerEndpoint: https://F4B054xxxxxxxxx9A8CA3EE4.yl4.cn-north-1.eks.amazonaws.com.cn
certificateAuthority: LS0tLS1CRUdJTiBDRVJUSUZJQLS0tLS0K
cidr: 10.100.0.0/16
name: test127
kubelet:
config:
maxPods: 17
clusterDNS:
- 10.100.0.10
flags:
- "--node-labels=eks.amazonaws.com/sourceLaunchTemplateVersion=11,eks.amazonaws.com/nodegroup-image=ami-0efa79cf5795c3dfa,eks.amazonaws.com/capacityType=ON_DEMAND,eks.amazonaws.com/nodegroup=tmp5,eks.amazonaws.com/sourceLaunchTemplateId=lt-0cf3f3c96601398d5"
--//
Content-Type: text/x-shellscript; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="userdata.txt"
#!/bin/bash
/bin/echo "Hello World" >> /tmp/testfile.txt
--//--
完整的启动配置如下,需要注意的是
- 配置对象将按照在 MIME 多部分文档中出现的顺序进行合并,最后一个配置对象中的值优先级最高
- 内联containerd的配置优先级最高
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="//"
--//
Content-Type: application/node.eks.aws
---
apiVersion: node.eks.aws/v1alpha1
kind: NodeConfig
spec:
cluster:
name: my-cluster
apiServerEndpoint: https://example.com
certificateAuthority: Y2VydGlmaWNhdGVBdXRob3JpdHk=
cidr: 10.100.0.0/16
# 下面部分是可选的
kubelet:
config:
maxPods: 17
clusterDNS:
- 10.100.0.10
flags:
- "--node-labels=eks.amazonaws.com/nodegroup-image=ami-0efa79cf5795c3dfa,eks.amazonaws.com/capacityType=ON_DEMAND,eks.amazonaws.com/nodegroup=testsimple"
containerd:
config: |
[plugins."io.containerd.grpc.v1.cri".containerd]
discard_unpacked_layers = false
instance:
localStorage:
strategy: RAID0
--//--
一些相关的子命令
开启debug模式日志
$ sudo nodeadm config check --development
2024-03-06T05:05:42.839Z INFO config/check.go:27 Checking configuration {"source": "imds://user-data"}
2024-03-06T05:05:42.841Z INFO config/check.go:36 Configuration is valid
[ec2-user@ip-192-168-9-133 log]$ sudo nodeadm init --development
2024-03-06T05:05:57.881Z INFO init/init.go:49 Checking user is root..
配置校验
# /usr/bin/nodeadm config check
{"level":"info","ts":1709623740.1376612,"caller":"config/check.go:27","msg":"Checking configuration","source":"imds://user-data"}
{"level":"info","ts":1709623740.1424549,"caller":"config/check.go:36","msg":"Configuration is valid"}
总的来说,nodeadm替代bootstrap脚本完成了节点的初始化工作,通过有限的配置项降低了节点配置的难度和错误概率。
调试配置
如果需要断点调试,vscode中nodeadm的调试配置如下
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${fileDirname}",
"args": ["init"]
}
]
}