How to share Resources between Stacks in AWS CDK

avatar
Borislav Hadzhiev

Last updated: Jan 26, 2024
10 min

banner

# Table of Contents

  1. Sharing Resources between Stacks in AWS CDK
  2. Sharing a VPC between Stacks in AWS CDK
  3. Using the Ref Intrinsic Function in AWS CDK
  4. How to Get Attribute [GetAtt] in AWS CDK

# Sharing Resources between Stacks in AWS CDK

To share resources between stacks, in the same CDK app, we have to:

  1. Assign the resources we want to share as class properties on stackA
  2. Add the types of the class properties to the props object of stackB
  3. Instantiate stackA, so we can access the class properties
  4. Pass the stackA class properties as props when instantiating stackB
  5. Reference the properties from the props object of stackB

Let's look at an example where we create 2 stacks and share an S3 bucket between them.

The code for this article is available on GitHub

The code snippet defines the following 2 CDK stacks:

  1. BucketStack provisions an S3 bucket
  2. LambdaStack creates a lambda function and references the shared bucket resource from the BucketStack
lib/cdk-starter-stack.ts
import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as cdk from 'aws-cdk-lib'; import * as path from 'path'; export class BucketStack extends cdk.Stack { // ๐Ÿ‘‡ set a property for the bucket public readonly bucket: s3.Bucket; constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // ๐Ÿ‘‡ assign an S3 bucket to the class property this.bucket = new s3.Bucket(this, 'my-bucket', { removalPolicy: cdk.RemovalPolicy.DESTROY, }); } } interface LambdaStackProps extends cdk.StackProps { bucket: s3.Bucket; } export class LambdaStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props: LambdaStackProps) { super(scope, id, props); const {bucket} = props; // ๐Ÿ‘‡ tag the shared bucket cdk.Tags.of(bucket).add('environment', 'staging'); cdk.Tags.of(bucket).add('department', 'accounting'); const lambdaFunction = new lambda.Function(this, 'lambda-function', { runtime: lambda.Runtime.NODEJS_18_X, handler: 'index.main', code: lambda.Code.fromAsset(path.join(__dirname, '/../src/my-lambda')), environment: { // ๐Ÿ‘‡ pass bucket name to lambda BUCKET_NAME: bucket.bucketName, }, }); } }

Let's go over the code snippet.

  1. We defined a BucketStack, which provisions an S3 bucket. The bucket resource is assigned as a class property, so we can access it when we instantiate the class.

  2. We extended the props object of our second stack, by adding the bucket type to it.

  3. We defined our LambdaStack, which will receive the shared bucket in the props object. In our LambdaStack, we add some tags to the shared bucket and pass its name as an environment variable to a Lambda function.

The code for this article is available on GitHub

Now let's look at how we instantiate the CDK stacks:

bin/cdk-starter-stack.ts
import * as cdk from 'aws-cdk-lib'; import {BucketStack, LambdaStack} from '../lib/cdk-starter-stack'; const app = new cdk.App(); const bucketStack = new BucketStack(app, 'bucket-stack', { stackName: 'bucket-stack', env: { region: process.env.CDK_DEFAULT_REGION, account: process.env.CDK_DEFAULT_ACCOUNT, }, }); const lambdaStack = new LambdaStack(app, 'lambda-stack', { // ๐Ÿ‘‡ pass the S3 bucket from the other stack bucket: bucketStack.bucket, stackName: 'lambda-stack', env: { region: process.env.CDK_DEFAULT_REGION, account: process.env.CDK_DEFAULT_ACCOUNT, }, });

We first instantiate the BucketStack and assign the instance to a variable.

We then instantiate the LambdaStack, passing in the S3 Bucket.

At this point, we can reference the bucket on the props object of our LambdaStack.

The code for this article is available on GitHub

Lastly, let's add the code for the Lambda function at src/my-lambda/index.js:

src/my-lambda/index.js
async function main(event) { console.log('BUCKET_NAME ๐Ÿ‘‰', process.env.BUCKET_NAME); return { body: JSON.stringify({message: `${process.env.BUCKET_NAME} ๐ŸŽ‰`}), statusCode: 200, }; } module.exports = {main};

The Lambda simply prints the name of the shared bucket.

When deploying the stacks, we have to make sure to deploy the BucketStack first because we are trying to reference it in our LambdaStack.

Let's deploy the stacks and look at the results.

shell
npx aws-cdk deploy bucket-stack npx aws-cdk deploy lambda-stack

After the stacks have been deployed, we can see that CDK has automatically created an Output with the S3 bucket's name to enable us to reference it in our other stack:

bucket stack outputs

The Tags section of our shared S3 Bucket shows that the tags we added to it from our second stack have been applied:

shared bucket tags

Finally, if we test our function via the Lambda management console, we can see that the function returns the name of the shared bucket:

shared bucket name

# Clean up

When deleting the stacks we have to first delete the LambdaStack and then the BucketStack because we can't delete a stack that exports an output that is referenced in another stack.

shell
npx aws-cdk destroy lambda-stack npx aws-cdk destroy bucket-stack

# Table of Contents

  1. Sharing a VPC between Stacks in AWS CDK
  2. Using the Ref Intrinsic Function in AWS CDK
  3. How to Get Attribute [GetAtt] in AWS CDK

# Sharing a VPC between Stacks in AWS CDK

We are going to look at an example of how to share a VPC between 2 CDK stacks in the same CDK app.

In order to share a VPC between stacks in CDK, we have to:

  1. Assign the VPC resource as a class property on stackA.
  2. Extend the props object of stackB with the VPC type.
  3. Instantiate stackA, so we get access to the VPC resource.
  4. Instantiate stackB and pass it the VPC resource as a prop.
  5. Access the VPC on the props object in stackB.

Let's start by defining the following 2 stacks:

  1. the VPCStack creates a VPC
  2. the LambdaStack creates a lambda function and places it in the shared VPC.
The code for this article is available on GitHub
lib/cdk-starter-stack.ts
import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as cdk from 'aws-cdk-lib'; import * as path from 'path'; export class VPCStack extends cdk.Stack { // ๐Ÿ‘‡ set a property for the vpc public readonly vpc: ec2.Vpc; constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); this.vpc = new ec2.Vpc(this, 'my-vpc', { ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'), natGateways: 0, subnetConfiguration: [ { name: 'public-subnet-1', subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24, }, ], }); } } // ๐Ÿ‘‡ extend the props interface of LambdaStack interface LambdaStackProps extends cdk.StackProps { vpc: ec2.Vpc; } export class LambdaStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props: LambdaStackProps) { super(scope, id, props); const {vpc} = props; cdk.Tags.of(vpc).add('environment', 'development'); cdk.Tags.of(vpc).add('department', 'dpt123'); // ๐Ÿ‘‡ lambda function definition const lambdaFunction = new lambda.Function(this, 'lambda-function', { // ๐Ÿ‘‡ place lambda in shared VPC vpc, allowPublicSubnet: true, runtime: lambda.Runtime.NODEJS_18_X, handler: 'index.main', code: lambda.Code.fromAsset(path.join(__dirname, '/../src/my-lambda')), environment: { // ๐Ÿ‘‡ pass the VPC ID as an environment variable VPC_ID: vpc.vpcId, }, }); } }

Let's go over what we did in the code sample:

  1. We defined our VPCStack, which creates a VPC. The important part here is that we created a vpc class property and assigned the VPC resource to it. The property can now be accessed on instances of the class.
  2. We extended the default StackProps object with the VPC type
  3. We created our LambdaStack, which references the shared VPC and provisions a lambda function in it
The code for this article is available on GitHub

Let's look at how the classes are instantiated:

bin/cdk-starter.ts
import * as cdk from 'aws-cdk-lib'; import {LambdaStack, VPCStack} from '../lib/cdk-starter-stack'; const app = new cdk.App(); const vpcStack = new VPCStack(app, 'vpc-stack', { stackName: 'vpc-stack', env: { region: process.env.CDK_DEFAULT_REGION, account: process.env.CDK_DEFAULT_ACCOUNT, }, }); const lambdaStack = new LambdaStack(app, 'lambda-stack', { // ๐Ÿ‘‡ pass the VPC from the other stack vpc: vpcStack.vpc, stackName: 'lambda-stack', env: { region: process.env.CDK_DEFAULT_REGION, account: process.env.CDK_DEFAULT_ACCOUNT, }, });

We first instantiated the VPCStack and assigned the result to a variable.

We then instantiated our LambdaStack, passing it the VPC resource as a prop.

The code for this article is available on GitHub

Finally, let's add the code for the lambda function at src/my-lambda/index.js:

src/my-lambda/index.js
async function main(event) { console.log('VPC_ID ๐Ÿ‘‰', process.env.VPC_ID); return { body: JSON.stringify({message: `${process.env.VPC_ID} ๐ŸŽ‰`}), statusCode: 200, }; } module.exports = {main};

The function simply references and returns the ID of the shared VPC.

The order of deployment matters because our LambdaStack references the VPC resource from the VPCStack so it has to exist before the LambdaStack is deployed.

Let's run the deploy commands:

shell
npx aws-cdk deploy vpc-stack npx aws-cdk deploy lambda-stack

By looking at the Outputs section of our VPCStack, we can see that CDK has automatically created outputs for the components of the VPC, which will allow us to access it in our second stack:

vpc stack outputs

If we look at the VPC section of the lambda function, we can see that it was provisioned in the shared VPC:

lambda uses shared vpc

Finally, if we run the lambda function via the management console, it returns the ID of the shared VPC:

lambda returns shared vpc id

# Clean up

We have to delete the lambda-stack first because it references an output in the vpc-stack.

shell
npx aws-cdk destroy lambda-stack npx aws-cdk destroy vpc-stack

I've also written articles on how to create multiple stacks and environments in CDK and how to use nested stacks in CDK.

# Table of Contents

  1. Using the Ref Intrinsic Function in AWS CDK
  2. How to Get Attribute [GetAtt] in AWS CDK

# Using the Ref Intrinsic Function in AWS CDK

The Ref intrinsic function in CloudFormation returns the value of a parameter or resource.

Most commonly Ref returns the name of the resource. For example, if we reference an S3 bucket or a Dynamodb table, the value would resolve to the name of the resource.

Before a deployment is run our CDK code gets compiled down to CloudFormation, so we're able to use the Ref intrinsic function in our CDK code.

To use the Ref intrinsic function in CDK, we have to access the ref property on a CfnResource.

The code for this article is available on GitHub

To demo using refs, I'll create a simple CDK stack that consists of a single S3 bucket:

lib/cdk-starter-stack.ts
import * as s3 from 'aws-cdk-lib/aws-s3'; import * as cdk from 'aws-cdk-lib'; export class MyCdkStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props: cdk.StackProps) { super(scope, id, props); const s3Bucket = new s3.Bucket(this, 'avatars-bucket', { removalPolicy: cdk.RemovalPolicy.DESTROY, }); const cfnBucket = s3Bucket.node.defaultChild as s3.CfnBucket; // ๐Ÿ‘‡ get the bucket ref const bucketRef1 = cfnBucket.ref; console.log('bucketRef1 ๐Ÿ‘‰', bucketRef1); // ๐Ÿ‘‡ same thing but using the Fn class const bucketRef2 = cdk.Fn.ref(cfnBucket.logicalId); console.log('bucketRef2 ๐Ÿ‘‰', bucketRef2); } }

In the code sample:

  1. We defined an S3 bucket using the Bucket construct

  2. We got access to the Bucket's CFN resource and casted the type to CfnBucket

  3. Now we are able to access the ref property on the CfnBucket. We then stored the ref in the bucketRef1 variable.

  4. Alternatively, we can access the resource's ref by invoking the ref static method on the Fn class. The ref method takes 1 parameter - the logical id of the resource.

I'll now synth the stack to see if the ref values resolved at synthesis time:

shell
npx aws-cdk synth

The output shows that the values are CDK Tokens:

ref tokens

In short, tokens in CDK are encoded values that will get resolved at deployment time by CloudFormation. This means that we don't have access to the resolved value in our CDK code and we shouldn't use refs in conditional statements.

The Ref value will get resolved by CloudFormation at deployment time. In order to demo this behavior, I'll provision an Output and set its value to be the bucket's ref:

lib/cdk-starter-stack.ts
// ๐Ÿ‘‡ Output with the ref as a value new cdk.CfnOutput(this, 'myBucketRef', { value: bucketRef1, description: 'The name of the s3 bucket', });

If I run cdk synth, the CloudFormation template will get generated in the cdk.out directory:

cdk out ref

We can see that the Ref intrinsic function has been used in the Outputs section of our template.

Next, I'll deploy the CDK stack to see the Ref value resolved in our CloudFormation template:

shell
npx aws-cdk deploy

If I open the CloudFormation console and click on the Outputs section, I can see the Ref value points to the bucket name:

cloudformation resolved ref

# How to Get Attribute [GetAtt] in AWS CDK

The GetAtt function returns the value of a specific attribute.

GetAtt is a function that comes from CloudFormation, but since our CDK code gets compiled down to CloudFormation before deployment, we can use this feature in CDK as well.

We can use the GetAtt function in CDK in two ways:

We are going to demonstrate how to use GetAtt both ways. I'll create a simple CDK stack consisting of a single S3 bucket.

# GetAtt directly on the CfnResource

The code for this article is available on GitHub

First, we'll use the getAtt function directly on the CfnResource:

lib/cdk-starter-stack.ts
import * as s3 from 'aws-cdk-lib/aws-s3'; import * as cdk from 'aws-cdk-lib'; export class MyCdkStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props: cdk.StackProps) { super(scope, id, props); const s3Bucket = new s3.Bucket(this, id); const cfnBucket = s3Bucket.node.defaultChild as s3.CfnBucket; console.log('directly ๐Ÿ‘‰', cfnBucket.getAtt('Arn').toString()); } }

In the code sample, we:

  1. Defined an S3 bucket using the level 2 Bucket construct

  2. Used the node.defaultChild property and cast the type to a CfnBucket so we can access the getAtt method

  3. Logged the call to getAtt as a string

If we synth the stack at this point, we'll see that the value resolves to a Token.

Token output

In short a token is an encoded value that will be resolved by CloudFormation at deployment time. If you want to read more on tokens I have written an article - What is a Token in CDK.

# GetAtt using the Fn class

lib/cdk-starter-stack.ts
import * as s3 from 'aws-cdk-lib/aws-s3'; import * as cdk from 'aws-cdk-lib'; export class MyCdkStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props: cdk.StackProps) { super(scope, id, props); // ...rest console.log( 'on Fn class ๐Ÿ‘‰', cdk.Fn.getAtt(cfnBucket.logicalId, 'Arn').toString(), ); // ๐Ÿ‘‡ add the call to getAtt as an Output new cdk.CfnOutput(this, 'bucketArn', { value: cfnBucket.getAtt('Arn').toString(), description: 'The arn of the s3 bucket', exportName: 'avatarsBucket', }); } }

In the code sample, we:

  1. Used the getAtt static method on the Fn class. The method takes the CloudFormation logical ID of the resource as the first parameter and the name of the attribute as the second.

  2. We created an Output and specified the bucket's ARN from the call to getAtt as the value.

The output from the console.log statement is a Token that will be resolved by CloudFormation at deployment time:

fn class token

Let's deploy the stack and see if the call to cfnBucket.getAtt resolves in the Outputs section:

shell
npx aws-cdk deploy \ --outputs-file ./cdk-outputs.json

After I've run the deploy command with the outputs redirected to a file on the local file system, the contents of cdk-outputs.json look like:

CDK outputs

We can see that the call to cfnBucket.getAtt got resolved by CloudFormation at deployment time.

The cdk.out directory is where the CDK CLI stores file assets in preparation for deployment. If we take a look at the synthesized CloudFormation template, we can see the use of the intrinsic GetAtt function:

cdk.out/my-cdk-stack.template.json
{ "Outputs": { "bucketArn": { "Description": "The arn of the s3 bucket", "Value": { // ๐Ÿ‘‡ Uses GetAtt intrinsic function "Fn::GetAtt": [ "mycdkstack4E08F0DD", "Arn" ] }, "Export": { "Name": "avatarsBucket" } } } }

I've also written a detailed guide on how to use outputs in AWS CDK.

# Additional Resources

You can learn more about the related topics by checking out the following tutorials:

I wrote a book in which I share everything I know about how to become a better, more efficient programmer.
book cover
You can use the search field on my Home Page to filter through all of my articles.

Copyright ยฉ 2024 Borislav Hadzhiev