EKS 的 audit log 默认只写在各自账号的 CloudWatch Logs 里。账号和集群一多,就没有任何一个地方能一次看到所有集群发生了什么。这篇讲怎么把它们集中存到一个账号,经 Vector 结构化后写进 MSK,让下游团队从 MSK 订阅,做告警和分析;中间的传输在 Firehose 和 KDS 之间怎么选,也在文中对比。

问题

  • 每个集群一个 log group(/aws/eks/<cluster>/cluster),audit 日志在 kube-apiserver-audit 开头的 stream 里,分散在各个账号和 Region,没有统一入口
  • 要回答“上周谁动了这个 namespace”,只能逐个账号开 Logs Insights,再手工拼时间线
  • 安全问题发现不了。有人在 Prod 里 kubectl exec 进容器、改 ConfigMap、起特权容器,日志是有的,但分散在各处,没有人在看
  • 下游团队想消费这些日志(告警、分析、排障),要么每个账号单独对接,要么干脆没法做

所以目标很明确:日志集中存一份,所有账号、所有集群放在同一个地方,下游团队按需各自消费,互不影响。

设计约束

很多人的第一反应是“每个账号装个 agent,全收上来”。账号多的时候这样走不通:

  • 每个账号一套 agent,要管同样数量的版本、配置和升级。更麻烦的是 agent 自己会变成新的故障源:某个账号的 agent 挂了、OOM、版本漂移,我们不一定知道,而审计完整性恰恰靠它保证。所以业务账号里不能跑需要维护的进程,改成由 AWS 托管服务主动推
  • 中心账号去拉,就要打通每个账号的 VPC,还有路由表、安全组和 CIDR 冲突,每接一个新账号都要重做一遍。所以跨账号只用 IAM 授权,数据走 AWS 服务内部通道
  • EKS 是托管的,拿不到 master 节点。CloudWatch Logs 是 audit log 唯一的出口,audit policy 也由 AWS 固定管理,不能改。这一点决定了第一跳只能是 CloudWatch Logs

结果是业务账号零部署、零网络改动,接一个新账号只需要配一条 Subscription Filter。

架构

整体架构

  • 业务账号:EKS 开启 audit 日志,日志写进 CloudWatch Logs,再由 Subscription Filter 推给中心账号
  • 中心账号:CloudWatch Logs Destination 接收,再交给 Firehose(方案一)或 KDS(方案二)。我们选了 Firehose
  • 跨账号授权靠两处 IAM 配置:Destination 上的 Destination Policy 列出允许 PutSubscriptionFilter 的账号;Destination 绑定的 role 信任 logs 服务,并用 aws:SourceArn 限制成这批账号
  • Destination 必须和订阅它的 log group 在同一个 Region,所以每个 Region 各建一个。后面的 Firehose 或 S3 不需要每个 Region 都建,可以集中在一个 Region,由它完成跨 Region 汇聚

中心存储和 Vector

端到端架构

完整的链路是 S3 → SQS → Vector → MSK → 下游:

  • Firehose 每攒够 5MB 或 60 秒写一个 GZIP 对象到中央 S3,所有账号、所有 Region 的日志都在同一个 bucket 里。S3 是终态存储,原始数据一直在这里
  • S3 有新对象时发一条 SQS 通知。队列里装的不是日志,是 S3 Event Notification(一条 1–2KB,只说明哪个 key 有新对象),Vector 再去 GetObject 拉文件。用 SQS 是因为它自带重试和削峰,多个 Vector 副本可以并行消费同一个队列
  • Vector 部署在 EKS 里,无状态,2 个副本。它做 ETL:解压 gzip、解析 JSON,把 CloudWatch 批量的 logEvents 展开成单条 audit event,补充元数据,再写进 MSK
  • Vector 只增补,不改写。新字段放在单独的命名空间下,audit 原文逐字节保留,不删字段、不改值

补充的元数据分三组:

  • 集群上下文:账号、集群、Region、环境。批次里本身就带着账号 ID、log group 和 log stream,所以来源是准确的
  • 运行时安全:是否 run_as_root、是否 privileged,从 Pod spec 判断
  • 操作元数据:操作人、verb、读写类型,以及 operation_type(manual 或 automated)

operation_type 是最关键的字段,“手动变更告警”能不能成立全靠它。系统 service account 和各类 controller 算 automated,人通过堡垒或代理访问的算 manual。

操作人字段不在同一个位置:IAM 直连时看 user.extra.sessionName;通过代理访问时,user 里是代理自己的 role,真实操作人在 impersonatedUser。只取 user.username 的话,所有经代理的操作都会归到同一个账号,“这个 delete 是谁做的”就永远查不出来。

MSK 是这个方案的边界。再往后每个团队从 MSK 独立订阅,各自管理 offset,互不影响;以后接新的消费方,只是加一个 consumer group,不动上游任何东西。用 MSK 而不是自建 Kafka,是因为 broker 打补丁、跨 AZ 切换和存储扩容都交给 AWS,审计链路不适合再挂一个自己运维的集群。

下游能做的事情就很直接:

  • 安全团队:检测 Prod 里的手动 exec、特权容器、secrets 读取和 RBAC 变更,命中就告警
  • DevOps 团队:在 OpenSearch 里查某个资源是谁、什么时候改的,服务出问题时先排除“是不是有人改过”
  • 跨账号的统计和分析:原来要写正则的查询变成等值过滤,比如“过去一周所有 Prod 的手动 delete”就是 env、verb、operation_type 三个条件

在 source 侧过滤

下游每一跳都按量收费、按量扩容,噪声进了存储也会淹没真正有用的事件,所以先在 source 侧去掉。Subscription Filter 里只保留 ResponseComplete 阶段,再排除健康检查和指标类请求(healthz、readyz、livez、metrics、version、swagger)。

audit 事件有 RequestReceived、ResponseStarted、ResponseComplete、Panic 四个 stage,同一个请求会产生多条。只有 ResponseComplete 同时带齐了结果、授权判定和完整的 request/response 对象,能单独回答“谁做了什么、成没成”。

以每月 10 TB 的原始量为例(示例数字):只留 ResponseComplete 约 5 TB,剔除健康检查和系统端点约 2.8 TB,再剔除高频的自动化 list/watch(kube-proxy、各类 controller)约 1 TB。

注意:Filter 在 CloudWatch 之后生效,省的是下游的传输和存储,不省 CloudWatch 的摄取费。另外 Panic 也被过滤掉了,它是独立的监控信号,需要的话要另外接入。

方案对比:Firehose 还是 KDS

两个方案前半段完全一样(同样的 Filter、同样的跨账号 Destination),区别只在中心账号之后。

  • 方案一:Destination → Firehose → S3,S3 有新对象时发 SQS 通知,下游从通知里取对象
  • 方案二:Destination → KDS,下游直接流式消费
维度 方案一 Firehose → S3 方案二 KDS
端到端延迟 约 2–10 分钟 约 1–5 分钟
链路长度 5 跳(Destination、Firehose、S3、SQS、Vector) 3 跳(Destination、KDS、Vector)
存储 落盘即存储,S3 是终态 不落盘,要存储得另写一路 S3
重放 不支持,要重放得自己写脚本重扫 S3 可以从指定时间点重新消费
下游停机后的补数窗口 不设上限(数据在 S3,SQS 通知保留 14 天,超期可按前缀重扫) 等于 KDS 保留期,默认 24 小时,延长要单独付费
多消费者 Firehose 只有一个目的地,多团队从 Vector 之后的 MSK 订阅 KDS 本身就能多个消费者并行读
运维 全托管 On-Demand 零运维;Provisioned 要自己管 shard
  • 延迟:两边共有的一段是 EKS 写进 CloudWatch Logs,官方只说 “within a few minutes”,best-effort、没有 SLA,大约 1–5 分钟,我们控制不了,这是主要瓶颈。方案一在此之上多了 Firehose 的 buffer(60–300 秒,可配)和 SQS 拉取(0–15 秒)。这些是按文档估算,不是实测
  • 链路长度:反直觉的是方案二更简单,少两个组件就少两个故障点
  • 补数窗口:这是两个方案真正的差异。方案二消费方停机超过保留期,数据就永久丢了,没有补救层;方案一的数据落在 S3 就是终态,消费方停几天甚至几周都能补
  • 重放:我认为这是方案二最有价值的地方,下游处理逻辑改了或出 bug 时能救命。但窗口有限,所以它和方案一“数据永久在盘上”是互相对冲的
  • 成本:差别很小。摄取费占总成本的绝大部分,而且和选哪个方案无关,所以成本不是决策依据

我们选了 Firehose。对“多团队各自消费一份集中存储”这个目标来说,落盘即存储、消费方能独立扩容、下游出故障后不设补数时限,比 KDS 少几分钟延迟更重要。如果实测延迟顶不住,或者后来需要重放,换成方案二只是换中间那一跳:前面的 Filter 和 Destination 不用动,Vector 后面的 transform 也不用动,只改 source(S3 + SQS 换成 KDS)。

新账号接入

整条链路的运维坑主要在这里:新账号要同时做对三件事,缺任何一个,日志都不会进来,也没有报错和告警:

  • 账号 ID 进了 Destination Policy 的白名单(以及 role 的 aws:SourceArn)
  • 目标 Region 已经有 Destination
  • 业务账号里配好了 Subscription Filter

白名单和这三件事都应该交给 Terraform 管。这样接一个新账号只是:把账号 ID 加进白名单,中心侧 apply,业务侧 apply 下发 Filter。

剩余问题

  • EKS 到 CloudWatch Logs 的延迟没有 SLA,这一段我们控不了
  • 白名单漏掉账号时没有任何告警,目前只能靠 IaC 保证不漏
  • 如果 Firehose 的输出格式变了,下游的解压和解析会静默失败,需要给解析错误配告警
  • CloudWatch Logs 的摄取费占大头,而且 EKS 托管版的 audit policy 改不了,没法在源头减量。能做的只有只开 audit 和 authenticator 两种控制平面日志,关掉 api、controllerManager 和 scheduler