Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions packages/cdkConstructs/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {CloudFormationClient, DescribeStacksCommand} from "@aws-sdk/client-cloudformation"
import {S3Client, HeadObjectCommand} from "@aws-sdk/client-s3"
import {StandardStackProps} from "../apps/createApp"
import {CDK_ENV_PREFIX} from "../constants"

export function getConfigFromEnvVar(
varName: string,
prefix: string = "CDK_CONFIG_",
prefix: string = CDK_ENV_PREFIX,
defaultValue: string | undefined = undefined
): string {
const value = process.env[prefix + varName]
Expand All @@ -19,7 +20,7 @@ export function getConfigFromEnvVar(

export function getBooleanConfigFromEnvVar(
varName: string,
prefix: string = "CDK_CONFIG_",
prefix: string = CDK_ENV_PREFIX,
defaultValue: string | undefined = undefined
): boolean {
const value = getConfigFromEnvVar(varName, prefix, defaultValue)
Expand All @@ -28,7 +29,7 @@ export function getBooleanConfigFromEnvVar(

export function getNumberConfigFromEnvVar(
varName: string,
prefix: string = "CDK_CONFIG_",
prefix: string = CDK_ENV_PREFIX,
defaultValue: string | undefined = undefined
): number {
const value = getConfigFromEnvVar(varName, prefix, defaultValue)
Expand Down
15 changes: 15 additions & 0 deletions packages/cdkConstructs/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import {Fn} from "aws-cdk-lib"

/** Default prefix used for CDK config environment variables. */
export const CDK_ENV_PREFIX = "CDK_CONFIG_"

/** Imported cross-stack account resource values used by constructs in this package. */
export const ACCOUNT_RESOURCES = {
CloudwatchEncryptionKMSPolicyArn: Fn.importValue("account-resources:CloudwatchEncryptionKMSPolicyArn"),
CloudwatchLogsKmsKeyArn: Fn.importValue("account-resources:CloudwatchLogsKmsKeyArn"),
EpsDomainName: Fn.importValue("eps-route53-resources:EPS-domain"),
EpsZoneId: Fn.importValue("eps-route53-resources:EPS-ZoneID"),
LambdaAccessSecretsPolicy: Fn.importValue("account-resources:LambdaAccessSecretsPolicy"),
LambdaDecryptSecretsKMSPolicy: Fn.importValue("account-resources:LambdaDecryptSecretsKMSPolicy"),
SpinePrivateKeyARN: Fn.importValue("account-resources:SpinePrivateKey"),
SpinePublicCertificateARN: Fn.importValue("account-resources:SpinePublicCertificate"),
SpineASIDARN: Fn.importValue("account-resources:SpineASID"),
SpinePartyKeyARN: Fn.importValue("account-resources:SpinePartyKey"),
SpineCAChainARN: Fn.importValue("account-resources:SpineCAChain"),
TrustStoreBucket: Fn.importValue("account-resources:TrustStoreBucket"),
TrustStoreBucketKMSKey: Fn.importValue("account-resources:TrustStoreBucketKMSKey"),
TrustStoreDeploymentBucket: Fn.importValue("account-resources:TrustStoreDeploymentBucket")
Expand All @@ -17,3 +27,8 @@ export const LAMBDA_RESOURCES = {
SplunkDeliveryStream: Fn.importValue("lambda-resources:SplunkDeliveryStream"),
SplunkSubscriptionFilterRole: Fn.importValue("lambda-resources:SplunkSubscriptionFilterRole")
}

/** Shared cfn-guard rule identifiers used for metadata suppressions. */
export const CFN_GUARD_RULES = {
LogGroupRetentionPeriodCheck: "CW_LOGGROUP_RETENTION_PERIOD_CHECK"
} as const
96 changes: 63 additions & 33 deletions packages/cdkConstructs/src/constructs/RestApiGateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,36 @@ import {accessLogFormat} from "./RestApiGateway/accessLogFormat.js"
import {Certificate, CertificateValidation} from "aws-cdk-lib/aws-certificatemanager"
import {Bucket} from "aws-cdk-lib/aws-s3"
import {BucketDeployment, Source} from "aws-cdk-lib/aws-s3-deployment"
import {ARecord, HostedZone, RecordTarget} from "aws-cdk-lib/aws-route53"
import {
ARecord,
AaaaRecord,
HostedZone,
IHostedZone,
RecordTarget
} from "aws-cdk-lib/aws-route53"
import {ApiGateway as ApiGatewayTarget} from "aws-cdk-lib/aws-route53-targets"
import {NagSuppressions} from "cdk-nag"
import {ACCOUNT_RESOURCES, LAMBDA_RESOURCES} from "../constants"
import {addSuppressions} from "../utils/helpers"

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

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

const enableServiceDomain = (props.enableServiceDomain ?? true)

if (props.forwardCsocLogs && props.csocApiGatewayDestination === "") {
throw new Error("csocApiGatewayDestination must be provided when forwardCsocLogs is true")
}

if (!enableServiceDomain && props.mutualTlsTrustStoreKey) {
throw new Error("mutualTlsTrustStoreKey should not be provided when enableServiceDomain is false")
}

// Imports
const cloudWatchLogsKmsKey = Key.fromKeyArn(
this, "cloudWatchLogsKmsKey", ACCOUNT_RESOURCES.CloudwatchLogsKmsKeyArn)
Expand All @@ -94,12 +112,17 @@ export class RestApiGateway extends Construct {
const trustStoreBucketKmsKey = Key.fromKeyArn(
this, "TrustStoreBucketKmsKey", ACCOUNT_RESOURCES.TrustStoreBucketKMSKey)

const epsDomainName: string = ACCOUNT_RESOURCES.EpsDomainName
const hostedZone = HostedZone.fromHostedZoneAttributes(this, "HostedZone", {
hostedZoneId: ACCOUNT_RESOURCES.EpsZoneId,
zoneName: epsDomainName
})
const serviceDomainName = `${props.stackName}.${epsDomainName}`
let hostedZone: IHostedZone | undefined
let serviceDomainName: string | undefined

if (enableServiceDomain) {
const epsDomainName: string = ACCOUNT_RESOURCES.EpsDomainName
hostedZone = HostedZone.fromHostedZoneAttributes(this, "HostedZone", {
hostedZoneId: ACCOUNT_RESOURCES.EpsZoneId,
zoneName: epsDomainName
})
serviceDomainName = `${props.stackName}.${epsDomainName}`
}

// Resources
const logGroup = new LogGroup(this, "ApiGatewayAccessLogGroup", {
Expand All @@ -125,14 +148,16 @@ export class RestApiGateway extends Construct {
})
}

const certificate = new Certificate(this, "Certificate", {
domainName: serviceDomainName,
validation: CertificateValidation.fromDns(hostedZone)
})
const certificate = enableServiceDomain && hostedZone && serviceDomainName
? new Certificate(this, "Certificate", {
domainName: serviceDomainName,
validation: CertificateValidation.fromDns(hostedZone)
})
: undefined

let mtlsConfig: MTLSConfig | undefined

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

const apiGateway = new RestApi(this, "ApiGateway", {
restApiName: `${props.stackName}-apigw`,
domainName: {
domainName: serviceDomainName,
certificate: certificate,
securityPolicy: SecurityPolicy.TLS_1_2,
endpointType: EndpointType.REGIONAL,
mtls: mtlsConfig
},
...(enableServiceDomain
? {
domainName: {
domainName: serviceDomainName!,
certificate: certificate!,
securityPolicy: SecurityPolicy.TLS_1_2,
endpointType: EndpointType.REGIONAL,
mtls: mtlsConfig
}
} : {}),
disableExecuteApiEndpoint: mtlsConfig ? true : false, // NOSONAR
endpointConfiguration: {
types: [EndpointType.REGIONAL]
Expand All @@ -239,21 +267,23 @@ export class RestApiGateway extends Construct {
managedPolicies: props.executionPolicies
}).withoutPolicyUpdates()

new ARecord(this, "ARecord", {
recordName: props.stackName,
target: RecordTarget.fromAlias(new ApiGatewayTarget(apiGateway)),
zone: hostedZone
})
if (enableServiceDomain && hostedZone) {
new ARecord(this, "ARecord", {
recordName: props.stackName,
target: RecordTarget.fromAlias(new ApiGatewayTarget(apiGateway)),
zone: hostedZone
})

const cfnStage = apiGateway.deploymentStage.node.defaultChild as CfnStage
cfnStage.cfnOptions.metadata = {
guard: {
SuppressedRules: [
"API_GW_CACHE_ENABLED_AND_ENCRYPTED"
]
}
new AaaaRecord(this, "AaaaRecord", {
recordName: props.stackName,
target: RecordTarget.fromAlias(new ApiGatewayTarget(apiGateway)),
zone: hostedZone
})
}

const cfnStage = apiGateway.deploymentStage.node.defaultChild as CfnStage
addSuppressions([cfnStage], ["API_GW_CACHE_ENABLED_AND_ENCRYPTED"])

// Outputs
this.api = apiGateway
this.role = role
Expand Down
11 changes: 3 additions & 8 deletions packages/cdkConstructs/src/constructs/StateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
} from "aws-cdk-lib/aws-stepfunctions"
import {Construct} from "constructs"
import {CfnDeliveryStream} from "aws-cdk-lib/aws-kinesisfirehose"
import {ACCOUNT_RESOURCES, LAMBDA_RESOURCES} from "../constants"
import {ACCOUNT_RESOURCES, CFN_GUARD_RULES, LAMBDA_RESOURCES} from "../constants"
import {addSuppressions} from "../utils/helpers"

/**
* Configuration for provisioning an Express Step Functions state machine
Expand Down Expand Up @@ -109,13 +110,7 @@ export class ExpressStateMachine extends Construct {
})

const cfnLogGroup = logGroup.node.defaultChild as CfnLogGroup
cfnLogGroup.cfnOptions.metadata = {
guard: {
SuppressedRules: [
"CW_LOGGROUP_RETENTION_PERIOD_CHECK"
]
}
}
addSuppressions([cfnLogGroup], [CFN_GUARD_RULES.LogGroupRetentionPeriodCheck])

if (addSplunkSubscriptionFilter) {
if (splunkDeliveryStream) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from "aws-cdk-lib/aws-iam"
import {NagSuppressions} from "cdk-nag"
import {LAMBDA_INSIGHTS_LAYER_ARNS} from "../config"
import {ACCOUNT_RESOURCES, LAMBDA_RESOURCES} from "../constants"
import {ACCOUNT_RESOURCES, CFN_GUARD_RULES, LAMBDA_RESOURCES} from "../constants"
import {addSuppressions} from "../utils/helpers"
import {CfnDeliveryStream} from "aws-cdk-lib/aws-kinesisfirehose"
import {Stream} from "aws-cdk-lib/aws-kinesis"
Expand Down Expand Up @@ -74,7 +74,7 @@ export const createSharedLambdaResources = (
})

const cfnlogGroup = logGroup.node.defaultChild as CfnLogGroup
addSuppressions([cfnlogGroup], ["CW_LOGGROUP_RETENTION_PERIOD_CHECK"])
addSuppressions([cfnlogGroup], [CFN_GUARD_RULES.LogGroupRetentionPeriodCheck])

if (addSplunkSubscriptionFilter) {
// This is in an if statement to ensure correct value is used
Expand Down
3 changes: 2 additions & 1 deletion packages/cdkConstructs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export * from "./config/index.js"
export * from "./utils/helpers.js"
export * from "./stacks/deleteUnusedStacks.js"
export * from "./nag/pack/epsNagPack.js"
export * from "./changesets/checkDestructiveChanges"
export * from "./changesets/checkDestructiveChanges.js"
export * from "./constants.js"
49 changes: 25 additions & 24 deletions packages/cdkConstructs/tests/apps/createApp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from "vitest"
import {createApp, type CreateAppParams} from "../../src/apps/createApp"
import {AwsSolutionsChecks} from "cdk-nag"
import {CDK_ENV_PREFIX} from "../../src/constants"

describe("createApp", () => {
const originalEnv = process.env
Expand Down Expand Up @@ -51,10 +52,10 @@ describe("createApp", () => {

describe("when all environment variables are set", () => {
beforeEach(() => {
process.env.CDK_CONFIG_versionNumber = "1.2.3"
process.env.CDK_CONFIG_commitId = "abc123def456"
process.env.CDK_CONFIG_isPullRequest = "false"
process.env.CDK_CONFIG_environment = "test-environment"
process.env[`${CDK_ENV_PREFIX}versionNumber`] = "1.2.3"
process.env[`${CDK_ENV_PREFIX}commitId`] = "abc123def456"
process.env[`${CDK_ENV_PREFIX}isPullRequest`] = "false"
process.env[`${CDK_ENV_PREFIX}environment`] = "test-environment"
})

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

describe("when isPullRequest is true", () => {
beforeEach(() => {
process.env.CDK_CONFIG_versionNumber = "0.0.1-pr"
process.env.CDK_CONFIG_commitId = "pr123"
process.env.CDK_CONFIG_isPullRequest = "true"
process.env.CDK_CONFIG_environment = "test-environment"
process.env[`${CDK_ENV_PREFIX}versionNumber`] = "0.0.1-pr"
process.env[`${CDK_ENV_PREFIX}commitId`] = "pr123"
process.env[`${CDK_ENV_PREFIX}isPullRequest`] = "true"
process.env[`${CDK_ENV_PREFIX}environment`] = "test-environment"
})

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

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

expect(() => {
createApp(buildParams())
}).toThrow("Environment variable CDK_CONFIG_versionNumber is not set")
}).toThrow(`Environment variable ${CDK_ENV_PREFIX}versionNumber is not set`)
})

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

expect(() => {
createApp(buildParams())
}).toThrow("Environment variable CDK_CONFIG_commitId is not set")
}).toThrow(`Environment variable ${CDK_ENV_PREFIX}commitId is not set`)
})

test("throws error when isPullRequest is not set", () => {
process.env.CDK_CONFIG_versionNumber = "1.0.0"
process.env.CDK_CONFIG_commitId = "abc123"
process.env.CDK_CONFIG_environment = "test-environment"
process.env[`${CDK_ENV_PREFIX}versionNumber`] = "1.0.0"
process.env[`${CDK_ENV_PREFIX}commitId`] = "abc123"
process.env[`${CDK_ENV_PREFIX}environment`] = "test-environment"
expect(() => {
createApp(buildParams())
}).toThrow("Environment variable CDK_CONFIG_isPullRequest is not set")
}).toThrow(`Environment variable ${CDK_ENV_PREFIX}isPullRequest is not set`)
})

test("throws error when environment is not set", () => {
process.env.CDK_CONFIG_versionNumber = "1.0.0"
process.env.CDK_CONFIG_commitId = "abc123"
process.env.CDK_CONFIG_isPullRequest = "false"
process.env[`${CDK_ENV_PREFIX}versionNumber`] = "1.0.0"
process.env[`${CDK_ENV_PREFIX}commitId`] = "abc123"
process.env[`${CDK_ENV_PREFIX}isPullRequest`] = "false"
expect(() => {
createApp(buildParams())
}).toThrow("Environment variable CDK_CONFIG_environment is not set")
}).toThrow(`Environment variable ${CDK_ENV_PREFIX}environment is not set`)
})

})
Expand Down
Loading
Loading