@fjall/components-infrastructure 2.33.0 → 3.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.d.ts +19 -2
- package/dist/lib/app.js +61 -4
- package/dist/lib/patterns/aws/account.js +6 -1
- package/dist/lib/patterns/aws/apexDomainPattern.d.ts +11 -12
- package/dist/lib/patterns/aws/apexDomainPattern.js +11 -31
- package/dist/lib/patterns/aws/cdn.js +25 -10
- package/dist/lib/patterns/aws/computeEcs.js +15 -6
- package/dist/lib/patterns/aws/computeEcsTypes.d.ts +13 -4
- package/dist/lib/patterns/aws/delegatedDomainPattern.d.ts +13 -4
- package/dist/lib/patterns/aws/delegatedDomainPattern.js +57 -8
- package/dist/lib/patterns/aws/devSubstrate.d.ts +2 -1
- package/dist/lib/patterns/aws/devSubstrate.js +18 -8
- package/dist/lib/patterns/aws/dnsRecordComposer.d.ts +2 -9
- package/dist/lib/patterns/aws/dnsRecordComposer.js +5 -97
- package/dist/lib/patterns/aws/domain.d.ts +3 -2
- package/dist/lib/patterns/aws/domain.js +3 -2
- package/dist/lib/patterns/aws/domainValidation.js +71 -46
- package/dist/lib/patterns/aws/externalRecordsPattern.js +2 -11
- package/dist/lib/patterns/aws/index.d.ts +1 -3
- package/dist/lib/patterns/aws/index.js +2 -3
- package/dist/lib/patterns/aws/interfaces/domain.d.ts +22 -8
- package/dist/lib/patterns/aws/interfaces/index.d.ts +0 -1
- package/dist/lib/patterns/aws/interfaces/index.js +3 -0
- package/dist/lib/patterns/aws/interfaces/pattern.d.ts +55 -10
- package/dist/lib/patterns/aws/patternDomain.d.ts +77 -0
- package/dist/lib/patterns/aws/patternDomain.js +198 -0
- package/dist/lib/patterns/aws/payload.js +23 -30
- package/dist/lib/patterns/aws/staticSite.d.ts +1 -0
- package/dist/lib/patterns/aws/staticSite.js +28 -33
- package/dist/lib/resources/aws/base/awsStack.d.ts +8 -0
- package/dist/lib/resources/aws/base/awsStack.js +20 -17
- package/dist/lib/resources/aws/compute/ecs.d.ts +1 -1
- package/dist/lib/resources/aws/compute/ecs.js +1 -1
- package/dist/lib/resources/aws/compute/ecsNetworking.d.ts +1 -1
- package/dist/lib/resources/aws/compute/ecsNetworking.js +212 -47
- package/dist/lib/resources/aws/compute/ecsTaskDefinition.js +4 -4
- package/dist/lib/resources/aws/compute/ecsTypes.d.ts +60 -9
- package/dist/lib/resources/aws/compute/ecsValidation.d.ts +14 -1
- package/dist/lib/resources/aws/compute/ecsValidation.js +51 -0
- package/dist/lib/resources/aws/logging/logGroup.d.ts +6 -0
- package/dist/lib/resources/aws/logging/logGroup.js +11 -1
- package/dist/lib/resources/aws/networking/dnsRecord/aRecord.js +3 -2
- package/dist/lib/resources/aws/networking/dnsRecord/aaaaRecord.js +3 -2
- package/dist/lib/resources/aws/networking/dnsRecord/aliasRecord.d.ts +23 -1
- package/dist/lib/resources/aws/networking/dnsRecord/aliasRecord.js +14 -3
- package/dist/lib/resources/aws/networking/dnsRecord/caaRecord.js +3 -2
- package/dist/lib/resources/aws/networking/dnsRecord/cnameRecord.js +3 -2
- package/dist/lib/resources/aws/networking/dnsRecord/dnsRecordBase.d.ts +10 -1
- package/dist/lib/resources/aws/networking/dnsRecord/dnsRecordBase.js +21 -4
- package/dist/lib/resources/aws/networking/dnsRecord/mxRecord.js +3 -2
- package/dist/lib/resources/aws/networking/dnsRecord/nsRecord.js +3 -2
- package/dist/lib/resources/aws/networking/dnsRecord/srvRecord.js +3 -2
- package/dist/lib/resources/aws/networking/dnsRecord/txtRecord.js +3 -2
- package/dist/lib/resources/aws/networking/domainCertificate.d.ts +28 -4
- package/dist/lib/resources/aws/networking/domainCertificate.js +24 -10
- package/dist/lib/resources/aws/networking/hostedZone.d.ts +11 -0
- package/dist/lib/resources/aws/networking/hostedZone.js +6 -0
- package/dist/lib/utils/costAllocationTags.d.ts +9 -0
- package/dist/lib/utils/costAllocationTags.js +6 -1
- package/dist/lib/utils/dnsRecordRegistry.d.ts +52 -0
- package/dist/lib/utils/dnsRecordRegistry.js +64 -0
- package/dist/lib/utils/domainTypes.d.ts +43 -36
- package/dist/lib/utils/domainTypes.js +65 -2
- package/dist/lib/utils/managedDomainContext.d.ts +37 -0
- package/dist/lib/utils/managedDomainContext.js +111 -0
- package/package.json +4 -4
package/dist/lib/app.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { type CloudAssembly } from "aws-cdk-lib/cx-api";
|
|
|
3
3
|
import { type Construct } from "constructs";
|
|
4
4
|
import { type IVpc } from "aws-cdk-lib/aws-ec2";
|
|
5
5
|
import { type Role } from "aws-cdk-lib/aws-iam";
|
|
6
|
-
import { AwsStack } from "./resources/index.js";
|
|
6
|
+
import { AwsStack, type AwsStackProps } from "./resources/index.js";
|
|
7
7
|
import { type Ecr } from "./resources/aws/storage/ecr.js";
|
|
8
8
|
import { type AnyDatabase } from "./patterns/aws/database.js";
|
|
9
9
|
import { type INetworkProps, type Network } from "./patterns/aws/network.js";
|
|
@@ -123,8 +123,10 @@ export declare class App extends CdkApp {
|
|
|
123
123
|
*
|
|
124
124
|
* @param key - The key of the stack
|
|
125
125
|
* @param dependencies - The stack(s) that this stack depends on
|
|
126
|
+
* @param props - Stack props applied on first creation only (cached
|
|
127
|
+
* stacks are returned as-is)
|
|
126
128
|
*/
|
|
127
|
-
getStack(key: string, dependencies?: AwsStack | AwsStack[]): AwsStack;
|
|
129
|
+
getStack(key: string, dependencies?: AwsStack | AwsStack[], props?: AwsStackProps): AwsStack;
|
|
128
130
|
/**
|
|
129
131
|
* Retrieve default compute stack - named as `${this.name}Compute`
|
|
130
132
|
*
|
|
@@ -162,6 +164,21 @@ export declare class App extends CdkApp {
|
|
|
162
164
|
* require VPC, but we maintain consistent stack dependency patterns.
|
|
163
165
|
*/
|
|
164
166
|
getDefaultMessagingStack(): AwsStack;
|
|
167
|
+
/**
|
|
168
|
+
* Retrieve the app-owned us-east-1 certificate stack — named
|
|
169
|
+
* `${this.name}UsEast1Certificates` (domain gold-plating D3, BYO path).
|
|
170
|
+
*
|
|
171
|
+
* CloudFront accepts viewer certificates from us-east-1 only, so
|
|
172
|
+
* CloudFront-consuming patterns (staticsite, payload) whose app deploys to
|
|
173
|
+
* another region provision their DomainCertificate here; the distribution
|
|
174
|
+
* in the Cdn stack consumes the ARN through CDK cross-region references.
|
|
175
|
+
*
|
|
176
|
+
* `crossRegionReferences: true` must be set on BOTH the producing and the
|
|
177
|
+
* consuming stack; both get it at construction (the Cdn stack in
|
|
178
|
+
* getDefaultCdnStack, this stack here) and the emitted
|
|
179
|
+
* Custom::CrossRegionExportWriter/Reader pair is pinned by template test.
|
|
180
|
+
*/
|
|
181
|
+
getUsEast1CertificateStack(): AwsStack;
|
|
165
182
|
/**
|
|
166
183
|
* Get a VPC by name. If no name is provided, returns the default VPC.
|
|
167
184
|
*
|
package/dist/lib/app.js
CHANGED
|
@@ -18,8 +18,10 @@ import AuditRoleFactory from "./resources/aws/audit/auditRole.js";
|
|
|
18
18
|
import { FJALL_AUDIT_CONFIG } from "./config/audit.js";
|
|
19
19
|
import { FjallLogger } from "./utils/validationLogger.js";
|
|
20
20
|
import { getManifestCollector, writeManifest } from "./utils/manifestWriter.js";
|
|
21
|
+
import { resetDnsRecordRegistry } from "./utils/dnsRecordRegistry.js";
|
|
21
22
|
import { toPascalCase, toKebab } from "./utils/capitaliseString.js";
|
|
22
23
|
import { COST_ALLOCATION_TAGS } from "./utils/costAllocationTags.js";
|
|
24
|
+
import { DEFAULT_ORG_ID, resolveOrgId } from "./utils/cdkContext.js";
|
|
23
25
|
/**
|
|
24
26
|
* The basic corner-stone of all Fjall-hosted applications.
|
|
25
27
|
* This class is a singleton and should be used to create and manage
|
|
@@ -167,6 +169,10 @@ export class App extends CdkApp {
|
|
|
167
169
|
}
|
|
168
170
|
static resetForTesting() {
|
|
169
171
|
App.instance = null;
|
|
172
|
+
// App-scoped synth state resets with the App singleton — the DNS
|
|
173
|
+
// record-collision registry (D5) follows the manifest-collector lifecycle
|
|
174
|
+
// without requiring a third per-test reset call.
|
|
175
|
+
resetDnsRecordRegistry();
|
|
170
176
|
}
|
|
171
177
|
/**
|
|
172
178
|
* Retrieve a stack by key. If the stack does not exist, it will be created.
|
|
@@ -174,8 +180,10 @@ export class App extends CdkApp {
|
|
|
174
180
|
*
|
|
175
181
|
* @param key - The key of the stack
|
|
176
182
|
* @param dependencies - The stack(s) that this stack depends on
|
|
183
|
+
* @param props - Stack props applied on first creation only (cached
|
|
184
|
+
* stacks are returned as-is)
|
|
177
185
|
*/
|
|
178
|
-
getStack(key, dependencies) {
|
|
186
|
+
getStack(key, dependencies, props) {
|
|
179
187
|
// Apply the aspect once before creating the first stack
|
|
180
188
|
if (!this.aspectApplied &&
|
|
181
189
|
Object.keys(this.stacks).length === 0 &&
|
|
@@ -183,7 +191,7 @@ export class App extends CdkApp {
|
|
|
183
191
|
this.applyTagsAspect();
|
|
184
192
|
}
|
|
185
193
|
if (!this.stacks[key]) {
|
|
186
|
-
this.stacks[key] = new AwsStack(key, dependencies);
|
|
194
|
+
this.stacks[key] = new AwsStack(key, dependencies, props);
|
|
187
195
|
}
|
|
188
196
|
return this.stacks[key];
|
|
189
197
|
}
|
|
@@ -224,7 +232,10 @@ export class App extends CdkApp {
|
|
|
224
232
|
* stack is synthesised.
|
|
225
233
|
*/
|
|
226
234
|
getDefaultCdnStack() {
|
|
227
|
-
|
|
235
|
+
// crossRegionReferences is enabled at creation so the Cdn stack can
|
|
236
|
+
// consume a us-east-1 certificate (domain gold-plating D3, BYO path).
|
|
237
|
+
// The flag is inert unless a cross-region reference is synthesised.
|
|
238
|
+
return this.getStack(`${this.stackPrefix}Cdn`, this.networkDisabled ? undefined : this.getDefaultNetworkStack(), { crossRegionReferences: true });
|
|
228
239
|
}
|
|
229
240
|
/**
|
|
230
241
|
* Retrieve default messaging stack - named as `${this.name}Messaging`
|
|
@@ -236,6 +247,39 @@ export class App extends CdkApp {
|
|
|
236
247
|
getDefaultMessagingStack() {
|
|
237
248
|
return this.getStack(`${this.stackPrefix}Messaging`, this.getDefaultNetworkStack());
|
|
238
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* Retrieve the app-owned us-east-1 certificate stack — named
|
|
252
|
+
* `${this.name}UsEast1Certificates` (domain gold-plating D3, BYO path).
|
|
253
|
+
*
|
|
254
|
+
* CloudFront accepts viewer certificates from us-east-1 only, so
|
|
255
|
+
* CloudFront-consuming patterns (staticsite, payload) whose app deploys to
|
|
256
|
+
* another region provision their DomainCertificate here; the distribution
|
|
257
|
+
* in the Cdn stack consumes the ARN through CDK cross-region references.
|
|
258
|
+
*
|
|
259
|
+
* `crossRegionReferences: true` must be set on BOTH the producing and the
|
|
260
|
+
* consuming stack; both get it at construction (the Cdn stack in
|
|
261
|
+
* getDefaultCdnStack, this stack here) and the emitted
|
|
262
|
+
* Custom::CrossRegionExportWriter/Reader pair is pinned by template test.
|
|
263
|
+
*/
|
|
264
|
+
getUsEast1CertificateStack() {
|
|
265
|
+
const key = `${this.stackPrefix}UsEast1Certificates`;
|
|
266
|
+
if (!this.stacks[key]) {
|
|
267
|
+
const account = process.env.CDK_DEFAULT_ACCOUNT;
|
|
268
|
+
if (!account) {
|
|
269
|
+
throw new Error(`App '${this.name}': the us-east-1 certificate stack needs a ` +
|
|
270
|
+
"concrete account (got CDK_DEFAULT_ACCOUNT undefined). " +
|
|
271
|
+
"Cross-region references resolve only between env-pinned stacks " +
|
|
272
|
+
"— deploy through the Fjall CLI, or export CDK_DEFAULT_ACCOUNT " +
|
|
273
|
+
"and CDK_DEFAULT_REGION before synthesising.");
|
|
274
|
+
}
|
|
275
|
+
this.stacks[key] = new AwsStack(key, undefined, {
|
|
276
|
+
accountId: account,
|
|
277
|
+
region: "us-east-1",
|
|
278
|
+
crossRegionReferences: true
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return this.stacks[key];
|
|
282
|
+
}
|
|
239
283
|
/**
|
|
240
284
|
* Get a VPC by name. If no name is provided, returns the default VPC.
|
|
241
285
|
*
|
|
@@ -702,6 +746,15 @@ export class App extends CdkApp {
|
|
|
702
746
|
}
|
|
703
747
|
applyTagsAspect() {
|
|
704
748
|
if (!this.aspectApplied && Object.keys(this.globalTags).length > 0) {
|
|
749
|
+
// Owner fallback resolves lazily HERE, not in initialiseStandardTags:
|
|
750
|
+
// at construction the orgId context may not be set yet, and an explicit
|
|
751
|
+
// addTags owner must win — by this point it is already in globalTags.
|
|
752
|
+
if (this.globalTags[COST_ALLOCATION_TAGS.OWNER] === undefined) {
|
|
753
|
+
const orgId = resolveOrgId(this.node);
|
|
754
|
+
if (orgId !== undefined && orgId !== DEFAULT_ORG_ID) {
|
|
755
|
+
this.globalTags[COST_ALLOCATION_TAGS.OWNER] = orgId;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
705
758
|
// Apply standard tags using Tags.of(this).add()
|
|
706
759
|
for (const [key, value] of Object.entries(this.globalTags)) {
|
|
707
760
|
Tags.of(this).add(key, value);
|
|
@@ -748,7 +801,11 @@ export class App extends CdkApp {
|
|
|
748
801
|
return this.manifestCollector;
|
|
749
802
|
}
|
|
750
803
|
synth(options) {
|
|
751
|
-
//
|
|
804
|
+
// Tagging is an invariant of synthesis, not of getStack(): governance
|
|
805
|
+
// stacks (Organisation/Account/Platform) and raw `extends Stack` classes
|
|
806
|
+
// attach directly to the App and never call getStack(), so without this
|
|
807
|
+
// they synthesise untagged. Idempotent via the aspectApplied guard.
|
|
808
|
+
this.applyTagsAspect();
|
|
752
809
|
const assembly = super.synth(options);
|
|
753
810
|
// After synthesis, write Fjall manifest to cdk.out
|
|
754
811
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CfnOutput, Stack } from "aws-cdk-lib";
|
|
1
|
+
import { CfnOutput, Stack, Tags } from "aws-cdk-lib";
|
|
2
2
|
import { EcrDefaultImage, SharedAlarmTopic } from "../../config/aws/index.js";
|
|
3
3
|
import { ManagementEventsTrail } from "../../config/aws/cloudTrail.js";
|
|
4
4
|
import { OidcConnector } from "../../config/aws/oidcConnector.js";
|
|
@@ -6,6 +6,7 @@ import { AccountMonitoringRole } from "../../config/aws/accountMonitoringRole.js
|
|
|
6
6
|
import { AccountAuditRole } from "../../config/aws/accountAuditRole.js";
|
|
7
7
|
import { CDK_CONTEXT_KEYS, resolveAccountTrailState } from "../../utils/cdkContext.js";
|
|
8
8
|
import { getConfig } from "../../utils/getConfig.js";
|
|
9
|
+
import { COST_ALLOCATION_TAGS } from "../../utils/costAllocationTags.js";
|
|
9
10
|
import { DisasterRecovery } from "../../config/aws/disasterRecovery.js";
|
|
10
11
|
import { S3BlockPublicAccess } from "../../config/aws/s3BlockPublicAccess.js";
|
|
11
12
|
import { EbsDefaultEncryption } from "../../config/aws/ebsDefaultEncryption.js";
|
|
@@ -28,6 +29,10 @@ export class Account extends Stack {
|
|
|
28
29
|
}
|
|
29
30
|
const env = props.env ?? { region, account: accountId };
|
|
30
31
|
super(scope, id, { ...props, env });
|
|
32
|
+
// Governance apps never name the App, so the App-level service tag would
|
|
33
|
+
// read "FjallApp"; the stack-scope tag wins over App scope and names the
|
|
34
|
+
// governance tier instead ("Organisation"/"Account"/"Platform").
|
|
35
|
+
Tags.of(this).add(COST_ALLOCATION_TAGS.SERVICE, id);
|
|
31
36
|
this.resolvedRegion = region ?? this.region;
|
|
32
37
|
const orgId = this.node.tryGetContext(CDK_CONTEXT_KEYS.ORG_ID);
|
|
33
38
|
if (orgId) {
|
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
import type { Construct } from "constructs";
|
|
2
2
|
import type { IHostedZone } from "aws-cdk-lib/aws-route53";
|
|
3
3
|
import type { ICertificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
4
|
-
import type { ManualRecord, Route53ApexProps
|
|
4
|
+
import type { ManualRecord, Route53ApexProps } from "./interfaces/domain.js";
|
|
5
5
|
export interface ApexDomainPatternResult {
|
|
6
6
|
readonly hostedZone: IHostedZone;
|
|
7
7
|
readonly certificates: Map<string, ICertificate>;
|
|
8
8
|
readonly nameServers: string[] | undefined;
|
|
9
9
|
readonly manualRecords: ManualRecord[];
|
|
10
|
-
readonly delegations: SubdomainDelegation[];
|
|
11
10
|
}
|
|
12
11
|
/**
|
|
13
12
|
* Composition for `registrar: "route53"`. Creates (or imports) the apex
|
|
14
|
-
* `HostedZone
|
|
15
|
-
* each child's nameservers published as exports), and composes all user
|
|
16
|
-
* records + certificates.
|
|
13
|
+
* `HostedZone` and composes all user records + certificates.
|
|
17
14
|
*
|
|
18
|
-
* Delegation
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* parent
|
|
24
|
-
*
|
|
15
|
+
* Delegation direction (D8): child writes NS. The apex zone's contribution
|
|
16
|
+
* to delegation is the org-gated `DelegationRole` the `HostedZone` wrapper
|
|
17
|
+
* creates on the create path — the delegated child `Domain` assumes that
|
|
18
|
+
* role by LITERAL ARN (`parentDelegationRoleArn`) and UPSERTs its own NS
|
|
19
|
+
* records into this zone via `CrossAccountZoneDelegationRecord`. The old
|
|
20
|
+
* parent-writes `delegations[]` path (NS records from `Fn.importValue` of
|
|
21
|
+
* child nameserver exports) was removed with the H13 legacy layer: it was
|
|
22
|
+
* export-locked, child-first-deploy-ordered, and removal required a parent
|
|
23
|
+
* redeploy.
|
|
25
24
|
*/
|
|
26
25
|
export declare function composeApexDomain(scope: Construct, props: Route53ApexProps): ApexDomainPatternResult;
|
|
@@ -1,24 +1,21 @@
|
|
|
1
|
-
import { Fn } from "aws-cdk-lib";
|
|
2
|
-
import { getDomainExportNames } from "@fjall/util";
|
|
3
1
|
import { HostedZone } from "../../resources/aws/networking/hostedZone.js";
|
|
4
2
|
import { DomainCertificate } from "../../resources/aws/networking/domainCertificate.js";
|
|
5
|
-
import { NsRecord } from "../../resources/aws/networking/dnsRecord/index.js";
|
|
6
3
|
import { composeTypedDnsRecords } from "./dnsRecordComposer.js";
|
|
7
4
|
import { toPascalCase, getSafeZoneName } from "../../utils/capitaliseString.js";
|
|
8
5
|
import { resolveOrgId } from "../../utils/cdkContext.js";
|
|
9
6
|
/**
|
|
10
7
|
* Composition for `registrar: "route53"`. Creates (or imports) the apex
|
|
11
|
-
* `HostedZone
|
|
12
|
-
* each child's nameservers published as exports), and composes all user
|
|
13
|
-
* records + certificates.
|
|
8
|
+
* `HostedZone` and composes all user records + certificates.
|
|
14
9
|
*
|
|
15
|
-
* Delegation
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* parent
|
|
21
|
-
*
|
|
10
|
+
* Delegation direction (D8): child writes NS. The apex zone's contribution
|
|
11
|
+
* to delegation is the org-gated `DelegationRole` the `HostedZone` wrapper
|
|
12
|
+
* creates on the create path — the delegated child `Domain` assumes that
|
|
13
|
+
* role by LITERAL ARN (`parentDelegationRoleArn`) and UPSERTs its own NS
|
|
14
|
+
* records into this zone via `CrossAccountZoneDelegationRecord`. The old
|
|
15
|
+
* parent-writes `delegations[]` path (NS records from `Fn.importValue` of
|
|
16
|
+
* child nameserver exports) was removed with the H13 legacy layer: it was
|
|
17
|
+
* export-locked, child-first-deploy-ordered, and removal required a parent
|
|
18
|
+
* redeploy.
|
|
22
19
|
*/
|
|
23
20
|
export function composeApexDomain(scope, props) {
|
|
24
21
|
const safeZone = toPascalCase(getSafeZoneName(props.zoneName));
|
|
@@ -51,22 +48,6 @@ export function composeApexDomain(scope, props) {
|
|
|
51
48
|
if (props.records && props.records.length > 0) {
|
|
52
49
|
composeTypedDnsRecords(scope, hostedZoneConstruct.hostedZone, props.zoneName, props.records);
|
|
53
50
|
}
|
|
54
|
-
const delegations = props.delegations ?? [];
|
|
55
|
-
delegations.forEach((delegation, index) => {
|
|
56
|
-
const childZoneName = `${delegation.subdomain}.${props.zoneName}`;
|
|
57
|
-
const safeChild = toPascalCase(getSafeZoneName(childZoneName));
|
|
58
|
-
const childExports = getDomainExportNames(childZoneName);
|
|
59
|
-
// Cross-phase dependency: child stack must deploy first; CFN fails here
|
|
60
|
-
// at deploy if the nameservers export does not yet exist.
|
|
61
|
-
const nameserversToken = Fn.importValue(childExports.nameservers);
|
|
62
|
-
new NsRecord(scope, `${safeZone}Delegation${safeChild}${index}`, {
|
|
63
|
-
zone: hostedZoneConstruct.hostedZone,
|
|
64
|
-
zoneName: props.zoneName,
|
|
65
|
-
recordName: delegation.subdomain,
|
|
66
|
-
// Fn.split returns a list token suitable for a multi-value NS record.
|
|
67
|
-
values: Fn.split(",", nameserversToken)
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
51
|
return {
|
|
71
52
|
hostedZone: hostedZoneConstruct.hostedZone,
|
|
72
53
|
certificates,
|
|
@@ -75,8 +56,7 @@ export function composeApexDomain(scope, props) {
|
|
|
75
56
|
nameServers: hostedZoneConstruct.isImported
|
|
76
57
|
? undefined
|
|
77
58
|
: (hostedZoneConstruct.nameServers ?? undefined),
|
|
78
|
-
manualRecords: []
|
|
79
|
-
delegations
|
|
59
|
+
manualRecords: []
|
|
80
60
|
};
|
|
81
61
|
}
|
|
82
62
|
function normaliseCertificate(cert) {
|
|
@@ -1,23 +1,38 @@
|
|
|
1
|
-
import { Fn } from "aws-cdk-lib";
|
|
1
|
+
import { Fn, Token } from "aws-cdk-lib";
|
|
2
2
|
import { Certificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
3
3
|
import { CloudFrontDistribution } from "../../resources/aws/cdn/index.js";
|
|
4
4
|
import { isStorage } from "./storage.js";
|
|
5
5
|
import { isCompute, isEcsCompute, isLambdaCompute } from "./compute.js";
|
|
6
|
-
import { FjallLogger } from "../../utils/validationLogger.js";
|
|
7
6
|
/**
|
|
8
|
-
* Validates CDN props
|
|
7
|
+
* Validates CDN props — synth-time hard errors for shapes CloudFormation
|
|
8
|
+
* would only reject at deploy.
|
|
9
9
|
*/
|
|
10
10
|
function validateCdnProps(props) {
|
|
11
11
|
const hasCertificate = !!(props.certificate || props.certificateArn);
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
// CloudFront accepts viewer certificates from us-east-1 only. A literal
|
|
13
|
+
// ARN is inspectable at synth (design H3/D3 — the old advisory log is
|
|
14
|
+
// replaced by real us-east-1 provisioning in the patterns; this assert
|
|
15
|
+
// covers the BYO-ARN escape hatch), so fail here with the cure rather
|
|
16
|
+
// than at CloudFormation with InvalidViewerCertificate.
|
|
17
|
+
if (props.certificateArn !== undefined &&
|
|
18
|
+
!Token.isUnresolved(props.certificateArn)) {
|
|
19
|
+
const certificateRegion = props.certificateArn.split(":")[3];
|
|
20
|
+
if (certificateRegion !== "us-east-1") {
|
|
21
|
+
throw new Error(`CDN 'certificateArn' is in region '${certificateRegion}' ` +
|
|
22
|
+
`(got '${props.certificateArn}'). CloudFront accepts viewer ` +
|
|
23
|
+
"certificates from us-east-1 only — pass the ARN of a us-east-1 " +
|
|
24
|
+
"certificate, or set the pattern's domain zone identity so it " +
|
|
25
|
+
"provisions one.");
|
|
26
|
+
}
|
|
16
27
|
}
|
|
17
|
-
//
|
|
28
|
+
// Aliases without a viewer certificate fail at CloudFormation deploy; fail
|
|
29
|
+
// at synth with the cure instead.
|
|
18
30
|
if (props.domainNames && props.domainNames.length > 0 && !hasCertificate) {
|
|
19
|
-
|
|
20
|
-
"
|
|
31
|
+
throw new Error(`CDN 'domainNames' [${props.domainNames.join(", ")}] were provided ` +
|
|
32
|
+
"without a certificate. CloudFront serves custom domain names only " +
|
|
33
|
+
"with a us-east-1 ACM certificate — pass 'certificate' or " +
|
|
34
|
+
"'certificateArn', or configure the pattern's 'domain' with a zone " +
|
|
35
|
+
"identity so it provisions one.");
|
|
21
36
|
}
|
|
22
37
|
// Validate ALB origin
|
|
23
38
|
if (props.originType === "alb") {
|
|
@@ -13,6 +13,7 @@ import { resolveImportedSecret } from "../../resources/aws/secrets/index.js";
|
|
|
13
13
|
import App from "../../app.js";
|
|
14
14
|
import EcsCluster from "../../resources/aws/compute/ecs.js";
|
|
15
15
|
import { createScheduledTaskDefinition, createMigrationTaskDefinition } from "../../resources/aws/compute/ecsTaskDefinition.js";
|
|
16
|
+
import { DEFAULT_LOG_RETENTION } from "../../resources/aws/compute/ecsConstants.js";
|
|
16
17
|
import { EcsLifecycleHookMigration } from "../../resources/aws/compute/ecsLifecycleHookMigration.js";
|
|
17
18
|
import { Role } from "../../resources/aws/iam/role.js";
|
|
18
19
|
import { LogGroup } from "../../resources/aws/logging/logGroup.js";
|
|
@@ -24,7 +25,7 @@ import { FjallLogger } from "../../utils/validationLogger.js";
|
|
|
24
25
|
import { VALIDATION_PATTERNS } from "@fjall/generator";
|
|
25
26
|
import { evaluateBakeGuard } from "@fjall/util/docker";
|
|
26
27
|
import { toKebab, buildParameterPath } from "@fjall/util";
|
|
27
|
-
import { validateSecretName } from "../../resources/aws/compute/ecsValidation.js";
|
|
28
|
+
import { validateEcsDomainConfig, validateSecretName } from "../../resources/aws/compute/ecsValidation.js";
|
|
28
29
|
import { COMPUTE_DEFAULTS, collectImportedSecretNames } from "./compute.js";
|
|
29
30
|
import { isHookMigrations } from "./computeEcsTypes.js";
|
|
30
31
|
export { ScalingType } from "./computeEcsTypes.js";
|
|
@@ -80,6 +81,11 @@ function validateSeparateTaskDef(serviceName, separateTaskDef) {
|
|
|
80
81
|
* Extracted for clarity and detail parity with database/network patterns.
|
|
81
82
|
*/
|
|
82
83
|
export function validateEcsProps(props) {
|
|
84
|
+
// Mirror of the resources-layer domainConfig checks (H8 legacy `region`
|
|
85
|
+
// rejection, routingPolicy shape) — defence-in-depth per generator-standards
|
|
86
|
+
// § "Validate at the Lowest Layer the Field Belongs To". The cluster name is
|
|
87
|
+
// the construct id, unavailable here, so errors prefix with the domain name.
|
|
88
|
+
validateEcsDomainConfig(props.cluster?.domainConfig);
|
|
83
89
|
// Validate services array exists and is not empty
|
|
84
90
|
if (!props.services || props.services.length === 0) {
|
|
85
91
|
throw new Error("At least one service must be specified in 'services' array.");
|
|
@@ -924,13 +930,16 @@ export class EcsCompute extends Construct {
|
|
|
924
930
|
});
|
|
925
931
|
}
|
|
926
932
|
}
|
|
933
|
+
// `logRetention` must not reach the driver: AwsLogDriver.bind() creates
|
|
934
|
+
// a raw RETAIN LogGroup inside aws-cdk-lib, escaping the env-aware
|
|
935
|
+
// removal default (D17).
|
|
936
|
+
const logGroup = entry.logGroup ??
|
|
937
|
+
new LogGroup(this, `${id}${toPascalCase(entry.name)}LogGroup`, {
|
|
938
|
+
retention: entry.logRetention ?? DEFAULT_LOG_RETENTION
|
|
939
|
+
});
|
|
927
940
|
const logging = new AwsLogDriver({
|
|
928
941
|
streamPrefix: `/ecs-scheduled/${id}/${entry.name}`,
|
|
929
|
-
|
|
930
|
-
...(entry.logGroup === undefined &&
|
|
931
|
-
entry.logRetention !== undefined && {
|
|
932
|
-
logRetention: entry.logRetention
|
|
933
|
-
})
|
|
942
|
+
logGroup
|
|
934
943
|
});
|
|
935
944
|
const container = taskDef.addContainer(`${id}${toPascalCase(entry.name)}Container`, {
|
|
936
945
|
image: entry.image,
|
|
@@ -501,7 +501,11 @@ export interface EcsScheduledTaskConfig {
|
|
|
501
501
|
secrets?: Record<string, EcsSecret>;
|
|
502
502
|
/** Pre-existing CDK log group. Mutually exclusive with `logRetention`. */
|
|
503
503
|
logGroup?: ILogGroup;
|
|
504
|
-
/**
|
|
504
|
+
/**
|
|
505
|
+
* When `logGroup` is omitted, an explicit framework log group is created
|
|
506
|
+
* with this retention (default: `DEFAULT_LOG_RETENTION`) and the env-aware
|
|
507
|
+
* removal default (D17).
|
|
508
|
+
*/
|
|
505
509
|
logRetention?: RetentionDays;
|
|
506
510
|
securityGroups?: ISecurityGroup[];
|
|
507
511
|
subnetSelection?: SubnetSelection;
|
|
@@ -518,6 +522,10 @@ export interface EcsClusterConfig {
|
|
|
518
522
|
* Domain for HTTPS access.
|
|
519
523
|
* - Omit: ALB created with default DNS (*.elb.amazonaws.com)
|
|
520
524
|
* - Specified: Creates ACM certificate + Route53 DNS A record
|
|
525
|
+
*
|
|
526
|
+
* Requires `domainConfig` to name a zone source (`managedDomain` or
|
|
527
|
+
* `hostedZone`) — Fjall never creates a hosted zone inside an app stack
|
|
528
|
+
* (design D1/H1), so a bare `domain` with no zone source throws at synth.
|
|
521
529
|
*/
|
|
522
530
|
domain?: string;
|
|
523
531
|
/**
|
|
@@ -534,9 +542,10 @@ export interface EcsClusterConfig {
|
|
|
534
542
|
*/
|
|
535
543
|
directAccess?: boolean;
|
|
536
544
|
/**
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
545
|
+
* Domain configuration: zone source (managed domain or imported zone),
|
|
546
|
+
* certificate, and routing policy (latency via `routingPolicy`, weighted,
|
|
547
|
+
* geo). Required for any custom domain; enables multi-region deployments
|
|
548
|
+
* with advanced DNS routing.
|
|
540
549
|
*/
|
|
541
550
|
domainConfig?: DomainConfig;
|
|
542
551
|
/**
|
|
@@ -9,9 +9,18 @@ export interface DelegatedDomainPatternResult {
|
|
|
9
9
|
readonly manualRecords: ManualRecord[];
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* Composition for `registrar: "external-delegated"
|
|
13
|
-
* for `{delegatedSubdomain}.{zoneName}`
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* Composition for `registrar: "external-delegated"` — the delegated-child
|
|
13
|
+
* topology. Creates a hosted zone for `{delegatedSubdomain}.{zoneName}` and,
|
|
14
|
+
* when `parentDelegationRoleArn` is present, UPSERTs the child NS into the
|
|
15
|
+
* parent zone via `CrossAccountZoneDelegationRecord` (D8 child-writes
|
|
16
|
+
* delegation; same shape as devSubstrate Case 1). The role ARN MUST be a
|
|
17
|
+
* literal — validated in `validateDomainProps` (VD2-a). When absent, the
|
|
18
|
+
* user delegates out of band (external registrar / manual NS) and a synth
|
|
19
|
+
* warning flags that the zone will not resolve until they do.
|
|
20
|
+
*
|
|
21
|
+
* `phase: "zone"` early-returns before certificate issuance (R2 cert-hang
|
|
22
|
+
* guard, shared `DomainDeployPhase` contract with `DevSubstrate`): issuing a
|
|
23
|
+
* DNS-validated certificate before the delegation has propagated hangs ACM
|
|
24
|
+
* until CloudFormation rolls back (~1 h).
|
|
16
25
|
*/
|
|
17
26
|
export declare function composeDelegatedDomain(scope: Construct, props: ExternalDelegatedProps): DelegatedDomainPatternResult;
|
|
@@ -1,22 +1,75 @@
|
|
|
1
|
+
import { Annotations } from "aws-cdk-lib";
|
|
2
|
+
import { Role } from "../../resources/aws/iam/index.js";
|
|
1
3
|
import { HostedZone } from "../../resources/aws/networking/hostedZone.js";
|
|
4
|
+
import { CrossAccountDelegationRecord } from "../../resources/aws/networking/crossAccountDelegationRecord.js";
|
|
2
5
|
import { DomainCertificate } from "../../resources/aws/networking/domainCertificate.js";
|
|
3
6
|
import { composeTypedDnsRecords } from "./dnsRecordComposer.js";
|
|
4
7
|
import { toPascalCase, getSafeZoneName } from "../../utils/capitaliseString.js";
|
|
8
|
+
import { DOMAIN_DEPLOY_DEFAULT_PHASE } from "../../utils/domainTypes.js";
|
|
5
9
|
/**
|
|
6
|
-
* Composition for `registrar: "external-delegated"
|
|
7
|
-
* for `{delegatedSubdomain}.{zoneName}`
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* Composition for `registrar: "external-delegated"` — the delegated-child
|
|
11
|
+
* topology. Creates a hosted zone for `{delegatedSubdomain}.{zoneName}` and,
|
|
12
|
+
* when `parentDelegationRoleArn` is present, UPSERTs the child NS into the
|
|
13
|
+
* parent zone via `CrossAccountZoneDelegationRecord` (D8 child-writes
|
|
14
|
+
* delegation; same shape as devSubstrate Case 1). The role ARN MUST be a
|
|
15
|
+
* literal — validated in `validateDomainProps` (VD2-a). When absent, the
|
|
16
|
+
* user delegates out of band (external registrar / manual NS) and a synth
|
|
17
|
+
* warning flags that the zone will not resolve until they do.
|
|
18
|
+
*
|
|
19
|
+
* `phase: "zone"` early-returns before certificate issuance (R2 cert-hang
|
|
20
|
+
* guard, shared `DomainDeployPhase` contract with `DevSubstrate`): issuing a
|
|
21
|
+
* DNS-validated certificate before the delegation has propagated hangs ACM
|
|
22
|
+
* until CloudFormation rolls back (~1 h).
|
|
10
23
|
*/
|
|
11
24
|
export function composeDelegatedDomain(scope, props) {
|
|
12
25
|
const effectiveZone = `${props.delegatedSubdomain}.${props.zoneName}`;
|
|
13
26
|
const safeZone = toPascalCase(getSafeZoneName(effectiveZone));
|
|
14
27
|
const hostedZoneConstruct = new HostedZone(scope, `${safeZone}HostedZone`, {
|
|
15
28
|
zoneName: effectiveZone,
|
|
29
|
+
// The PARENT account owns the delegation role — this is the child.
|
|
16
30
|
createDelegationRole: false,
|
|
17
31
|
costAllocationEnvironment: props.costAllocationEnvironment,
|
|
18
32
|
costAllocationDomain: props.zoneName
|
|
19
33
|
});
|
|
34
|
+
// D8 child-writes delegation. The in-stack custom resource assumes the
|
|
35
|
+
// parent account's DelegationRole by LITERAL ARN (Role.fromRoleArn) — NOT
|
|
36
|
+
// Fn.importValue, which resolves same-account only (VD2-a). The parent
|
|
37
|
+
// DelegationRole trusts the whole org (OrganizationPrincipal), so a child
|
|
38
|
+
// account can assume it. The CLI injects the ARN via
|
|
39
|
+
// ManagedDomainBinding.delegationRoleArn at deploy time.
|
|
40
|
+
if (props.parentDelegationRoleArn !== undefined) {
|
|
41
|
+
const parentRole = Role.fromRoleArn(scope, `${safeZone}ParentDelegationRole`, props.parentDelegationRoleArn);
|
|
42
|
+
new CrossAccountDelegationRecord(scope, `${safeZone}DelegationRecord`, {
|
|
43
|
+
delegationRole: parentRole,
|
|
44
|
+
delegatedZone: hostedZoneConstruct.hostedZone,
|
|
45
|
+
delegatedZoneName: effectiveZone,
|
|
46
|
+
parentHostedZoneName: props.zoneName,
|
|
47
|
+
costAllocationEnvironment: props.costAllocationEnvironment,
|
|
48
|
+
costAllocationDomain: props.zoneName
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
Annotations.of(scope).addWarning(`Zone '${effectiveZone}' will not resolve until it is delegated: no ` +
|
|
53
|
+
`parentDelegationRoleArn was provided, so no NS records are written ` +
|
|
54
|
+
`into '${props.zoneName}'. Deploy via the Fjall CLI (which injects ` +
|
|
55
|
+
`the parent DelegationRole ARN from the managed-domain binding) or ` +
|
|
56
|
+
`add the zone's NS records at the parent registrar manually.`);
|
|
57
|
+
}
|
|
58
|
+
const nameServers = hostedZoneConstruct.nameServers ?? [];
|
|
59
|
+
const records = props.records ?? [];
|
|
60
|
+
if (records.length > 0) {
|
|
61
|
+
composeTypedDnsRecords(scope, hostedZoneConstruct.hostedZone, effectiveZone, records);
|
|
62
|
+
}
|
|
63
|
+
// Step 2 of the R2 two-step gate: issuing the cert before the delegation
|
|
64
|
+
// NS has propagated hangs ACM DNS-validation, so "zone" stops here.
|
|
65
|
+
if ((props.phase ?? DOMAIN_DEPLOY_DEFAULT_PHASE) !== "full") {
|
|
66
|
+
return {
|
|
67
|
+
hostedZone: hostedZoneConstruct.hostedZone,
|
|
68
|
+
certificates: new Map(),
|
|
69
|
+
nameServers,
|
|
70
|
+
manualRecords: []
|
|
71
|
+
};
|
|
72
|
+
}
|
|
20
73
|
const certificates = new Map();
|
|
21
74
|
(props.certificates ?? []).forEach((cert, index) => {
|
|
22
75
|
const normalised = normaliseCertificate(cert);
|
|
@@ -31,10 +84,6 @@ export function composeDelegatedDomain(scope, props) {
|
|
|
31
84
|
});
|
|
32
85
|
certificates.set(normalised.domainName, dc.certificate);
|
|
33
86
|
});
|
|
34
|
-
if (props.records && props.records.length > 0) {
|
|
35
|
-
composeTypedDnsRecords(scope, hostedZoneConstruct.hostedZone, effectiveZone, props.records);
|
|
36
|
-
}
|
|
37
|
-
const nameServers = hostedZoneConstruct.nameServers ?? [];
|
|
38
87
|
return {
|
|
39
88
|
hostedZone: hostedZoneConstruct.hostedZone,
|
|
40
89
|
certificates,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
2
|
import type App from "../../app.js";
|
|
3
|
+
import { type DomainDeployPhase } from "../../utils/domainTypes.js";
|
|
3
4
|
/**
|
|
4
5
|
* Props for the internal `DevSubstrate` construct.
|
|
5
6
|
*
|
|
@@ -44,7 +45,7 @@ export interface IDevSubstrateProps {
|
|
|
44
45
|
* only after the delegated NS has propagated, else ACM DNS-validation hangs.
|
|
45
46
|
* Defaults to `"full"`. Inert unless `domain` is set.
|
|
46
47
|
*/
|
|
47
|
-
readonly phase?:
|
|
48
|
+
readonly phase?: DomainDeployPhase;
|
|
48
49
|
/**
|
|
49
50
|
* Public domain the substrate hangs a `dev.<appDomain>` child zone under. When
|
|
50
51
|
* omitted, the substrate stands up no zone/cert (early milestone — G2 reaches
|