Teg

EKS集群升级与多环境部署实战

2026/02/26 20:46 31 次阅读王梓
★ 打赏
✸ ✸ ✸

EKS 集群升级与多环境部署实战:从踩坑到丝滑

管理一个 EKS 集群不难,难的是管理四个环境、十五个节点组、每个节点组都有自己的 Launch Template 版本,然后老板跟你说:"下周把所有集群从 1.28 升到 1.32,别影响业务。"

这篇文章来自一个真实的生产项目。四套环境(dev / staging / preprod / prod),每套环境十几个节点组,全部用 Terraform 管理。我会把整个架构拆开给你看,从 Terraform 项目结构到节点滚动更新,从 Launch Template 版本控制到 MFA 凭证管理,每一步都是踩过坑之后总结出来的。

一、整体架构:先看全貌

先上一张架构图,让你对整个项目有个直观的认识:

EKS 多环境架构总览 Terraform 管理层 S3 Backend DynamoDB Lock Workspace 切换 MFA 凭证生成 四套环境 (Terraform Workspace) dev (Account A) | staging (Account A) | preprod (Account A) | prod (Account B) EKS Control Plane (v1.32) Addons: vpc-cni | coredns | kube-proxy | aws-ebs-csi-driver Managed Node Groups (15+) shared label: app=others All Envs default label: app=others Prod Only service-a taint: dedicated Prod Only service-b taint: dedicated Prod Only service-c taint: dedicated Prod Only service-d Custom IAM Role Prod Only ... +10 more groups Launch Template (per node group) AMI | EBS gp3 | Security Groups | User Data (bootstrap + sysctl + containerd + SSH hardening) Shared/Default Dedicated (Taint) Custom IAM

 

看起来复杂?别急,我们一层一层拆。

 

二、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-reservedkube-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 第三步:升级节点(滚动更新)

 

这是最复杂也最容易出问题的一步。节点升级的本质是:用新配置的节点替换旧节点。

 

流程是这样的:

 

  1. 修改 Launch Template(换 AMI 或改 User Data),创建新版本
  2. 在 tfvars 里更新对应节点组的 launch_version
  3. terraform apply,EKS 开始滚动更新
  4. 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集群升级与多环境部署实战

出处:葫芦的运维日志 | 转载请注明出处并保留原文链接

📜 留言板

留言提交后需管理员审核通过才会显示