在 Amazon CloudFront 前面挂 WAF 做防护是常规操作,但如果你还想记录所有失败请求的完整信息用于补数重放,事情就没那么简单了。这篇文章记录了我们在方案选型过程中踩过的坑,希望能帮后来人少走弯路。

需求是什么

我们的业务场景是这样的:一个动态站点,源站部署在非 AWS 环境(如友商云平台),前面通过 Amazon CloudFront 做全球加速,并挂载 AWS WAF 做安全防护。

业务方提了一个看似简单的需求:

记录所有失败请求的完整信息,包括全部 request headers 和 request body,用于后续异步补数重放。

这里的"失败"包括两类:

  • 被 WAF 拦截的请求:触发了安全规则(如 XSS、SQL 注入),直接返回 403
  • 源站响应 4xx/5xx 的请求:请求到达了源站,但处理失败(如 500 内部错误、502 网关超时)

成功请求(2xx/3xx)不记录,控制成本和存储量。

为什么需要补数?因为 WAF 规则可能误拦截正常请求,源站也可能因为临时故障返回 500。如果能记录这些失败请求的完整信息,后续就可以通过脚本从 S3 读取日志,异步重放恢复数据。

听起来不复杂对吧?但在实际落地过程中,我们踩了不少 CloudFront 和 Lambda@Edge 的坑。

📌 本文配套了 4 张带 AWS 官方架构图标的高清配图,图片文件在  blog-assets/ 目录下。

坑 1:origin-response 阶段拿不到 request body

最初的想法

最直觉的方案是:只用一个 Lambda@Edge,挂在 origin-response 事件上。当源站返回响应后,检查状态码,如果 >= 400 就把完整请求信息(headers + body)记录下来。

一个 Lambda 搞定,简单优雅。

现实的打击

我们写了代码部署上去,在 origin-response 里直接访问 request.body

// origin-response.js — 尝试直接读取 body
const status = parseInt(response.status);
if (status >= 400) {
    console.log(JSON.stringify({
        bodyAvailable: !!request.body,
        body: request.body ? request.body.data : null
    }));
}

发送一个带 body 的 POST 请求到会返回 500 的端点,满怀期待地去看 CloudWatch 日志:

{
    "bodyAvailable": false,
    "body": null
}

**request.body 是 undefined。**

文档怎么说

翻了 AWS 官方文档 Work with requests and responses,找到了明确说明:

You can opt to have Lambda@Edge expose the body in a request... choose  Include Body when you create a CloudFront trigger for your function that's for a  viewer request or origin request event.

Include Body 选项只适用于 viewer-request 和 origin-request 两个阶段。到了 origin-response 阶段,CloudFront 传给 Lambda 的 request 对象里,method、URI、headers 都有,唯独 body 被丢弃了。

同一个文档里还有一句:

When you're working with the HTTP response, Lambda@Edge  does not expose the body that is returned by the origin server to the origin-response trigger.

教训:CloudFront 各个阶段能访问的数据是不同的。不要想当然地认为某个阶段能拿到所有信息,动手之前先查文档确认 API 能力边界。


坑 2:两套 Header 限制,差点让我们放弃最优方案

想到了 header 传递

既然 origin-response 拿不到 body,一个自然的想法是:在 origin-request 阶段把 body 写入自定义 header(如 X-Original-Body),这样 origin-response 就能从 header 里读取了。

被文档吓到了

但查阅 CloudFront Quotas 时,我们在 "Quotas on headers" 部分看到了这个:

限制项限制值
Custom headers: 单个 header value 最大长度1,783 字符
Custom headers: 所有 header 名称+值总长度10,240 字符

我们客户的 request body 最大可以到 10KB,base64 编码后约 13.3KB,远超 1,783 字符的限制。

当时的结论是:这条路走不通。

虚惊一场

后来仔细研究才发现,文档里的 "Custom headers" 限制指的是 CloudFront 控制台里配置的静态 Origin Custom Headers——就是你在 Distribution 设置里手动添加的那种固定 header。

Lambda@Edge 在代码里动态设置的 header,受的是另一个限制,在 "General quotas" 部分:

Maximum length of a request or an origin response, including headers and query strings, but not including the body content —  20,480 bytes

也就是说,Lambda@Edge 动态注入的 header 受的是 20KB 总请求大小限制(包含所有 headers + query string)。10KB 的 body base64 编码后约 13.3KB,加上其他原始 headers(通常 2-4KB),总共约 15-17KB,在 20KB 限制内。

同一份文档里有两套不同的 header 限制,适用于完全不同的场景:

限制类型适用场景限制值
Custom header value 最大长度CloudFront 控制台配置的静态 Origin Custom Headers1,783 字符
Custom headers 总长度CloudFront 控制台配置的静态 Origin Custom Headers10,240 字符
请求总大小(含 headers + query string)Lambda@Edge 动态设置的 headers、所有请求 headers20,480 bytes

教训:CloudFront 的 Quotas 文档很长,不同章节的限制适用于不同场景。看到一个限制就下结论之前,先确认它是不是针对你的使用方式。


坑 3:Custom Error Pages 方案看起来完美,但丢失了关键信息

一篇让人兴奋的 Blog

我们找到了一篇 AWS 官方 Blog:Enhanced Origin Failover using Amazon CloudFront and AWS Lambda@Edge

它介绍了一种巧妙的方案:利用 CloudFront 的 Custom Error Pages 功能,只在源站返回错误时才触发 Lambda@Edge:

  1. 配置 CloudFront Custom Error Page,当源站返回 4xx/5xx 时重定向到 /error-failover* 路径
  2. 为该路径创建单独的 Cache Behavior,关联 Lambda@Edge(origin-request 触发)
  3. CloudFront 自动注入两个特殊 header:cloudfront-error-uri(原始 URI)和 cloudfront-error-args(原始 query string)

这样 Lambda 只在错误时执行,正常请求完全零开销。对于日均百万级请求的场景,成本几乎可以忽略。

为什么不适合我们

这个方案的设计目的是 failover——把错误请求转发到备用源站(比如 S3 静态页面),不是用来记录完整请求信息的:

  • ❌ 拿不到 request body:特殊 header 只传递了 URI 和 query string
  • ❌ 拿不到原始 request headers:错误重定向后是一个全新的内部请求,原始的 Authorization、Content-Type、自定义业务 header 全部丢失
  • ✅ 成本确实极低

如果你的需求只是在错误时展示一个友好的错误页面或做 failover,这个方案非常好。但如果你需要记录完整的请求信息用于重放,它就不够了。

教训:一个方案在某个场景下是最优解,不代表它适合所有场景。评估方案时,先列清楚你的硬性需求,再逐一验证。


坑 4:Lambda@Edge 比你想的贵,但也没那么贵

计费方式

很多人知道 AWS Lambda 的定价,但 Lambda@Edge 的计费是不同的:

计费项Lambda@Edge普通 Lambda
请求次数$0.60/百万$0.20/百万
计算时长$0.00000625125/128MB-s$0.0000166667/GB-s
免费额度100万次/月 + 400,000 GB-s

请求单价贵 3 倍,而且没有免费额度。

实际成本

以日均 100 万请求为例,每个请求触发 2 个 Lambda@Edge(origin-request + origin-response),月均 6000 万次执行:

项目计算月费用
请求费6000万 × $0.60/百万$36.00
时长费(按 10ms 估算)6000万 × 0.01s × $0.00000625125$3.75
Lambda@Edge 合计~$40/月

加上 WAF 和日志收集的费用:

组件月费用
WAF(Web ACL $5 + 规则 $1 + 请求检查 3000万 × $0.60/百万)~$24
Lambda@Edge~$40
CloudWatch + Firehose + S3~$2
总计~$66/月
以上价格基于 AWS 官方定价页面(2026 年 3 月),实际费用可能因 region 和使用模式有所不同。建议使用  AWS Pricing Calculator 进行精确估算。

教训:Lambda@Edge 没有免费额度,请求单价是普通 Lambda 的 3 倍。但对于大多数业务场景,绝对金额并不高。不要因为"贵"就放弃,先算一下实际数字再做决定。


坑 5:Lambda@Edge 日志不在你以为的地方

这个坑不影响方案选型,但会影响你的调试效率。

Lambda@Edge 的日志不是写入函数所在 region(通常是 us-east-1)的 CloudWatch,而是写入执行节点所在 region 的 CloudWatch。

比如你的 Lambda@Edge 函数部署在 us-east-1,但用户从中国访问,请求被路由到了 us-west-2 的边缘节点,那日志就写在 us-west-2 的 CloudWatch 里。

我们部署完新版本后,在 us-east-1 的 CloudWatch 里怎么也找不到日志,最后在 us-west-2 才找到。

实际影响:

  • 调试时需要在多个 region 查看日志
  • 如果要通过 Subscription Filter 把日志汇聚到 Kinesis Firehose → S3,需要在每个有用户访问的 region 都设置

教训:部署 Lambda@Edge 后找不到日志?别慌,去其他 region 的 CloudWatch 看看。


我们评估过的所有方案

在确定最终方案之前,我们评估了 6 种不同的方案。这里做一个完整的对比,方便你根据自己的场景选择:

方案 A:应用层记录

在源站应用里加中间件,当响应 4xx/5xx 时记录完整请求信息。

  • ✅ 零额外 AWS 成本,对正常请求零开销
  • ❌ 需要改源站代码,如果源站部署在友商云平台(比如在友商云平台),改动和日志同步都不方便
  • 适合:源站在 AWS 上,且有权限修改应用代码的场景

方案 B:CloudFront 实时日志 + Kinesis

CloudFront Real-time Logs 可以异步记录请求详情到 Kinesis Data Streams。

  • ✅ 不需要 Lambda@Edge,异步不影响延迟
  • ❌ 没有 request body,只有 headers、status code、URI 等
  • 适合:不需要 body,只需要请求元数据的场景

方案 C:ALB Access Logs

如果源站前面有 ALB,可以开启 Access Logs。

  • ✅ 零额外成本
  • ❌ 不记录 request body 和完整 headers;源站不在 AWS 则不适用
  • 适合:源站在 AWS 上,只需要基本访问日志的场景

方案 D:Custom Error Pages + Lambda@Edge

利用 CloudFront Custom Error Pages,只在错误时触发 Lambda。

  • ✅ 成本极低(只在错误时执行)
  • ❌ 丢失原始 request body 和 headers
  • 适合:错误时做 failover 或展示友好错误页面的场景

方案 E:origin-request Lambda + 实时日志关联

origin-request 记录 body + requestId 到 CloudWatch,CloudFront 实时日志记录 status code,后续用 requestId 关联。

  • ✅ 只需要一个 Lambda,成本减半
  • ❌ 需要额外的 Kinesis,关联逻辑复杂,总成本并没有明显降低
  • 适合:对架构复杂度不敏感,想减少 Lambda 调用次数的场景

方案 F:双 Lambda@Edge(最终方案)

origin-request 把 body 写入自定义 header,origin-response 在错误时从 header 读取并记录完整日志。

  • ✅ 一条日志包含完整信息(status + headers + body),不需要关联
  • ✅ 不依赖源站做任何改动
  • ✅ 架构简单,易于维护
  • ⚠️ 每个请求触发两次 Lambda@Edge
  • ⚠️ body 通过 header 传递,受 20KB 总请求大小限制

对比总结

方案完整 headers完整 body不改源站月成本 (日100万请求)
A. 应用层记录~$0
B. 实时日志 + Kinesis~$35
C. ALB Access Logs~$0
D. Error Pages + Lambda~$25
E. origin-request + 实时日志~$54
F. 双 Lambda@Edge~$40

最终方案详解

在"源站部署在友商云平台、不方便改源站代码、需要完整 headers + body"这三个硬约束下,双 Lambda@Edge 是唯一同时满足所有需求的方案。

架构图

核心代码

origin-request.js — 把 body 写入自定义 header,传递给下游:

'use strict';
exports.handler = async (event) => {
    const request = event.Records[0].cf.request;

    // 有 body 的请求(POST/PUT/PATCH),把 body 存入自定义 header
    // 因为 origin-response 阶段无法访问 request body
    if (request.body && request.body.data) {
        request.headers['x-original-body'] = [
            { key: 'X-Original-Body', value: request.body.data }
        ];
        request.headers['x-original-body-encoding'] = [
            { key: 'X-Original-Body-Encoding', value: request.body.encoding || 'base64' }
        ];
    }

    return request;
};

origin-response.js — 只在错误时记录完整日志:

'use strict';
exports.handler = async (event) => {
    const cf = event.Records[0].cf;
    const response = cf.response;
    const request = cf.request;
    if (!response) return { status: '502', statusDescription: 'Bad Gateway' };
    const status = parseInt(response.status);

    // 只记录失败请求,成功请求直接放行(零开销)
    if (status >= 400) {
        // 过滤掉内部传递用的 header,只保留原始 headers
        const headers = Object.fromEntries(
            Object.entries(request.headers)
                .filter(([k]) => !k.startsWith('x-original-body'))
                .map(([k, v]) => [k, v[0].value])
        );

        // console.log 写入 CloudWatch Logs(异步,不阻塞响应)
        console.log(JSON.stringify({
            type: 'origin_error',
            requestId: cf.config.requestId,
            timestamp: new Date().toISOString(),
            status,
            method: request.method,
            uri: request.uri,
            querystring: request.querystring,
            clientIp: request.clientIp,
            headers,
            body: request.headers['x-original-body']
                ? request.headers['x-original-body'][0].value : null,
            bodyEncoding: request.headers['x-original-body-encoding']
                ? request.headers['x-original-body-encoding'][0].value : null
        }));
    }

    return response;
};

日志流向

失败请求的日志通过两条路径汇聚到 S3:

  1. WAF 拦截的请求:WAF 自动记录日志到 CloudWatch Logs(包含 headers + body 前 8KB + 触发规则),通过 Subscription Filter → Kinesis Firehose → S3
  2. 源站失败的请求:origin-response Lambda 通过 console.log 写入 CloudWatch Logs(包含完整 headers + body + status code),同样通过 Subscription Filter → Kinesis Firehose → S3

两类日志最终统一存储在 S3,补数脚本从 S3 读取、解析、重放。

已知限制

限制说明影响
Body 大小Lambda@Edge Include Body 最大 1MB,超过会被截断大文件上传的 body 可能不完整
Header 总大小请求总大小(含 headers + query string)最大 20KBbody 超过约 15KB 时可能超限
WAF body 检查WAF 日志只包含 body 的前 8KBWAF 拦截的请求,body 可能不完整
日志分散Lambda@Edge 日志写入执行节点所在 region需要在多个 region 设置日志收集

踩坑速查表

最后,把所有踩过的坑整理成一张表,方便快速查阅:

#踩坑点现象原因解决方案
1origin-response 拿不到 bodyrequest.body 为 undefinedInclude Body 只支持 viewer-request 和 origin-request在 origin-request 阶段通过 header 传递 body
2Custom Header 限制混淆以为 header value 最大只有 1,783 字符那是控制台静态配置的限制,Lambda@Edge 动态设置受 20KB 总请求大小限制区分两套限制,确认适用场景
3Error Pages 方案丢失请求信息拿不到原始 headers 和 body错误重定向是全新的内部请求该方案只适合 failover,不适合记录请求信息
4Lambda@Edge 成本高于预期没有免费额度,请求单价 $0.60/百万Lambda@Edge 定价独立于普通 Lambda先算实际数字,日百万级请求约 $40/月
5找不到 Lambda@Edge 日志us-east-1 的 CloudWatch 里没有日志日志写入执行节点所在 region在多个 region 查看,或设置跨 region 日志收集
6CloudFront Functions 不支持 body想用更便宜的 CF Functions 替代CF Functions 不支持 Include Body只能用 Lambda@Edge

写在最后

CloudFront + Lambda@Edge 是一个强大的组合,但它的各种限制分散在不同的文档页面里,很容易踩坑。希望这篇文章能帮你在做类似方案时少走弯路。

核心建议:

  1. 先查文档确认能力边界,再动手写代码。CloudFront 四个阶段(viewer-request、origin-request、origin-response、viewer-response)能访问的数据是不同的
  2. 注意区分不同的 Quota 限制,同一份文档里的不同章节可能针对不同场景
  3. 先算成本再做决定,Lambda@Edge 看起来贵,但绝对金额可能完全可以接受
  4. 选方案时列清楚硬性需求,然后逐一验证每个候选方案是否满足

如果你也在做类似的失败请求记录方案,欢迎留言交流。


参考资料

  1. AWS Lambda Pricing — Lambda@Edge 定价在页面底部 "Lambda@Edge Pricing" 部分
  2. AWS WAF Pricing — Web ACL、规则、请求检查定价
  3. Work with requests and responses (Lambda@Edge) — Include Body 选项说明、origin-response 的限制
  4. CloudFront Quotas — Lambda@Edge 限制、Custom Header 限制、请求总大小限制
  5. Enhanced Origin Failover using CloudFront and Lambda@Edge — Custom Error Pages + Lambda@Edge 方案
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐