skip to Main Content

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?

2

Answers


  1. The complexity is that during the CDK’s planning phase (known as synth), you don’t have the actual subnetId value of this parameter. Instead, you have a placeholder.

    The approach is:

    1. Use the CDK’s and CloudFormation’s built-in functions to extract the subnetId value from the JSON-formatted string.
    2. For the JSON {"subnetId": "subnet-xxxxxx"}, you can use string manipulation functions like split and select to dissect the string and grab the subnetId.

    Here’s how you can achieve this:

    // Create a reader to get the parameter from AWS.
    const ssmParameterReader = new SSMParameterReader(stack, 'MyParameterReader', {
      parameterName: '/my/parameter/path',
      region: 'us-west-2',
    });
    
    // Get the raw JSON string value.
    const rawJsonValue = ssmParameterReader.getParameterValue();
    
    // Manipulate the string to extract "subnet-xxxxxx" from it.
    const subnetId = cdk.Fn.select(1, cdk.Fn.split('": "', cdk.Fn.select(0, cdk.Fn.split('}', cdk.Fn.select(0, cdk.Fn.split('{', rawJsonValue)))));
    

    This method breaks down the JSON string into pieces and picks the part you need. It might seem a bit tricky, but it works for this specific JSON format. ¯(ツ)

    Login or Signup to reply.
  2. 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.

    Step 1: Define a custom resource

    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,
        });
      }
    }
    

    Step 2: Define the CR lambda hander

    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 };
    };
    

    Step 3: Add the CR to a stack

    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"),
        });
      }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search