AWS EKS 升級 AL2023 實戰:踩坑 Custom Networking 與 nodeadm 的啟動機制

隨著 Amazon Linux 2 (AL2) 逐漸進入維護期,將 EKS Node Group 升級到 Amazon Linux 2023 (AL2023) 已成為許多 SRE 的待辦事項。AL2023 引入了全新的初始化工具 nodeadm,雖然讓配置更標準化(使用 YAML),但也改變了我們習慣的 User Data 撰寫邏輯。

這篇文章將分享我們在升級過程中遇到的特定場景:當你需要「高度客製化」的網路配置(Custom Networking)時,該如何在 AL2023 中正確啟動節點。

背景:為什麼我們的場景比較複雜?

我們的 EKS 環境使用了 VPC CNI Custom Networking,目的是讓 Pod 使用與 Node 不同的 Subnet 網段。

在標準的 AWS 設計中,如果你的 ENIConfig 名稱與 Availability Zone (AZ) 名稱一致(例如 ap-northeast-1a),VPC CNI 可以自動對應。但我們的環境為了隔離不同業務(例如 Billing 系統),使用了帶有前綴的命名方式:

  • AZ: ap-northeast-1a
  • ENIConfig: billing-ap-northeast-1a

因為名稱無法自動對應,我們必須在 Node 啟動時,強制打上特定的 Label 告訴 VPC CNI 該用哪個設定檔:

1
2
eks.amazonaws.com/custom-eni-config=billing-ap-northeast-1a

這在 AL2 時期很簡單,只要在 bootstrap.sh 裡算好 AZ 變數並塞進 --kubelet-extra-args 即可。但在 AL2023,這變成了一個挑戰。

踩到的坑:Node 無法加入 Cluster

AL2023 官方建議使用 MIME Multi-part 格式傳遞 NodeConfig YAML。但問題來了:Terraform 在 Plan/Apply 階段無法預知 Node 會跑在哪個 AZ,我們必須在 Runtime (Shell Script) 透過 IMDS (Instance Metadata Service) 動態獲取 AZ,才能組出正確的 custom-eni-config Label。

於是,我們將 User Data 改寫為 Shell Script,手動生成設定檔。然而,Terraform Apply 後,Node Group 卡在 Creating 狀態長達 20 分鐘,最後超時失敗。

排查 Log

我們 SSH 進入那台「只活在 EC2 Console 但進不了 K8s」的節點,查看 /var/log/user-data.log (我們自定義的輸出) 和 journalctl

執行結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
[ec2-user@ip-10-xxx-xxx-xxx ~]$ sudo cat /var/log/user-data.log
...
+ echo 'Configuration file written to /etc/eks/nodeadm-config.yaml'
Configuration file written to /etc/eks/nodeadm-config.yaml
+ echo 'Executing nodeadm init...'
Executing nodeadm init...
+ /usr/bin/nodeadm init
info init/init.go:55 Checking user is root..
info cli/options.go:45 Using default config sources...
info init/init.go:65 Loading configuration.. {"configSource": ["imds://user-data"], "configCache": ""}
warn configprovider/chain.go:30 Encountered error in config provider {"error": "could not find NodeConfig within UserData"}
fatal cli/main.go:35 Command failed {"error": "no config in chain"}

關鍵錯誤訊息

Encountered error in config provider {"error": "could not find NodeConfig within UserData"}
Command failed {"error": "no config in chain"}

原因分析

這段 Log 揭示了 nodeadm 的預設行為。當我們執行 /usr/bin/nodeadm init 而不帶任何參數時,它預設會去 IMDS (Instance Metadata Service) 抓取 User Data。

  1. 標準場景:User Data 是 YAML 格式的 NodeConfig ➝ nodeadm 讀取成功 ➝ 啟動 Kubelet。
  2. 我們的場景:User Data 是 Bash Script ➝ nodeadm 抓下來發現是 Script 不是 YAML ➝ 解析失敗 ➝ 報錯 no config in chain

雖然我們的 Script 已經在本地生成了正確的 YAML 檔 (/etc/eks/nodeadm-config.yaml),但 nodeadm 並不知道要去讀它

解決方案:指定 –config-source

我們查閱了 nodeadm 的說明文件(或是直接看 Source Code/Help),發現它支援指定配置來源。

1
2
3
4
5
6
7
8
9
$ nodeadm init --help
Usage:
nodeadm init [flags]

Flags:
-c, --config-source string Source of the configuration.
Allowed schemes: "file:", "imds:"
(default "imds://user-data")

原來關鍵在於 file:// scheme。我們需要明確告訴 nodeadm:「不要去網路抓,請讀取我剛寫入本機的檔案」。

修正後的 Terraform User Data

以下是最終成功的 User Data 腳本(簡化版):

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
user_data = base64encode(<<-EOF
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="==MYBOUNDARY=="

--==MYBOUNDARY==
Content-Type: text/x-shellscript; charset="us-ascii"

#!/bin/bash
set -ex
# Debug
exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1

# 1. Get IMDSv2 Token & AZ
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
AZ=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/placement/availability-zone)

# 2. Construct Labels, including the dynamic custom-eni-config
# Note: Here we mix Terraform interpolation and Bash variables
FINAL_LABELS="...,eks.amazonaws.com/custom-eni-config=${var.node_group_name}-$AZ"

# 3. Generate NodeConfig YAML
cat > /etc/eks/nodeadm-config.yaml <<CONFIG
apiVersion: node.eks.aws/v1alpha1
kind: NodeConfig
spec:
cluster:
name: ${var.cluster.cluster_id}
apiServerEndpoint: ${var.cluster.cluster_endpoint}
certificateAuthority: ${var.cluster.cluster_certificate_authority_data}
cidr: 192.168.0.0/16
kubelet:
flags:
- --node-labels=$FINAL_LABELS
CONFIG

# 4. [Critical] Execute nodeadm init and specify the file source
# Note: Must use file:/// (three slashes)
/usr/bin/nodeadm init --config-source file:///etc/eks/nodeadm-config.yaml

--==MYBOUNDARY==--
EOF
)

重點提示

  • URI 格式:必須是 file:///absolute/path
  • MIME:依然建議包在 MIME Multipart 中,這是 AWS 推薦的標準做法。

同場加映:VPC CNI 的環境變數陷阱

在解決 Node 啟動問題後,我們發現 Node 雖然 Ready 了,但如果 VPC CNI (aws-node) 的設定不正確,Pod 依然無法產生。

在 AL2023 + Custom Networking 的架構下,請務必檢查 aws-node DaemonSet 的以下變數:

  1. AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG=true
  • 一定要開,否則 CNI 會忽略我們辛苦打上的 custom-eni-config Label,嘗試用主網段發 IP,導致 IP 不足或連線失敗。
  1. ENABLE_PREFIX_DELEGATION=true
  • 如果你有設定 maxPods: 110 這類高密度部署,務必開啟此項,否則節點會因為 Private IP 耗盡而無法排程 Pod。

驗證結果

修正 User Data 與 CNI 變數後,Node 順利加入 Cluster。我們透過以下方式驗證 Custom Networking 是否生效:

1. 檢查 Node Label
確保 Label 正確指向了帶有前綴的 ENIConfig:

1
2
3
kubectl get node <node-name> --show-labels | grep custom-eni-config
# 輸出: eks.amazonaws.com/custom-eni-config=billing-ap-northeast-1d

2. 檢查 Pod IP 與 Node IP 的網段差異
這是最直接的證據。我們的設定中,Node 使用 10.xx.233.x (主網段),而 Pod 應該使用 10.xx.139.x (Custom Subnet)。

1
2
3
4
$ kubectl get pod -o wide
NAME IP NODE
billing-service-xxxx 10.23.xxx.xxx ip-10-23-xxx-xxx...

  • Node IP: 10.23.xxx.xxx
  • Pod IP: 10.23.xxx.xxx

透過 aws ec2 describe-subnets 確認 10.23.xxx.xxx 確實屬於我們指定的 Custom Subnet CIDR (10.23.xxx.xxx/20),驗證成功!

總結

升級 AL2023 時,如果你的架構涉及動態參數(如依賴 AZ 的 Custom Networking),不要害怕放棄純 YAML User Data。使用 Shell Script 搭配 nodeadm init --config-source file://... 是官方支援且穩健的解法。

同時,別忘了檢查 aws-node 的環境變數,確保 CNI 能正確理解你的網路拓撲。