@fjall/components-infrastructure 0.94.1 → 0.96.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/dist/lib/app.d.ts +25 -109
- package/dist/lib/app.js +37 -136
- package/dist/lib/patterns/aws/account.js +5 -4
- package/dist/lib/patterns/aws/computeEcs.d.ts +8 -397
- package/dist/lib/patterns/aws/computeEcs.js +13 -9
- package/dist/lib/patterns/aws/computeEcsTypes.d.ts +386 -0
- package/dist/lib/patterns/aws/computeEcsTypes.js +2 -0
- package/dist/lib/patterns/aws/domain.js +4 -5
- package/dist/lib/patterns/aws/index.d.ts +2 -0
- package/dist/lib/patterns/aws/index.js +2 -0
- package/dist/lib/patterns/aws/interfaces/compute.d.ts +6 -0
- package/dist/lib/patterns/aws/interfaces/connector.d.ts +1 -1
- package/dist/lib/patterns/aws/interfaces/connector.js +1 -1
- package/dist/lib/patterns/aws/interfaces/index.d.ts +2 -1
- package/dist/lib/patterns/aws/interfaces/index.js +1 -1
- package/dist/lib/patterns/aws/interfaces/vpcPeer.d.ts +7 -0
- package/dist/lib/patterns/aws/interfaces/vpcPeer.js +1 -0
- package/dist/lib/patterns/aws/organisation.js +2 -1
- package/dist/lib/patterns/aws/vpcPeer.d.ts +34 -0
- package/dist/lib/patterns/aws/vpcPeer.js +36 -0
- package/dist/lib/patterns/aws/vpcPeerAccepter.d.ts +29 -0
- package/dist/lib/patterns/aws/vpcPeerAccepter.js +196 -0
- package/dist/lib/resources/aws/analytics/clickhouse.js +10 -1
- package/dist/lib/resources/aws/analytics/clickhouseAlarms.d.ts +34 -0
- package/dist/lib/resources/aws/analytics/clickhouseAlarms.js +89 -0
- package/dist/lib/resources/aws/analytics/clickhouseConstants.d.ts +1 -1
- package/dist/lib/resources/aws/analytics/clickhouseConstants.js +3 -1
- package/dist/lib/resources/aws/analytics/clickhouseTypes.d.ts +6 -0
- package/dist/lib/resources/aws/analytics/clickhouseUserData.d.ts +1 -0
- package/dist/lib/resources/aws/analytics/clickhouseUserData.js +3 -2
- package/dist/lib/resources/aws/analytics/index.d.ts +2 -0
- package/dist/lib/resources/aws/analytics/index.js +1 -0
- package/dist/lib/resources/aws/compute/ecsRemoteConnections.d.ts +38 -0
- package/dist/lib/resources/aws/compute/ecsRemoteConnections.js +80 -0
- package/dist/lib/resources/aws/compute/ecsTaskDefinition.js +8 -0
- package/dist/lib/resources/aws/compute/ecsTypes.d.ts +7 -0
- package/dist/lib/resources/aws/iam/delegationRole.js +11 -4
- package/dist/lib/resources/aws/networking/crossAccountDelegationRecord.js +2 -1
- package/dist/lib/resources/aws/networking/crossAccountReturnRoutes.d.ts +40 -0
- package/dist/lib/resources/aws/networking/crossAccountReturnRoutes.js +154 -0
- package/dist/lib/resources/aws/networking/dnsRecord/dnsRecordBase.js +2 -1
- package/dist/lib/resources/aws/networking/domainCertificate.js +2 -1
- package/dist/lib/resources/aws/networking/hostedZone.js +2 -1
- package/dist/lib/resources/aws/networking/index.d.ts +3 -0
- package/dist/lib/resources/aws/networking/index.js +3 -0
- package/dist/lib/resources/aws/networking/vpc.js +6 -2
- package/dist/lib/resources/aws/networking/vpcPeeringAccepterRole.d.ts +18 -0
- package/dist/lib/resources/aws/networking/vpcPeeringAccepterRole.js +61 -0
- package/dist/lib/resources/aws/networking/vpcPeeringConnection.d.ts +49 -0
- package/dist/lib/resources/aws/networking/vpcPeeringConnection.js +88 -0
- package/dist/lib/utils/bastionFactory.d.ts +10 -0
- package/dist/lib/utils/bastionFactory.js +29 -0
- package/dist/lib/utils/capitaliseString.d.ts +1 -1
- package/dist/lib/utils/capitaliseString.js +1 -1
- package/dist/lib/utils/cdkContext.d.ts +8 -0
- package/dist/lib/utils/cdkContext.js +11 -0
- package/dist/lib/utils/connections.d.ts +7 -1
- package/dist/lib/utils/connections.js +15 -0
- package/dist/lib/utils/connector.d.ts +18 -2
- package/dist/lib/utils/connector.js +6 -1
- package/dist/lib/utils/costAllocationTags.d.ts +6 -0
- package/dist/lib/utils/costAllocationTags.js +6 -0
- package/dist/lib/utils/index.d.ts +3 -0
- package/dist/lib/utils/index.js +3 -0
- package/dist/lib/utils/vpcPeerInterface.d.ts +22 -0
- package/dist/lib/utils/vpcPeerInterface.js +1 -0
- package/package.json +4 -3
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { Annotations, Stack } from "aws-cdk-lib";
|
|
2
|
+
import { Peer, Port, SubnetType } from "aws-cdk-lib/aws-ec2";
|
|
3
|
+
import { CfnResourcePolicy, StringListParameter, StringParameter } from "aws-cdk-lib/aws-ssm";
|
|
4
|
+
import { CIDR_REGEX, VALIDATION_PATTERNS } from "@fjall/generator";
|
|
5
|
+
import { buildSsmPrefix, DEFAULT_ORG_ID, resolveOrgId } from "../../utils/cdkContext.js";
|
|
6
|
+
import { VpcPeeringAccepterRole } from "../../resources/aws/networking/vpcPeeringAccepterRole.js";
|
|
7
|
+
import { isDatabase, isRelationalDatabase } from "./interfaces/database.js";
|
|
8
|
+
import { isCompute, isEcsCompute } from "./interfaces/compute.js";
|
|
9
|
+
export class VpcPeerAccepterFactory {
|
|
10
|
+
static build(id, props) {
|
|
11
|
+
return (app, scope) => {
|
|
12
|
+
if (props.requesterAccountIds.length === 0) {
|
|
13
|
+
throw new Error("VpcPeerAccepterFactory requires at least one requester account ID.");
|
|
14
|
+
}
|
|
15
|
+
for (const accountId of props.requesterAccountIds) {
|
|
16
|
+
if (!VALIDATION_PATTERNS.AWS_ACCOUNT_ID.test(accountId)) {
|
|
17
|
+
throw new Error(`Invalid requester account ID "${accountId}". Must be a 12-digit AWS account ID.`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const localVpc = app.getVpc(props.localVpcName);
|
|
21
|
+
const accepterRole = new VpcPeeringAccepterRole(scope, id, {
|
|
22
|
+
requesterAccountIds: props.requesterAccountIds,
|
|
23
|
+
localVpc,
|
|
24
|
+
costAllocationEnvironment: props.costAllocationEnvironment,
|
|
25
|
+
costAllocationDomain: props.costAllocationDomain
|
|
26
|
+
});
|
|
27
|
+
const orgId = resolveOrgId(app.node, DEFAULT_ORG_ID);
|
|
28
|
+
const ssmPrefix = buildSsmPrefix(orgId, app.getName());
|
|
29
|
+
const publishVpcMetadata = props.publishToSsm ?? true;
|
|
30
|
+
const vpcMetadataParams = [];
|
|
31
|
+
if (publishVpcMetadata) {
|
|
32
|
+
const vpcIdParam = new StringParameter(scope, `${id}VpcIdParam`, {
|
|
33
|
+
parameterName: `${ssmPrefix}/vpc-id`,
|
|
34
|
+
stringValue: localVpc.vpcId
|
|
35
|
+
});
|
|
36
|
+
const vpcCidrParam = new StringParameter(scope, `${id}VpcCidrParam`, {
|
|
37
|
+
parameterName: `${ssmPrefix}/vpc-cidr`,
|
|
38
|
+
stringValue: localVpc.vpcCidrBlock
|
|
39
|
+
});
|
|
40
|
+
const roleArnParam = new StringParameter(scope, `${id}PeeringRoleArnParam`, {
|
|
41
|
+
parameterName: `${ssmPrefix}/peering-role-arn`,
|
|
42
|
+
stringValue: accepterRole.roleArn
|
|
43
|
+
});
|
|
44
|
+
const privateSubnets = localVpc.selectSubnets({
|
|
45
|
+
subnetType: SubnetType.PRIVATE_WITH_EGRESS
|
|
46
|
+
});
|
|
47
|
+
const routeTableIds = privateSubnets.subnets.map((s) => s.routeTable.routeTableId);
|
|
48
|
+
const routeTableIdsParam = new StringListParameter(scope, `${id}RouteTableIdsParam`, {
|
|
49
|
+
parameterName: `${ssmPrefix}/route-table-ids`,
|
|
50
|
+
stringListValue: routeTableIds
|
|
51
|
+
});
|
|
52
|
+
vpcMetadataParams.push(vpcIdParam, vpcCidrParam, roleArnParam, routeTableIdsParam);
|
|
53
|
+
}
|
|
54
|
+
if (vpcMetadataParams.length > 0) {
|
|
55
|
+
attachCrossAccountReadPolicy(scope, id, props.requesterAccountIds, vpcMetadataParams);
|
|
56
|
+
}
|
|
57
|
+
applyExposedResources(scope, id, ssmPrefix, props.requesterAccountIds, props.exposedResources ?? []);
|
|
58
|
+
return accepterRole;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function applyExposedResources(accepterScope, id, ssmPrefix, requesterAccountIds, entries) {
|
|
63
|
+
const seenNames = new Set();
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
if (seenNames.has(entry.name)) {
|
|
66
|
+
throw new Error(`Duplicate exposedResource name '${entry.name}' — each exposed resource must have a unique name.`);
|
|
67
|
+
}
|
|
68
|
+
seenNames.add(entry.name);
|
|
69
|
+
for (const cidr of entry.allowedFromCidrs) {
|
|
70
|
+
if (!CIDR_REGEX.test(cidr)) {
|
|
71
|
+
throw new Error(`exposedResource '${entry.name}' has malformed CIDR '${cidr}' — expected dotted-quad/prefix-length form (e.g. 10.0.0.0/16).`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const resourcePrefix = `${ssmPrefix}/resources/${entry.name}`;
|
|
75
|
+
const constructPrefix = `${id}Exposed${entry.name}`;
|
|
76
|
+
const { params, scope: resourceScope } = resolveExposedResource(accepterScope, constructPrefix, resourcePrefix, entry);
|
|
77
|
+
if (params.length > 0) {
|
|
78
|
+
attachCrossAccountReadPolicy(resourceScope, constructPrefix, requesterAccountIds, params);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function resolveExposedResource(accepterScope, constructPrefix, resourcePrefix, entry) {
|
|
83
|
+
if ("serviceName" in entry) {
|
|
84
|
+
const resource = entry.resource;
|
|
85
|
+
if (!isCompute(resource) || !isEcsCompute(resource)) {
|
|
86
|
+
throw new Error(`exposedResource '${entry.name}' carries 'serviceName' (ECS variant) but its resource is not IEcsCompute.`);
|
|
87
|
+
}
|
|
88
|
+
const scope = stackOfNode(resource.node);
|
|
89
|
+
const params = applyEcsServiceExposure(scope, constructPrefix, resourcePrefix, entry);
|
|
90
|
+
return { params, scope };
|
|
91
|
+
}
|
|
92
|
+
const resource = entry.resource;
|
|
93
|
+
if (!isDatabase(resource) || !isRelationalDatabase(resource)) {
|
|
94
|
+
throw new Error(`exposedResource '${entry.name}' has no 'serviceName' (database variant) but its resource is not IRelationalDatabase.`);
|
|
95
|
+
}
|
|
96
|
+
const scope = stackOfNode(resource.node);
|
|
97
|
+
const params = applyDatabaseExposure(scope, constructPrefix, resourcePrefix, {
|
|
98
|
+
...entry,
|
|
99
|
+
resource
|
|
100
|
+
});
|
|
101
|
+
return { params, scope };
|
|
102
|
+
}
|
|
103
|
+
function applyDatabaseExposure(scope, constructPrefix, resourcePrefix, entry) {
|
|
104
|
+
const port = parseInt(entry.resource.getHostPort(), 10);
|
|
105
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
106
|
+
throw new Error(`exposedResource '${entry.name}' resolved an out-of-range port from getHostPort() (must be 1-65535); cannot open ingress.`);
|
|
107
|
+
}
|
|
108
|
+
if (entry.allowedFromCidrs.length === 0) {
|
|
109
|
+
Annotations.of(scope).addWarning(`exposedResource '${entry.name}' has no allowed CIDRs; consumers will not be able to reach it.`);
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
for (const cidr of entry.allowedFromCidrs) {
|
|
113
|
+
entry.resource.connections.allowFrom(Peer.ipv4(cidr), Port.tcp(port), `fjall:peer:${cidr}`);
|
|
114
|
+
}
|
|
115
|
+
return publishExposedResourceParams(scope, constructPrefix, resourcePrefix, {
|
|
116
|
+
kind: "relational-db",
|
|
117
|
+
endpoint: entry.resource.getHostEndpoint(),
|
|
118
|
+
port: String(port),
|
|
119
|
+
access: entry.access
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function applyEcsServiceExposure(scope, constructPrefix, resourcePrefix, entry) {
|
|
123
|
+
const loadBalancer = entry.resource.getLoadBalancer();
|
|
124
|
+
if (!loadBalancer) {
|
|
125
|
+
throw new Error(`exposedResource '${entry.name}' targets ECS service '${entry.serviceName}' but the cluster has no load balancer (cluster.loadBalancer: false). Enable an internal ALB to expose this service.`);
|
|
126
|
+
}
|
|
127
|
+
const resolvedListenerPort = entry.port === undefined
|
|
128
|
+
? entry.resource.getPrimaryListenerPort()
|
|
129
|
+
: undefined;
|
|
130
|
+
const port = entry.port ?? resolvedListenerPort ?? 443;
|
|
131
|
+
if (entry.port === undefined && resolvedListenerPort !== undefined) {
|
|
132
|
+
Annotations.of(scope).addInfo(`exposedResource '${entry.name}' defaulted to listener port ${resolvedListenerPort}`);
|
|
133
|
+
}
|
|
134
|
+
if (entry.allowedFromCidrs.length === 0) {
|
|
135
|
+
Annotations.of(scope).addWarning(`exposedResource '${entry.name}' has no allowed CIDRs; consumers will not be able to reach it.`);
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
for (const cidr of entry.allowedFromCidrs) {
|
|
139
|
+
loadBalancer.connections.allowFrom(Peer.ipv4(cidr), Port.tcp(port), `fjall:peer:${cidr}`);
|
|
140
|
+
}
|
|
141
|
+
return publishExposedResourceParams(scope, constructPrefix, resourcePrefix, {
|
|
142
|
+
kind: "ecs-service",
|
|
143
|
+
endpoint: loadBalancer.loadBalancerDnsName,
|
|
144
|
+
port: String(port)
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function publishExposedResourceParams(scope, constructPrefix, resourcePrefix, values) {
|
|
148
|
+
const kindParam = new StringParameter(scope, `${constructPrefix}KindParam`, {
|
|
149
|
+
parameterName: `${resourcePrefix}/kind`,
|
|
150
|
+
stringValue: values.kind
|
|
151
|
+
});
|
|
152
|
+
const endpointParam = new StringParameter(scope, `${constructPrefix}EndpointParam`, {
|
|
153
|
+
parameterName: `${resourcePrefix}/endpoint`,
|
|
154
|
+
stringValue: values.endpoint
|
|
155
|
+
});
|
|
156
|
+
const portParam = new StringParameter(scope, `${constructPrefix}PortParam`, {
|
|
157
|
+
parameterName: `${resourcePrefix}/port`,
|
|
158
|
+
stringValue: values.port
|
|
159
|
+
});
|
|
160
|
+
const params = [kindParam, endpointParam, portParam];
|
|
161
|
+
if (values.access !== undefined) {
|
|
162
|
+
const accessParam = new StringParameter(scope, `${constructPrefix}AccessParam`, {
|
|
163
|
+
parameterName: `${resourcePrefix}/access`,
|
|
164
|
+
stringValue: values.access
|
|
165
|
+
});
|
|
166
|
+
params.push(accessParam);
|
|
167
|
+
}
|
|
168
|
+
return params;
|
|
169
|
+
}
|
|
170
|
+
function attachCrossAccountReadPolicy(scope, id, requesterAccountIds, parameters) {
|
|
171
|
+
const partition = Stack.of(scope).partition;
|
|
172
|
+
const principals = requesterAccountIds.map((accountId) => `arn:${partition}:iam::${accountId}:root`);
|
|
173
|
+
for (const [index, parameter] of parameters.entries()) {
|
|
174
|
+
new CfnResourcePolicy(scope, `${id}ReadPolicy${index}`, {
|
|
175
|
+
resourceArn: parameter.parameterArn,
|
|
176
|
+
policy: {
|
|
177
|
+
Version: "2012-10-17",
|
|
178
|
+
Statement: [
|
|
179
|
+
{
|
|
180
|
+
Effect: "Allow",
|
|
181
|
+
Principal: { AWS: principals },
|
|
182
|
+
Action: ["ssm:GetParameter"],
|
|
183
|
+
Resource: parameter.parameterArn
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function stackOfNode(node) {
|
|
191
|
+
const stack = node.scopes.find((a) => Stack.isStack(a));
|
|
192
|
+
if (!stack) {
|
|
193
|
+
throw new Error("exposedResource resource is not bound to a CDK Stack — cannot derive scope for SSM publishing.");
|
|
194
|
+
}
|
|
195
|
+
return stack;
|
|
196
|
+
}
|
|
@@ -12,6 +12,7 @@ import { vpcHasNatGateways } from "../../../utils/vpcUtils.js";
|
|
|
12
12
|
import { inferAmiHardwareType } from "../compute/ecsConstants.js";
|
|
13
13
|
import { createClickHouseSecurityGroup } from "./clickhouseSecurityGroup.js";
|
|
14
14
|
import { generateClickHouseUserData } from "./clickhouseUserData.js";
|
|
15
|
+
import { createClickHouseAlarms } from "./clickhouseAlarms.js";
|
|
15
16
|
import { CLICKHOUSE_CLUSTER_NAME, DEFAULT_CLICKHOUSE_INSTANCE_TYPE, CLICKHOUSE_IMAGE, CLICKHOUSE_EBS_VOLUME_SIZE_GB, CLICKHOUSE_EBS_IOPS, CLICKHOUSE_EBS_THROUGHPUT_MBPS, CLICKHOUSE_TASK_MEMORY_MIB, CLICKHOUSE_TASK_CPU_UNITS, CLICKHOUSE_HTTP_PORT, CLICKHOUSE_NATIVE_PORT, CLICKHOUSE_PROMETHEUS_PORT, CLICKHOUSE_DATA_MOUNT_PATH, CLICKHOUSE_SECRETS_PREFIX, CLICKHOUSE_SECRET_NAMES, CLICKHOUSE_SECRET_OPTIONS, CLICKHOUSE_HEALTH_CHECK, CLICKHOUSE_EBS_DEVICE_NAME, CLICKHOUSE_CONFIG_SUBDIR, CLICKHOUSE_USERS_SUBDIR, OPTIMISE_FINAL_SCHEDULE, REPLACING_MERGE_TREE_TABLES, OPTIMISE_MV_TABLES, CLICKHOUSE_CLOUDMAP_NAMESPACE, CLICKHOUSE_CLOUDMAP_SERVICE_NAME, OPTIMISE_TASK_MEMORY_MIB, OPTIMISE_TASK_CPU_UNITS, BACKUP_SCHEDULE, BACKUP_TASK_MEMORY_MIB, BACKUP_TASK_CPU_UNITS, BACKUP_RETENTION_DAYS } from "./clickhouseConstants.js";
|
|
16
17
|
function createClickHouseSecret(scope, id, secretKey, description) {
|
|
17
18
|
return new Secret(scope, id, {
|
|
@@ -273,7 +274,15 @@ export default class ClickHouse extends Construct {
|
|
|
273
274
|
auditPasswordSecret.secret.grantRead(executionRole);
|
|
274
275
|
backupPasswordSecret.secret.grantRead(executionRole);
|
|
275
276
|
schemaPasswordSecret.secret.grantRead(executionRole);
|
|
276
|
-
// 14.
|
|
277
|
+
// 14. CloudWatch alarms (CPU, memory, disk) — wired only when an SNS topic is supplied
|
|
278
|
+
if (props.alarmTopic) {
|
|
279
|
+
createClickHouseAlarms({
|
|
280
|
+
scope: this,
|
|
281
|
+
asg,
|
|
282
|
+
alarmTopic: props.alarmTopic
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
// 15. Connections and outputs
|
|
277
286
|
this.connections = new Connections({
|
|
278
287
|
securityGroups: [securityGroup],
|
|
279
288
|
defaultPort: Port.tcp(CLICKHOUSE_HTTP_PORT)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Alarm } from "aws-cdk-lib/aws-cloudwatch";
|
|
2
|
+
import type { AutoScalingGroup } from "aws-cdk-lib/aws-autoscaling";
|
|
3
|
+
import type { ITopic } from "aws-cdk-lib/aws-sns";
|
|
4
|
+
import type { Construct } from "constructs";
|
|
5
|
+
export interface ClickHouseAlarmThresholds {
|
|
6
|
+
/** EC2 host CPU % over 5 min. Default 90. */
|
|
7
|
+
cpuThreshold?: number;
|
|
8
|
+
/** EC2 host memory % over 5 min (requires CWAgent). Default 80. */
|
|
9
|
+
memoryThreshold?: number;
|
|
10
|
+
/** EBS root-volume disk % used. Default 70 (warn) — paired with critical at 85. */
|
|
11
|
+
diskWarnThreshold?: number;
|
|
12
|
+
/** EBS root-volume disk % used. Default 85. */
|
|
13
|
+
diskCriticalThreshold?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface ClickHouseAlarmsProps {
|
|
16
|
+
scope: Construct;
|
|
17
|
+
asg: AutoScalingGroup;
|
|
18
|
+
alarmTopic: ITopic;
|
|
19
|
+
config?: ClickHouseAlarmThresholds;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Single-node ClickHouse posture alarms. Covers host-level CPU + (optional)
|
|
23
|
+
* memory and disk via the CloudWatch Agent metric namespace `CWAgent`.
|
|
24
|
+
*
|
|
25
|
+
* Engine-level alarms (parts count, failed inserts) require the Prometheus
|
|
26
|
+
* exporter to be scraped into CW custom metrics. The Prometheus endpoint is
|
|
27
|
+
* enabled in the user-data XML (port 9363); the scraper is a follow-up.
|
|
28
|
+
*
|
|
29
|
+
* Stuck merges are detected app-side: `client.ts` queries `system.merges`
|
|
30
|
+
* every 5 min and logs `serverLogger.warn("ClickHouse", "Stuck merge detected")`
|
|
31
|
+
* when elapsed > 30 min. A CW Logs metric filter on that pattern + alarm
|
|
32
|
+
* completes the circuit once the webapp log group ARN is available here.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createClickHouseAlarms(props: ClickHouseAlarmsProps): Alarm[];
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Duration } from "aws-cdk-lib";
|
|
2
|
+
import { Alarm, ComparisonOperator, TreatMissingData } from "aws-cdk-lib/aws-cloudwatch";
|
|
3
|
+
import { SnsAction } from "aws-cdk-lib/aws-cloudwatch-actions";
|
|
4
|
+
import { Metric } from "aws-cdk-lib/aws-cloudwatch";
|
|
5
|
+
import { ALARM_DEFAULTS, registerAlarm, buildAlarmDescription } from "../monitoring/alarmDefaults.js";
|
|
6
|
+
/**
|
|
7
|
+
* Single-node ClickHouse posture alarms. Covers host-level CPU + (optional)
|
|
8
|
+
* memory and disk via the CloudWatch Agent metric namespace `CWAgent`.
|
|
9
|
+
*
|
|
10
|
+
* Engine-level alarms (parts count, failed inserts) require the Prometheus
|
|
11
|
+
* exporter to be scraped into CW custom metrics. The Prometheus endpoint is
|
|
12
|
+
* enabled in the user-data XML (port 9363); the scraper is a follow-up.
|
|
13
|
+
*
|
|
14
|
+
* Stuck merges are detected app-side: `client.ts` queries `system.merges`
|
|
15
|
+
* every 5 min and logs `serverLogger.warn("ClickHouse", "Stuck merge detected")`
|
|
16
|
+
* when elapsed > 30 min. A CW Logs metric filter on that pattern + alarm
|
|
17
|
+
* completes the circuit once the webapp log group ARN is available here.
|
|
18
|
+
*/
|
|
19
|
+
export function createClickHouseAlarms(props) {
|
|
20
|
+
const { scope, asg, alarmTopic, config = {} } = props;
|
|
21
|
+
const alarms = [];
|
|
22
|
+
const snsAction = new SnsAction(alarmTopic);
|
|
23
|
+
const asgName = asg.autoScalingGroupName;
|
|
24
|
+
const cpuAlarm = new Alarm(scope, "ClickHouseCpuAlarm", {
|
|
25
|
+
alarmDescription: buildAlarmDescription("ClickHouse host CPU utilisation exceeds threshold", undefined),
|
|
26
|
+
metric: new Metric({
|
|
27
|
+
namespace: "AWS/EC2",
|
|
28
|
+
metricName: "CPUUtilization",
|
|
29
|
+
dimensionsMap: { AutoScalingGroupName: asgName },
|
|
30
|
+
period: ALARM_DEFAULTS.EVALUATION_PERIOD,
|
|
31
|
+
statistic: "Average"
|
|
32
|
+
}),
|
|
33
|
+
threshold: config.cpuThreshold ?? 90,
|
|
34
|
+
evaluationPeriods: 3,
|
|
35
|
+
datapointsToAlarm: 2,
|
|
36
|
+
comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
37
|
+
treatMissingData: TreatMissingData.NOT_BREACHING
|
|
38
|
+
});
|
|
39
|
+
registerAlarm(cpuAlarm, snsAction, alarms);
|
|
40
|
+
const memoryAlarm = new Alarm(scope, "ClickHouseMemoryAlarm", {
|
|
41
|
+
alarmDescription: buildAlarmDescription("ClickHouse host memory utilisation exceeds threshold (CWAgent)", undefined),
|
|
42
|
+
metric: new Metric({
|
|
43
|
+
namespace: "CWAgent",
|
|
44
|
+
metricName: "mem_used_percent",
|
|
45
|
+
dimensionsMap: { AutoScalingGroupName: asgName },
|
|
46
|
+
period: ALARM_DEFAULTS.EVALUATION_PERIOD,
|
|
47
|
+
statistic: "Average"
|
|
48
|
+
}),
|
|
49
|
+
threshold: config.memoryThreshold ?? 80,
|
|
50
|
+
evaluationPeriods: 3,
|
|
51
|
+
datapointsToAlarm: 2,
|
|
52
|
+
comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
53
|
+
treatMissingData: TreatMissingData.NOT_BREACHING
|
|
54
|
+
});
|
|
55
|
+
registerAlarm(memoryAlarm, snsAction, alarms);
|
|
56
|
+
const diskWarnAlarm = new Alarm(scope, "ClickHouseDiskWarnAlarm", {
|
|
57
|
+
alarmDescription: buildAlarmDescription("ClickHouse data volume above 70% used — plan growth response", undefined),
|
|
58
|
+
metric: new Metric({
|
|
59
|
+
namespace: "CWAgent",
|
|
60
|
+
metricName: "disk_used_percent",
|
|
61
|
+
dimensionsMap: { AutoScalingGroupName: asgName },
|
|
62
|
+
period: Duration.minutes(15),
|
|
63
|
+
statistic: "Average"
|
|
64
|
+
}),
|
|
65
|
+
threshold: config.diskWarnThreshold ?? 70,
|
|
66
|
+
evaluationPeriods: 2,
|
|
67
|
+
datapointsToAlarm: 2,
|
|
68
|
+
comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
69
|
+
treatMissingData: TreatMissingData.NOT_BREACHING
|
|
70
|
+
});
|
|
71
|
+
registerAlarm(diskWarnAlarm, snsAction, alarms);
|
|
72
|
+
const diskCriticalAlarm = new Alarm(scope, "ClickHouseDiskCriticalAlarm", {
|
|
73
|
+
alarmDescription: buildAlarmDescription("ClickHouse data volume above 85% used — imminent insert failures", undefined),
|
|
74
|
+
metric: new Metric({
|
|
75
|
+
namespace: "CWAgent",
|
|
76
|
+
metricName: "disk_used_percent",
|
|
77
|
+
dimensionsMap: { AutoScalingGroupName: asgName },
|
|
78
|
+
period: Duration.minutes(5),
|
|
79
|
+
statistic: "Average"
|
|
80
|
+
}),
|
|
81
|
+
threshold: config.diskCriticalThreshold ?? 85,
|
|
82
|
+
evaluationPeriods: 2,
|
|
83
|
+
datapointsToAlarm: 2,
|
|
84
|
+
comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
85
|
+
treatMissingData: TreatMissingData.NOT_BREACHING
|
|
86
|
+
});
|
|
87
|
+
registerAlarm(diskCriticalAlarm, snsAction, alarms);
|
|
88
|
+
return alarms;
|
|
89
|
+
}
|
|
@@ -60,7 +60,7 @@ export declare const CLICKHOUSE_CLOUDMAP_SERVICE_NAME = "clickhouse";
|
|
|
60
60
|
/** Materialised views that benefit from periodic OPTIMIZE to reduce part count at read time.
|
|
61
61
|
* These are not ReplacingMergeTree (no dedup needed) but un-merged parts force
|
|
62
62
|
* read-time aggregation which degrades query performance. */
|
|
63
|
-
export declare const OPTIMISE_MV_TABLES: readonly ["metrics_hourly_mv", "metrics_daily_mv", "response_time_quantiles_hourly_mv", "deployment_duration_quantiles_daily_mv", "log_severity_hourly_mv", "compliance_score_daily_mv", "ai_usage_daily_mv"];
|
|
63
|
+
export declare const OPTIMISE_MV_TABLES: readonly ["metrics_hourly_mv", "metrics_daily_mv", "response_time_quantiles_hourly_mv", "deployment_duration_quantiles_daily_mv", "log_severity_hourly_mv", "compliance_score_daily_mv", "ai_usage_daily_mv", "finding_daily_aggregate", "insight_pattern_dismissals"];
|
|
64
64
|
/** Resource allocation for the lightweight optimise task. */
|
|
65
65
|
export declare const OPTIMISE_TASK_MEMORY_MIB = 256;
|
|
66
66
|
export declare const OPTIMISE_TASK_CPU_UNITS = 256;
|
|
@@ -73,7 +73,9 @@ export const OPTIMISE_MV_TABLES = [
|
|
|
73
73
|
"deployment_duration_quantiles_daily_mv",
|
|
74
74
|
"log_severity_hourly_mv",
|
|
75
75
|
"compliance_score_daily_mv",
|
|
76
|
-
"ai_usage_daily_mv"
|
|
76
|
+
"ai_usage_daily_mv",
|
|
77
|
+
"finding_daily_aggregate",
|
|
78
|
+
"insight_pattern_dismissals"
|
|
77
79
|
];
|
|
78
80
|
/** Resource allocation for the lightweight optimise task. */
|
|
79
81
|
export const OPTIMISE_TASK_MEMORY_MIB = 256;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { IVpc, ISecurityGroup } from "aws-cdk-lib/aws-ec2";
|
|
2
2
|
import type { IBucket } from "aws-cdk-lib/aws-s3";
|
|
3
3
|
import type { ISecret } from "aws-cdk-lib/aws-secretsmanager";
|
|
4
|
+
import type { ITopic } from "aws-cdk-lib/aws-sns";
|
|
4
5
|
/** Props for the ClickHouse CDK construct. */
|
|
5
6
|
export interface ClickHouseProps {
|
|
6
7
|
/** VPC to deploy into. */
|
|
@@ -21,6 +22,11 @@ export interface ClickHouseProps {
|
|
|
21
22
|
* If omitted, tiered storage is disabled (local-only).
|
|
22
23
|
*/
|
|
23
24
|
r2Config?: ClickHouseR2Config;
|
|
25
|
+
/**
|
|
26
|
+
* SNS topic for CloudWatch alarms (CPU, memory, disk).
|
|
27
|
+
* If omitted, posture alarms are not created.
|
|
28
|
+
*/
|
|
29
|
+
alarmTopic?: ITopic;
|
|
24
30
|
}
|
|
25
31
|
/** Cloudflare R2 configuration for tiered storage and backups. */
|
|
26
32
|
export interface ClickHouseR2Config {
|
|
@@ -2,4 +2,5 @@ export interface ClickHouseUserDataOptions {
|
|
|
2
2
|
/** Cloudflare account ID for R2 cold storage. If omitted, local-only storage is used. */
|
|
3
3
|
cfAccountId?: string;
|
|
4
4
|
}
|
|
5
|
+
export declare const USERS_CONFIG_XML = "<clickhouse>\n <users>\n <default>\n <networks>\n <ip>127.0.0.1</ip>\n <ip>::1</ip>\n </networks>\n </default>\n </users>\n <profiles>\n <default>\n <optimize_move_to_prewhere>1</optimize_move_to_prewhere>\n </default>\n <app_writer>\n <max_threads>2</max_threads>\n <max_insert_threads>1</max_insert_threads>\n <max_concurrent_queries_for_user>4</max_concurrent_queries_for_user>\n <log_queries_min_query_duration_ms>100</log_queries_min_query_duration_ms>\n <optimize_move_to_prewhere>1</optimize_move_to_prewhere>\n <use_query_condition_cache>1</use_query_condition_cache>\n <!-- Re-enable skip indexes under FINAL (tenantQuery auto-FINALs RMT tables;\n default disables idx_aws_account, idx_application, idx_dedup, idx_fingerprint). -->\n <use_skip_indexes_if_final>1</use_skip_indexes_if_final>\n <async_insert>1</async_insert>\n <wait_for_async_insert>1</wait_for_async_insert>\n <async_insert_max_data_size>10000000</async_insert_max_data_size>\n <!-- Adaptive batching: tune flush window between 50 ms (low-latency rare inserts)\n and 2 s (absorbs bursts). A single fixed value is silently overridden by the\n adaptive algorithm. -->\n <async_insert_busy_timeout_min_ms>50</async_insert_busy_timeout_min_ms>\n <async_insert_busy_timeout_max_ms>2000</async_insert_busy_timeout_max_ms>\n <async_insert_use_adaptive_busy_timeout>1</async_insert_use_adaptive_busy_timeout>\n <input_format_parallel_parsing>0</input_format_parallel_parsing>\n <output_format_parallel_formatting>0</output_format_parallel_formatting>\n <max_memory_usage_for_user>2684354560</max_memory_usage_for_user>\n <max_bytes_before_external_sort>536870912</max_bytes_before_external_sort>\n <max_bytes_before_external_group_by>536870912</max_bytes_before_external_group_by>\n </app_writer>\n <readonly>\n <readonly>1</readonly>\n </readonly>\n </profiles>\n <quotas>\n <tenant_default>\n <interval>\n <duration>3600</duration>\n <queries>1000</queries>\n <result_rows>10000000</result_rows>\n </interval>\n </tenant_default>\n </quotas>\n</clickhouse>";
|
|
5
6
|
export declare function generateClickHouseUserData(options?: ClickHouseUserDataOptions): string;
|
|
@@ -96,7 +96,6 @@ function generateServerConfigXml(cfAccountId) {
|
|
|
96
96
|
</merge_tree>
|
|
97
97
|
<http_port>${CLICKHOUSE_HTTP_PORT}</http_port>
|
|
98
98
|
<custom_settings_prefixes>current_</custom_settings_prefixes>
|
|
99
|
-
<allow_experimental_full_text_index>1</allow_experimental_full_text_index>
|
|
100
99
|
<!-- HTTP keep-alive window. Must exceed @clickhouse/client idle_socket_ttl (15 s)
|
|
101
100
|
so the client always closes the socket first. Prevents ECONNRESET on reuse. -->
|
|
102
101
|
<keep_alive_timeout>30</keep_alive_timeout>
|
|
@@ -145,7 +144,7 @@ ${storageBlock}
|
|
|
145
144
|
<processors_profile_log remove="1"/>
|
|
146
145
|
</clickhouse>`;
|
|
147
146
|
}
|
|
148
|
-
const USERS_CONFIG_XML = `<clickhouse>
|
|
147
|
+
export const USERS_CONFIG_XML = `<clickhouse>
|
|
149
148
|
<users>
|
|
150
149
|
<default>
|
|
151
150
|
<networks>
|
|
@@ -161,6 +160,8 @@ const USERS_CONFIG_XML = `<clickhouse>
|
|
|
161
160
|
<app_writer>
|
|
162
161
|
<max_threads>2</max_threads>
|
|
163
162
|
<max_insert_threads>1</max_insert_threads>
|
|
163
|
+
<max_concurrent_queries_for_user>4</max_concurrent_queries_for_user>
|
|
164
|
+
<log_queries_min_query_duration_ms>100</log_queries_min_query_duration_ms>
|
|
164
165
|
<optimize_move_to_prewhere>1</optimize_move_to_prewhere>
|
|
165
166
|
<use_query_condition_cache>1</use_query_condition_cache>
|
|
166
167
|
<!-- Re-enable skip indexes under FINAL (tenantQuery auto-FINALs RMT tables;
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { default as ClickHouse } from "./clickhouse.js";
|
|
2
2
|
export type { ClickHouseProps, ClickHouseR2Config, ClickHouseOutputs } from "./clickhouseTypes.js";
|
|
3
|
+
export { createClickHouseAlarms } from "./clickhouseAlarms.js";
|
|
4
|
+
export type { ClickHouseAlarmThresholds, ClickHouseAlarmsProps } from "./clickhouseAlarms.js";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synth-time resolver for ECS `remoteConnections` — turns each declared
|
|
3
|
+
* cross-app resource into a pair of `${PREFIX}_HOST` / `${PREFIX}_PORT` env
|
|
4
|
+
* vars resolved from SSM (`StringParameter.valueForStringParameter`) and
|
|
5
|
+
* indexed by service name for downstream merge into container environments.
|
|
6
|
+
*
|
|
7
|
+
* SSM path layout (matches the accepter side's publishing layout in
|
|
8
|
+
* `vpcPeerAccepter.ts`):
|
|
9
|
+
* /fjall/{orgId ?? "default"}/{peer.peerAppName}/resources/{resource}/endpoint
|
|
10
|
+
* /fjall/{orgId ?? "default"}/{peer.peerAppName}/resources/{resource}/port
|
|
11
|
+
*
|
|
12
|
+
* Default env-var prefix: `${toScreamingSnake(peer.peerAppName)}_${toScreamingSnake(resource)}`.
|
|
13
|
+
* Override via `RemoteConnectionSpec.envPrefix`.
|
|
14
|
+
*/
|
|
15
|
+
import { type Construct } from "constructs";
|
|
16
|
+
import { type IVpcPeer } from "../../../utils/vpcPeerInterface.js";
|
|
17
|
+
export interface RemoteConnectionSpec {
|
|
18
|
+
peer: IVpcPeer;
|
|
19
|
+
resource: string;
|
|
20
|
+
envPrefix?: string;
|
|
21
|
+
}
|
|
22
|
+
interface ServiceWithRemoteConnections {
|
|
23
|
+
name: string;
|
|
24
|
+
remoteConnections?: RemoteConnectionSpec[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolve `remoteConnections` for every service into a per-service env-var
|
|
28
|
+
* bag. Returns `Record<serviceName, Record<envVarName, value>>`.
|
|
29
|
+
*
|
|
30
|
+
* Validates each `RemoteConnectionSpec` at synth time:
|
|
31
|
+
* - `resource` matches `RESOURCE_NAME_PATTERN` (PascalCase).
|
|
32
|
+
* - `envPrefix` (when supplied) matches `ENV_PREFIX_PATTERN`.
|
|
33
|
+
* - `peer.peerAppName` is defined and is not a CFN token — token-derived app
|
|
34
|
+
* names produce `${Token[…]}` literals in env-var names, breaking ECS task
|
|
35
|
+
* definitions silently.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveRemoteConnections(services: ServiceWithRemoteConnections[], scope: Construct, orgId: string | undefined): Record<string, Record<string, string>>;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synth-time resolver for ECS `remoteConnections` — turns each declared
|
|
3
|
+
* cross-app resource into a pair of `${PREFIX}_HOST` / `${PREFIX}_PORT` env
|
|
4
|
+
* vars resolved from SSM (`StringParameter.valueForStringParameter`) and
|
|
5
|
+
* indexed by service name for downstream merge into container environments.
|
|
6
|
+
*
|
|
7
|
+
* SSM path layout (matches the accepter side's publishing layout in
|
|
8
|
+
* `vpcPeerAccepter.ts`):
|
|
9
|
+
* /fjall/{orgId ?? "default"}/{peer.peerAppName}/resources/{resource}/endpoint
|
|
10
|
+
* /fjall/{orgId ?? "default"}/{peer.peerAppName}/resources/{resource}/port
|
|
11
|
+
*
|
|
12
|
+
* Default env-var prefix: `${toScreamingSnake(peer.peerAppName)}_${toScreamingSnake(resource)}`.
|
|
13
|
+
* Override via `RemoteConnectionSpec.envPrefix`.
|
|
14
|
+
*/
|
|
15
|
+
import { Token } from "aws-cdk-lib";
|
|
16
|
+
import { StringParameter } from "aws-cdk-lib/aws-ssm";
|
|
17
|
+
import { VALIDATION_PATTERNS } from "@fjall/generator";
|
|
18
|
+
import { buildSsmPrefix, DEFAULT_ORG_ID } from "../../../utils/cdkContext.js";
|
|
19
|
+
import { toScreamingSnake } from "../../../utils/capitaliseString.js";
|
|
20
|
+
const RESOURCE_NAME_PATTERN = VALIDATION_PATTERNS.RESOURCE_NAME;
|
|
21
|
+
/** SCREAMING_SNAKE_CASE env-var prefix (digits allowed, must start with letter). */
|
|
22
|
+
const ENV_PREFIX_PATTERN = /^[A-Z][A-Z0-9_]*$/;
|
|
23
|
+
/**
|
|
24
|
+
* Resolve `remoteConnections` for every service into a per-service env-var
|
|
25
|
+
* bag. Returns `Record<serviceName, Record<envVarName, value>>`.
|
|
26
|
+
*
|
|
27
|
+
* Validates each `RemoteConnectionSpec` at synth time:
|
|
28
|
+
* - `resource` matches `RESOURCE_NAME_PATTERN` (PascalCase).
|
|
29
|
+
* - `envPrefix` (when supplied) matches `ENV_PREFIX_PATTERN`.
|
|
30
|
+
* - `peer.peerAppName` is defined and is not a CFN token — token-derived app
|
|
31
|
+
* names produce `${Token[…]}` literals in env-var names, breaking ECS task
|
|
32
|
+
* definitions silently.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveRemoteConnections(services, scope, orgId) {
|
|
35
|
+
const byService = {};
|
|
36
|
+
for (const service of services) {
|
|
37
|
+
const specs = service.remoteConnections ?? [];
|
|
38
|
+
if (specs.length === 0)
|
|
39
|
+
continue;
|
|
40
|
+
const envVars = {};
|
|
41
|
+
const seenPrefixes = new Set();
|
|
42
|
+
for (const spec of specs) {
|
|
43
|
+
const peerAppName = validateSpec(service.name, spec);
|
|
44
|
+
const peerOrgId = spec.peer.peerOrgId ?? orgId ?? DEFAULT_ORG_ID;
|
|
45
|
+
const ssmPrefix = `${buildSsmPrefix(peerOrgId, peerAppName)}/resources/${spec.resource}`;
|
|
46
|
+
const endpoint = StringParameter.valueForStringParameter(scope, `${ssmPrefix}/endpoint`);
|
|
47
|
+
const port = StringParameter.valueForStringParameter(scope, `${ssmPrefix}/port`);
|
|
48
|
+
const prefix = spec.envPrefix ??
|
|
49
|
+
`${toScreamingSnake(peerAppName)}_${toScreamingSnake(spec.resource)}`;
|
|
50
|
+
if (seenPrefixes.has(prefix)) {
|
|
51
|
+
throw new Error(`remoteConnections on service '${service.name}': duplicate env-var prefix '${prefix}' — set distinct envPrefix on each spec to avoid silently clobbering ${prefix}_HOST and ${prefix}_PORT.`);
|
|
52
|
+
}
|
|
53
|
+
seenPrefixes.add(prefix);
|
|
54
|
+
envVars[`${prefix}_HOST`] = endpoint;
|
|
55
|
+
envVars[`${prefix}_PORT`] = port;
|
|
56
|
+
}
|
|
57
|
+
byService[service.name] = envVars;
|
|
58
|
+
}
|
|
59
|
+
return byService;
|
|
60
|
+
}
|
|
61
|
+
function validateSpec(serviceName, spec) {
|
|
62
|
+
if (!RESOURCE_NAME_PATTERN.test(spec.resource)) {
|
|
63
|
+
throw new Error(`remoteConnections on service '${serviceName}': resource '${spec.resource}' is not a valid PascalCase resource name (must match ${RESOURCE_NAME_PATTERN.source}).`);
|
|
64
|
+
}
|
|
65
|
+
if (spec.envPrefix !== undefined &&
|
|
66
|
+
!ENV_PREFIX_PATTERN.test(spec.envPrefix)) {
|
|
67
|
+
throw new Error(`remoteConnections on service '${serviceName}': envPrefix '${spec.envPrefix}' is not SCREAMING_SNAKE_CASE (must match ${ENV_PREFIX_PATTERN.source}).`);
|
|
68
|
+
}
|
|
69
|
+
const peerAppName = spec.peer.peerAppName;
|
|
70
|
+
if (peerAppName === undefined) {
|
|
71
|
+
throw new Error(`remoteConnections on service '${serviceName}': peer.peerAppName is undefined — pass a peer built via VpcPeerFactory.build() so the SSM path can be resolved at synth.`);
|
|
72
|
+
}
|
|
73
|
+
if (typeof peerAppName === "string" && peerAppName.length === 0) {
|
|
74
|
+
throw new Error(`remoteConnections on service '${serviceName}': peer.peerAppName is an empty string, which would produce malformed env-var names and an invalid SSM path. Pass a literal app name (typically the Fjall app's name string).`);
|
|
75
|
+
}
|
|
76
|
+
if (Token.isUnresolved(peerAppName)) {
|
|
77
|
+
throw new Error(`remoteConnections on service '${serviceName}': peer.peerAppName is a CFN token, which would corrupt env-var names. Pass a literal app name (typically the Fjall app's name string).`);
|
|
78
|
+
}
|
|
79
|
+
return peerAppName;
|
|
80
|
+
}
|
|
@@ -3,9 +3,11 @@ import { Duration } from "aws-cdk-lib";
|
|
|
3
3
|
import { Secret as EcsSecret } from "aws-cdk-lib/aws-ecs";
|
|
4
4
|
import { Secret } from "aws-cdk-lib/aws-secretsmanager";
|
|
5
5
|
import { StringParameter } from "aws-cdk-lib/aws-ssm";
|
|
6
|
+
import { resolveOrgId } from "../../../utils/cdkContext.js";
|
|
6
7
|
import { validateSsmPathComponent } from "./ecsValidation.js";
|
|
7
8
|
import { DEFAULT_LOG_RETENTION_DAYS } from "./ecsConstants.js";
|
|
8
9
|
import { getContainerImage } from "./ecsImages.js";
|
|
10
|
+
import { resolveRemoteConnections } from "./ecsRemoteConnections.js";
|
|
9
11
|
// Re-export extracted functions so existing consumers are not broken
|
|
10
12
|
export { createExecutionRole, createTaskRole } from "./ecsRoles.js";
|
|
11
13
|
export { getContainerImage } from "./ecsImages.js";
|
|
@@ -91,6 +93,9 @@ export function createTaskDefinition(ctx, serviceName, serviceProps, executionRo
|
|
|
91
93
|
export function addContainersToTask(ctx, serviceName, serviceProps, taskDefinition) {
|
|
92
94
|
const containers = [];
|
|
93
95
|
let primaryContainer;
|
|
96
|
+
const orgId = resolveOrgId(ctx.scope.node);
|
|
97
|
+
const remoteEnvByService = resolveRemoteConnections([serviceProps], ctx.scope, orgId);
|
|
98
|
+
const remoteEnv = remoteEnvByService[serviceName] ?? {};
|
|
94
99
|
for (const containerConfig of serviceProps.containers) {
|
|
95
100
|
const image = getContainerImage(ctx, serviceName, containerConfig, serviceProps);
|
|
96
101
|
const isFirstWithPort = !primaryContainer && containerConfig.port !== undefined;
|
|
@@ -125,8 +130,11 @@ export function addContainersToTask(ctx, serviceName, serviceProps, taskDefiniti
|
|
|
125
130
|
streamPrefix: `/ecs/${ctx.props.clusterName}/${serviceName}`,
|
|
126
131
|
logRetention: DEFAULT_LOG_RETENTION_DAYS
|
|
127
132
|
}),
|
|
133
|
+
// remoteEnv (cross-app `${PREFIX}_HOST/_PORT`) intentionally overrides user
|
|
134
|
+
// values — a stale manual setting must not mask the resolved peer.
|
|
128
135
|
environment: {
|
|
129
136
|
...containerConfig.environment,
|
|
137
|
+
...remoteEnv,
|
|
130
138
|
...(containerConfig.port
|
|
131
139
|
? { PORT: String(containerConfig.port) }
|
|
132
140
|
: {})
|
|
@@ -10,6 +10,7 @@ import { type Role } from "aws-cdk-lib/aws-iam";
|
|
|
10
10
|
import { type HostedZone as FjallHostedZone } from "../networking/hostedZone.js";
|
|
11
11
|
import { type Certificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
12
12
|
import { type ConnectionSpec } from "../../../utils/connector.js";
|
|
13
|
+
import { type RemoteConnectionSpec } from "./ecsRemoteConnections.js";
|
|
13
14
|
import { type SecretImport } from "../secrets/index.js";
|
|
14
15
|
import type { ManagedDomainExports } from "../../../utils/domainTypes.js";
|
|
15
16
|
import type { ITopic } from "aws-cdk-lib/aws-sns";
|
|
@@ -249,6 +250,12 @@ export interface EcsServiceProps {
|
|
|
249
250
|
* ]
|
|
250
251
|
*/
|
|
251
252
|
connections?: ConnectionSpec[];
|
|
253
|
+
/**
|
|
254
|
+
* Cross-app resources reachable via VPC peering. Resolved at synth time
|
|
255
|
+
* into `${PREFIX}_HOST` / `${PREFIX}_PORT` env vars merged into every
|
|
256
|
+
* container in this service's task definition.
|
|
257
|
+
*/
|
|
258
|
+
remoteConnections?: RemoteConnectionSpec[];
|
|
252
259
|
/**
|
|
253
260
|
* Capacity provider for this service. REQUIRED.
|
|
254
261
|
* Each service specifies its own capacity provider.
|