AWS CloudWatch 是公有云监控的标配,但很多企业的可观测性底座是部署在 VPC 内网的 OpenTelemetry (OTel) Collector。CloudWatch 默认没有内网推送通道,直接让内网 Collector 拉取公有云 API 既不安全又受带宽与 API 限流制约。本文拆解一种实战方案:用 AWS Lambda 做格式转换与内网穿透,把 CloudWatch 指标以 OTLP 格式直接灌进 VPC 内的 Collector。
为什么需要 Lambda 做桥接
当 OpenTelemetry Collector 部署在私有子网时,它面临一个尴尬的网络隔离:它能访问内网服务,却无法高效、安全地与 AWS 公有云 API 交互。如果让 Collector 主动通过 NAT 网关拉取 CloudWatch 指标,不仅会产生公网流量费,还会因为 GetMetricData API 的调用限制导致指标延迟。
把 Lambda 放在中间做“桥接”,能从根本上扭转数据流向: - Lambda 主动拉取或接收流:利用 CloudWatch Metric Streams 或 EventBridge 定时触发,Lambda 在 AWS 内部网络拿到原始指标。 - Lambda 转换格式:将 CloudWatch 的 JSON 结构转为 OpenTelemetry 标准的 OTLP JSON 或 Protobuf。 - Lambda 推送内网:Lambda 被挂载到 VPC 内,直接通过私有 IP 把数据 POST 到 Collector,全程不走公网。
数据流转路径设计
这套架构的核心在于触发机制与网络路由的配合。典型的流转路径如下:
- 指标采集:CloudWatch Metric Streams 将指标实时推送到 Kinesis Data Firehose,或者由 EventBridge 规则按分钟触发 Lambda 拉取指定指标。
- 格式转换:Lambda 函数解析 CloudWatch 的命名空间、维度与指标值,映射为 OTLP 的
scopeMetrics和gauge/sum数据点。 - 内网投递:Lambda 通过 VPC 内的 ENI,向 Collector 的
4318(HTTP)或4317(gRPC)端口发送数据。
下面进入实操环节,看如何用代码把这条路径落地。
Lambda 转换层实战
Lambda 的核心任务是把 CloudWatch 的指标结构“翻译”成 OTLP 格式。CloudWatch Metric Streams 推送过来的 JSON 通常包含 namespace、metric_name、dimensions 和 values,我们需要把它们平铺为 OTLP 的属性键值对。
以下是一个可以直接部署的 Python Lambda 函数示例,它接收 CloudWatch 指标事件,转换为 OTLP JSON 并推送到内网 Collector:
import json
import urllib.request
import os
from datetime import datetime
# 从环境变量读取内网 Collector 地址
OTEL_COLLECTOR_URL = os.environ.get('OTEL_COLLECTOR_URL', 'http://10.0.1.50:4318/v1/metrics')
def lambda_handler(event, context):
# 假设事件来自 CloudWatch Metric Stream (经 Firehose 解码) 或定时拉取
# 这里以简化的 CloudWatch 指标结构为例
cw_metrics = event.get('metrics', [])
if not cw_metrics:
print("未收到指标,跳过处理")
return {"status": "skipped"}
otlp_payload = transform_to_otlp(cw_metrics)
push_to_collector(otlp_payload)
return {"status": "success", "metrics_processed": len(cw_metrics)}
def transform_to_otlp(cw_metrics):
"""将 CloudWatch 指标转为 OpenTelemetry OTLP JSON 格式"""
otlp_metrics = []
for m in cw_metrics:
# 将 CloudWatch 命名空间转为 OTel 指标名,如 AWS/EC2 -> aws.ec2.cpu_utilization
namespace = m['namespace'].replace('/', '.').lower()
metric_name = m['metric_name'].lower()
otel_metric_name = f"{namespace}.{metric_name}"
# 将 CloudWatch Dimensions 转为 OTel Attributes
attributes = []
for dim_key, dim_val in m.get('dimensions', {}).items():
attributes.append({"key": dim_key.lower(), "value": {"stringValue": dim_val}})
# 构造 OTLP Gauge 数据点 (假设 CloudWatch 传来的 value 是单值)
data_points = []
timestamp_ms = m.get('timestamp', datetime.utcnow().isoformat())
# OTLP 时间戳需为纳秒字符串
time_unix_nano = str(int(datetime.fromisoformat(timestamp_ms).timestamp() * 1e9))
data_points.append({
"timeUnixNano": time_unix_nano,
"asDouble": m['value']
})
otlp_metrics.append({
"name": otel_metric_name,
"unit": m.get('unit', '1'),
"gauge": {
"dataPoints": data_points
},
"attributes": attributes
})
# 组装完整的 OTLP ResourceMetrics 结构
return {
"resourceMetrics": [{
"resource": {
"attributes": [
{"key": "cloud.provider", "value": {"stringValue": "aws"}}
]
},
"scopeMetrics": [{
"scope": {"name": "cloudwatch-lambda-bridge"},
"metrics": otlp_metrics
}]
}]
}
def push_to_collector(otlp_payload):
"""通过内网将 OTLP JSON 推送到 VPC 内的 Collector"""
req = urllib.request.Request(
OTEL_COLLECTOR_URL,
data=json.dumps(otlp_payload).encode('utf-8'),
headers={'Content-Type': 'application/json'},
method='POST'
)
try:
with urllib.request.urlopen(req, timeout=5) as resp:
print(f"推送成功,Collector 返回状态码: {resp.status}")
except urllib.error.URLError as e:
print(f"推送失败,请检查 Collector 是否在内网可达: {e}")
raise e
部署与改造提示:
- 如果你的数据源是 CloudWatch Metric Streams,Lambda 前面需要挂一层 Kinesis Firehose,并在 Firehose 中配置数据转换 Lambda 来解压与解码。
- OTEL_COLLECTOR_URL 环境变量必须填写 Collector 的私有 IP(如 http://10.0.1.50:4318/v1/metrics),不要填写公网域名。
网络与权限配置要点
Lambda 要能访问 VPC 内的 Collector,必须将其挂载到对应的 VPC 与私有子网。这带来一个关键副作用:挂载到 VPC 的 Lambda 会失去公网出站权限。如果 Lambda 还需要访问其他 AWS 公有服务(如读取 S3 配置或调用 CloudWatch API),必须为 VPC 配置 NAT 网关或 VPC Endpoints。
以下是使用 AWS SAM 部署该 Lambda 的 YAML 模板片段,展示了如何将 Lambda 放入 VPC 并配置安全组:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
CloudWatchToOtelFunction:
Type: AWS::Serverless::Function
Properties:
Handler: app.lambda_handler
Runtime: python3.9
Timeout: 10
# 关键配置:将 Lambda 挂载到 VPC 内网
VpcConfig:
SubnetIds:
- subnet-0a1b2c3d4e5f # 你的私有子网 ID
SecurityGroupIds:
- sg-0123456789abcdef # 允许出站访问 Collector 的安全组
Environment:
Variables:
OTEL_COLLECTOR_URL: http://10.0.1.50:4318/v1/metrics
Policies:
# 如果 Lambda 需要主动拉取 CloudWatch,需赋予相关权限
- Statement:
Effect: Allow
Action:
- cloudwatch:GetMetricData
Resource: '*'
# Lambda 挂载 VPC 需要创建 ENI 的权限
- VPCAccessPolicy: {}
安全组规则提醒:Collector 所在实例的安全组,必须放行 Lambda 安全组对 4317(gRPC)或 4318(HTTP)端口的入站访问。
架构权衡与落地建议
用 Lambda 做指标桥接并不是银弹,它在解决网络隔离的同时,也引入了新的复杂度。
主要权衡: - 延迟与成本:如果使用 EventBridge 定时触发 Lambda 拉取指标,拉取频率越高,Lambda 调用费越贵;频率越低,指标时效性越差。如果使用 Metric Streams + Firehose 实时推流,数据延迟可降到秒级,但 Firehose 会产生按数据量计费的成本。 - 网络陷阱:Lambda 一旦挂载 VPC,冷启动时间会因 ENI 分配而显著增加。如果 VPC 内没有配置 NAT 网关,Lambda 将无法访问 AWS 公有 API(如 CloudWatch 与 STS),此时必须为 CloudWatch 配置 VPC Endpoint。
落地检查清单:
1. 确认 Lambda 所在子网有到 Collector 的路由(同 VPC 内直接路由,跨 VPC 需 Peering 或 TGW)。
2. 确认 Collector 的 Security Group 允许 Lambda 访问 OTLP 端口。
3. 确认 Lambda 执行角色包含 ec2:CreateNetworkInterface 等权限(SAM 的 VPCAccessPolicy 已自动包含)。
4. 评估是否需要为 CloudWatch 或 Kinesis 配置 VPC Endpoint,避免 Lambda 因失去公网而无法拉取数据。
5. 在 Collector 端配置 file_exporter 或调试日志,先验证 OTLP 数据结构是否被正确接收与解析,再对接后端 Prometheus 或 Jaeger。
这套 Lambda 桥接方案,本质上是在公有云监控与内网可观测性之间修了一条“专线”。只要网络与权限配置到位,它能让 VPC 内的 Collector 安稳地坐在内网,源源不断地接收标准化的 OTLP 指标流。