@fjall/components-infrastructure 33.1.0 → 35.0.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.js +2 -0
- package/dist/lib/config/aws/deployBoundarySingleton.d.ts +6 -0
- package/dist/lib/config/aws/deployBoundarySingleton.js +10 -2
- package/dist/lib/config/aws/identityCenter.js +2 -0
- package/dist/lib/config/aws/scpPreset.js +11 -1
- package/dist/lib/patterns/aws/bastionFactory.js +2 -1
- package/dist/lib/patterns/aws/cdn.d.ts +4 -1
- package/dist/lib/patterns/aws/clickhouseDatabase.js +2 -4
- package/dist/lib/patterns/aws/compute.d.ts +2 -2
- package/dist/lib/patterns/aws/computeEc2.d.ts +5 -18
- package/dist/lib/patterns/aws/computeEc2.js +9 -3
- package/dist/lib/patterns/aws/computeEcs.d.ts +4 -4
- package/dist/lib/patterns/aws/computeEcs.js +5 -9
- package/dist/lib/patterns/aws/computePropApplicability.js +1 -1
- package/dist/lib/patterns/aws/fivetranProxy.d.ts +8 -3
- package/dist/lib/patterns/aws/fivetranProxy.js +9 -6
- package/dist/lib/patterns/aws/interfaces/compute.d.ts +8 -0
- package/dist/lib/resources/aws/cdn/cloudFront.d.ts +16 -0
- package/dist/lib/resources/aws/cdn/cloudFront.js +52 -2
- package/dist/lib/resources/aws/compute/ec2.d.ts +33 -6
- package/dist/lib/resources/aws/compute/ec2.js +63 -14
- package/dist/lib/resources/aws/compute/ec2SshAccess.d.ts +80 -0
- package/dist/lib/resources/aws/compute/ec2SshAccess.js +166 -0
- package/dist/lib/resources/aws/compute/ecs.d.ts +11 -2
- package/dist/lib/resources/aws/compute/ecs.js +29 -6
- package/dist/lib/resources/aws/compute/lambda.d.ts +5 -0
- package/dist/lib/resources/aws/compute/lambda.js +9 -3
- package/dist/lib/resources/aws/database/dynamodb.js +5 -6
- package/dist/lib/resources/aws/iam/identityCenter/user.js +2 -0
- package/dist/lib/resources/aws/logging/accessLogBucket.d.ts +5 -3
- package/dist/lib/resources/aws/logging/accessLogBucket.js +6 -3
- package/dist/lib/resources/aws/messaging/sns.d.ts +17 -7
- package/dist/lib/resources/aws/messaging/sns.js +4 -4
- package/dist/lib/resources/aws/networking/vpcPeeringAccepterRole.js +3 -2
- package/dist/lib/resources/aws/networking/vpcPeeringConnection.js +2 -1
- package/dist/lib/resources/aws/secrets/stackEncryptionKey.d.ts +17 -4
- package/dist/lib/resources/aws/secrets/stackEncryptionKey.js +20 -4
- package/dist/lib/resources/aws/storage/bucketDeployment.d.ts +5 -0
- package/dist/lib/resources/aws/storage/bucketDeployment.js +7 -0
- package/dist/lib/resources/aws/storage/s3.d.ts +2 -1
- package/dist/lib/resources/aws/utilities/awsCustomResource.d.ts +9 -0
- package/dist/lib/resources/aws/utilities/awsCustomResource.js +16 -0
- package/dist/lib/resources/aws/utilities/customResource.js +2 -0
- package/dist/lib/resources/aws/utilities/customResourceProvider.d.ts +6 -0
- package/dist/lib/resources/aws/utilities/customResourceProvider.js +8 -0
- package/dist/lib/utils/deployTimeWriterMarkerAspect.d.ts +23 -0
- package/dist/lib/utils/deployTimeWriterMarkerAspect.js +35 -0
- package/dist/lib/utils/resourceTypeTags.d.ts +41 -0
- package/dist/lib/utils/resourceTypeTags.js +54 -0
- package/package.json +5 -4
package/dist/lib/app.js
CHANGED
|
@@ -12,6 +12,7 @@ import { ServiceDiscoveryNamespace } from "./resources/aws/networking/serviceDis
|
|
|
12
12
|
import { BuildkiteFactory } from "./patterns/aws/buildkite.js";
|
|
13
13
|
import { DevSubstrate } from "./patterns/aws/devSubstrate.js";
|
|
14
14
|
import { StandardTagsAspect } from "./utils/standardTagsAspect.js";
|
|
15
|
+
import { DeployTimeWriterMarkerAspect } from "./utils/deployTimeWriterMarkerAspect.js";
|
|
15
16
|
import { emitAppLevelAlbAliasExports } from "./utils/albAliasTargetRegistry.js";
|
|
16
17
|
import { emitAppLevelCdnAliasExports } from "./utils/cdnAliasTargetRegistry.js";
|
|
17
18
|
import { ensureFunctionLogGroups } from "./resources/aws/logging/logGroupHygiene.js";
|
|
@@ -61,6 +62,7 @@ export class App extends CdkApp {
|
|
|
61
62
|
context: { [RDS_PREVENT_RENDERING_DEPRECATED_CREDENTIALS]: true }
|
|
62
63
|
});
|
|
63
64
|
App.instance = this;
|
|
65
|
+
Aspects.of(this).add(new DeployTimeWriterMarkerAspect());
|
|
64
66
|
this.name = name ?? "FjallApp";
|
|
65
67
|
this.stackPrefix = toPascalCase(this.name);
|
|
66
68
|
// Initialise manifest collector for build-time service discovery
|
|
@@ -102,6 +102,12 @@ export declare const SECURITY_TIER_DIGESTS: {
|
|
|
102
102
|
* Five is IAM's per-policy version ceiling: at five, the oldest non-default
|
|
103
103
|
* version is dropped before the new default is published.
|
|
104
104
|
* `GetPolicyVersion` returns the document URL-encoded, hence the decode.
|
|
105
|
+
*
|
|
106
|
+
* TODO 2026-09-05, owner: paul — tag on adopt (a `TagPolicy` after the live
|
|
107
|
+
* read when the marker is absent, so an ADOPTED policy carries the marker the
|
|
108
|
+
* create path applies via `Tags: props.PolicyTags`); blocked on the
|
|
109
|
+
* inline-code budget, tracked in the deploy-boundary raises decisions design
|
|
110
|
+
* note (2026-09-05, § 6 "Train 8").
|
|
105
111
|
*/
|
|
106
112
|
export declare const DEPLOY_BOUNDARY_HANDLER_CORE: string;
|
|
107
113
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SECURITY_TIER_TO_BOUNDARY, deployBoundaryName } from "@fjall/generator";
|
|
1
|
+
import { SECURITY_TIER_TO_BOUNDARY, deployBoundaryName, deployBoundaryPolicyArn } from "@fjall/generator";
|
|
2
2
|
import { Aws, Duration } from "aws-cdk-lib";
|
|
3
3
|
import * as iam from "aws-cdk-lib/aws-iam";
|
|
4
4
|
import { Runtime } from "aws-cdk-lib/aws-lambda";
|
|
@@ -147,6 +147,12 @@ const ALLOWED_DIGEST_LITERALS = Object.values(SECURITY_TIER_DIGESTS)
|
|
|
147
147
|
* Five is IAM's per-policy version ceiling: at five, the oldest non-default
|
|
148
148
|
* version is dropped before the new default is published.
|
|
149
149
|
* `GetPolicyVersion` returns the document URL-encoded, hence the decode.
|
|
150
|
+
*
|
|
151
|
+
* TODO 2026-09-05, owner: paul — tag on adopt (a `TagPolicy` after the live
|
|
152
|
+
* read when the marker is absent, so an ADOPTED policy carries the marker the
|
|
153
|
+
* create path applies via `Tags: props.PolicyTags`); blocked on the
|
|
154
|
+
* inline-code budget, tracked in the deploy-boundary raises decisions design
|
|
155
|
+
* note (2026-09-05, § 6 "Train 8").
|
|
150
156
|
*/
|
|
151
157
|
export const DEPLOY_BOUNDARY_HANDLER_CORE = `const { IAMClient, GetPolicyCommand, GetPolicyVersionCommand, CreatePolicyCommand, CreatePolicyVersionCommand, ListPolicyVersionsCommand, DeletePolicyVersionCommand } = require('@aws-sdk/client-iam');
|
|
152
158
|
const { createHash } = require('crypto');
|
|
@@ -225,7 +231,9 @@ export class DeployBoundarySingleton extends Construct {
|
|
|
225
231
|
constructor(scope, id, props) {
|
|
226
232
|
super(scope, id);
|
|
227
233
|
this.policyName = deployBoundaryName(props.fjallOrgId);
|
|
228
|
-
|
|
234
|
+
// Same SSoT builder the seam-2 probe and the destroy sweep address the
|
|
235
|
+
// policy with — three renderers, one derivation.
|
|
236
|
+
const policyArn = deployBoundaryPolicyArn(Aws.PARTITION, Aws.ACCOUNT_ID, props.fjallOrgId);
|
|
229
237
|
const writer = new CustomResource(this, "Policy", {
|
|
230
238
|
runtime: Runtime.NODEJS_24_X,
|
|
231
239
|
timeout: Duration.seconds(DEPLOY_BOUNDARY_WRITER_TIMEOUT_SECONDS),
|
|
@@ -5,6 +5,7 @@ import { Annotations, CfnOutput, NestedStack } from "aws-cdk-lib";
|
|
|
5
5
|
import { AwsCustomResource } from "../../resources/aws/utilities/awsCustomResource.js";
|
|
6
6
|
import { Group, GroupMembership, PermissionSet, Assignment, IdentityStoreUser, IdentityStoreUserProvider, identityStoreUserArnPattern } from "../../resources/aws/iam/identityCenter/index.js";
|
|
7
7
|
import { ManagedPolicy, Role } from "../../resources/aws/iam/index.js";
|
|
8
|
+
import { markDeployTimeWriter } from "../../utils/resourceTypeTags.js";
|
|
8
9
|
import { allGroupNames, assignmentConstructId, boundGroupsForSet, effectivePermissionSets, implicitGroupNames, resolveTargetAccountNames, validateIdentityCentreConfig } from "./identityCentreConfig.js";
|
|
9
10
|
// CreateAccountAssignment has a 15-outstanding-async limit. Batching at 8
|
|
10
11
|
// keeps headroom for CFN's internal retries within the same backoff window.
|
|
@@ -74,6 +75,7 @@ export class IdentityCenter extends NestedStack {
|
|
|
74
75
|
})
|
|
75
76
|
}
|
|
76
77
|
});
|
|
78
|
+
markDeployTimeWriter(this.listInstancesRole);
|
|
77
79
|
}
|
|
78
80
|
listIdentityCenterInstance() {
|
|
79
81
|
const listInstancesCall = {
|
|
@@ -346,9 +346,19 @@ function buildCostControls() {
|
|
|
346
346
|
"rds:ModifyDBCluster"
|
|
347
347
|
],
|
|
348
348
|
Resource: "*",
|
|
349
|
+
// A negated operator over an ABSENT key evaluates true, so without
|
|
350
|
+
// the Null guard this statement denies every RDS request that names
|
|
351
|
+
// no instance class: a class-less ModifyDBInstance (the destroy
|
|
352
|
+
// pre-flight's DeletionProtection flip, a retention or parameter-group
|
|
353
|
+
// change), and Aurora's CreateDBCluster/ModifyDBCluster, where the
|
|
354
|
+
// class lives on the instances. The guard limits the deny to requests
|
|
355
|
+
// that carry a class; the instances still gate Aurora, whose Fjall
|
|
356
|
+
// construct is Serverless v2 only (`db.serverless`).
|
|
357
|
+
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html
|
|
349
358
|
Condition: {
|
|
359
|
+
Null: { "rds:DatabaseClass": "false" },
|
|
350
360
|
StringNotLike: {
|
|
351
|
-
"rds:DatabaseClass": ["db.t3.*", "db.t4g.*"]
|
|
361
|
+
"rds:DatabaseClass": ["db.t3.*", "db.t4g.*", "db.serverless"]
|
|
352
362
|
}
|
|
353
363
|
}
|
|
354
364
|
}
|
|
@@ -11,7 +11,8 @@ export function createBastion(networkStack, appName, stackPrefix, vpc, config) {
|
|
|
11
11
|
serviceName: `${stackPrefix}Bastion`,
|
|
12
12
|
instanceType,
|
|
13
13
|
vpc,
|
|
14
|
-
|
|
14
|
+
// Session Manager only: no sshAccess, so no key pair, no port-22 ingress
|
|
15
|
+
// and private subnets.
|
|
15
16
|
minCapacity: 1,
|
|
16
17
|
maxCapacity: 1
|
|
17
18
|
});
|
|
@@ -87,7 +87,10 @@ interface BaseCdnProps {
|
|
|
87
87
|
* `cloudFrontLogging` column — enterprise logs to the stack's shared,
|
|
88
88
|
* lifecycle-expired access-log bucket under `cloudfront/<id>/`. Set either
|
|
89
89
|
* knob to take over: `enableLogging: false` opts out at any tier,
|
|
90
|
-
* `logBucket` delivers to a bucket of your own.
|
|
90
|
+
* `logBucket` delivers to a bucket of your own. Standard logging cannot
|
|
91
|
+
* deliver into an opt-in region, so a tier- or explicitly-enabled CDN whose
|
|
92
|
+
* delivery region is opt-in is refused at synth with those two cures — see
|
|
93
|
+
* {@link CloudFrontDistributionProps.enableLogging}.
|
|
91
94
|
*/
|
|
92
95
|
enableLogging?: boolean;
|
|
93
96
|
logBucket?: IBucket;
|
|
@@ -16,7 +16,6 @@ import { S3Bucket } from "../../resources/aws/storage/s3.js";
|
|
|
16
16
|
import { Secret } from "../../resources/aws/secrets/secret.js";
|
|
17
17
|
import { LogGroup } from "../../resources/aws/logging/logGroup.js";
|
|
18
18
|
import { StackEncryptionKey } from "../../resources/aws/secrets/stackEncryptionKey.js";
|
|
19
|
-
import { tierDefault } from "../../utils/backupTierMapping.js";
|
|
20
19
|
import { createClickHouseSecurityGroup } from "../../resources/aws/database/clickhouseSecurityGroup.js";
|
|
21
20
|
import { buildMaintenancePreamble, maintenanceOkMarker, maintenanceRun } from "../../resources/aws/database/clickhouseMaintenanceScript.js";
|
|
22
21
|
import { buildClickHouseEntrypointWrapper, buildClickHouseUserData, generateUsersConfigXml } from "../../resources/aws/database/clickhouseUserData.js";
|
|
@@ -393,9 +392,8 @@ export class ClickHouseDatabase extends Construct {
|
|
|
393
392
|
// The maintenance-task log groups sit beside the compute's own container
|
|
394
393
|
// and exec-audit groups and follow the same tier: the stack's shared key
|
|
395
394
|
// at enterprise, nothing otherwise.
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
: {};
|
|
395
|
+
const stackKey = StackEncryptionKey.forTier(this, props.appBackupTier);
|
|
396
|
+
const taskLogGroupEncryption = stackKey === undefined ? {} : { encryptionKey: stackKey.forLogGroups() };
|
|
399
397
|
const backupTaskLogGroup = backupEnabled
|
|
400
398
|
? new LogGroup(this, "ClickHouseBackupTaskLogGroup", {
|
|
401
399
|
retention: RetentionDays.TWO_WEEKS,
|
|
@@ -5,8 +5,8 @@ import { type ComputeType, type IEcsCompute, type ILambdaCompute, type IEc2Compu
|
|
|
5
5
|
import type App from "../../app.js";
|
|
6
6
|
import { EcsCompute, type EcsComputeProps, type EcsServiceConfig, type ServiceLogAlarm, type EcsContainerConfig, type EcsScalingConfig, type EcsClusterConfig, type EcsRoutingConfig, type EcsCapacityProviderConfig, type ContainerDependency, type EcsMigrationsConfig, ECS_CAPACITY_PROVIDER_CONFIG, getEcsCapacityProviderConfig, ScalingType, type EcsCapacityProvider, type Ec2CapacityConfig, validateEcsProps, buildContainerConfigs, expandMigrationsSugar, type ResolvedScalingConfig, resolveScalingConfig } from "./computeEcs.js";
|
|
7
7
|
import { LambdaCompute, type LambdaComputeProps, type ContainerLambdaProps, type CodeLambdaProps, type FunctionUrlConfig, type ResolvedLambdaDeployment, resolveLambdaDeployment, lambdaImageKey, Architecture, HttpMethod, InvokeMode, type FunctionUrlCorsOptions } from "./computeLambda.js";
|
|
8
|
-
import { Ec2Compute, type Ec2ComputeProps
|
|
9
|
-
export { EcsCompute, type EcsComputeProps, type EcsServiceConfig, type ServiceLogAlarm, type EcsContainerConfig, type EcsScalingConfig, type EcsClusterConfig, type EcsRoutingConfig, type EcsCapacityProviderConfig, ECS_CAPACITY_PROVIDER_CONFIG, getEcsCapacityProviderConfig, ScalingType, type EcsCapacityProvider, type Ec2CapacityConfig, type ContainerDependency, type EcsMigrationsConfig, validateEcsProps, buildContainerConfigs, expandMigrationsSugar, type ResolvedScalingConfig, resolveScalingConfig, LambdaCompute, type LambdaComputeProps, type ContainerLambdaProps, type CodeLambdaProps, type FunctionUrlConfig, type ResolvedLambdaDeployment, resolveLambdaDeployment, lambdaImageKey, Architecture, HttpMethod, InvokeMode, type FunctionUrlCorsOptions, Ec2Compute, type Ec2ComputeProps
|
|
8
|
+
import { Ec2Compute, type Ec2ComputeProps } from "./computeEc2.js";
|
|
9
|
+
export { EcsCompute, type EcsComputeProps, type EcsServiceConfig, type ServiceLogAlarm, type EcsContainerConfig, type EcsScalingConfig, type EcsClusterConfig, type EcsRoutingConfig, type EcsCapacityProviderConfig, ECS_CAPACITY_PROVIDER_CONFIG, getEcsCapacityProviderConfig, ScalingType, type EcsCapacityProvider, type Ec2CapacityConfig, type ContainerDependency, type EcsMigrationsConfig, validateEcsProps, buildContainerConfigs, expandMigrationsSugar, type ResolvedScalingConfig, resolveScalingConfig, LambdaCompute, type LambdaComputeProps, type ContainerLambdaProps, type CodeLambdaProps, type FunctionUrlConfig, type ResolvedLambdaDeployment, resolveLambdaDeployment, lambdaImageKey, Architecture, HttpMethod, InvokeMode, type FunctionUrlCorsOptions, Ec2Compute, type Ec2ComputeProps };
|
|
10
10
|
export type { ComputeType } from "./interfaces/compute.js";
|
|
11
11
|
/**
|
|
12
12
|
* Configuration defaults for each compute type.
|
|
@@ -3,31 +3,18 @@ import { type IAutoScalingGroup } from "aws-cdk-lib/aws-autoscaling";
|
|
|
3
3
|
import { Construct } from "constructs";
|
|
4
4
|
import { type IEc2Compute } from "./interfaces/compute.js";
|
|
5
5
|
import { Ec2Instance } from "../../resources/aws/compute/ec2.js";
|
|
6
|
-
|
|
7
|
-
* SSH access configuration for EC2 instances.
|
|
8
|
-
* - Omit: disabled (default)
|
|
9
|
-
* - `{}`: enabled with auto-generated key
|
|
10
|
-
* - `{ keyName: "my-key" }`: enabled with existing key
|
|
11
|
-
* - `false`: explicitly disabled
|
|
12
|
-
*/
|
|
13
|
-
export interface SshConfig {
|
|
14
|
-
/** SSH key pair name */
|
|
15
|
-
keyName?: string;
|
|
16
|
-
/** Allowed CIDR blocks for SSH access */
|
|
17
|
-
allowedCidrs?: string[];
|
|
18
|
-
}
|
|
6
|
+
import { type Ec2SshAccessProps } from "../../resources/aws/compute/ec2SshAccess.js";
|
|
19
7
|
export interface Ec2ComputeProps {
|
|
20
8
|
type: "ec2";
|
|
21
9
|
vpc?: IVpc;
|
|
22
10
|
/** EC2 instance type. Default: "t4g.micro" */
|
|
23
11
|
instanceType?: string;
|
|
24
12
|
/**
|
|
25
|
-
* SSH access
|
|
26
|
-
*
|
|
27
|
-
* -
|
|
28
|
-
* - `false`: explicitly disabled
|
|
13
|
+
* SSH access: who may reach port 22 (`allowedIpCidrs`), which key pair
|
|
14
|
+
* (`keyName`, or omit to mint one) and whether the instances are public
|
|
15
|
+
* (`public`). Omit for Session Manager-only access, the default.
|
|
29
16
|
*/
|
|
30
|
-
|
|
17
|
+
sshAccess?: Ec2SshAccessProps;
|
|
31
18
|
/** User data script */
|
|
32
19
|
userData?: UserData;
|
|
33
20
|
/** Machine image (AMI) */
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
|
+
import { ec2ServiceName } from "@fjall/util";
|
|
2
3
|
import { Ec2Instance } from "../../resources/aws/compute/ec2.js";
|
|
4
|
+
import { checkRetiredSshProp } from "../../resources/aws/compute/ec2SshAccess.js";
|
|
3
5
|
import { COMPUTE_DEFAULTS } from "./compute.js";
|
|
4
6
|
/**
|
|
5
7
|
* EC2 compute wrapper implementing IEc2Compute.
|
|
@@ -11,11 +13,15 @@ export class Ec2Compute extends Construct {
|
|
|
11
13
|
ec2Instance;
|
|
12
14
|
constructor(scope, id, props) {
|
|
13
15
|
super(scope, id);
|
|
14
|
-
|
|
16
|
+
// The retired `ssh` prop read only its own presence (`keyName` and
|
|
17
|
+
// `allowedCidrs` were declared and never forwarded); a caller still
|
|
18
|
+
// passing it gets the translation, or a warning when it asked for the
|
|
19
|
+
// default.
|
|
20
|
+
checkRetiredSshProp(this, "Ec2Compute", props);
|
|
15
21
|
const ec2Props = {
|
|
16
|
-
serviceName:
|
|
22
|
+
serviceName: ec2ServiceName(id),
|
|
17
23
|
instanceType: props.instanceType || COMPUTE_DEFAULTS.EC2.INSTANCE_TYPE,
|
|
18
|
-
|
|
24
|
+
...(props.sshAccess !== undefined && { sshAccess: props.sshAccess }),
|
|
19
25
|
userData: props.userData,
|
|
20
26
|
machineImage: props.machineImage,
|
|
21
27
|
minCapacity: props.minCapacity || COMPUTE_DEFAULTS.EC2.MIN_CAPACITY,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ICluster, type IBaseService, type TaskDefinition, type FargateTaskDefinition } from "aws-cdk-lib/aws-ecs";
|
|
2
2
|
import { type Connections, type ISecurityGroup } from "aws-cdk-lib/aws-ec2";
|
|
3
|
-
import { type IGrantable, type IRole
|
|
3
|
+
import { type Grant, type IGrantable, type IRole } from "aws-cdk-lib/aws-iam";
|
|
4
4
|
import { type IApplicationLoadBalancer, type ApplicationListener } from "aws-cdk-lib/aws-elasticloadbalancingv2";
|
|
5
5
|
import { Construct } from "constructs";
|
|
6
6
|
import { type IEcsCompute } from "./interfaces/compute.js";
|
|
@@ -264,9 +264,9 @@ export declare class EcsCompute extends Construct implements IEcsCompute {
|
|
|
264
264
|
*/
|
|
265
265
|
getEcsCluster(): EcsCluster;
|
|
266
266
|
/**
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
267
|
+
* Grant what an ECS Exec caller needs: `ecs:ExecuteCommand` on this
|
|
268
|
+
* cluster's tasks and, at a tier that encrypts the session, the
|
|
269
|
+
* `kms:GenerateDataKey` on the stack key the caller must hold.
|
|
270
270
|
*/
|
|
271
271
|
grantExecuteCommand(grantee: IGrantable): Grant;
|
|
272
272
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AwsLogDriver, ContainerImage, DeploymentLifecycleStage, Secret as EcsSecret } from "aws-cdk-lib/aws-ecs";
|
|
2
2
|
import { Peer, Port, SubnetType } from "aws-cdk-lib/aws-ec2";
|
|
3
|
-
import { Effect,
|
|
3
|
+
import { Effect, PolicyStatement, ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
|
4
4
|
import { Annotations, Aws, Stack, Token } from "aws-cdk-lib";
|
|
5
5
|
import { RetentionDays } from "aws-cdk-lib/aws-logs";
|
|
6
6
|
import { StringParameter } from "aws-cdk-lib/aws-ssm";
|
|
@@ -1688,16 +1688,12 @@ export class EcsCompute extends Construct {
|
|
|
1688
1688
|
return this.ecsCluster;
|
|
1689
1689
|
}
|
|
1690
1690
|
/**
|
|
1691
|
-
*
|
|
1692
|
-
*
|
|
1693
|
-
*
|
|
1691
|
+
* Grant what an ECS Exec caller needs: `ecs:ExecuteCommand` on this
|
|
1692
|
+
* cluster's tasks and, at a tier that encrypts the session, the
|
|
1693
|
+
* `kms:GenerateDataKey` on the stack key the caller must hold.
|
|
1694
1694
|
*/
|
|
1695
1695
|
grantExecuteCommand(grantee) {
|
|
1696
|
-
return
|
|
1697
|
-
grantee,
|
|
1698
|
-
actions: ["ecs:ExecuteCommand"],
|
|
1699
|
-
resourceArns: ["*"]
|
|
1700
|
-
});
|
|
1696
|
+
return this.ecsCluster.grantExecuteCommand(grantee);
|
|
1701
1697
|
}
|
|
1702
1698
|
/**
|
|
1703
1699
|
* Get the EC2 instance role for the cluster's underlying ASG. EC2-mode
|
|
@@ -3,15 +3,20 @@ import { type Construct } from "constructs";
|
|
|
3
3
|
import { type KeyValue } from "../../types.js";
|
|
4
4
|
import { type Connections } from "aws-cdk-lib/aws-ec2";
|
|
5
5
|
import { type IVpc } from "aws-cdk-lib/aws-ec2";
|
|
6
|
-
interface
|
|
6
|
+
export interface FivetranProxyProps extends StackProps {
|
|
7
7
|
accountId?: string;
|
|
8
8
|
vpc?: IVpc;
|
|
9
9
|
publicKey: string;
|
|
10
10
|
instanceType: string;
|
|
11
|
+
/**
|
|
12
|
+
* Source ranges Fivetran connects from, one port-22 ingress rule each.
|
|
13
|
+
* Fivetran publishes its addresses per region; the proxy ships no
|
|
14
|
+
* world-open default.
|
|
15
|
+
*/
|
|
16
|
+
allowedIpCidrs: string[];
|
|
11
17
|
databaseConnection?: Connections;
|
|
12
18
|
tags?: KeyValue;
|
|
13
19
|
}
|
|
14
20
|
export declare class FivetranProxy extends Stack {
|
|
15
|
-
constructor(scope: Construct, id: string, props:
|
|
21
|
+
constructor(scope: Construct, id: string, props: FivetranProxyProps);
|
|
16
22
|
}
|
|
17
|
-
export {};
|
|
@@ -4,6 +4,10 @@ import { UserData } from "aws-cdk-lib/aws-ec2";
|
|
|
4
4
|
export class FivetranProxy extends Stack {
|
|
5
5
|
constructor(scope, id, props) {
|
|
6
6
|
super(scope, id, props);
|
|
7
|
+
if (!Array.isArray(props.allowedIpCidrs) ||
|
|
8
|
+
props.allowedIpCidrs.length === 0) {
|
|
9
|
+
throw new Error("FivetranProxy.allowedIpCidrs must list at least one IPv4 CIDR: the ranges Fivetran connects from for your region. The proxy ships no world-open default.");
|
|
10
|
+
}
|
|
7
11
|
const userData = UserData.forLinux();
|
|
8
12
|
userData.addCommands("groupadd fivetran", "useradd -m -g fivetran fivetran", "mkdir -p /home/fivetran/.ssh", "chown fivetran:fivetran /home/fivetran/.ssh", "chmod 700 /home/fivetran/.ssh", `echo "${props.publicKey}" > /home/fivetran/.ssh/authorized_keys`, "chown fivetran:fivetran /home/fivetran/.ssh/authorized_keys", "chmod 600 /home/fivetran/.ssh/authorized_keys");
|
|
9
13
|
new Ec2Instance(this, "FivetranProxy", {
|
|
@@ -11,12 +15,11 @@ export class FivetranProxy extends Stack {
|
|
|
11
15
|
serviceName: "fivetranProxy",
|
|
12
16
|
vpc: props.vpc,
|
|
13
17
|
instanceType: props.instanceType || "t2.micro",
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
enableSSH: true,
|
|
18
|
+
// This proxy exists so Fivetran can reach a private database over SSH
|
|
19
|
+
// (it provisions the `fivetran` user and its authorized_keys above), so
|
|
20
|
+
// the instance must be publicly reachable on port 22. `public` is
|
|
21
|
+
// therefore not the caller's choice; the source range is.
|
|
22
|
+
sshAccess: { allowedIpCidrs: props.allowedIpCidrs, public: true },
|
|
20
23
|
env: {
|
|
21
24
|
account: props.env?.account,
|
|
22
25
|
region: props.env?.region
|
|
@@ -73,6 +73,14 @@ export interface IEcsCompute extends ICompute, IConnectable {
|
|
|
73
73
|
* that front this compute. Undefined when there is no load balancer.
|
|
74
74
|
*/
|
|
75
75
|
getIngressProfile(): EcsIngressProfile | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Grant an operator principal what an `aws ecs execute-command` session
|
|
78
|
+
* needs against this cluster: `ecs:ExecuteCommand` scoped to the cluster
|
|
79
|
+
* and its tasks, plus `kms:GenerateDataKey` on the stack key when the
|
|
80
|
+
* session channel is key-encrypted (backup tier `enterprise`).
|
|
81
|
+
* @param grantee The operator principal
|
|
82
|
+
*/
|
|
83
|
+
grantExecuteCommand(grantee: IGrantable): Grant;
|
|
76
84
|
}
|
|
77
85
|
/**
|
|
78
86
|
* Lambda compute interface.
|
|
@@ -58,7 +58,23 @@ export interface CloudFrontDistributionProps {
|
|
|
58
58
|
domainNames?: string[];
|
|
59
59
|
certificate?: ICertificate;
|
|
60
60
|
comment?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Standard (legacy) S3 access logging. It cannot deliver to a bucket in an
|
|
63
|
+
* opt-in region (`af-south-1`, `ap-east-1`, …): CloudFormation rejects the
|
|
64
|
+
* distribution at deploy, so a concrete opt-in delivery region is refused
|
|
65
|
+
* at synth instead — cure with `enableLogging: false`, or a `logBucket`
|
|
66
|
+
* whose region CDK knows to be default-enabled (see `logBucket`).
|
|
67
|
+
*/
|
|
61
68
|
enableLogging?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Delivery bucket for standard logs; without it CDK creates one in the
|
|
71
|
+
* distribution's own stack (and region). Its `env.region` is the delivery
|
|
72
|
+
* region the opt-in refusal on `enableLogging` checks, so an imported
|
|
73
|
+
* bucket must declare it: `Bucket.fromBucketAttributes(…, { region })`, or
|
|
74
|
+
* a `Bucket` in a stack pinned to its region. `fromBucketName` and
|
|
75
|
+
* `fromBucketArn` inherit this stack's region, and a token bucket region
|
|
76
|
+
* falls back to it.
|
|
77
|
+
*/
|
|
62
78
|
logBucket?: IBucket;
|
|
63
79
|
/** Key prefix for standard logs delivered to `logBucket`. */
|
|
64
80
|
logFilePrefix?: string;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
|
-
import { CfnOutput, Names, Stack } from "aws-cdk-lib";
|
|
2
|
+
import { CfnOutput, Names, Stack, Token } from "aws-cdk-lib";
|
|
3
3
|
import { toPascalCase } from "../../../utils/capitaliseString.js";
|
|
4
4
|
import { registerCdnAliasTarget } from "../../../utils/cdnAliasTargetRegistry.js";
|
|
5
|
-
import { cdnDomainExportName } from "@fjall/util";
|
|
5
|
+
import { cdnDomainExportName, getRegionInfo, OPT_IN_REGION_CODES } from "@fjall/util";
|
|
6
6
|
import { Distribution, PriceClass, ViewerProtocolPolicy, CachePolicy, OriginRequestPolicy, AllowedMethods, OriginProtocolPolicy, Function as CloudFrontFunction, FunctionCode, FunctionRuntime, FunctionEventType, OriginAccessIdentity, S3OriginAccessControl } from "aws-cdk-lib/aws-cloudfront";
|
|
7
7
|
import { S3BucketOrigin, LoadBalancerV2Origin, HttpOrigin } from "aws-cdk-lib/aws-cloudfront-origins";
|
|
8
8
|
/**
|
|
@@ -42,6 +42,55 @@ const URI_REWRITE_FRAGMENTS = {
|
|
|
42
42
|
" else { var seg = uri.substring(uri.lastIndexOf('/') + 1);" +
|
|
43
43
|
" if (seg.indexOf('.') === -1) { request.uri = uri + '/index.html'; } } }"
|
|
44
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* CloudFront standard (legacy) logging cannot deliver to an S3 bucket in an
|
|
47
|
+
* opt-in region: CDK's `renderLogging` performs no region check and
|
|
48
|
+
* CloudFormation rejects the distribution at deploy, after the rest of the
|
|
49
|
+
* stack has been created. The delivery region is known at synth whenever the
|
|
50
|
+
* bucket's stack is env-pinned, so the refusal lives here — the seam every
|
|
51
|
+
* distribution crosses, whether built directly or through the `Cdn`
|
|
52
|
+
* pattern — and names both cures.
|
|
53
|
+
*
|
|
54
|
+
* Logging is on exactly when `renderLogging` emits a Logging block:
|
|
55
|
+
* `enableLogging: true`, or a `logBucket` without an explicit
|
|
56
|
+
* `enableLogging: false` (that pair is CDK's own contradiction error and is
|
|
57
|
+
* left to it). The delivery region is the bucket's `env.region` when it is
|
|
58
|
+
* concrete, else the distribution stack's — the bucket CDK would create.
|
|
59
|
+
* A token delivery region (an unpinned stack, and no region on the bucket)
|
|
60
|
+
* cannot be checked and passes. A concrete region `AWS_REGIONS_METADATA`
|
|
61
|
+
* does not list is refused too — every region launched since 2019 is opt-in,
|
|
62
|
+
* so an unlisted one cannot be assumed default-enabled — naming the metadata
|
|
63
|
+
* as the cure. The guard fires whenever the delivery region is concrete and
|
|
64
|
+
* known to the metadata.
|
|
65
|
+
*/
|
|
66
|
+
function validateStandardLoggingRegion(scope, id, props) {
|
|
67
|
+
const loggingOn = props.enableLogging === true ||
|
|
68
|
+
(props.enableLogging === undefined && props.logBucket !== undefined);
|
|
69
|
+
if (!loggingOn)
|
|
70
|
+
return;
|
|
71
|
+
const bucketRegion = props.logBucket?.env.region;
|
|
72
|
+
const region = bucketRegion !== undefined && !Token.isUnresolved(bucketRegion)
|
|
73
|
+
? bucketRegion
|
|
74
|
+
: Stack.of(scope).region;
|
|
75
|
+
if (Token.isUnresolved(region))
|
|
76
|
+
return;
|
|
77
|
+
if (getRegionInfo(region) === undefined) {
|
|
78
|
+
throw new Error(`CloudFront distribution '${id}': region ${region} is not in ` +
|
|
79
|
+
"@fjall/util's AWS_REGIONS_METADATA, so its opt-in status cannot be " +
|
|
80
|
+
"checked — update the metadata, or set enableLogging: false on this " +
|
|
81
|
+
"CDN.");
|
|
82
|
+
}
|
|
83
|
+
if (!OPT_IN_REGION_CODES.has(region))
|
|
84
|
+
return;
|
|
85
|
+
throw new Error(`CloudFront distribution '${id}': standard (legacy) logging cannot ` +
|
|
86
|
+
`deliver to an S3 bucket in opt-in region ${region} — logging was ` +
|
|
87
|
+
"enabled explicitly or by the app backup tier's cloudFrontLogging " +
|
|
88
|
+
"default. Set enableLogging: false on this CDN, or pass a logBucket " +
|
|
89
|
+
"whose region CDK knows to be default-enabled: " +
|
|
90
|
+
"Bucket.fromBucketAttributes(…, { region }), or a bucket in a stack " +
|
|
91
|
+
"pinned to that region (fromBucketName and fromBucketArn inherit this " +
|
|
92
|
+
"stack's region).");
|
|
93
|
+
}
|
|
45
94
|
export class CloudFrontDistribution extends Construct {
|
|
46
95
|
id;
|
|
47
96
|
distribution;
|
|
@@ -53,6 +102,7 @@ export class CloudFrontDistribution extends Construct {
|
|
|
53
102
|
constructor(scope, id, props) {
|
|
54
103
|
super(scope, id);
|
|
55
104
|
this.id = id;
|
|
105
|
+
validateStandardLoggingRegion(this, id, props);
|
|
56
106
|
// Sanitise id for CloudFormation output keys (must be alphanumeric)
|
|
57
107
|
const outputName = toPascalCase(id);
|
|
58
108
|
const defaultOrigin = this.createOrigin(props.defaultOrigin);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type BlockDevice, type IMachineImage, type IVpc, type UserData, type ISecurityGroup, type IConnectable, Connections, type SubnetConfiguration, type SubnetSelection } from "aws-cdk-lib/aws-ec2";
|
|
2
2
|
import { Construct } from "constructs";
|
|
3
|
+
import { type Ec2SshAccessProps } from "./ec2SshAccess.js";
|
|
3
4
|
import { Duration, Stack, type StackProps } from "aws-cdk-lib";
|
|
4
5
|
import { type Role } from "aws-cdk-lib/aws-iam";
|
|
5
6
|
import { AutoScalingGroup, Monitoring, type WarmPoolOptions } from "aws-cdk-lib/aws-autoscaling";
|
|
@@ -42,6 +43,7 @@ export type Ec2InstanceUpdatePolicyConfig = {
|
|
|
42
43
|
} | {
|
|
43
44
|
type: "none";
|
|
44
45
|
};
|
|
46
|
+
export { type Ec2SshAccessProps } from "./ec2SshAccess.js";
|
|
45
47
|
export interface Ec2InstanceProps extends StackProps {
|
|
46
48
|
spotCapacityPercentage?: number;
|
|
47
49
|
blockDevices?: BlockDevice[];
|
|
@@ -57,7 +59,13 @@ export interface Ec2InstanceProps extends StackProps {
|
|
|
57
59
|
machineImage?: IMachineImage;
|
|
58
60
|
userData?: UserData;
|
|
59
61
|
role?: Role;
|
|
60
|
-
|
|
62
|
+
/**
|
|
63
|
+
* SSH access: who may reach port 22, which key pair, and whether the
|
|
64
|
+
* instances are public. Omit for Session Manager-only access. Rejected
|
|
65
|
+
* alongside a caller-supplied `securityGroup` — the construct adds ingress
|
|
66
|
+
* only to a security group it owns.
|
|
67
|
+
*/
|
|
68
|
+
sshAccess?: Ec2SshAccessProps;
|
|
61
69
|
defaultPort?: number;
|
|
62
70
|
/**
|
|
63
71
|
* Caller-supplied EC2 instance monitoring resolution. Routes through the
|
|
@@ -79,8 +87,10 @@ export interface Ec2InstanceProps extends StackProps {
|
|
|
79
87
|
securityGroup?: ISecurityGroup;
|
|
80
88
|
/**
|
|
81
89
|
* CDK `AutoScalingGroupProps.vpcSubnets`. Overrides the default
|
|
82
|
-
* `
|
|
83
|
-
* the caller needs a precise subnet selection (e.g. AZ pinning).
|
|
90
|
+
* `sshAccess.public ? PUBLIC : resolvePrivateSubnetType(vpc)` inference.
|
|
91
|
+
* Use when the caller needs a precise subnet selection (e.g. AZ pinning).
|
|
92
|
+
* With `sshAccess.public: true` the selection may pin availability zones
|
|
93
|
+
* but not choose a non-public subnet type, group or list.
|
|
84
94
|
*/
|
|
85
95
|
vpcSubnets?: SubnetSelection;
|
|
86
96
|
/** CDK `AutoScalingGroupProps.capacityRebalance`. Absent → CDK default. */
|
|
@@ -92,9 +102,9 @@ export interface Ec2InstanceProps extends StackProps {
|
|
|
92
102
|
*/
|
|
93
103
|
warmPool?: WarmPoolOptions;
|
|
94
104
|
/**
|
|
95
|
-
* CDK `LaunchTemplateProps.associatePublicIpAddress`.
|
|
96
|
-
*
|
|
97
|
-
*
|
|
105
|
+
* CDK `LaunchTemplateProps.associatePublicIpAddress`. Absent → `true` only
|
|
106
|
+
* when `sshAccess.public` is `true`. An explicit `false` alongside
|
|
107
|
+
* `sshAccess.public: true` is rejected rather than silently overridden.
|
|
98
108
|
*/
|
|
99
109
|
associatePublicIpAddress?: boolean;
|
|
100
110
|
/**
|
|
@@ -162,11 +172,28 @@ export declare class Ec2Instance extends Construct implements IConnectable {
|
|
|
162
172
|
private autoScalingGroup;
|
|
163
173
|
private keyPair;
|
|
164
174
|
private persistentDataVolume?;
|
|
175
|
+
private readonly vpcSubnets;
|
|
176
|
+
private readonly associatePublicIpAddress;
|
|
165
177
|
readonly connections: Connections;
|
|
166
178
|
constructor(scope: Construct, id: string, props: Ec2InstanceProps);
|
|
167
179
|
private addVpc;
|
|
168
180
|
private addKeyPair;
|
|
181
|
+
/**
|
|
182
|
+
* One ingress rule per source range. A world-open range still lands (the
|
|
183
|
+
* caller asked for it) but warns; the wording reads the placement the group
|
|
184
|
+
* actually resolved to, not `sshAccess.public` alone, because a caller who
|
|
185
|
+
* owns the placement through public `vpcSubnets` and
|
|
186
|
+
* `associatePublicIpAddress` exposes it just the same.
|
|
187
|
+
*/
|
|
188
|
+
private addSshIngress;
|
|
169
189
|
private addLaunchTemplate;
|
|
190
|
+
/**
|
|
191
|
+
* `sshAccess.public` decides the subnet type; an explicit `vpcSubnets` keeps
|
|
192
|
+
* its other selectors (AZ pinning) — `validateEc2InstanceProps` has already
|
|
193
|
+
* rejected a selection that contradicts `public: true`, so filling in
|
|
194
|
+
* `PUBLIC` here only completes an AZ-only pin.
|
|
195
|
+
*/
|
|
196
|
+
private resolveVpcSubnets;
|
|
170
197
|
private addAutoScalingGroup;
|
|
171
198
|
/**
|
|
172
199
|
* Apply `props.tags` to the underlying ASG with
|