Search code examples
aws-cdkssm

How to fetch SSM Parameters from two different accounts using AWS CDK


I have a scenario where I'm using CodePipeline to deploy my cdk project from a tools account to several environment accounts. The way my pipeline is deploying is by running cdk deploy from within a CodeBuild job.

My team has decided to use SSM Parameter Store to store configuration and we ended up with some parameters living in the environment account, for example the VPC_ID (resources/vpc/id) that I can read in deployment time => ssm.StringParameter.valueForStringParameter.

However, other parameters are living in the tools account, such as the Account Ids from my environment accounts (environment/nonprod/account/id) and other Global Config. I'm having trouble fetching those values.

At the moment, the only way I could think of was by using a step to read all those values in a previous step and loaded them into the context values.

Is there a more elegant approach for this problem? I was hoping I could specify in which account to get the SSM values from. Any ideas?

Thank you.


Solution

  • As part of your pipeline, I would add a preliminary step to execute a Lambda. That Lambda can then execute whatever queries you wish to obtain whatever metadata/config that is required. The output from that Lambda can then be passed in to the CodeBuild step.

    e.g. within the Lambda:

    export class ConfigFetcher {
    
      codepipeline = new AWS.CodePipeline();
    
      async fetchConfig(event: CodePipelineEvent, context : Context) : Promise<void> {
    
        // Retrieve the Job ID from the Lambda action
        const jobId = event['CodePipeline.job'].id;
    
        // now get your config by executing whatever queries you need, even cross-account, via the SDK
        // we assume that the answer is in the variable someValue
        const params = {
          jobId: jobId,
          outputVariables: {
            MY_CONFIG: someValue,
          },
        };
        // now tell CodePipeline you're done
        await this.codepipeline.putJobSuccessResult(params).promise().catch(err => {
          console.error('Error reporting build success to CodePipeline: ' + err);
          throw err;
        });
     
        // make sure you have some sort of catch wrapping the above to post a failure to CodePipeline
    // ...
      }
    }
    
    const configFetcher = new ConfigFetcher();
    
    exports.handler = async function fetchConfigMetadata(event: CodePipelineEvent, context : Context): Promise<void> {
      return configFetcher.fetchConfig(event, context);
    };
    

    Assuming that you create your pipeline using CDK, then your Lambda step will be created using something like this:

    const fetcherAction = new LambdaInvokeAction({
      actionName: 'FetchConfigMetadata',
      lambda: configFetcher,
      variablesNamespace: 'ConfigMetadata',
    });
    

    Note the use of variablesNamespace: we need to refer to this later in order to retrieve the values from the Lambda's output and insert them as env variables into the CodeBuild environment.

    Now our CodeBuild definition, again assuming we create using CDK:

    new CodeBuildAction({
      // ...
      environmentVariables: {
        MY_CONFIG: {
          type: BuildEnvironmentVariableType.PLAINTEXT,
          value: '#{ConfigMetadata.MY_CONFIG}',
        },
      },
    

    We can call the variable whatever we want within CodeBuild, but note that ConfigMetadata.MY_CONFIG needs to match the namespace and output value of the Lambda.

    You can have your lambda do anything you want to retrieve whatever data it needs - it's just going to need to be given appropriate permissions to reach across into other AWS accounts if required, which you can do using role assumption. Using a Lambda as a pipeline step will be a LOT faster than using a CodeBuild step in the pipeline, plus it's easier to change: if you write your Lambda code in Typescript/JS or Python, you can even use the AWS console to do in-place edits whilst you test that it executes correctly.