Skip to content

Commit 930842c

Browse files
New: [AEA-6256] - update to API gateway and StateMachine constructs (#648)
## Summary - ✨ New Feature ### Details - feat: permit ApiGateway to skip domain record - fix: merge suppressions instead of re-writing - fix: support IP6 AAAA record --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 2fa86a2 commit 930842c

File tree

9 files changed

+238
-89
lines changed

9 files changed

+238
-89
lines changed

packages/cdkConstructs/src/config/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {CloudFormationClient, DescribeStacksCommand} from "@aws-sdk/client-cloudformation"
22
import {S3Client, HeadObjectCommand} from "@aws-sdk/client-s3"
33
import {StandardStackProps} from "../apps/createApp"
4+
import {CDK_ENV_PREFIX} from "../constants"
45

56
export function getConfigFromEnvVar(
67
varName: string,
7-
prefix: string = "CDK_CONFIG_",
8+
prefix: string = CDK_ENV_PREFIX,
89
defaultValue: string | undefined = undefined
910
): string {
1011
const value = process.env[prefix + varName]
@@ -19,7 +20,7 @@ export function getConfigFromEnvVar(
1920

2021
export function getBooleanConfigFromEnvVar(
2122
varName: string,
22-
prefix: string = "CDK_CONFIG_",
23+
prefix: string = CDK_ENV_PREFIX,
2324
defaultValue: string | undefined = undefined
2425
): boolean {
2526
const value = getConfigFromEnvVar(varName, prefix, defaultValue)
@@ -28,7 +29,7 @@ export function getBooleanConfigFromEnvVar(
2829

2930
export function getNumberConfigFromEnvVar(
3031
varName: string,
31-
prefix: string = "CDK_CONFIG_",
32+
prefix: string = CDK_ENV_PREFIX,
3233
defaultValue: string | undefined = undefined
3334
): number {
3435
const value = getConfigFromEnvVar(varName, prefix, defaultValue)

packages/cdkConstructs/src/constants.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
import {Fn} from "aws-cdk-lib"
22

3+
/** Default prefix used for CDK config environment variables. */
4+
export const CDK_ENV_PREFIX = "CDK_CONFIG_"
5+
36
/** Imported cross-stack account resource values used by constructs in this package. */
47
export const ACCOUNT_RESOURCES = {
58
CloudwatchEncryptionKMSPolicyArn: Fn.importValue("account-resources:CloudwatchEncryptionKMSPolicyArn"),
69
CloudwatchLogsKmsKeyArn: Fn.importValue("account-resources:CloudwatchLogsKmsKeyArn"),
710
EpsDomainName: Fn.importValue("eps-route53-resources:EPS-domain"),
811
EpsZoneId: Fn.importValue("eps-route53-resources:EPS-ZoneID"),
12+
LambdaAccessSecretsPolicy: Fn.importValue("account-resources:LambdaAccessSecretsPolicy"),
13+
LambdaDecryptSecretsKMSPolicy: Fn.importValue("account-resources:LambdaDecryptSecretsKMSPolicy"),
14+
SpinePrivateKeyARN: Fn.importValue("account-resources:SpinePrivateKey"),
15+
SpinePublicCertificateARN: Fn.importValue("account-resources:SpinePublicCertificate"),
16+
SpineASIDARN: Fn.importValue("account-resources:SpineASID"),
17+
SpinePartyKeyARN: Fn.importValue("account-resources:SpinePartyKey"),
18+
SpineCAChainARN: Fn.importValue("account-resources:SpineCAChain"),
919
TrustStoreBucket: Fn.importValue("account-resources:TrustStoreBucket"),
1020
TrustStoreBucketKMSKey: Fn.importValue("account-resources:TrustStoreBucketKMSKey"),
1121
TrustStoreDeploymentBucket: Fn.importValue("account-resources:TrustStoreDeploymentBucket")
@@ -17,3 +27,8 @@ export const LAMBDA_RESOURCES = {
1727
SplunkDeliveryStream: Fn.importValue("lambda-resources:SplunkDeliveryStream"),
1828
SplunkSubscriptionFilterRole: Fn.importValue("lambda-resources:SplunkSubscriptionFilterRole")
1929
}
30+
31+
/** Shared cfn-guard rule identifiers used for metadata suppressions. */
32+
export const CFN_GUARD_RULES = {
33+
LogGroupRetentionPeriodCheck: "CW_LOGGROUP_RETENTION_PERIOD_CHECK"
34+
} as const

packages/cdkConstructs/src/constructs/RestApiGateway.ts

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,36 @@ import {accessLogFormat} from "./RestApiGateway/accessLogFormat.js"
2424
import {Certificate, CertificateValidation} from "aws-cdk-lib/aws-certificatemanager"
2525
import {Bucket} from "aws-cdk-lib/aws-s3"
2626
import {BucketDeployment, Source} from "aws-cdk-lib/aws-s3-deployment"
27-
import {ARecord, HostedZone, RecordTarget} from "aws-cdk-lib/aws-route53"
27+
import {
28+
ARecord,
29+
AaaaRecord,
30+
HostedZone,
31+
IHostedZone,
32+
RecordTarget
33+
} from "aws-cdk-lib/aws-route53"
2834
import {ApiGateway as ApiGatewayTarget} from "aws-cdk-lib/aws-route53-targets"
2935
import {NagSuppressions} from "cdk-nag"
3036
import {ACCOUNT_RESOURCES, LAMBDA_RESOURCES} from "../constants"
37+
import {addSuppressions} from "../utils/helpers"
3138

3239
/** Configuration for creating a REST API with optional mTLS and log forwarding integrations. */
3340
export interface RestApiGatewayProps {
3441
/** Stack name, used as prefix for resource naming and DNS records. */
3542
readonly stackName: string
3643
/** Shared retention period for API and deployment-related log groups. */
3744
readonly logRetentionInDays: number
38-
/** Truststore object key to enable mTLS; leave undefined to disable mTLS. */
45+
/** Truststore object key to enable mTLS; leave undefined to disable mTLS or when enableServiceDomain is false. */
3946
readonly mutualTlsTrustStoreKey: string | undefined
4047
/** Enables creation of a second subscription filter to forward logs to CSOC. */
4148
readonly forwardCsocLogs: boolean
4249
/** Destination ARN used by the optional CSOC subscription filter. */
4350
readonly csocApiGatewayDestination: string
4451
/** Managed policies attached to the API Gateway execution role. */
4552
readonly executionPolicies: Array<IManagedPolicy>
53+
/**
54+
* When true (default), creates the custom service domain, ACM certificate, and Route53 records.
55+
*/
56+
readonly enableServiceDomain?: boolean
4657
}
4758

4859
/** Creates a regional REST API with standard logging, DNS, and optional mTLS/CSOC integration. */
@@ -63,18 +74,25 @@ export class RestApiGateway extends Construct {
6374
* mutualTlsTrustStoreKey: "truststore.pem",
6475
* forwardCsocLogs: true,
6576
* csocApiGatewayDestination: "arn:aws:logs:eu-west-2:123456789012:destination:csoc",
66-
* executionPolicies: [myLambdaInvokePolicy]
77+
* executionPolicies: [myLambdaInvokePolicy],
78+
* enableServiceDomain: true
6779
* })
6880
* api.api.root.addResource("patients")
6981
* ```
7082
*/
7183
public constructor(scope: Construct, id: string, props: RestApiGatewayProps) {
7284
super(scope, id)
7385

86+
const enableServiceDomain = (props.enableServiceDomain ?? true)
87+
7488
if (props.forwardCsocLogs && props.csocApiGatewayDestination === "") {
7589
throw new Error("csocApiGatewayDestination must be provided when forwardCsocLogs is true")
7690
}
7791

92+
if (!enableServiceDomain && props.mutualTlsTrustStoreKey) {
93+
throw new Error("mutualTlsTrustStoreKey should not be provided when enableServiceDomain is false")
94+
}
95+
7896
// Imports
7997
const cloudWatchLogsKmsKey = Key.fromKeyArn(
8098
this, "cloudWatchLogsKmsKey", ACCOUNT_RESOURCES.CloudwatchLogsKmsKeyArn)
@@ -94,12 +112,17 @@ export class RestApiGateway extends Construct {
94112
const trustStoreBucketKmsKey = Key.fromKeyArn(
95113
this, "TrustStoreBucketKmsKey", ACCOUNT_RESOURCES.TrustStoreBucketKMSKey)
96114

97-
const epsDomainName: string = ACCOUNT_RESOURCES.EpsDomainName
98-
const hostedZone = HostedZone.fromHostedZoneAttributes(this, "HostedZone", {
99-
hostedZoneId: ACCOUNT_RESOURCES.EpsZoneId,
100-
zoneName: epsDomainName
101-
})
102-
const serviceDomainName = `${props.stackName}.${epsDomainName}`
115+
let hostedZone: IHostedZone | undefined
116+
let serviceDomainName: string | undefined
117+
118+
if (enableServiceDomain) {
119+
const epsDomainName: string = ACCOUNT_RESOURCES.EpsDomainName
120+
hostedZone = HostedZone.fromHostedZoneAttributes(this, "HostedZone", {
121+
hostedZoneId: ACCOUNT_RESOURCES.EpsZoneId,
122+
zoneName: epsDomainName
123+
})
124+
serviceDomainName = `${props.stackName}.${epsDomainName}`
125+
}
103126

104127
// Resources
105128
const logGroup = new LogGroup(this, "ApiGatewayAccessLogGroup", {
@@ -125,14 +148,16 @@ export class RestApiGateway extends Construct {
125148
})
126149
}
127150

128-
const certificate = new Certificate(this, "Certificate", {
129-
domainName: serviceDomainName,
130-
validation: CertificateValidation.fromDns(hostedZone)
131-
})
151+
const certificate = enableServiceDomain && hostedZone && serviceDomainName
152+
? new Certificate(this, "Certificate", {
153+
domainName: serviceDomainName,
154+
validation: CertificateValidation.fromDns(hostedZone)
155+
})
156+
: undefined
132157

133158
let mtlsConfig: MTLSConfig | undefined
134159

135-
if (props.mutualTlsTrustStoreKey) {
160+
if (enableServiceDomain && props.mutualTlsTrustStoreKey) {
136161
const trustStoreKeyPrefix = `cpt-api/${props.stackName}-truststore`
137162
const logGroup = new LogGroup(this, "LambdaLogGroup", {
138163
encryptionKey: cloudWatchLogsKmsKey,
@@ -214,13 +239,16 @@ export class RestApiGateway extends Construct {
214239

215240
const apiGateway = new RestApi(this, "ApiGateway", {
216241
restApiName: `${props.stackName}-apigw`,
217-
domainName: {
218-
domainName: serviceDomainName,
219-
certificate: certificate,
220-
securityPolicy: SecurityPolicy.TLS_1_2,
221-
endpointType: EndpointType.REGIONAL,
222-
mtls: mtlsConfig
223-
},
242+
...(enableServiceDomain
243+
? {
244+
domainName: {
245+
domainName: serviceDomainName!,
246+
certificate: certificate!,
247+
securityPolicy: SecurityPolicy.TLS_1_2,
248+
endpointType: EndpointType.REGIONAL,
249+
mtls: mtlsConfig
250+
}
251+
} : {}),
224252
disableExecuteApiEndpoint: mtlsConfig ? true : false, // NOSONAR
225253
endpointConfiguration: {
226254
types: [EndpointType.REGIONAL]
@@ -239,21 +267,23 @@ export class RestApiGateway extends Construct {
239267
managedPolicies: props.executionPolicies
240268
}).withoutPolicyUpdates()
241269

242-
new ARecord(this, "ARecord", {
243-
recordName: props.stackName,
244-
target: RecordTarget.fromAlias(new ApiGatewayTarget(apiGateway)),
245-
zone: hostedZone
246-
})
270+
if (enableServiceDomain && hostedZone) {
271+
new ARecord(this, "ARecord", {
272+
recordName: props.stackName,
273+
target: RecordTarget.fromAlias(new ApiGatewayTarget(apiGateway)),
274+
zone: hostedZone
275+
})
247276

248-
const cfnStage = apiGateway.deploymentStage.node.defaultChild as CfnStage
249-
cfnStage.cfnOptions.metadata = {
250-
guard: {
251-
SuppressedRules: [
252-
"API_GW_CACHE_ENABLED_AND_ENCRYPTED"
253-
]
254-
}
277+
new AaaaRecord(this, "AaaaRecord", {
278+
recordName: props.stackName,
279+
target: RecordTarget.fromAlias(new ApiGatewayTarget(apiGateway)),
280+
zone: hostedZone
281+
})
255282
}
256283

284+
const cfnStage = apiGateway.deploymentStage.node.defaultChild as CfnStage
285+
addSuppressions([cfnStage], ["API_GW_CACHE_ENABLED_AND_ENCRYPTED"])
286+
257287
// Outputs
258288
this.api = apiGateway
259289
this.role = role

packages/cdkConstructs/src/constructs/StateMachine.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import {
2020
} from "aws-cdk-lib/aws-stepfunctions"
2121
import {Construct} from "constructs"
2222
import {CfnDeliveryStream} from "aws-cdk-lib/aws-kinesisfirehose"
23-
import {ACCOUNT_RESOURCES, LAMBDA_RESOURCES} from "../constants"
23+
import {ACCOUNT_RESOURCES, CFN_GUARD_RULES, LAMBDA_RESOURCES} from "../constants"
24+
import {addSuppressions} from "../utils/helpers"
2425

2526
/**
2627
* Configuration for provisioning an Express Step Functions state machine
@@ -109,13 +110,7 @@ export class ExpressStateMachine extends Construct {
109110
})
110111

111112
const cfnLogGroup = logGroup.node.defaultChild as CfnLogGroup
112-
cfnLogGroup.cfnOptions.metadata = {
113-
guard: {
114-
SuppressedRules: [
115-
"CW_LOGGROUP_RETENTION_PERIOD_CHECK"
116-
]
117-
}
118-
}
113+
addSuppressions([cfnLogGroup], [CFN_GUARD_RULES.LogGroupRetentionPeriodCheck])
119114

120115
if (addSplunkSubscriptionFilter) {
121116
if (splunkDeliveryStream) {

packages/cdkConstructs/src/constructs/lambdaSharedResources.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
} from "aws-cdk-lib/aws-iam"
1414
import {NagSuppressions} from "cdk-nag"
1515
import {LAMBDA_INSIGHTS_LAYER_ARNS} from "../config"
16-
import {ACCOUNT_RESOURCES, LAMBDA_RESOURCES} from "../constants"
16+
import {ACCOUNT_RESOURCES, CFN_GUARD_RULES, LAMBDA_RESOURCES} from "../constants"
1717
import {addSuppressions} from "../utils/helpers"
1818
import {CfnDeliveryStream} from "aws-cdk-lib/aws-kinesisfirehose"
1919
import {Stream} from "aws-cdk-lib/aws-kinesis"
@@ -74,7 +74,7 @@ export const createSharedLambdaResources = (
7474
})
7575

7676
const cfnlogGroup = logGroup.node.defaultChild as CfnLogGroup
77-
addSuppressions([cfnlogGroup], ["CW_LOGGROUP_RETENTION_PERIOD_CHECK"])
77+
addSuppressions([cfnlogGroup], [CFN_GUARD_RULES.LogGroupRetentionPeriodCheck])
7878

7979
if (addSplunkSubscriptionFilter) {
8080
// This is in an if statement to ensure correct value is used

packages/cdkConstructs/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ export * from "./config/index.js"
1313
export * from "./utils/helpers.js"
1414
export * from "./stacks/deleteUnusedStacks.js"
1515
export * from "./nag/pack/epsNagPack.js"
16-
export * from "./changesets/checkDestructiveChanges"
16+
export * from "./changesets/checkDestructiveChanges.js"
17+
export * from "./constants.js"

packages/cdkConstructs/tests/apps/createApp.test.ts

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
} from "vitest"
2525
import {createApp, type CreateAppParams} from "../../src/apps/createApp"
2626
import {AwsSolutionsChecks} from "cdk-nag"
27+
import {CDK_ENV_PREFIX} from "../../src/constants"
2728

2829
describe("createApp", () => {
2930
const originalEnv = process.env
@@ -51,10 +52,10 @@ describe("createApp", () => {
5152

5253
describe("when all environment variables are set", () => {
5354
beforeEach(() => {
54-
process.env.CDK_CONFIG_versionNumber = "1.2.3"
55-
process.env.CDK_CONFIG_commitId = "abc123def456"
56-
process.env.CDK_CONFIG_isPullRequest = "false"
57-
process.env.CDK_CONFIG_environment = "test-environment"
55+
process.env[`${CDK_ENV_PREFIX}versionNumber`] = "1.2.3"
56+
process.env[`${CDK_ENV_PREFIX}commitId`] = "abc123def456"
57+
process.env[`${CDK_ENV_PREFIX}isPullRequest`] = "false"
58+
process.env[`${CDK_ENV_PREFIX}environment`] = "test-environment"
5859
})
5960

6061
test("creates an App with correct configuration", () => {
@@ -124,10 +125,10 @@ describe("createApp", () => {
124125

125126
describe("when isPullRequest is true", () => {
126127
beforeEach(() => {
127-
process.env.CDK_CONFIG_versionNumber = "0.0.1-pr"
128-
process.env.CDK_CONFIG_commitId = "pr123"
129-
process.env.CDK_CONFIG_isPullRequest = "true"
130-
process.env.CDK_CONFIG_environment = "test-environment"
128+
process.env[`${CDK_ENV_PREFIX}versionNumber`] = "0.0.1-pr"
129+
process.env[`${CDK_ENV_PREFIX}commitId`] = "pr123"
130+
process.env[`${CDK_ENV_PREFIX}isPullRequest`] = "true"
131+
process.env[`${CDK_ENV_PREFIX}environment`] = "test-environment"
131132
})
132133

133134
test("correctly modifies props", () => {
@@ -159,41 +160,41 @@ describe("createApp", () => {
159160
describe("when environment variables are missing", () => {
160161

161162
test("throws error when versionNumber is not set", () => {
162-
process.env.CDK_CONFIG_commitId = "abc123"
163-
process.env.CDK_CONFIG_isPullRequest = "false"
164-
process.env.CDK_CONFIG_environment = "test-environment"
163+
process.env[`${CDK_ENV_PREFIX}commitId`] = "abc123"
164+
process.env[`${CDK_ENV_PREFIX}isPullRequest`] = "false"
165+
process.env[`${CDK_ENV_PREFIX}environment`] = "test-environment"
165166

166167
expect(() => {
167168
createApp(buildParams())
168-
}).toThrow("Environment variable CDK_CONFIG_versionNumber is not set")
169+
}).toThrow(`Environment variable ${CDK_ENV_PREFIX}versionNumber is not set`)
169170
})
170171

171172
test("throws error when commitId is not set", () => {
172-
process.env.CDK_CONFIG_versionNumber = "1.0.0"
173-
process.env.CDK_CONFIG_isPullRequest = "false"
174-
process.env.CDK_CONFIG_environment = "test-environment"
173+
process.env[`${CDK_ENV_PREFIX}versionNumber`] = "1.0.0"
174+
process.env[`${CDK_ENV_PREFIX}isPullRequest`] = "false"
175+
process.env[`${CDK_ENV_PREFIX}environment`] = "test-environment"
175176

176177
expect(() => {
177178
createApp(buildParams())
178-
}).toThrow("Environment variable CDK_CONFIG_commitId is not set")
179+
}).toThrow(`Environment variable ${CDK_ENV_PREFIX}commitId is not set`)
179180
})
180181

181182
test("throws error when isPullRequest is not set", () => {
182-
process.env.CDK_CONFIG_versionNumber = "1.0.0"
183-
process.env.CDK_CONFIG_commitId = "abc123"
184-
process.env.CDK_CONFIG_environment = "test-environment"
183+
process.env[`${CDK_ENV_PREFIX}versionNumber`] = "1.0.0"
184+
process.env[`${CDK_ENV_PREFIX}commitId`] = "abc123"
185+
process.env[`${CDK_ENV_PREFIX}environment`] = "test-environment"
185186
expect(() => {
186187
createApp(buildParams())
187-
}).toThrow("Environment variable CDK_CONFIG_isPullRequest is not set")
188+
}).toThrow(`Environment variable ${CDK_ENV_PREFIX}isPullRequest is not set`)
188189
})
189190

190191
test("throws error when environment is not set", () => {
191-
process.env.CDK_CONFIG_versionNumber = "1.0.0"
192-
process.env.CDK_CONFIG_commitId = "abc123"
193-
process.env.CDK_CONFIG_isPullRequest = "false"
192+
process.env[`${CDK_ENV_PREFIX}versionNumber`] = "1.0.0"
193+
process.env[`${CDK_ENV_PREFIX}commitId`] = "abc123"
194+
process.env[`${CDK_ENV_PREFIX}isPullRequest`] = "false"
194195
expect(() => {
195196
createApp(buildParams())
196-
}).toThrow("Environment variable CDK_CONFIG_environment is not set")
197+
}).toThrow(`Environment variable ${CDK_ENV_PREFIX}environment is not set`)
197198
})
198199

199200
})

0 commit comments

Comments
 (0)