@mettlecast/domain-cdk-packer 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/dist/DomainStack.d.ts +68 -0
- package/dist/DomainStack.js +567 -0
- package/dist/__tests__/domain-stack.test.d.ts +1 -0
- package/dist/__tests__/domain-stack.test.js +223 -0
- package/dist/__tests__/lambda-factory.test.d.ts +1 -0
- package/dist/__tests__/lambda-factory.test.js +76 -0
- package/dist/__tests__/pack-flows.test.d.ts +1 -0
- package/dist/__tests__/pack-flows.test.js +283 -0
- package/dist/__tests__/registry.test.d.ts +1 -0
- package/dist/__tests__/registry.test.js +34 -0
- package/dist/__tests__/step-functions-codegen.test.d.ts +1 -0
- package/dist/__tests__/step-functions-codegen.test.js +159 -0
- package/dist/aspects/iam-boundaries-aspect.d.ts +11 -0
- package/dist/aspects/iam-boundaries-aspect.js +15 -0
- package/dist/aspects/index.d.ts +5 -0
- package/dist/aspects/index.js +3 -0
- package/dist/aspects/log-retention-aspect.d.ts +8 -0
- package/dist/aspects/log-retention-aspect.js +24 -0
- package/dist/aspects/tagging-aspect.d.ts +13 -0
- package/dist/aspects/tagging-aspect.js +17 -0
- package/dist/constructs/action-construct.d.ts +35 -0
- package/dist/constructs/action-construct.js +35 -0
- package/dist/constructs/alarm-construct.d.ts +19 -0
- package/dist/constructs/alarm-construct.js +58 -0
- package/dist/constructs/alarms-construct.d.ts +17 -0
- package/dist/constructs/alarms-construct.js +42 -0
- package/dist/constructs/api-construct.d.ts +30 -0
- package/dist/constructs/api-construct.js +64 -0
- package/dist/constructs/canary-construct.d.ts +13 -0
- package/dist/constructs/canary-construct.js +30 -0
- package/dist/constructs/cmk-construct.d.ts +9 -0
- package/dist/constructs/cmk-construct.js +20 -0
- package/dist/constructs/dashboard-construct.d.ts +15 -0
- package/dist/constructs/dashboard-construct.js +62 -0
- package/dist/constructs/health-construct.d.ts +11 -0
- package/dist/constructs/health-construct.js +29 -0
- package/dist/constructs/job-construct.d.ts +33 -0
- package/dist/constructs/job-construct.js +54 -0
- package/dist/constructs/schedule-construct.d.ts +27 -0
- package/dist/constructs/schedule-construct.js +54 -0
- package/dist/constructs/subscriber-construct.d.ts +30 -0
- package/dist/constructs/subscriber-construct.js +63 -0
- package/dist/constructs/waf-construct.d.ts +13 -0
- package/dist/constructs/waf-construct.js +75 -0
- package/dist/constructs/webhook-construct.d.ts +33 -0
- package/dist/constructs/webhook-construct.js +50 -0
- package/dist/flow-registry.d.ts +81 -0
- package/dist/flow-registry.js +2 -0
- package/dist/grouped-lambda-factory.d.ts +43 -0
- package/dist/grouped-lambda-factory.js +71 -0
- package/dist/iam/iam-policy-builder.d.ts +95 -0
- package/dist/iam/iam-policy-builder.js +232 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +17 -0
- package/dist/lambda-factory.d.ts +46 -0
- package/dist/lambda-factory.js +80 -0
- package/dist/pack-domain.d.ts +35 -0
- package/dist/pack-domain.js +32 -0
- package/dist/pack-flows.d.ts +28 -0
- package/dist/pack-flows.js +154 -0
- package/dist/registry.d.ts +224 -0
- package/dist/registry.js +1 -0
- package/dist/step-functions-codegen.d.ts +65 -0
- package/dist/step-functions-codegen.js +55 -0
- package/package.json +36 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { StepFunctionsCodegen } from '../step-functions-codegen.js';
|
|
3
|
+
describe('StepFunctionsCodegen', () => {
|
|
4
|
+
const codegen = new StepFunctionsCodegen();
|
|
5
|
+
describe('generateTaskState', () => {
|
|
6
|
+
it('should set Next when node has next property', () => {
|
|
7
|
+
const node = {
|
|
8
|
+
type: 'domain-action',
|
|
9
|
+
domainId: 'payments',
|
|
10
|
+
actionId: 'charge-card',
|
|
11
|
+
next: 'Step2',
|
|
12
|
+
};
|
|
13
|
+
const state = codegen.generateTaskState(node);
|
|
14
|
+
expect(state.Next).toBe('Step2');
|
|
15
|
+
expect(state.End).toBeUndefined();
|
|
16
|
+
});
|
|
17
|
+
it('should set End=true when node has no next property', () => {
|
|
18
|
+
const node = {
|
|
19
|
+
type: 'domain-action',
|
|
20
|
+
domainId: 'payments',
|
|
21
|
+
actionId: 'charge-card',
|
|
22
|
+
};
|
|
23
|
+
const state = codegen.generateTaskState(node);
|
|
24
|
+
expect(state.End).toBe(true);
|
|
25
|
+
expect(state.Next).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
it('should generate correct FunctionName from domainId and actionId', () => {
|
|
28
|
+
const node = {
|
|
29
|
+
type: 'domain-action',
|
|
30
|
+
domainId: 'payments',
|
|
31
|
+
actionId: 'charge-card',
|
|
32
|
+
};
|
|
33
|
+
const state = codegen.generateTaskState(node);
|
|
34
|
+
expect(state.Parameters?.FunctionName).toBe('payments-charge-card');
|
|
35
|
+
});
|
|
36
|
+
it('should use label as Comment when provided', () => {
|
|
37
|
+
const node = {
|
|
38
|
+
type: 'domain-action',
|
|
39
|
+
domainId: 'payments',
|
|
40
|
+
actionId: 'charge-card',
|
|
41
|
+
label: 'Charge the card',
|
|
42
|
+
};
|
|
43
|
+
const state = codegen.generateTaskState(node);
|
|
44
|
+
expect(state.Comment).toBe('Charge the card');
|
|
45
|
+
});
|
|
46
|
+
it('should use default comment format when label is not provided', () => {
|
|
47
|
+
const node = {
|
|
48
|
+
type: 'domain-action',
|
|
49
|
+
domainId: 'payments',
|
|
50
|
+
actionId: 'charge-card',
|
|
51
|
+
};
|
|
52
|
+
const state = codegen.generateTaskState(node);
|
|
53
|
+
expect(state.Comment).toBe('Invoke domain action payments/charge-card');
|
|
54
|
+
});
|
|
55
|
+
it('should include standard Task fields in the state', () => {
|
|
56
|
+
const node = {
|
|
57
|
+
type: 'domain-action',
|
|
58
|
+
domainId: 'payments',
|
|
59
|
+
actionId: 'charge-card',
|
|
60
|
+
};
|
|
61
|
+
const state = codegen.generateTaskState(node);
|
|
62
|
+
expect(state.Type).toBe('Task');
|
|
63
|
+
expect(state.Resource).toBe('arn:aws:states:::lambda:invoke');
|
|
64
|
+
expect(state.Parameters?.['Payload.$']).toBe('$');
|
|
65
|
+
});
|
|
66
|
+
it('should include node parameters in the Parameters object', () => {
|
|
67
|
+
const node = {
|
|
68
|
+
type: 'domain-action',
|
|
69
|
+
domainId: 'payments',
|
|
70
|
+
actionId: 'charge-card',
|
|
71
|
+
parameters: { amount: 100, currency: 'USD' },
|
|
72
|
+
};
|
|
73
|
+
const state = codegen.generateTaskState(node);
|
|
74
|
+
expect(state.Parameters?.amount).toBe(100);
|
|
75
|
+
expect(state.Parameters?.currency).toBe('USD');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('generateStateMachine', () => {
|
|
79
|
+
it('should set StartAt to first node label by default', () => {
|
|
80
|
+
const nodes = [
|
|
81
|
+
{
|
|
82
|
+
type: 'domain-action',
|
|
83
|
+
domainId: 'payments',
|
|
84
|
+
actionId: 'charge-card',
|
|
85
|
+
label: 'Charge Card',
|
|
86
|
+
next: 'Validate Payment',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
type: 'domain-action',
|
|
90
|
+
domainId: 'payments',
|
|
91
|
+
actionId: 'validate-payment',
|
|
92
|
+
label: 'Validate Payment',
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
const result = codegen.generateStateMachine(nodes);
|
|
96
|
+
expect(result.StartAt).toBe('Charge Card');
|
|
97
|
+
});
|
|
98
|
+
it('should set StartAt to first node actionId when label is missing', () => {
|
|
99
|
+
const nodes = [
|
|
100
|
+
{
|
|
101
|
+
type: 'domain-action',
|
|
102
|
+
domainId: 'payments',
|
|
103
|
+
actionId: 'charge-card',
|
|
104
|
+
next: 'payments/validate-payment',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
type: 'domain-action',
|
|
108
|
+
domainId: 'payments',
|
|
109
|
+
actionId: 'validate-payment',
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
const result = codegen.generateStateMachine(nodes);
|
|
113
|
+
expect(result.StartAt).toBe('payments/charge-card');
|
|
114
|
+
});
|
|
115
|
+
it('should include all nodes as states with correct state names', () => {
|
|
116
|
+
const nodes = [
|
|
117
|
+
{
|
|
118
|
+
type: 'domain-action',
|
|
119
|
+
domainId: 'payments',
|
|
120
|
+
actionId: 'charge-card',
|
|
121
|
+
label: 'Charge Card',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
type: 'domain-action',
|
|
125
|
+
domainId: 'payments',
|
|
126
|
+
actionId: 'validate-payment',
|
|
127
|
+
label: 'Validate Payment',
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
const result = codegen.generateStateMachine(nodes);
|
|
131
|
+
expect(Object.keys(result.States)).toHaveLength(2);
|
|
132
|
+
expect(result.States['Charge Card']).toBeDefined();
|
|
133
|
+
expect(result.States['Validate Payment']).toBeDefined();
|
|
134
|
+
});
|
|
135
|
+
it('should use custom startAt parameter when provided', () => {
|
|
136
|
+
const nodes = [
|
|
137
|
+
{
|
|
138
|
+
type: 'domain-action',
|
|
139
|
+
domainId: 'payments',
|
|
140
|
+
actionId: 'charge-card',
|
|
141
|
+
label: 'Charge Card',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
type: 'domain-action',
|
|
145
|
+
domainId: 'payments',
|
|
146
|
+
actionId: 'validate-payment',
|
|
147
|
+
label: 'Validate Payment',
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
const result = codegen.generateStateMachine(nodes, 'CustomStart');
|
|
151
|
+
expect(result.StartAt).toBe('CustomStart');
|
|
152
|
+
});
|
|
153
|
+
it('should handle empty nodes array', () => {
|
|
154
|
+
const result = codegen.generateStateMachine([]);
|
|
155
|
+
expect(result.StartAt).toBe('Start');
|
|
156
|
+
expect(Object.keys(result.States)).toHaveLength(0);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as cdk from 'aws-cdk-lib';
|
|
2
|
+
import { IConstruct } from 'constructs';
|
|
3
|
+
export interface IamBoundariesAspectProps {
|
|
4
|
+
/** ARN of the permission boundary policy to attach to all Lambda roles. */
|
|
5
|
+
permissionBoundaryArn: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class IamBoundariesAspect implements cdk.IAspect {
|
|
8
|
+
private readonly props;
|
|
9
|
+
constructor(props: IamBoundariesAspectProps);
|
|
10
|
+
visit(node: IConstruct): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
2
|
+
export class IamBoundariesAspect {
|
|
3
|
+
props;
|
|
4
|
+
constructor(props) {
|
|
5
|
+
this.props = props;
|
|
6
|
+
}
|
|
7
|
+
visit(node) {
|
|
8
|
+
if (node instanceof iam.Role) {
|
|
9
|
+
const cfnRole = node.node.defaultChild;
|
|
10
|
+
if (cfnRole) {
|
|
11
|
+
cfnRole.permissionsBoundary = this.props.permissionBoundaryArn;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { TibTaggingAspect } from './tagging-aspect.js';
|
|
2
|
+
export type { TibTaggingAspectProps } from './tagging-aspect.js';
|
|
3
|
+
export { LogRetentionAspect } from './log-retention-aspect.js';
|
|
4
|
+
export { IamBoundariesAspect } from './iam-boundaries-aspect.js';
|
|
5
|
+
export type { IamBoundariesAspectProps } from './iam-boundaries-aspect.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as cdk from 'aws-cdk-lib';
|
|
2
|
+
import { IConstruct } from 'constructs';
|
|
3
|
+
export declare class LogRetentionAspect implements cdk.IAspect {
|
|
4
|
+
private readonly retentionDays;
|
|
5
|
+
constructor(retentionDays?: number);
|
|
6
|
+
visit(node: IConstruct): void;
|
|
7
|
+
private toRetentionDays;
|
|
8
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as logs from 'aws-cdk-lib/aws-logs';
|
|
2
|
+
export class LogRetentionAspect {
|
|
3
|
+
retentionDays;
|
|
4
|
+
constructor(retentionDays = 30) {
|
|
5
|
+
this.retentionDays = this.toRetentionDays(retentionDays);
|
|
6
|
+
}
|
|
7
|
+
visit(node) {
|
|
8
|
+
if (node instanceof logs.LogGroup) {
|
|
9
|
+
const cfnLogGroup = node.node.defaultChild;
|
|
10
|
+
if (cfnLogGroup) {
|
|
11
|
+
cfnLogGroup.addPropertyOverride('RetentionInDays', this.retentionDays);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
toRetentionDays(days) {
|
|
16
|
+
const map = {
|
|
17
|
+
30: logs.RetentionDays.ONE_MONTH,
|
|
18
|
+
90: logs.RetentionDays.THREE_MONTHS,
|
|
19
|
+
365: logs.RetentionDays.ONE_YEAR,
|
|
20
|
+
2557: logs.RetentionDays.SEVEN_YEARS,
|
|
21
|
+
};
|
|
22
|
+
return map[days] ?? logs.RetentionDays.ONE_MONTH;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as cdk from 'aws-cdk-lib';
|
|
2
|
+
import { IConstruct } from 'constructs';
|
|
3
|
+
export interface TibTaggingAspectProps {
|
|
4
|
+
env: string;
|
|
5
|
+
domainId: string;
|
|
6
|
+
owner?: string;
|
|
7
|
+
costCenter?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class TibTaggingAspect implements cdk.IAspect {
|
|
10
|
+
private readonly props;
|
|
11
|
+
constructor(props: TibTaggingAspectProps);
|
|
12
|
+
visit(node: IConstruct): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as cdk from 'aws-cdk-lib';
|
|
2
|
+
export class TibTaggingAspect {
|
|
3
|
+
props;
|
|
4
|
+
constructor(props) {
|
|
5
|
+
this.props = props;
|
|
6
|
+
}
|
|
7
|
+
visit(node) {
|
|
8
|
+
if (cdk.CfnResource.isCfnResource(node)) {
|
|
9
|
+
cdk.Tags.of(node).add('tib:env', this.props.env);
|
|
10
|
+
cdk.Tags.of(node).add('tib:domain', this.props.domainId);
|
|
11
|
+
if (this.props.owner)
|
|
12
|
+
cdk.Tags.of(node).add('tib:owner', this.props.owner);
|
|
13
|
+
if (this.props.costCenter)
|
|
14
|
+
cdk.Tags.of(node).add('tib:cost-center', this.props.costCenter);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as lambdaNode from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
import { LambdaFactory } from '../lambda-factory.js';
|
|
4
|
+
import { IamPolicyBuilder } from '../iam/iam-policy-builder.js';
|
|
5
|
+
import type { DomainRegistry } from '../registry.js';
|
|
6
|
+
/**
|
|
7
|
+
* Configuration properties for ActionConstruct.
|
|
8
|
+
*/
|
|
9
|
+
export interface ActionConstructProps {
|
|
10
|
+
/** Domain registry containing all action entries. */
|
|
11
|
+
registry: DomainRegistry;
|
|
12
|
+
/** Factory for creating Lambda functions from registry entries. */
|
|
13
|
+
lambdaFactory: LambdaFactory;
|
|
14
|
+
/** IAM policy builder for granting permissions to Lambda roles. */
|
|
15
|
+
iamBuilder: IamPolicyBuilder;
|
|
16
|
+
/**
|
|
17
|
+
* Allowed origins for Function URL CORS on workspace-visible actions.
|
|
18
|
+
* Defaults to ['*']. Set to specific dashboard URLs + localhost for production.
|
|
19
|
+
*/
|
|
20
|
+
corsAllowedOrigins?: string[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* CDK Construct that synthesises one Lambda per registered action, with optional Function URL for workspace-visible actions.
|
|
24
|
+
*/
|
|
25
|
+
export declare class ActionConstruct extends Construct {
|
|
26
|
+
/** Lambda function per action id. */
|
|
27
|
+
readonly functions: Map<string, lambdaNode.NodejsFunction>;
|
|
28
|
+
/**
|
|
29
|
+
* Create a new ActionConstruct instance.
|
|
30
|
+
* @param scope - Parent CDK scope.
|
|
31
|
+
* @param id - Construct identifier.
|
|
32
|
+
* @param props - Construct properties.
|
|
33
|
+
*/
|
|
34
|
+
constructor(scope: Construct, id: string, props: ActionConstructProps);
|
|
35
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
/**
|
|
4
|
+
* CDK Construct that synthesises one Lambda per registered action, with optional Function URL for workspace-visible actions.
|
|
5
|
+
*/
|
|
6
|
+
export class ActionConstruct extends Construct {
|
|
7
|
+
/** Lambda function per action id. */
|
|
8
|
+
functions;
|
|
9
|
+
/**
|
|
10
|
+
* Create a new ActionConstruct instance.
|
|
11
|
+
* @param scope - Parent CDK scope.
|
|
12
|
+
* @param id - Construct identifier.
|
|
13
|
+
* @param props - Construct properties.
|
|
14
|
+
*/
|
|
15
|
+
constructor(scope, id, props) {
|
|
16
|
+
super(scope, id);
|
|
17
|
+
this.functions = new Map();
|
|
18
|
+
for (const entry of props.registry.actions) {
|
|
19
|
+
const fn = props.lambdaFactory.createFunction(entry, entry.deployment);
|
|
20
|
+
this.functions.set(entry.id, fn);
|
|
21
|
+
props.iamBuilder.forAction().forEach((statement) => {
|
|
22
|
+
fn.addToRolePolicy(statement);
|
|
23
|
+
});
|
|
24
|
+
if (entry.visibility === 'workspace') {
|
|
25
|
+
fn.addFunctionUrl({
|
|
26
|
+
authType: lambda.FunctionUrlAuthType.NONE,
|
|
27
|
+
cors: {
|
|
28
|
+
allowedOrigins: props.corsAllowedOrigins ?? ['*'],
|
|
29
|
+
allowedMethods: [lambda.HttpMethod.ALL],
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
|
|
2
|
+
import * as sqs from 'aws-cdk-lib/aws-sqs';
|
|
3
|
+
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
4
|
+
import { Construct } from 'constructs';
|
|
5
|
+
export interface AlarmConstructProps {
|
|
6
|
+
domainId: string;
|
|
7
|
+
lambdaFunctions: lambda.Function[];
|
|
8
|
+
dlqs: sqs.Queue[];
|
|
9
|
+
/** SNS topic ARN for alarm notifications. If omitted, alarms are created without actions. */
|
|
10
|
+
snsTopicArn?: string;
|
|
11
|
+
/** p99 latency threshold in ms. Default: 5000. */
|
|
12
|
+
p99ThresholdMs?: number;
|
|
13
|
+
/** Error rate threshold (0–1). Default: 0.01 (1%). */
|
|
14
|
+
errorRateThreshold?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare class AlarmConstruct extends Construct {
|
|
17
|
+
readonly alarms: cloudwatch.Alarm[];
|
|
18
|
+
constructor(scope: Construct, id: string, props: AlarmConstructProps);
|
|
19
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as cdk from 'aws-cdk-lib';
|
|
2
|
+
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
|
|
3
|
+
import * as cloudwatchActions from 'aws-cdk-lib/aws-cloudwatch-actions';
|
|
4
|
+
import * as sns from 'aws-cdk-lib/aws-sns';
|
|
5
|
+
import { Construct } from 'constructs';
|
|
6
|
+
export class AlarmConstruct extends Construct {
|
|
7
|
+
alarms = [];
|
|
8
|
+
constructor(scope, id, props) {
|
|
9
|
+
super(scope, id);
|
|
10
|
+
const snsTopic = props.snsTopicArn
|
|
11
|
+
? sns.Topic.fromTopicArn(this, 'AlarmTopic', props.snsTopicArn)
|
|
12
|
+
: undefined;
|
|
13
|
+
const alarmAction = snsTopic
|
|
14
|
+
? [new cloudwatchActions.SnsAction(snsTopic)]
|
|
15
|
+
: [];
|
|
16
|
+
// DLQ depth alarms — fire immediately when any message lands in DLQ
|
|
17
|
+
for (const dlq of props.dlqs) {
|
|
18
|
+
const alarm = new cloudwatch.Alarm(this, `DlqAlarm-${dlq.node.id}`, {
|
|
19
|
+
alarmName: `tib-${props.domainId}-dlq-${dlq.node.id}-depth`,
|
|
20
|
+
alarmDescription: `DLQ ${dlq.node.id} has messages — investigate failed processing`,
|
|
21
|
+
metric: dlq.metricApproximateNumberOfMessagesVisible({
|
|
22
|
+
period: cdk.Duration.minutes(1),
|
|
23
|
+
statistic: 'Maximum',
|
|
24
|
+
}),
|
|
25
|
+
threshold: 0,
|
|
26
|
+
evaluationPeriods: 1,
|
|
27
|
+
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
28
|
+
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
|
|
29
|
+
});
|
|
30
|
+
if (alarmAction.length > 0)
|
|
31
|
+
alarm.addAlarmAction(...alarmAction);
|
|
32
|
+
this.alarms.push(alarm);
|
|
33
|
+
}
|
|
34
|
+
// Lambda error rate alarms
|
|
35
|
+
const errorThreshold = props.errorRateThreshold ?? 0.01;
|
|
36
|
+
for (const fn of props.lambdaFunctions) {
|
|
37
|
+
const errorAlarm = new cloudwatch.Alarm(this, `ErrorAlarm-${fn.node.id}`, {
|
|
38
|
+
alarmName: `tib-${props.domainId}-${fn.node.id}-error-rate`,
|
|
39
|
+
alarmDescription: `Lambda ${fn.node.id} error rate exceeded ${errorThreshold * 100}%`,
|
|
40
|
+
metric: new cloudwatch.MathExpression({
|
|
41
|
+
expression: 'errors / invocations',
|
|
42
|
+
usingMetrics: {
|
|
43
|
+
errors: fn.metricErrors({ period: cdk.Duration.minutes(5) }),
|
|
44
|
+
invocations: fn.metricInvocations({ period: cdk.Duration.minutes(5) }),
|
|
45
|
+
},
|
|
46
|
+
period: cdk.Duration.minutes(5),
|
|
47
|
+
}),
|
|
48
|
+
threshold: errorThreshold,
|
|
49
|
+
evaluationPeriods: 2,
|
|
50
|
+
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
51
|
+
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
|
|
52
|
+
});
|
|
53
|
+
if (alarmAction.length > 0)
|
|
54
|
+
errorAlarm.addAlarmAction(...alarmAction);
|
|
55
|
+
this.alarms.push(errorAlarm);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Construct } from 'constructs';
|
|
2
|
+
export interface AlarmThresholds {
|
|
3
|
+
latencyP99Ms?: number;
|
|
4
|
+
errorRatePct?: number;
|
|
5
|
+
coldStartRatePct?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface AlarmsConstructProps {
|
|
8
|
+
domainId: string;
|
|
9
|
+
envCode: string;
|
|
10
|
+
endpointId: string;
|
|
11
|
+
primitiveClass: string;
|
|
12
|
+
alarmTopicArn: string;
|
|
13
|
+
thresholds?: AlarmThresholds;
|
|
14
|
+
}
|
|
15
|
+
export declare class AlarmsConstruct extends Construct {
|
|
16
|
+
constructor(scope: Construct, id: string, props: AlarmsConstructProps);
|
|
17
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as cdk from 'aws-cdk-lib';
|
|
2
|
+
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
|
|
3
|
+
import * as sns from 'aws-cdk-lib/aws-sns';
|
|
4
|
+
import { Construct } from 'constructs';
|
|
5
|
+
export class AlarmsConstruct extends Construct {
|
|
6
|
+
constructor(scope, id, props) {
|
|
7
|
+
super(scope, id);
|
|
8
|
+
const { domainId, envCode, endpointId, primitiveClass, alarmTopicArn, thresholds = {} } = props;
|
|
9
|
+
const namespace = `TIB/Domain/${domainId}`;
|
|
10
|
+
const dims = { domainId, endpointId, primitiveClass };
|
|
11
|
+
const alarmTopic = sns.Topic.fromTopicArn(scope, `AlarmTopic-${id}`, alarmTopicArn);
|
|
12
|
+
const latencyThreshold = thresholds.latencyP99Ms ?? 2000;
|
|
13
|
+
const errorThreshold = thresholds.errorRatePct ?? 1;
|
|
14
|
+
// p99 latency > threshold (sev3)
|
|
15
|
+
new cloudwatch.Alarm(this, 'LatencyAlarm', {
|
|
16
|
+
alarmName: `TIB-${envCode}-${domainId}-${endpointId}-latency-p99`,
|
|
17
|
+
metric: new cloudwatch.Metric({ namespace, metricName: 'latency_ms', dimensionsMap: dims, statistic: 'p99', period: cdk.Duration.minutes(5) }),
|
|
18
|
+
threshold: latencyThreshold,
|
|
19
|
+
evaluationPeriods: 1,
|
|
20
|
+
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
21
|
+
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
|
|
22
|
+
}).addAlarmAction({ bind: () => ({ alarmActionArn: alarmTopicArn }) });
|
|
23
|
+
// Error count > 0 over 5 min (sev2)
|
|
24
|
+
new cloudwatch.Alarm(this, 'ErrorAlarm', {
|
|
25
|
+
alarmName: `TIB-${envCode}-${domainId}-${endpointId}-errors`,
|
|
26
|
+
metric: new cloudwatch.Metric({ namespace, metricName: 'errors', dimensionsMap: dims, statistic: 'Sum', period: cdk.Duration.minutes(5) }),
|
|
27
|
+
threshold: 0,
|
|
28
|
+
evaluationPeriods: 1,
|
|
29
|
+
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
30
|
+
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
|
|
31
|
+
}).addAlarmAction({ bind: () => ({ alarmActionArn: alarmTopicArn }) });
|
|
32
|
+
// Throttles sustained 5 min (sev2)
|
|
33
|
+
new cloudwatch.Alarm(this, 'ThrottleAlarm', {
|
|
34
|
+
alarmName: `TIB-${envCode}-${domainId}-${endpointId}-throttles`,
|
|
35
|
+
metric: new cloudwatch.Metric({ namespace, metricName: 'throttles', dimensionsMap: dims, statistic: 'Sum', period: cdk.Duration.minutes(5) }),
|
|
36
|
+
threshold: 0,
|
|
37
|
+
evaluationPeriods: 1,
|
|
38
|
+
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
39
|
+
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
|
|
40
|
+
}).addAlarmAction({ bind: () => ({ alarmActionArn: alarmTopicArn }) });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as apigwv2 from 'aws-cdk-lib/aws-apigatewayv2';
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
import { LambdaFactory } from '../lambda-factory.js';
|
|
4
|
+
import { IamPolicyBuilder } from '../iam/iam-policy-builder.js';
|
|
5
|
+
import type { DomainRegistry } from '../registry.js';
|
|
6
|
+
/**
|
|
7
|
+
* Configuration properties for ApiConstruct.
|
|
8
|
+
*/
|
|
9
|
+
export interface ApiConstructProps {
|
|
10
|
+
/** Domain registry containing all API entries. */
|
|
11
|
+
registry: DomainRegistry;
|
|
12
|
+
/** Shared HttpApi to route requests to Lambdas. */
|
|
13
|
+
httpApi: apigwv2.HttpApi;
|
|
14
|
+
/** Factory for creating Lambda functions from registry entries. */
|
|
15
|
+
lambdaFactory: LambdaFactory;
|
|
16
|
+
/** IAM policy builder for granting permissions to Lambda roles. */
|
|
17
|
+
iamBuilder: IamPolicyBuilder;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* CDK Construct that synthesises one Lambda per registered API and wires each to the shared HttpApi.
|
|
21
|
+
*/
|
|
22
|
+
export declare class ApiConstruct extends Construct {
|
|
23
|
+
/**
|
|
24
|
+
* Create a new ApiConstruct instance.
|
|
25
|
+
* @param scope - Parent CDK scope.
|
|
26
|
+
* @param id - Construct identifier.
|
|
27
|
+
* @param props - Construct properties.
|
|
28
|
+
*/
|
|
29
|
+
constructor(scope: Construct, id: string, props: ApiConstructProps);
|
|
30
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as apigwv2 from 'aws-cdk-lib/aws-apigatewayv2';
|
|
2
|
+
import * as apigwv2integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
|
|
3
|
+
import { Construct } from 'constructs';
|
|
4
|
+
/**
|
|
5
|
+
* CDK Construct that synthesises one Lambda per registered API and wires each to the shared HttpApi.
|
|
6
|
+
*/
|
|
7
|
+
export class ApiConstruct extends Construct {
|
|
8
|
+
/**
|
|
9
|
+
* Create a new ApiConstruct instance.
|
|
10
|
+
* @param scope - Parent CDK scope.
|
|
11
|
+
* @param id - Construct identifier.
|
|
12
|
+
* @param props - Construct properties.
|
|
13
|
+
*/
|
|
14
|
+
constructor(scope, id, props) {
|
|
15
|
+
super(scope, id);
|
|
16
|
+
for (const entry of props.registry.apis) {
|
|
17
|
+
const fn = props.lambdaFactory.createFunction(entry, entry.deployment);
|
|
18
|
+
props.iamBuilder.forApi().forEach((statement) => {
|
|
19
|
+
fn.addToRolePolicy(statement);
|
|
20
|
+
});
|
|
21
|
+
props.httpApi.addRoutes({
|
|
22
|
+
path: entry.path,
|
|
23
|
+
methods: [toHttpMethod(entry.method)],
|
|
24
|
+
integration: new apigwv2integrations.HttpLambdaIntegration(`${id}${toPascalCase(entry.id)}Integration`, fn),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Convert kebab-case or snake_case string to PascalCase.
|
|
31
|
+
* @param s - Input string.
|
|
32
|
+
* @returns PascalCase string.
|
|
33
|
+
*/
|
|
34
|
+
function toPascalCase(s) {
|
|
35
|
+
return s
|
|
36
|
+
.split(/[-_]/)
|
|
37
|
+
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
38
|
+
.join('');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Convert HTTP method string to ApiGatewayV2 HttpMethod enum.
|
|
42
|
+
* @param method - HTTP method string (e.g., 'GET', 'POST').
|
|
43
|
+
* @returns HttpMethod enum value.
|
|
44
|
+
*/
|
|
45
|
+
function toHttpMethod(method) {
|
|
46
|
+
switch (method.toUpperCase()) {
|
|
47
|
+
case 'GET':
|
|
48
|
+
return apigwv2.HttpMethod.GET;
|
|
49
|
+
case 'POST':
|
|
50
|
+
return apigwv2.HttpMethod.POST;
|
|
51
|
+
case 'PUT':
|
|
52
|
+
return apigwv2.HttpMethod.PUT;
|
|
53
|
+
case 'PATCH':
|
|
54
|
+
return apigwv2.HttpMethod.PATCH;
|
|
55
|
+
case 'DELETE':
|
|
56
|
+
return apigwv2.HttpMethod.DELETE;
|
|
57
|
+
case 'HEAD':
|
|
58
|
+
return apigwv2.HttpMethod.HEAD;
|
|
59
|
+
case 'OPTIONS':
|
|
60
|
+
return apigwv2.HttpMethod.OPTIONS;
|
|
61
|
+
default:
|
|
62
|
+
return apigwv2.HttpMethod.ANY;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
2
|
+
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
|
|
3
|
+
import { Construct } from 'constructs';
|
|
4
|
+
export interface CanaryConstructProps {
|
|
5
|
+
domainId: string;
|
|
6
|
+
lambdaFunctions: lambda.Function[];
|
|
7
|
+
/** CloudWatch alarms that trigger auto-rollback. Typically from AlarmConstruct. */
|
|
8
|
+
rollbackAlarms?: cloudwatch.Alarm[];
|
|
9
|
+
}
|
|
10
|
+
export declare class CanaryConstruct extends Construct {
|
|
11
|
+
readonly aliases: lambda.Alias[];
|
|
12
|
+
constructor(scope: Construct, id: string, props: CanaryConstructProps);
|
|
13
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
2
|
+
import * as codedeploy from 'aws-cdk-lib/aws-codedeploy';
|
|
3
|
+
import { Construct } from 'constructs';
|
|
4
|
+
export class CanaryConstruct extends Construct {
|
|
5
|
+
aliases = [];
|
|
6
|
+
constructor(scope, id, props) {
|
|
7
|
+
super(scope, id);
|
|
8
|
+
const app = new codedeploy.LambdaApplication(this, 'DeployApp', {
|
|
9
|
+
applicationName: `tib-${props.domainId}`,
|
|
10
|
+
});
|
|
11
|
+
for (const fn of props.lambdaFunctions) {
|
|
12
|
+
const alias = new lambda.Alias(this, `${fn.node.id}LiveAlias`, {
|
|
13
|
+
aliasName: 'live',
|
|
14
|
+
version: fn.currentVersion,
|
|
15
|
+
});
|
|
16
|
+
this.aliases.push(alias);
|
|
17
|
+
new codedeploy.LambdaDeploymentGroup(this, `${fn.node.id}DeployGroup`, {
|
|
18
|
+
application: app,
|
|
19
|
+
alias,
|
|
20
|
+
deploymentConfig: codedeploy.LambdaDeploymentConfig.CANARY_10PERCENT_5MINUTES,
|
|
21
|
+
alarms: props.rollbackAlarms ?? [],
|
|
22
|
+
autoRollback: {
|
|
23
|
+
failedDeployment: true,
|
|
24
|
+
stoppedDeployment: true,
|
|
25
|
+
deploymentInAlarm: true,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as kms from 'aws-cdk-lib/aws-kms';
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
export interface CmkConstructProps {
|
|
4
|
+
domainId: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class CmkConstruct extends Construct {
|
|
7
|
+
readonly key: kms.Key;
|
|
8
|
+
constructor(scope: Construct, id: string, props: CmkConstructProps);
|
|
9
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as cdk from 'aws-cdk-lib';
|
|
2
|
+
import * as kms from 'aws-cdk-lib/aws-kms';
|
|
3
|
+
import { Construct } from 'constructs';
|
|
4
|
+
export class CmkConstruct extends Construct {
|
|
5
|
+
key;
|
|
6
|
+
constructor(scope, id, props) {
|
|
7
|
+
super(scope, id);
|
|
8
|
+
this.key = new kms.Key(this, 'DomainKey', {
|
|
9
|
+
alias: `tib/${props.domainId}`,
|
|
10
|
+
description: `TIB domain encryption key for ${props.domainId}`,
|
|
11
|
+
enableKeyRotation: true,
|
|
12
|
+
pendingWindow: cdk.Duration.days(30),
|
|
13
|
+
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
|
14
|
+
});
|
|
15
|
+
new cdk.CfnOutput(this, 'DomainKeyArn', {
|
|
16
|
+
value: this.key.keyArn,
|
|
17
|
+
description: `CMK ARN for domain ${props.domainId}`,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|