Setting the file. One moment. Template Events · AWS Marketplace Metering · aws/agent-toolkit-for-aws · Skills DocsQuery Patterns
70
Creating Amazon Aurora Db Cluster With Instances
93
Routing Traffic With Route53 And CloudFront
Resilience Program Design
Creating API Gateway Stage
assets/template-events.yaml
YAML·639 lines·27 KB
:
String
12 Default: live
13 AllowedValues: [live, dry-run]
14 Description: >-
15 live (production events stack): the marketplace-events rule matches all
16 aws.agreement-marketplace events. dry-run (non-production sandbox events stack): the rule
17 is SCOPED to the TestAccountAllowlist so it processes ONLY test-buyer events (never real
18 production traffic), and the test event publisher is created. Prod vs non-prod is decided
19 by THIS switch, not by the free-form stage name.
20 TestAccountAllowlist:
21 Type: CommaDelimitedList
22 Default: ''
23 Description: >-
24 For a dry-run (non-prod) events stack ONLY: the seller's TEST buyer AWS account IDs. The
25 marketplace-events rule and the test event publisher are scoped to these accounts so a
26 non-prod events stack never processes production buyers. MUST be non-empty when
27 MeteringMode=dry-run (enforced by deploy.sh).
28 EventSource:
29 Type: String
30 Default: aws.agreement-marketplace
31 Description: >-
32 The EventBridge `source` the marketplace-events rule matches. LIVE stacks use the real
33 `aws.agreement-marketplace`. A dry-run (non-prod) stack MUST use a STAGE-SCOPED source
34 (e.g. `<stageName>.agreement-marketplace`) — NOT the reserved `aws.` prefix (a custom
35 PutEvents cannot reliably deliver an `aws.` source) — which also makes it DISJOINT from the
36 production source, so a same-account non-prod stage's events can never be matched by the
37 production rule. deploy.sh sets this per stage/mode.
38 LogsKmsKeyArn:
39 Type: String
40 Default: ''
41 Description: >-
42 Optional KMS key ARN to encrypt the Subscription Lambda's CloudWatch log group
43 (logs contain buyer AWS account IDs / agreement metadata). Leave empty
44 to use CloudWatch default encryption. The key policy MUST allow the CloudWatch Logs
45 service principal (logs.<region>.amazonaws.com) to use it.
46 LogRetentionInDays:
47 Type: Number
48 Default: 90
49 Description: Retention (days) for the Subscription Lambda log group.
50 AlertsTopicArn:
51 Type: String
52 Default: ''
53 Description: >-
54 Optional SNS topic ARN (in us-east-1, this stack's region) that alarm actions
55 notify. A CloudWatch alarm can only notify an SNS topic in its own region, so this
56 must be a us-east-1 topic. TODO(seller): supply your in-region alerts topic. Alarms
57 are always created; leaving this empty just means no notification action is wired.
58 Neither the deployer role nor any Lambda publishes to it (CloudWatch fires the
59 action); acting on alarms is the seller's responsibility.
60 PermissionsBoundaryName:
61 Type: String
62 Default: awsmp-metering-boundary
63 Description: >-
64 Name of the IAM permissions-boundary policy applied to the SAM-generated
65 Subscription Lambda execution role. MUST match the boundary the deployer role's
66 iam:CreateRole condition requires, and the boundary policy MUST already exist in
67 the account before deployment (see references/iam-credentials.md).
68 CreateDashboard:
69 Type: String
70 Default: 'true'
71 AllowedValues: ['true', 'false']
72 Description: >-
73 Whether to create the events-stack CloudWatch HEALTH dashboard (default true):
74 subscription Lambda errors/throttles/invocations + subscription queue/DLQ depth, so
75 an operator sees events-stack health in one place. Health-only (no business metrics),
76 shared across products (not per-product). Set 'false' to skip; delete/customize as needed.
77
78Conditions:
79 HasLogsKmsKey: !Not [!Equals [!Ref LogsKmsKeyArn, '']]
80 HasAlertsTopic: !Not [!Equals [!Ref AlertsTopicArn, '']]
81 CreateHealthDashboard: !Equals [!Ref CreateDashboard, 'true']
82 # Non-production sandbox events stack: the marketplace-events rule is scoped to the
83 # TestAccountAllowlist and the test event publisher is created.
84 IsDryRun: !Equals [!Ref MeteringMode, dry-run]
85
86# Template-level guard (defence in depth alongside deploy.sh + the publisher Lambda): a dry-run
87# (non-prod) events stack MUST NOT use the reserved production source aws.agreement-marketplace as
88# its EventSource — that would make the dry-run rule match REAL production events (e.g. if a stage
89# were literally named "aws"). Reject such a deploy at change-set creation.
90Rules:
91 DryRunEventSourceNotReserved:
92 RuleCondition: !Equals [!Ref MeteringMode, dry-run]
93 Assertions:
94 - Assert: !Not [!Equals [!Ref EventSource, aws.agreement-marketplace]]
95 AssertDescription: >-
96 A dry-run (non-prod) events stack must use a STAGE-SCOPED EventSource
97 (e.g. <stageName>.agreement-marketplace), NOT the reserved production source
98 aws.agreement-marketplace (a stage named 'aws' would collide with it).
99
100Globals:
101 Function:
102 Runtime: python3.12
103 Timeout: 30
104 # Apply the permissions boundary to the SAM-auto-generated Subscription Lambda role
105 # so iam:CreateRole succeeds under the deployer role (gated on a matching
106 # iam:PermissionsBoundary). The boundary policy must exist before deployment.
107 PermissionsBoundary: !Sub arn:aws:iam::${AWS::AccountId}:policy/${PermissionsBoundaryName}
108
109Resources:
110 # Unified subscribers table (subscription/agreement STATE) — PII-FREE. Holds CA identity
111 # (licenseArn, customerAWSAccountId, productCode, agreementId), the two lifecycle statuses,
112 # and registeredRegions only. Buyer PII / registration data lives in the per-Region
113 # customer-profile table (main stack), NOT here — so this table is safe in us-east-1 even
114 # for opt-in-Region products.
115 # Lives in us-east-1 alongside EventBridge marketplace events.
116 # Schema follows the Serverless SaaS Integration reference (CA-aware):
117 # licenseArn (PK) + customerAWSAccountId (SK)
118 # GSIs: lookups by customerAWSAccountId (register) and by
119 # agreementId (Agreement Ended/Amended fallback) use a Query on these indexes —
120 # NOT a full-table Scan — so access is least-privilege and permission-consistent.
121 SubscribersTable:
122 Type: AWS::DynamoDB::Table
123 DeletionPolicy: Retain
124 UpdateReplacePolicy: Retain
125 Properties:
126 TableName: !Sub ${StackPrefix}-subscribers
127 BillingMode: PAY_PER_REQUEST
128 PointInTimeRecoverySpecification:
129 PointInTimeRecoveryEnabled: true
130 SSESpecification:
131 SSEEnabled: true
132 Tags:
133 - Key: ManagedBy
134 Value: marketplace-metering-skill
135 AttributeDefinitions:
136 - AttributeName: licenseArn
137 AttributeType: S
138 - AttributeName: customerAWSAccountId
139 AttributeType: S
140 - AttributeName: agreementId
141 AttributeType: S
142 - AttributeName: deprovisioningPendingFlag
143 AttributeType: S
144 - AttributeName: deprovisioningExpiry
145 AttributeType: S
146 KeySchema:
147 - AttributeName: licenseArn
148 KeyType: HASH
149 - AttributeName: customerAWSAccountId
150 KeyType: RANGE
151 GlobalSecondaryIndexes:
152 # Look up all rows for one buyer account (register Lambda). customerAWSAccountId
153 # is the base-table SORT key, so it cannot be queried alone without this GSI.
154 - IndexName: customerAWSAccountId-index
155 KeySchema:
156 - AttributeName: customerAWSAccountId
157 KeyType: HASH
158 Projection:
159 ProjectionType: ALL
160 # Resolve licenseArn from agreementId for Agreement Ended/Amended events that
161 # omit license.arn (subscription Lambda). Sparse — only rows with agreementId.
162 - IndexName: agreementId-index
163 KeySchema:
164 - AttributeName: agreementId
165 KeyType: HASH
166 Projection:
167 ProjectionType: ALL
168 # SPARSE index of licenses currently in their ~1-hour deprovisioning flush window.
169 # The subscription Lambda SETS `deprovisioningPendingFlag="1"` + `deprovisioningExpiry`
170 # (= event time + 1h, ISO-8601) on License Deprovisioned, and the events-stack cleanup
171 # Lambda REMOVEs both on finalize (deprovisioning->inactive once expired), so this index
172 # holds ONLY the licenses still in a flush window (self-emptying). Keyed HASH=constant
173 # flag + RANGE=expiry so the cleanup Lambda can Query "flag='1' AND deprovisioningExpiry
174 # <= now" to find expired ones; the main-stack discoverer's rate(5m) flush-deprovisioning
175 # sweep and the submitter Query "flag='1'" (all) to get the active set. Never a Scan.
176 # KEYS_ONLY keeps the index tiny (base keys licenseArn+customerAWSAccountId + the index
177 # keys deprovisioningPendingFlag+deprovisioningExpiry are projected). The flush sweep uses
178 # the projected deprovisioningExpiry to decide whether to also flush a license's CURRENT
179 # hour (once within ~10 min of the window close) — no base-table read needed.
180 - IndexName: deprovisioning-pending-index
181 KeySchema:
182 - AttributeName: deprovisioningPendingFlag
183 KeyType: HASH
184 - AttributeName: deprovisioningExpiry
185 KeyType: RANGE
186 Projection:
187 ProjectionType: KEYS_ONLY
188
189 SubscriptionDLQ:
190 Type: AWS::SQS::Queue
191 Properties:
192 QueueName: !Sub ${StackPrefix}-subscription-dlq
193 MessageRetentionPeriod: 1209600
194 SqsManagedSseEnabled: true
195 Tags:
196 - Key: ManagedBy
197 Value: marketplace-metering-skill
198
199 SubscriptionQueue:
200 Type: AWS::SQS::Queue
201 Properties:
202 QueueName: !Sub ${StackPrefix}-subscription-notifications
203 VisibilityTimeout: 60
204 SqsManagedSseEnabled: true
205 Tags:
206 - Key: ManagedBy
207 Value: marketplace-metering-skill
208 RedrivePolicy:
209 deadLetterTargetArn: !GetAtt SubscriptionDLQ.Arn
210 maxReceiveCount: 3
211
212 MarketplaceEventRule:
213 Type: AWS::Events::Rule
214 Properties:
215 Name: !Sub ${StackPrefix}-marketplace-events
216 # LIVE (prod): match the real AWS Marketplace source aws.agreement-marketplace.
217 # DRY-RUN (non-prod): match the STAGE-SCOPED EventSource (e.g. beta.agreement-marketplace)
218 # that the test event publisher emits — NOT aws.agreement-marketplace. Because the dry-run
219 # source is DISJOINT from the real production source, a same-account prod + non-prod split
220 # cannot cross-match: production events (aws.agreement-marketplace) are never seen by the
221 # dry-run rule, and the dry-run test events (<stage>.agreement-marketplace) are never seen
222 # by the production rule — no double-write, no anything-but exclusion needed. The
223 # TestAccountAllowlist detail filter is kept as defence-in-depth so the dry-run rule only
224 # accepts declared test buyers.
225 EventPattern: !If
226 - IsDryRun
227 - source:
228 - !Ref EventSource
229 detail:
230 acceptor:
231 accountId: !Ref TestAccountAllowlist
232 - source:
233 - aws.agreement-marketplace
234 Targets:
235 - Arn: !GetAtt SubscriptionQueue.Arn
236 Id: SubscriptionQueueTarget
237 Tags:
238 - Key: ManagedBy
239 Value: marketplace-metering-skill
240
241 EventBridgeToSqsPolicy:
242 Type: AWS::SQS::QueuePolicy
243 Properties:
244 Queues:
245 - !Ref SubscriptionQueue
246 PolicyDocument:
247 Statement:
248 - Effect: Allow
249 Principal:
250 Service: events.amazonaws.com
251 Action: sqs:SendMessage
252 Resource: !GetAtt SubscriptionQueue.Arn
253 Condition:
254 ArnEquals:
255 aws:SourceArn: !GetAtt MarketplaceEventRule.Arn
256
257 # Explicit execution role with an awsmp-* name (single-prefix scope) so it is created
258 # within the deployer's role/awsmp-* write scope, and carries the permissions boundary.
259 SubscriptionFunctionRole:
260 Type: AWS::IAM::Role
261 Properties:
262 RoleName: !Sub ${StackPrefix}-subscription-role
263 PermissionsBoundary: !Sub arn:aws:iam::${AWS::AccountId}:policy/${PermissionsBoundaryName}
264 AssumeRolePolicyDocument:
265 Version: '2012-10-17'
266 Statement:
267 - Effect: Allow
268 Principal:
269 Service: lambda.amazonaws.com
270 Action: sts:AssumeRole
271 Policies:
272 - PolicyName: subscription-ddb-and-sqs
273 PolicyDocument:
274 Version: '2012-10-17'
275 Statement:
276 - Effect: Allow
277 Action:
278 - dynamodb:PutItem
279 - dynamodb:UpdateItem
280 - dynamodb:GetItem
281 - dynamodb:Query
282 Resource:
283 - !GetAtt SubscribersTable.Arn
284 # Query the agreementId GSI to resolve licenseArn for Agreement
285 # Ended/Amended events that omit license.arn.
286 - !Sub ${SubscribersTable.Arn}/index/agreementId-index
287 - Effect: Allow
288 Action:
289 - sqs:ReceiveMessage
290 - sqs:DeleteMessage
291 - sqs:GetQueueAttributes
292 Resource:
293 - !GetAtt SubscriptionQueue.Arn
294 - Effect: Allow
295 Action:
296 - logs:CreateLogStream
297 - logs:PutLogEvents
298 Resource:
299 - !GetAtt SubscriptionLogGroup.Arn
300 Tags:
301 - Key: ManagedBy
302 Value: marketplace-metering-skill
303
304 SubscriptionFunction:
305 Type: AWS::Serverless::Function
306 Properties:
307 FunctionName: !Sub ${StackPrefix}-subscription-handler
308 Handler: handlers/subscription.handler
309 CodeUri: src/
310 MemorySize: 128
311 Role: !GetAtt SubscriptionFunctionRole.Arn
312 Environment:
313 Variables:
314 SUBSCRIBERS_TABLE: !Ref SubscribersTable
315 Tags:
316 ManagedBy: marketplace-metering-skill
317 Events:
318 SQSEvent:
319 Type: SQS
320 Properties:
321 Queue: !GetAtt SubscriptionQueue.Arn
322 BatchSize: 10
323 FunctionResponseTypes:
324 - ReportBatchItemFailures
325
326 # Explicit log group: KMS-encrypted + retention-bounded, instead of the
327 # auto-created, unencrypted, never-expiring default. Logs contain buyer AWS account
328 # IDs and agreement metadata.
329 SubscriptionLogGroup:
330 Type: AWS::Logs::LogGroup
331 DeletionPolicy: Retain
332 UpdateReplacePolicy: Retain
333 Properties:
334 LogGroupName: !Sub /aws/lambda/${StackPrefix}-subscription-handler
335 RetentionInDays: !Ref LogRetentionInDays
336 KmsKeyId: !If [HasLogsKmsKey, !Ref LogsKmsKeyArn, !Ref 'AWS::NoValue']
337
338 # ── Deprovision cleanup (finalize expired deprovisioning licenses) ────────────────
339 # A rate(15m) EventBridge schedule invokes this Lambda; it Queries the sparse
340 # deprovisioning-pending-index for entries whose deprovisioningExpiry <= now and sets
341 # subscriptionStatus=inactive + REMOVEs both deprovisioning markers. Time-based finalization
342 # (not submitter-driven) is correct across multiple hours AND regions: once the ~1h flush
343 # window has elapsed no region can meter the license, so one us-east-1 finalize covers all.
344 DeprovisionCleanupFunctionRole:
345 Type: AWS::IAM::Role
346 Properties:
347 RoleName: !Sub ${StackPrefix}-deprovision-cleanup-role
348 PermissionsBoundary: !Sub arn:aws:iam::${AWS::AccountId}:policy/${PermissionsBoundaryName}
349 AssumeRolePolicyDocument:
350 Version: '2012-10-17'
351 Statement:
352 - Effect: Allow
353 Principal:
354 Service: lambda.amazonaws.com
355 Action: sts:AssumeRole
356 Policies:
357 - PolicyName: deprovision-cleanup-ddb
358 PolicyDocument:
359 Version: '2012-10-17'
360 Statement:
361 - Effect: Allow
362 Action:
363 - dynamodb:UpdateItem
364 Resource:
365 - !GetAtt SubscribersTable.Arn
366 - Effect: Allow
367 Action:
368 - dynamodb:Query
369 Resource:
370 # Query the sparse deprovisioning index for expired entries. Query, not Scan.
371 - !Sub ${SubscribersTable.Arn}/index/deprovisioning-pending-index
372 - Effect: Allow
373 Action:
374 - logs:CreateLogStream
375 - logs:PutLogEvents
376 Resource:
377 - !GetAtt DeprovisionCleanupLogGroup.Arn
378 Tags:
379 - Key: ManagedBy
380 Value: marketplace-metering-skill
381
382 DeprovisionCleanupFunction:
383 Type: AWS::Serverless::Function
384 Properties:
385 FunctionName: !Sub ${StackPrefix}-deprovision-cleanup
386 Handler: handlers/deprovision_cleanup.handler
387 CodeUri: src/
388 MemorySize: 128
389 Timeout: 60
390 Role: !GetAtt DeprovisionCleanupFunctionRole.Arn
391 Environment:
392 Variables:
393 SUBSCRIBERS_TABLE: !Ref SubscribersTable
394 Tags:
395 ManagedBy: marketplace-metering-skill
396 Events:
397 Schedule:
398 Type: Schedule
399 Properties:
400 Schedule: rate(15 minutes)
401
402 DeprovisionCleanupLogGroup:
403 Type: AWS::Logs::LogGroup
404 DeletionPolicy: Retain
405 UpdateReplacePolicy: Retain
406 Properties:
407 LogGroupName: !Sub /aws/lambda/${StackPrefix}-deprovision-cleanup
408 RetentionInDays: !Ref LogRetentionInDays
409 KmsKeyId: !If [HasLogsKmsKey, !Ref LogsKmsKeyArn, !Ref 'AWS::NoValue']
410
411 # ── Test event publisher (NON-PROD / dry-run ONLY) ───────────────────────────────
412 # Simulates AWS Marketplace subscription events under a STAGE-SCOPED source (EventSource, e.g.
413 # beta.agreement-marketplace — never the reserved aws.* prefix) with randomized-but-valid
414 # values, using ONLY the TestAccountAllowlist accounts, so a seller can exercise the non-prod
415 # pipeline without real subscriptions. Created ONLY when MeteringMode=dry-run. Invoke manually
416 # with {"scenario":"...","count":N,"productCode":"<awsMarketplaceProductCode>"} — productCode is
417 # REQUIRED per invocation (no stack param / env), so this shared publisher can simulate any
418 # product in the stage.
419 TestEventPublisherRole:
420 Type: AWS::IAM::Role
421 Condition: IsDryRun
422 Properties:
423 RoleName: !Sub ${StackPrefix}-test-event-publisher-role
424 PermissionsBoundary: !Sub arn:aws:iam::${AWS::AccountId}:policy/${PermissionsBoundaryName}
425 AssumeRolePolicyDocument:
426 Version: '2012-10-17'
427 Statement:
428 - Effect: Allow
429 Principal:
430 Service: lambda.amazonaws.com
431 Action: sts:AssumeRole
432 Policies:
433 - PolicyName: test-event-publisher-putevents
434 PolicyDocument:
435 Version: '2012-10-17'
436 Statement:
437 - Effect: Allow
438 Action: events:PutEvents
439 # Publish to the default bus in this (us-east-1) region — the same bus the
440 # scoped marketplace-events rule reads.
441 Resource: !Sub arn:aws:events:${AWS::Region}:${AWS::AccountId}:event-bus/default
442 - Effect: Allow
443 Action:
444 - logs:CreateLogStream
445 - logs:PutLogEvents
446 Resource: !GetAtt TestEventPublisherLogGroup.Arn
447 Tags:
448 - Key: ManagedBy
449 Value: marketplace-metering-skill
450 - Key: TestOnly
451 Value: 'true'
452
453 TestEventPublisherFunction:
454 Type: AWS::Serverless::Function
455 Condition: IsDryRun
456 Properties:
457 FunctionName: !Sub ${StackPrefix}-test-event-publisher
458 Handler: handlers/test_event_publisher.handler
459 CodeUri: src/
460 MemorySize: 128
461 Timeout: 60
462 Role: !GetAtt TestEventPublisherRole.Arn
463 Environment:
464 Variables:
465 METERING_MODE: !Ref MeteringMode
466 TEST_ACCOUNT_ALLOWLIST: !Join [',', !Ref TestAccountAllowlist]
467 # Stage-scoped source the events rule matches (never aws.*); set by deploy.sh.
468 EVENT_SOURCE: !Ref EventSource
469 # NOTE: there is intentionally NO PRODUCT_CODE env. productCode is supplied PER INVOCATION
470 # (payload {"productCode": "..."}), so this SINGLE shared events-stack publisher can
471 # simulate ANY product in the stage and a second product's deploy can never overwrite a
472 # shared per-product value. See scripts/test_event_publisher.py handler() for the payload.
473 Tags:
474 ManagedBy: marketplace-metering-skill
475 TestOnly: 'true'
476
477 TestEventPublisherLogGroup:
478 Type: AWS::Logs::LogGroup
479 Condition: IsDryRun
480 DeletionPolicy: Retain
481 UpdateReplacePolicy: Retain
482 Properties:
483 LogGroupName: !Sub /aws/lambda/${StackPrefix}-test-event-publisher
484 RetentionInDays: !Ref LogRetentionInDays
485 KmsKeyId: !If [HasLogsKmsKey, !Ref LogsKmsKeyArn, !Ref 'AWS::NoValue']
486
487 # ── Monitoring & Alarms (events stack, seller account, us-east-1) ─────────────────
488 # Alarms are ALWAYS created; their actions wire to an OPTIONAL seller-supplied in-region
489 # (us-east-1) SNS topic (AlertsTopicArn). Observability does not depend on the stack
490 # pre-creating a topic. Neither the deployer role nor any Lambda publishes to it
491 # (CloudWatch fires the action); acting on alarms is the seller's responsibility.
492
493 DLQAlarm:
494 Type: AWS::CloudWatch::Alarm
495 Properties:
496 AlarmName: !Sub ${StackPrefix}-dlq-depth
497 AlarmDescription: Subscription lifecycle events failed processing - subscriber state may be stale
498 Namespace: AWS/SQS
499 MetricName: ApproximateNumberOfMessagesVisible
500 Dimensions:
501 - Name: QueueName
502 Value: !GetAtt SubscriptionDLQ.QueueName
503 Statistic: Sum
504 Period: 300
505 EvaluationPeriods: 1
506 Threshold: 1
507 ComparisonOperator: GreaterThanOrEqualToThreshold
508 TreatMissingData: notBreaching
509 AlarmActions:
510 - !If [HasAlertsTopic, !Ref AlertsTopicArn, !Ref 'AWS::NoValue']
511
512 SubscriptionErrorAlarm:
513 Type: AWS::CloudWatch::Alarm
514 Properties:
515 AlarmName: !Sub ${StackPrefix}-subscription-errors
516 AlarmDescription: Subscription Lambda errors - lifecycle events may not be applied
517 Namespace: AWS/Lambda
518 MetricName: Errors
519 Dimensions:
520 - Name: FunctionName
521 Value: !Ref SubscriptionFunction
522 Statistic: Sum
523 Period: 3600
524 EvaluationPeriods: 1
525 Threshold: 1
526 ComparisonOperator: GreaterThanOrEqualToThreshold
527 TreatMissingData: notBreaching
528 AlarmActions:
529 - !If [HasAlertsTopic, !Ref AlertsTopicArn, !Ref 'AWS::NoValue']
530
531 # Deprovision-cleanup Lambda Errors — this Lambda solely owns deprovisioning->inactive
532 # finalization AND removing the sparse-index markers. If it fails repeatedly, licenses stay
533 # `deprovisioning` and the sparse deprovisioning-pending-index never self-empties (grows
534 # unbounded), silently. Mirrors SubscriptionErrorAlarm (no-silent-failure coverage).
535 DeprovisionCleanupErrorAlarm:
536 Type: AWS::CloudWatch::Alarm
537 Properties:
538 AlarmName: !Sub ${StackPrefix}-deprovision-cleanup-errors
539 AlarmDescription: Deprovision-cleanup Lambda errors - deprovisioning licenses may not be finalized and the sparse index may grow unbounded
540 Namespace: AWS/Lambda
541 MetricName: Errors
542 Dimensions:
543 - Name: FunctionName
544 Value: !Ref DeprovisionCleanupFunction
545 Statistic: Sum
546 Period: 3600
547 EvaluationPeriods: 1
548 Threshold: 1
549 ComparisonOperator: GreaterThanOrEqualToThreshold
550 TreatMissingData: notBreaching
551 AlarmActions:
552 - !If [HasAlertsTopic, !Ref AlertsTopicArn, !Ref 'AWS::NoValue']
553
554 # Deprovision-cleanup "did not run" — the rate(15m) schedule stalled. Missing data = breaching
555 # so a full stop is caught (the index would otherwise silently stop self-emptying).
556 DeprovisionCleanupDidNotRunAlarm:
557 Type: AWS::CloudWatch::Alarm
558 Properties:
559 AlarmName: !Sub ${StackPrefix}-deprovision-cleanup-did-not-run
560 AlarmDescription: Deprovision-cleanup Lambda has not been invoked - its rate(15m) schedule may be disabled/broken
561 Namespace: AWS/Lambda
562 MetricName: Invocations
563 Dimensions:
564 - Name: FunctionName
565 Value: !Ref DeprovisionCleanupFunction
566 Statistic: Sum
567 Period: 3600
568 EvaluationPeriods: 1
569 Threshold: 1
570 ComparisonOperator: LessThanThreshold
571 TreatMissingData: breaching
572 AlarmActions:
573 - !If [HasAlertsTopic, !Ref AlertsTopicArn, !Ref 'AWS::NoValue']
574
575 # ── Events-stack health dashboard (shared; ) ─────────────────────────────
576 # Events-stack health in one place: subscription Lambda health + subscription queue/DLQ
577 # depth. HEALTH-only, shared across products (not per-product). Created by default;
578 # set CreateDashboard=false to skip, or delete/customize after deploy.
579 EventsHealthDashboard:
580 Type: AWS::CloudWatch::Dashboard
581 Condition: CreateHealthDashboard
582 Properties:
583 # Account-global name; keep the Region so the name is unique per Region
584 # (the events stack deploys to us-east-1, but the same prefix in another
585 # Region would otherwise collide on the account-global dashboard namespace).
586 DashboardName: !Sub ${StackPrefix}-health-${AWS::Region}
587 DashboardBody: !Sub |
588 {
589 "widgets": [
590 {
591 "type": "text", "x": 0, "y": 0, "width": 24, "height": 1,
592 "properties": {"markdown": "# Events stack health — ${StackPrefix} (shared, us-east-1)"}
593 },
594 {
595 "type": "metric", "x": 0, "y": 1, "width": 12, "height": 6,
596 "properties": {
597 "title": "Subscription Lambda Errors / Throttles",
598 "region": "${AWS::Region}", "stat": "Sum", "period": 300,
599 "metrics": [
600 ["AWS/Lambda", "Errors", "FunctionName", "${SubscriptionFunction}", {"label": "errors"}],
601 ["AWS/Lambda", "Throttles", "FunctionName", "${SubscriptionFunction}", {"label": "throttles"}]
602 ]
603 }
604 },
605 {
606 "type": "metric", "x": 12, "y": 1, "width": 12, "height": 6,
607 "properties": {
608 "title": "Subscription Lambda Invocations",
609 "region": "${AWS::Region}", "stat": "Sum", "period": 300,
610 "metrics": [
611 ["AWS/Lambda", "Invocations", "FunctionName", "${SubscriptionFunction}"]
612 ]
613 }
614 },
615 {
616 "type": "metric", "x": 0, "y": 7, "width": 24, "height": 6,
617 "properties": {
618 "title": "Subscription queue depth + DLQ depth",
619 "region": "${AWS::Region}", "stat": "Maximum", "period": 300,
620 "metrics": [
621 ["AWS/SQS", "ApproximateNumberOfMessagesVisible", "QueueName", "${SubscriptionQueue.QueueName}", {"label": "notifications"}],
622 ["...", "${SubscriptionDLQ.QueueName}", {"label": "DLQ"}]
623 ]
624 }
625 }
626 ]
627 }
628
629Outputs:
630 SubscribersTableName:
631 Description: Pass this to the main stack SubscribersTableName parameter
632 Value: !Ref SubscribersTable
633 SubscribersTableArn:
634 Description: ARN of the unified subscribers table
635 Value: !GetAtt SubscribersTable.Arn
636 SubscriptionQueueArn:
637 Value: !GetAtt SubscriptionQueue.Arn
638 DLQUrl:
639 Value: !Ref SubscriptionDLQ