Karpenter + Keda 让Kuberntes实现动态扩缩容机制方案指南

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: 5m

    View 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: 10s

    View 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
            ;;
    esac

    View 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

相关推荐
.冰块.1 天前
VMware+Ubuntu24.04 kubeadm 部署 K8s 1.30.2 三节点集群
运维·云原生·容器·kubernetes·k8s·容器编排
我不会起名字3223 天前
K8s Ingress 返回 502 但后端日志正常:4 类上游断连与排查链
kubernetes·k8s·ingress·502·keepalive
Eminem893 天前
18、k8s配置管理中心Secret
k8s
我不会起名字3223 天前
K8s 服务 DNS 解析偶发 5 秒超时:conntrack 与 ndots 的 3 个坑
kubernetes·k8s·dns·conntrack·coredns
KIDULT°4 天前
Kubeadm 部署 Kubernetes v1.30.2 实战:Ubuntu24.04 三节点集群 + 排坑记录
ubuntu·云原生·容器·kubernetes·k8s
Eminem894 天前
15、k8s控制器Statefulset
k8s
Eminem894 天前
17、k8s配置管理中心configmap
k8s
Eminem894 天前
16、k8s控制器Daemnonset
k8s
深蓝电商API5 天前
Docker+K8s部署爬虫:从单机到集群的完整路线
爬虫·docker·k8s