Skip to content

Commit 65307a7

Browse files
wildjameststephen-nhsCopilot
authored
New: [AEA-6258] - Add SSM Parameter construct (#619)
## Summary - ✨ New Feature ### Details CDK exposes a `StringParameter` which builds an SSM parameter. I've added an `SsmParametersConstruct` which takes an array of parameter definitions, and handles making the `StringParameter` for each, creates a policy to read the parameters, and produces relevant outputs. --------- Co-authored-by: tstephen-nhs <231503406+tstephen-nhs@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 9548de9 commit 65307a7

File tree

9 files changed

+524
-1314
lines changed

9 files changed

+524
-1314
lines changed

package-lock.json

Lines changed: 74 additions & 1301 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cdkConstructs/src/config/index.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,36 @@ import {CloudFormationClient, DescribeStacksCommand} from "@aws-sdk/client-cloud
22
import {S3Client, HeadObjectCommand} from "@aws-sdk/client-s3"
33
import {StandardStackProps} from "../apps/createApp"
44

5-
export function getConfigFromEnvVar(varName: string, prefix: string = "CDK_CONFIG_"): string {
5+
export function getConfigFromEnvVar(
6+
varName: string,
7+
prefix: string = "CDK_CONFIG_",
8+
defaultValue: string | undefined
9+
): string {
610
const value = process.env[prefix + varName]
711
if (!value) {
12+
if (defaultValue !== undefined) {
13+
return defaultValue
14+
}
815
throw new Error(`Environment variable ${prefix}${varName} is not set`)
916
}
1017
return value
1118
}
1219

13-
export function getBooleanConfigFromEnvVar(varName: string, prefix: string = "CDK_CONFIG_"): boolean {
14-
const value = getConfigFromEnvVar(varName, prefix)
20+
export function getBooleanConfigFromEnvVar(
21+
varName: string,
22+
prefix: string = "CDK_CONFIG_",
23+
defaultValue: string | undefined
24+
): boolean {
25+
const value = getConfigFromEnvVar(varName, prefix, defaultValue)
1526
return value.toLowerCase().trim() === "true"
1627
}
1728

18-
export function getNumberConfigFromEnvVar(varName: string, prefix: string = "CDK_CONFIG_"): number {
19-
const value = getConfigFromEnvVar(varName, prefix)
29+
export function getNumberConfigFromEnvVar(
30+
varName: string,
31+
prefix: string = "CDK_CONFIG_",
32+
defaultValue: string | undefined
33+
): number {
34+
const value = getConfigFromEnvVar(varName, prefix, defaultValue)
2035
return Number(value)
2136
}
2237

packages/cdkConstructs/src/constructs/RestApiGateway/StateMachineEndpoint.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ export class StateMachineEndpoint extends Construct {
6363
]
6464
}), {
6565
methodResponses: [
66-
{ statusCode: "200" },
67-
{ statusCode: "400" },
68-
{ statusCode: "500" }
66+
{statusCode: "200"},
67+
{statusCode: "400"},
68+
{statusCode: "500"}
6969
]
7070
})
7171

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import {CfnOutput} from "aws-cdk-lib"
2+
import {Effect, ManagedPolicy, PolicyStatement} from "aws-cdk-lib/aws-iam"
3+
import {StringParameter} from "aws-cdk-lib/aws-ssm"
4+
import {Construct} from "constructs"
5+
6+
/**
7+
* Definition for a single SSM String parameter and its output export metadata.
8+
*
9+
* @property id Unique identifier used for construct and output logical IDs.
10+
* @property nameSuffix Suffix appended to stackName to create the parameter name.
11+
* The final SSM parameter name is `${stackName}-${nameSuffix}`.
12+
* @property description Description stored with the SSM parameter.
13+
* @property value Value stored in the SSM parameter.
14+
* @property outputExportSuffix Optional export suffix for the output containing
15+
* the parameter name. Defaults to `nameSuffix`.
16+
* @property outputDescription Optional output description. Defaults to
17+
* `description`.
18+
*/
19+
export interface SsmParameterDefinition {
20+
/**
21+
* Unique identifier used for construct and output logical IDs.
22+
*/
23+
readonly id: string
24+
/**
25+
* Suffix appended to stackName to create the parameter name.
26+
* The final SSM parameter name is `${stackName}-${nameSuffix}`.
27+
*/
28+
readonly nameSuffix: string
29+
/**
30+
* Description stored with the SSM parameter.
31+
*/
32+
readonly description: string
33+
/**
34+
* Value stored in the SSM parameter.
35+
*/
36+
readonly value: string
37+
/**
38+
* Optional export suffix for the output containing the parameter name.
39+
* @default nameSuffix value
40+
*/
41+
readonly outputExportSuffix?: string
42+
/**
43+
* Optional output description.
44+
* @default description value
45+
*/
46+
readonly outputDescription?: string
47+
}
48+
49+
/**
50+
* Properties used to configure {@link SsmParametersConstruct}.
51+
*
52+
* @property namePrefix Prefix used in SSM parameter names and CloudFormation
53+
* export names.
54+
* @property parameters List of SSM parameters to create.
55+
* @property readPolicyDescription Description for the managed policy that grants
56+
* read access. Defaults to "Allows reading SSM parameters".
57+
* @property readPolicyOutputDescription Description for the output exporting the
58+
* managed policy ARN. Defaults to "Access to the parameters used by the integration".
59+
* @property readPolicyExportSuffix Export suffix for the output exporting the
60+
* managed policy ARN.
61+
*/
62+
export interface SsmParametersConstructProps {
63+
/**
64+
* Prefix used in SSM parameter names and CloudFormation export names.
65+
*/
66+
readonly namePrefix: string
67+
/**
68+
* List of SSM parameters to create.
69+
*/
70+
readonly parameters: Array<SsmParameterDefinition>
71+
/**
72+
* Description for the managed policy that grants read access.
73+
* @default "Allows reading SSM parameters"
74+
*/
75+
readonly readPolicyDescription?: string
76+
/**
77+
* Description for the output exporting the managed policy ARN.
78+
* @default "Access to the parameters used by the integration"
79+
*/
80+
readonly readPolicyOutputDescription?: string
81+
/**
82+
* Export suffix for the output exporting the managed policy ARN.
83+
*/
84+
readonly readPolicyExportSuffix: string
85+
}
86+
87+
/**
88+
* Creates a bundle of SSM String parameters, a managed policy to read them,
89+
* and CloudFormation outputs to export parameter names and policy ARN.
90+
*/
91+
export class SsmParametersConstruct extends Construct {
92+
public readonly parameters: Record<string, StringParameter>
93+
public readonly readParametersPolicy: ManagedPolicy
94+
95+
/**
96+
* Creates SSM String parameters, a managed read policy, and CloudFormation outputs.
97+
*
98+
* @param scope CDK construct scope.
99+
* @param id Unique construct identifier.
100+
* @param props Configuration for parameter names, values, and exported outputs.
101+
* @throws {Error} Throws when no parameter definitions are provided.
102+
* @throws {Error} Throws when duplicate parameter IDs or parameter names are detected.
103+
*/
104+
public constructor(scope: Construct, id: string, props: SsmParametersConstructProps) {
105+
super(scope, id)
106+
107+
const {
108+
namePrefix,
109+
parameters,
110+
readPolicyExportSuffix,
111+
readPolicyDescription = "Allows reading SSM parameters",
112+
readPolicyOutputDescription = "Access to the parameters used by the integration"
113+
} = props
114+
115+
if (parameters.length === 0) {
116+
throw new Error("SsmParametersConstruct requires at least one parameter definition")
117+
}
118+
119+
const createdParameters: Record<string, StringParameter> = {}
120+
121+
const seenIds = new Set<string>()
122+
const seenNames = new Set<string>()
123+
124+
for (const parameter of parameters) {
125+
const parameterId = `${parameter.id}Parameter`
126+
if (seenIds.has(parameterId)) {
127+
throw new Error(`Duplicate parameter id detected: ${parameter.id}.`)
128+
}
129+
seenIds.add(parameterId)
130+
131+
const parameterName = `${namePrefix}-${parameter.nameSuffix}`
132+
if (seenNames.has(parameterName)) {
133+
throw new Error(`Duplicate parameter name detected: ${parameterName}.`)
134+
}
135+
seenNames.add(parameterName)
136+
137+
const ssmParameter = new StringParameter(this, parameterId, {
138+
parameterName,
139+
description: parameter.description,
140+
stringValue: parameter.value
141+
})
142+
143+
createdParameters[parameter.id] = ssmParameter
144+
145+
new CfnOutput(this, `${parameter.id}ParameterNameOutput`, {
146+
description: parameter.outputDescription ?? parameter.description,
147+
value: ssmParameter.parameterName,
148+
exportName: `${namePrefix}-${parameter.outputExportSuffix ?? parameter.nameSuffix}`
149+
})
150+
}
151+
152+
const readParametersPolicy = new ManagedPolicy(this, "GetParametersPolicy", {
153+
description: readPolicyDescription,
154+
statements: [
155+
new PolicyStatement({
156+
effect: Effect.ALLOW,
157+
actions: ["ssm:GetParameter", "ssm:GetParameters"],
158+
resources: Object.values(createdParameters).map((parameter) => parameter.parameterArn)
159+
})
160+
]
161+
})
162+
163+
new CfnOutput(this, "ReadParametersPolicyOutput", {
164+
description: readPolicyOutputDescription,
165+
value: readParametersPolicy.managedPolicyArn,
166+
exportName: `${namePrefix}-${readPolicyExportSuffix}`
167+
})
168+
169+
this.parameters = createdParameters
170+
this.readParametersPolicy = readParametersPolicy
171+
}
172+
}

packages/cdkConstructs/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from "./constructs/RestApiGateway/accessLogFormat.js"
77
export * from "./constructs/RestApiGateway/LambdaEndpoint.js"
88
export * from "./constructs/RestApiGateway/StateMachineEndpoint.js"
99
export * from "./constructs/PythonLambdaFunction.js"
10+
export * from "./constructs/SsmParametersConstruct.js"
1011
export * from "./apps/createApp.js"
1112
export * from "./config/index.js"
1213
export * from "./utils/helpers.js"

packages/cdkConstructs/tests/config/index.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ describe("config helpers", () => {
9393
expect(getConfigFromEnvVar("STACK_NAME")).toBe("primary")
9494
})
9595

96+
test("getConfigFromEnvVar returns the default value when env var is not set", () => {
97+
delete process.env.CDK_CONFIG_MISSING
98+
99+
expect(getConfigFromEnvVar("MISSING", "CDK_CONFIG_", "fallback")).toBe("fallback")
100+
})
101+
96102
test("getConfigFromEnvVar throws when value is missing", () => {
97103
delete process.env.CDK_CONFIG_MISSING
98104

@@ -114,12 +120,31 @@ describe("config helpers", () => {
114120
expect(getBooleanConfigFromEnvVar("OTHER_FLAG")).toBe(false)
115121
})
116122

123+
test("getBooleanConfigFromEnvVar uses default value when env var is not set", () => {
124+
delete process.env.CDK_CONFIG_BOOL_MISSING
125+
126+
expect(getBooleanConfigFromEnvVar("BOOL_MISSING", "CDK_CONFIG_", "true")).toBe(true)
127+
expect(getBooleanConfigFromEnvVar("BOOL_MISSING", "CDK_CONFIG_", "false")).toBe(false)
128+
})
129+
117130
test("getNumberConfigFromEnvVar parses numeric strings", () => {
118131
process.env.CDK_CONFIG_TIMEOUT = "45"
119132

120133
expect(getNumberConfigFromEnvVar("TIMEOUT")).toBe(45)
121134
})
122135

136+
test("getNumberConfigFromEnvVar uses default value when env var is not set", () => {
137+
delete process.env.CDK_CONFIG_NUM_MISSING
138+
139+
expect(getNumberConfigFromEnvVar("NUM_MISSING", "CDK_CONFIG_", "99")).toBe(99)
140+
})
141+
142+
test("getConfigFromEnvVar ignores default value when env var is set", () => {
143+
process.env.CDK_CONFIG_STACK_NAME = "primary"
144+
145+
expect(getConfigFromEnvVar("STACK_NAME", "CDK_CONFIG_", "ignored")).toBe("primary")
146+
})
147+
123148
test("getTrustStoreVersion returns the version ID from S3", async () => {
124149
mockCloudFormationSend.mockResolvedValueOnce({
125150
Stacks: [{

0 commit comments

Comments
 (0)