@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.
Files changed (67) hide show
  1. package/dist/lib/app.d.ts +25 -109
  2. package/dist/lib/app.js +37 -136
  3. package/dist/lib/patterns/aws/account.js +5 -4
  4. package/dist/lib/patterns/aws/computeEcs.d.ts +8 -397
  5. package/dist/lib/patterns/aws/computeEcs.js +13 -9
  6. package/dist/lib/patterns/aws/computeEcsTypes.d.ts +386 -0
  7. package/dist/lib/patterns/aws/computeEcsTypes.js +2 -0
  8. package/dist/lib/patterns/aws/domain.js +4 -5
  9. package/dist/lib/patterns/aws/index.d.ts +2 -0
  10. package/dist/lib/patterns/aws/index.js +2 -0
  11. package/dist/lib/patterns/aws/interfaces/compute.d.ts +6 -0
  12. package/dist/lib/patterns/aws/interfaces/connector.d.ts +1 -1
  13. package/dist/lib/patterns/aws/interfaces/connector.js +1 -1
  14. package/dist/lib/patterns/aws/interfaces/index.d.ts +2 -1
  15. package/dist/lib/patterns/aws/interfaces/index.js +1 -1
  16. package/dist/lib/patterns/aws/interfaces/vpcPeer.d.ts +7 -0
  17. package/dist/lib/patterns/aws/interfaces/vpcPeer.js +1 -0
  18. package/dist/lib/patterns/aws/organisation.js +2 -1
  19. package/dist/lib/patterns/aws/vpcPeer.d.ts +34 -0
  20. package/dist/lib/patterns/aws/vpcPeer.js +36 -0
  21. package/dist/lib/patterns/aws/vpcPeerAccepter.d.ts +29 -0
  22. package/dist/lib/patterns/aws/vpcPeerAccepter.js +196 -0
  23. package/dist/lib/resources/aws/analytics/clickhouse.js +10 -1
  24. package/dist/lib/resources/aws/analytics/clickhouseAlarms.d.ts +34 -0
  25. package/dist/lib/resources/aws/analytics/clickhouseAlarms.js +89 -0
  26. package/dist/lib/resources/aws/analytics/clickhouseConstants.d.ts +1 -1
  27. package/dist/lib/resources/aws/analytics/clickhouseConstants.js +3 -1
  28. package/dist/lib/resources/aws/analytics/clickhouseTypes.d.ts +6 -0
  29. package/dist/lib/resources/aws/analytics/clickhouseUserData.d.ts +1 -0
  30. package/dist/lib/resources/aws/analytics/clickhouseUserData.js +3 -2
  31. package/dist/lib/resources/aws/analytics/index.d.ts +2 -0
  32. package/dist/lib/resources/aws/analytics/index.js +1 -0
  33. package/dist/lib/resources/aws/compute/ecsRemoteConnections.d.ts +38 -0
  34. package/dist/lib/resources/aws/compute/ecsRemoteConnections.js +80 -0
  35. package/dist/lib/resources/aws/compute/ecsTaskDefinition.js +8 -0
  36. package/dist/lib/resources/aws/compute/ecsTypes.d.ts +7 -0
  37. package/dist/lib/resources/aws/iam/delegationRole.js +11 -4
  38. package/dist/lib/resources/aws/networking/crossAccountDelegationRecord.js +2 -1
  39. package/dist/lib/resources/aws/networking/crossAccountReturnRoutes.d.ts +40 -0
  40. package/dist/lib/resources/aws/networking/crossAccountReturnRoutes.js +154 -0
  41. package/dist/lib/resources/aws/networking/dnsRecord/dnsRecordBase.js +2 -1
  42. package/dist/lib/resources/aws/networking/domainCertificate.js +2 -1
  43. package/dist/lib/resources/aws/networking/hostedZone.js +2 -1
  44. package/dist/lib/resources/aws/networking/index.d.ts +3 -0
  45. package/dist/lib/resources/aws/networking/index.js +3 -0
  46. package/dist/lib/resources/aws/networking/vpc.js +6 -2
  47. package/dist/lib/resources/aws/networking/vpcPeeringAccepterRole.d.ts +18 -0
  48. package/dist/lib/resources/aws/networking/vpcPeeringAccepterRole.js +61 -0
  49. package/dist/lib/resources/aws/networking/vpcPeeringConnection.d.ts +49 -0
  50. package/dist/lib/resources/aws/networking/vpcPeeringConnection.js +88 -0
  51. package/dist/lib/utils/bastionFactory.d.ts +10 -0
  52. package/dist/lib/utils/bastionFactory.js +29 -0
  53. package/dist/lib/utils/capitaliseString.d.ts +1 -1
  54. package/dist/lib/utils/capitaliseString.js +1 -1
  55. package/dist/lib/utils/cdkContext.d.ts +8 -0
  56. package/dist/lib/utils/cdkContext.js +11 -0
  57. package/dist/lib/utils/connections.d.ts +7 -1
  58. package/dist/lib/utils/connections.js +15 -0
  59. package/dist/lib/utils/connector.d.ts +18 -2
  60. package/dist/lib/utils/connector.js +6 -1
  61. package/dist/lib/utils/costAllocationTags.d.ts +6 -0
  62. package/dist/lib/utils/costAllocationTags.js +6 -0
  63. package/dist/lib/utils/index.d.ts +3 -0
  64. package/dist/lib/utils/index.js +3 -0
  65. package/dist/lib/utils/vpcPeerInterface.d.ts +22 -0
  66. package/dist/lib/utils/vpcPeerInterface.js +1 -0
  67. package/package.json +4 -3
@@ -1,9 +1,10 @@
1
1
  import { Construct } from "constructs";
2
- import { CfnOutput, Fn, Tags } from "aws-cdk-lib";
2
+ import { ArnFormat, CfnOutput, Fn, Stack, Tags } from "aws-cdk-lib";
3
3
  import { OrganizationPrincipal, PolicyDocument, PolicyStatement } from "aws-cdk-lib/aws-iam";
4
4
  import { getDomainExportNames } from "@fjall/util";
5
5
  import { Role } from "./role.js";
6
6
  import { toPascalCase, getSafeZoneName } from "../../../utils/capitaliseString.js";
7
+ import { DEFAULT_COST_ALLOCATION_ENVIRONMENT } from "../../../utils/costAllocationTags.js";
7
8
  export class DelegationRole extends Construct {
8
9
  role;
9
10
  roleArn;
@@ -14,7 +15,6 @@ export class DelegationRole extends Construct {
14
15
  const firstLabel = props.zoneName.split(".")[0] ?? "default";
15
16
  const safeFirstLabel = toPascalCase(firstLabel);
16
17
  const safeZone = toPascalCase(getSafeZoneName(props.zoneName));
17
- const _kebabZone = props.zoneName.replace(/\./g, "-");
18
18
  this.description =
19
19
  props.description ??
20
20
  `Fjall-managed cross-account delegation role for ${props.zoneName}`;
@@ -36,7 +36,14 @@ export class DelegationRole extends Construct {
36
36
  new PolicyStatement({
37
37
  actions: ["route53:ChangeResourceRecordSets"],
38
38
  resources: [
39
- `arn:aws:route53:::hostedzone/${props.hostedZone.hostedZoneId}`
39
+ Stack.of(this).formatArn({
40
+ service: "route53",
41
+ region: "",
42
+ account: "",
43
+ resource: "hostedzone",
44
+ resourceName: props.hostedZone.hostedZoneId,
45
+ arnFormat: ArnFormat.SLASH_RESOURCE_NAME
46
+ })
40
47
  ]
41
48
  })
42
49
  ]
@@ -46,7 +53,7 @@ export class DelegationRole extends Construct {
46
53
  props.hostedZone.grantDelegation(role);
47
54
  this.role = role;
48
55
  this.roleArn = role.roleArn;
49
- Tags.of(this).add("fjall:costAllocation:environment", props.costAllocationEnvironment ?? "management");
56
+ Tags.of(this).add("fjall:costAllocation:environment", props.costAllocationEnvironment ?? DEFAULT_COST_ALLOCATION_ENVIRONMENT);
50
57
  Tags.of(this).add("fjall:costAllocation:service", "delegationRole");
51
58
  Tags.of(this).add("fjall:costAllocation:domain", props.costAllocationDomain ?? props.zoneName);
52
59
  const exportName = getDomainExportNames(props.zoneName).delegationRoleArn;
@@ -1,6 +1,7 @@
1
1
  import { Construct } from "constructs";
2
2
  import { Tags } from "aws-cdk-lib";
3
3
  import { CrossAccountZoneDelegationRecord as CdkCrossAccountZoneDelegationRecord } from "aws-cdk-lib/aws-route53";
4
+ import { DEFAULT_COST_ALLOCATION_ENVIRONMENT } from "../../../utils/costAllocationTags.js";
4
5
  // CDK's CrossAccountZoneDelegationRecord is a custom-resource Lambda: user-level tags
5
6
  // on the record itself may not reach AWS. We tag the wrapping Fjall Construct for
6
7
  // assertion purposes; create-path tags still propagate to child resources (Lambda,
@@ -19,7 +20,7 @@ export class CrossAccountDelegationRecord extends Construct {
19
20
  parentHostedZoneName: props.parentHostedZoneName
20
21
  });
21
22
  Tags.of(this).add("fjall:description", this.description);
22
- Tags.of(this).add("fjall:costAllocation:environment", props.costAllocationEnvironment ?? "management");
23
+ Tags.of(this).add("fjall:costAllocation:environment", props.costAllocationEnvironment ?? DEFAULT_COST_ALLOCATION_ENVIRONMENT);
23
24
  Tags.of(this).add("fjall:costAllocation:service", "crossAccountDelegation");
24
25
  Tags.of(this).add("fjall:costAllocation:domain", props.costAllocationDomain ?? props.delegatedZoneName);
25
26
  }
@@ -0,0 +1,40 @@
1
+ import { Construct } from "constructs";
2
+ import { CustomResource } from "../utilities/customResource.js";
3
+ /**
4
+ * Properties for {@link CrossAccountReturnRoutes}.
5
+ *
6
+ * The route-table IDs are passed as a comma-joined string because CloudFormation
7
+ * custom-resource properties are flat strings. Inside the handler (which runs
8
+ * after CFN token resolution) the string is split back into an array.
9
+ */
10
+ export interface CrossAccountReturnRoutesProps {
11
+ /** ARN of the accepter-account role the Lambda assumes to make EC2 calls. */
12
+ readonly peerRoleArn: string;
13
+ /** Region of the accepter VPC (so the EC2 client targets the correct region). */
14
+ readonly peerRegion: string;
15
+ /** Route-table IDs in the accepter VPC that should receive return routes. */
16
+ readonly routeTableIds: string[];
17
+ /** Local VPC CIDR — destination of the return routes in the accepter's tables. */
18
+ readonly localVpcCidr: string;
19
+ /** The peering connection ID (the target of the return routes). */
20
+ readonly peeringConnectionId: string;
21
+ /** Whether to also set `AccepterPeeringConnectionOptions.AllowDnsResolutionFromRemoteVpc`. */
22
+ readonly enableDns?: boolean;
23
+ }
24
+ /**
25
+ * Cross-account return-route manager for a VPC peering connection.
26
+ *
27
+ * Uses the shared {@link CustomResource} wrapper with an inline Lambda handler.
28
+ * The Lambda's execution role is local to the requester account — it holds only
29
+ * `sts:AssumeRole` on the accepter role. All EC2 calls happen under the assumed
30
+ * credentials in the accepter account.
31
+ *
32
+ * Create paths add `CreateRouteCommand` entries per route-table ID; delete
33
+ * paths reverse them via `DeleteRouteCommand`. When `enableDns` is true the
34
+ * handler additionally calls `ModifyVpcPeeringConnectionOptionsCommand` on the
35
+ * accepter side so DNS resolution works bidirectionally.
36
+ */
37
+ export declare class CrossAccountReturnRoutes extends Construct {
38
+ readonly customResource: CustomResource;
39
+ constructor(scope: Construct, id: string, props: CrossAccountReturnRoutesProps);
40
+ }
@@ -0,0 +1,154 @@
1
+ import { Construct } from "constructs";
2
+ import { Duration } from "aws-cdk-lib";
3
+ import { Runtime } from "aws-cdk-lib/aws-lambda";
4
+ import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
5
+ import { CustomResource } from "../utilities/customResource.js";
6
+ /**
7
+ * Cross-account return-route manager for a VPC peering connection.
8
+ *
9
+ * Uses the shared {@link CustomResource} wrapper with an inline Lambda handler.
10
+ * The Lambda's execution role is local to the requester account — it holds only
11
+ * `sts:AssumeRole` on the accepter role. All EC2 calls happen under the assumed
12
+ * credentials in the accepter account.
13
+ *
14
+ * Create paths add `CreateRouteCommand` entries per route-table ID; delete
15
+ * paths reverse them via `DeleteRouteCommand`. When `enableDns` is true the
16
+ * handler additionally calls `ModifyVpcPeeringConnectionOptionsCommand` on the
17
+ * accepter side so DNS resolution works bidirectionally.
18
+ */
19
+ export class CrossAccountReturnRoutes extends Construct {
20
+ customResource;
21
+ constructor(scope, id, props) {
22
+ super(scope, id);
23
+ this.customResource = new CustomResource(this, "Handler", {
24
+ runtime: Runtime.NODEJS_22_X,
25
+ timeout: Duration.minutes(5),
26
+ lambdaDescription: `${id} cross-account return-route manager`,
27
+ roleDescription: `${id} return-route Lambda execution role`,
28
+ inlinePolicy: [
29
+ new PolicyStatement({
30
+ effect: Effect.ALLOW,
31
+ actions: ["sts:AssumeRole"],
32
+ resources: [props.peerRoleArn]
33
+ })
34
+ ],
35
+ properties: {
36
+ PeerRoleArn: props.peerRoleArn,
37
+ PeerRegion: props.peerRegion,
38
+ RouteTableIds: props.routeTableIds.join(","),
39
+ LocalVpcCidr: props.localVpcCidr,
40
+ PeeringConnectionId: props.peeringConnectionId,
41
+ EnableDns: props.enableDns === false ? "false" : "true"
42
+ },
43
+ inlineCode: HANDLER_SOURCE
44
+ });
45
+ }
46
+ }
47
+ // Handler source. Inline because the CustomResource utility expects a string;
48
+ // extracting to its own file would require switching to a bundled asset.
49
+ const HANDLER_SOURCE = `
50
+ const { STSClient, AssumeRoleCommand } = require("@aws-sdk/client-sts");
51
+ const {
52
+ EC2Client,
53
+ CreateRouteCommand,
54
+ DeleteRouteCommand,
55
+ ModifyVpcPeeringConnectionOptionsCommand
56
+ } = require("@aws-sdk/client-ec2");
57
+
58
+ async function buildAccepterClient(props) {
59
+ const sts = new STSClient({});
60
+ const assumed = await sts.send(new AssumeRoleCommand({
61
+ RoleArn: props.PeerRoleArn,
62
+ RoleSessionName: "fjall-vpc-peer-return-routes"
63
+ }));
64
+ const creds = assumed.Credentials;
65
+ if (!creds || !creds.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) {
66
+ throw new Error("AssumeRole returned no credentials");
67
+ }
68
+ return new EC2Client({
69
+ region: props.PeerRegion,
70
+ credentials: {
71
+ accessKeyId: creds.AccessKeyId,
72
+ secretAccessKey: creds.SecretAccessKey,
73
+ sessionToken: creds.SessionToken
74
+ }
75
+ });
76
+ }
77
+
78
+ function parseRouteTableIds(value) {
79
+ if (!value) return [];
80
+ return value.split(",").map((s) => s.trim()).filter(Boolean);
81
+ }
82
+
83
+ exports.handler = async (event) => {
84
+ const props = event.ResourceProperties || {};
85
+ const physicalResourceId = event.PhysicalResourceId || event.LogicalResourceId || "return-routes";
86
+ const routeTableIds = parseRouteTableIds(props.RouteTableIds);
87
+ const ec2 = await buildAccepterClient(props);
88
+
89
+ if (event.RequestType === "Delete") {
90
+ for (const routeTableId of routeTableIds) {
91
+ try {
92
+ await ec2.send(new DeleteRouteCommand({
93
+ RouteTableId: routeTableId,
94
+ DestinationCidrBlock: props.LocalVpcCidr
95
+ }));
96
+ } catch (err) {
97
+ const name = err && err.name;
98
+ if (name === "InvalidRoute.NotFound" || name === "InvalidRouteTableID.NotFound") {
99
+ continue;
100
+ }
101
+ throw err;
102
+ }
103
+ }
104
+ return { PhysicalResourceId: physicalResourceId };
105
+ }
106
+
107
+ if (event.RequestType === "Update") {
108
+ const oldProps = event.OldResourceProperties || {};
109
+ const oldRouteTableIds = parseRouteTableIds(oldProps.RouteTableIds);
110
+ const currentSet = new Set(routeTableIds);
111
+ const removedIds = oldRouteTableIds.filter((id) => !currentSet.has(id));
112
+ for (const routeTableId of removedIds) {
113
+ try {
114
+ await ec2.send(new DeleteRouteCommand({
115
+ RouteTableId: routeTableId,
116
+ DestinationCidrBlock: oldProps.LocalVpcCidr || props.LocalVpcCidr
117
+ }));
118
+ } catch (err) {
119
+ const name = err && err.name;
120
+ if (name === "InvalidRoute.NotFound" || name === "InvalidRouteTableID.NotFound") {
121
+ continue;
122
+ }
123
+ throw err;
124
+ }
125
+ }
126
+ }
127
+
128
+ for (const routeTableId of routeTableIds) {
129
+ try {
130
+ await ec2.send(new CreateRouteCommand({
131
+ RouteTableId: routeTableId,
132
+ DestinationCidrBlock: props.LocalVpcCidr,
133
+ VpcPeeringConnectionId: props.PeeringConnectionId
134
+ }));
135
+ } catch (err) {
136
+ const name = err && err.name;
137
+ if (name !== "RouteAlreadyExists") {
138
+ throw err;
139
+ }
140
+ }
141
+ }
142
+
143
+ if (props.EnableDns === "true") {
144
+ await ec2.send(new ModifyVpcPeeringConnectionOptionsCommand({
145
+ VpcPeeringConnectionId: props.PeeringConnectionId,
146
+ AccepterPeeringConnectionOptions: {
147
+ AllowDnsResolutionFromRemoteVpc: true
148
+ }
149
+ }));
150
+ }
151
+
152
+ return { PhysicalResourceId: physicalResourceId };
153
+ };
154
+ `.trim();
@@ -1,5 +1,6 @@
1
1
  import { Duration, Tags } from "aws-cdk-lib";
2
2
  import { DNS_APEX } from "@fjall/util";
3
+ import { DEFAULT_COST_ALLOCATION_ENVIRONMENT } from "../../../../utils/costAllocationTags.js";
3
4
  export const DEFAULT_DNS_TTL_SECONDS = 300;
4
5
  export function resolveFqdn(zoneName, recordName) {
5
6
  return recordName === DNS_APEX ? zoneName : `${recordName}.${zoneName}`;
@@ -11,7 +12,7 @@ export function defaultDnsComment(recordType, fqdn) {
11
12
  return `Fjall-managed ${recordType} record for ${fqdn}`;
12
13
  }
13
14
  export function applyDnsRecordTags(construct, props) {
14
- Tags.of(construct).add("fjall:costAllocation:environment", props.costAllocationEnvironment ?? "management");
15
+ Tags.of(construct).add("fjall:costAllocation:environment", props.costAllocationEnvironment ?? DEFAULT_COST_ALLOCATION_ENVIRONMENT);
15
16
  Tags.of(construct).add("fjall:costAllocation:service", "dnsRecord");
16
17
  Tags.of(construct).add("fjall:costAllocation:domain", props.costAllocationDomain ?? props.zoneName);
17
18
  }
@@ -3,6 +3,7 @@ import { CfnOutput, Tags } from "aws-cdk-lib";
3
3
  import { Certificate, CertificateValidation } from "aws-cdk-lib/aws-certificatemanager";
4
4
  import { getDomainExportNames } from "@fjall/util";
5
5
  import { toPascalCase } from "../../../utils/capitaliseString.js";
6
+ import { DEFAULT_COST_ALLOCATION_ENVIRONMENT } from "../../../utils/costAllocationTags.js";
6
7
  export class DomainCertificate extends Construct {
7
8
  certificate;
8
9
  certificateArn;
@@ -20,7 +21,7 @@ export class DomainCertificate extends Construct {
20
21
  });
21
22
  this.certificateArn = this.certificate.certificateArn;
22
23
  Tags.of(this).add("fjall:description", this.description);
23
- Tags.of(this).add("fjall:costAllocation:environment", props.costAllocationEnvironment ?? "management");
24
+ Tags.of(this).add("fjall:costAllocation:environment", props.costAllocationEnvironment ?? DEFAULT_COST_ALLOCATION_ENVIRONMENT);
24
25
  Tags.of(this).add("fjall:costAllocation:service", "certificate");
25
26
  Tags.of(this).add("fjall:costAllocation:domain", props.costAllocationDomain ?? props.domainName);
26
27
  const safeKey = toPascalCase(props.domainName.split(".").join(""));
@@ -4,6 +4,7 @@ import { HostedZone as AWSHostedZone } from "aws-cdk-lib/aws-route53";
4
4
  import { getDomainExportNames } from "@fjall/util";
5
5
  import { toPascalCase, getSafeZoneName } from "../../../utils/capitaliseString.js";
6
6
  import { DelegationRole } from "../iam/delegationRole.js";
7
+ import { DEFAULT_COST_ALLOCATION_ENVIRONMENT } from "../../../utils/costAllocationTags.js";
7
8
  export class HostedZoneFactory {
8
9
  static import(stack, hostedZoneId, zoneName, opts) {
9
10
  const safeZone = toPascalCase(getSafeZoneName(zoneName));
@@ -71,7 +72,7 @@ export class HostedZone extends Construct {
71
72
  Tags.of(this).add("fjall:description", this.description);
72
73
  }
73
74
  // Cost allocation tags applied uniformly on both create and import paths.
74
- Tags.of(this).add("fjall:costAllocation:environment", props.costAllocationEnvironment ?? "management");
75
+ Tags.of(this).add("fjall:costAllocation:environment", props.costAllocationEnvironment ?? DEFAULT_COST_ALLOCATION_ENVIRONMENT);
75
76
  Tags.of(this).add("fjall:costAllocation:service", "hostedZone");
76
77
  Tags.of(this).add("fjall:costAllocation:domain", props.costAllocationDomain ?? props.zoneName);
77
78
  new CfnOutput(this, `${safeZone}HostedZoneId`, {
@@ -1,4 +1,5 @@
1
1
  export * from "./crossAccountDelegationRecord.js";
2
+ export * from "./crossAccountReturnRoutes.js";
2
3
  export * from "./dnsRecord/index.js";
3
4
  export * from "./domainCertificate.js";
4
5
  export * from "./hostedZone.js";
@@ -6,3 +7,5 @@ export * from "./ipam.js";
6
7
  export * from "./ipamPool.js";
7
8
  export * from "./securityGroup.js";
8
9
  export * from "./vpc.js";
10
+ export * from "./vpcPeeringAccepterRole.js";
11
+ export * from "./vpcPeeringConnection.js";
@@ -1,4 +1,5 @@
1
1
  export * from "./crossAccountDelegationRecord.js";
2
+ export * from "./crossAccountReturnRoutes.js";
2
3
  export * from "./dnsRecord/index.js";
3
4
  export * from "./domainCertificate.js";
4
5
  export * from "./hostedZone.js";
@@ -6,3 +7,5 @@ export * from "./ipam.js";
6
7
  export * from "./ipamPool.js";
7
8
  export * from "./securityGroup.js";
8
9
  export * from "./vpc.js";
10
+ export * from "./vpcPeeringAccepterRole.js";
11
+ export * from "./vpcPeeringConnection.js";
@@ -1,4 +1,4 @@
1
- import { Duration, RemovalPolicy, Stack } from "aws-cdk-lib";
1
+ import { CfnOutput, Duration, RemovalPolicy, Stack } from "aws-cdk-lib";
2
2
  import * as ec2 from "aws-cdk-lib/aws-ec2";
3
3
  import * as s3 from "aws-cdk-lib/aws-s3";
4
4
  import { LogGroup } from "../logging/logGroup.js";
@@ -12,7 +12,7 @@ export class Vpc extends ec2.Vpc {
12
12
  const maxAzs = maxAzsFromProps ?? 3;
13
13
  const natGateways = Vpc.resolveNatGateways(props);
14
14
  const vpcName = explicitName ?? id;
15
- super(scope, `vpc-${id}`, {
15
+ super(scope, `Vpc${id}`, {
16
16
  ...restProps,
17
17
  vpcName,
18
18
  natGateways,
@@ -23,6 +23,10 @@ export class Vpc extends ec2.Vpc {
23
23
  });
24
24
  this.hasNatGateways = natGateways !== 0;
25
25
  this.addInterfaceEndpoints(id, props, this.hasNatGateways);
26
+ new CfnOutput(this, "VpcCidrBlock", {
27
+ value: this.vpcCidrBlock,
28
+ exportName: `${Stack.of(this).stackName}-vpc-cidr`
29
+ });
26
30
  }
27
31
  static gatewayEndpoints(props) {
28
32
  if (props?.endpointsConfig === false) {
@@ -0,0 +1,18 @@
1
+ import { Construct } from "constructs";
2
+ import { Role } from "aws-cdk-lib/aws-iam";
3
+ import { type IVpc } from "aws-cdk-lib/aws-ec2";
4
+ export interface VpcPeeringAccepterRoleProps {
5
+ /** AWS account IDs allowed to initiate peering and manage return routes. */
6
+ readonly requesterAccountIds: string[];
7
+ /** The local VPC this role is scoped to. */
8
+ readonly localVpc: IVpc;
9
+ /** Cost-allocation override — defaults to `"management"`. */
10
+ readonly costAllocationEnvironment?: string;
11
+ /** Cost-allocation override — defaults to the local VPC's construct id. */
12
+ readonly costAllocationDomain?: string;
13
+ }
14
+ export declare class VpcPeeringAccepterRole extends Construct {
15
+ readonly role: Role;
16
+ readonly roleArn: string;
17
+ constructor(scope: Construct, id: string, props: VpcPeeringAccepterRoleProps);
18
+ }
@@ -0,0 +1,61 @@
1
+ import { Construct } from "constructs";
2
+ import { ArnFormat, Stack, Tags } from "aws-cdk-lib";
3
+ import { AccountPrincipal, CompositePrincipal, PolicyDocument, PolicyStatement, Role } from "aws-cdk-lib/aws-iam";
4
+ import { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT } from "../../../utils/costAllocationTags.js";
5
+ export class VpcPeeringAccepterRole extends Construct {
6
+ role;
7
+ roleArn;
8
+ constructor(scope, id, props) {
9
+ super(scope, id);
10
+ const vpcArnPattern = Stack.of(this).formatArn({
11
+ service: "ec2",
12
+ region: "*",
13
+ account: "*",
14
+ resource: "vpc",
15
+ resourceName: props.localVpc.vpcId,
16
+ arnFormat: ArnFormat.SLASH_RESOURCE_NAME
17
+ });
18
+ this.role = new Role(this, "Role", {
19
+ assumedBy: new CompositePrincipal(...props.requesterAccountIds.map((accountId) => new AccountPrincipal(accountId))),
20
+ inlinePolicies: {
21
+ allowPeering: new PolicyDocument({
22
+ statements: [
23
+ new PolicyStatement({
24
+ actions: [
25
+ "ec2:AcceptVpcPeeringConnection",
26
+ "ec2:ModifyVpcPeeringConnectionOptions"
27
+ ],
28
+ resources: ["*"],
29
+ conditions: {
30
+ StringEquals: {
31
+ "ec2:AccepterVpc": vpcArnPattern
32
+ }
33
+ }
34
+ }),
35
+ new PolicyStatement({
36
+ actions: ["ec2:CreateRoute", "ec2:DeleteRoute"],
37
+ resources: ["*"],
38
+ conditions: {
39
+ StringEquals: {
40
+ "ec2:Vpc": vpcArnPattern
41
+ }
42
+ }
43
+ }),
44
+ new PolicyStatement({
45
+ actions: [
46
+ "ec2:DescribeRouteTables",
47
+ "ec2:DescribeVpcPeeringConnections"
48
+ ],
49
+ resources: ["*"]
50
+ })
51
+ ]
52
+ })
53
+ }
54
+ });
55
+ this.roleArn = this.role.roleArn;
56
+ Tags.of(this).add("fjall:resource:type", "vpc-peering-accepter");
57
+ Tags.of(this).add(COST_ALLOCATION_TAGS.ENVIRONMENT, props.costAllocationEnvironment ?? DEFAULT_COST_ALLOCATION_ENVIRONMENT);
58
+ Tags.of(this).add(COST_ALLOCATION_TAGS.SERVICE, "vpcPeeringAccepter");
59
+ Tags.of(this).add(COST_ALLOCATION_TAGS.DOMAIN, props.costAllocationDomain ?? props.localVpc.node.id);
60
+ }
61
+ }
@@ -0,0 +1,49 @@
1
+ import { Construct } from "constructs";
2
+ import { CfnVPCPeeringConnection, type IVpc, type SubnetSelection } from "aws-cdk-lib/aws-ec2";
3
+ export interface VpcPeeringConnectionProps {
4
+ /** The local VPC to peer from. */
5
+ readonly localVpc: IVpc;
6
+ /** The local VPC CIDR (destination of the accepter's return routes). */
7
+ readonly localVpcCidr: string;
8
+ /** The remote VPC ID. */
9
+ readonly peerVpcId: string;
10
+ /** The remote VPC CIDR (destination of the local routes). */
11
+ readonly peerVpcCidr: string;
12
+ /** Remote account ID — required for cross-account peering. */
13
+ readonly peerAccountId?: string;
14
+ /** Remote region — required for cross-region peering. */
15
+ readonly peerRegion?: string;
16
+ /** ARN of the accepter role (for auto-accept and cross-account route management). */
17
+ readonly peerRoleArn?: string;
18
+ /** Route-table IDs in the accepter VPC (for cross-account return routes). */
19
+ readonly peerRouteTableIds?: string[];
20
+ /** Local subnet selection for routes. Defaults to `PRIVATE_WITH_EGRESS`. */
21
+ readonly routeSubnets?: SubnetSelection;
22
+ /** Enable DNS resolution across the peering on both sides. Defaults to true. */
23
+ readonly enableDnsResolution?: boolean;
24
+ /**
25
+ * Optional cross-app label — when present, added as the `fjall:peering:peer-app`
26
+ * tag and used as the default cost-allocation domain.
27
+ */
28
+ readonly peerAppName?: string;
29
+ /**
30
+ * Optional organisation ID of the remote app — surfaced as a public-readonly
31
+ * property on the construct for downstream consumers (ECS `remoteConnections`)
32
+ * that need to build cross-app SSM paths.
33
+ */
34
+ readonly peerOrgId?: string;
35
+ /** Cost-allocation override — defaults to `"management"`. */
36
+ readonly costAllocationEnvironment?: string;
37
+ /**
38
+ * Cost-allocation override — defaults to `peerAppName` if provided, otherwise
39
+ * the peer VPC ID.
40
+ */
41
+ readonly costAllocationDomain?: string;
42
+ }
43
+ export declare class VpcPeeringConnection extends Construct {
44
+ readonly peeringConnectionId: string;
45
+ readonly peering: CfnVPCPeeringConnection;
46
+ readonly peerAppName: string | undefined;
47
+ readonly peerOrgId: string | undefined;
48
+ constructor(scope: Construct, id: string, props: VpcPeeringConnectionProps);
49
+ }
@@ -0,0 +1,88 @@
1
+ import { Construct } from "constructs";
2
+ import { Stack, Tags } from "aws-cdk-lib";
3
+ import { CfnRoute, CfnVPCPeeringConnection, SubnetType } from "aws-cdk-lib/aws-ec2";
4
+ import { PolicyStatement } from "aws-cdk-lib/aws-iam";
5
+ import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from "aws-cdk-lib/custom-resources";
6
+ import { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT } from "../../../utils/costAllocationTags.js";
7
+ import { CrossAccountReturnRoutes } from "./crossAccountReturnRoutes.js";
8
+ export class VpcPeeringConnection extends Construct {
9
+ peeringConnectionId;
10
+ peering;
11
+ peerAppName;
12
+ peerOrgId;
13
+ constructor(scope, id, props) {
14
+ super(scope, id);
15
+ this.peerAppName = props.peerAppName;
16
+ this.peerOrgId = props.peerOrgId;
17
+ this.peering = new CfnVPCPeeringConnection(this, "Peering", {
18
+ vpcId: props.localVpc.vpcId,
19
+ peerVpcId: props.peerVpcId,
20
+ peerOwnerId: props.peerAccountId,
21
+ peerRegion: props.peerRegion,
22
+ peerRoleArn: props.peerRoleArn
23
+ });
24
+ this.peeringConnectionId = this.peering.attrId;
25
+ const subnets = props.localVpc.selectSubnets(props.routeSubnets ?? { subnetType: SubnetType.PRIVATE_WITH_EGRESS });
26
+ for (const subnet of subnets.subnets) {
27
+ new CfnRoute(this, `Route${subnet.node.id}`, {
28
+ routeTableId: subnet.routeTable.routeTableId,
29
+ destinationCidrBlock: props.peerVpcCidr,
30
+ vpcPeeringConnectionId: this.peering.attrId
31
+ });
32
+ }
33
+ const enableDnsResolution = props.enableDnsResolution !== false;
34
+ // Requester-side DNS runs in the local account, so a plain AwsCustomResource
35
+ // is sufficient. The accepter side is delegated to the cross-account Lambda.
36
+ if (enableDnsResolution) {
37
+ new AwsCustomResource(this, "EnableRequesterDns", {
38
+ onCreate: {
39
+ service: "EC2",
40
+ action: "modifyVpcPeeringConnectionOptions",
41
+ parameters: {
42
+ VpcPeeringConnectionId: this.peering.attrId,
43
+ RequesterPeeringConnectionOptions: {
44
+ AllowDnsResolutionFromRemoteVpc: true
45
+ }
46
+ },
47
+ physicalResourceId: PhysicalResourceId.of(`${id}-requester-dns`)
48
+ },
49
+ policy: AwsCustomResourcePolicy.fromStatements([
50
+ new PolicyStatement({
51
+ actions: ["ec2:ModifyVpcPeeringConnectionOptions"],
52
+ resources: [
53
+ Stack.of(this).formatArn({
54
+ service: "ec2",
55
+ resource: "vpc-peering-connection",
56
+ resourceName: this.peering.attrId
57
+ })
58
+ ]
59
+ })
60
+ ])
61
+ });
62
+ }
63
+ if (props.peerRoleArn &&
64
+ props.peerRegion &&
65
+ props.peerRouteTableIds &&
66
+ props.peerRouteTableIds.length > 0) {
67
+ new CrossAccountReturnRoutes(this, "ReturnRoutes", {
68
+ peerRoleArn: props.peerRoleArn,
69
+ peerRegion: props.peerRegion,
70
+ routeTableIds: props.peerRouteTableIds,
71
+ localVpcCidr: props.localVpcCidr,
72
+ peeringConnectionId: this.peering.attrId,
73
+ enableDns: enableDnsResolution
74
+ });
75
+ }
76
+ Tags.of(this).add("fjall:resource:type", "vpc-peering");
77
+ Tags.of(this).add("fjall:peering:peer-vpc", props.peerVpcId);
78
+ if (props.peerAccountId) {
79
+ Tags.of(this).add("fjall:peering:peer-account", props.peerAccountId);
80
+ }
81
+ if (props.peerAppName) {
82
+ Tags.of(this).add("fjall:peering:peer-app", props.peerAppName);
83
+ }
84
+ Tags.of(this).add(COST_ALLOCATION_TAGS.ENVIRONMENT, props.costAllocationEnvironment ?? DEFAULT_COST_ALLOCATION_ENVIRONMENT);
85
+ Tags.of(this).add(COST_ALLOCATION_TAGS.SERVICE, "vpcPeering");
86
+ Tags.of(this).add(COST_ALLOCATION_TAGS.DOMAIN, props.costAllocationDomain ?? props.peerAppName ?? props.peerVpcId);
87
+ }
88
+ }
@@ -0,0 +1,10 @@
1
+ import { type IVpc } from "aws-cdk-lib/aws-ec2";
2
+ import { Ec2Instance } from "../resources/aws/compute/ec2.js";
3
+ import { type AwsStack } from "../resources/index.js";
4
+ export interface BastionConfig {
5
+ instanceType?: string;
6
+ }
7
+ export interface BastionResult {
8
+ bastion: Ec2Instance;
9
+ }
10
+ export declare function createBastion(networkStack: AwsStack, appName: string, stackPrefix: string, vpc: IVpc, config: BastionConfig | true): BastionResult;
@@ -0,0 +1,29 @@
1
+ import { CfnOutput } from "aws-cdk-lib";
2
+ import { Ec2Instance } from "../resources/aws/compute/ec2.js";
3
+ import { toPascalCase } from "./capitaliseString.js";
4
+ export function createBastion(networkStack, appName, stackPrefix, vpc, config) {
5
+ const instanceType = typeof config === "object" && config.instanceType
6
+ ? config.instanceType
7
+ : "t4g.micro";
8
+ const bastionId = `${stackPrefix}Bastion`;
9
+ const scope = networkStack.getStack();
10
+ const bastion = new Ec2Instance(scope, bastionId, {
11
+ serviceName: `${stackPrefix}Bastion`,
12
+ instanceType,
13
+ vpc,
14
+ enableSSH: false,
15
+ minCapacity: 1,
16
+ maxCapacity: 1
17
+ });
18
+ networkStack.addConstruct(bastion);
19
+ const outputPrefix = toPascalCase(appName);
20
+ new CfnOutput(scope, `${outputPrefix}BastionInstanceId`, {
21
+ value: bastion.getAutoScalingGroup().autoScalingGroupName,
22
+ description: "Bastion ASG name for SSM tunnel discovery"
23
+ });
24
+ new CfnOutput(scope, `${outputPrefix}BastionSecurityGroupId`, {
25
+ value: bastion.asgSecurityGroup.securityGroupId,
26
+ description: "Bastion security group ID"
27
+ });
28
+ return { bastion };
29
+ }
@@ -1 +1 @@
1
- export { capitalise as capitaliseString, toPascalCase, toKebab, toValidDatabaseName, getSafeZoneName } from "@fjall/util";
1
+ export { capitalise as capitaliseString, toPascalCase, toKebab, toValidDatabaseName, toScreamingSnake, getSafeZoneName } from "@fjall/util";
@@ -1 +1 @@
1
- export { capitalise as capitaliseString, toPascalCase, toKebab, toValidDatabaseName, getSafeZoneName } from "@fjall/util";
1
+ export { capitalise as capitaliseString, toPascalCase, toKebab, toValidDatabaseName, toScreamingSnake, getSafeZoneName } from "@fjall/util";
@@ -0,0 +1,8 @@
1
+ import type { Node } from "constructs";
2
+ export declare const CDK_CONTEXT_KEYS: {
3
+ readonly ORG_ID: "orgId";
4
+ };
5
+ export declare const DEFAULT_ORG_ID: "default";
6
+ export declare function resolveOrgId(node: Node, fallback: string): string;
7
+ export declare function resolveOrgId(node: Node): string | undefined;
8
+ export declare function buildSsmPrefix(orgId: string, appName: string): string;
@@ -0,0 +1,11 @@
1
+ export const CDK_CONTEXT_KEYS = {
2
+ ORG_ID: "orgId"
3
+ };
4
+ export const DEFAULT_ORG_ID = "default";
5
+ export function resolveOrgId(node, fallback) {
6
+ const raw = node.tryGetContext(CDK_CONTEXT_KEYS.ORG_ID);
7
+ return typeof raw === "string" ? raw : fallback;
8
+ }
9
+ export function buildSsmPrefix(orgId, appName) {
10
+ return `/fjall/${orgId}/${appName}`;
11
+ }