Skip to content

Commit f3a9207

Browse files
committed
Add a construct that creates a batch of SSM parameters, thier outputs, and the policy to read them
1 parent d906e86 commit f3a9207

3 files changed

Lines changed: 249 additions & 0 deletions

File tree

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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+
export interface SsmParameterDefinition {
7+
/**
8+
* Unique identifier used for construct and output logical IDs.
9+
*/
10+
readonly id: string
11+
/**
12+
* Suffix appended to stackName to create the parameter name.
13+
* The final SSM parameter name is `${stackName}-${nameSuffix}`.
14+
*/
15+
readonly nameSuffix: string
16+
/**
17+
* Description stored with the SSM parameter.
18+
*/
19+
readonly description: string
20+
/**
21+
* Value stored in the SSM parameter.
22+
*/
23+
readonly value: string
24+
/**
25+
* Optional export suffix for the output containing the parameter name.
26+
* Defaults to `${nameSuffix}Parameter`.
27+
*/
28+
readonly outputExportSuffix?: string
29+
/**
30+
* Optional output description.
31+
*/
32+
readonly outputDescription?: string
33+
}
34+
35+
export interface SsmParametersConstructProps {
36+
/**
37+
* Prefix used in SSM parameter names and CloudFormation export names.
38+
*/
39+
readonly stackName: string
40+
/**
41+
* List of SSM parameters to create.
42+
*/
43+
readonly parameters: Array<SsmParameterDefinition>
44+
/**
45+
* Description for the managed policy that grants read access.
46+
* @default "Allows reading SSM parameters"
47+
*/
48+
readonly readPolicyDescription?: string
49+
/**
50+
* Description for the output exporting the managed policy ARN.
51+
* @default "Access to the parameters used by the integration"
52+
*/
53+
readonly readPolicyOutputDescription?: string
54+
/**
55+
* Export suffix for the output exporting the managed policy ARN.
56+
* @default "GetParametersPolicy"
57+
*/
58+
readonly readPolicyExportSuffix?: string
59+
}
60+
61+
/**
62+
* Creates a bundle of SSM String parameters, a managed policy to read them,
63+
* and CloudFormation outputs to export parameter names and policy ARN.
64+
*/
65+
export class SsmParametersConstruct extends Construct {
66+
public readonly parameters: Record<string, StringParameter>
67+
public readonly readParametersPolicy: ManagedPolicy
68+
69+
public constructor(scope: Construct, id: string, props: SsmParametersConstructProps) {
70+
super(scope, id)
71+
72+
const {
73+
stackName,
74+
parameters,
75+
readPolicyDescription = "Allows reading SSM parameters",
76+
readPolicyOutputDescription = "Access to the parameters used by the integration",
77+
readPolicyExportSuffix = "GetParametersPolicy"
78+
} = props
79+
80+
if (parameters.length === 0) {
81+
throw new Error("SsmParametersConstruct requires at least one parameter definition")
82+
}
83+
84+
const createdParameters: Record<string, StringParameter> = {}
85+
86+
for (const parameter of parameters) {
87+
const ssmParameter = new StringParameter(this, `${parameter.id}Parameter`, {
88+
parameterName: `${stackName}-${parameter.nameSuffix}`,
89+
description: parameter.description,
90+
stringValue: parameter.value
91+
})
92+
93+
createdParameters[parameter.id] = ssmParameter
94+
95+
new CfnOutput(this, `${parameter.id}ParameterNameOutput`, {
96+
description: parameter.outputDescription ?? `Name of the SSM parameter holding ${parameter.nameSuffix}`,
97+
value: ssmParameter.parameterName,
98+
exportName: `${stackName}-${parameter.outputExportSuffix ?? `${parameter.nameSuffix}Parameter`}`
99+
})
100+
}
101+
102+
const readParametersPolicy = new ManagedPolicy(this, "GetParametersPolicy", {
103+
description: readPolicyDescription,
104+
statements: [
105+
new PolicyStatement({
106+
effect: Effect.ALLOW,
107+
actions: ["ssm:GetParameter", "ssm:GetParameters"],
108+
resources: Object.values(createdParameters).map((parameter) => parameter.parameterArn)
109+
})
110+
]
111+
})
112+
113+
new CfnOutput(this, "ReadParametersPolicyOutput", {
114+
description: readPolicyOutputDescription,
115+
value: readParametersPolicy.managedPolicyArn,
116+
exportName: `${stackName}-${readPolicyExportSuffix}`
117+
})
118+
119+
this.parameters = createdParameters
120+
this.readParametersPolicy = readParametersPolicy
121+
}
122+
}

packages/cdkConstructs/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Export all constructs
22
export * from "./constructs/TypescriptLambdaFunction.js"
33
export * from "./constructs/PythonLambdaFunction.js"
4+
export * from "./constructs/SsmParametersConstruct.js"
45
export * from "./apps/createApp.js"
56
export * from "./config/index.js"
67
export * from "./utils/helpers.js"
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import {App, Stack} from "aws-cdk-lib"
2+
import {Template} from "aws-cdk-lib/assertions"
3+
import {
4+
beforeAll,
5+
describe,
6+
expect,
7+
test
8+
} from "vitest"
9+
10+
import {SsmParametersConstruct} from "../../src/constructs/SsmParametersConstruct"
11+
12+
describe("SsmParametersConstruct", () => {
13+
let template: Template
14+
15+
beforeAll(() => {
16+
const app = new App()
17+
const stack = new Stack(app, "parameterStack")
18+
19+
new SsmParametersConstruct(stack, "TestingParameters", {
20+
stackName: "mock-stack",
21+
parameters: [
22+
{
23+
id: "MockParam1",
24+
nameSuffix: "MockParam1",
25+
description: "Description for mock parameter 1",
26+
value: "mock-value-1",
27+
outputExportSuffix: "MockParam1Parameter",
28+
outputDescription: "Name of the SSM parameter holding MockParam1"
29+
},
30+
{
31+
id: "MockParam2",
32+
nameSuffix: "MockParam2",
33+
description: "Description for mock parameter 2",
34+
value: "mock-value-2",
35+
outputExportSuffix: "MockParam2Parameter",
36+
outputDescription: "Name of the SSM parameter holding MockParam2"
37+
},
38+
{
39+
id: "MockParam3",
40+
nameSuffix: "MockParam3",
41+
description: "Description for mock parameter 3",
42+
value: "mock-value-3",
43+
outputExportSuffix: "MockParam3Parameter",
44+
outputDescription: "Name of the SSM parameter holding MockParam3"
45+
}
46+
],
47+
readPolicyDescription: "Mock policy description",
48+
readPolicyOutputDescription: "Mock read policy output description",
49+
readPolicyExportSuffix: "MockGetParametersPolicy"
50+
})
51+
52+
template = Template.fromStack(stack)
53+
})
54+
55+
test("creates all SSM parameters", () => {
56+
template.hasResourceProperties("AWS::SSM::Parameter", {
57+
Name: "mock-stack-MockParam1",
58+
Type: "String",
59+
Value: "mock-value-1"
60+
})
61+
62+
template.hasResourceProperties("AWS::SSM::Parameter", {
63+
Name: "mock-stack-MockParam2",
64+
Type: "String",
65+
Value: "mock-value-2"
66+
})
67+
68+
template.hasResourceProperties("AWS::SSM::Parameter", {
69+
Name: "mock-stack-MockParam3",
70+
Type: "String",
71+
Value: "mock-value-3"
72+
})
73+
})
74+
75+
test("creates read policy with GetParameter actions for all parameters", () => {
76+
const policies = template.findResources("AWS::IAM::ManagedPolicy", {
77+
Properties: {
78+
Description: "Mock policy description"
79+
}
80+
})
81+
82+
expect(Object.keys(policies)).toHaveLength(1)
83+
84+
const policy = Object.values(policies)[0] as {
85+
Properties: {
86+
PolicyDocument: {
87+
Statement: Array<{
88+
Action: Array<string>
89+
Resource: Array<unknown>
90+
}>
91+
}
92+
}
93+
}
94+
95+
const statement = policy.Properties.PolicyDocument.Statement[0]
96+
expect(statement.Action).toEqual(["ssm:GetParameter", "ssm:GetParameters"])
97+
expect(statement.Resource).toHaveLength(3)
98+
})
99+
100+
test("exports parameter names and policy ARN", () => {
101+
const outputs = template.toJSON().Outputs as Record<string, {
102+
Description?: string
103+
Export?: {
104+
Name?: string
105+
}
106+
}>
107+
108+
const exportedNames = Object.values(outputs)
109+
.map((output) => output.Export?.Name)
110+
.filter((name): name is string => name !== undefined)
111+
112+
const descriptions = Object.values(outputs)
113+
.map((output) => output.Description)
114+
.filter((description): description is string => description !== undefined)
115+
116+
expect(exportedNames).toContain("mock-stack-MockParam1Parameter")
117+
expect(exportedNames).toContain("mock-stack-MockParam2Parameter")
118+
expect(exportedNames).toContain("mock-stack-MockParam3Parameter")
119+
expect(exportedNames).toContain("mock-stack-MockGetParametersPolicy")
120+
121+
expect(descriptions).toContain("Name of the SSM parameter holding MockParam1")
122+
expect(descriptions).toContain("Name of the SSM parameter holding MockParam2")
123+
expect(descriptions).toContain("Name of the SSM parameter holding MockParam3")
124+
expect(descriptions).toContain("Mock read policy output description")
125+
})
126+
})

0 commit comments

Comments
 (0)