Skip to content

Commit

Permalink
feat!(vpc-import): Reimplement reference to a pre-existing VPC
Browse files Browse the repository at this point in the history
  • Loading branch information
akash1810 committed Feb 4, 2025
1 parent 04ee730 commit 433d49f
Show file tree
Hide file tree
Showing 13 changed files with 491 additions and 227 deletions.
128 changes: 128 additions & 0 deletions .changeset/kind-dots-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
"@guardian/cdk": major
---

## Reimplement reference to a pre-existing VPC
This version reimplements how GuCDK references a pre-existing VPC. The changes should improve in two areas:
1. Provide more stability by reducing the number of scenarios where the following error is thrown during synth:

```
> Found an encoded list token string in a scalar string context. Use 'Fn.select(0, list)' (not 'list[0]') to extract elements from token lists.
```

2. Reduce the need to add values to `cdk.context.json`

For most applications, these changes will be minimal changing only the name of CloudFormation (CFN) parameters.

### CloudFormation Parameter changes
Previously, the CFN parameters were prefixed with the `app` being deployed.
For example, with an `app` called `api`, GuCDK added the following CFN parameters:
- `apiPrivateSubnets` - referencing an SSM Parameter holding the private subnets
- `apiPublicSubnets` - referencing an SSM Parameter holding the public subnets

The VPC of these subnets were referred to by the CFN parameter:
- `VpcId`

Now, the parameters are named:
- `VpcId`
- `VpcPrivateSubnets`
- `VpcPublicSubnets`

That is, the prefix is removed from the subnet parameters.
The aim is to make the relation between the parameters more explicit and easier to understand.
Additionally, prefixing the parameters with the `app` name was unnecessary and could lead to confusion and duplication.

#### `GuSubnetListParameter` is replaced with `GuVpcPrivateSubnetsParameter` and `GuVpcPublicSubnetsParameter`
The `GuSubnetListParameter` class has been replaced with `GuVpcPrivateSubnetsParameter` and `GuVpcPublicSubnetsParameter`.
The aim is to be more intention revealing and improve clarity.

They are now also implemented as singletons, similar to `GuVpcParameter`.

> [!NOTE]
> If you were previously overriding the default value of the VPC parameters, you'll need to reimplement this.
> Here is the updated implementation:
>
> ```typescript
> const vpcId = GuVpcParameter.getInstance(this);
> vpcIdParameter.default = "/account/vpc/alternative-vpc/id";
>
> const privateSubnets = GuVpcPrivateSubnetsParameter.getInstance(this);
> privateSubnets.default = "/account/vpc/alternative-vpc/subnets/private";
>
> const publicSubnets = GuVpcPublicSubnetsParameter.getInstance(this);
> publicSubnets.default = "/account/vpc/alternative-vpc/subnets/public";
> ```
### `GuEc2App` changes
The `privateSubnets` and `publicSubnets` properties have been removed from `GuEc2App` in favour of reading these values directly from the `vpc` prop:
```typescript
const { privateSubnets, publicSubnets } = props.vpc;
```
An error now will be thrown if these values are not present. This is to reinforce the relation between VPC and subnets.

### `GuVpc` is replaced with `GuVpcImport`
The `GuVpc` class at `@guardian/cdk/lib/constructs/ec2` has been replaced with `GuVpcImport` at `@guardian/cdk/lib/constructs/vpc`.
This naming is more intuitive and helps distinguish it from the other `GuVpc` construct that creates a new account VPC.

Typically, you shouldn't need to call `GuVpcImport` as GuCDK uses it internally by default.
Below are the migration paths for the most common use cases.

#### Migrating `GuVpc.fromIdParameter`
Before:

```typescript
import { GuVpc } from '@guardian/cdk/lib/constructs/ec2';

const vpc = GuVpc.fromIdParameter(this, 'vpc', { } );
```

After:

```typescript
import { GuVpcImport } from '@guardian/cdk/lib/constructs/vpc';

const vpc = GuVpcImport.fromSsmParameters(this);
```

#### Migrating `GuVpc.subnetsFromParameter`
Before:

```typescript
import { GuVpc, SubnetType } from '@guardian/cdk/lib/constructs/ec2';

const privateSubnets = GuVpc.subnetsFromParameter(this, {
type: SubnetType.PRIVATE,
});

const publicSubnets = GuVpc.subnetsFromParameter(this, {
type: SubnetType.PUBLIC,
});
```

After:

```typescript
import { GuVpcImport } from '@guardian/cdk/lib/constructs/vpc';

const vpc = GuVpcImport.fromSsmParameters(this);
const { privateSubnets, publicSubnets } = vpc;
```

#### Migrating `GuVpc.subnetsFromParameterFixedNumber`
Before:

```typescript
import { GuVpc } from '@guardian/cdk/lib/constructs/ec2';

const vpc = GuVpc.subnetsFromParameterFixedNumber(this, 'vpc', { } );
```

After:

```typescript
import { GuVpcImport } from '@guardian/cdk/lib/constructs/vpc';

const vpc = GuVpcImport.fromSsmParametersRegional(this);
```
46 changes: 39 additions & 7 deletions src/constructs/core/parameters/vpc.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,48 @@
import { Template } from "aws-cdk-lib/assertions";
import { simpleGuStackForTesting } from "../../../utils/test";
import { GuSubnetListParameter } from "./vpc";
import { GuVpcParameter, GuVpcPrivateSubnetsParameter, GuVpcPublicSubnetsParameter } from "./vpc";

describe("The GuSubnetListParameter class", () => {
it("should combine override and prop values", () => {
describe("GuVpcParameter", () => {
it("can have its default overridden", () => {
const stack = simpleGuStackForTesting();

new GuSubnetListParameter(stack, "Parameter", { description: "This is a test" });
const parameter = GuVpcParameter.getInstance(stack);
parameter.default = "/account/vpc/secondary/id";

Template.fromStack(stack).hasParameter("Parameter", {
Type: "List<AWS::EC2::Subnet::Id>",
Description: "This is a test",
Template.fromStack(stack).hasParameter("VpcId", {
Type: "AWS::SSM::Parameter::Value<AWS::EC2::VPC::Id>",
Description: "Virtual Private Cloud to run EC2 instances within. Should NOT be the account default VPC.",
Default: "/account/vpc/secondary/id",
});
});
});

describe("GuVpcPrivateSubnetsParameter", () => {
it("can have its default overridden", () => {
const stack = simpleGuStackForTesting();

const parameter = GuVpcPrivateSubnetsParameter.getInstance(stack);
parameter.default = "/account/vpc/secondary/subnets/private";

Template.fromStack(stack).hasParameter("VpcPrivateSubnets", {
Type: "AWS::SSM::Parameter::Value<List<AWS::EC2::Subnet::Id>>",
Description: "A comma-separated list of private subnets",
Default: "/account/vpc/secondary/subnets/private",
});
});
});

describe("GuVpcPublicSubnetsParameter", () => {
it("can have its default overridden", () => {
const stack = simpleGuStackForTesting();

const parameter = GuVpcPublicSubnetsParameter.getInstance(stack);
parameter.default = "/account/vpc/secondary/subnets/public";

Template.fromStack(stack).hasParameter("VpcPublicSubnets", {
Type: "AWS::SSM::Parameter::Value<List<AWS::EC2::Subnet::Id>>",
Description: "A comma-separated list of public subnets",
Default: "/account/vpc/secondary/subnets/public",
});
});
});
88 changes: 74 additions & 14 deletions src/constructs/core/parameters/vpc.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,27 @@
import { CfnParameter } from "aws-cdk-lib";
import { NAMED_SSM_PARAMETER_PATHS } from "../../../constants";
import { isSingletonPresentInStack } from "../../../utils/singleton";
import type { GuStack } from "../stack";
import { GuParameter } from "./base";
import type { GuNoTypeParameterProps } from "./base";

export class GuSubnetListParameter extends GuParameter {
constructor(scope: GuStack, id: string, props: GuNoTypeParameterProps) {
super(scope, id, { ...props, type: "List<AWS::EC2::Subnet::Id>" });
}
}

/**
* Adds a "VpcId" parameter to a stack.
* This parameter will read from Parameter Store.
* By default it will read from `/account/vpc/primary/id`, this can be changed at runtime if needed.
* A CloudFormation parameter referencing an SSM Parameter holding the VPC ID.
* The parameter name is `VpcId` and default value `/account/vpc/primary/id`.
*
* The default value can be changed if needed:
*
* ```typescript
* const vpcIdParameter = GuVpcParameter.getInstance(this);
* vpcIdParameter.default = "/account/vpc/secondary/id";
* ```
*/
export class GuVpcParameter extends GuParameter {
export class GuVpcParameter extends CfnParameter {
private static instance: GuVpcParameter | undefined;

private constructor(scope: GuStack) {
super(scope, "VpcId", {
description: NAMED_SSM_PARAMETER_PATHS.PrimaryVpcId.description,
default: NAMED_SSM_PARAMETER_PATHS.PrimaryVpcId.path,
type: "AWS::EC2::VPC::Id",
fromSSM: true,
type: "AWS::SSM::Parameter::Value<AWS::EC2::VPC::Id>",
});
}

Expand All @@ -35,3 +33,65 @@ export class GuVpcParameter extends GuParameter {
return this.instance;
}
}

/**
* A CloudFormation parameter referencing an SSM Parameter holding the VPC private subnets.
* The parameter name is `VpcPrivateSubnets` and default value `/account/vpc/primary/subnets/private`.
*
* The default value can be changed if needed:
*
* ```typescript
* const vpcPrivateSubnetsParameter = GuVpcPrivateSubnetsParameter.getInstance(this);
* vpcPrivateSubnetsParameter.default = "/account/vpc/secondary/subnets/private";
* ```
*/
export class GuVpcPrivateSubnetsParameter extends CfnParameter {
private static instance: GuVpcPrivateSubnetsParameter | undefined;

private constructor(scope: GuStack) {
super(scope, "VpcPrivateSubnets", {
description: NAMED_SSM_PARAMETER_PATHS.PrimaryVpcPrivateSubnets.description,
default: NAMED_SSM_PARAMETER_PATHS.PrimaryVpcPrivateSubnets.path,
type: "AWS::SSM::Parameter::Value<List<AWS::EC2::Subnet::Id>>",
});
}

public static getInstance(stack: GuStack): GuVpcPrivateSubnetsParameter {
if (!this.instance || !isSingletonPresentInStack(stack, this.instance)) {
this.instance = new GuVpcPrivateSubnetsParameter(stack);
}

return this.instance;
}
}

/**
* A CloudFormation parameter referencing an SSM Parameter holding the VPC public subnets.
* The parameter name is `VpcPublicSubnets` and default value `/account/vpc/primary/subnets/public`.
*
* The default value can be changed if needed:
*
* ```typescript
* const vpcPublicSubnetsParameter = GuVpcPublicSubnetsParameter.getInstance(this);
* vpcPublicSubnetsParameter.default = "/account/vpc/secondary/subnets/public";
* ```
*/
export class GuVpcPublicSubnetsParameter extends CfnParameter {
private static instance: GuVpcPublicSubnetsParameter | undefined;

private constructor(scope: GuStack) {
super(scope, "VpcPublicSubnets", {
description: NAMED_SSM_PARAMETER_PATHS.PrimaryVpcPublicSubnets.description,
default: NAMED_SSM_PARAMETER_PATHS.PrimaryVpcPublicSubnets.path,
type: "AWS::SSM::Parameter::Value<List<AWS::EC2::Subnet::Id>>",
});
}

public static getInstance(stack: GuStack): GuVpcPublicSubnetsParameter {
if (!this.instance || !isSingletonPresentInStack(stack, this.instance)) {
this.instance = new GuVpcPublicSubnetsParameter(stack);
}

return this.instance;
}
}
1 change: 0 additions & 1 deletion src/constructs/ec2/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from "./security-groups";
export * from "./vpc";
66 changes: 0 additions & 66 deletions src/constructs/ec2/vpc.test.ts

This file was deleted.

Loading

0 comments on commit 433d49f

Please sign in to comment.