如何在CDK中使用S3动态JSON构建CloudWatch仪表板?
解决CDK中基于S3动态JSON构建CloudWatch仪表板的$TOKEN问题
问题核心是CDK的声明式特性:自定义资源返回的是运行时令牌($TOKEN),而CloudWatch仪表板的定义需要在CDK合成(synth)阶段确定静态结构,直接用自定义资源的输出无法解析为具体值。下面提供两种可行方案:
方案一:合成阶段读取S3文件(静态生成仪表板)
在CDK执行synth时直接读取S3中的JSON文件,解析后生成仪表板的静态定义。这种方式能避免$TOKEN问题,因为所有值在构建时就已确定。
代码示例(TypeScript)
import * as cdk from 'aws-cdk-lib'; import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; export class AppDashboardStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // 初始化S3客户端(确保CDK运行环境有S3读取权限) const s3Client = new S3Client({ region: props?.env?.region }); const getObjectCmd = new GetObjectCommand({ Bucket: 'your-target-bucket', Key: 'path/to/app-pages.json', }); // 同步读取并解析JSON(synth阶段需同步执行) const objectData = await s3Client.send(getObjectCmd); const jsonStr = await objectData.Body?.transformToString(); const appPages = JSON.parse(jsonStr || '{"applications": []}'); // 创建CloudWatch仪表板 const dashboard = new cloudwatch.Dashboard(this, 'AppPagesDashboard', { dashboardName: 'App-Pages-Metrics', }); // 遍历应用和页面生成指标组件 for (const app of appPages.applications) { dashboard.addWidgets(new cloudwatch.TextWidget({ markdown: `## ${app.name}`, width: 24, })); for (const page of app.pages) { dashboard.addWidgets(new cloudwatch.GraphWidget({ title: `${page.name} - Average Latency`, left: [ new cloudwatch.Metric({ namespace: 'Your-App-Namespace', metricName: 'Latency', dimensionsMap: { App: app.name, Page: page.name }, statistic: 'Average', }), ], width: 12, })); } } } }
注意事项
- 确保CDK运行环境(本地或CI/CD管道)有目标S3桶的
s3:GetObject权限; - JSON文件更新后,必须重新执行
cdk synth && cdk deploy才能更新仪表板; - 若文件过大,会增加synth阶段的耗时,需权衡文件大小与构建效率。
方案二:自定义资源+Lambda动态生成仪表板(运行时创建)
通过Lambda函数在堆栈部署时读取S3 JSON,调用CloudWatch API动态创建/更新仪表板。这种方式绕开了CDK合成阶段的静态要求,适合JSON频繁更新的场景。
步骤1:编写Lambda处理函数
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; import { CloudWatchClient, PutDashboardCommand } from '@aws-sdk/client-cloudwatch'; import axios from 'axios'; const s3Client = new S3Client({ region: process.env.AWS_REGION }); const cwClient = new CloudWatchClient({ region: process.env.AWS_REGION }); export const handler = async (event: any) => { const sendResponse = async (status: string, physicalId?: string) => { await axios.put(event.ResponseURL, { Status: status, PhysicalResourceId: physicalId || `dashboard-${Date.now()}`, StackId: event.StackId, RequestId: event.RequestId, LogicalResourceId: event.LogicalResourceId, }); }; try { if (event.RequestType === 'Create' || event.RequestType === 'Update') { // 读取S3中的JSON文件 const objectData = await s3Client.send(new GetObjectCommand({ Bucket: process.env.S3_BUCKET!, Key: process.env.S3_KEY!, })); const jsonStr = await objectData.Body?.transformToString(); const appPages = JSON.parse(jsonStr || '{"applications": []}'); // 构建CloudWatch仪表板配置 const widgets = []; for (const app of appPages.applications) { widgets.push({ type: 'text', width: 24, properties: { markdown: `## ${app.name}` } }); for (const page of app.pages) { widgets.push({ type: 'metric', width: 12, properties: { title: `${page.name} Latency`, metrics: [['Your-App-Namespace', 'Latency', 'App', app.name, 'Page', page.name, { stat: 'Average' }]], view: 'timeSeries' } }); } } // 调用CloudWatch API创建/更新仪表板 await cwClient.send(new PutDashboardCommand({ DashboardName: process.env.DASHBOARD_NAME!, DashboardBody: JSON.stringify({ widgets }) })); } await sendResponse('SUCCESS'); } catch (err) { console.error('Error generating dashboard:', err); await sendResponse('FAILED'); } };
步骤2:CDK中配置自定义资源
import * as cdk from 'aws-cdk-lib'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as iam from 'aws-cdk-lib/aws-iam'; import { CustomResource, Provider } from 'aws-cdk-lib/custom-resources'; export class DynamicDashboardStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); const config = { dashboardName: 'Dynamic-App-Pages-Dashboard', s3Bucket: 'your-target-bucket', s3Key: 'path/to/app-pages.json' }; // 创建Lambda函数 const dashboardLambda = new lambda.Function(this, 'DashboardGenerator', { runtime: lambda.Runtime.NODEJS_18_X, code: lambda.Code.fromAsset('path/to/lambda/code'), handler: 'index.handler', environment: { S3_BUCKET: config.s3Bucket, S3_KEY: config.s3Key, DASHBOARD_NAME: config.dashboardName } }); // 授予Lambda必要权限 dashboardLambda.addToRolePolicy(new iam.PolicyStatement({ actions: ['s3:GetObject'], resources: [`arn:aws:s3:::${config.s3Bucket}/${config.s3Key}`] })); dashboardLambda.addToRolePolicy(new iam.PolicyStatement({ actions: ['cloudwatch:PutDashboard'], resources: [`arn:aws:cloudwatch::${this.account}:dashboard/${config.dashboardName}`] })); // 创建自定义资源提供商 const provider = new Provider(this, 'DashboardProvider', { onEventHandler: dashboardLambda }); // 触发自定义资源执行 new CustomResource(this, 'DynamicDashboardResource', { serviceToken: provider.serviceToken }); } }
注意事项
- 仪表板定义不会出现在CDK的synth输出中,因为是运行时动态生成;
- 若需JSON更新时自动同步仪表板,可给Lambda添加S3对象更新事件触发器;
- 自定义资源支持Create/Update/Delete事件,可在Lambda中处理删除逻辑(比如删除仪表板)。
方案选择建议
- 方案一:适合JSON更新频率低、需版本控制仪表板定义的场景;
- 方案二:适合JSON频繁更新、CDK运行环境无法访问S3的场景。
内容的提问来源于stack exchange,提问作者mohanakrishnavh
相关产品推荐
相关产品推荐

