I have been using the solution proposed here (https://stackoverflow.com/a/59774628/91403) in order to access configurations stored in an AWS parameter.
export class SSMParameterReader extends AwsCustomResource {
constructor(scope: Construct, name: string, props: SSMParameterReaderProps) {
const { parameterName, region } = props;
const ssmAwsSdkCall: AwsSdkCall = {
service: 'SSM',
action: 'getParameter',
parameters: {
Name: parameterName
},
region,
physicalResourceId: {id:Date.now().toString()}
};
super(scope, name, { onUpdate: ssmAwsSdkCall,policy:{
statements:[new iam.PolicyStatement({
resources : ['*'],
actions : ['ssm:GetParameter'],
effect:iam.Effect.ALLOW,
}
)]
}});
}
public getParameterValue(): string {
return this.getResponseField('Parameter.Value').toString();
}
}
Now I came across a parameter in the JSON format and have found no way to access its value. I can't seem to be able to parse it. Here is one example: { "subnetId": "subnet-xxxxxx" }
.
I have tried modifying the code in several ways but mainly in the likes of:
...
public getParameterValue(path: string): string {
return this.getResponseField(`Parameter.Value.${path}`).toString();
}
How can I extract the subnetId
value?
Using a lambda-backed custom resource (CR), you can fetch the parameter and parse the JSON string within the custom resource handler itself. The solution below supports (1) optional cross-region parameters and (2) an arbitrary number of keys in the stringified parameter.
For convenience, I encapsulate the custom resource, the provider, and the lambda handler in a construct wrapper. Pass the SSM parameter name and optionally a region as props. The default region is the stack's region.
// GetJsonParamCR.ts
export interface GetJsonParamCRProps {
parameterName: string;
region?: string;
}
export class GetJsonParamCR extends Construct {
readonly customResource: CustomResource;
constructor(scope: Construct, id: string, props: GetJsonParamCRProps) {
super(scope, id);
const func = new nodejs.NodejsFunction(this, "GetJsonParamLambda", {
entry: path.join(__dirname, "index.ts"),
runtime: lambda.Runtime.NODEJS_18_X,
bundling: { externalModules: ["@aws-sdk/client-ssm"] },
});
const paramArn = Stack.of(this).formatArn({
region: props.region,
service: "ssm",
resource: "parameter",
resourceName: props.parameterName,
arnFormat: ArnFormat.SLASH_RESOURCE_NAME,
});
func.addToRolePolicy(
new iam.PolicyStatement({
actions: ["ssm:GetParameter"],
resources: [paramArn],
})
);
const provider = new cr.Provider(this, "ProviderCR", {
onEventHandler: func,
});
this.customResource = new CustomResource(this, "Resource", {
resourceType: "Custom::GetJsonParam",
serviceToken: provider.serviceToken,
properties: props,
});
}
}
The Lambda code first gets the parameter with the JS SDK v3 (preloaded with 18.x Lambdas). Then it parses the parameter string and returns the key-value pairs. I am omitting error handling for simplicity.
// index.ts
export const handler = async (
event: Omit<lambda.CdkCustomResourceEvent, "ResourceProperties"> & {
ResourceProperties: GetJsonParamCRProps & { ServiceToken: string };
}
): Promise<lambda.CdkCustomResourceResponse> => {
if (event.RequestType === "Delete") return {};
const { parameterName, region } = event.ResourceProperties;
const client = new SSMClient({ region: region ?? process.env.AWS_REGION });
const cmd = new GetParameterCommand({ Name: parameterName });
const res = await client.send(cmd);
const parsed = JSON.parse(res.Parameter?.Value ?? "{}");
return { Data: parsed };
};
Instantiate the CR in a stack. The CfnOutput
is added to illustate how to consume the value. Upon deployment, the output value will print to the console: MyStackDD013518.SubnetId = subnet-xxxxxx
.
Note that this solution supports a JSON parameter with multiple keys (e.g. r.customResource.getAttString("anotherKey")
).
// MyStack.ts
export class GetJsonParamStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: cdk.StackProps) {
super(scope, id, props);
const r = new GetJsonParamCR(this, "GetJsonParamCR", {
parameterName: "myJsonParam",
region: "eu-central-1",
});
new cdk.CfnOutput(this, "SubnetId", {
value: r.customResource.getAttString("subnetId"),
});
}
}
OP explicitly requires use of a custom resource and implicitly needs to support cross-region parameters. If these two requirements are relaxed, the solution is much simpler. Use the StringParameter.valueFromLookup static method. valueFromLookup
is a so-called context method, which makes the GetParameter
SDK call at synth-time (on the first run), and caches the result. Because the resolved parameter value is available at synth-time, you can parse it directly within your CDK code.
As above, a CfnOutput
value is added for illustration. It returns subnet-xxxxxx
.
export class NoCustomResourceStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: cdk.StackProps) {
super(scope, id, props);
const param = ssm.StringParameter.valueFromLookup(this, "myJsonParam");
// this ternary check handles the dummy context value the CDK sets internally on first run
const subnetId = param.startsWith("dummy-value") ? "dummy" : JSON.parse(param)?.subnetId;
new cdk.CfnOutput(this, "SubnetId", {
value: subnetId,
});
}
}