UNPKG

aws-cdk-lib

Version:

Version 2 of the AWS Cloud Development Kit library

1,206 lines (934 loc) 101 kB
# Amazon EC2 Construct Library The `aws-cdk-lib/aws-ec2` package contains primitives for setting up networking and instances. ```ts nofixture import * as ec2 from 'aws-cdk-lib/aws-ec2'; ``` ## VPC Most projects need a Virtual Private Cloud to provide security by means of network partitioning. This is achieved by creating an instance of `Vpc`: ```ts const vpc = new ec2.Vpc(this, 'VPC'); ``` All default constructs require EC2 instances to be launched inside a VPC, so you should generally start by defining a VPC whenever you need to launch instances for your project. ### Subnet Types A VPC consists of one or more subnets that instances can be placed into. CDK distinguishes three different subnet types: * **Public (`SubnetType.PUBLIC`)** - public subnets connect directly to the Internet using an Internet Gateway. If you want your instances to have a public IP address and be directly reachable from the Internet, you must place them in a public subnet. * **Private with Internet Access (`SubnetType.PRIVATE_WITH_EGRESS`)** - instances in private subnets are not directly routable from the Internet, and you must provide a way to connect out to the Internet. By default, a NAT gateway is created in every public subnet for maximum availability. Be aware that you will be charged for NAT gateways. Alternatively you can set `natGateways:0` and provide your own egress configuration (i.e through Transit Gateway) * **Isolated (`SubnetType.PRIVATE_ISOLATED`)** - isolated subnets do not route from or to the Internet, and as such do not require NAT gateways. They can only connect to or be connected to from other instances in the same VPC. A default VPC configuration will not include isolated subnets, A default VPC configuration will create public and **private** subnets. However, if `natGateways:0` **and** `subnetConfiguration` is undefined, default VPC configuration will create public and **isolated** subnets. See [*Advanced Subnet Configuration*](#advanced-subnet-configuration) below for information on how to change the default subnet configuration. Constructs using the VPC will "launch instances" (or more accurately, create Elastic Network Interfaces) into one or more of the subnets. They all accept a property called `subnetSelection` (sometimes called `vpcSubnets`) to allow you to select in what subnet to place the ENIs, usually defaulting to *private* subnets if the property is omitted. If you would like to save on the cost of NAT gateways, you can use *isolated* subnets instead of *private* subnets (as described in Advanced *Subnet Configuration*). If you need private instances to have internet connectivity, another option is to reduce the number of NAT gateways created by setting the `natGateways` property to a lower value (the default is one NAT gateway per availability zone). Be aware that this may have availability implications for your application. [Read more about subnets](https://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Subnets.html). ### Control over availability zones By default, a VPC will spread over at most 3 Availability Zones available to it. To change the number of Availability Zones that the VPC will spread over, specify the `maxAzs` property when defining it. The number of Availability Zones that are available depends on the *region* and *account* of the Stack containing the VPC. If the [region and account are specified](https://docs.aws.amazon.com/cdk/latest/guide/environments.html) on the Stack, the CLI will [look up the existing Availability Zones](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#using-regions-availability-zones-describe) and get an accurate count. The result of this operation will be written to a file called `cdk.context.json`. You must commit this file to source control so that the lookup values are available in non-privileged environments such as CI build steps, and to ensure your template builds are repeatable. If region and account are not specified, the stack could be deployed anywhere and it will have to make a safe choice, limiting itself to 2 Availability Zones. Therefore, to get the VPC to spread over 3 or more availability zones, you must specify the environment where the stack will be deployed. You can gain full control over the availability zones selection strategy by overriding the Stack's [`get availabilityZones()`](https://github.com/aws/aws-cdk/blob/main/packages/@aws-cdk/core/lib/stack.ts) method: ```text // This example is only available in TypeScript class MyStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); // ... } get availabilityZones(): string[] { return ['us-west-2a', 'us-west-2b']; } } ``` Note that overriding the `get availabilityZones()` method will override the default behavior for all constructs defined within the Stack. ### Choosing subnets for resources When creating resources that create Elastic Network Interfaces (such as databases or instances), there is an option to choose which subnets to place them in. For example, a VPC endpoint by default is placed into a subnet in every availability zone, but you can override which subnets to use. The property is typically called one of `subnets`, `vpcSubnets` or `subnetSelection`. The example below will place the endpoint into two AZs (`us-east-1a` and `us-east-1c`), in Isolated subnets: ```ts declare const vpc: ec2.Vpc; new ec2.InterfaceVpcEndpoint(this, 'VPC Endpoint', { vpc, service: new ec2.InterfaceVpcEndpointService('com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', 443), subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED, availabilityZones: ['us-east-1a', 'us-east-1c'] } }); ``` You can also specify specific subnet objects for granular control: ```ts declare const vpc: ec2.Vpc; declare const subnet1: ec2.Subnet; declare const subnet2: ec2.Subnet; new ec2.InterfaceVpcEndpoint(this, 'VPC Endpoint', { vpc, service: new ec2.InterfaceVpcEndpointService('com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', 443), subnets: { subnets: [subnet1, subnet2] } }); ``` Which subnets are selected is evaluated as follows: * `subnets`: if specific subnet objects are supplied, these are selected, and no other logic is used. * `subnetType`/`subnetGroupName`: otherwise, a set of subnets is selected by supplying either type or name: * `subnetType` will select all subnets of the given type. * `subnetGroupName` should be used to distinguish between multiple groups of subnets of the same type (for example, you may want to separate your application instances and your RDS instances into two distinct groups of Isolated subnets). * If neither are given, the first available subnet group of a given type that exists in the VPC will be used, in this order: Private, then Isolated, then Public. In short: by default ENIs will preferentially be placed in subnets not connected to the Internet. * `availabilityZones`/`onePerAz`: finally, some availability-zone based filtering may be done. This filtering by availability zones will only be possible if the VPC has been created or looked up in a non-environment agnostic stack (so account and region have been set and availability zones have been looked up). * `availabilityZones`: only the specific subnets from the selected subnet groups that are in the given availability zones will be returned. * `onePerAz`: per availability zone, a maximum of one subnet will be returned (Useful for resource types that do not allow creating two ENIs in the same availability zone). * `subnetFilters`: additional filtering on subnets using any number of user-provided filters which extend `SubnetFilter`. The following methods on the `SubnetFilter` class can be used to create a filter: * `byIds`: chooses subnets from a list of ids * `availabilityZones`: chooses subnets in the provided list of availability zones * `onePerAz`: chooses at most one subnet per availability zone * `containsIpAddresses`: chooses a subnet which contains *any* of the listed ip addresses * `byCidrMask`: chooses subnets that have the provided CIDR netmask * `byCidrRanges`: chooses subnets which are inside any of the specified CIDR ranges ### Using NAT instances By default, the `Vpc` construct will create NAT *gateways* for you, which are managed by AWS. If you would prefer to use your own managed NAT *instances* instead, specify a different value for the `natGatewayProvider` property, as follows: The construct will automatically selects the latest version of Amazon Linux 2023. If you prefer to use a custom AMI, use `machineImage: MachineImage.genericLinux({ ... })` and configure the right AMI ID for the regions you want to deploy to. > **Warning** > The NAT instances created using this method will be **unmonitored**. > They are not part of an Auto Scaling Group, > and if they become unavailable or are terminated for any reason, > will not be restarted or replaced. By default, the NAT instances will route all traffic. To control what traffic gets routed, pass a custom value for `defaultAllowedTraffic` and access the `NatInstanceProvider.connections` member after having passed the NAT provider to the VPC: ```ts declare const instanceType: ec2.InstanceType; const provider = ec2.NatProvider.instanceV2({ instanceType, defaultAllowedTraffic: ec2.NatTrafficDirection.OUTBOUND_ONLY, }); new ec2.Vpc(this, 'TheVPC', { natGatewayProvider: provider, }); provider.connections.allowFrom(ec2.Peer.ipv4('1.2.3.4/8'), ec2.Port.HTTP); ``` You can also customize the characteristics of your NAT instances, including their security group, as well as their initialization scripts: ```ts declare const bucket: s3.Bucket; const userData = ec2.UserData.forLinux(); userData.addCommands( ...ec2.NatInstanceProviderV2.DEFAULT_USER_DATA_COMMANDS, 'echo "hello world!" > hello.txt', `aws s3 cp hello.txt s3://${bucket.bucketName}`, ); const provider = ec2.NatProvider.instanceV2({ instanceType: new ec2.InstanceType('t3.small'), creditSpecification: ec2.CpuCredits.UNLIMITED, defaultAllowedTraffic: ec2.NatTrafficDirection.NONE, }); const vpc = new ec2.Vpc(this, 'TheVPC', { natGatewayProvider: provider, natGateways: 2, }); const securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { vpc }); securityGroup.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443)); for (const gateway of provider.gatewayInstances) { bucket.grantWrite(gateway); gateway.addSecurityGroup(securityGroup); } ``` [using NAT instances](test/integ.nat-instances.lit.ts) [Deprecated] The V1 `NatProvider.instance` construct will use the AWS official NAT instance AMI, which has already reached EOL on Dec 31, 2023. For more information, see the following blog post: [Amazon Linux AMI end of life](https://aws.amazon.com/blogs/aws/update-on-amazon-linux-ami-end-of-life/). ```ts declare const instanceType: ec2.InstanceType; const provider = ec2.NatProvider.instance({ instanceType, defaultAllowedTraffic: ec2.NatTrafficDirection.OUTBOUND_ONLY, }); new ec2.Vpc(this, 'TheVPC', { natGatewayProvider: provider, }); provider.connections.allowFrom(ec2.Peer.ipv4('1.2.3.4/8'), ec2.Port.HTTP); ``` ### Associate Public IP Address to NAT Instance You can choose to associate public IP address to a NAT instance V2 by specifying `associatePublicIpAddress` like the following: ```ts const natGatewayProvider = ec2.NatProvider.instanceV2({ instanceType: new ec2.InstanceType('t3.small'), associatePublicIpAddress: true, }); ``` In certain scenarios where the public subnet has set `mapPublicIpOnLaunch` to `false`, NAT instances does not get public IP addresses assigned which would result in non-working NAT instance as NAT instance requires a public IP address to enable outbound internet connectivity. Users can specify `associatePublicIpAddress` to `true` to solve this problem. ### Ip Address Management The VPC spans a supernet IP range, which contains the non-overlapping IPs of its contained subnets. Possible sources for this IP range are: * You specify an IP range directly by specifying a CIDR * You allocate an IP range of a given size automatically from AWS IPAM By default the Vpc will allocate the `10.0.0.0/16` address range which will be exhaustively spread across all subnets in the subnet configuration. This behavior can be changed by passing an object that implements `IIpAddresses` to the `ipAddress` property of a Vpc. See the subsequent sections for the options. Be aware that if you don't explicitly reserve subnet groups in `subnetConfiguration`, the address space will be fully allocated! If you predict you may need to add more subnet groups later, add them early on and set `reserved: true` (see the "Advanced Subnet Configuration" section for more information). #### Specifying a CIDR directly Use `IpAddresses.cidr` to define a Cidr range for your Vpc directly in code: ```ts import { IpAddresses } from 'aws-cdk-lib/aws-ec2'; new ec2.Vpc(this, 'TheVPC', { ipAddresses: IpAddresses.cidr('10.0.1.0/20') }); ``` Space will be allocated to subnets in the following order: * First, spaces is allocated for all subnets groups that explicitly have a `cidrMask` set as part of their configuration (including reserved subnets). * Afterwards, any remaining space is divided evenly between the rest of the subnets (if any). The argument to `IpAddresses.cidr` may not be a token, and concrete Cidr values are generated in the synthesized CloudFormation template. #### Allocating an IP range from AWS IPAM Amazon VPC IP Address Manager (IPAM) manages a large IP space, from which chunks can be allocated for use in the Vpc. For information on Amazon VPC IP Address Manager please see the [official documentation](https://docs.aws.amazon.com/vpc/latest/ipam/what-it-is-ipam.html). An example of allocating from AWS IPAM looks like this: ```ts import { IpAddresses } from 'aws-cdk-lib/aws-ec2'; declare const pool: ec2.CfnIPAMPool; new ec2.Vpc(this, 'TheVPC', { ipAddresses: IpAddresses.awsIpamAllocation({ ipv4IpamPoolId: pool.ref, ipv4NetmaskLength: 18, defaultSubnetIpv4NetmaskLength: 24 }) }); ``` `IpAddresses.awsIpamAllocation` requires the following: * `ipv4IpamPoolId`, the id of an IPAM Pool from which the VPC range should be allocated. * `ipv4NetmaskLength`, the size of the IP range that will be requested from the Pool at deploy time. * `defaultSubnetIpv4NetmaskLength`, the size of subnets in groups that don't have `cidrMask` set. With this method of IP address management, no attempt is made to guess at subnet group sizes or to exhaustively allocate the IP range. All subnet groups must have an explicit `cidrMask` set as part of their subnet configuration, or `defaultSubnetIpv4NetmaskLength` must be set for a default size. If not, synthesis will fail and you must provide one or the other. ### Dual Stack configuration To allocate both IPv4 and IPv6 addresses in your VPC, you can configure your VPC to have a dual stack protocol. ```ts new ec2.Vpc(this, 'DualStackVpc', { ipProtocol: ec2.IpProtocol.DUAL_STACK, }) ``` By default, a dual stack VPC will create an Amazon provided IPv6 /56 CIDR block associated to the VPC. It will then assign /64 portions of the block to each subnet. For each subnet, auto-assigning an IPv6 address will be enabled, and auto-asigning a public IPv4 address will be disabled. An egress only internet gateway will be created for `PRIVATE_WITH_EGRESS` subnets, and IPv6 routes will be added for IGWs and EIGWs. Disabling the auto-assigning of a public IPv4 address by default can avoid the cost of public IPv4 addresses starting 2/1/2024. For use cases that need an IPv4 address, the `mapPublicIpOnLaunch` property in `subnetConfiguration` can be set to auto-assign the IPv4 address. Note that private IPv4 address allocation will not be changed. See [Advanced Subnet Configuration](#advanced-subnet-configuration) for all IPv6 specific properties. ### Reserving availability zones There are situations where the IP space for availability zones will need to be reserved. This is useful in situations where availability zones would need to be added after the vpc is originally deployed, without causing IP renumbering for availability zones subnets. The IP space for reserving `n` availability zones can be done by setting the `reservedAzs` to `n` in vpc props, as shown below: ```ts const vpc = new ec2.Vpc(this, 'TheVPC', { cidr: '10.0.0.0/21', maxAzs: 3, reservedAzs: 1, }); ``` In the example above, the subnets for reserved availability zones is not actually provisioned but its IP space is still reserved. If, in the future, new availability zones needs to be provisioned, then we would decrement the value of `reservedAzs` and increment the `maxAzs` or `availabilityZones` accordingly. This action would not cause the IP address of subnets to get renumbered, but rather the IP space that was previously reserved will be used for the new availability zones subnets. ### Advanced Subnet Configuration If the default VPC configuration (public and private subnets spanning the size of the VPC) don't suffice for you, you can configure what subnets to create by specifying the `subnetConfiguration` property. It allows you to configure the number and size of all subnets. Specifying an advanced subnet configuration could look like this: ```ts const vpc = new ec2.Vpc(this, 'TheVPC', { // 'IpAddresses' configures the IP range and size of the entire VPC. // The IP space will be divided based on configuration for the subnets. ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/21'), // 'maxAzs' configures the maximum number of availability zones to use. // If you want to specify the exact availability zones you want the VPC // to use, use `availabilityZones` instead. maxAzs: 3, // 'subnetConfiguration' specifies the "subnet groups" to create. // Every subnet group will have a subnet for each AZ, so this // configuration will create `3 groups × 3 AZs = 9` subnets. subnetConfiguration: [ { // 'subnetType' controls Internet access, as described above. subnetType: ec2.SubnetType.PUBLIC, // 'name' is used to name this particular subnet group. You will have to // use the name for subnet selection if you have more than one subnet // group of the same type. name: 'Ingress', // 'cidrMask' specifies the IP addresses in the range of of individual // subnets in the group. Each of the subnets in this group will contain // `2^(32 address bits - 24 subnet bits) - 2 reserved addresses = 254` // usable IP addresses. // // If 'cidrMask' is left out the available address space is evenly // divided across the remaining subnet groups. cidrMask: 24, }, { cidrMask: 24, name: 'Application', subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, }, { cidrMask: 28, name: 'Database', subnetType: ec2.SubnetType.PRIVATE_ISOLATED, // 'reserved' can be used to reserve IP address space. No resources will // be created for this subnet, but the IP range will be kept available for // future creation of this subnet, or even for future subdivision. reserved: true } ], }); ``` The example above is one possible configuration, but the user can use the constructs above to implement many other network configurations. The `Vpc` from the above configuration in a Region with three availability zones will be the following: Subnet Name |Type |IP Block |AZ|Features ------------------|----------|--------------|--|-------- IngressSubnet1 |`PUBLIC` |`10.0.0.0/24` |#1|NAT Gateway IngressSubnet2 |`PUBLIC` |`10.0.1.0/24` |#2|NAT Gateway IngressSubnet3 |`PUBLIC` |`10.0.2.0/24` |#3|NAT Gateway ApplicationSubnet1|`PRIVATE` |`10.0.3.0/24` |#1|Route to NAT in IngressSubnet1 ApplicationSubnet2|`PRIVATE` |`10.0.4.0/24` |#2|Route to NAT in IngressSubnet2 ApplicationSubnet3|`PRIVATE` |`10.0.5.0/24` |#3|Route to NAT in IngressSubnet3 DatabaseSubnet1 |`ISOLATED`|`10.0.6.0/28` |#1|Only routes within the VPC DatabaseSubnet2 |`ISOLATED`|`10.0.6.16/28`|#2|Only routes within the VPC DatabaseSubnet3 |`ISOLATED`|`10.0.6.32/28`|#3|Only routes within the VPC #### Dual Stack Configurations Here is a break down of IPv4 and IPv6 specific `subnetConfiguration` properties in a dual stack VPC: ```ts const vpc = new ec2.Vpc(this, 'TheVPC', { ipProtocol: ec2.IpProtocol.DUAL_STACK, subnetConfiguration: [ { // general properties name: 'Public', subnetType: ec2.SubnetType.PUBLIC, reserved: false, // IPv4 specific properties mapPublicIpOnLaunch: true, cidrMask: 24, // new IPv6 specific property ipv6AssignAddressOnCreation: true, }, ], }); ``` The property `mapPublicIpOnLaunch` controls if a public IPv4 address will be assigned. This defaults to `false` for dual stack VPCs to avoid inadvertant costs of having the public address. However, a public IP must be enabled (or otherwise configured with BYOIP or IPAM) in order for services that rely on the address to function. The `ipv6AssignAddressOnCreation` property controls the same behavior for the IPv6 address. It defaults to true. Using IPv6 specific properties in an IPv4 only VPC will result in errors. ### Accessing the Internet Gateway If you need access to the internet gateway, you can get its ID like so: ```ts declare const vpc: ec2.Vpc; const igwId = vpc.internetGatewayId; ``` For a VPC with only `ISOLATED` subnets, this value will be undefined. This is only supported for VPCs created in the stack - currently you're unable to get the ID for imported VPCs. To do that you'd have to specifically look up the Internet Gateway by name, which would require knowing the name beforehand. This can be useful for configuring routing using a combination of gateways: for more information see [Routing](#routing) below. ### Disabling the creation of the default internet gateway If you need to control the creation of the internet gateway explicitly, you can disable the creation of the default one using the `createInternetGateway` property: ```ts const vpc = new ec2.Vpc(this, "VPC", { createInternetGateway: false, subnetConfiguration: [{ subnetType: ec2.SubnetType.PUBLIC, name: 'Public', }] }); ``` #### Routing It's possible to add routes to any subnets using the `addRoute()` method. If for example you want an isolated subnet to have a static route via the default Internet Gateway created for the public subnet - perhaps for routing a VPN connection - you can do so like this: ```ts const vpc = new ec2.Vpc(this, "VPC", { subnetConfiguration: [{ subnetType: ec2.SubnetType.PUBLIC, name: 'Public', },{ subnetType: ec2.SubnetType.PRIVATE_ISOLATED, name: 'Isolated', }] }); (vpc.isolatedSubnets[0] as ec2.Subnet).addRoute("StaticRoute", { routerId: vpc.internetGatewayId!, routerType: ec2.RouterType.GATEWAY, destinationCidrBlock: "8.8.8.8/32", }) ``` *Note that we cast to `Subnet` here because the list of subnets only returns an `ISubnet`.* ### Reserving subnet IP space There are situations where the IP space for a subnet or number of subnets will need to be reserved. This is useful in situations where subnets would need to be added after the vpc is originally deployed, without causing IP renumbering for existing subnets. The IP space for a subnet may be reserved by setting the `reserved` subnetConfiguration property to true, as shown below: ```ts const vpc = new ec2.Vpc(this, 'TheVPC', { natGateways: 1, subnetConfiguration: [ { cidrMask: 26, name: 'Public', subnetType: ec2.SubnetType.PUBLIC, }, { cidrMask: 26, name: 'Application1', subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, }, { cidrMask: 26, name: 'Application2', subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, reserved: true, // <---- This subnet group is reserved }, { cidrMask: 27, name: 'Database', subnetType: ec2.SubnetType.PRIVATE_ISOLATED, } ], }); ``` In the example above, the subnet for Application2 is not actually provisioned but its IP space is still reserved. If in the future this subnet needs to be provisioned, then the `reserved: true` property should be removed. Reserving parts of the IP space prevents the other subnets from getting renumbered. ### Sharing VPCs between stacks If you are creating multiple `Stack`s inside the same CDK application, you can reuse a VPC defined in one Stack in another by simply passing the VPC instance around: [sharing VPCs between stacks](test/integ.share-vpcs.lit.ts) > Note: If you encounter an error like "Delete canceled. Cannot delete export ..." > when using a cross-stack reference to a VPC, it's likely due to CloudFormation > export/import constraints. In such cases, it's safer to use Vpc.fromLookup() > in the consuming stack instead of directly referencing the VPC object, more details > is provided in [Importing an existing VPC](#importing-an-existing-vpc). This > avoids creating CloudFormation exports and gives more flexibility, especially > when stacks need to be deleted or updated independently. ### Importing an existing VPC If your VPC is created outside your CDK app, you can use `Vpc.fromLookup()`. The CDK CLI will search for the specified VPC in the the stack's region and account, and import the subnet configuration. Looking up can be done by VPC ID, but more flexibly by searching for a specific tag on the VPC. Subnet types will be determined from the `aws-cdk:subnet-type` tag on the subnet if it exists, or the presence of a route to an Internet Gateway otherwise. Subnet names will be determined from the `aws-cdk:subnet-name` tag on the subnet if it exists, or will mirror the subnet type otherwise (i.e. a public subnet will have the name `"Public"`). The result of the `Vpc.fromLookup()` operation will be written to a file called `cdk.context.json`. You must commit this file to source control so that the lookup values are available in non-privileged environments such as CI build steps, and to ensure your template builds are repeatable. Here's how `Vpc.fromLookup()` can be used: [importing existing VPCs](test/integ.import-default-vpc.lit.ts) `Vpc.fromLookup` is the recommended way to import VPCs. If for whatever reason you do not want to use the context mechanism to look up a VPC at synthesis time, you can also use `Vpc.fromVpcAttributes`. This has the following limitations: * Every subnet group in the VPC must have a subnet in each availability zone (for example, each AZ must have both a public and private subnet). Asymmetric VPCs are not supported. * All VpcId, SubnetId, RouteTableId, ... parameters must either be known at synthesis time, or they must come from deploy-time list parameters whose deploy-time lengths are known at synthesis time. Using `Vpc.fromVpcAttributes()` looks like this: ```ts const vpc = ec2.Vpc.fromVpcAttributes(this, 'VPC', { vpcId: 'vpc-1234', availabilityZones: ['us-east-1a', 'us-east-1b'], // Either pass literals for all IDs publicSubnetIds: ['s-12345', 's-67890'], // OR: import a list of known length privateSubnetIds: Fn.importListValue('PrivateSubnetIds', 2), // OR: split an imported string to a list of known length isolatedSubnetIds: Fn.split(',', ssm.StringParameter.valueForStringParameter(this, `MyParameter`), 2), }); ``` For each subnet group the import function accepts optional parameters for subnet names, route table ids and IPv4 CIDR blocks. When supplied, the length of these lists are required to match the length of the list of subnet ids, allowing the lists to be zipped together to form `ISubnet` instances. Public subnet group example (for private or isolated subnet groups, use the properties with the respective prefix): ```ts const vpc = ec2.Vpc.fromVpcAttributes(this, 'VPC', { vpcId: 'vpc-1234', availabilityZones: ['us-east-1a', 'us-east-1b', 'us-east-1c'], publicSubnetIds: ['s-12345', 's-34567', 's-56789'], publicSubnetNames: ['Subnet A', 'Subnet B', 'Subnet C'], publicSubnetRouteTableIds: ['rt-12345', 'rt-34567', 'rt-56789'], publicSubnetIpv4CidrBlocks: ['10.0.0.0/24', '10.0.1.0/24', '10.0.2.0/24'], }); ``` The above example will create an `IVpc` instance with three public subnets: | Subnet id | Availability zone | Subnet name | Route table id | IPv4 CIDR | | --------- | ----------------- | ----------- | -------------- | ----------- | | s-12345 | us-east-1a | Subnet A | rt-12345 | 10.0.0.0/24 | | s-34567 | us-east-1b | Subnet B | rt-34567 | 10.0.1.0/24 | | s-56789 | us-east-1c | Subnet B | rt-56789 | 10.0.2.0/24 | ### Restricting access to the VPC default security group AWS Security best practices recommend that the [VPC default security group should not allow inbound and outbound traffic](https://docs.aws.amazon.com/securityhub/latest/userguide/ec2-controls.html#ec2-2). When the `@aws-cdk/aws-ec2:restrictDefaultSecurityGroup` feature flag is set to `true` (default for new projects) this will be enabled by default. If you do not have this feature flag set you can either set the feature flag _or_ you can set the `restrictDefaultSecurityGroup` property to `true`. ```ts new ec2.Vpc(this, 'VPC', { restrictDefaultSecurityGroup: true, }); ``` If you set this property to `true` and then later remove it or set it to `false` the default ingress/egress will be restored on the default security group. ## Allowing Connections In AWS, all network traffic in and out of **Elastic Network Interfaces** (ENIs) is controlled by **Security Groups**. You can think of Security Groups as a firewall with a set of rules. By default, Security Groups allow no incoming (ingress) traffic and all outgoing (egress) traffic. You can add ingress rules to them to allow incoming traffic streams. To exert fine-grained control over egress traffic, set `allowAllOutbound: false` on the `SecurityGroup`, after which you can add egress traffic rules. You can manipulate Security Groups directly: ```ts fixture=with-vpc const mySecurityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { vpc, description: 'Allow ssh access to ec2 instances', allowAllOutbound: true // Can be set to false }); mySecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22), 'allow ssh access from the world'); ``` All constructs that create ENIs on your behalf (typically constructs that create EC2 instances or other VPC-connected resources) will all have security groups automatically assigned. Those constructs have an attribute called **connections**, which is an object that makes it convenient to update the security groups. If you want to allow connections between two constructs that have security groups, you have to add an **Egress** rule to one Security Group, and an **Ingress** rule to the other. The connections object will automatically take care of this for you: ```ts declare const loadBalancer: elbv2.ApplicationLoadBalancer; declare const appFleet: autoscaling.AutoScalingGroup; declare const dbFleet: autoscaling.AutoScalingGroup; // Allow connections from anywhere loadBalancer.connections.allowFromAnyIpv4(ec2.Port.HTTPS, 'Allow inbound HTTPS'); // The same, but an explicit IP address loadBalancer.connections.allowFrom(ec2.Peer.ipv4('1.2.3.4/32'), ec2.Port.HTTPS, 'Allow inbound HTTPS'); // Allow connection between AutoScalingGroups appFleet.connections.allowTo(dbFleet, ec2.Port.HTTPS, 'App can call database'); ``` ### Connection Peers There are various classes that implement the connection peer part: ```ts declare const appFleet: autoscaling.AutoScalingGroup; declare const dbFleet: autoscaling.AutoScalingGroup; // Simple connection peers let peer = ec2.Peer.ipv4('10.0.0.0/16'); peer = ec2.Peer.anyIpv4(); peer = ec2.Peer.ipv6('::0/0'); peer = ec2.Peer.anyIpv6(); appFleet.connections.allowTo(peer, ec2.Port.HTTPS, 'Allow outbound HTTPS'); ``` Any object that has a security group can itself be used as a connection peer: ```ts declare const fleet1: autoscaling.AutoScalingGroup; declare const fleet2: autoscaling.AutoScalingGroup; declare const appFleet: autoscaling.AutoScalingGroup; // These automatically create appropriate ingress and egress rules in both security groups fleet1.connections.allowTo(fleet2, ec2.Port.HTTP, 'Allow between fleets'); appFleet.connections.allowFromAnyIpv4(ec2.Port.HTTP, 'Allow from load balancer'); ``` A managed prefix list is also a connection peer: ``` ts declare const appFleet: autoscaling.AutoScalingGroup; const prefixList = new ec2.PrefixList(this, 'PrefixList', { maxEntries: 10 }); appFleet.connections.allowFrom(prefixList, ec2.Port.HTTPS); ``` ### Port Ranges The connections that are allowed are specified by port ranges. A number of classes provide the connection specifier: ```ts ec2.Port.tcp(80) ec2.Port.HTTPS ec2.Port.tcpRange(60000, 65535) ec2.Port.allTcp() ec2.Port.allIcmp() ec2.Port.allIcmpV6() ec2.Port.allTraffic() ``` > NOTE: Not all protocols have corresponding helper methods. In the absence of a helper method, > you can instantiate `Port` yourself with your own settings. You are also welcome to contribute > new helper methods. ### Default Ports Some Constructs have default ports associated with them. For example, the listener of a load balancer does (it's the public port), or instances of an RDS database (it's the port the database is accepting connections on). If the object you're calling the peering method on has a default port associated with it, you can call `allowDefaultPortFrom()` and omit the port specifier. If the argument has an associated default port, call `allowDefaultPortTo()`. For example: ```ts declare const listener: elbv2.ApplicationListener; declare const appFleet: autoscaling.AutoScalingGroup; declare const rdsDatabase: rds.DatabaseCluster; // Port implicit in listener listener.connections.allowDefaultPortFromAnyIpv4('Allow public'); // Port implicit in peer appFleet.connections.allowDefaultPortTo(rdsDatabase, 'Fleet can access database'); ``` ### Security group rules By default, security group wills be added inline to the security group in the output cloud formation template, if applicable. This includes any static rules by ip address and port range. This optimization helps to minimize the size of the template. In some environments this is not desirable, for example if your security group access is controlled via tags. You can disable inline rules per security group or globally via the context key `@aws-cdk/aws-ec2.securityGroupDisableInlineRules`. ```ts fixture=with-vpc const mySecurityGroupWithoutInlineRules = new ec2.SecurityGroup(this, 'SecurityGroup', { vpc, description: 'Allow ssh access to ec2 instances', allowAllOutbound: true, disableInlineRules: true }); //This will add the rule as an external cloud formation construct mySecurityGroupWithoutInlineRules.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.SSH, 'allow ssh access from the world'); ``` ### Importing an existing security group If you know the ID and the configuration of the security group to import, you can use `SecurityGroup.fromSecurityGroupId`: ```ts const sg = ec2.SecurityGroup.fromSecurityGroupId(this, 'SecurityGroupImport', 'sg-1234', { allowAllOutbound: true, }); ``` Alternatively, use lookup methods to import security groups if you do not know the ID or the configuration details. Method `SecurityGroup.fromLookupByName` looks up a security group if the security group ID is unknown. ```ts fixture=with-vpc const sg = ec2.SecurityGroup.fromLookupByName(this, 'SecurityGroupLookup', 'security-group-name', vpc); ``` If the security group ID is known and configuration details are unknown, use method `SecurityGroup.fromLookupById` instead. This method will lookup property `allowAllOutbound` from the current configuration of the security group. ```ts const sg = ec2.SecurityGroup.fromLookupById(this, 'SecurityGroupLookup', 'sg-1234'); ``` The result of `SecurityGroup.fromLookupByName` and `SecurityGroup.fromLookupById` operations will be written to a file called `cdk.context.json`. You must commit this file to source control so that the lookup values are available in non-privileged environments such as CI build steps, and to ensure your template builds are repeatable. ### Cross Stack Connections If you are attempting to add a connection from a peer in one stack to a peer in a different stack, sometimes it is necessary to ensure that you are making the connection in a specific stack in order to avoid a cyclic reference. If there are no other dependencies between stacks then it will not matter in which stack you make the connection, but if there are existing dependencies (i.e. stack1 already depends on stack2), then it is important to make the connection in the dependent stack (i.e. stack1). Whenever you make a `connections` function call, the ingress and egress security group rules will be added to the stack that the calling object exists in. So if you are doing something like `peer1.connections.allowFrom(peer2)`, then the security group rules (both ingress and egress) will be created in `peer1`'s Stack. As an example, if we wanted to allow a connection from a security group in one stack (egress) to a security group in a different stack (ingress), we would make the connection like: **If Stack1 depends on Stack2** ```ts fixture=with-vpc // Stack 1 declare const stack1: Stack; declare const stack2: Stack; const sg1 = new ec2.SecurityGroup(stack1, 'SG1', { allowAllOutbound: false, // if this is `true` then no egress rule will be created vpc, }); // Stack 2 const sg2 = new ec2.SecurityGroup(stack2, 'SG2', { allowAllOutbound: false, // if this is `true` then no egress rule will be created vpc, }); // `connections.allowTo` on `sg1` since we want the // rules to be created in Stack1 sg1.connections.allowTo(sg2, ec2.Port.tcp(3333)); ``` In this case both the Ingress Rule for `sg2` and the Egress Rule for `sg1` will both be created in `Stack 1` which avoids the cyclic reference. **If Stack2 depends on Stack1** ```ts fixture=with-vpc // Stack 1 declare const stack1: Stack; declare const stack2: Stack; const sg1 = new ec2.SecurityGroup(stack1, 'SG1', { allowAllOutbound: false, // if this is `true` then no egress rule will be created vpc, }); // Stack 2 const sg2 = new ec2.SecurityGroup(stack2, 'SG2', { allowAllOutbound: false, // if this is `true` then no egress rule will be created vpc, }); // `connections.allowFrom` on `sg2` since we want the // rules to be created in Stack2 sg2.connections.allowFrom(sg1, ec2.Port.tcp(3333)); ``` In this case both the Ingress Rule for `sg2` and the Egress Rule for `sg1` will both be created in `Stack 2` which avoids the cyclic reference. ## Machine Images (AMIs) AMIs control the OS that gets launched when you start your EC2 instance. The EC2 library contains constructs to select the AMI you want to use. Depending on the type of AMI, you select it a different way. Here are some examples of images you might want to use: [example of creating images](test/example.images.lit.ts) > NOTE: The AMIs selected by `MachineImage.lookup()` will be cached in > `cdk.context.json`, so that your AutoScalingGroup instances aren't replaced while > you are making unrelated changes to your CDK app. > > To query for the latest AMI again, remove the relevant cache entry from > `cdk.context.json`, or use the `cdk context` command. For more information, see > [Runtime Context](https://docs.aws.amazon.com/cdk/latest/guide/context.html) in the CDK > developer guide. > > To customize the cache key, use the `additionalCacheKey` parameter. > This allows you to have multiple lookups with the same parameters > cache their values separately. This can be useful if you want to > scope the context variable to a construct (ie, using `additionalCacheKey: this.node.path`), > so that if the value in the cache needs to be updated, it does not need to be updated > for all constructs at the same time. > > `MachineImage.genericLinux()`, `MachineImage.genericWindows()` will use `CfnMapping` in > an agnostic stack. ## Special VPC configurations ### VPN connections to a VPC Create your VPC with VPN connections by specifying the `vpnConnections` props (keys are construct `id`s): ```ts import { SecretValue } from 'aws-cdk-lib/core'; const vpc = new ec2.Vpc(this, 'MyVpc', { vpnConnections: { dynamic: { // Dynamic routing (BGP) ip: '1.2.3.4', tunnelOptions: [ { preSharedKeySecret: SecretValue.unsafePlainText('secretkey1234'), }, { preSharedKeySecret: SecretValue.unsafePlainText('secretkey5678'), }, ], }, static: { // Static routing ip: '4.5.6.7', staticRoutes: [ '192.168.10.0/24', '192.168.20.0/24' ] } } }); ``` To create a VPC that can accept VPN connections, set `vpnGateway` to `true`: ```ts const vpc = new ec2.Vpc(this, 'MyVpc', { vpnGateway: true }); ``` VPN connections can then be added: ```ts fixture=with-vpc vpc.addVpnConnection('Dynamic', { ip: '1.2.3.4' }); ``` By default, routes will be propagated on the route tables associated with the private subnets. If no private subnets exist, isolated subnets are used. If no isolated subnets exist, public subnets are used. Use the `Vpc` property `vpnRoutePropagation` to customize this behavior. VPN connections expose [metrics (cloudwatch.Metric)](https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk-lib/aws-cloudwatch/README.md) across all tunnels in the account/region and per connection: ```ts fixture=with-vpc // Across all tunnels in the account/region const allDataOut = ec2.VpnConnection.metricAllTunnelDataOut(); // For a specific vpn connection const vpnConnection = vpc.addVpnConnection('Dynamic', { ip: '1.2.3.4' }); const state = vpnConnection.metricTunnelState(); ``` ### VPC endpoints A VPC endpoint enables you to privately connect your VPC to supported AWS services and VPC endpoint services powered by PrivateLink without requiring an internet gateway, NAT device, VPN connection, or AWS Direct Connect connection. Instances in your VPC do not require public IP addresses to communicate with resources in the service. Traffic between your VPC and the other service does not leave the Amazon network. Endpoints are virtual devices. They are horizontally scaled, redundant, and highly available VPC components that allow communication between instances in your VPC and services without imposing availability risks or bandwidth constraints on your network traffic. [example of setting up VPC endpoints](test/integ.vpc-endpoint.lit.ts) By default, CDK will place a VPC endpoint in one subnet per AZ. If you wish to override the AZs CDK places the VPC endpoint in, use the `subnets` parameter as follows: ```ts declare const vpc: ec2.Vpc; new ec2.InterfaceVpcEndpoint(this, 'VPC Endpoint', { vpc, service: new ec2.InterfaceVpcEndpointService('com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', 443), // Choose which availability zones to place the VPC endpoint in, based on // available AZs subnets: { availabilityZones: ['us-east-1a', 'us-east-1c'] } }); ``` Per the [AWS documentation](https://aws.amazon.com/premiumsupport/knowledge-center/interface-endpoint-availability-zone/), not all VPC endpoint services are available in all AZs. If you specify the parameter `lookupSupportedAzs`, CDK attempts to discover which AZs an endpoint service is available in, and will ensure the VPC endpoint is not placed in a subnet that doesn't match those AZs. These AZs will be stored in cdk.context.json. ```ts declare const vpc: ec2.Vpc; new ec2.InterfaceVpcEndpoint(this, 'VPC Endpoint', { vpc, service: new ec2.InterfaceVpcEndpointService('com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', 443), // Choose which availability zones to place the VPC endpoint in, based on // available AZs lookupSupportedAzs: true }); ``` Pre-defined AWS services are defined in the [InterfaceVpcEndpointAwsService](lib/vpc-endpoint.ts) class, and can be used to create VPC endpoints without having to configure name, ports, etc. For example, a Keyspaces endpoint can be created for use in your VPC: ``` ts declare const vpc: ec2.Vpc; new ec2.InterfaceVpcEndpoint(this, 'VPC Endpoint', { vpc, service: ec2.InterfaceVpcEndpointAwsService.KEYSPACES, }); ``` #### Security groups for interface VPC endpoints By default, interface VPC endpoints create a new security group and all traffic to the endpoint from within the VPC will be automatically allowed. Use the `connections` object to allow other traffic to flow to the endpoint: ```ts declare const myEndpoint: ec2.InterfaceVpcEndpoint; myEndpoint.connections.allowDefaultPortFromAnyIpv4(); ``` Alternatively, existing security groups can be used by specifying the `securityGroups` prop. #### IPv6 and Dualstack support As IPv4 addresses are running out, many AWS services are adding support for IPv6 or Dualstack (IPv4 and IPv6 support) for their VPC Endpoints. IPv6 and Dualstack address types can be configured by using: ```ts fixture=with-vpc vpc.addInterfaceEndpoint('ExampleEndpoint', { service: ec2.InterfaceVpcEndpointAwsService.ECR, ipAddressType: ec2.VpcEndpointIpAddressType.IPV6, dnsRecordIpType: ec2.VpcEndpointDnsRecordIpType.IPV6, }); ``` The possible values for `ipAddressType` are: * `IPV4` This option is supported only if all selected subnets have IPv4 address ranges and the endpoint service accepts IPv4 requests. * `IPV6` This option is supported only if all selected subnets are IPv6 only subnets and the endpoint service accepts IPv6 requests. * `DUALSTACK` Assign both IPv4 and IPv6 addresses to the endpoint network interfaces. This option is supported only if all selected subnets have both IPv4 and IPv6 address ranges and the endpoint service accepts both IPv4 and IPv6 requests. The possible values for `dnsRecordIpType` are: * `IPV4` Create A records for the private, Regional, and zonal DNS names. `ipAddressType` MUST be `IPV4` or `DUALSTACK` * `IPV6` Create AAAA records for the private, Regional, and zonal DNS names. `ipAddressType` MUST be `IPV6` or `DUALSTACK` * `DUALSTACK` Create A and AAAA records for the private, Regional, and zonal DNS names. `ipAddressType` MUST be `DUALSTACK` * `SERVICE_DEFINED` Create A records for the private, Regional, and zonal DNS names and AAAA records for the Regional and zonal DNS names. `ipAddressType` MUST be `DUALSTACK` We can only configure dnsRecordIpType when ipAddressType is specified and private DNS must be enabled to use any DNS related features. To avoid complications, it is recommended to always set `privateDnsEnabled` to true (defaults to true) and set the `ipAddressType` and `dnsRecordIpType` explicitly when needing specific IP type behavior. Furthermore, check that the VPC being used supports the IP address type that is being configued. More documentation on compatibility and specifications can be found [here](https://docs.aws.amazon.com/vpc/latest/privatelink/create-endpoint-service.html#connect-to-endpoint-service) ### VPC endpoint services A VPC endpoint service enables you to expose a Network Load Balancer(s) as a provider service to consumers, who connect to your service over a VPC endpoint. You can restrict access to your service via allowed principals (anything that extends ArnPrincipal), and require that new connections be manually accepted. You can also enable Contributor Insight rules. ```ts declare const networkLoadBalancer1: elbv2.NetworkLoadBalancer; declare const networkLoadBalancer2: elbv2.NetworkLoadBalancer; new ec2.VpcEndpointService(this, 'EndpointService', { vpcEndpointServiceLoadBalancers: [networkLoadBalancer1, networkLoadBalancer2], acceptanceRequired: true, allowedPrincipals: [new iam.ArnPrincipal('arn:aws:iam::123456789012:root')], contributorInsights: true }); ``` You can also include a service principal in the `allowedPrincipals` property by specifying it as a parameter to the `ArnPrincipal` constructor. The resulting VPC endpoint will have an allowlisted principal of type `Service`, instead of `Arn` for that item in the list. ```ts declare const networkLoadBalancer: elbv2.NetworkLoadBalancer; new ec2.VpcEndpointService(this, 'EndpointService', { vpcEndpointServiceLoadBalancers: [networkLoadBalancer], allowedPrincipals: [new iam.ArnPrincipal('ec2.amazonaws.com')], }); ``` You can specify which IP address types (IPv4, IPv6, or both) are supported for your VPC endpoint service: ```ts declare const networkLoadBalancer: elbv2.NetworkLoadBalancer; new ec2.VpcEndpointService(this, 'EndpointService', { vpcEndpointServiceLoadBalancers: [networkLoadBalancer], // Support both IPv4 and IPv6 connections to the endpoint service supportedIpAddressTypes: [ ec2.IpAddressType.IPV4, ec2.IpAddressType.IPV6, ], }); ``` You can restrict access to your endpoint service to specific AWS regions: ```ts declare const networkLoadBalancer: elbv2.NetworkLoadBalancer; new ec2.VpcEndpointService(this, 'EndpointService', { vpcEndpointServiceLoadBalancers: [networkLoadBalancer], // Allow service consumers from these regions only allowedRegions: ['us-east-1', 'eu-west-1'], }); ``` Endpoint services support private DNS, which makes it easier for clients to connect to your service by automatically setting up DNS in their VPC. You can enable private DNS on an endpoint service like so: ```ts import { PublicHostedZone, VpcEndpointServiceDomainName } from 'aws-cdk-lib/aws-route53'; declare const zone: PublicHostedZone; declare cons