1.项目背景
当Kuberntes集群越来越多,当服务实现了无状态以后。大量集群在闲置时间处于低效能状态,为了节省AWS资源和费用,想要通过动态的扩缩容机制来提高集群的使用率。
目前成熟扩缩容方案是Karpenter(纵向扩缩容)+Keda(横向扩缩容),所以要总结这种方案的部署和常用方案。
# Karpenter + KEDA 快速参考总览
> 这是所有文档的总参考,先读这份建立全局认识,再按需查看详细文档
> 集群:ltp-eks-nonprod / ap-northeast-1 / Account 29xxxxxx
---
## 一、核心概念
### 整体目标
实现「流量上涨 → 自动扩 Pod → 资源不够自动扩 Node → 流量下降 → 自动缩 Pod → 自动删 Node」的弹性伸缩。
### 两层扩缩分工
| 层级 | 组件 | 职责 | 触发信号 |
|------|------|------|----------|
| Pod 层 | KEDA + HPA | 扩缩 Pod 副本数 | Prometheus 指标(RPS)|
| Node 层 | Karpenter | 扩缩 EC2 节点 | Pending Pod(调度失败)|
**两者不直接通信,靠「Pending Pod」耦合。** KEDA 产出扩容需求,Karpenter 看到 Pending Pod 才开机器。
### 三层权限模型
```
第零层:OIDC Provider(IRSA 基础,让 pod 能假设 IAM Role)
↓
第一层:Controller IAM Role(Karpenter controller 调 EC2/SQS API)
- ec2:RunInstances/CreateFleet/TerminateInstances(开/删机器)
- ec2:Describe*(查机型/subnet/sg)
- iam:PassRole(传 node role 给 EC2)
- sqs:*(方案B 读中断队列)
↓
第二层:Node IAM Role(EC2 实例加入集群 + 拉镜像 + 挂 EBS)
- AmazonEKSWorkerNodePolicy
- AmazonEC2ContainerRegistryReadOnly
- AmazonEBSCSIDriverPolicy
```
---
## 二、两种方案对比
| 维度 | 方案A:ON_DEMAND 精简版 | 方案B:SPOT + SQS 完整版 |
|------|------------------------|--------------------------|
| 实例类型 | on-demand(稳定不回收)| spot(便宜可能被回收)|
| 成本 | 中(标准价)| 最低(spot 折扣 60-90%)|
| 中断风险 | 无 | 有(AWS 提前 2 分钟通知)|
| 中断处理 | 不需要 | SQS+EventBridge 优雅迁移 |
| 需要 SQS | ❌ 否 | ✅ 是 |
| 需要 EventBridge | ❌ 否 | ✅ 是 |
| 动态扩缩 | ✅ 是 | ✅ 是 |
| 部署复杂度 | 低(1 个 IAM Role)| 中(Role + SQS + 3 条规则)|
### 为什么 SPOT 需要 SQS+EventBridge
SQS 不是"创建 spot"的前提(任何方式都能创建 spot),而是 **Karpenter 优雅处理 spot 中断**的唯一机制:
```
无 SQS:AWS 收回 spot 实例 → Pod 直接被 kill → 业务中断
有 SQS:AWS 提前 2 分钟发中断事件 → EventBridge → SQS → Karpenter
→ 主动 drain(Pod 在别处重建)→ 实例被收回时 Pod 已迁移,业务无感
```
Karpenter 的中断监听代码只认 SQS,不能用 Redis/RDS/其他队列替代。
### 方案选型
- **选方案A**:生产环境求稳定,不想引入额外组件,成本敏感度中等
- **选方案B**:成本敏感想最大化省钱,能接受偶尔中断(已优雅处理),无状态业务
- **生产推荐组合**:managed node group (ON_DEMAND) 跑有状态服务 + Karpenter + SPOT + SQS 跑无状态业务
---
## 三、架构图
### 方案A:ON_DEMAND 精简版架构图
```
┌─────────────────────────────────────────────────────────────────────┐
│ 流量(压测/生产) │
└──────────────────────────────┬──────────────────────────────────────┘
│
▼
┌────────────────────────────────┐
│ http-app Service (keda ns) │
│ ClusterIP: 80, 9113 │
└────────────────┬───────────────┘
│
▼
┌──────────────────────────────────────────────────┐
│ KEDA ScaledObject (scaled.yaml) │
│ - Prometheus trigger │
│ - query: rate(nginx_http_requests_total[1m]) │
│ - threshold: 3 │
│ - min:1, max:10 │
└──────────────────────┬───────────────────────────┘
│ 每 30s poll Prometheus
▼
┌────────────────────────────────┐
│ Prometheus (monitoring ns) │
│ kube-prom-stack │
│ ServiceMonitor 采集 http-app │
└────────────────────────────────┘
│ 指标超 threshold
▼
┌──────────────────────────────────────────────────┐
│ HPA → Deployment replicas 1→10 │
│ nodeSelector: nodepool=default │
│ toleration: karpenter.sh/disruption │
└──────────────────────┬───────────────────────────┘
│ Pod 指定去 Karpenter 节点
▼
┌──────────────────────────────┴──────────────────────────────────────┐
│ │
│ ┌─────────────────────────┐ ┌─────────────────────────────────┐ │
│ │ managed node group │ │ Karpenter NodePool (动态) │ │
│ │ (Terraform 管理) │ │ karpenter-nodepool-simple.yaml │ │
│ │ │ │ │ │
│ │ r6a.2xlarge × 1 常驻 │ │ capacity-type: on-demand │ │
│ │ desired_size=1 │ │ 机型: r6a/r5a 系列 │ │
│ │ │ │ taint: karpenter.sh/disruption │ │
│ │ 跑系统组件: │ │ label: nodepool=default │ │
│ │ - kube-proxy │ │ │ │
│ │ - VPC CNI │ │ Pod 来 → 现有 node 不够 │ │
│ │ - CoreDNS │ │ → Pending │ │
│ │ - KEDA operator │ │ → Karpenter 开 EC2 (on-demand) │ │
│ │ │ │ → Node Ready → Pod Running │ │
│ │ (无 taint,系统组件在此) │ │ Pod 走光 → 60s 后删 node │ │
│ └─────────────────────────┘ └─────────────────────────────────┘ │
│ ↑ ↑ │
│ │ │ │
│ Terraform 维持 Controller IAM Role │
│ Karpenter 碰不到 (karpenter-controller-*) │
│ (无 discovery tag) EC2 创建/查询/终止权限 │
│ (无 SQS 权限) │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
**方案A 特点:**
- 只创建 1 个 IAM Role(Controller Role),无 SQS/EventBridge
- on-demand 实例不会被 AWS 回收,无中断要处理
- helm 安装不传 `interruptionQueueName`
- 与 Terraform managed node group 通过 taint+nodeSelector 隔离并存
---
### 方案B:SPOT + SQS 完整版架构图
```
┌─────────────────────────────────────────────────────────────────────┐
│ 流量(压测/生产) │
└──────────────────────────────┬──────────────────────────────────────┘
│
▼
┌────────────────────────────────┐
│ http-app Service (keda ns) │
│ ClusterIP: 80, 9113 │
└────────────────┬───────────────┘
│
▼
┌──────────────────────────────────────────────────┐
│ KEDA ScaledObject (scaled.yaml) │
│ - Prometheus trigger │
│ - threshold: 3 │
│ - min:1, max:10 │
└──────────────────────┬───────────────────────────┘
│
▼
┌────────────────────────────────┐
│ Prometheus (monitoring ns) │
└────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────┐
│ HPA → Deployment replicas 1→10 │
└──────────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────┴──────────────────────────────────────┐
│ │
│ ┌─────────────────────────┐ ┌─────────────────────────────────┐ │
│ │ managed node group │ │ Karpenter NodePool (动态) │ │
│ │ (Terraform 管理) │ │ karpenter-nodepool-spot.yaml │ │
│ │ │ │ │ │
│ │ r6a.2xlarge × 1 常驻 │ │ capacity-type: spot (+on-demand)│ │
│ │ desired_size=1 │ │ 机型: r6a/r5a 多机型 │ │
│ │ │ │ taint: karpenter.sh/disruption │ │
│ │ 跑系统组件 │ │ │ │
│ │ - kube-proxy │ │ Pod 来 → 开 spot EC2 (最便宜) │ │
│ │ - VPC CNI │ │ → Node Ready → Pod Running │ │
│ │ - CoreDNS │ │ Pod 走光 → 60s 后删 node │ │
│ │ - KEDA operator │ │ │ │
│ └─────────────────────────┘ └──────────────┬──────────────────┘ │
│ │ │
│ │ spot 实例被回收 │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐│
│ │ AWS Spot 中断事件链路 ││
│ │ ││
│ │ AWS 提前 2 分钟通知 ││
│ │ │ ││
│ │ ▼ ││
│ │ ┌──────────────────┐ 转发 ┌──────────────────┐ ││
│ │ │ EventBridge │ ────→ │ SQS 队列 │ ││
│ │ │ 3 条规则: │ │ ltp-eks-nonprod- │ ││
│ │ │ - spot 中断 │ │ karpenter │ ││
│ │ │ - rebalance │ │ │ ││
│ │ │ - unhealthy │ │ (队列访问策略 │ ││
│ │ └──────────────────┘ │ 允许 EventBridge │ ││
│ │ │ 写入) │ ││
│ │ └─────────┬─────────┘ ││
│ │ │ sqs:ReceiveMessage ││
│ │ ▼ ││
│ │ ┌──────────────────┐ ││
│ │ │ Karpenter │ ││
│ │ │ Controller │ ││
│ │ │ (IRSA 假设 Role) │ ││
│ │ │ │ ││
│ │ │ 收到中断通知 │ ││
│ │ │ → drain 节点 │ ││
│ │ │ → Pod 在别处重建 │ ││
│ │ │ → 实例被收回时 │ ││
│ │ │ Pod 已安全迁移 │ ││
│ │ └──────────────────┘ ││
│ └─────────────────────────────────────────────────────────────────┘│
│ │
│ Controller IAM Role (karpenter-controller-*) │
│ - EC2 创建/查询/终止权限 │
│ - SQS 权限 (sqs:DeleteMessage/GetQueueUrl/ReceiveMessage) │
│ - iam:PassRole │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
**方案B 特点:**
- 创建 Controller Role + SQS 队列 + 3 条 EventBridge 规则
- Controller Role 比 A 多了 SQS 权限
- SQS 队列访问策略允许 EventBridge 写入
- helm 安装传 `interruptionQueueName`
- spot 优先(最便宜),无库存时回退 on-demand
- 中断时 Karpenter 提前优雅迁移 Pod
---
## 四、扩缩容联动链路(两方案通用)
### 扩容链路
```
1. 压测流量上涨
↓
2. Prometheus 每 15s scrape 一次 http-app 指标
↓
3. KEDA 每 30s poll Prometheus,发现 rate > threshold(3)
↓
4. KEDA 更新 HPA 期望副本数
↓
5. HPA 把 Deployment replicas 从 1 调到 10
↓
6. 新 Pod 带 nodeSelector=nodepool=default
↓
┌───────────────────────────────────┐
│ 现有 node 资源够? │
└──────────────┬────────────────────┘
够 │ 不够
↓ │ ↓
Pod Running │ Pod Pending
│ ↓
│ Karpenter 检测 Pending Pod
│ ↓
│ 选机型 + 开 EC2 (方案A:on-demand / 方案B:spot)
│ ↓
│ 新 Node Ready (约 2-3 分钟,EC2 冷启动)
│ ↓
│ Pending Pod 调度 → Running
```
### 缩容链路
```
1. 停止压测
↓
2. rate(...[1m]) 窗口归零(约 1 分钟)
↓
3. HPA 算出期望 < 当前副本数
↓ (stabilizationWindowSeconds: 60 冷却)
4. KEDA 缩 Pod:10 → 5 → 2 → 1(每 30s 砍 50%)
↓
5. Pod 减少 → Karpenter 节点出现空闲
↓ (consolidateAfter: 60s)
6. Karpenter 删空节点(只删带 discovery tag 的,不碰 managed node group)
↓
7. 最终状态:
- Pod 回到 minReplicaCount=1
- Karpenter 节点全删
- managed node group 的 1 台常驻节点不受影响
```
### 延迟时间线
| 阶段 | 扩容 | 缩容 |
|------|------|------|
| 指标采集 | 15s scrape | 1m rate 窗口归零 |
| KEDA/HPA 响应 | 30s poll + 30s HPA | 60s stabilization |
| Pod 层变化 | 1→10(约 2 分钟)| 10→1(约 2 分钟)|
| Node 层变化 | 2-3 分钟(EC2 冷启动)| 60s consolidateAfter |
| **总延迟** | **约 3-4 分钟** | **约 5-6 分钟** |
---
## 五、文件清单与阅读顺序
### 必读文档(按顺序)
| 顺序 | 文件 | 何时读 |
|------|------|--------|
| 1 | **QUICK-REFERENCE.md**(本文件)| 先读,建立全局认识 |
| 2 | KARPENTER-ONDEMAND-AND-SPOT-GUIDE.md | 部署时精读,含实战记录 |
| 3 | MANUAL-PERMISSION-CHECKLIST.md | 手动配置权限时参考 |
| 4 | PERMISSION-TROUBLESHOOTING.md | 遇到权限问题时排查 |
### 配置文件(按方案选)
| 文件 | 方案A | 方案B |
|------|-------|-------|
| Terraform IAM | `karpenter-iam-simple.tf` | `karpenter-iam-full.tf` |
| NodePool | `karpenter-nodepool-simple.yaml` | `karpenter-nodepool-spot.yaml` |
### 共用文件
| 文件 | 用途 |
|------|------|
| `http-app-deployment.yaml` | http-app + Service + ServiceMonitor |
| `scaled.yaml` | KEDA ScaledObject |
| `deploy-karpenter-keda.sh` | 一键部署脚本(可选)|
---
## 六、关键参数速查
### KEDA(scaled.yaml)
| 参数 | 值 | 含义 |
|------|-----|------|
| threshold | 3 | 每 3 RPS 扩 1 Pod |
| minReplicaCount | 1 | 最少 Pod |
| maxReplicaCount | 10 | 最多 Pod |
| cooldownPeriod | 60s | 标记 inactive 前等待 |
| stabilizationWindowSeconds | 60 | HPA 缩容冷却 |
| scaleDown Percent | 50% / 30s | 缩容速度 |
### Karpenter(NodePool)
| 参数 | 方案A | 方案B | 含义 |
|------|-------|-------|------|
| capacity-type | on-demand | spot | 实例类型 |
| instance-types | r6a/r5a | r6a/r5a 多机型 | 机型列表 |
| limits.cpu | 100 | 100 | 总 CPU 上限 |
| consolidateAfter | 60s | 60s | 空节点回收等待 |
### helm 参数
| 参数 | 方案A | 方案B |
|------|-------|-------|
| clusterName | ltp-eks-nonprod | ltp-eks-nonprod |
| role-arn | controller role ARN | controller role ARN |
| interruptionQueueName | 不传 | SQS 队列名 |
---
## 七、与现有 Terraform 的关系
你的 `node-group` terraform(managed node group)**两种方案都不需要改**:
- 现有 `eks_managed_node_groups` 的 r6a.2xlarge 保留,跑系统组件
- Karpenter 独立管理一批动态节点,跑业务 Pod
- 两批节点通过 taint + nodeSelector 隔离,互不干扰
- Terraform 不感知 Karpenter 的节点(不在 ASG 里,不带 discovery tag)
- Karpenter 只删带 `karpenter.sh/discovery=ltp-eks-nonprod` tag 的 EC2,碰不到 managed node group
- Terraform 的 `desired_size=1` 始终维持,不报 drift
**Karpenter 是额外的动态扩缩层,在现有基础上新增,不冲突。**
---
## 八、关键命令速查
```bash
# === 部署 ===
terraform apply -var="cluster_name=ltp-eks-nonprod" # 创建 IAM/SQS
helm install karpenter karpenter/karpenter ... # 安装 controller
kubectl apply -f karpenter-nodepool-*.yaml # 部署 NodePool
kubectl apply -f http-app-deployment.yaml # 部署应用
kubectl apply -f scaled.yaml # 部署 KEDA
# === 观察扩缩容 ===
kubectl get deploy -n keda http-app -w
kubectl get hpa -n keda keda-hpa-http-app-scaledobject -w
kubectl get node -w
kubectl get events -A --field-selector reason=Karpenter -w
# === 压测 ===
kubectl run -n keda loadgen --rm -it --image=busybox -- /bin/sh
# pod 内:while true; do wget -q -O /dev/null http://http-app.keda.svc.cluster.local/; done
# === 节点区分 ===
kubectl get nodes -l nodepool=default # Karpenter 节点
kubectl get nodes -l '!nodepool' # managed node group 节点
# === 排查 ===
kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter --tail=50
kubectl describe scaledobject -n keda http-app-scaledobject
```
2.方案内容导航
-
主文档: KARPENTER-ONDEMAND-AND-SPOT-GUIDE.md


1 # Karpenter 完整方案指南:ON_DEMAND 精简版 + SPOT 完整版 2 3 > 集群:`ltp-eks-nonprod` / 区域:`ap-northeast-1` / Account:`29xxxxxxxxx` 4 > Node Role:`EKSInformalNodeRole`(复用现有 managed node group) 5 > 在现有 Terraform managed node group 基础上新增 Karpenter 动态扩缩容能力 6 7 --- 8 9 ## 目录 10 11 1. [两种方案对比](#1-两种方案对比) 12 2. [方案选型建议](#2-方案选型建议) 13 3. [前置条件(通用)](#3-前置条件通用) 14 4. [方案A:ON_DEMAND 精简版部署](#4-方案aon_demand-精简版部署) 15 5. [方案B:SPOT + SQS 完整版部署](#5-方案bspot--sqs-完整版部署) 16 6. [从方案A切换到方案B](#6-从方案a切换到方案b) 17 7. [共用配置文件](#7-共用配置文件) 18 8. [验证扩缩容](#8-验证扩缩容) 19 9. [故障排查](#9-故障排查) 20 10. [文件清单](#10-文件清单) 21 22 --- 23 24 ## 1. 两种方案对比 25 26 | 维度 | 方案A:ON_DEMAND 精简版 | 方案B:SPOT + SQS 完整版 | 27 |------|------------------------|--------------------------| 28 | 实例类型 | on-demand(稳定,不回收)| spot(便宜,可能被回收)| 29 | 成本 | 中(标准 EC2 价格)| 最低(spot 折扣 60-90%)| 30 | 中断风险 | 无 | 有(AWS 提前 2 分钟通知)| 31 | 中断处理 | 不需要 | SQS+EventBridge 优雅迁移 | 32 | 需要 SQS | ❌ 否 | ✅ 是 | 33 | 需要 EventBridge | ❌ 否 | ✅ 是 | 34 | 动态扩缩 | ✅ 是 | ✅ 是 | 35 | 与 Terraform 冲突 | 否 | 否 | 36 | 部署复杂度 | 低(只创建 1 个 IAM Role)| 中(IAM Role + SQS + EventBridge)| 37 38 ### 为什么 SPOT 需要 SQS+EventBridge 39 40 ``` 41 Spot 实例被 AWS 收回时,只提前 2 分钟通知: 42 43 无 SQS+EventBridge(无法优雅处理): 44 AWS 收回实例 → Pod 直接被 kill → 业务中断几秒~几十秒 45 Karpenter 事后才发现节点没了,才重建 Pod 46 47 有 SQS+EventBridge(优雅迁移): 48 AWS 提前 2 分钟发中断事件 49 → EventBridge 规则捕获 50 → 转发到 SQS 队列 51 → Karpenter 从 SQS 读取 52 → Karpenter 主动 drain(驱逐 Pod,在别处重建) 53 → 实例被收回时,Pod 早已安全迁移,业务无感 54 ``` 55 56 **SQS 不是"创建 spot"的前提**(任何方式都能创建 spot),而是 **Karpenter 优雅处理 spot 中断**的唯一机制。Karpenter 的中断监听代码只认 SQS,不能用 Redis/RDS/其他队列替代。 57 58 --- 59 60 ## 2. 方案选型建议 61 62 ### 选方案A(ON_DEMAND 精简版)的情况 63 64 - 生产环境,对稳定性要求高 65 - 不想引入额外组件(SQS、EventBridge) 66 - 成本敏感度中等 67 - 团队不熟悉 spot 管理 68 69 ### 选方案B(SPOT + SQS 完整版)的情况 70 71 - 成本敏感,想最大化节省 EC2 费用 72 - 能接受偶尔的中断(已优雅处理) 73 - 有状态服务不跑 spot(只跑无状态业务) 74 - 团队能维护 SQS+EventBridge 75 76 ### 生产推荐组合 77 78 - **managed node group (ON_DEMAND)**:跑有状态服务(DB、缓存、有状态应用)+ 系统组件 79 - **Karpenter + SPOT + SQS**:跑无状态业务 Pod(http-app 这类) 80 81 这样既省钱又稳定。 82 83 --- 84 85 ## 3. 前置条件(通用) 86 87 两种方案都需要以下前提(你的集群已满足): 88 89 ### 3.1 OIDC Provider 已启用 90 91 ```bash 92 # 集群侧 issuer 93 aws eks describe-cluster --name ltp-eks-nonprod --region ap-northeast-1 \ 94 --query 'cluster.identity.oidc.issuer' --output text 95 # 期望:https://oidc.eks.ap-northeast-1.amazonaws.com/id/882CDxxxxxxxxx 96 97 # IAM 侧 provider 98 OIDC_ID=882CDxxxxxxxxx 99 aws iam list-open-id-connect-providers \ 100 --query "OpenIDConnectProviderList[?contains(Arn, '${OIDC_ID}')].Arn" --output text 101 # 期望:arn:aws:iam::29xxxxxxxxx:oidc-provider/oidc.eks.ap-northeast-1.amazonaws.com/id/882CDxxxxxxxxx 102 ``` 103 104 ✅ 你的集群已确认通过。 105 106 ### 3.2 Node Role 权限齐全 107 108 ```bash 109 aws iam list-attached-role-policies --role-name EKSInformalNodeRole \ 110 --query 'AttachedPolicies[].PolicyName' --output table 111 ``` 112 113 必须包含: 114 - `AmazonEKSWorkerNodePolicy`(节点加入集群) 115 - `AmazonEC2ContainerRegistryReadOnly`(拉 ECR 镜像) 116 - `AmazonEBSCSIDriverPolicy`(挂 EBS 卷) 117 118 ✅ 你的 `EKSInformalNodeRole` 已确认齐全。 119 120 ### 3.3 现有资源信息 121 122 ```bash 123 # 确认 subnet 对应的 AZ 124 aws ec2 describe-subnets --region ap-northeast-1 \ 125 --subnet-ids subnet-07fb6f96bcb2ae748 subnet-016233b0ff4879d15 subnet-071b80ca92f54e9f4 \ 126 --query 'Subnets[*].[SubnetId,AvailabilityZone]' --output table 127 # 核对是否为 ap-northeast-1a / 1c / 1d 128 ``` 129 130 --- 131 132 ## 4. 方案A:ON_DEMAND 精简版部署 133 134 ### 4.1 创建 Controller IAM Role(Terraform) 135 136 使用 `karpenter-iam-simple.tf`,只创建一个 IAM Role,**不创建 SQS、不创建 EventBridge**。 137 138 ```bash 139 cd outputs/ 140 terraform init 141 terraform apply -var="cluster_name=ltp-eks-nonprod" 142 143 CONTROLLER_ROLE_ARN=$(terraform output -raw karpenter_controller_role_arn) 144 echo $CONTROLLER_ROLE_ARN 145 # arn:aws:iam::29xxxxxxxxx:role/karpenter-controller-ltp-eks-nonprod 146 ``` 147 148 **创建的资源:** 149 - IAM Role:`karpenter-controller-ltp-eks-nonprod` 150 - 权限策略:EC2 创建/查询/终止 + iam:PassRole(无 SQS 权限) 151 152 **关键:终止实例的权限带 tag 条件限制,只删带 `karpenter.sh/discovery=ltp-eks-nonprod` 的 EC2,碰不到 managed node group。** 153 154 ### 4.2 安装 Karpenter(helm) 155 156 ```bash 157 helm repo add karpenter https://charts.karpenter.sh 158 helm repo update 159 160 helm install karpenter karpenter/karpenter \ 161 --namespace karpenter --create-namespace \ 162 --version 1.0.0 \ 163 --set "serviceAccount.annotations.eks\.amazonaws\.com/role-arn=${CONTROLLER_ROLE_ARN}" \ 164 --set clusterName=ltp-eks-nonprod 165 # 注意:不传 interruptionQueueName(on-demand 不需要中断处理) 166 167 kubectl get pod -n karpenter 168 # karpenter-xxx 1/1 Running 0 30s 169 ``` 170 171 ### 4.3 部署 NodePool(on-demand) 172 173 ```bash 174 kubectl apply -f karpenter-nodepool-simple.yaml 175 kubectl get nodepool,ec2nodeclass 176 # READY=True 即正常 177 ``` 178 179 **`karpenter-nodepool-simple.yaml` 关键配置:** 180 ```yaml 181 requirements: 182 - key: karpenter.sh/capacity-type 183 operator: In 184 values: 185 - on-demand # ← 只用 on-demand,不用 spot 186 ``` 187 188 ### 4.4 部署应用 189 190 ```bash 191 kubectl apply -f http-app-deployment.yaml # http-app(带隔离) 192 kubectl apply -f scaled.yaml # KEDA ScaledObject 193 ``` 194 195 --- 196 197 ## 5. 方案B:SPOT + SQS 完整版部署 198 199 ### 5.1 创建 AWS 基础设施(Terraform) 200 201 使用 `karpenter-iam-full.tf`,创建 Controller Role + SQS + EventBridge(三件套)。 202 203 ```bash 204 cd outputs/ 205 terraform init 206 terraform apply -var="cluster_name=ltp-eks-nonprod" 207 208 CONTROLLER_ROLE_ARN=$(terraform output -raw karpenter_controller_role_arn) 209 QUEUE_NAME=$(terraform output -raw sqs_queue_name) 210 echo "Controller Role: $CONTROLLER_ROLE_ARN" 211 echo "SQS Queue Name: $QUEUE_NAME" 212 ``` 213 214 **创建的资源:** 215 216 | 资源 | 名称 | 作用 | 217 |------|------|------| 218 | IAM Role | karpenter-controller-ltp-eks-nonprod | Controller 假设的 role | 219 | 权限策略 | karpenter-controller-ltp-eks-nonprod | EC2 创建/查询/终止 + SQS + iam:PassRole | 220 | SQS 队列 | ltp-eks-nonprod-karpenter | 缓存中断事件 | 221 | EventBridge 规则 1 | ltp-eks-nonprod-karpenter-spot | 捕获 Spot 中断警告 | 222 | EventBridge 规则 2 | ltp-eks-nonprod-karpenter-rebalance | 捕获 Rebalance 建议 | 223 | EventBridge 规则 3 | ltp-eks-nonprod-karpenter-unhealthy | 捕获 ASG 不健康实例 | 224 225 ### 5.2 SQS + EventBridge 权限详解 226 227 **SQS 队列**的权限(谁可读写): 228 ```json 229 // Karpenter controller 需要的 SQS 权限(在 controller role policy 里) 230 { 231 "Effect": "Allow", 232 "Action": ["sqs:DeleteMessage", "sqs:GetQueueUrl", "sqs:ReceiveMessage"], 233 "Resource": "arn:aws:sqs:ap-northeast-1:29xxxxxxxxx:ltp-eks-nonprod-karpenter" 234 } 235 ``` 236 237 **EventBridge → SQS**的转发权限(队列访问策略): 238 ```json 239 // SQS 队列的访问策略,允许 EventBridge 写入 240 { 241 "Effect": "Allow", 242 "Principal": { 243 "Service": "events.amazonaws.com" 244 }, 245 "Action": "sqs:SendMessage", 246 "Resource": "arn:aws:sqs:ap-northeast-1:29xxxxxxxxx:ltp-eks-nonprod-karpenter", 247 "Condition": { 248 "ArnEquals": { 249 "aws:SourceArn": [ 250 "arn:aws:events:ap-northeast-1:29xxxxxxxxx:rule/ltp-eks-nonprod-karpenter-spot", 251 "arn:aws:events:ap-northeast-1:29xxxxxxxxx:rule/ltp-eks-nonprod-karpenter-rebalance", 252 "arn:aws:events:ap-northeast-1:29xxxxxxxxx:rule/ltp-eks-nonprod-karpenter-unhealthy" 253 ] 254 } 255 } 256 } 257 ``` 258 259 **EventBridge 规则**的事件匹配模式: 260 ```json 261 // Spot 中断警告规则 262 { 263 "source": ["aws.ec2"], 264 "detail-type": ["EC2 Spot Instance Interruption Warning"] 265 } 266 267 // Rebalance 建议规则 268 { 269 "source": ["aws.ec2"], 270 "detail-type": ["EC2 Instance Rebalance Recommendation"] 271 } 272 273 // ASG 不健康实例规则 274 { 275 "source": ["aws.health"], 276 "detail-type": ["AWS Health Event"] 277 } 278 ``` 279 280 ### 5.3 验证 SQS + EventBridge 已创建 281 282 ```bash 283 # SQS 队列 284 aws sqs get-queue-url --queue-name ltp-eks-nonprod-karpenter --region ap-northeast-1 285 # 期望:https://sqs.ap-northeast-1.amazonaws.com/29xxxxxxxxx/ltp-eks-nonprod-karpenter 286 287 # 队列访问策略 288 aws sqs get-queue-attributes \ 289 --queue-url https://sqs.ap-northeast-1.amazonaws.com/29xxxxxxxxx/ltp-eks-nonprod-karpenter \ 290 --attribute-names Policy --region ap-northeast-1 \ 291 --query 'Attributes.Policy' --output text | jq . 292 293 # EventBridge 规则 294 aws events list-rules --name-prefix ltp-eks-nonprod-karpenter --region ap-northeast-1 295 # 期望看到 3 条规则:spot / rebalance / unhealthy 296 297 # 每条规则的目标(应指向 SQS) 298 aws events list-targets-by-rule --rule ltp-eks-nonprod-karpenter-spot --region ap-northeast-1 299 # 期望 Arn 指向 SQS 队列 300 301 # Controller Role 是否包含 SQS 权限 302 aws iam get-role-policy --role-name karpenter-controller-ltp-eks-nonprod \ 303 --policy-name karpenter-controller-ltp-eks-nonprod \ 304 --query 'PolicyDocument.Statement[?Action[?contains(@, `sqs`)]' --output text 305 # 应输出包含 sqs:DeleteMessage, sqs:GetQueueUrl, sqs:ReceiveMessage 的语句 306 ``` 307 308 ### 5.4 安装 Karpenter(helm,带 SQS) 309 310 ```bash 311 ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) 312 QUEUE_URL="https://sqs.ap-northeast-1.amazonaws.com/${ACCOUNT_ID}/${QUEUE_NAME}" 313 314 helm install karpenter karpenter/karpenter \ 315 --namespace karpenter --create-namespace \ 316 --version 1.0.0 \ 317 --set "serviceAccount.annotations.eks\.amazonaws\.com/role-arn=${CONTROLLER_ROLE_ARN}" \ 318 --set clusterName=ltp-eks-nonprod \ 319 --set "interruptionQueueName=${QUEUE_NAME}" 320 # 关键:传 interruptionQueueName,让 Karpenter 监听 SQS 321 322 kubectl get pod -n karpenter 323 kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter --tail=30 324 # 确认无 SQS 权限错误 325 ``` 326 327 ### 5.5 部署 NodePool(spot) 328 329 ```bash 330 kubectl apply -f karpenter-nodepool-spot.yaml 331 kubectl get nodepool,ec2nodeclass 332 # READY=True 333 ``` 334 335 **`karpenter-nodepool-spot.yaml` 关键配置:** 336 ```yaml 337 requirements: 338 - key: karpenter.sh/capacity-type 339 operator: In 340 values: 341 - spot # ← 只用 spot(最便宜) 342 # 或 [on-demand, spot] 让 Karpenter 自动选(优先 spot,无库存时回退 on-demand) 343 - key: node.kubernetes.io/instance-type 344 operator: In 345 values: 346 - r6a.2xlarge 347 - r5a.2xlarge 348 - r6a.xlarge 349 - r5a.xlarge 350 - r6a.large 351 - r5a.large 352 # 多机型,提高 spot 拿到实例的概率 353 ``` 354 355 ### 5.6 部署应用(同方案A) 356 357 ```bash 358 kubectl apply -f http-app-deployment.yaml # http-app(带隔离) 359 kubectl apply -f scaled.yaml # KEDA ScaledObject 360 ``` 361 362 ### 5.7 测试 spot 中断处理(可选) 363 364 手动模拟 spot 中断,验证 Karpenter 能从 SQS 收到通知: 365 366 ```bash 367 # 方法一:通过 AWS 终止一个 spot 实例(真实测试) 368 aws ec2 describe-instances --region ap-northeast-1 \ 369 --filters "Name=tag:karpenter.sh/nodepool,Values=default" \ 370 "Name=instance-lifecycle,Values=spot" \ 371 --query 'Reservations[].Instances[].InstanceId' --output text 372 # 拿到一个 spot 实例 ID 373 374 # 发起中断请求(AWS 会提前 2 分钟通知) 375 aws ec2 terminate-instances --instance-ids i-0xxx --region ap-northeast-1 376 377 # 观察 Karpenter 提前 drain 378 kubectl get events -A --field-selector reason=Karpenter -w 379 kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter -f | grep -i "interruption\|drain" 380 381 # 方法二:往 SQS 手动发一条测试消息(不真终止实例) 382 aws sqs send-message \ 383 --queue-url https://sqs.ap-northeast-1.amazonaws.com/29xxxxxxxxx/ltp-eks-nonprod-karpenter \ 384 --message-body '{"version":"0","id":"test","detail-type":"EC2 Spot Instance Interruption Warning","source":"aws.ec2","detail":{"instance-id":"i-test"}}' \ 385 --region ap-northeast-1 386 387 # 看 Karpenter 是否收到并处理 388 kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter -f | grep -i "interruption" 389 ``` 390 391 --- 392 393 ## 6. 从方案A切换到方案B 394 395 如果先用了方案A(on-demand),后来想切到方案B(spot+SQS),按以下步骤: 396 397 ### Step 1: 补创建 SQS + EventBridge 398 399 ```bash 400 # 用完整版 terraform 文件 401 cd outputs/ 402 # 先把 karpenter-iam-simple.tf 移走或删除,避免冲突 403 mv karpenter-iam-simple.tf karpenter-iam-simple.tf.bak 404 405 terraform apply -var="cluster_name=ltp-eks-nonprod" -var="aws_region=ap-northeast-1" 406 # 会补创建 SQS + EventBridge,并给 controller role 加 SQS 权限 407 408 QUEUE_NAME=$(terraform output -raw sqs_queue_name) 409 ``` 410 411 ### Step 2: 给 Karpenter 加回 SQS 配置 412 413 ```bash 414 ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) 415 helm upgrade karpenter karpenter/karpenter \ 416 --namespace karpenter \ 417 --set "serviceAccount.annotations.eks\.amazonaws\.com/role-arn=${CONTROLLER_ROLE_ARN}" \ 418 --set clusterName=ltp-eks-nonprod \ 419 --set "interruptionQueueName=${QUEUE_NAME}" 420 421 # 重启 controller 让 SQS 监听生效 422 kubectl rollout restart deployment -n karpenter karpenter 423 ``` 424 425 ### Step 3: 切换 NodePool 到 spot 426 427 ```bash 428 # 方式一:直接替换 NodePool 429 kubectl delete nodepool default 430 kubectl apply -f karpenter-nodepool-spot.yaml 431 432 # 方式二:原地 patch capacity-type 433 kubectl patch nodepool default --type='json' \ 434 -p='[{"op":"replace","path":"/spec/template/spec/requirements/1/values","value":["spot"]}]' 435 ``` 436 437 ### Step 4: 验证 438 439 ```bash 440 # 新开的节点应是 spot 441 kubectl get nodes -l nodepool=default -o wide 442 # 或 443 aws ec2 describe-instances --region ap-northeast-1 \ 444 --filters "Name=tag:karpenter.sh/nodepool,Values=default" \ 445 --query 'Reservations[].Instances[].[InstanceId,InstanceLifecycle]' --output table 446 # InstanceLifecycle 列应为 spot 447 ``` 448 449 --- 450 451 ## 7. 共用配置文件 452 453 两种方案共用以下文件,不用区分: 454 455 ### 7.1 http-app-deployment.yaml(共用) 456 457 http-app 应用配置,带 nodeSelector + toleration 隔离。两种方案都用这份。 458 459 ### 7.2 scaled.yaml(共用) 460 461 KEDA ScaledObject 配置。两种方案都用这份。 462 463 ### 7.3 方案专属文件 464 465 | 文件 | 方案A | 方案B | 466 |------|-------|-------| 467 | Terraform | `karpenter-iam-simple.tf` | `karpenter-iam-full.tf` | 468 | NodePool | `karpenter-nodepool-simple.yaml` | `karpenter-nodepool-spot.yaml` | 469 | helm 参数 | 无 `interruptionQueueName` | 有 `interruptionQueueName` | 470 471 --- 472 473 ## 8. 验证扩缩容 474 475 两种方案的扩缩容验证流程相同。 476 477 ### 8.1 扩容验证 478 479 ```bash 480 # 启动压测 481 kubectl run -n keda loadgen --rm -it --image=busybox -- /bin/sh 482 # pod 内: 483 while true; do wget -q -O /dev/null http://http-app.keda.svc.cluster.local/; done 484 485 # 另开终端观察 486 kubectl get deploy -n keda http-app -w # KEDA 扩 Pod 1→10 487 kubectl get node -w # Karpenter 扩 Node 488 kubectl get events -A --field-selector reason=Karpenter -w 489 490 # 确认新节点类型(方案B 确认是 spot) 491 kubectl get nodes -l nodepool=default -o wide 492 # 方案B 应看到 InstanceLifecycle=spot 493 ``` 494 495 ### 8.2 缩容验证 496 497 ```bash 498 # 停止压测(Ctrl+C,exit) 499 500 kubectl get deploy -n keda http-app -w # KEDA 缩 Pod 10→5→2→1 501 kubectl get node -l nodepool=default -w # Karpenter 删空节点 502 503 # 最终:Karpenter 节点全删,managed node group 的 1 台仍在 504 kubectl get nodes -l nodepool=default # 空 505 kubectl get nodes -l '!nodepool' # Terraform 管理的常驻节点 506 ``` 507 508 ### 8.3 方案B 额外:验证 SQS 中断处理 509 510 ```bash 511 # 看 Karpenter 是否成功连接 SQS 512 kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter | grep -i "queue\|interruption" 513 # 期望看到类似:"listening for interruption events on queue ltp-eks-nonprod-karpenter" 514 515 # 看 SQS 队列是否有消息进出 516 aws sqs get-queue-attributes \ 517 --queue-url https://sqs.ap-northeast-1.amazonaws.com/29xxxxxxxxx/ltp-eks-nonprod-karpenter \ 518 --attribute-names ApproximateNumberOfMessagesVisible \ 519 --region ap-northeast-1 --query 'Attributes' --output table 520 # 空闲时 ApproximateNumberOfMessagesVisible=0 521 # 中断发生时会短暂 >0,Karpenter 消费后归零 522 ``` 523 524 --- 525 526 ## 9. 故障排查 527 528 ### 9.1 方案A特有:on-demand 节点开不出来 529 530 ```bash 531 # Controller 日志 532 kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter --tail=50 533 534 # 常见: 535 # - UnauthorizedOperation → controller role 权限不足 536 # - InsufficientCapacity → on-demand 也可能无库存,换机型 537 # - NodePool limits 满 → 调大 limits.cpu 538 ``` 539 540 ### 9.2 方案B特有:SQS 相关错误 541 542 ```bash 543 # 错误:Karpenter 连不上 SQS 544 kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter | grep -i "sqs\|queue" 545 # 常见: 546 # - AccessDenied → controller role 缺 sqs:ReceiveMessage 权限 547 # - QueueNotFound → interruptionQueueName 传错 548 549 # 检查 controller role 是否有 SQS 权限 550 aws iam get-role-policy --role-name karpenter-controller-ltp-eks-nonprod \ 551 --policy-name karpenter-controller-ltp-eks-nonprod \ 552 --query 'PolicyDocument.Statement[?Action[?contains(@, `sqs`)]' --output text 553 554 # 检查 helm 是否传了 queueName 555 kubectl get deployment -n karpenter karpenter -o yaml | grep -i queue 556 ``` 557 558 ### 9.3 方案B特有:spot 一直拿不到实例 559 560 ```bash 561 # spot 库存不足,Karpenter 会一直重试 562 kubectl get events -A --field-selector reason=Karpenter -w 563 # 看到 "insufficient capacity" 或 "no instances available" 564 565 # 解决: 566 # 1. 加更多机型到 NodePool(机型越多,拿到 spot 概率越高) 567 # 2. 允许回退 on-demand(capacity-type: [on-demand, spot]) 568 # 3. 换 AZ(某些 AZ spot 库存更多) 569 ``` 570 571 ### 9.4 方案B特有:中断时 Pod 没有优雅迁移 572 573 ```bash 574 # 确认 SQS+EventBridge 链路 575 # 1. EventBridge 规则存在 576 aws events list-rules --name-prefix ltp-eks-nonprod-karpenter --region ap-northeast-1 577 578 # 2. 规则目标指向 SQS 579 aws events list-targets-by-rule --rule ltp-eks-nonprod-karpenter-spot --region ap-northeast-1 580 581 # 3. SQS 队列访问策略允许 EventBridge 写入 582 aws sqs get-queue-attributes \ 583 --queue-url https://sqs.ap-northeast-1.amazonaws.com/29xxxxxxxxx/ltp-eks-nonprod-karpenter \ 584 --attribute-names Policy --region ap-northeast-1 \ 585 --query 'Attributes.Policy' --output text | jq . 586 587 # 4. Karpenter 监听 SQS 588 kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter | grep -i "queue" 589 # 应有 "listening for interruption events" 590 ``` 591 592 ### 9.5 通用:Pod 一直 Pending 593 594 ```bash 595 # 看 Pending 原因 596 kubectl describe pod -n keda <pod-name> | tail -20 597 598 # 看 Karpenter 是否在扩容 599 kubectl get events -A --field-selector reason=Karpenter -w 600 601 # 看 EC2NodeClass 是否就绪 602 kubectl describe ec2nodeclass default | grep -A10 Conditions 603 # Ready=False 说明 subnet/sg 选不到 604 ``` 605 606 --- 607 608 ## 10. 文件清单 609 610 ### 通用文件(两方案共用) 611 612 | 文件 | 用途 | 613 |------|------| 614 | `http-app-deployment.yaml` | http-app + Service + ServiceMonitor(带隔离)| 615 | `scaled.yaml` | KEDA ScaledObject | 616 617 ### 方案A专属文件(ON_DEMAND 精简版) 618 619 | 文件 | 用途 | 620 |------|------| 621 | `karpenter-iam-simple.tf` | Terraform:只创建 Controller Role(无 SQS)| 622 | `karpenter-nodepool-simple.yaml` | NodePool + EC2NodeClass(on-demand)| 623 624 ### 方案B专属文件(SPOT + SQS 完整版) 625 626 | 文件 | 用途 | 627 |------|------| 628 | `karpenter-iam-full.tf` | Terraform:Controller Role + SQS + EventBridge | 629 | `karpenter-nodepool-spot.yaml` | NodePool + EC2NodeClass(spot)| 630 631 ### 部署命令对照 632 633 | 步骤 | 方案A | 方案B | 634 |------|-------|-------| 635 | Terraform | `terraform apply -var=cluster_name=ltp-eks-nonprod`(用 simple.tf)| 同左(用 full.tf)| 636 | helm 安装 | 无 `interruptionQueueName` | 有 `interruptionQueueName` | 637 | NodePool | `karpenter-nodepool-simple.yaml` | `karpenter-nodepool-spot.yaml` | 638 | 应用 + ScaledObject | 相同 | 相同 | 639 640 --- 641 642 ## 11. 与现有 Terraform 的关系 643 644 你的 `node-group` terraform(managed node group)**两种方案都不需要改**: 645 646 - 现有 `eks_managed_node_groups` 那台 r6a.2xlarge 保留,跑系统组件 647 - Karpenter 独立管理一批动态节点,跑业务 Pod 648 - 两批节点通过 taint + nodeSelector 隔离,互不干扰 649 - Terraform 不感知 Karpenter 的节点(不在 ASG 里,不带 discovery tag) 650 - Karpenter 只删带 `karpenter.sh/discovery=ltp-eks-nonprod` tag 的 EC2,碰不到 managed node group 651 652 **不需要改 terraform,不需要改现有 node group 配置。** Karpenter 是额外的动态扩缩层,在现有基础上新增。 653 654 --- 655 656 ## 附录:关键参数速查 657 658 ### Terraform 变量 659 660 | 变量 | 默认值 | 说明 | 661 |------|--------|------| 662 | `cluster_name` | ltp-eks-nonprod | 集群名 | 663 | `aws_region` | ap-northeast-1 | 区域 | 664 665 ### Karpenter helm 参数 666 667 | 参数 | 方案A | 方案B | 说明 | 668 |------|-------|-------|------| 669 | `clusterName` | ltp-eks-nonprod | ltp-eks-nonprod | 集群名 | 670 | `serviceAccount.annotations...role-arn` | controller role ARN | controller role ARN | IRSA | 671 | `interruptionQueueName` | 不设 | SQS 队列名 | 方案B 专用 | 672 673 ### NodePool 关键参数 674 675 | 参数 | 方案A | 方案B | 说明 | 676 |------|-------|-------|------| 677 | `capacity-type` | on-demand | spot | 实例类型 | 678 | `instance-types` | r6a/r5a 系列 | r6a/r5a 系列(更多机型)| 机型列表 | 679 | `limits.cpu` | 100 | 100 | 总 CPU 上限 | 680 | `consolidateAfter` | 60s | 60s | 空节点回收等待 |View Code
-
权限配置参考: MANUAL-PERMISSION-CHECKLIST.md


# Karpenter 手动权限配置清单(不用 Terraform) > 如果不用 Terraform,手动通过 AWS 控制台/aws cli 配置 Karpenter 所需的所有权限 > 集群:`ltp-eks-nonprod` / 区域:`ap-northeast-1` / Account:`2923xxxxxxxxx` > Node Role:`EKSInformalNodeRole`(已有,复用) > OIDC ID:`882CDxxxxxxxxx` --- ## 目录 1. [手动配置总览](#1-手动配置总览) 2. [步骤1: 确认 OIDC Provider](#步骤1-确认-oidc-provider) 3. [步骤2: 创建 Karpenter Controller IAM Role](#步骤2-创建-karpenter-controller-iam-role) 4. [步骤3: 创建 SQS 队列](#步骤3-创建-sqs-队列) 5. [步骤4: 创建 EventBridge 规则](#步骤4-创建-eventbridge-规则) 6. [步骤5: 配置 SQS 队列访问策略](#步骤5-配置-sqs-队列访问策略) 7. [步骤6: 验证 EKSInformalNodeRole](#步骤6-验证-eksinformalnoderole) 8. [步骤7: 确认 aws-auth ConfigMap](#步骤7-确认-aws-auth-configmap) 9. [步骤8: 安装 Karpenter 时绑定 Role](#步骤8-安装-karpenter-时绑定-role) 10. [两种方案的手动配置差异](#9-两种方案的手动配置差异) 11. [完整验证清单](#10-完整验证清单) --- ## 1. 手动配置总览 手动配置需要创建的资源: | 资源 | 方案A (ON_DEMAND) | 方案B (SPOT+SQS) | 配置位置 | |------|-------------------|------------------|----------| | OIDC Provider | ✅ 确认已有 | ✅ 确认已有 | IAM | | Controller IAM Role | ✅ 必须创建 | ✅ 必须创建 | IAM | | Controller 权限策略 | 无 SQS 权限 | 有 SQS 权限 | IAM | | SQS 队列 | ❌ 不需要 | ✅ 必须创建 | SQS | | SQS 队列访问策略 | ❌ 不需要 | ✅ 必须配置 | SQS | | EventBridge 规则 × 3 | ❌ 不需要 | ✅ 必须创建 | EventBridge | | Node Role policy | ✅ 确认已有 | ✅ 确认已有 | IAM | **方案A 需要手动配置:步骤1、2、6、7、8** **方案B 需要手动配置:步骤1~8 全部** --- ## 步骤1: 确认 OIDC Provider ### 1.1 确认集群侧 OIDC issuer **AWS 控制台:** - EKS → 集群 ltp-eks-nonprod → Configuration → Details - 查看 "OpenID Connect issuer URL" - 期望:`https://oidc.eks.ap-northeast-1.amazonaws.com/id/882CDxxxxxxxxx` **aws cli:** ```bash aws eks describe-cluster --name ltp-eks-nonprod --region ap-northeast-1 \ --query 'cluster.identity.oidc.issuer' --output text # 期望:https://oidc.eks.ap-northeast-1.amazonaws.com/id/882CDxxxxxxxxx ``` ### 1.2 确认 IAM 侧 OIDC Provider **AWS 控制台:** - IAM → Identity providers - 找包含 `882CDxxxxxxxxx` 的 provider **aws cli:** ```bash aws iam list-open-id-connect-providers \ --query "OpenIDConnectProviderList[?contains(Arn, '882CDxxxxxxxxx')].Arn" \ --output text # 期望:arn:aws:iam::2923xxxxxxxxx:oidc-provider/oidc.eks.ap-northeast-1.amazonaws.com/id/882CDxxxxxxxxx ``` ### 1.3 如果不存在,创建 OIDC Provider ```bash eksctl utils associate-iam-oidc-provider \ --cluster ltp-eks-nonprod \ --region ap-northeast-1 \ --approve # 或用 aws cli aws iam create-open-id-connect-provider \ --url https://oidc.eks.ap-northeast-1.amazonaws.com/id/882CDxxxxxxxxx \ --client-id-list sts.amazonaws.com \ --thumbprint-list 9e99a48a9960c14951fb80d9e1c29e6616e52e3b ``` --- ## 步骤2: 创建 Karpenter Controller IAM Role 这个 Role 是 Karpenter controller pod 通过 IRSA 假设的,用来调 EC2/SQS API。 ### 2.1 创建 Role(带信任策略) **方法一:aws cli(推荐)** 创建信任策略文件 `trust-policy.json`: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::2923xxxxxxxxx:oidc-provider/oidc.eks.ap-northeast-1.amazonaws.com/id/882CDxxxxxxxxx" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "oidc.eks.ap-northeast-1.amazonaws.com/id/882CDxxxxxxxxx:sub": "system:serviceaccount:karpenter:karpenter" } } } ] } ``` 创建 Role: ```bash aws iam create-role \ --role-name karpenter-controller-ltp-eks-nonprod \ --assume-role-policy-document file://trust-policy.json ``` **方法二:AWS 控制台** 1. IAM → Roles → Create role 2. Trusted entity type: **Web identity** 3. Identity provider: 选 `oidc.eks.ap-northeast-1.amazonaws.com/id/882CDxxxxxxxxx` 4. Audience: `sts.amazonaws.com` 5. Role name: `karpenter-controller-ltp-eks-nonprod` 6. Create role(先不挂 policy,下一步手动加) ### 2.2 创建权限策略 **方案A(ON_DEMAND 精简版)—— 无 SQS 权限** 创建策略文件 `karpenter-policy-simple.json`: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ec2:CreateLaunchTemplate", "ec2:CreateFleet", "ec2:RunInstances", "ec2:CreateTags", "iam:PassRole" ], "Resource": ["*"] }, { "Effect": "Allow", "Action": [ "ec2:DescribeImages", "ec2:DescribeInstances", "ec2:DescribeLaunchTemplates", "ec2:DescribeInstanceTypes", "ec2:DescribeSubnets", "ec2:DescribeSecurityGroups", "ec2:DescribeAvailabilityZones", "ec2:DescribeSpotPriceHistory", "ec2:DescribeVpcs" ], "Resource": ["*"] }, { "Effect": "Allow", "Action": ["ec2:TerminateInstances"], "Resource": ["*"], "Condition": { "StringEquals": { "ec2:ResourceTag/karpenter.sh/discovery": "ltp-eks-nonprod" } } } ] } ``` 挂载到 Role(inline policy): ```bash aws iam put-role-policy \ --role-name karpenter-controller-ltp-eks-nonprod \ --policy-name karpenter-controller-ltp-eks-nonprod \ --policy-document file://karpenter-policy-simple.json ``` **方案B(SPOT + SQS 完整版)—— 含 SQS 权限** 创建策略文件 `karpenter-policy-full.json`: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ec2:CreateLaunchTemplate", "ec2:CreateFleet", "ec2:RunInstances", "ec2:CreateTags", "iam:PassRole" ], "Resource": ["*"] }, { "Effect": "Allow", "Action": [ "ec2:DescribeImages", "ec2:DescribeInstances", "ec2:DescribeLaunchTemplates", "ec2:DescribeInstanceTypes", "ec2:DescribeSubnets", "ec2:DescribeSecurityGroups", "ec2:DescribeAvailabilityZones", "ec2:DescribeSpotPriceHistory", "ec2:DescribeVpcs" ], "Resource": ["*"] }, { "Effect": "Allow", "Action": ["ec2:TerminateInstances"], "Resource": ["*"], "Condition": { "StringEquals": { "ec2:ResourceTag/karpenter.sh/discovery": "ltp-eks-nonprod" } } }, { "Effect": "Allow", "Action": [ "sqs:DeleteMessage", "sqs:GetQueueUrl", "sqs:ReceiveMessage" ], "Resource": "arn:aws:sqs:ap-northeast-1:2923xxxxxxxxx:ltp-eks-nonprod-karpenter" }, { "Effect": "Allow", "Action": ["iam:CreateServiceLinkedRole"], "Resource": ["arn:aws:iam::*:role/aws-service-role/spotfleet.amazonaws.com/AWSServiceRoleForEC2SpotFleet"] } ] } ``` 挂载到 Role: ```bash aws iam put-role-policy \ --role-name karpenter-controller-ltp-eks-nonprod \ --policy-name karpenter-controller-ltp-eks-nonprod \ --policy-document file://karpenter-policy-full.json ``` ### 2.3 验证 Role 创建成功 ```bash # Role 存在 aws iam get-role --role-name karpenter-controller-ltp-eks-nonprod --query 'Role.Arn' --output text # 期望:arn:aws:iam::2923xxxxxxxxx:role/karpenter-controller-ltp-eks-nonprod # 信任策略正确 aws iam get-role --role-name karpenter-controller-ltp-eks-nonprod \ --query 'Role.AssumeRolePolicyDocument.Statement[0].Principal.Federated' --output text # 期望包含:882CDxxxxxxxxx # 权限策略已挂载 aws iam get-role-policy --role-name karpenter-controller-ltp-eks-nonprod \ --policy-name karpenter-controller-ltp-eks-nonprod --query 'PolicyDocument.Statement[].Action' --output text ``` --- ## 步骤3: 创建 SQS 队列(方案B 专用) ### 3.1 创建队列 **aws cli:** ```bash aws sqs create-queue \ --queue-name ltp-eks-nonprod-karpenter \ --attributes MessageRetentionPeriod=300 \ --region ap-northeast-1 # 期望输出: # https://sqs.ap-northeast-1.amazonaws.com/2923xxxxxxxxx/ltp-eks-nonprod-karpenter ``` **AWS 控制台:** 1. SQS → Create queue 2. Type: Standard 3. Name: `ltp-eks-nonprod-karpenter` 4. Configuration: - Message retention: 5 分钟(300 秒) - 其他默认 5. Encryption: SSE-SQS(推荐开启) 6. Create queue ### 3.2 记录队列 ARN ```bash QUEUE_URL=$(aws sqs get-queue-url --queue-name ltp-eks-nonprod-karpenter --region ap-northeast-1 --output text) aws sqs get-queue-attributes \ --queue-url $QUEUE_URL \ --attribute-names QueueArn \ --region ap-northeast-1 --query 'Attributes.QueueArn' --output text # 期望:arn:aws:sqs:ap-northeast-1:2923xxxxxxxxx:ltp-eks-nonprod-karpenter ``` --- ## 步骤4: 创建 EventBridge 规则(方案B 专用) 需要创建 3 条规则,分别捕获 3 类事件,都转发到上面的 SQS 队列。 ### 4.1 规则1: Spot 中断警告 **aws cli:** ```bash # 创建规则 aws events put-rule \ --name ltp-eks-nonprod-karpenter-spot \ --event-pattern '{"source":["aws.ec2"],"detail-type":["EC2 Spot Instance Interruption Warning"]}' \ --region ap-northeast-1 # 添加目标(转发到 SQS) aws events put-targets \ --rule ltp-eks-nonprod-karpenter-spot \ --targets Id="KarpenterQueueSpot",Arn="arn:aws:sqs:ap-northeast-1:2923xxxxxxxxx:ltp-eks-nonprod-karpenter" \ --region ap-northeast-1 ``` **AWS 控制台:** 1. EventBridge → Rules → Create rule 2. Name: `ltp-eks-nonprod-karpenter-spot` 3. Rule type: Rule with an event pattern 4. Event source: AWS events or EventBridge partner events 5. Creation method: Pattern form 6. Event pattern: - Event source: AWS services - AWS service: EC2 - Event type: Spot Instance Interruption Warning 7. Target: SQS queue → 选 `ltp-eks-nonprod-karpenter` 8. Create ### 4.2 规则2: Rebalance 建议 ```bash aws events put-rule \ --name ltp-eks-nonprod-karpenter-rebalance \ --event-pattern '{"source":["aws.ec2"],"detail-type":["EC2 Instance Rebalance Recommendation"]}' \ --region ap-northeast-1 aws events put-targets \ --rule ltp-eks-nonprod-karpenter-rebalance \ --targets Id="KarpenterQueueRebalance",Arn="arn:aws:sqs:ap-northeast-1:2923xxxxxxxxx:ltp-eks-nonprod-karpenter" \ --region ap-northeast-1 ``` 控制台操作同上,Event type 选 "EC2 Instance Rebalance Recommendation"。 ### 4.3 规则3: ASG 不健康实例 ```bash aws events put-rule \ --name ltp-eks-nonprod-karpenter-unhealthy \ --event-pattern '{"source":["aws.health"],"detail-type":["AWS Health Event"]}' \ --region ap-northeast-1 aws events put-targets \ --rule ltp-eks-nonprod-karpenter-unhealthy \ --targets Id="KarpenterQueueUnhealthy",Arn="arn:aws:sqs:ap-northeast-1:2923xxxxxxxxx:ltp-eks-nonprod-karpenter" \ --region ap-northeast-1 ``` 控制台操作:AWS service 选 "Health",Event type 选 "AWS Health Event"。 ### 4.4 验证规则创建成功 ```bash # 列出所有规则 aws events list-rules --name-prefix ltp-eks-nonprod-karpenter --region ap-northeast-1 # 期望看到 3 条规则 # 每条规则的目标应指向 SQS aws events list-targets-by-rule --rule ltp-eks-nonprod-karpenter-spot --region ap-northeast-1 aws events list-targets-by-rule --rule ltp-eks-nonprod-karpenter-rebalance --region ap-northeast-1 aws events list-targets-by-rule --rule ltp-eks-nonprod-karpenter-unhealthy --region ap-northeast-1 # 每条都应输出 Arn 指向 SQS 队列 ``` --- ## 步骤5: 配置 SQS 队列访问策略(方案B 专用) EventBridge 把事件发到 SQS 时,需要 SQS 队列允许 EventBridge 写入。这一步最容易漏。 ### 5.1 创建队列访问策略 创建策略文件 `sqs-policy.json`: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "events.amazonaws.com" }, "Action": "sqs:SendMessage", "Resource": "arn:aws:sqs:ap-northeast-1:2923xxxxxxxxx:ltp-eks-nonprod-karpenter", "Condition": { "ArnEquals": { "aws:SourceArn": [ "arn:aws:events:ap-northeast-1:2923xxxxxxxxx:rule/ltp-eks-nonprod-karpenter-spot", "arn:aws:events:ap-northeast-1:2923xxxxxxxxx:rule/ltp-eks-nonprod-karpenter-rebalance", "arn:aws:events:ap-northeast-1:2923xxxxxxxxx:rule/ltp-eks-nonprod-karpenter-unhealthy" ] } } } ] } ``` **aws cli:** ```bash aws sqs set-queue-attributes \ --queue-url https://sqs.ap-northeast-1.amazonaws.com/2923xxxxxxxxx/ltp-eks-nonprod-karpenter \ --attributes Policy="$(cat sqs-policy.json)" \ --region ap-northeast-1 ``` **AWS 控制台:** 1. SQS → 队列 `ltp-eks-nonprod-karpenter` → Edit 2. Access policy 3. 粘贴上面的 JSON(替换整个内容) 4. Save ### 5.2 验证队列访问策略 ```bash aws sqs get-queue-attributes \ --queue-url https://sqs.ap-northeast-1.amazonaws.com/2923xxxxxxxxx/ltp-eks-nonprod-karpenter \ --attribute-names Policy \ --region ap-northeast-1 --query 'Attributes.Policy' --output text | jq . # 期望输出包含: # - Principal.Service: events.amazonaws.com # - Action: sqs:SendMessage # - Condition.ArnEquals.aws:SourceArn: 3 条 EventBridge 规则的 ARN ``` ### 5.3 测试 EventBridge → SQS 链路 ```bash # 手动往 SQS 发一条测试消息(模拟 EventBridge 转发) aws sqs send-message \ --queue-url https://sqs.ap-northeast-1.amazonaws.com/2923xxxxxxxxx/ltp-eks-nonprod-karpenter \ --message-body '{"version":"0","id":"test","detail-type":"EC2 Spot Instance Interruption Warning","source":"aws.ec2","detail":{"instance-id":"i-test"}}' \ --region ap-northeast-1 # 检查队列里有消息 aws sqs get-queue-attributes \ --queue-url https://sqs.ap-northeast-1.amazonaws.com/2923xxxxxxxxx/ltp-eks-nonprod-karpenter \ --attribute-names ApproximateNumberOfMessagesVisible \ --region ap-northeast-1 --query 'Attributes' --output table # ApproximateNumberOfMessagesVisible 应 > 0 # 读出消息 aws sqs receive-message \ --queue-url https://sqs.ap-northeast-1.amazonaws.com/2923xxxxxxxxx/ltp-eks-nonprod-karpenter \ --region ap-northeast-1 # 清理测试消息 aws sqs purge-queue \ --queue-url https://sqs.ap-northeast-1.amazonaws.com/2923xxxxxxxxx/ltp-eks-nonprod-karpenter \ --region ap-northeast-1 ``` --- ## 步骤6: 验证 EKSInformalNodeRole 这个 Role 是 Karpenter 开出的 EC2 实例用的,必须已挂以下 policy。 ### 6.1 列出已挂 policy ```bash aws iam list-attached-role-policies --role-name EKSInformalNodeRole \ --query 'AttachedPolicies[].PolicyName' --output table ``` ### 6.2 必须 policy 清单 | Policy | 用途 | 必须性 | |--------|------|--------| | AmazonEKSWorkerNodePolicy | 节点加入集群 | ✅ 必须 | | AmazonEC2ContainerRegistryReadOnly | 拉 ECR 镜像 | ✅ 必须 | | AmazonEBSCSIDriverPolicy | 挂 EBS 卷 | ✅ 推荐 | | AmazonSSMManagedInstanceCore | SSM 排查 | 可选 | | AmazonEKS_CNI_Policy | VPC CNI 分配 IP | ✅ 必须(除非 CNI 用 IRSA)| ### 6.3 缺失时补挂 ```bash # 补 AmazonEKSWorkerNodePolicy aws iam attach-role-policy --role-name EKSInformalNodeRole \ --policy-arn arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy # 补 AmazonEC2ContainerRegistryReadOnly aws iam attach-role-policy --role-name EKSInformalNodeRole \ --policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly # 补 AmazonEBSCSIDriverPolicy aws iam attach-role-policy --role-name EKSInformalNodeRole \ --policy-arn arn:aws:iam::aws:policy/AmazonEBSCSIDriverPolicy # 补 AmazonEKS_CNI_Policy(仅在 CNI 不用 IRSA 时) aws iam attach-role-policy --role-name EKSInformalNodeRole \ --policy-arn arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy ``` **AWS 控制台:** 1. IAM → Roles → EKSInformalNodeRole → Permissions 2. Add permissions → Attach policies 3. 搜索并勾选上面的 policy 4. Add permissions --- ## 步骤7: 确认 aws-auth ConfigMap `aws-auth` ConfigMap 决定哪个 IAM Role 能映射成 K8s 的 node 身份。如果 `EKSInformalNodeRole` 不在里面,Karpenter 开的节点无法注册到集群。 ### 7.1 查看 aws-auth ```bash kubectl get configmap -n kube-system aws-auth -o yaml ``` ### 7.2 确认包含 EKSInformalNodeRole 期望看到: ```yaml mapRoles: - rolearn: arn:aws:iam::2923xxxxxxxxx:role/EKSInformalNodeRole username: system:node:{{EC2PrivateDNSName}} groups: - system:bootstrappers - system:nodes ``` ### 7.3 如果缺失,手动添加 ```bash kubectl edit configmap -n kube-system aws-auth # 在 mapRoles 下添加上面的内容 ``` 或用 eksctl 自动处理: ```bash eksctl create iamidentitymapping \ --cluster ltp-eks-nonprod \ --region ap-northeast-1 \ --arn arn:aws:iam::2923xxxxxxxxx:role/EKSInformalNodeRole \ --username system:node:{{EC2PrivateDNSName}} \ --group system:bootstrappers \ --group system:nodes ``` --- ## 步骤8: 安装 Karpenter 时绑定 Role ### 8.1 准备 Role ARN ```bash CONTROLLER_ROLE_ARN="arn:aws:iam::2923xxxxxxxxx:role/karpenter-controller-ltp-eks-nonprod" ``` ### 8.2 方案A安装(on-demand,不传 queue) ```bash helm install karpenter karpenter/karpenter \ --namespace karpenter --create-namespace \ --version 1.0.0 \ --set "serviceAccount.annotations.eks\.amazonaws\.com/role-arn=${CONTROLLER_ROLE_ARN}" \ --set clusterName=ltp-eks-nonprod # 不传 interruptionQueueName ``` ### 8.3 方案B安装(spot + SQS,传 queue) ```bash QUEUE_NAME="ltp-eks-nonprod-karpenter" helm install karpenter karpenter/karpenter \ --namespace karpenter --create-namespace \ --version 1.0.0 \ --set "serviceAccount.annotations.eks\.amazonaws\.com/role-arn=${CONTROLLER_ROLE_ARN}" \ --set clusterName=ltp-eks-nonprod \ --set "interruptionQueueName=${QUEUE_NAME}" # 传 interruptionQueueName ``` ### 8.4 验证 IRSA 绑定成功 ```bash # ServiceAccount 有 IRSA annotation kubectl get sa -n karpenter karpenter -o yaml | grep -A2 annotations # 期望: # annotations: # eks.amazonaws.com/role-arn: arn:aws:iam::2923xxxxxxxxx:role/karpenter-controller-ltp-eks-nonprod # Controller pod 注入了 IRSA 环境变量 kubectl exec -n karpenter deployment/karpenter -- env | grep AWS # 期望: # AWS_ROLE_ARN=arn:aws:iam::2923xxxxxxxxx:role/karpenter-controller-ltp-eks-nonprod # AWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/eks.amazonaws.com/serviceaccount/token # Controller 无权限错误 kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter --tail=30 # 无 AccessDenied / UnauthorizedOperation ``` --- ## 9. 两种方案的手动配置差异 ### 方案A(ON_DEMAND 精简版)手动配置清单 | 步骤 | 资源 | 状态 | |------|------|------| | 1 | OIDC Provider(确认已有)| ✅ | | 2 | Controller IAM Role + 权限策略(无 SQS)| 手动创建 | | 3 | SQS 队列 | ❌ 不需要 | | 4 | EventBridge 规则 | ❌ 不需要 | | 5 | SQS 队列访问策略 | ❌ 不需要 | | 6 | EKSInformalNodeRole(确认已有)| ✅ | | 7 | aws-auth ConfigMap(确认已有)| ✅ | | 8 | helm 安装(不传 queue)| 手动 | **方案A 只需手动做:步骤 2 + 步骤 8** ### 方案B(SPOT + SQS 完整版)手动配置清单 | 步骤 | 资源 | 状态 | |------|------|------| | 1 | OIDC Provider(确认已有)| ✅ | | 2 | Controller IAM Role + 权限策略(含 SQS)| 手动创建 | | 3 | SQS 队列 | 手动创建 | | 4 | EventBridge 规则 × 3 | 手动创建 | | 5 | SQS 队列访问策略 | 手动配置 | | 6 | EKSInformalNodeRole(确认已有)| ✅ | | 7 | aws-auth ConfigMap(确认已有)| ✅ | | 8 | helm 安装(传 queue)| 手动 | **方案B 需手动做:步骤 2~5 + 步骤 8** --- ## 10. 完整验证清单 手动配置完后,按此清单逐项验证。 ### 10.1 IAM 侧验证 ```bash # 1. OIDC Provider 存在 aws iam list-open-id-connect-providers \ --query "OpenIDConnectProviderList[?contains(Arn, '882CDxxxxxxxxx')].Arn" --output text # 2. Controller Role 存在 aws iam get-role --role-name karpenter-controller-ltp-eks-nonprod --query 'Role.Arn' --output text # 3. Controller Role 信任策略正确(指向 OIDC + SA) aws iam get-role --role-name karpenter-controller-ltp-eks-nonprod \ --query 'Role.AssumeRolePolicyDocument.Statement[0].Principal.Federated' --output text # 4. Controller Role 权限策略已挂 aws iam get-role-policy --role-name karpenter-controller-ltp-eks-nonprod \ --policy-name karpenter-controller-ltp-eks-nonprod --query 'PolicyDocument.Statement[].Action' --output text # 5. Node Role policy 齐全 aws iam list-attached-role-policies --role-name EKSInformalNodeRole \ --query 'AttachedPolicies[].PolicyName' --output table ``` ### 10.2 SQS 侧验证(方案B) ```bash # 6. SQS 队列存在 aws sqs get-queue-url --queue-name ltp-eks-nonprod-karpenter --region ap-northeast-1 # 7. SQS 队列访问策略允许 EventBridge 写入 aws sqs get-queue-attributes \ --queue-url https://sqs.ap-northeast-1.amazonaws.com/2923xxxxxxxxx/ltp-eks-nonprod-karpenter \ --attribute-names Policy --region ap-northeast-1 \ --query 'Attributes.Policy' --output text | jq . ``` ### 10.3 EventBridge 侧验证(方案B) ```bash # 8. 3 条规则存在 aws events list-rules --name-prefix ltp-eks-nonprod-karpenter --region ap-northeast-1 # 9. 每条规则的目标指向 SQS aws events list-targets-by-rule --rule ltp-eks-nonprod-karpenter-spot --region ap-northeast-1 aws events list-targets-by-rule --rule ltp-eks-nonprod-karpenter-rebalance --region ap-northeast-1 aws events list-targets-by-rule --rule ltp-eks-nonprod-karpenter-unhealthy --region ap-northeast-1 ``` ### 10.4 EKS 侧验证 ```bash # 10. aws-auth ConfigMap 包含 EKSInformalNodeRole kubectl get configmap -n kube-system aws-auth -o yaml | grep EKSInformalNodeRole # 11. Karpenter SA 有 IRSA annotation kubectl get sa -n karpenter karpenter -o yaml | grep role-arn # 12. Controller pod 运行正常 kubectl get pod -n karpenter # 13. Controller 日志无权限错误 kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter --tail=30 ``` ### 10.5 联动验证 ```bash # 14. 触发一次扩容,看 Karpenter 能开机器 kubectl apply -f - <<EOF apiVersion: apps/v1 kind: Deployment metadata: name: inflate spec: replicas: 1 selector: matchLabels: app: inflate template: metadata: labels: app: inflate spec: containers: - name: inflate image: public.ecr.aws/eks-distro/kubernetes/pause:3.7 resources: requests: cpu: 8 memory: 16Gi EOF # 15. 看 Karpenter 事件 kubectl get events -A --field-selector reason=Karpenter -w # 16. 节点出现且 Ready kubectl get node -w ``` --- ## 附录:手动配置命令汇总(方案B,可直接复制) ```bash # ===== 变量 ===== CLUSTER=ltp-eks-nonprod REGION=ap-northeast-1 ACCOUNT=2923xxxxxxxxx OIDC=882CDxxxxxxxxx NODE_ROLE=EKSInformalNodeRole CTRL_ROLE=karpenter-controller-${CLUSTER} QUEUE=${CLUSTER}-karpenter QUEUE_URL=https://sqs.${REGION}.amazonaws.com/${ACCOUNT}/${QUEUE} QUEUE_ARN=arn:aws:sqs:${REGION}:${ACCOUNT}:${QUEUE} # ===== 步骤2: Controller Role ===== cat > trust-policy.json <<EOF { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Federated": "arn:aws:iam::${ACCOUNT}:oidc-provider/oidc.eks.${REGION}.amazonaws.com/id/${OIDC}"}, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": {"StringEquals": {"oidc.eks.${REGION}.amazonaws.com/id/${OIDC}:sub": "system:serviceaccount:karpenter:karpenter"}} }] } EOF aws iam create-role --role-name $CTRL_ROLE --assume-role-policy-document file://trust-policy.json cat > karpenter-policy-full.json <<EOF { "Version": "2012-10-17", "Statement": [ {"Effect":"Allow","Action":["ec2:CreateLaunchTemplate","ec2:CreateFleet","ec2:RunInstances","ec2:CreateTags","iam:PassRole"],"Resource":["*"]}, {"Effect":"Allow","Action":["ec2:DescribeImages","ec2:DescribeInstances","ec2:DescribeLaunchTemplates","ec2:DescribeInstanceTypes","ec2:DescribeSubnets","ec2:DescribeSecurityGroups","ec2:DescribeAvailabilityZones","ec2:DescribeSpotPriceHistory","ec2:DescribeVpcs"],"Resource":["*"]}, {"Effect":"Allow","Action":["ec2:TerminateInstances"],"Resource":["*"],"Condition":{"StringEquals":{"ec2:ResourceTag/karpenter.sh/discovery":"${CLUSTER}"}}}, {"Effect":"Allow","Action":["sqs:DeleteMessage","sqs:GetQueueUrl","sqs:ReceiveMessage"],"Resource":"${QUEUE_ARN}"}, {"Effect":"Allow","Action":["iam:CreateServiceLinkedRole"],"Resource":["arn:aws:iam::*:role/aws-service-role/spotfleet.amazonaws.com/AWSServiceRoleForEC2SpotFleet"]} ] } EOF aws iam put-role-policy --role-name $CTRL_ROLE --policy-name $CTRL_ROLE --policy-document file://karpenter-policy-full.json # ===== 步骤3: SQS 队列 ===== aws sqs create-queue --queue-name $QUEUE --attributes MessageRetentionPeriod=300 --region $REGION # ===== 步骤4: EventBridge 规则 ===== aws events put-rule --name ${CLUSTER}-karpenter-spot --event-pattern '{"source":["aws.ec2"],"detail-type":["EC2 Spot Instance Interruption Warning"]}' --region $REGION aws events put-targets --rule ${CLUSTER}-karpenter-spot --targets Id=Spot,Arn=${QUEUE_ARN} --region $REGION aws events put-rule --name ${CLUSTER}-karpenter-rebalance --event-pattern '{"source":["aws.ec2"],"detail-type":["EC2 Instance Rebalance Recommendation"]}' --region $REGION aws events put-targets --rule ${CLUSTER}-karpenter-rebalance --targets Id=Rebalance,Arn=${QUEUE_ARN} --region $REGION aws events put-rule --name ${CLUSTER}-karpenter-unhealthy --event-pattern '{"source":["aws.health"],"detail-type":["AWS Health Event"]}' --region $REGION aws events put-targets --rule ${CLUSTER}-karpenter-unhealthy --targets Id=Unhealthy,Arn=${QUEUE_ARN} --region $REGION # ===== 步骤5: SQS 队列访问策略 ===== cat > sqs-policy.json <<EOF { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Service": "events.amazonaws.com"}, "Action": "sqs:SendMessage", "Resource": "${QUEUE_ARN}", "Condition": {"ArnEquals": {"aws:SourceArn": [ "arn:aws:events:${REGION}:${ACCOUNT}:rule/${CLUSTER}-karpenter-spot", "arn:aws:events:${REGION}:${ACCOUNT}:rule/${CLUSTER}-karpenter-rebalance", "arn:aws:events:${REGION}:${ACCOUNT}:rule/${CLUSTER}-karpenter-unhealthy" ]}} }] } EOF aws sqs set-queue-attributes --queue-url $QUEUE_URL --attributes Policy="$(cat sqs-policy.json)" --region $REGION # ===== 步骤8: helm 安装 ===== helm repo add karpenter https://charts.karpenter.sh helm repo update helm install karpenter karpenter/karpenter \ --namespace karpenter --create-namespace \ --version 1.0.0 \ --set "serviceAccount.annotations.eks\.amazonaws\.com/role-arn=arn:aws:iam::${ACCOUNT}:role/${CTRL_ROLE}" \ --set clusterName=$CLUSTER \ --set "interruptionQueueName=${QUEUE}" ``` 方案A 把 `karpenter-policy-full.json` 换成 `karpenter-policy-simple.json`(去掉 SQS 那条 Statement),跳过步骤3~5,helm 安装去掉 `interruptionQueueName` 参数。View Code
-
方案A 用:karpenter-iam-simple.tf


# Karpenter 精简版 IAM(on-demand 专用,无 SQS/EventBridge) # 只创建 Controller IAM Role,够开/删/查 EC2 # 用法:terraform init && terraform apply -var="cluster_name=ltp-eks-nonprod" terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } variable "cluster_name" { default = "ltp-eks-nonprod" } data "aws_region" "current" {} data "aws_caller_identity" "current" {} data "aws_eks_cluster" "eks" { name = var.cluster_name } data "aws_iam_openid_connect_provider" "eks" { url = data.aws_eks_cluster.eks.identity[0].oidc[0].issuer } # ============================================================ # Karpenter Controller IAM 角色(通过 IRSA 绑到 controller pod) # ============================================================ resource "aws_iam_role" "karpenter_controller" { name = "karpenter-controller-${var.cluster_name}" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow" Principal = { Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${data.aws_iam_openid_connect_provider.eks.url}" } Action = "sts:AssumeRoleWithWebIdentity" Condition = { StringEquals = { "${data.aws_iam_openid_connect_provider.eks.url}:sub" = "system:serviceaccount:karpenter:karpenter" } } }] }) } resource "aws_iam_role_policy" "karpenter_controller" { name = "karpenter-controller-${var.cluster_name}" role = aws_iam_role.karpenter_controller.id policy = jsonencode({ Version = "2012-10-17" Statement = [ # 开机器 { Effect = "Allow" Action = [ "ec2:CreateLaunchTemplate", "ec2:CreateFleet", "ec2:RunInstances", "ec2:CreateTags", "iam:PassRole" ] Resource = ["*"] }, # 查资源(机型/subnet/sg/AMI) { Effect = "Allow" Action = [ "ec2:DescribeImages", "ec2:DescribeInstances", "ec2:DescribeLaunchTemplates", "ec2:DescribeInstanceTypes", "ec2:DescribeSubnets", "ec2:DescribeSecurityGroups", "ec2:DescribeAvailabilityZones", "ec2:DescribeSpotPriceHistory", "ec2:DescribeVpcs" ] Resource = ["*"] }, # 删机器(只删带 discovery tag 的,碰不到 managed node group) { Effect = "Allow" Action = ["ec2:TerminateInstances"] Resource = ["*"] Condition = { StringEquals = { "ec2:ResourceTag/karpenter.sh/discovery" = var.cluster_name } } } # 注意:没有 SQS 权限(on-demand 不需要) # 如果以后要用 spot,再补 SQS + EventBridge ] }) } output "karpenter_controller_role_arn" { value = aws_iam_role.karpenter_controller.arn }View Code
-
方案A 用:karpenter-nodepool-simple.yaml


# Karpenter NodePool + EC2NodeClass(ON_DEMAND 精简版,无 SQS) # 与现有 managed node group (EKSInformalNodeRole) 并存 # 用法:kubectl apply -f karpenter-nodepool-simple.yaml # 前提:已用 karpenter-iam-simple.tf 创建 Controller Role,helm 不传 interruptionQueueName apiVersion: karpenter.k8s.aws/v1 kind: EC2NodeClass metadata: name: default spec: # 复用现有 node role(与 managed node group 一致) role: EKSInformalNodeRole # AMI 系列:与现有 node group 的 ami_type: AL2023_x86_64_STANDARD 一致 amiFamily: AL2023 # 用 subnet ID 直接选择(来自 terraform 的 subnet_ids) subnetSelectorTerms: - ids: - subnet-07fb6f96bcb2ae748 - subnet-016233b0ff4879d15 - subnet-071b80ca92f54e9f4 # 用 security group ID 直接选择(来自 terraform 的 vpc_security_group_ids) securityGroupSelectorTerms: - ids: - sg-00481e6a7dcc6c115 - sg-0997da79cee0a0c7e - sg-080049927008c894e # block device 与现有 node group 一致(100GB gp3) blockDeviceMappings: - deviceName: /dev/xvda ebs: volumeSize: 100Gi volumeType: gp3 iops: 3000 throughput: 125 encrypted: true deleteOnTermination: true # IMDS 配置与现有 node group 一致 metadataOptions: httpEndpoint: enabled httpProtocol: IPv4 httpPutResponseHopLimit: 2 httpTokens: required --- apiVersion: karpenter.sh/v1 kind: NodePool metadata: name: default spec: template: metadata: labels: nodepool: default managed-by: karpenter spec: nodeClassRef: group: karpenter.k8s.aws kind: EC2NodeClass name: default # taint:隔离系统组件,只让带 toleration 的业务 Pod 来 Karpenter 节点 taints: - key: karpenter.sh/disruption value: "true" effect: NoSchedule requirements: # 机型:沿用现有 r6a/r5a - key: node.kubernetes.io/instance-type operator: In values: - r6a.2xlarge - r5a.2xlarge - r6a.xlarge - r5a.xlarge - r6a.large - r5a.large # 关键:只用 on-demand,不用 spot # on-demand 不需要 SQS/EventBridge 中断处理 - key: karpenter.sh/capacity-type operator: In values: - on-demand # CPU 架构 - key: kubernetes.io/arch operator: In values: - amd64 # 3 个 AZ(对应 3 个 subnet,请核对) - key: topology.kubernetes.io/zone operator: In values: - ap-northeast-1a - ap-northeast-1c - ap-northeast-1d # 资源上限:控制总开销 limits: cpu: 100 memory: 800Gi # 空节点 60s 后回收 disruption: consolidationPolicy: WhenEmptyOrUnderutilized consolidateAfter: 60s budgets: - nodes: "10%"View Code
-
方案B 用:karpenter-iam-full.tf


# Karpenter 完整版 IAM(SPOT + SQS + EventBridge) # 创建:Controller IAM Role + SQS 队列 + EventBridge 规则 # 用法:terraform init && terraform apply -var="cluster_name=ltp-eks-nonprod" terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } variable "cluster_name" { default = "ltp-eks-nonprod" } variable "aws_region" { default = "ap-northeast-1" } data "aws_region" "current" {} data "aws_caller_identity" "current" {} data "aws_eks_cluster" "eks" { name = var.cluster_name } data "aws_iam_openid_connect_provider" "eks" { url = data.aws_eks_cluster.eks.identity[0].oidc[0].issuer } locals { account_id = data.aws_caller_identity.current.account_id region = var.aws_region } # ============================================================ # 1. SQS 队列:缓存 Spot 中断 / Rebalance / Health 事件 # ============================================================ resource "aws_sqs_queue" "karpenter" { name = "${var.cluster_name}-karpenter" message_retention_seconds = 300 sqs_managed_sse_enabled = true } # SQS 队列访问策略:允许 EventBridge 写入 resource "aws_sqs_queue_policy" "karpenter" { queue_url = aws_sqs_queue.karpenter.id policy = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow" Principal = { Service = "events.amazonaws.com" } Action = "sqs:SendMessage" Resource = aws_sqs_queue.karpenter.arn Condition = { ArnEquals = { "aws:SourceArn" = [ aws_cloudwatch_event_rule.spot_interruption.arn, aws_cloudwatch_event_rule.rebalance.arn, aws_cloudwatch_event_rule.unhealthy.arn ] } } }] }) } # ============================================================ # 2. EventBridge 规则:捕获 Spot 中断警告 # ============================================================ resource "aws_cloudwatch_event_rule" "spot_interruption" { name = "${var.cluster_name}-karpenter-spot" description = "Spot interruption notices for Karpenter" event_pattern = jsonencode({ source = ["aws.ec2"] detail-type = ["EC2 Spot Instance Interruption Warning"] }) } resource "aws_cloudwatch_event_target" "spot_interruption" { rule = aws_cloudwatch_event_rule.spot_interruption.name arn = aws_sqs_queue.karpenter.arn } # EventBridge 规则:捕获 Rebalance 建议 resource "aws_cloudwatch_event_rule" "rebalance" { name = "${var.cluster_name}-karpenter-rebalance" description = "Rebalance recommendation for Karpenter" event_pattern = jsonencode({ source = ["aws.ec2"] detail-type = ["EC2 Instance Rebalance Recommendation"] }) } resource "aws_cloudwatch_event_target" "rebalance" { rule = aws_cloudwatch_event_rule.rebalance.name arn = aws_sqs_queue.karpenter.arn } # EventBridge 规则:捕获 ASG 不健康实例 resource "aws_cloudwatch_event_rule" "unhealthy" { name = "${var.cluster_name}-karpenter-unhealthy" description = "Unhealthy instances for Karpenter" event_pattern = jsonencode({ source = ["aws.health"] detail-type = ["AWS Health Event"] }) } resource "aws_cloudwatch_event_target" "unhealthy" { rule = aws_cloudwatch_event_rule.unhealthy.name arn = aws_sqs_queue.karpenter.arn } # ============================================================ # 3. Karpenter Controller IAM 角色(通过 IRSA 绑到 controller pod) # ============================================================ resource "aws_iam_role" "karpenter_controller" { name = "karpenter-controller-${var.cluster_name}" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow" Principal = { Federated = "arn:aws:iam::${local.account_id}:oidc-provider/${data.aws_iam_openid_connect_provider.eks.url}" } Action = "sts:AssumeRoleWithWebIdentity" Condition = { StringEquals = { "${data.aws_iam_openid_connect_provider.eks.url}:sub" = "system:serviceaccount:karpenter:karpenter" } } }] }) } resource "aws_iam_role_policy" "karpenter_controller" { name = "karpenter-controller-${var.cluster_name}" role = aws_iam_role.karpenter_controller.id policy = jsonencode({ Version = "2012-10-17" Statement = [ # 开机器 { Effect = "Allow" Action = [ "ec2:CreateLaunchTemplate", "ec2:CreateFleet", "ec2:RunInstances", "ec2:CreateTags", "iam:PassRole" ] Resource = ["*"] }, # 查资源 { Effect = "Allow" Action = [ "ec2:DescribeImages", "ec2:DescribeInstances", "ec2:DescribeLaunchTemplates", "ec2:DescribeInstanceTypes", "ec2:DescribeSubnets", "ec2:DescribeSecurityGroups", "ec2:DescribeAvailabilityZones", "ec2:DescribeSpotPriceHistory", "ec2:DescribeVpcs" ] Resource = ["*"] }, # 删机器(只删带 discovery tag 的,碰不到 managed node group) { Effect = "Allow" Action = ["ec2:TerminateInstances"] Resource = ["*"] Condition = { StringEquals = { "ec2:ResourceTag/karpenter.sh/discovery" = var.cluster_name } } }, # SQS 权限:读取中断队列(方案B 专用) { Effect = "Allow" Action = [ "sqs:DeleteMessage", "sqs:GetQueueUrl", "sqs:ReceiveMessage" ] Resource = aws_sqs_queue.karpenter.arn }, # Spot Fleet 服务相关角色(可选,部分场景需要) { Effect = "Allow" Action = ["iam:CreateServiceLinkedRole"] Resource = ["arn:aws:iam::*:role/aws-service-role/spotfleet.amazonaws.com/AWSServiceRoleForEC2SpotFleet"] } ] }) } # ============================================================ # 输出 # ============================================================ output "karpenter_controller_role_arn" { value = aws_iam_role.karpenter_controller.arn } output "sqs_queue_name" { value = aws_sqs_queue.karpenter.name } output "sqs_queue_arn" { value = aws_sqs_queue.karpenter.arn } output "sqs_queue_url" { value = aws_sqs_queue.karpenter.url } output "aws_region" { value = local.region } output "aws_account_id" { value = local.account_id }View Code
-
方案B 用:karpenter-nodepool-spot.yaml


# Karpenter NodePool + EC2NodeClass(SPOT 版,需配合 SQS+EventBridge) # 与现有 managed node group (EKSInformalNodeRole) 并存 # 用法:kubectl apply -f karpenter-nodepool-spot.yaml # 前提:已用 karpenter-iam-full.tf 创建 SQS+EventBridge,helm 已传 interruptionQueueName apiVersion: karpenter.k8s.aws/v1 kind: EC2NodeClass metadata: name: default spec: # 复用现有 node role(与 managed node group 一致) role: EKSInformalNodeRole # AMI 系列:与现有 node group 的 ami_type: AL2023_x86_64_STANDARD 一致 amiFamily: AL2023 # 用 subnet ID 直接选择(来自 terraform 的 subnet_ids) subnetSelectorTerms: - ids: - subnet-07fb6f96bcb2ae748 - subnet-016233b0ff4879d15 - subnet-071b80ca92f54e9f4 # 用 security group ID 直接选择(来自 terraform 的 vpc_security_group_ids) securityGroupSelectorTerms: - ids: - sg-00481e6a7dcc6c115 - sg-0997da79cee0a0c7e - sg-080049927008c894e # block device 与现有 node group 一致(100GB gp3) blockDeviceMappings: - deviceName: /dev/xvda ebs: volumeSize: 100Gi volumeType: gp3 iops: 3000 throughput: 125 encrypted: true deleteOnTermination: true # IMDS 配置与现有 node group 一致 metadataOptions: httpEndpoint: enabled httpProtocol: IPv4 httpPutResponseHopLimit: 2 httpTokens: required --- apiVersion: karpenter.sh/v1 kind: NodePool metadata: name: default spec: template: metadata: labels: nodepool: default managed-by: karpenter spec: nodeClassRef: group: karpenter.k8s.aws kind: EC2NodeClass name: default # taint:隔离系统组件,只让带 toleration 的业务 Pod 来 Karpenter 节点 taints: - key: karpenter.sh/disruption value: "true" effect: NoSchedule requirements: # 机型:沿用现有 r6a/r5a,加更多机型提高 spot 拿到概率 # spot 场景下机型越多越好(某个无库存时自动换另一个) - key: node.kubernetes.io/instance-type operator: In values: - r6a.2xlarge - r5a.2xlarge - r6a.xlarge - r5a.xlarge - r6a.large - r5a.large # 关键:spot 优先(最便宜),无库存时回退 on-demand # 如要纯 spot:values: [spot] # 如要 spot 优先 + on-demand 兜底:values: [on-demand, spot] - key: karpenter.sh/capacity-type operator: In values: - spot - on-demand # CPU 架构 - key: kubernetes.io/arch operator: In values: - amd64 # 3 个 AZ(对应 3 个 subnet,请核对) # spot 多 AZ 分布能提高拿到实例的概率 - key: topology.kubernetes.io/zone operator: In values: - ap-northeast-1a - ap-northeast-1c - ap-northeast-1d # 资源上限:控制总开销 limits: cpu: 100 memory: 800Gi # 空节点 60s 后回收 disruption: consolidationPolicy: WhenEmptyOrUnderutilized consolidateAfter: 60s budgets: - nodes: "10%" # 节点启动超时(spot 拿实例可能稍慢,放宽到 5m) startupTimeout: 5mView Code
-
http-app-deployment.yaml ← 共用


# http-app 完整部署文件 # nginx + nginx-prometheus-exporter + Service + ServiceMonitor # 带 nodeSelector + toleration,只调度到 Karpenter 节点 # 用法:kubectl apply -f http-app-deployment.yaml # ============================================================ # ConfigMap: nginx 配置(返回 Hello, KEDA + 暴露 stub_status) # ============================================================ apiVersion: v1 kind: ConfigMap metadata: name: http-app-nginx-conf namespace: keda data: nginx.conf: | events {} http { server { listen 80; location / { return 200 "Hello, KEDA!\n"; } location /stub_status { stub_status; } } } --- # ============================================================ # Deployment: http-app(nginx 业务容器 + exporter sidecar) # ============================================================ apiVersion: apps/v1 kind: Deployment metadata: name: http-app namespace: keda labels: app: http-app spec: replicas: 1 selector: matchLabels: app: http-app template: metadata: labels: app: http-app spec: # 关键:只调度到 Karpenter 创建的节点 nodeSelector: nodepool: default # 关键:容忍 Karpenter 节点的 taint tolerations: - key: karpenter.sh/disruption operator: Exists effect: NoSchedule containers: # 业务容器:nginx - name: nginx image: nginx:stable ports: - name: http containerPort: 80 volumeMounts: - name: nginx-conf mountPath: /etc/nginx/nginx.conf subPath: nginx.conf # 关键:设置 requests,Karpenter 据此挑选机型 resources: requests: cpu: 500m memory: 512Mi limits: cpu: 1 memory: 1Gi # sidecar:nginx-prometheus-exporter,把 nginx 指标暴露成 Prometheus 格式 - name: exporter image: nginx/nginx-prometheus-exporter:1.1.0 args: - "-nginx.scrape-uri=http://localhost/stub_status" ports: - name: metrics containerPort: 9113 resources: requests: cpu: 100m memory: 128Mi volumes: - name: nginx-conf configMap: name: http-app-nginx-conf --- # ============================================================ # Service: 暴露 http 和 metrics 两个端口 # ============================================================ apiVersion: v1 kind: Service metadata: name: http-app namespace: keda labels: app: http-app spec: selector: app: http-app ports: # 业务端口 - name: http port: 80 targetPort: http # metrics 端口,给 Prometheus 抓 - name: metrics port: 9113 targetPort: metrics --- # ============================================================ # ServiceMonitor: 让 kube-prometheus-stack 自动采集 http-app 指标 # ============================================================ apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: http-app namespace: keda labels: app: http-app spec: selector: matchLabels: app: http-app namespaceSelector: matchNames: - keda endpoints: - port: metrics path: /metrics interval: 15s scrapeTimeout: 10sView Code
-
scaled.yaml ← 共用


apiVersion: keda.sh/v1alpha1 kind: ScaledObject metadata: name: http-app-scaledobject namespace: keda spec: scaleTargetRef: name: http-app minReplicaCount: 1 maxReplicaCount: 10 cooldownPeriod: 60 advanced: horizontalPodAutoscalerConfig: behavior: scaleDown: stabilizationWindowSeconds: 60 policies: - type: Percent value: 50 periodSeconds: 30 scaleUp: stabilizationWindowSeconds: 0 policies: - type: Percent value: 100 periodSeconds: 15 - type: Pods value: 4 periodSeconds: 15 selectPolicy: Max triggers: - type: prometheus metadata: serverAddress: http://kube-prom-stack-kube-prome-prometheus.monitoring.svc.cluster.local:9090 threshold: '3' query: sum(rate(nginx_http_requests_total{namespace="keda",service="http-app"}[1m]))View Code
-
一键部署脚本:
deploy-karpenter-keda.sh


#!/bin/bash ############################################################################### # Karpenter + KEDA 一键部署脚本 # 集群:ltp-eks-nonprod (ap-northeast-1) # 用法: # bash deploy-karpenter-keda.sh prereq # 检查前置条件 # bash deploy-karpenter-keda.sh iam # 阶段1: AWS 基础设施 (Terraform) # bash deploy-karpenter-keda.sh keda # 阶段2: 安装 KEDA (helm) # bash deploy-karpenter-keda.sh karpenter # 阶段3: 安装 Karpenter (helm) # bash deploy-karpenter-keda.sh nodepool # 阶段4: 部署 NodePool # bash deploy-karpenter-keda.sh app # 阶段5: 部署 http-app # bash deploy-karpenter-keda.sh scaled # 阶段6: 部署 ScaledObject # bash deploy-karpenter-keda.sh all # 全部执行 # bash deploy-karpenter-keda.sh test # 压测验证 # bash deploy-karpenter-keda.sh uninstall # 卸载全部 ############################################################################### set -euo pipefail # ============ 配置区(按需修改)============ CLUSTER_NAME="ltp-eks-nonprod" AWS_REGION="ap-northeast-1" NAMESPACE_KEDA="keda" NAMESPACE_KARPENTER="karpenter" NAMESPACE_APP="keda" # http-app 也放 keda namespace NODE_ROLE="EKSInformalNodeRole" PROMETHEUS_URL="http://kube-prom-stack-kube-prome-prometheus.monitoring.svc.cluster.local:9090" # subnet 和 security group(来自现有 terraform) SUBNET_IDS=("subnet-07fb6f96bcb2ae748" "subnet-016233b0ff4879d15" "subnet-071b80ca92f54e9f4") SG_IDS=("sg-00481e6a7dcc6c115" "sg-0997da79cee0a0c7e" "sg-080049927008c894e") # 可用区(先查:aws ec2 describe-subnets ...) AZS=("ap-northeast-1a" "ap-northeast-1c" "ap-northeast-1d") # KEDA 版本 KEDA_HELM_REPO="https://kedacore.github.io/charts" KEDA_VERSION="2.15.0" # Karpenter 版本 KARPENTER_HELM_REPO="https://charts.karpenter.sh" KARPENTER_VERSION="1.0.0" # 脚本所在目录(yaml 文件都在这里) SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # ============ 颜色输出 ============ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' log() { echo -e "${GREEN}[$(date +%H:%M:%S)]${NC} $*"; } warn() { echo -e "${YELLOW}[$(date +%H:%M:%S)] [WARN]${NC} $*"; } err() { echo -e "${RED}[$(date +%H:%M:%S)] [ERROR]${NC} $*" >&2; } step() { echo -e "\n${BLUE}========== $* ==========${NC}"; } # ============ 前置检查 ============ check_prereq() { step "前置条件检查" local ok=true # kubectl if ! command -v kubectl &>/dev/null; then err "kubectl 未安装" ok=false else log "kubectl: $(kubectl version --client --short 2>/dev/null || kubectl version --client | head -1)" fi # helm if ! command -v helm &>/dev/null; then err "helm 未安装" ok=false else log "helm: $(helm version --short)" fi # terraform if ! command -v terraform &>/dev/null; then warn "terraform 未安装(阶段1需要)" else log "terraform: $(terraform version | head -1)" fi # aws cli if ! command -v aws &>/dev/null; then err "aws cli 未安装" ok=false else log "aws cli: $(aws --version)" fi # 集群连通性 if kubectl get nodes &>/dev/null; then log "集群连通: $(kubectl get nodes --no-headers | wc -l) 个节点" else err "无法连接集群,请检查 kubeconfig" ok=false fi # EKS OIDC local oidc oidc=$(aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \ --query 'cluster.identity.oidc.issuer' --output text 2>/dev/null || echo "") if [ -n "$oidc" ] && [ "$oidc" != "None" ]; then log "OIDC provider: $oidc" else warn "OIDC provider 未启用(Karpenter IRSA 需要)" fi # 确认 AZ log "确认 subnet 对应的 AZ:" aws ec2 describe-subnets --region "$AWS_REGION" \ --subnet-ids "${SUBNET_IDS[@]}" \ --query 'Subnets[*].[SubnetId,AvailabilityZone]' --output table if [ "$ok" = true ]; then log "前置检查通过 ✓" else err "前置检查失败,请修复上述问题" exit 1 fi } # ============ 阶段1: AWS 基础设施 ============ deploy_iam() { step "阶段1: 创建 AWS 基础设施 (Terraform)" cd "$SCRIPT_DIR" if [ ! -f "karpenter-iam.tf" ]; then err "karpenter-iam.tf 不存在,请确认文件在 $SCRIPT_DIR 目录" exit 1 fi log "terraform init..." terraform init log "terraform plan..." terraform plan -var="cluster_name=$CLUSTER_NAME" -var="aws_region=$AWS_REGION" -out=tfplan log "terraform apply...(需要输入 yes 确认)" terraform apply tfplan log "记录输出值:" echo " CONTROLLER_ROLE_ARN = $(terraform output -raw karpenter_controller_role_arn)" echo " SQS_QUEUE_NAME = $(terraform output -raw sqs_queue_name)" log "阶段1 完成 ✓" } # ============ 阶段2: 安装 KEDA ============ deploy_keda() { step "阶段2: 安装 KEDA (helm)" log "添加 helm 仓库" helm repo add kedacore "$KEDA_HELM_REPO" 2>/dev/null || true helm repo update log "安装 KEDA v$KEDA_VERSION 到 $NAMESPACE_KEDA namespace" helm upgrade --install keda kedacore/keda \ --namespace "$NAMESPACE_KEDA" --create-namespace \ --version "$KEDA_VERSION" \ --set watchNamespace="\"\"" \ --wait --timeout 5m log "等待 KEDA pod 就绪..." kubectl wait --for=condition=Ready pod -l app.kubernetes.io/name=keda-operator \ -n "$NAMESPACE_KEDA" --timeout=300s kubectl get pod -n "$NAMESPACE_KEDA" log "阶段2 完成 ✓ KEDA 安装成功" } # ============ 阶段3: 安装 Karpenter ============ deploy_karpenter() { step "阶段3: 安装 Karpenter (helm)" cd "$SCRIPT_DIR" # 获取 terraform 输出值 local controller_role_arn queue_name account_id queue_url controller_role_arn=$(terraform output -raw karpenter_controller_role_arn 2>/dev/null || echo "") queue_name=$(terraform output -raw sqs_queue_name 2>/dev/null || echo "") account_id=$(aws sts get-caller-identity --query Account --output text) if [ -z "$controller_role_arn" ] || [ -z "$queue_name" ]; then err "无法获取 terraform 输出,请先执行阶段1 (iam)" err "或手动设置:controller_role_arn 和 queue_name" exit 1 fi queue_url="https://sqs.${AWS_REGION}.amazonaws.com/${account_id}/${queue_name}" log "Controller Role ARN: $controller_role_arn" log "Interruption Queue: $queue_url" log "添加 helm 仓库" helm repo add karpenter "$KARPENTER_HELM_REPO" 2>/dev/null || true helm repo update log "安装 Karpenter v$KARPENTER_VERSION 到 $NAMESPACE_KARPENTER namespace" helm upgrade --install karpenter karpenter/karpenter \ --namespace "$NAMESPACE_KARPENTER" --create-namespace \ --version "$KARPENTER_VERSION" \ --set "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn=${controller_role_arn}" \ --set "clusterName=${CLUSTER_NAME}" \ --set "interruptionQueueName=${queue_name}" \ --set "controller.resources.requests.cpu=1" \ --set "controller.resources.requests.memory=1Gi" \ --set "controller.resources.limits.memory=1Gi" \ --wait --timeout 5m log "等待 Karpenter pod 就绪..." kubectl wait --for=condition=Ready pod -l app.kubernetes.io/name=karpenter \ -n "$NAMESPACE_KARPENTER" --timeout=300s kubectl get pod -n "$NAMESPACE_KARPENTER" log "阶段3 完成 ✓ Karpenter 安装成功" # 检查 controller 日志有无报错 log "检查 controller 日志(最近 20 行):" kubectl logs -n "$NAMESPACE_KARPENTER" -l app.kubernetes.io/name=karpenter --tail=20 || true } # ============ 阶段4: 部署 NodePool ============ deploy_nodepool() { step "阶段4: 部署 Karpenter NodePool + EC2NodeClass" cd "$SCRIPT_DIR" if [ ! -f "karpenter-nodepool.yaml" ]; then err "karpenter-nodepool.yaml 不存在" exit 1 fi log "部署 NodePool..." kubectl apply -f karpenter-nodepool.yaml log "等待 NodePool 就绪..." sleep 5 kubectl get nodepool kubectl get ec2nodeclass # 检查 READY 状态 local np_ready np_ready=$(kubectl get nodepool default -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "Unknown") if [ "$np_ready" = "True" ]; then log "NodePool Ready ✓" else warn "NodePool 未就绪 ($np_ready),查看详情:" kubectl describe nodepool default fi log "阶段4 完成 ✓" } # ============ 阶段5: 部署 http-app ============ deploy_app() { step "阶段5: 部署 http-app (nginx + exporter + Service + ServiceMonitor)" cd "$SCRIPT_DIR" if [ ! -f "http-app-deployment.yaml" ]; then err "http-app-deployment.yaml 不存在" exit 1 fi log "部署 http-app..." kubectl apply -f http-app-deployment.yaml log "等待 Pod 就绪..." kubectl wait --for=condition=Ready pod -l app=http-app \ -n "$NAMESPACE_APP" --timeout=300s || { warn "Pod 未就绪,可能正在等待 Karpenter 开机器(nodeSelector 隔离)" warn "查看事件:kubectl get events -n $NAMESPACE_APP --field-selector reason=Karpenter -w" } kubectl get pod -n "$NAMESPACE_APP" -l app=http-app -o wide kubectl get svc -n "$NAMESPACE_APP" http-app kubectl get servicemonitor -n "$NAMESPACE_APP" http-app log "阶段5 完成 ✓" } # ============ 阶段6: 部署 ScaledObject ============ deploy_scaled() { step "阶段6: 部署 KEDA ScaledObject" cd "$SCRIPT_DIR" if [ ! -f "scaled.yaml" ]; then err "scaled.yaml 不存在" exit 1 fi log "部署 ScaledObject..." kubectl apply -f scaled.yaml log "等待 ScaledObject 就绪..." sleep 10 kubectl get scaledobject -n "$NAMESPACE_APP" local so_ready so_ready=$(kubectl get scaledobject -n "$NAMESPACE_APP" http-app-scaledobject \ -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "Unknown") if [ "$so_ready" = "True" ]; then log "ScaledObject Ready ✓" else warn "ScaledObject 未就绪,查看:" kubectl describe scaledobject -n "$NAMESPACE_APP" http-app-scaledobject fi log "阶段6 完成 ✓" } # ============ 压测验证 ============ run_test() { step "压测验证:KEDA + Karpenter 联动" log "当前状态:" kubectl get deploy -n "$NAMESPACE_APP" http-app kubectl get scaledobject -n "$NAMESPACE_APP" http-app-scaledobject kubectl get hpa -n "$NAMESPACE_APP" keda-hpa-http-app-scaledobject echo "" warn "即将启动压测,会打开一个 busybox pod 持续打流量" warn "请在 pod 内执行: while true; do wget -q -O /dev/null http://http-app.$NAMESPACE_APP.svc.cluster.local/; done" warn "另开终端观察:" echo " kubectl get deploy -n $NAMESPACE_APP http-app -w" echo " kubectl get node -w" echo " kubectl get hpa -n $NAMESPACE_APP keda-hpa-http-app-scaledobject -w" echo "" read -p "确认开始压测?(y/N) " confirm if [ "$confirm" != "y" ]; then log "已取消" return fi kubectl run -n "$NAMESPACE_APP" loadgen --rm -it --image=busybox -- /bin/sh } # ============ 全部部署 ============ deploy_all() { check_prereq deploy_iam deploy_keda deploy_karpenter deploy_nodepool deploy_app deploy_scaled step "全部部署完成" log "现在可以执行压测验证:" echo " bash $0 test" echo "" log "或手动压测:" echo " kubectl run -n $NAMESPACE_APP loadgen --rm -it --image=busybox -- /bin/sh" echo " # pod 内:while true; do wget -q -O /dev/null http://http-app.$NAMESPACE_APP.svc.cluster.local/; done" } # ============ 卸载 ============ uninstall_all() { step "卸载 KEDA + Karpenter" warn "即将删除以下资源:" echo " - ScaledObject + HPA" echo " - http-app (Deployment/Service/ServiceMonitor/ConfigMap)" echo " - Karpenter NodePool + EC2NodeClass(Karpenter 自动删除它创建的 EC2)" echo " - Karpenter controller (helm)" echo " - KEDA (helm)" echo " - AWS 基础设施 (Terraform destroy)" echo "" read -p "确认卸载?(y/N) " confirm if [ "$confirm" != "y" ]; then log "已取消" return fi # ScaledObject log "删除 ScaledObject..." kubectl delete scaledobject -n "$NAMESPACE_APP" http-app-scaledobject --ignore-not-found || true # http-app log "删除 http-app..." kubectl delete -f "$SCRIPT_DIR/http-app-deployment.yaml" --ignore-not-found || true # NodePool(先删 NodePool,等 Karpenter 删 EC2) log "删除 NodePool + EC2NodeClass..." kubectl delete nodepool default --ignore-not-found || true kubectl delete ec2nodeclass default --ignore-not-found || true log "等待 Karpenter 删除它创建的 EC2 节点(60s)..." echo " 观察:kubectl get node -w" sleep 60 # Karpenter controller log "卸载 Karpenter..." helm uninstall karpenter -n "$NAMESPACE_KARPENTER" 2>/dev/null || true kubectl delete namespace "$NAMESPACE_KARPENTER" --ignore-not-found || true # KEDA log "卸载 KEDA..." helm uninstall keda -n "$NAMESPACE_KEDA" 2>/dev/null || true # Terraform log "销毁 AWS 基础设施..." cd "$SCRIPT_DIR" terraform destroy -var="cluster_name=$CLUSTER_NAME" -var="aws_region=$AWS_REGION" || warn "Terraform destroy 需手动确认" log "卸载完成 ✓" } # ============ 主入口 ============ case "${1:-}" in prereq) check_prereq ;; iam) deploy_iam ;; keda) deploy_keda ;; karpenter) deploy_karpenter ;; nodepool) deploy_nodepool ;; app) deploy_app ;; scaled) deploy_scaled ;; all) deploy_all ;; test) run_test ;; uninstall) uninstall_all ;; *) echo "用法:" echo " bash $0 <command>" echo "" echo "命令:" echo " prereq 检查前置条件" echo " iam 阶段1: AWS 基础设施 (Terraform)" echo " keda 阶段2: 安装 KEDA (helm)" echo " karpenter 阶段3: 安装 Karpenter (helm)" echo " nodepool 阶段4: 部署 NodePool" echo " app 阶段5: 部署 http-app" echo " scaled 阶段6: 部署 ScaledObject" echo " all 全部部署" echo " test 压测验证" echo " uninstall 卸载全部" exit 1 ;; esacView Code
-
排查参考:
PERMISSION-TROUBLESHOOTING.md


# Karpenter 权限排查完整指南 > 集群:`ltp-eks-nonprod` / 区域:`ap-northeast-1` / Account:`2923xxxxxxxxx` > Node Role:`EKSInformalNodeRole` > OIDC ID:`882CDxxxxxxxxx` --- ## 目录 1. [权限分层模型](#1-权限分层模型) 2. [当前集群权限状态](#2-当前集群权限状态) 3. [排查一:OIDC Provider(IRSA 基础)](#3-排查一oidc-providerirsa-基础) 4. [排查二:第一层 AWS API 权限(Controller Role)](#4-排查二第一层-aws-api-权限controller-role) 5. [排查三:第二层 Node 权限(EKS/ECR)](#5-排查三第二层-node-权限eksecr) 6. [排查四:VPC CNI 权限](#6-排查四vpc-cni-权限) 7. [排查五:运行时权限验证](#7-排查五运行时权限验证) 8. [权限不足的修复方法](#8-权限不足的修复方法) 9. [完整排查脚本](#9-完整排查脚本) --- ## 1. 权限分层模型 Karpenter 扩缩节点涉及**三层权限**,缺任何一层都会失败: ``` ┌─────────────────────────────────────────────────────────────┐ │ Karpenter Controller Pod (namespace: karpenter) │ │ │ │ │ │ 通过 IRSA 假设 Controller IAM Role │ │ │ ↓ │ │ │ 第一层:AWS API 权限 │ │ │ - ec2:RunInstances/CreateFleet → 开机器 │ │ │ - ec2:TerminateInstances → 删机器 │ │ │ - ec2:Describe* → 查机型/subnet/sg │ │ │ - iam:PassRole → 传 node role 给 EC2│ │ │ - sqs:* → 读中断队列 │ │ │ │ │ └──────────────────────────────────────────────────┬───────┤ │ │ │ │ Karpenter 调用 EC2 API,用 Node Role 开机器 │ │ │ ↓ │ │ │ EC2 实例启动,用 Node IAM Role │ │ │ │ │ │ │ │ 第二层:Node EKS/ECR 权限 │ │ │ │ - AmazonEKSWorkerNodePolicy → 加入集群 │ │ │ │ - AmazonEC2ContainerRegistryReadOnly → 拉镜像 │ │ │ │ - AmazonEBSCSIDriverPolicy → 挂 EBS 卷 │ │ │ │ │ │ │ │ 第三层:VPC CNI 权限 │ │ │ │ - AmazonEKS_CNI_Policy → 分配 Pod IP │ │ │ │ (可在 node role 或 IRSA 给 CNI pod) │ │ │ │ │ │ │ └──────────────────────────────────────────────────┴───────┘ │ EC2 节点加入集群,kubelet 注册,Pod 调度上来 └─────────────────────────────────────────────────────────────┘ ``` | 层级 | 谁需要 | 作用 | 你的状态 | |------|--------|------|----------| | **第零层** OIDC | 集群 + IAM | 让 pod 能假设 IAM Role | ✅ 已确认 | | **第一层** AWS API | Controller Role | 开/删/查 EC2 | ❌ 未创建 Role | | **第二层** Node | EKSInformalNodeRole | 节点加入集群 + 拉镜像 | ✅ 齐全 | | **第三层** VPC CNI | node role 或 IRSA | 分配 Pod IP | ⚠️ 需确认 | --- ## 2. 当前集群权限状态 基于你的实际检查结果: ### OIDC(第零层)✅ ``` 集群侧 issuer:https://oidc.eks.ap-northeast-1.amazonaws.com/id/882CDxxxxxxxxx IAM 侧 provider:arn:aws:iam::2923xxxxxxxxx:oidc-provider/oidc.eks.ap-northeast-1.amazonaws.com/id/882CDxxxxxxxxx ``` **IRSA 链路完全通**,pod 可以假设 IAM Role。 ### Controller Role(第一层)❌ **尚未创建**。需要执行 `terraform apply -var="cluster_name=ltp-eks-nonprod"` 创建: - IAM Role:`karpenter-controller-ltp-eks-nonprod` - 权限策略:EC2 创建/查询/终止 + SQS + iam:PassRole - SQS 队列:`ltp-eks-nonprod-karpenter` - EventBridge 规则:Spot 中断/Rebalance/Health 通知 ### Node Role(第二层)✅ `EKSInformalNodeRole` 已挂的 policy: | Policy | 用途 | Karpenter 需要 | 状态 | |--------|------|----------------|------| | AmazonEKSWorkerNodePolicy | node 加入集群 | ✅ 必须 | ✅ 已有 | | AmazonEC2ContainerRegistryReadOnly | 拉 ECR 镜像 | ✅ 必须 | ✅ 已有 | | AmazonEC2FullAccess | EC2 完整权限 | bonus(含终止实例)| ✅ 已有 | | AmazonSSMManagedInstanceCore | SSM 登录排查 | 可选 | ✅ 已有 | | AmazonEBSCSIDriverPolicy | EBS 卷挂载 | ✅ 必须 | ✅ 已有 | | AmazonEKSClusterPolicy | 集群 API | 通常 cluster role 才挂 | ⚠️ node role 挂着无害 | | ROSAAmazonEBSCSIDriverOperatorPolicy | ROSA EBS | ROSA 专用 | 不影响 | | 自定义 RDS policy | 业务相关 | 不影响 | 不影响 | **第二层权限齐全**,Karpenter 开出的实例用这个 role 能正常加入集群、拉镜像、挂 EBS。 ### VPC CNI(第三层)⚠️ `EKSInformalNodeRole` 没挂 `AmazonEKS_CNI_Policy`。需要确认: ```bash # 检查 aws-node daemonset 是否用 IRSA(推荐方式) kubectl get sa -n kube-system aws-node -o yaml | grep -A2 annotations # 如果有 eks.amazonaws.com/role-arn → CNI 用 IRSA,node role 不需要 CNI policy # 如果没有 → CNI 依赖 node role 的 CNI policy,需要补 ``` **为什么你的节点能正常跑 Pod**(即使没挂 CNI policy):大概率 VPC CNI 用了 IRSA 单独授权。这不影响 Karpenter------新节点用同一个 node role,CNI pod 同样能用 IRSA。 --- ## 3. 排查一:OIDC Provider(IRSA 基础) ### 3.1 检查集群侧 OIDC issuer ```bash # 方法一:aws cli aws eks describe-cluster --name ltp-eks-nonprod --region ap-northeast-1 \ --query 'cluster.identity.oidc.issuer' --output text # 期望输出:https://oidc.eks.ap-northeast-1.amazonaws.com/id/882CDxxxxxxxxx # 方法二:kubectl(通过集群 tag) kubectl get cluster-info -o jsonpath='{.metadata}' 2>/dev/null || true # 方法三:eksctl eksctl get cluster --name ltp-eks-nonprod --region ap-northeast-1 -o json | grep -i oidc ``` **判断标准**:有 URL 输出 = 集群侧已启用。 ### 3.2 检查 IAM 侧 OIDC provider ```bash # 提取 OIDC ID OIDC_ID=$(aws eks describe-cluster --name ltp-eks-nonprod --region ap-northeast-1 \ --query 'cluster.identity.oidc.issuer' --output text | cut -d'/' -f5) echo $OIDC_ID # 期望:882CDxxxxxxxxx # 查 IAM OIDC provider 是否存在 aws iam list-open-id-connect-providers \ --query "OpenIDConnectProviderList[?contains(Arn, '${OIDC_ID}')].Arn" \ --output text # 期望输出:arn:aws:iam::2923xxxxxxxxx:oidc-provider/oidc.eks.ap-northeast-1.amazonaws.com/id/882CDxxxxxxxxx ``` **判断标准**:有 ARN 输出 = IAM 侧已创建。 ### 3.3 验证 OIDC provider 的 client ID 和 thumbprint ```bash # 查 OIDC provider 的详细配置 aws iam get-open-id-connect-provider \ --open-id-connect-provider-arn arn:aws:iam::2923xxxxxxxxx:oidc-provider/oidc.eks.ap-northeast-1.amazonaws.com/id/882CDxxxxxxxxx # 期望输出: # { # "Url": "oidc.eks.ap-northeast-1.amazonaws.com/id/882CDxxxxxxxxx", # "ClientIDList": ["sts.amazonaws.com"], ← 必须有这个 # "ThumbprintList": ["9e99a48a9960c14951fb80d9e1c29e6616e52e3b"] ← 根 CA thumbprint # } ``` **判断标准**: - `ClientIDList` 包含 `sts.amazonaws.com` - `ThumbprintList` 非空(thumbprint 历史上是根 CA,新版 AWS 可能自动管理) ### 3.4 OIDC 不通的修复 ```bash # 方法一:eksctl(推荐,自动处理 thumbprint) eksctl utils associate-iam-oidc-provider \ --cluster ltp-eks-nonprod \ --region ap-northeast-1 \ --approve # 方法二:aws cli aws iam create-open-id-connect-provider \ --url https://oidc.eks.ap-northeast-1.amazonaws.com/id/882CDxxxxxxxxx \ --client-id-list sts.amazonaws.com \ --thumbprint-list 9e99a48a9960c14951fb80d9e1c29e6616e52e3b ``` --- ## 4. 排查二:第一层 AWS API 权限(Controller Role) ### 4.1 检查 Controller Role 是否存在 ```bash # 查 role 是否已创建 aws iam get-role --role-name karpenter-controller-ltp-eks-nonprod 2>&1 # 已创建 → 输出 role 详情 # 未创建 → NoSuchEntity 错误(你的当前状态) ``` ### 4.2 检查 Role 的信任策略(Trust Policy) 信任策略决定谁能假设这个 role。Karpenter 的 controller pod 通过 IRSA 假设,trust 必须指向 OIDC provider。 ```bash aws iam get-role --role-name karpenter-controller-ltp-eks-nonprod \ --query 'Role.AssumeRolePolicyDocument' --output text | jq . # 期望输出: # { # "Version": "2012-10-17", # "Statement": [{ # "Effect": "Allow", # "Principal": { # "Federated": "arn:aws:iam::2923xxxxxxxxx:oidc-provider/oidc.eks.ap-northeast-1.amazonaws.com/id/882CDxxxxxxxxx" # }, # "Action": "sts:AssumeRoleWithWebIdentity", # "Condition": { # "StringEquals": { # "oidc.eks.ap-northeast-1.amazonaws.com/id/882CDxxxxxxxxx:sub": "system:serviceaccount:karpenter:karpenter" # } # } # }] # } ``` **判断标准**: - `Federated` ARN 包含你的 OIDC ID - `Condition.StringEquals` 的 key 是 `<oidc-url>:sub`,value 是 `system:serviceaccount:karpenter:karpenter` - `Action` 是 `sts:AssumeRoleWithWebIdentity` ### 4.3 检查 Role 的权限策略 ```bash # 列出 role 的 inline policy aws iam list-role-policies --role-name karpenter-controller-ltp-eks-nonprod # 查看具体 policy 内容 aws iam get-role-policy --role-name karpenter-controller-ltp-eks-nonprod \ --policy-name karpenter-controller-ltp-eks-nonprod --query 'PolicyDocument' --output text | jq . ``` **必须包含的 Action 清单**: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ec2:CreateLaunchTemplate", "ec2:CreateFleet", "ec2:RunInstances", "ec2:CreateTags", "iam:PassRole" ], "Resource": ["*"] }, { "Effect": "Allow", "Action": [ "ec2:DescribeImages", "ec2:DescribeInstances", "ec2:DescribeLaunchTemplates", "ec2:DescribeInstanceTypes", "ec2:DescribeSubnets", "ec2:DescribeSecurityGroups", "ec2:DescribeAvailabilityZones", "ec2:DescribeSpotPriceHistory", "ec2:DescribeVpcs" ], "Resource": ["*"] }, { "Effect": "Allow", "Action": ["ec2:TerminateInstances"], "Resource": ["*"], "Condition": { "StringEquals": { "ec2:ResourceTag/karpenter.sh/discovery": "ltp-eks-nonprod" } } }, { "Effect": "Allow", "Action": ["sqs:DeleteMessage", "sqs:GetQueueUrl", "sqs:ReceiveMessage"], "Resource": "arn:aws:sqs:ap-northeast-1:2923xxxxxxxxx:ltp-eks-nonprod-karpenter" } ] } ``` **逐项验证清单**: | Action | 用途 | 验证命令 | |--------|------|----------| | `ec2:RunInstances` | 开机器 | controller 日志无 UnauthorizedOperation | | `ec2:CreateFleet` | 批量开机器 | 同上 | | `ec2:TerminateInstances` | 删机器 | 缩容时 controller 无权限错 | | `ec2:DescribeSubnets` | 找 subnet | EC2NodeClass Ready=True | | `ec2:DescribeSecurityGroups` | 找 sg | 同上 | | `ec2:DescribeInstanceTypes` | 列机型 | NodePool 能选机型 | | `ec2:DescribeImages` | 找 AMI | EC2NodeClass Ready=True | | `iam:PassRole` | 传 node role | 开机器时报错 `is not authorized to perform: iam:PassRole` | | `sqs:ReceiveMessage` | 读中断队列 | spot 节点中断时能收到通知 | ### 4.4 检查 SQS 队列 ```bash # 队列是否存在 aws sqs get-queue-url --queue-name ltp-eks-nonprod-karpenter --region ap-northeast-1 # 期望输出:https://sqs.ap-northeast-1.amazonaws.com/2923xxxxxxxxx/ltp-eks-nonprod-karpenter # 队列的访问策略(是否允许 EventBridge 写入) aws sqs get-queue-attributes \ --queue-url https://sqs.ap-northeast-1.amazonaws.com/2923xxxxxxxxx/ltp-eks-nonprod-karpenter \ --attribute-names Policy --region ap-northeast-1 --query 'Attributes.Policy' --output text | jq . # 期望包含:允许 events.amazonaws.com 写入 ``` ### 4.5 检查 EventBridge 规则 ```bash # 列出规则 aws events list-rules --name-prefix ltp-eks-nonprod-karpenter --region ap-northeast-1 # 查看规则详情 aws events describe-rule --name ltp-eks-nonprod-karpenter-spot --region ap-northeast-1 # 查看目标(是否指向 SQS) aws events list-targets-by-rule --rule ltp-eks-nonprod-karpenter-spot --region ap-northeast-1 # 期望 Arn 指向 SQS 队列 ``` ### 4.6 检查 K8s ServiceAccount 的 IRSA annotation ```bash # Karpenter 的 SA 是否有 IRSA annotation kubectl get sa -n karpenter karpenter -o yaml | grep -A2 annotations # 期望输出: # annotations: # eks.amazonaws.com/role-arn: arn:aws:iam::2923xxxxxxxxx:role/karpenter-controller-ltp-eks-nonprod # 如果没有,helm 安装时没传 role-arn,需要重装 ``` ### 4.7 Controller Role 不存在的修复 ```bash # 执行 Terraform 创建 cd outputs/ terraform init terraform apply -var="cluster_name=ltp-eks-nonprod" -var="aws_region=ap-northeast-1" # 输入 yes 确认 ``` --- ## 5. 排查三:第二层 Node 权限(EKS/ECR) ### 5.1 列出 Node Role 的所有 managed policy ```bash # managed policy(AWS 托管的) aws iam list-attached-role-policies --role-name EKSInformalNodeRole \ --query 'AttachedPolicies[].PolicyName' --output table # inline policy(用户自定义嵌入的) aws iam list-role-policies --role-name EKSInformalNodeRole \ --query 'PolicyNames' --output table ``` ### 5.2 必须项验证清单 | Policy | 作用 | 必须性 | 你的状态 | |--------|------|--------|----------| | AmazonEKSWorkerNodePolicy | node 向 EKS 注册、加入集群 | ✅ 必须 | ✅ 已有 | | AmazonEC2ContainerRegistryReadOnly | 从 ECR 拉业务镜像 | ✅ 必须 | ✅ 已有 | | AmazonEBSCSIDriverPolicy | 挂载 EBS PVC | ✅ 推荐 | ✅ 已有 | | AmazonSSMManagedInstanceCore | SSM Session Manager 登录节点排查 | 可选 | ✅ 已有 | | AmazonEKS_CNI_Policy | VPC CNI 分配 Pod IP | ✅ 必须(除非用 IRSA)| ⚠️ 未挂 | ### 5.3 验证 node role 能加入集群 ```bash # 检查 aws-auth ConfigMap 是否包含 EKSInformalNodeRole kubectl get configmap -n kube-system aws-auth -o yaml | grep -A2 EKSInformalNodeRole # 期望输出: # - rolearn: arn:aws:iam::2923xxxxxxxxx:role/EKSInformalNodeRole # username: system:node:{{EC2PrivateDNSName}} # groups: # - system:bootstrappers # - system:nodes # 如果没有,Karpenter 开的实例无法注册到集群(现有 managed node group 能跑说明已有) ``` ### 5.4 验证能拉 ECR 镜像 ```bash # 现有节点是否正常拉镜像(看 kubelet 日志无 403/401) kubectl get nodes -o wide # 选一个节点 NODE=$(kubectl get nodes -o name | head -1) # 看节点上的 Pod 是否正常 Running(镜像拉成功) kubectl get pod -A --field-selector spec.nodeName=${NODE} --no-headers | grep -v Running # 空输出 = 都在 Running,镜像拉取正常 ``` ### 5.5 验证 EBS CSI 驱动 ```bash # EBS CSI driver 是否安装(Karpenter 节点要挂 EBS) kubectl get pod -n kube-system | grep ebs-csi # 或 kubectl get csidriver ebs.csi.aws.com # 存在 = EBS CSI 已装,能用 AmazonEBSCSIDriverPolicy ``` ### 5.6 Node Role 权限不足的修复 ```bash # 补 AmazonEKSWorkerNodePolicy aws iam attach-role-policy --role-name EKSInformalNodeRole \ --policy-arn arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy # 补 AmazonEC2ContainerRegistryReadOnly aws iam attach-role-policy --role-name EKSInformalNodeRole \ --policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly # 补 AmazonEBSCSIDriverPolicy aws iam attach-role-policy --role-name EKSInformalNodeRole \ --policy-arn arn:aws:iam::aws:policy/AmazonEBSCSIDriverPolicy # 补 AmazonEKS_CNI_Policy(仅在 CNI 不用 IRSA 时才需要) aws iam attach-role-policy --role-name EKSInformalNodeRole \ --policy-arn arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy # 补到 aws-auth ConfigMap(如果缺失) kubectl edit configmap -n kube-system aws-auth # 添加: # - rolearn: arn:aws:iam::2923xxxxxxxxx:role/EKSInformalNodeRole # username: system:node:{{EC2PrivateDNSName}} # groups: # - system:bootstrappers # - system:nodes ``` --- ## 6. 排查四:VPC CNI 权限 ### 6.1 检查 VPC CNI 是否用 IRSA(推荐方式) ```bash # 看 aws-node ServiceAccount 是否有 IRSA annotation kubectl get sa -n kube-system aws-node -o yaml | grep -A2 annotations # 有 eks.amazonaws.com/role-arn → 用 IRSA(推荐) # 没有 → 依赖 node role 的 AmazonEKS_CNI_Policy ``` ### 6.2 如果用 IRSA,验证 CNI Role ```bash # 提取 CNI role ARN CNI_ROLE_ARN=$(kubectl get sa -n kube-system aws-node -o jsonpath='{.metadata.annotations.eks\.amazonaws\.com/role-arn}') echo $CNI_ROLE_ARN # 查 CNI role 的 policy CNI_ROLE_NAME=$(echo $CNI_ROLE_ARN | cut -d'/' -f2) aws iam list-attached-role-policies --role-name $CNI_ROLE_NAME \ --query 'AttachedPolicies[].PolicyName' --output table # 期望包含:AmazonEKS_CNI_Policy ``` ### 6.3 验证 CNI 在新节点能工作 ```bash # Karpenter 开机器后,检查新节点的 aws-node pod kubectl get pod -n kube-system -l k8s-app=aws-node --field-selector spec.nodeName=<karpenter-node> -o wide # Running + 无错误 = CNI 正常 # 如果 ImagePullBackOff → node role 缺 ECR policy # 如果 CrashLoopBackOff → CNI 权限问题 ``` ### 6.4 CNI 权限不足的修复 ```bash # 方案一:给 node role 补 CNI policy(简单但不推荐) aws iam attach-role-policy --role-name EKSInformalNodeRole \ --policy-arn arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy # 方案二:用 IRSA 给 CNI pod 单独授权(推荐,更安全) eksctl create iamserviceaccount \ --cluster ltp-eks-nonprod \ --region ap-northeast-1 \ --name aws-node \ --namespace kube-system \ --attach-policy-arn arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy \ --override \ --approve ``` --- ## 7. 排查五:运行时权限验证 部署完 Karpenter 后,用这些命令验证权限是否真正生效。 ### 7.1 Controller pod 假设 Role 成功 ```bash # 看 controller pod 的环境变量(IRSA token 注入成功) kubectl exec -n karpenter -l app.kubernetes.io/name=karpenter -- env | grep AWS # 期望输出: # AWS_ROLE_ARN=arn:aws:iam::2923xxxxxxxxx:role/karpenter-controller-ltp-eks-nonprod # AWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/eks.amazonaws.com/serviceaccount/token # 如果没有这两个变量 → IRSA 没注入,SA annotation 错或 OIDC 不通 ``` ### 7.2 Controller 日志无权限错误 ```bash # 实时看 controller 日志 kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter -f # 常见权限错误: # - "AccessDenied: User: arn:aws:sts::xxx:assumed-role/... is not authorized" # → Controller Role 权限策略缺 action # - "NoCredentialProviders: no valid providers in chain" # → IRSA 没注入 # - "failed to get credentials" → 同上 ``` ### 7.3 触发一次扩容,看是否成功 ```bash # 部署一个占资源的 pod 触发 Karpenter kubectl apply -f - <<EOF apiVersion: apps/v1 kind: Deployment metadata: name: inflate spec: replicas: 1 selector: matchLabels: app: inflate template: metadata: labels: app: inflate spec: containers: - name: inflate image: public.ecr.aws/eks-distro/kubernetes/pause:3.7 resources: requests: cpu: 8 memory: 16Gi EOF # 看 Karpenter 事件 kubectl get events -A --field-selector reason=Karpenter -w # 成功: # - created nodeclaim ... # - launched instance ... # - nodeclaim became ready # 失败: # - UnauthorizedOperation → Controller Role 权限不足 # - InsufficientInstanceCapacity → 机型无库存 # - operations error → 看 controller 日志 ``` ### 7.4 节点成功加入集群 ```bash # 等几分钟,看新节点出现 kubectl get node -w # 新节点 Ready = node role 权限正常(加入集群 + 拉镜像) # 看新节点的 role kubectl get node <node-name> -o yaml | grep "eks.amazonaws.com/node-identity" # 或 aws ec2 describe-instances --region ap-northeast-1 \ --filters "Name=tag:karpenter.sh/nodepool,Values=default" \ --query 'Reservations[].Instances[].IamInstanceProfile.Arn' --output text # 应指向 EKSInformalNodeRole ``` ### 7.5 Pod 成功调度到新节点 ```bash # inflate pod 变 Running kubectl get pod -l app=inflate -o wide # STATUS=Running, NODE=<karpenter-node> → 全链路权限通 # 如果 Pod 一直 ContainerCreating: kubectl describe pod <pod-name> | tail -10 # - ImagePullBackOff → node role 缺 ECR policy # - FailedMount → EBS CSI 权限问题 # - failed to setup network → CNI 权限问题 ``` --- ## 8. 权限不足的修复方法 ### 8.1 Controller Role 不存在(你的当前状态) ```bash # 用 Terraform 创建 cd outputs/ terraform init terraform apply -var="cluster_name=ltp-eks-nonprod" -var="aws_region=ap-northeast-1" ``` 创建后重新安装或重启 Karpenter controller: ```bash # 重启 controller 让它重新加载 IRSA kubectl rollout restart deployment -n karpenter karpenter ``` ### 8.2 Controller Role 权限不足 ```bash # 看缺哪个 action kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter | grep -i "not authorized\|denied" # 输出形如:is not authorized to perform: ec2:RunInstances # → 在 policy 里补对应 action # 修改 policy aws iam put-role-policy \ --role-name karpenter-controller-ltp-eks-nonprod \ --policy-name karpenter-controller-ltp-eks-nonprod \ --policy-document file://updated-policy.json # 或直接改 Terraform 的 karpenter-iam.tf,重新 terraform apply ``` ### 8.3 IRSA 没注入到 controller pod ```bash # 检查 SA annotation kubectl get sa -n karpenter karpenter -o yaml | grep role-arn # 没有 → helm 安装时没传 role-arn,重装 helm upgrade karpenter karpenter/karpenter \ --namespace karpenter \ --set "serviceAccount.annotations.eks\.amazonaws\.com/role-arn=arn:aws:iam::2923xxxxxxxxx:role/karpenter-controller-ltp-eks-nonprod" \ --reuse-values # 重启 pod 让 IRSA 生效 kubectl rollout restart deployment -n karpenter karpenter ``` ### 8.4 Node Role 缺 policy ```bash # 补 managed policy aws iam attach-role-policy --role-name EKSInformalNodeRole \ --policy-arn arn:aws:iam::aws:policy/<PolicyName> # 补 inline policy(自定义权限) aws iam put-role-policy \ --role-name EKSInformalNodeRole \ --policy-name karpenter-node-extra \ --policy-document file://node-extra-policy.json ``` ### 8.5 节点加入集群失败 ```bash # 检查 aws-auth ConfigMap kubectl get configmap -n kube-system aws-auth -o yaml # 如果缺 EKSInformalNodeRole 的映射,编辑加上 kubectl edit configmap -n kube-system aws-auth # 在 mapRoles 下添加: # - rolearn: arn:aws:iam::2923xxxxxxxxx:role/EKSInformalNodeRole # username: system:node:{{EC2PrivateDNSName}} # groups: # - system:bootstrappers # - system:nodes ``` --- ## 9. 完整排查脚本 把下面的脚本存为 `check-permissions.sh`,一键检查所有权限层: ```bash #!/bin/bash # 权限排查脚本 # 用法:bash check-permissions.sh set -e CLUSTER_NAME="ltp-eks-nonprod" REGION="ap-northeast-1" ACCOUNT_ID="2923xxxxxxxxx" NODE_ROLE="EKSInformalNodeRole" CONTROLLER_ROLE="karpenter-controller-${CLUSTER_NAME}" OIDC_ID=$(aws eks describe-cluster --name $CLUSTER_NAME --region $REGION \ --query 'cluster.identity.oidc.issuer' --output text | cut -d'/' -f5) echo "==========================================" echo "Karpenter 权限排查" echo "集群: $CLUSTER_NAME | 区域: $REGION" echo "==========================================" # 第零层:OIDC echo "" echo "[第零层] OIDC Provider" echo "OIDC ID: $OIDC_ID" OIDC_ARN=$(aws iam list-open-id-connect-providers \ --query "OpenIDConnectProviderList[?contains(Arn, '${OIDC_ID}')].Arn" \ --output text 2>/dev/null || echo "未找到") echo "IAM OIDC Provider: $OIDC_ARN" if [ -n "$OIDC_ARN" ] && [ "$OIDC_ARN" != "未找到" ]; then echo "✅ OIDC 正常" else echo "❌ OIDC 未创建,需修复:eksctl utils associate-iam-oidc-provider --cluster $CLUSTER_NAME --region $REGION --approve" fi # 第一层:Controller Role echo "" echo "[第一层] Controller Role: $CONTROLLER_ROLE" if aws iam get-role --role-name $CONTROLLER_ROLE &>/dev/null; then echo "✅ Role 存在" # 检查 trust policy TRUST=$(aws iam get-role --role-name $CONTROLLER_ROLE \ --query 'Role.AssumeRolePolicyDocument.Statement[0].Principal.Federated' \ --output text 2>/dev/null || echo "") if [[ "$TRUST" == *"$OIDC_ID"* ]]; then echo "✅ Trust Policy 指向正确 OIDC" else echo "⚠️ Trust Policy 可能不对,请检查" fi # 检查 SA annotation if kubectl get sa -n karpenter karpenter &>/dev/null; then SA_ROLE=$(kubectl get sa -n karpenter karpenter -o jsonpath='{.metadata.annotations.eks\.amazonaws\.com/role-arn}' 2>/dev/null || echo "") if [[ "$SA_ROLE" == *"$CONTROLLER_ROLE"* ]]; then echo "✅ ServiceAccount IRSA annotation 正确" else echo "⚠️ ServiceAccount 缺 IRSA annotation" fi fi else echo "❌ Role 不存在,需创建:terraform apply -var=cluster_name=$CLUSTER_NAME" fi # SQS 队列 echo "" echo "[SQS] 中断队列: ${CLUSTER_NAME}-karpenter" if aws sqs get-queue-url --queue-name "${CLUSTER_NAME}-karpenter" --region $REGION &>/dev/null; then echo "✅ SQS 队列存在" else echo "❌ SQS 队列不存在" fi # 第二层:Node Role echo "" echo "[第二层] Node Role: $NODE_ROLE" POLICIES=$(aws iam list-attached-role-policies --role-name $NODE_ROLE \ --query 'AttachedPolicies[].PolicyName' --output text 2>/dev/null || echo "") echo "已挂的 policy:" echo "$POLICIES" | tr '\t' '\n' REQUIRED=("AmazonEKSWorkerNodePolicy" "AmazonEC2ContainerRegistryReadOnly" "AmazonEBSCSIDriverPolicy") for p in "${REQUIRED[@]}"; do if echo "$POLICIES" | grep -q "$p"; then echo "✅ $p" else echo "❌ 缺 $p" fi done # VPC CNI echo "" echo "[第三层] VPC CNI" CNI_SA_ROLE=$(kubectl get sa -n kube-system aws-node -o jsonpath='{.metadata.annotations.eks\.amazonaws\.com/role-arn}' 2>/dev/null || echo "") if [ -n "$CNI_SA_ROLE" ]; then echo "✅ CNI 用 IRSA: $CNI_SA_ROLE" else if echo "$POLICIES" | grep -q "AmazonEKS_CNI_Policy"; then echo "✅ CNI 用 node role 的 AmazonEKS_CNI_Policy" else echo "⚠️ CNI 权限未明确(可能有问题)" fi fi # aws-auth ConfigMap echo "" echo "[aws-auth] Node Role 映射" if kubectl get configmap -n kube-system aws-auth -o yaml 2>/dev/null | grep -q "$NODE_ROLE"; then echo "✅ aws-auth 包含 $NODE_ROLE" else echo "⚠️ aws-auth 可能缺 $NODE_ROLE 映射" fi echo "" echo "==========================================" echo "排查完成" echo "==========================================" ``` ### 使用方法 ```bash # 赋予执行权限 chmod +x check-permissions.sh # 执行排查 bash check-permissions.sh ``` ### 预期输出(你修复后的状态) ``` ========================================== Karpenter 权限排查 集群: ltp-eks-nonprod | 区域: ap-northeast-1 ========================================== [第零层] OIDC Provider OIDC ID: 882CDxxxxxxxxx IAM OIDC Provider: arn:aws:iam::2923xxxxxxxxx:oidc-provider/... ✅ OIDC 正常 [第一层] Controller Role: karpenter-controller-ltp-eks-nonprod ✅ Role 存在 ✅ Trust Policy 指向正确 OIDC ✅ ServiceAccount IRSA annotation 正确 [SQS] 中断队列: ltp-eks-nonprod-karpenter ✅ SQS 队列存在 [第二层] Node Role: EKSInformalNodeRole ✅ AmazonEKSWorkerNodePolicy ✅ AmazonEC2ContainerRegistryReadOnly ✅ AmazonEBSCSIDriverPolicy [第三层] VPC CNI ✅ CNI 用 IRSA: arn:aws:iam::2923xxxxxxxxx:role/... [aws-auth] Node Role 映射 ✅ aws-auth 包含 EKSInformalNodeRole ========================================== 排查完成 ========================================== ``` --- ## 10. 权限矩阵速查 | 权限项 | 谁需要 | 验证命令 | 你的状态 | |--------|--------|----------|----------| | OIDC issuer | 集群 | `aws eks describe-cluster ... oidc.issuer` | ✅ | | OIDC provider | IAM | `aws iam list-open-id-connect-providers` | ✅ | | Controller Role | IAM | `aws iam get-role --role-name karpenter-controller-*` | ❌ 未创建 | | Controller Trust Policy | Role | `aws iam get-role ... AssumeRolePolicyDocument` | ❌ | | Controller 权限策略 | Role | `aws iam get-role-policy ...` | ❌ | | SA IRSA annotation | K8s SA | `kubectl get sa -n karpenter karpenter` | ❌ | | SQS 队列 | SQS | `aws sqs get-queue-url` | ❌ | | EventBridge 规则 | Events | `aws events list-rules` | ❌ | | AmazonEKSWorkerNodePolicy | Node Role | `aws iam list-attached-role-policies` | ✅ | | AmazonEC2ContainerRegistryReadOnly | Node Role | 同上 | ✅ | | AmazonEBSCSIDriverPolicy | Node Role | 同上 | ✅ | | AmazonEKS_CNI_Policy | Node Role 或 IRSA | 同上 / `kubectl get sa aws-node` | ⚠️ 用 IRSA | | aws-auth ConfigMap | K8s | `kubectl get cm aws-auth -n kube-system` | ✅ | --- ## 11. 你的下一步 **只差第一层(Controller Role)**,执行: ```bash cd outputs/ terraform init terraform apply -var="cluster_name=ltp-eks-nonprod" -var="aws_region=ap-northeast-1" ``` 创建完成后,用上面的排查脚本验证: ```bash bash check-permissions.sh ``` 全部 ✅ 后,就可以继续安装 Karpenter controller(helm)和部署 NodePool 了。View Code
4.方案总结
1.架构决策:分层节点池设计(系统组件留 managed node group,业务 Pod 走 Karpenter,taint+nodeSelector 隔离)
2.方案选型矩阵:on-demand / spot+SQS / 混合分层三种方案的适用场景
3. 五阶段落地流程:前置评估 → IAM → 组件安装 → NodePool+应用 → 验证压测 → 渐进上线
4. 成本实测:r6a.2xlarge spot 比 on-demand 每月省约 $2,735(10 台),spot 节省 60-90%
5. 五条铁律 + 7 条避坑经验:指标先行、分层隔离、权限最小化、缩容保守、spot 审慎;以及 query 必须带 label、Pending Pod 是 Node 层信号等
6. 安全加固:IAM tag 条件隔离、SQS 访问策略、PDB 兜底
7. 生产 Checklist + 运维巡检命令 + 演进路线(多指标/预测性扩容/FinOps)
# KEDA + Karpenter 企业实战总结方案


# KEDA + Karpenter 企业实战总结方案
> 基于 ltp-eks-nonprod 集群实战验证的生产级方案总结
> 覆盖架构决策、落地步骤、运维实践、成本优化、安全加固、避坑经验
---
## 一、方案定位与核心价值
### 解决什么问题
传统 EKS 扩容痛点:
- **Managed Node Group + Cluster Autoscaler**:扩容慢(5-10 分钟),机型固定不灵活,资源碎片浪费
- **纯 HPA**:只扩 Pod 不扩 Node,资源不够时 Pod 卡 Pending,业务受损
- **静态节点**:流量低谷期节点闲置浪费成本,高峰期来不及扩容
本方案的核心价值:
```
流量驱动 → KEDA 按业务指标(RPS)扩 Pod
→ Karpenter 按 Pending Pod 秒级扩 Node(比 Cluster Autoscaler 快 3-5 倍)
→ 流量退去自动缩 Pod + 删 Node,成本随流量弹性
```
### 适用场景
| 场景 | 是否适合 | 说明 |
|------|----------|------|
| 无状态微服务(API/Web)| ✅ 最佳 | 流量波动大,弹性收益明显 |
| 批处理/定时任务 | ✅ 适合 | 突发资源需求 |
| CI/CD Runner | ✅ 适合 | 短时高负载 |
| 有状态服务(DB/缓存)| ❌ 不适合 | 保留 managed node group 或专用节点 |
| GPU/AI 训练 | ⚠️ 需评估 | 机型选择和成本核算要单独做 |
---
## 二、企业级架构决策
### 推荐生产架构:分层节点池
不要把所有工作负载都丢给 Karpenter,生产环境推荐**分层隔离**:
```
┌─────────────────────────────────────────────────────────────┐
│ 流量入口 │
└────────────────────────────┬────────────────────────────────┘
│
┌────────────────────┴────────────────────┐
│ │
▼ ▼
┌───────────────────┐ ┌─────────────────────┐
│ 系统节点层 │ │ 业务节点层(动态) │
│ managed node │ │ Karpenter │
│ group (ON_DEMAND)│ │ │
│ │ │ 方案A: on-demand │
│ - 常驻 1-2 台 │ │ 方案B: spot+SQS │
│ - 跑系统组件 │ │ │
│ kube-proxy │ │ 跑无状态业务 Pod │
│ VPC CNI │ │ http-app / API │
│ CoreDNS │ │ 按 Pending 扩缩 │
│ KEDA operator │ │ │
│ Prometheus │ │ taint 隔离 │
│ 监控 agent │ │ nodeSelector 定向 │
│ │ │ │
│ Terraform 管理 │ │ Pod 走光即删 Node │
│ 稳定不波动 │ │ 成本随流量弹性 │
└───────────────────┘ └─────────────────────┘
```
**决策理由:**
1. 系统组件(kube-proxy、CNI、CoreDNS)必须常驻,不能被 Karpenter 随意删------否则集群基础功能受损
2. 业务 Pod 可以容忍中断和迁移,适合动态扩缩
3. 分层隔离让故障域清晰:系统层稳定,业务层弹性
### 关键架构原则
| 原则 | 实践 | 为什么 |
|------|------|--------|
| 关注点分离 | 系统节点 vs 业务节点分层 | 系统要稳,业务要弹 |
| 故障隔离 | taint + nodeSelector + toleration | 防止 Pod 串节点,引发连锁故障 |
| 成本弹性 | baseline 用 on-demand,弹性用 spot | 兼顾稳定与省钱 |
| 渐进扩缩 | 阶梯式缩容(Percent policy)| 避免流量抖动导致频繁缩扩 |
| 可观测先行 | Prometheus 采集 + KEDA 指标驱动 | 没指标就没弹性 |
---
## 三、两种方案的企业选型
### 决策矩阵
| 你的情况 | 推荐方案 | 理由 |
|----------|----------|------|
| 金融/支付等强一致业务 | 方案A (on-demand) | 不能容忍任何中断 |
| 团队不熟悉 spot 运维 | 方案A | 降低运维复杂度 |
| 成本压力大,业务能容忍秒级中断 | 方案B (spot+SQS) | spot 折扣 60-90% |
| 业务流量波动大(白天高峰夜间低谷)| 方案B | 弹性收益最大化 |
| 混合业务(核心+边缘)| A+B 组合 | 核心用 A,边缘用 B |
### 企业推荐:混合分层方案
生产环境最推荐的不是纯 A 或纯 B,而是**按业务重要性分层**:
```
核心业务(订单/支付/认证)
→ managed node group (ON_DEMAND) 或 Karpenter on-demand
→ 保证不中断,成本可接受
普通业务(内容展示/日志/统计)
→ Karpenter spot + SQS
→ 最省钱,中断由 SQS 优雅处理,PDB 兜底
批处理任务(报表/ETL)
→ Karpenter spot
→ 中断了重跑即可,最省钱
```
**对应多个 NodePool:**
```yaml
# NodePool 1:核心业务(on-demand)
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: critical-on-demand
spec:
template:
spec:
taints:
- key: workload
value: critical
effect: NoSchedule
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: [on-demand]
---
# NodePool 2:普通业务(spot)
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: normal-spot
spec:
template:
spec:
taints:
- key: workload
value: normal
effect: NoSchedule
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: [spot, on-demand] # spot 优先,无库存回退 on-demand
```
业务 Pod 按重要性加对应 toleration + nodeSelector,自动落到对应节点池。
---
## 四、落地步骤总览(企业标准流程)
### 阶段 0:前置评估(1-2 天)
```bash
# 1. 确认集群版本兼容
kubectl version --short
# Karpenter v1.x 需要 K8s 1.28+
# 2. 确认 OIDC
aws eks describe-cluster --name <cluster> --query 'cluster.identity.oidc.issuer' --output text
# 3. 评估现有资源
kubectl get nodes -o wide
kubectl top nodes
# 4. 评估业务流量模式(Prometheus 历史数据)
# 看过去 7-30 天的 RPS 曲线,判断弹性收益
```
**产出**:选型决策(方案A/B/混合)+ 节点池规划(几个 NodePool,各自机型/capacity)
### 阶段 1:基础设施准备(半天)
```bash
# 1. 创建 IAM(Terraform 或手动)
terraform apply # Controller Role + SQS + EventBridge(方案B)
# 2. 确认 Node Role policy 齐全
aws iam list-attached-role-policies --role-name <node-role>
# 3. 确认 aws-auth ConfigMap
kubectl get cm -n kube-system aws-auth -o yaml
```
**产出**:IAM Role + SQS + EventBridge 就绪,权限验证通过
### 阶段 2:组件安装(半天)
```bash
# 1. 安装 KEDA
helm install keda kedacore/keda --namespace keda --create-namespace
# 2. 安装 Karpenter
helm install karpenter karpenter/karpenter --namespace karpenter --create-namespace \
--set clusterName=<cluster> \
--set "serviceAccount.annotations.eks\.amazonaws\.com/role-arn=<role-arn>" \
--set interruptionQueueName=<queue> # 方案B 才需要
# 3. 验证
kubectl get pod -n keda
kubectl get pod -n karpenter
```
**产出**:KEDA + Karpenter 运行正常,IRSA 注入成功
### 阶段 3:节点池与应用部署(半天)
```bash
# 1. 部署 NodePool(带 taint 隔离)
kubectl apply -f karpenter-nodepool-*.yaml
# 2. 部署业务应用(带 nodeSelector + toleration)
kubectl apply -f http-app-deployment.yaml
# 3. 部署 ScaledObject(KEDA 配置)
kubectl apply -f scaled.yaml
```
**产出**:NodePool Ready,Pod Running,ScaledObject Ready
### 阶段 4:验证与压测(1-2 天)
```bash
# 1. 压测验证扩容链路
kubectl run loadgen --rm -it --image=busybox -- /bin/sh
while true; do wget -q -O /dev/null http://<svc>; done
# 2. 观察三层联动
kubectl get deploy <app> -w # KEDA 扩 Pod
kubectl get node -w # Karpenter 扩 Node
kubectl get hpa -w # 指标变化
# 3. 停止压测验证缩容
# 观察 Pod 10→5→2→1,Node 删除
# 4. 方案B 额外验证 spot 中断处理
aws sqs send-message --queue-url <queue> --message-body '<test-event>'
kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter | grep interruption
```
**产出**:扩缩容链路验证通过,延迟符合预期
### 阶段 5:生产上线(渐进式)
1. **先灰度**:选 1-2 个非核心业务接入 KEDA + Karpenter,观察 1 周
2. **逐步迁移**:确认稳定后,扩大到更多业务
3. **监控告警**:配置 Karpenter/KEDA 异常告警
4. **成本复盘**:上线 1 个月后对比成本变化
---
## 五、运维实践与监控
### 必须配置的监控告警
| 指标 | 告警阈值 | 含义 |
|------|----------|------|
| Karpenter pod 状态 | 非 Running | controller 挂了,扩容失效 |
| KEDA operator pod 状态 | 非 Running | KEDA 挂了,弹性失效 |
| Pod Pending 数 | > 0 持续 5 分钟 | 资源不够或 Karpenter 异常 |
| Node Ready 状态 | NotReady | 节点异常 |
| HPA TARGETS | `<unknown>` 持续 5 分钟 | 指标采集断了 |
| ScaledObject Ready | False | KEDA 配置异常 |
| SQS 队列消息积压 | > 100 | 中断事件未消费 |
### 监控命令(日常巡检)
```bash
# 每日巡检脚本
echo "=== KEDA 状态 ==="
kubectl get scaledobject -A
echo "=== Karpenter 状态 ==="
kubectl get nodepool,ec2nodeclass -A
kubectl get pod -n karpenter
echo "=== 节点分布 ==="
echo "Karpenter 节点:$(kubectl get nodes -l nodepool=default --no-headers | wc -l)"
echo "Managed 节点:$(kubectl get nodes -l '!nodepool' --no-headers 2>/dev/null | wc -l)"
echo "=== Pending Pod ==="
kubectl get pod -A --field-selector status.phase=Pending
echo "=== 最近扩缩事件 ==="
kubectl get events -A --field-selector reason=Karpenter --sort-by=.lastTimestamp | tail -20
```
### 成本监控
```bash
# 按节点来源统计成本
kubectl get nodes -l nodepool=default -o custom-columns=NAME:.metadata.name,TYPE:.metadata.labels.karpenter\.sh/capacity-type,INSTANCE:.metadata.labels.node\.kubernetes\.io/instance-type
# 对比 spot vs on-demand 节点数
echo "Spot 节点:$(kubectl get nodes -l 'karpenter.sh/capacity-type=spot' --no-headers | wc -l)"
echo "On-demand 节点:$(kubectl get nodes -l 'karpenter.sh/capacity-type=on-demand' --no-headers | wc -l)"
```
配合 AWS Cost Explorer 按 tag `karpenter.sh/discovery` 过滤,看 Karpenter 节点的实际花费。
---
## 六、成本优化实践
### 实测成本对比(以 r6a.2xlarge 为例)
| 方案 | 单价(ap-northeast-1)| 10 台/月 | 节省 |
|------|----------------------|----------|------|
| on-demand | ~$0.544/h | ~$3,916 | baseline |
| spot | ~$0.164/h | ~$1,181 | **节省 70%** |
**10 台节点用 spot 每月省约 $2,735**,流量低谷期节点自动删除,实际节省更多。
### 成本优化策略
**1. 机型多样化**
spot 拿到实例的概率取决于机型池大小。配置 6+ 种机型,某机型无库存时自动换另一个:
```yaml
requirements:
- key: node.kubernetes.io/instance-type
operator: In
values:
- r6a.2xlarge, r5a.2xlarge, r6a.xlarge, r5a.xlarge, r6a.large, r5a.large
- m6i.xlarge, m5.xlarge # 跨家族,提高命中率
```
**2. spot 优先 + on-demand 兜底**
```yaml
- key: karpenter.sh/capacity-type
operator: In
values: [spot, on-demand] # spot 没货时自动回退,保证可用性
```
**3. 合理设 limits**
```yaml
limits:
cpu: 100 # 防止失控扩容,控制成本上限
memory: 800Gi
```
**4. 快速缩容**
```yaml
disruption:
consolidateAfter: 60s # 空节点 60s 就删,不浪费
consolidationPolicy: WhenEmptyOrUnderutilized # 低利用率也合并
```
**5. Pod requests 精准设置**
Karpenter 按 Pod requests 选机型,requests 设得过大浪费,过小节点装不下。建议按实际用量 1.2-1.5 倍设。
---
## 七、安全加固
### 1. IRSA 最小权限
Controller Role 的 `ec2:TerminateInstances` 必须带 tag 条件:
```json
{
"Action": ["ec2:TerminateInstances"],
"Resource": ["*"],
"Condition": {
"StringEquals": {
"ec2:ResourceTag/karpenter.sh/discovery": "ltp-eks-nonprod"
}
}
}
```
**只删带本集群 tag 的 EC2,碰不到其他集群或 managed node group 的节点。**
### 2. SQS 队列访问策略限制
队列访问策略必须限制 SourceArn,只允许自己的 EventBridge 规则写入:
```json
"Condition": {
"ArnEquals": {
"aws:SourceArn": [
"arn:aws:events:<region>:<account>:rule/<cluster>-karpenter-spot"
]
}
}
```
### 3. Node Role 权限收敛
定期检查 `EKSInformalNodeRole`,移除不再需要的 policy。生产环境 node role 不应挂 `AmazonEC2FullAccess`(你的环境有,建议评估是否收敛为最小权限)。
### 4. PodDisruptionBudget
给关键业务加 PDB,防止 Karpenter drain 时把所有副本同时驱逐:
```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: http-app-pdb
spec:
minAvailable: 1 # 至少保持 1 个可用
selector:
matchLabels:
app: http-app
```
### 5. 避免敏感信息泄露
- 不要在 ScaledObject query 里硬编码敏感信息
- Prometheus serverAddress 用集群内 Service,不暴露公网
- Controller Role 不给 `ec2:Describe*` 以外的敏感 EC2 权限
---
## 八、避坑经验(实战总结)
### 坑1:指标链路不通,KEDA 不扩容
**现象**:压测了但副本数不涨,HPA TARGETS 显示 `<unknown>` 或 0。
**根因**:应用没暴露 `/metrics` 或 ServiceMonitor 没被 Prometheus 加载。
**解决**:
- 确认应用有 `/metrics` 端点(http-echo 这种黑盒应用不行,要加 exporter 或换镜像)
- 确认 ServiceMonitor 的 namespaceSelector 与应用 namespace 匹配
- 确认 Prometheus 的 serviceMonitorSelector 为空或匹配 label
- 在 Prometheus UI 先执行 query 确认有数据,再配 ScaledObject
**经验**:先验证指标存在,再配 KEDA。链路从下游往上游排查:HPA → KEDA → Prometheus → ServiceMonitor → 应用。
### 坑2:query 没限定 label,扩容逻辑混乱
**现象**:query 用 `sum(rate(http_requests_total[1m]))` 全集群聚合,压测 A 服务却扩容了 B 服务。
**根因**:query 没用 label 过滤到具体服务。
**解决**:query 必须带 namespace/service label:
```promql
sum(rate(nginx_http_requests_total{namespace="keda",service="http-app"}[1m]))
```
**经验**:每个 ScaledObject 的 query 必须唯一定位到目标服务。
### 坑3:Pod 扩容了但一直 Pending
**现象**:HPA 把 replicas 调上去了,但新 Pod 卡 Pending。
**根因**:
- 现有 node 资源不够(Karpenter 应该扩容但没扩)
- nodeSelector/taint 配错,Pod 调度不上
- Karpenter controller 异常或权限不足
**解决**:
- `kubectl describe pod <pending>` 看 Events 的 FailedScheduling 原因
- `kubectl get events -A --field-selector reason=Karpenter -w` 看 Karpenter 是否在扩
- `kubectl logs -n karpenter ...` 看 controller 有无权限错误
- 检查 NodePool limits 是否已满
**经验**:Pending Pod 是定位 Node 层问题的关键信号,优先看 describe 和 Karpenter 事件。
### 坑4:缩容太慢,以为坏了
**现象**:停止压测后,副本数和节点迟迟不降。
**根因**:
- `rate(...[1m])` 窗口需要 1 分钟归零
- HPA 默认 stabilizationWindowSeconds=300(5 分钟)
- Karpenter consolidateAfter 默认较慢
**解决**:
- 调 `stabilizationWindowSeconds: 60`(生产谨慎,测试环境可调小)
- 调 `consolidateAfter: 60s`
- 明白缩容天然比扩容慢,是保护机制
**经验**:缩容慢是设计如此(防抖动),不是 bug。生产别调太激进。
### 坑5:Karpenter 和 managed node group 冲突?
**担心**:Karpenter 会不会删掉 Terraform 管的节点?
**事实**:不会。Karpenter 的 IAM 策略限制了 `ec2:TerminateInstances` 只删带 `karpenter.sh/discovery` tag 的实例,managed node group 的节点不带这个 tag,Karpenter 看不到也删不了。Terraform 状态不漂移。
**经验**:靠 IAM tag 条件隔离,比靠人为约定可靠。
### 坑6:spot 实例频繁中断导致业务抖动
**现象**:spot 节点被回收,Pod 重启,业务偶尔报错。
**根因**:SQS+EventBridge 没配好,或没有 PDB 兜底。
**解决**:
- 确认 SQS+EventBridge 链路通(手动发测试消息验证)
- 确认 Karpenter helm 传了 interruptionQueueName
- 加 PDB 保证最少可用副本
- 核心业务不用 spot,改 on-demand 或 managed node group
**经验**:spot 适合无状态/能容忍中断的业务,核心业务别省钱。
### 坑7:扩容延迟比预期长
**现象**:从流量涨到 Pod Running 要 5+ 分钟。
**根因**:
- Prometheus scrape 间隔太长(默认 15s 可接受,但有人改成 60s)
- rate 窗口太大(1m 合理,5m 就慢)
- KEDA poll 间隔(默认 30s)
- EC2 冷启动(2-3 分钟,无法消除)
**解决**:
- scrape interval 设 15-30s
- rate 窗口用 1m
- 接受 EC2 冷启动的物理延迟,核心业务用 minReplicaCount 保持 baseline
**经验**:突发流量场景,靠 minReplicaCount 预热 baseline,别完全依赖冷启动。
---
## 九、生产 Checklist
### 上线前检查
- [ ] OIDC Provider 已启用并验证
- [ ] Controller IAM Role 创建,信任策略指向正确 SA
- [ ] Controller 权限策略含 EC2 + SQS(方案B)权限
- [ ] Node Role(EKSInformalNodeRole)policy 齐全
- [ ] aws-auth ConfigMap 包含 Node Role 映射
- [ ] SQS + EventBridge 创建并验证(方案B)
- [ ] SQS 队列访问策略允许 EventBridge 写入
- [ ] KEDA + Karpenter pod Running
- [ ] Controller IRSA 注入成功(env 有 AWS_ROLE_ARN)
- [ ] NodePool + EC2NodeClass Ready=True
- [ ] 业务应用有 /metrics 端点且 Prometheus 能采到
- [ ] ScaledObject Ready=True,HPA 创建成功
- [ ] 应用带 nodeSelector + toleration 隔离
- [ ] PodDisruptionBudget 配置(关键业务)
- [ ] 压测验证扩容链路通
- [ ] 缩容验证节点能删除
- [ ] 监控告警配置
- [ ] 成本上限(limits.cpu)设置合理
### 运维日常
- [ ] 每日巡检 KEDA/Karpenter pod 状态
- [ ] 检查 Pending Pod 是否有积压
- [ ] 检查 NodePool limits 使用率
- [ ] 检查 spot 节点中断频率(方案B)
- [ ] 每月成本复盘
- [ ] 定期检查 IAM 权限是否需要收敛
---
## 十、方案演进路线
### 当前已实现(本方案)
```
KEDA(Pod 层弹性)+ Karpenter(Node 层弹性)
基于 Prometheus 指标驱动
```
### 未来可演进方向
**1. 多指标驱动**
- 加 CPU/Memory 指标触发器(不仅 RPS)
- 加队列长度触发器(Kafka/CronJob 场景)
- 加自定义指标(业务 QPS、响应时间)
**2. Karpenter consolidation 升级**
- 启用 `WhenUnderutilized` 自动合并低利用率节点
- 配合 disruption budgets 控制干扰速率
**3. 预测性扩容**
- 接入历史流量预测(KEDA Predictive Scaling 或外部工具)
- 流量高峰前提前扩容,消除冷启动延迟
**4. 多集群弹性**
- Karpenter 跨集群调度(Karmada/Kubefed)
- 全局资源池优化
**5. FinOps 自动化**
- 自动成本归因(按 namespace/team 标签)
- 自动优化建议(机型/right-size)
- 预算告警与自动限流
---
## 十一、核心经验总结
### 一句话方案
**KEDA 管 Pod 弹性,Karpenter 管 Node 弹性,Prometheus 做指标驱动,taint+nodeSelector 做隔离,IAM tag 条件做安全边界。**
### 五条铁律
1. **指标先行**:没指标就没弹性。先确保 Prometheus 能采到业务指标,再配 KEDA。
2. **分层隔离**:系统组件留 managed node group,业务 Pod 走 Karpenter,taint 隔离。
3. **权限最小化**:Controller Role 用 tag 条件限制终止权限,碰不到其他节点。
4. **缩容保守**:缩容比扩容慢是设计,别为快调激进参数,防流量抖动。
5. **spot 审慎**:核心业务不用 spot,边缘业务用 spot 配 SQS+PDB 兜底。
### 三条避坑
1. **query 必须带 label**:否则全集群聚合,扩容逻辑错乱。
2. **Pending Pod 是信号**:它是 Node 层问题的报警器,优先排查。
3. **SQS 不能被替代**:Karpenter 中断处理只认 SQS,Redis/RDS 都接不上。
### 成本收益
- spot vs on-demand 节省 60-90%
- 动态扩缩让低谷期节点归零,baseline 仅系统组件
- 10 台 r6a.2xlarge 用 spot 每月省约 $2,735
- 配合 consolidateAfter 60s,空节点快速回收
---
## 十二、文档与文件索引
### 阅读顺序
```
QUICK-REFERENCE.md ← 总参考(架构图+核心概念,先读)
ENTERPRISE-SUMMARY.md ← 企业总结(本文件,实战经验)
KARPENTER-ONDEMAND-AND-SPOT-GUIDE.md ← 主文档(部署+实战记录)
MANUAL-PERMISSION-CHECKLIST.md ← 手动权限配置
PERMISSION-TROUBLESHOOTING.md ← 权限排查
```
### 配置文件
```
方案A (on-demand): karpenter-iam-simple.tf + karpenter-nodepool-simple.yaml
方案B (spot+SQS): karpenter-iam-full.tf + karpenter-nodepool-spot.yaml
共用: http-app-deployment.yaml + scaled.yaml
自动化: deploy-karpenter-keda.sh
```
### 关键命令速查
```bash
# 部署
terraform apply -var="cluster_name=ltp-eks-nonprod"
helm install karpenter karpenter/karpenter ...
kubectl apply -f karpenter-nodepool-*.yaml
kubectl apply -f http-app-deployment.yaml
kubectl apply -f scaled.yaml
# 验证
kubectl get scaledobject -A
kubectl get nodepool,ec2nodeclass
kubectl get pod -n karpenter
# 压测
kubectl run loadgen --rm -it --image=busybox -- /bin/sh
while true; do wget -q -O /dev/null http://<svc>; done
# 观察
kubectl get deploy <app> -w
kubectl get node -w
kubectl get events -A --field-selector reason=Karpenter -w
# 排查
kubectl describe pod <pending>
kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter --tail=50
```
---
## 附录:本方案在 ltp-eks-nonprod 的实际配置
| 项 | 值 |
|----|-----|
| 集群 | ltp-eks-nonprod |
| 区域 | ap-northeast-1 |
| Account | 2923xxxxxxxxx |
| K8s 版本 | 1.34 |
| OIDC ID | 882CDxxxxxxxxx |
| Node Role | EKSInformalNodeRole |
| managed node group | r6a.2xlarge × 1 (desired=1) |
| Karpenter 机型 | r6a/r5a 系列 |
| Subnet | subnet-07fb6f96bcb2ae748 等 3 个 |
| SG | sg-00481e6a7dcc6c115 等 3 个 |
| Prometheus | kube-prom-stack (monitoring ns) |
| http-app namespace | keda |
| KEDA threshold | 3 (每 3 RPS 扩 1 Pod) |
| maxReplicaCount | 10 |
| NodePool limits.cpu | 100 |
| consolidateAfter | 60s |
View Code