EKS 集群升级与多环境部署实战:从踩坑到丝滑
管理一个 EKS 集群不难,难的是管理四个环境、十五个节点组、每个节点组都有自己的 Launch Template 版本,然后老板跟你说:"下周把所有集群从 1.28 升到 1.32,别影响业务。"
这篇文章来自一个真实的生产项目。四套环境(dev / staging / preprod / prod),每套环境十几个节点组,全部用 Terraform 管理。我会把整个架构拆开给你看,从 Terraform 项目结构到节点滚动更新,从 Launch Template 版本控制到 MFA 凭证管理,每一步都是踩过坑之后总结出来的。
一、整体架构:先看全貌
先上一张架构图,让你对整个项目有个直观的认识:
看起来复杂?别急,我们一层一层拆。
二、Terraform 项目结构:多环境管理的正确姿势
先看项目目录结构(已脱敏):
my-eks-project/
|-- main.tf # Provider + Backend 配置
|-- eks.tf # EKS 集群 + Addons
|-- variables.tf # 所有变量定义
|-- dns.tf # Route53 DNS 记录
|-- nodes_shared.tf # shared 节点组
|-- nodes_default.tf # default 节点组 (prod only)
|-- nodes_service_a.tf # 业务A 节点组 (prod only)
|-- nodes_service_b.tf # 业务B 节点组 (prod only)
|-- nodes_service_c.tf # ... 更多业务节点组
|-- environments/
| |-- dev.tfvars # dev 环境变量
| |-- staging.tfvars # staging 环境变量
| |-- preprod.tfvars # preprod 环境变量
| |-- prod.tfvars # prod 环境变量
|-- init_script/
| |-- dev/
| | |-- shared.sh # dev 环境 shared 节点初始化脚本
| | |-- default.sh # dev 环境 default 节点初始化脚本
| | |-- service_a.sh # ...
| |-- staging/
| |-- preprod/
| |-- prod/
|-- tf-deploy.sh # 部署入口脚本
|-- tf-run.sh # 增强版部署脚本
|-- gen-credentials.sh # MFA 凭证生成脚本
2.1 Backend 配置:状态文件别丢了
Terraform 的状态文件(tfstate)是命根子。丢了它,你的基础设施就变成了"薛定谔的集群"--你不知道它是什么状态,直到你 terraform import 到崩溃。
# main.tf
terraform {
required_version = ">= 1.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "my-eks-terraform-state"
key = "my-eks-terraform.tfstate"
region = "cn-north-1"
dynamodb_table = "my-eks-terraform-state-lock"
encrypt = true
}
}
provider "aws" {
access_key = var.aws_access_key_id
secret_key = var.aws_secret_access_key
token = var.aws_session_token
region = "cn-north-1"
}
几个关键设计决策:
- S3 存储状态文件,开启加密。别用本地文件,除非你想体验"我改了什么来着"的恐惧
- DynamoDB 做状态锁。防止两个人同时
terraform apply,一个改 A 节点组,一个改 B 节点组,最后状态文件变成一锅粥 - Provider 用临时凭证(STS Token),不是长期 AK/SK。安全第一
2.2 Terraform Workspace:一套代码管四套环境
这个项目用 Terraform Workspace 来区分环境。同一套 .tf 代码,通过不同的 .tfvars 文件注入不同的参数:
# 切换到 dev 环境
terraform workspace select dev
terraform plan -var-file environments/dev.tfvars
# 切换到 prod 环境
terraform workspace select prod
terraform plan -var-file environments/prod.tfvars
Workspace 的好处是状态文件自动隔离。dev 的状态和 prod 的状态存在 S3 的不同路径下,互不干扰。但也有个坑:terraform.workspace 这个变量可以在 .tf 代码里用,这就引出了一个很实用的模式--条件资源。
2.3 条件资源:dev 不需要那么多节点组
生产环境有 15 个节点组,每个业务一个。但 dev 环境不需要这么多,所有服务跑在 shared 节点组就够了。怎么做?
# nodes_service_a.tf -- 只在 prod 创建
resource "aws_eks_node_group" "service_a" {
count = terraform.workspace == "prod" ? 1 : 0
cluster_name = var.eks_name
node_group_name = "service-a"
node_role_arn = var.node_role_arn
subnet_ids = var.subnets
launch_template {
id = aws_launch_template.service_a[count.index].id
version = var.service_a_launch_version
}
# ...
}
# nodes_shared.tf -- 所有环境都有
resource "aws_eks_node_group" "shared" {
# 没有 count,所有环境都创建
cluster_name = var.eks_name
node_group_name = "shared"
node_role_arn = var.node_role_arn
subnet_ids = var.subnets
# ...
}
count = terraform.workspace == "prod" ? 1 : 0 这一行就是精髓。dev/staging/preprod 环境下 count=0,资源不会被创建。只有 prod 才会创建这些专用节点组。
这样做的好处:
- dev 环境成本大幅降低(只跑 shared 节点组)
- prod 环境每个业务有独立的节点组,互不影响
- 同一套代码,不需要维护多个分支
不过 Workspace 也有局限性。如果 dev 和 prod 在不同的 AWS 账号(这个项目就是),你需要在部署脚本里根据环境切换凭证。后面会讲到。
2.4 环境变量差异:tfvars 文件对比
来看看 dev 和 prod 的 tfvars 有什么不同:
# environments/dev.tfvars
eks_name = "eks-dev"
log_retain_days = 7 # 日志只保留 7 天
k8s_version = "1.32"
node_volume_size = "40" # 40GB 够用了
default_instance_type = ["t3.xlarge"] # 小一号
shared_desired_size = 10 # 所有服务都跑这里
shared_max_size = 20
# 其他节点组全部 desired=0
svc_a_scaling_desired_size = 0
svc_b_scaling_desired_size = 0
# environments/prod.tfvars
eks_name = "eks-prod"
log_retain_days = 30 # 日志保留 30 天
k8s_version = "1.32"
node_volume_size = "128" # 128GB,生产要大方
default_instance_type = ["t3.2xlarge"] # 大一号
shared_desired_size = 0 # prod 不用 shared
# 每个业务独立节点组
svc_a_scaling_desired_size = 3
svc_b_scaling_desired_size = 4
svc_c_scaling_desired_size = 5
svc_data_scaling_desired_size = 8 # 数据服务最吃资源
注意几个有意思的点:
- dev 的 shared 节点组有 10 个节点,承载所有服务;prod 的 shared 反而是 0,因为每个业务都有自己的节点组
- dev 用 t3.xlarge(4C16G),prod 用 t3.2xlarge(8C32G)甚至 m5.4xlarge(16C64G)
- dev 磁盘 40GB,prod 磁盘 128GB。生产环境的日志和临时文件会多很多
- prod 的数据服务节点组有 8 个节点,min=6,max=12,说明这个服务流量波动大
三、节点组设计:隔离是门艺术
3.1 为什么要按业务拆分节点组?
你可能会问:所有 Pod 跑在一个大节点组不行吗?行,但迟早你会后悔。
想象一下:业务 A 的 Pod 突然内存泄漏,把节点的内存吃光了。如果业务 B 的 Pod 恰好也在这个节点上,它会被 OOM Kill。业务 B 的同事跑来问你:"我的服务怎么挂了?"你说:"因为业务 A 内存泄漏。"他说:"关我什么事?"
按业务拆分节点组,配合 Taint/Toleration,可以实现故障隔离:
# 节点组配置 Taint
resource "aws_eks_node_group" "service_a" {
# ...
labels = {
app = "service-a"
}
taint {
effect = "NO_SCHEDULE"
key = "dedicated"
value = "service-a"
}
}
# Pod 配置 Toleration + NodeSelector
apiVersion: apps/v1
kind: Deployment
metadata:
name: service-a-app
spec:
template:
spec:
nodeSelector:
app: service-a
tolerations:
- key: "dedicated"
operator: "Equal"
value: "service-a"
effect: "NoSchedule"
containers:
- name: app
image: my-registry/service-a:latest
这样配置后,service-a 的 Pod 只会调度到 service-a 的节点上,其他 Pod 也不会跑到这些节点上。完美隔离。
3.2 Shared 节点组:兜底选手
不是所有服务都需要独立节点组。一些基础设施组件(监控、日志、Ingress Controller)和小型服务可以共享节点:
resource "aws_eks_node_group" "shared" {
cluster_name = var.eks_name
node_group_name = "shared"
# 没有 count,所有环境都有
labels = {
compute = "true"
app = "others" # 通用标签
}
# 没有 taint,任何 Pod 都可以调度上来
lifecycle {
ignore_changes = [scaling_config[0].desired_size]
create_before_destroy = true
}
}
注意 ignore_changes = [scaling_config[0].desired_size] 这行。这是因为如果你用了 Cluster Autoscaler 或 Karpenter,它会动态调整 desired_size。如果 Terraform 不忽略这个字段,每次 apply 都会把 desired_size 改回 tfvars 里的值,跟 Autoscaler 打架。
3.3 特殊 IAM 角色:精细权限控制
大部分节点组共用一个 IAM Role(eks-node-{env}),但有些业务需要特殊的 AWS 权限。比如某个服务需要直接访问 S3 或 DynamoDB,就需要一个独立的 IAM Role:
resource "aws_eks_node_group" "special_service" {
count = terraform.workspace == "prod" ? 1 : 0
node_role_arn = var.special_service_node_role_arn # 独立的 IAM Role
# 其他节点组用的是 var.node_role_arn
}
这种设计遵循最小权限原则:只有需要特殊权限的节点组才有特殊的 IAM Role,其他节点组用通用 Role。避免了"一个 Role 权限越加越大,最后变成 Admin"的尴尬局面。
四、Launch Template:节点的"出生证明"
每个节点组都有一个 Launch Template,定义了节点的"出厂配置":用什么 AMI、多大的磁盘、什么安全组、启动时跑什么脚本。
4.1 Launch Template 结构
resource "aws_launch_template" "service_a" {
count = terraform.workspace == "prod" ? 1 : 0
name_prefix = "eks-node-prod"
block_device_mappings {
device_name = "/dev/xvda"
ebs {
volume_size = var.node_volume_size # 128GB for prod
volume_type = var.node_volume_type # gp3
delete_on_termination = true
}
}
disable_api_termination = true # 防止误删
ebs_optimized = true
image_id = var.ami_id
key_name = var.keypairs_name
monitoring { enabled = true }
network_interfaces {
associate_public_ip_address = false
security_groups = var.node_security_groups
}
tag_specifications {
resource_type = "instance"
tags = { Name = "service-a-eks-prod" }
}
tag_specifications {
resource_type = "volume"
tags = { Name = "service-a-eks-prod" }
}
user_data = data.local_file.service_a.content_base64
}
data "local_file" "service_a" {
filename = "${path.module}/init_script/${terraform.workspace}/service_a.sh"
}
几个值得注意的设计:
disable_api_termination = true:防止手滑删除节点。生产环境必须开associate_public_ip_address = false:节点不需要公网 IP,通过 NAT Gateway 出网- User Data 按环境区分:
init_script/${terraform.workspace}/service_a.sh,dev 和 prod 的启动脚本可以不同
4.2 User Data:节点启动时都干了什么
这是整个项目最有意思的部分之一。每个节点启动时会执行一个 Shell 脚本,做了这些事:
#!/bin/bash
set -ex
# 1. EKS Bootstrap:加入集群
B64_CLUSTER_CA="xxx..." # Base64 编码的集群 CA 证书
API_SERVER_URL="https://xxx.eks.amazonaws.com.cn"
K8S_CLUSTER_DNS_IP="192.168.0.10"
/etc/eks/bootstrap.sh eks-prod \
--kubelet-extra-args '\
--node-labels=app=service-a \
--container-log-max-files=5 \
--container-log-max-size=100Mi \
--system-reserved=cpu=50m,memory=1330Mi,ephemeral-storage=1Gi \
--kube-reserved=cpu=50m,memory=1330Mi,ephemeral-storage=1Gi' \
--b64-cluster-ca $B64_CLUSTER_CA \
--apiserver-endpoint $API_SERVER_URL \
--dns-cluster-ip $K8S_CLUSTER_DNS_IP
# 2. 安装基础工具
yum -y install jq curl
# 3. 内核参数优化:禁用 IPv6
cat <<EOF >>/etc/sysctl.conf
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.all.autoconf = 0
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.default.autoconf = 0
EOF
sysctl -p
# 4. containerd 配置:增大日志行长度限制
sed -i '13 a max_container_log_line_size = 65536' /etc/containerd/config.toml
systemctl restart containerd
# 5. SSH 安全加固:禁用弱密钥交换算法
echo 'KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,\
ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,\
diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,\
diffie-hellman-group18-sha512' | sudo tee -a /etc/ssh/sshd_config
sudo systemctl restart sshd
逐个解释:
- EKS Bootstrap:调用 AWS 提供的 bootstrap.sh 脚本,把节点注册到 EKS 集群。
--node-labels给节点打标签,后面 Pod 调度要用 - 资源预留:
system-reserved和kube-reserved各预留 50m CPU + 1330Mi 内存给系统和 kubelet。不预留的话,Pod 可能把系统资源吃光,导致节点 NotReady - 禁用 IPv6:如果你的 VPC 不用 IPv6,关掉它可以避免一些奇怪的网络问题
- containerd 日志行限制:默认 16KB,有些应用(比如 Java 的 stack trace)一行日志可能超过这个限制,改成 64KB
- SSH 加固:禁用弱密钥交换算法,这是安全合规的常见要求
4.3 Launch Template 版本控制:升级的核心机制
这是整篇文章最重要的概念之一。Launch Template 支持版本管理,每次修改(换 AMI、改 User Data、调安全组)都会创建一个新版本。节点组通过指定版本号来决定用哪个配置:
# 节点组引用 Launch Template 的特定版本
resource "aws_eks_node_group" "service_a" {
launch_template {
id = aws_launch_template.service_a[count.index].id
version = var.service_a_launch_version # 版本号!
}
}
# tfvars 里控制版本号
# environments/prod.tfvars
default_launch_version = 18
service_a_launch_version = 14
service_b_launch_version = 14
service_c_launch_version = 11
为什么不用 $Latest?因为你不想在 terraform apply 的时候,所有节点组同时开始滚动更新。用固定版本号,你可以:
- 先更新 Launch Template(创建新版本)
- 在 tfvars 里只改一个节点组的版本号
- apply,观察这个节点组的滚动更新
- 没问题后,再改下一个节点组的版本号
这就是受控滚动更新。
五、EKS 集群升级:完整流程
终于到了重头戏。EKS 集群升级分三大步:升级控制面、升级 Addons、升级节点。顺序不能乱。
5.1 升级前的准备工作
别急着动手,先做功课:
# 1. 查看当前版本
kubectl version --short
# 2. 查看 AWS 支持的 EKS 版本
aws eks describe-addon-versions --kubernetes-version 1.32 \
--query 'addons[].{Name:addonName,Versions:addonVersions[0].addonVersion}' \
--output table
# 3. 检查废弃 API(重要!)
brew install FairwindsOps/tap/pluto
pluto detect-all-in-cluster
# 4. 检查 PodDisruptionBudget
kubectl get pdb --all-namespaces
第 3 步特别重要。Kubernetes 每个版本都会废弃一些 API,如果你的 YAML 还在用旧 API,升级后可能直接报错。比如 1.22 移除了 extensions/v1beta1 的 Ingress,1.25 移除了 policy/v1beta1 的 PodSecurityPolicy。
5.2 第一步:升级 EKS 控制面
控制面升级是 AWS 托管的,你只需要改一个版本号:
# eks.tf
resource "aws_eks_cluster" "main" {
name = var.eks_name
version = var.k8s_version # 改这里
}
# environments/dev.tfvars
k8s_version = "1.32" # 从 1.28 改到 1.32
# 先在 dev 环境升级
bash tf-deploy.sh plan dev
bash tf-deploy.sh apply dev
# 控制面升级大约需要 15-30 分钟
注意:EKS 只支持逐版本升级。如果你当前是 1.28,不能直接跳到 1.32,必须 1.28 -> 1.29 -> 1.30 -> 1.31 -> 1.32。每次升级间隔建议至少观察一天。
5.3 第二步:升级 EKS Addons
控制面升级完后,Addons 的版本也要跟上。这个项目管理了四个核心 Addon:
resource "aws_eks_addon" "network-pod" {
addon_name = "vpc-cni"
addon_version = var.vpc_cni_version
resolve_conflicts = "OVERWRITE"
}
resource "aws_eks_addon" "dns" {
addon_name = "coredns"
addon_version = var.coredns_version
resolve_conflicts = "OVERWRITE"
}
resource "aws_eks_addon" "network-service" {
addon_name = "kube-proxy"
addon_version = var.kube_proxy_version
resolve_conflicts = "OVERWRITE"
}
resource "aws_eks_addon" "aws-ebs-csi" {
addon_name = "aws-ebs-csi-driver"
addon_version = var.aws_ebs_csi_driver_version
resolve_conflicts = "OVERWRITE"
}
怎么知道该用哪个版本?
aws eks describe-addon-versions \
--addon-name vpc-cni \
--kubernetes-version 1.32 \
--query 'addons[0].addonVersions[*].addonVersion'
resolve_conflicts = "OVERWRITE" 的意思是:如果 Addon 的配置被手动修改过,Terraform 会强制覆盖回来。这保证了 Terraform 是唯一的配置来源(Single Source of Truth)。
5.4 第三步:升级节点(滚动更新)
这是最复杂也最容易出问题的一步。节点升级的本质是:用新配置的节点替换旧节点。
流程是这样的:
- 修改 Launch Template(换 AMI 或改 User Data),创建新版本
- 在 tfvars 里更新对应节点组的 launch_version
terraform apply,EKS 开始滚动更新- EKS 自动执行:启动新节点 -> 等新节点 Ready -> Drain 旧节点 -> 终止旧节点
# 更新 tfvars
# environments/prod.tfvars
service_a_launch_version = 15 # 从 14 改到 15
# 执行
bash tf-run.sh prod plan
bash tf-run.sh prod apply
滚动更新期间,你可以实时观察:
# 观察节点状态
watch -n 5 'kubectl get nodes -l app=service-a -o wide'
# 观察 Pod 迁移
kubectl get pods -n service-a -o wide -w
# 查看节点组更新状态
aws eks describe-nodegroup \
--cluster-name eks-prod \
--nodegroup-name service-a \
--query 'nodegroup.{Status:status,UpdateConfig:updateConfig}'
5.5 升级顺序策略
15 个节点组不能一起升级,要有策略。核心原则:先低风险后高风险,先小规模后大规模。
- Phase 1:shared, default -- 跑基础设施组件,影响范围小,风险低,观察 30 min
- Phase 2:中等流量业务节点组 -- 流量中等,出问题影响有限,观察 1-2h
- Phase 3:高流量核心业务节点组 -- 核心业务,节点多,建议低峰期操作,观察 2-4h
- Phase 4:特殊节点组(独立 IAM Role)-- 需要额外验证权限是否正常
每个 Phase 之间至少间隔半天到一天,确认没有问题再继续。
六、部署脚本:自动化是生产力
6.1 MFA 凭证生成
这个项目的 AWS 账号开启了 MFA(多因素认证),每次操作都需要先生成临时凭证:
#!/bin/bash
# gen-credentials.sh -- MFA 凭证生成脚本
env=$1 # dev / staging / preprod / prod
mfa_code=$2 # 手机上的 6 位验证码
case $env in
dev | staging | preprod)
# dev/staging/preprod 共用一个 AWS 账号
bash sts-assume-dev.sh $mfa_code | \
jq -rc '.Credentials | (.AccessKeyId,.SecretAccessKey,.SessionToken)' | \
awk '{print "aws_access_key_id=\""$1"\"" ...}' > ./dev.credentials
;;
prod)
# prod 是独立的 AWS 账号
bash sts-assume-prod.sh $mfa_code | \
jq -rc '.Credentials | (...)' > ./prod.credentials
;;
esac
生成的临时凭证写入 .credentials 文件,格式是 Terraform 变量格式,可以直接作为 -var-file 传入。
注意:dev/staging/preprod 三个环境在同一个 AWS 账号下,prod 在另一个账号。这是很常见的企业级 AWS 账号架构--非生产环境共享一个账号,生产环境独立账号,通过 AWS Organizations 管理。
6.2 部署入口脚本
#!/bin/bash -eu
# tf-deploy.sh -- 基础版
command=$1 # plan / apply / destroy
environment=$2 # dev / staging / preprod / prod
case $environment in
dev | staging | preprod) aws_key="dev" ;;
prod) aws_key="prod" ;;
esac
terraform fmt
terraform workspace select $environment || terraform workspace new $environment
terraform $command \
-var-file environments/$environment.tfvars \
-var-file ./${aws_key}.credentials \
-parallelism 30
#!/bin/bash -eu
# tf-run.sh -- 增强版(凭证通过环境变量传入,更安全)
environment=$1
command=$2
terraform init -upgrade
terraform fmt
terraform workspace select $environment || terraform workspace new $environment
[ "prod" == $environment ] && source load-prod-credentials.sh
terraform $command \
-var-file environments/$environment.tfvars \
-var aws_access_key_id="${AWS_ACCESS_KEY_ID}" \
-var aws_secret_access_key="${AWS_SECRET_ACCESS_KEY}" \
-var aws_session_token="${AWS_SESSION_TOKEN}"
两个脚本的区别:tf-deploy.sh 从文件读凭证;tf-run.sh 从环境变量读凭证,更安全(凭证不落盘),还会自动 terraform init -upgrade。
-parallelism 30 值得说一下。默认 Terraform 并行度是 10,当你有 15 个节点组 + 15 个 Launch Template 时,并行度太低会导致 apply 很慢。调到 30 可以显著加速。
七、EKS Addons 深入:四大金刚
EKS 的四个核心 Addon,每个都有自己的职责,升级时要注意兼容性:
| Addon | 职责 | 升级风险 | 注意事项 |
| vpc-cni | Pod 网络,给每个 Pod 分配 VPC IP | 高 | 可能导致 Pod 网络短暂中断,建议低峰期 |
| coredns | 集群 DNS,解析 Service 名称 | 中 | 滚动更新通常无感知,确保 2+ 副本 |
| kube-proxy | Service 网络,维护 iptables/IPVS 规则 | 中 | DaemonSet 逐节点更新,版本需匹配 K8s |
| aws-ebs-csi | 块存储,动态创建/挂载 EBS 卷 | 低 | 不影响已挂载的 EBS 卷,需要 IAM 权限 |
Addon 升级的推荐顺序:aws-ebs-csi(风险最低)-> coredns -> kube-proxy -> vpc-cni(风险最高)。
vpc-cni 要特别小心。它负责给 Pod 分配 IP,升级过程中如果出问题,新创建的 Pod 可能拿不到 IP。建议在业务低峰期操作,并且提前确认 VPC 的 IP 地址池还有足够的余量。
八、踩坑实录:血泪教训
理论讲完了,来点真实的踩坑经验。
8.1 坑一:desired_size 被 Terraform 覆盖
场景:你用了 Cluster Autoscaler,它把 shared 节点组从 14 扩到了 20。然后你 terraform apply 改了个不相关的东西,Terraform 发现 desired_size 跟 tfvars 里的 14 不一样,贴心地帮你改回去了。20 个节点瞬间变成 14 个,6 个节点被终止,上面的 Pod 全部被驱逐。
解决方案:
lifecycle {
ignore_changes = [scaling_config[0].desired_size]
}
这行代码告诉 Terraform:"desired_size 这个字段你别管,让 Autoscaler 自己调。"
8.2 坑二:Launch Template 版本号忘了改
场景:你修改了 User Data 脚本,Terraform 自动创建了 Launch Template v15。但你忘了在 tfvars 里把版本号从 14 改成 15。结果 apply 之后,Launch Template 更新了,但节点组还在用 v14,新节点还是用旧配置启动。
更坑的是:你以为改了,其实没生效,直到某天出了问题才发现。
解决方案:每次修改 User Data 后,养成习惯立刻更新 tfvars 里的版本号。或者写个 CI 检查脚本,对比 Launch Template 最新版本和 tfvars 里的版本号。
8.3 坑三:PDB 阻止节点 Drain
场景:滚动更新时,EKS 需要 Drain 旧节点。但如果 Pod 配置了 PodDisruptionBudget(PDB),且 minAvailable 设得太高,Drain 会一直卡住。
# 这个 PDB 会阻止 Drain
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: my-app-pdb
spec:
minAvailable: 3 # 至少保持 3 个 Pod
selector:
matchLabels:
app: my-app
# 如果 my-app 只有 3 个副本,Drain 永远无法驱逐任何一个 Pod
解决方案:升级前检查所有 PDB,确保 minAvailable 小于副本数。或者用 maxUnavailable: 1 代替。
8.4 坑四:AMI 不兼容新版本 Kubernetes
场景:你升级了 EKS 控制面到 1.32,但节点还在用 1.28 的 AMI。kubelet 版本是 1.28,跟 1.32 的 API Server 通信出问题。
EKS 的版本偏差策略:kubelet 版本可以比 API Server 低最多 3 个小版本。1.28 的 kubelet 可以跟 1.31 的 API Server 工作,但跟 1.32 就超出范围了。
解决方案:升级控制面后,尽快更新 AMI 并滚动更新节点。不要让版本偏差超过 2 个小版本。
8.5 坑五:STS Token 过期
场景:你用 MFA 生成了临时凭证,开始 terraform apply。15 个节点组的滚动更新需要 2-3 小时,但 STS Token 默认有效期只有 1 小时。apply 到一半,Token 过期了。
解决方案:
- 生成 Token 时指定更长的有效期:
--duration-seconds 43200(12 小时) - 或者分批 apply:每次只改 2-3 个节点组的版本号,控制单次 apply 时间在 30 分钟内
- 如果 Token 过期导致 apply 中断,不要慌。重新生成 Token,再次
terraform apply,Terraform 会从中断的地方继续
九、最佳实践清单
| 类别 | 实践 | 原因 |
| 状态管理 | S3 + DynamoDB 远程 Backend | 状态文件加密存储,防止并发冲突 |
| 环境隔离 | Terraform Workspace + tfvars | 一套代码管多套环境,状态自动隔离 |
| 节点隔离 | 按业务拆分节点组 + Taint | 故障隔离,防止业务间互相影响 |
| 成本优化 | 非 prod 环境用 count=0 关闭专用节点组 | dev 环境不需要 15 个节点组 |
| 版本控制 | Launch Template 固定版本号 | 受控滚动更新,避免意外变更 |
| 升级策略 | 先控制面 -> Addons -> 节点,逐个节点组 | 降低风险,随时可回滚 |
| 安全 | STS 临时凭证 + MFA | 不用长期 AK/SK,降低泄露风险 |
| 安全 | SSH 弱算法禁用 | 安全合规要求 |
| 稳定性 | ignore_changes desired_size | 防止 Terraform 覆盖 Autoscaler 的调整 |
| 稳定性 | create_before_destroy | 先创建新资源再删除旧资源,减少中断 |
| 可观测 | 节点启动回调 + CloudWatch 日志 | 知道每个节点什么时候启动的,出问题好排查 |
| 内核优化 | 禁用 IPv6 + containerd 日志行限制 | 避免网络问题,防止日志截断 |
十、升级 Checklist
每次升级前,过一遍这个清单:
[ ] 阅读目标版本的 Release Notes 和 Breaking Changes
[ ] 用 pluto 扫描废弃 API
[ ] 检查所有 PDB,确保不会阻止 Drain
[ ] 确认 VPC IP 地址池余量充足
[ ] 查询目标版本兼容的 Addon 版本
[ ] 在 dev 环境完整走一遍升级流程
[ ] 准备回滚方案(控制面无法回滚,但节点可以)
[ ] 通知相关团队升级时间窗口
[ ] 生成足够长有效期的 STS Token
[ ] 升级控制面(逐版本,不能跳版本)
[ ] 升级 Addons(ebs-csi -> coredns -> kube-proxy -> vpc-cni)
[ ] 逐个节点组滚动更新(先低风险后高风险)
[ ] 每个节点组更新后观察 10-15 分钟
[ ] 全部完成后,跑一遍端到端测试
[ ] 更新文档,记录本次升级的版本号和变更
写在最后
EKS 集群管理看起来复杂,但核心就是三件事:隔离(环境隔离、节点隔离、权限隔离)、版本控制(Launch Template 版本、Addon 版本、K8s 版本)、渐进式变更(先 dev 后 prod,先小规模后大规模)。
把这三件事做好,你就能在老板说"升级集群"的时候,淡定地回一句:"已经在 dev 上验证过了,prod 预计周三完成。"而不是"让我先查查怎么升级..."
Terraform + EKS 的组合确实有不少坑,但一旦你把项目结构搭好、把流程跑通,后续的维护和升级就是改几个数字的事。这大概就是 Infrastructure as Code 的魅力:第一次很痛苦,后面越来越轻松。
希望这篇文章能帮你少踩几个坑。如果你也在管理多环境 EKS 集群,欢迎留言交流。
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=xdbp05fgqmd
📜 版权声明
本文作者:王梓 | 原文链接:https://www.bthlt.com/note/14920599-TegEKS集群升级与多环境部署实战
出处:葫芦的运维日志 | 转载请注明出处并保留原文链接
📜 留言板
留言提交后需管理员审核通过才会显示