Skip to content

How do I use parameters for cross-account references in my AWS CDK project?

10 minute read
0

I have an AWS Cloud Development Kit (AWS CDK) project. I want to use parameters from Parameter Store, a capability of AWS Systems Manager, to configure references across AWS accounts.

Short description

Use parameters with AWS Resource Access Manager (AWS RAM) to reference resources across accounts. Store a resource attribute in a parameter in one account, and then use resource share to reference the resource attribute in another account.

Resolution

Note: If you receive errors when you run AWS Command Line Interface (AWS CLI) commands, then see Troubleshooting errors for the AWS CLI. Also, make sure that you're using the most recent AWS CLI version.

In the following resolution, Account A creates and owns an AWS Key Management Service (AWS KMS) key. Account B uses the AWS KMS key to encrypt its Amazon Simple Notification Service (Amazon SNS) topic. The resolution steps use Linux and Typescript for commands and the Linux operating system (OS). Unless otherwise specified, all commands run on a terminal from the root of the AWS CDK project directory.

Important: The parameter must be in the same AWS Region as the AWS CDK stack that uses it. The following resolution deploys both stacks to the same Region. To set up cross-Region access, you must replicate the parameter to the destination Region.

Prerequisites:

Bootstrap both accounts

Important: When you configure bootstrapping for production accounts, use a restrictive policy that adheres to the principle of least privilege. Make sure that the restrictive policy still adheres to the required bootstrapping permissions. If you use AdministratorAccess to bootstrap Account B with trust to Account A, then Account A can create, modify, or delete any resource in Account B. For more information, see Customize AWS CDK bootstrapping.

Run the following command to bootstrap Account A:

cdk bootstrap aws://ACCOUNT-A/REGION

Note: Replace ACCOUNT-A with the ID for Account A and Region with the Region for Account A.

Run the following command to bootstrap Account B with trust to Account A:

cdk bootstrap aws://ACCOUNT-B/REGION \
 --trust ACCOUNT-A \
 --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess

Note: For production accounts, replace AdministratorAccess with a more restrictive policy. Also, replace ACCOUNT-B with the ID for Account B, Region with the Region for Account B, and ACCOUNT-A with the ID for Account A. For AWS CDK or pipeline bootstrapping, the source and destination accounts can have different Regions. However, you must bootstrap each destination account in each Region that you deploy in.

Create the CDK project

Complete the following steps:

  1. Run the following commands in your terminal to create and initialize a new AWS CDK project:

    mkdir cdk-cross-account-references 
    cd cdk-cross-account-references 
    cdk init --language typescript
  2. After the project initializes, run the following command to verify that the project builds successfully:

    npm run build
  3. In the new AWS CDK project folder, open the cdk.json file, and then add your account IDs to the AWS CDK context.
    Important: Add the code to the existing context segment. Don't overwrite the file.
    Example code:

    {
     "context": {
      "prodAccount": "ACCOUNT-A",
      "devAccount": "ACCOUNT-B"
     }
    }

    Note: Replace ACCOUNT-A with the ID for Account A and ACCOUNT-B with the ID for Account B.

Create the AWS KMS stack in Account A

Prerequisite: Before you deploy your code, make sure that you create all IAM roles that you reference in the AWS KMS policy. If a role that you reference in the stack doesn't exist in your account, then the deployment fails. As a result, you receive the "Policy contains a statement with one or more invalid principals" error message.

The following stack creates an AWS KMS key and stores the key's Amazon Resource Name (ARN) in an advanced parameter. The stack also shares the parameter with Account B through AWS RAM.

Run the following command from the root of the AWS CDK project to rename the default stack file in the lib/ folder to KmsStack.ts:

mv lib/cdk-cross-account-references-stack.ts lib/KmsStack.ts

Then, replace the contents of lib/KmsStack.ts with the following code:

import * as cdk from 'aws-cdk-lib'; 
import { Construct } from 'constructs'; 
import * as kms from 'aws-cdk-lib/aws-kms'; 
import * as ssm from 'aws-cdk-lib/aws-ssm'; 
import * as iam from 'aws-cdk-lib/aws-iam'; 
import { aws_ram as ram } from 'aws-cdk-lib'; 


export class KmsStack extends cdk.Stack { 
 constructor(scope: Construct, id: string, props?: cdk.StackProps) { 
  super(scope, id, props); 

  const devEnvironment = this.node.tryGetContext('devAccount'); 
  const prodEnvironment = this.node.tryGetContext('prodAccount'); 
  const region = this.region; 

  const devEnvPrincipal = new iam.AccountPrincipal(devEnvironment); 
  const prodEnvPrincipal = new iam.AccountPrincipal(prodEnvironment); 

  const keyAdminPrincipal1 = iam.Role.fromRoleArn( 
   this, 'KeyOwnerPrincipal', 
   `arn:aws:iam::${prodEnvironment}:role/AdminConsole` 
  ); 
  const keyAdminPrincipal2 = iam.Role.fromRoleArn( 
   this, 'KeyAdminPrincipal', 
   `arn:aws:iam::${prodEnvironment}:role/cdk-hnb659fds-cfn-exec-role-${prodEnvironment}-${region}` 
  ); 

  const devAccessPrincipal = iam.Role.fromRoleArn( 
   this, 'DevAccessPrincipal', 
   `arn:aws:iam::${devEnvironment}:role/cdk-hnb659fds-deploy-role-${devEnvironment}-${region}` 
  ); 

  const key = new kms.Key(this, 'MyKey', { 
   alias: 'alias/mykey', 
   description: 'KMS key for cross-account SNS encryption', 
   enableKeyRotation: true, 
   enabled: true, 
   policy: new iam.PolicyDocument({ 
    statements: [ 
     new iam.PolicyStatement({ 
      sid: 'AllowRootAccountAccess', 
      actions: ['kms:*'], 
      resources: ['*'], 
      principals: [ 
       new iam.AccountRootPrincipal() 
      ], 
     }), 
     new iam.PolicyStatement({ 
      sid: 'AllowKeyUsage', 
      actions: [ 
       'kms:GenerateDataKey', 
       'kms:Decrypt', 
       'kms:Encrypt', 
       'kms:DescribeKey', 
       'kms:ReEncrypt*' 
      ], 
      resources: ['*'], 
      principals: [ 
       devEnvPrincipal, 
       prodEnvPrincipal, 
       new iam.ServicePrincipal('sns.amazonaws.com') 
      ], 
     }), 
     new iam.PolicyStatement({ 
      sid: 'AllowKeyAdministration', 
      actions: [ 
       'kms:Create*', 
       'kms:Describe*', 
       'kms:Enable*', 
       'kms:List*', 
       'kms:Put*', 
       'kms:Update*', 
       'kms:Revoke*', 
       'kms:Disable*', 
       'kms:Get*', 
       'kms:Delete*', 
       'kms:ScheduleKeyDeletion', 
       'kms:CancelKeyDeletion', 
       'kms:GenerateDataKey', 
       'kms:TagResource', 
       'kms:UntagResource' 
      ], 
      resources: ['*'], 
      principals: [ 
       keyAdminPrincipal1, 
       keyAdminPrincipal2, 
       new iam.ServicePrincipal('cloudformation.amazonaws.com') 
      ], 
     }), 
    ], 
   }), 
  }); 

  const kmsKeyParameter = new ssm.StringParameter(this, 'KMSKeyParameter', { 
   parameterName: '/myapp/kms/key-arn', 
   stringValue: key.keyArn, 
   tier: ssm.ParameterTier.ADVANCED, 
   description: 'KMS Key ARN for cross-account SNS encryption' 
  }); 

  kmsKeyParameter.grantRead(devAccessPrincipal); 

  const resourceShare = new ram.CfnResourceShare(this, 'KMSKeyResourceShare', { 
   name: 'KMSKeyShare', 
   allowExternalPrincipals: false, 
   permissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionSSMParameterReadOnlyWithHistory'], 
   principals: [devEnvironment, devAccessPrincipal.roleArn], 
   resourceArns: [kmsKeyParameter.parameterArn] 
  }); 

  new cdk.CfnOutput(this, 'SSMParameterArn', { 
   value: kmsKeyParameter.parameterArn, 
   description: 'ARN of the SSM parameter containing the KMS key ARN', 
   exportName: 'KMSKeySSMParameterArn' 
  }); 
 } 
}

Note:

  • Replace AdminConsole with your administrator role name.
  • If you use a custom AWS CDK bootstrap qualifier, then replace hnb659fds with the custom qualifier. To find your qualifier, check the CDKToolkit stack outputs in your account, or check the @aws-cdk/core:bootstrapQualifier context value in the cdk.json file.
  • If you use AWS Organizations and your accounts are in different organizations, then set allowExternalPrincipals to true.
  • The AllowRootAccountAccess statement grants full AWS KMS permissions to the account root. Any IAM principal in the account with corresponding IAM permissions can manage the AWS KMS key. This is the default policy for AWS KMS keys that you create through the AWS KMS console. However, for production environments, it's a best practice to restrict key administration to specific IAM roles.

Create the Amazon SNS stack in Account B

The following stack retrieves the AWS KMS key ARN from the shared parameter and creates an encrypted Amazon SNS topic.

Run the following command to create a new file that's named SnsStack.ts in the AWS CDKproject /lib folder:

touch lib/SnsStack.ts

Then, replace the contents of lib/SnsStack.ts with the following code:

// Import the Required CDK Modules
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as ssm from 'aws-cdk-lib/aws-ssm';
import * as kms from 'aws-cdk-lib/aws-kms';

//Defines an interface
export interface SnsStackProps extends cdk.StackProps {
readonly ssmParameterArn: string;
}

export class SnsStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: SnsStackProps) {
super(scope, id, props);

// Get KMS key ARN from SSM parameter
const ssmParameterArn = ssm.StringParameter.fromStringParameterArn (this, 'KMSKeyArn', props.ssmParameterArn)

const getkeyArn = ssmParameterArn.stringValue

// Create SNS topic
const topic = new sns.Topic(this, 'MyTopic', {
displayName: 'My SNS Topic',
topicName: 'MyTopic',
masterKey: kms.Key.fromKeyArn(this, 'KMSKey', getkeyArn),
enforceSSL: true
});

}
}

Note: The fromStringParameterArn() method creates a dynamic reference in AWS CloudFormation that resolves the parameter value at deployment. The shared parameter must exist and be accessible to Account B before you deploy the stack. If the parameter value changes, then you must redeploy the stack.

Configure the main application file

In the AWS CDK project folder, edit the bin/cdk-cross-account-references.ts to include the following code:

#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { KmsStack } from '../lib/KmsStack';
import { SnsStack } from '../lib/SnsStack';

const app = new cdk.App();

const prodAccount = app.node.tryGetContext('prodAccount');
const devAccount = app.node.tryGetContext('devAccount');
const region = process.env.AWS_REGION || 'us-east-1';

if (!prodAccount || !devAccount) {
throw new Error('prodAccount and devAccount must be defined in cdk.json context');
}

const ssmKeyArn = `arn:aws:ssm:${region}:${prodAccount}:parameter/myapp/kms/key-arn`;

const KmsStackInstance = new KmsStack(app, 'KmsStack', {
env: { account: prodAccount, region: region },
description: 'KMS key and SSM parameter for cross-account sharing'
});

const SnsStackInstance = new SnsStack(app, 'SnsStack', {
env: { account: devAccount, region: region },
ssmParameterArn: ssmKeyArn,
description: 'SNS topic encrypted with cross-account KMS key'
});

SnsStackInstance.addDependency(KmsStackInstance);

Note: The addDependency method makes sure that AWS CDK deploys KmsStack before SnsStack when you use the cdk deploy --all command. However, AWS RAM propagation might take a few minutes. If the SnsStack deployment fails, then wait a few minutes before you retry the deployment.

Deploy the stacks

If your accounts are in different organizations, then complete the following steps:

  1. Run the following command to deploy KmsStack:
    cdk deploy KmsStack --profile prod-profile
    Note: Replace prod-profile with the AWS CLI profile for Account A.
  2. Sign in to Account B, and then accept the share request.
  3. Run the following command to deploy SnsStack:
    cdk deploy SnsStack --profile prod-profile
    Note: Replace prod-profile with the AWS CLI profile for Account A.

If your accounts are in the same organization and allow RAM sharing, then run the following command to simultaneously deploy both stacks:

cdk deploy --all --profile prod-profile

Note: Replace prod-profile with the AWS CLI profile for Account A. The --profile flag specifies the credentials for Account A that has trust to deploy into Account B.

When prompted, review and approve the IAM changes.

Verify the deployment

To check the AWS KMS key attribute, run the following get-topic-attributes AWS CLI command:

aws sns get-topic-attributes \
 --topic-arn arn:aws:sns:REGION:ACCOUNT-B:TOPIC-NAME \
 --query 'Attributes.KmsMasterKeyId'

Note: Replace REGION with your Region, ACCOUNT-B with the ID for Account B, and TOPIC-NAME with the SNS topic name.

To verify that the cross-account AWS KMS key encrypted the topic, run the following publish command to publish a test message:

aws sns publish \
 --topic-arn arn:aws:sns:REGION:ACCOUNT-B:MyTopic \
 --message "Test message" \
 --region REGION

Note: Replace REGION with your Region and ACCOUNT-B with the ID for Account B. Make sure to run the command on a terminal with a profile that has access to the account where you deployed the topic.

To avoid ongoing charges, run the following command to delete all deployed resources:

cdk destroy --all --profile prod-profile

Note: Replace prod-profile with the AWS CLI profile for Account A. By default, the AWS KMS key enters a waiting period of 30 days before deletion. To change the waiting period, update the pendingWindow property on the key construct. For more information, see class Key (construct).

AWS CDK deletes resources in reverse dependency order. In this resolution's configuration, AWS CDK deletes SnsStack, and then KmsStack. If you encounter issues when you run the preceding command, then run the following commands to manually delete SnsStack first:

cdk destroy SnsStack --profile prod-profile
cdk destroy KmsStack --profile prod-profile

Note: Replace prod-profile with the AWS CLI profile for Account A.

Related information

Allowing users in other accounts to use an AWS KMS key

Refer to resource outputs in another CloudFormation stack

Sharing your AWS resources

class CfnResourceShare (construct)

Viewing managed permissions