本文讲一套用完就拆的 EKS 环境,覆盖版本与权限、成本结构、资源边界、集群与应用部署、销毁顺序、残留复核。
环境:Terraform 1.14.9 + AWS provider 6.x + terraform-aws-eks 21.x,EKS 1.35,kubectl 1.35,区域 us-east-1。起因:一次练手集群只删了节点组,NAT 网关和两个 ELB 挂了 11 天,账单多出 96 美元。
版本与权限
工具版本先钉死,EKS 相关的返工九成来自版本漂移。
| 工具 | 版本 | 用途 |
|---|---|---|
| Terraform | 1.14.9 | 建 VPC、EKS、节点组 |
| AWS provider | 6.x | 资源声明 |
| terraform-aws-eks | 21.x | 集群与托管节点组模块 |
| Kubernetes | 1.35 | 集群版本 |
| kubectl | 1.35 | 部署与验证 |
| AWS CLI | 2.31 | 残留资源复核 |
bash
terraform version
aws --version
kubectl version --client
aws sts get-caller-identity --query Account --output text
练手环境直接用 AdministratorAccess,跑完连角色一起删,比逐条拼最小权限省事。backend 用 local 就够,state 里只有集群地址和几十个资源 ID,推到远端反而多一层要清的东西。
| 阶段 | 需要的权限 |
|---|---|
| 建网络 | ec2:*Vpc*、ec2:*Subnet*、ec2:*NatGateway*、ec2:AllocateAddress |
| 建集群 | eks:*、iam:CreateRole、iam:AttachRolePolicy |
| 建节点组 | autoscaling:*、ec2:RunInstances、iam:PassRole |
| 销毁 | 同创建,外加 ec2:DeleteVpc、logs:DeleteLogGroup |
⚠️ EKS 1.33 的标准支持在 2026 年 7 月 29 日结束,新建集群别写 1.33,一上线就落进扩展支持,控制面单价翻倍。
成本结构
哪几行在按小时跳,决定了销毁时要盯什么。
| 资源 | 计费口径(us-east-1 按需) | 月成本量级 | destroy 覆盖 |
|---|---|---|---|
| EKS 控制面 | $0.10/小时/集群 | 约 $73 | 是 |
| 托管节点组 m6i.large ×2 | $0.096/小时/台 | 约 $140 | 是 |
| NAT 网关 | 约 $33 | 是,EIP 除外 | |
| ALB / CLB | $0.0225/小时 + LCU | 约 $18 | 否 |
| EBS gp3 | $0.08/GB·月 | 按卷算 | 否 |
| CloudWatch Logs | $0.03/GB 存储 | 按量 | 否 |
控制面一创建就计费,跟有没有节点无关;节点组缩到 0 也省不掉这 $73。
价格随区域和合约浮动,列出来只为标出哪几行是每天在跳的。按每天 8 小时算,这套环境跑一周约 60 美元,跑满一个月接近 260 美元。
环境拓扑

公有子网只放 ALB 和 NAT 网关,节点组全在私有子网,出网走单个 NAT。这样节点拿不到公网地址,代价是所有镜像拉取都算 NAT 流量,一个 200MB 的镜像反复拉几次就是几美分。
plain
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 6.0"
name = "${var.cluster_name}-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
enable_nat_gateway = true
single_nat_gateway = true
enable_dns_hostnames = true
tags = { Env = "sandbox", TTL = "8h" }
}
single_nat_gateway = true 只建一个 NAT,两个可用区共用,练手环境够用且省一半。
资源边界
销毁失败的根源是把 terraform destroy 当成了全部。

| 类别 | 谁创建 | 谁删除 | 漏删的代价 |
|---|---|---|---|
| VPC、子网、EKS、节点组 | Terraform | terraform destroy |
无 |
| ALB / CLB | Service 控制器 | kubectl delete svc |
每小时 $0.0225 起 |
| EBS 卷 | CSI 驱动 | kubectl delete pvc |
$0.08/GB·月 |
| EIP | Terraform 分配 | aws ec2 release-address |
每小时 $0.005 |
| ENI | VPC CNI / ELB | 随归属资源释放 | 卡住 VPC 删除 |
| 日志组 | EKS 控制面 | 手动删 | $0.03/GB 起 |
destroy 只逆序删 state 里的对象,表里后四行必须单独清,否则账单永远不归零。
这张表决定了销毁脚本要写几行。只跑 destroy 的话,前两行能清掉,后四行会留在账号里继续计费,直到你按标签把它们捞出来。
集群定义
plain
terraform {
required_version = ">= 1.9.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 6.0" }
}
backend "local" {}
}
provider "aws" { region = var.region }
variable "region" { default = "us-east-1" }
variable "cluster_name" { default = "demo-eks" }
plain
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 21.0"
name = var.cluster_name
kubernetes_version = "1.35"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
enable_cluster_creator_admin_permissions = true
eks_managed_node_groups = {
default = {
instance_types = ["m6i.large"]
capacity_type = "ON_DEMAND"
min_size = 1
max_size = 3
desired_size = 2
}
}
tags = { Env = "sandbox", TTL = "8h" }
}
kubernetes_version 写主次版本号,补丁版本交给 EKS 自己跟。
创建与接入
bash
terraform init -upgrade
terraform plan -out=tfplan
terraform apply tfplan
首次 apply 约 15--20 分钟,控制面占大头。中途 Ctrl+C 会留下半建的控制面,重跑前先 plan 看一眼差异。
latex
Apply complete! Resources: 63 added, 0 changed, 0 destroyed.
Outputs:
cluster_endpoint = "https://A1B2C3.gr7.us-east-1.eks.amazonaws.com"
cluster_name = "demo-eks"
bash
aws eks update-kubeconfig --name demo-eks --region us-east-1
kubectl get nodes -o wide
latex
NAME STATUS ROLES AGE VERSION
ip-10-0-1-24.ec2.internal Ready <none> 92s v1.35.4-eks-1f0ca9d
ip-10-0-2-31.ec2.internal Ready <none> 92s v1.35.4-eks-1f0ca9d
应用部署
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 2
selector:
matchLabels: { app: web }
template:
metadata:
labels: { app: web }
spec:
containers:
- name: web
image: public.ecr.aws/nginx/nginx:1.27-alpine
ports:
- containerPort: 80
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 256Mi }
yaml
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: LoadBalancer
selector: { app: web }
ports:
- port: 80
targetPort: 80
type: LoadBalancer 会由控制器在公有子网拉一个 CLB,这个 CLB 不在 state 里,图2 的第①类。镜像用 public.ecr.aws 的 nginx,省掉 imagePullSecret;接私有仓库则要给节点组角色挂 ECR 只读策略,否则 Pod 停在 ImagePullBackOff。
bash
kubectl apply -f k8s/web.yaml
kubectl rollout status deploy/web --timeout=180s
kubectl get svc web -w
latex
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
web LoadBalancer 172.20.11.204 a1b2c3d4e5-1234567890.us-east-1.elb.amazonaws.com 80:31234/TCP
验证
bash
LB=$(kubectl get svc web -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
curl -s -o /dev/null -w "%{http_code}\n" "http://$LB"
kubectl get pods -o wide
latex
200
NAME READY STATUS RESTARTS AGE IP NODE
web-6b7c8d9f4b-2xk9p 1/1 Running 0 3m 10.0.1.37 ip-10-0-1-24.ec2.internal
web-6b7c8d9f4b-9wq4n 1/1 Running 0 3m 10.0.2.44 ip-10-0-2-31.ec2.internal
验证只看两件事:Pod 全部 Ready,LB 域名返回 200。缺一件就退回部署,不要进销毁。
销毁顺序

顺序错了会留下删不掉的孤儿资源,按下面五步走。
latex
kubectl delete svc web ──> CLB 释放,约 60 s
kubectl delete pvc --all ──> EBS 卷删除,reclaimPolicy=Delete
kubectl delete ns demo ──> 剩余对象清空
terraform destroy ──> state 内 63 个资源逆序删除
aws 复核 ──> EIP / ENI / 日志组 计数归零
bash
kubectl delete svc web --ignore-not-found
kubectl delete pvc --all -n demo --ignore-not-found
sleep 60
terraform destroy -auto-approve
⚠️ 先 destroy 再删 Service 会直接卡死:控制面没了,kubectl 连不上,CLB 也没人回收。
| 阶段 | 命令 | 耗时 | 失败后果 |
|---|---|---|---|
| 释放负载均衡 | kubectl delete svc |
60 s | CLB 常驻计费 |
| 释放存储 | kubectl delete pvc |
30 s | 卷 in-use 卡住子网删除 |
| 删集群 | terraform destroy |
10--15 min | 半删状态难恢复 |
| 清残留 | aws ec2 describe-* |
1 min | 账单继续跑 |
destroy 期间不要关终端。删控制面时会反复轮询,看着像卡住,实际还在跑;中断后 state 里留一批半删对象,下次 apply 直接报冲突。
销毁排障
| 报错 | 根因 | 处理 |
|---|---|---|
DependencyViolation: resource sg-xxx has a dependent object |
安全组仍被 ENI 引用 | 按 Name=group-id 反查 ENI,删孤儿网卡 |
VPC has dependencies and cannot be deleted |
子网内还有 ELB、NAT 或 ENI | 从 ENI 反查归属资源,逐个删 |
InvalidParameterException: Cluster has nodegroups attached |
节点组未删净 | aws eks list-nodegroups 后 delete-nodegroup |
VolumeInUse: vol-xxx is still attached |
Pod 未退或 PVC 未删 | 先 scale 到 0,再删 PVC |
destroy 停在 Still destroying... 20 分钟 |
控制面删除本身慢 | 等,中断会留半删状态 |
| EIP 仍计费 | NAT 删除后地址未释放 | aws ec2 release-address --allocation-id |
bash
aws ec2 describe-network-interfaces \
--filters Name=group-id,Values=sg-0123456789abcdef0 \
--query 'NetworkInterfaces[].[NetworkInterfaceId,Status,Description]' --output table
ENI 反查是解 DependencyViolation 最快的一步,网卡描述里通常直接写着 ELB app/demo/... 或 Amazon EKS。一次反查能捞出两三个孤儿网卡,删完再跑 destroy 基本就通了。
账单复核
bash
aws eks list-clusters --query 'length(clusters)'
aws ec2 describe-nat-gateways --filter Name=state,Values=available --query 'length(NatGateways)'
aws ec2 describe-addresses --filters Name=tag:Env,Values=sandbox --query 'length(Addresses)'
aws ec2 describe-volumes --filters Name=status,Values=available --query 'length(Volumes)'
aws logs describe-log-groups --log-group-name-prefix /aws/eks/demo-eks --query 'length(logGroups)'
| 复核项 | 期望值 | 不为 0 时怎么办 |
|---|---|---|
| EKS 集群数 | 0 | aws eks delete-cluster |
| NAT 网关数 | 0 | 手动 delete-nat-gateway |
| EIP 数 | 0 | release-address |
| 可用 EBS 卷 | 0 | delete-volume |
/aws/eks/demo-eks 日志组 |
0 | logs delete-log-group |
EIP 和可用卷建议按 Env=sandbox 标签过滤,账号里有其他资源时全局计数不会归零。五项全 0 才算结束,有一项不为 0 就回到对应的删除命令,别指望下一轮 destroy 补上。
总结
可销毁的核心在顺序:先让 K8s 把自己的云资源收回去,再 destroy,最后按 ENI 反查残留。
| 检查点 | 判据 |
|---|---|
| 版本 | 集群版本仍在标准支持期内 |
| 标签 | 所有资源带 TTL 与 Env,方便按标签复核 |
| 销毁 | 先删 Service/PVC,再 destroy |
| 复核 | 五项计数归零才算结束 |