Importing VPC IDs into a stack with CDK
Tags: aws, cdk, typescript
Published: 2022-09-15 07:14:46

When we want to import a VPC ID from another stack using CDK, not all methods will accept the imported value. Errors can appear at synthesis time, so we should find a workaround that correctly refers to the VPC.

## 1. The problem

I wanted to add a resource to an existing <a href="https://aws.amazon.com/vpc/" class="link" target="_blank" rel="noopener noreferrer">VPC</a> using <a href="https://aws.amazon.com/cdk/" class="link" target="_blank" rel="noopener noreferrer">CDK</a> in <a href="https://www.typescriptlang.org/docs/handbook/intro.html" class="link" target="_blank" rel="noopener noreferrer">TypeScript</a> the other day. The VPC was in another stack, and I exported its ID as an `Output` there:

```ts
import { CfnOutput } from 'aws-cdk-lib';

// ...
// inside the construct
new CfnOutput(this, 'VpcIdOutput', {
  exportName: `my-vpc-id-${stage}`,
  value: this.vpc.vpcId,
})
```

In the other stack, where I wanted to use the VPC, I tried the following:

```ts
import { Vpc } from 'aws-cdk-lib/aws-ec2';

// ...
// inside the construct
const vpc = Vpc.fromLookup(this, 'Vpc', {
  vpcId: Fn.importValue(`my-vpc-id-${stage}`),
});
```

When I wanted to create a <a href="https://aws.amazon.com/cloudformation/" class="link" target="_blank" rel="noopener noreferrer">CloudFormation</a> template from the CDK code with `cdk synth`, I received the following error:

```bash
Error: All arguments to Vpc.fromLookup() must be concrete (no Tokens)
```

It was not the optimal outcome, and I had to find another way to import the VPC ID.

## 2. The reason

We have a couple of things going on here.

### 2.1. Tokens

Tokens are **placeholders** for the resources whose values are only available at a later phase of the deployment process.

We will see a string containing a sequence of random characters if we log the tokens. At the beginning of the deployment, CDK builds the template from the code. At this time, CDK will pass placeholders on to the relevant part of the template code when needed. An example is when we use the imported VPC ID to get the whole VPC object.

At a later stage, when the token's value (for example, a VPC ID) is available, CDK will replace it with the actual value.

### 2.2. Vpc.fromLookup

In this case, the `Vpc.fromLookup()` method doesn't allow tokens as arguments, only concrete values. It indicates that we can synthesize the infrastructure code if we replace the import value with the real hard-coded VPC ID. It is because it's a defined value and not a placeholder that the process will resolve later.

The AWS documentation states it:

```
Calling this method will lead to a lookup when the CDK CLI is executed.
You can therefore not use any values that will only be available at
CloudFormation execution time (i.e., Tokens).
```

`vpc.fromLookup()` didn't work, so we have to find another way to get the VPC.

### 2.3. Synthesis

The `cdk synth` CLI command executes the **synthesis** phase.

When CDK synthesizes the constructs, it will create some necessary artifacts for the deployment.

One of these artifacts is the CloudFormation template, which CDK creates and writes in JSON to the `cdk.out` folder. It also shows the template in YAML format in the terminal.

The generated template is now ready for deployment.

The error message and the documentation indicate that imported VPC IDs are not available at the time when the `Vpc.fromLookup()` method runs. It will only be available <q>at CloudFormation execution time</q>. It's the last stage when CloudFormation creates or updates the resources that we have defined in the CDK app code. The process will resolve most placeholder (token) values at this phase.

## 3. A solution

But we can replace the `Vpc.fromLookup` method with `Vpc.fromVpcAttributes`:

```ts
import { Vpc } from 'aws-cdk-lib/aws-ec2';

const vpc = Vpc.fromVpcAttributes(this, 'Vpc', {
  availabilityZones: ['us-west-2a', 'us-west-2b'],
  privateSubnetIds: [
    Fn.importValue(`private-subnet-id-1-${stage}`),
    Fn.importValue(`private-subnet-id-2-${stage}`)
  ],
  vpcId: Fn.importValue(`my-vpc-id-${stage}`),
});
```

In this solution, we will need the **subnet ids** where we want to create the additional resources. So we will have to export them from the stack where they live.

CDK offers lots of options here. We can import not only private but also public and isolated subnets. I wanted to add the resources to the private subnets in this case.

This solution involves more work in the current and other stacks, but it is a working solution.

## 4. Considerations

A word of caution about `Vpc.fromVpcAttributes`. The documentation has the following warning:

```
NOTE: using `fromVpcAttributes()` with deploy-time parameters
(like a `Fn.importValue()` or `CfnParameter` to represent a list
of subnet IDs) sometimes accidentally works. It happens to work
for constructs that need a list of subnets (like `AutoScalingGroup`
and `eks.Cluster`) but it does not work for constructs that need
individual subnets (like `Instance`).
See https://github.com/aws/aws-cdk/issues/4118 for more information.

Prefer to use `Vpc.fromLookup()` instead.
```

The <q>sometimes accidentally works</q> part sounds a bit scary as AWS won't guarantee that the method will work in every scenario. So we should thoroughly test our solution with `cdk synth` and deploy it to our feature stage before we merge the code.

But the above solution has worked for me. It can be a good workaround in this scenario!

I've seen solutions that use <a href="https://aws.amazon.com/sdk-for-javascript/" class="link" target="_blank" rel="noopener noreferrer">AWS SDK</a> to get all CloudFormation exports, filter out the one with the correct VPC ID and pass it down to the construct. The script can run when the CDK app starts.

I didn't want to implement this solution because it seemed to require more work and would have resulted in harder-to-read code. Also, I wouldn't mix up CDK with SDK. I believe CDK is about infrastructure, and SDK is about application code. But this is just my opinion, so feel free to challenge me and use SDK in your CDK code.

If nothing else works, we can always **hardcode** the value of the VPC ID. It's not elegant or dynamic, but it will work with `Vpc.fromLookup()`. In our world of "deploy as many features as you can in the shortest time possible", hardcoding can sound good temporary solution. (We all know what it means. It will probably remain there forever.)

## 5. Summary

Imported values from another stack are available at a later phase of the deployment process. Some CDK methods won't accept placeholders called tokens for these values.

There are different ways to work around this issue. We can use another CDK method if it exists, create custom solutions to get the value from all exports, or hardcode it. Each has pros and cons, and we should use the solution that fits our use case and circumstances.

## 6. Further reading

<a href="https://docs.aws.amazon.com/cdk/v2/guide/tokens.html" class="link" target="_blank" rel="noopener noreferrer">Tokens</a> - AWS documentation about placeholder tokens in CDK

<a href="https://docs.aws.amazon.com/cdk/v2/guide/apps.html#lifecycle" class="link" target="_blank" rel="noopener noreferrer">App lifecycle</a> - Brief description of the CDK app lifecycle stages

<a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.Vpc.html" class="link" target="_blank" rel="noopener noreferrer">class Vpc (construct)</a> - The VPC construct API documentation for CDK

<a href="https://docs.aws.amazon.com/vpc/latest/userguide/configure-subnets.html" class="link" target="_blank" rel="noopener noreferrer">Subnets for your VPC</a> - Subnet basics, private and public subnets