Skip to main content

CDK Constructs for AWS EC2

Project description

Amazon EC2 Construct Library


Stability: Stable


The @aws-cdk/aws-ec2 package contains primitives for setting up networking and instances.

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:

import ec2 = require('@aws-cdk/aws-ec2');

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 - 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 - instances in private subnets are not directly routable from the Internet, and connect out to the Internet via a NAT gateway. By default, a NAT gateway is created in every public subnet for maximum availability. Be aware that you will be charged for NAT gateways.
  • 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, but not isolated subnets. See 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.

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 on the Stack, the CLI will look up the existing Availability Zones and get an accurate count. 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.

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:

const vpc = new ec2.Vpc(this, 'TheVPC', {
  // 'cidr' configures the IP range and size of the entire VPC.
  // The IP space will be divided over the configured subnets.
  cidr: '10.0.0.0/21',

  // 'maxAzs' configures the maximum number of availability zones to use
  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,
    },
    {
      cidrMask: 28,
      name: 'Database',
      subnetType: ec2.SubnetType.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

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:

import ec2 = require('@aws-cdk/aws-ec2');
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,
    },
    {
      cidrMask: 26,
      name: 'Application2',
      subnetType: ec2.SubnetType.PRIVATE,
      reserved: true,   // <---- This subnet group is reserved
    },
    {
      cidrMask: 27,
      name: 'Database',
      subnetType: ec2.SubnetType.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 Stacks inside the same CDK application, you can reuse a VPC defined in one Stack in another by simply passing the VPC instance around:

/**
 * Stack1 creates the VPC
 */
class Stack1 extends cdk.Stack {
  public readonly vpc: ec2.Vpc;

  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    this.vpc = new ec2.Vpc(this, 'VPC');
  }
}

interface Stack2Props extends cdk.StackProps {
  vpc: ec2.IVpc;
}

/**
 * Stack2 consumes the VPC
 */
class Stack2 extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props: Stack2Props) {
    super(scope, id, props);

    // Pass the VPC to a construct that needs it
    new ConstructThatTakesAVpc(this, 'Construct', {
      vpc: props.vpc
    });
  }
}

const stack1 = new Stack1(app, 'Stack1');
const stack2 = new Stack2(app, 'Stack2', {
  vpc: stack1.vpc,
});

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.

The import does assume that the VPC will be symmetric, i.e. that there are subnet groups that have a subnet in every Availability Zone that the VPC spreads over. VPCs with other layouts cannot currently be imported, and will either lead to an error on import, or when another construct tries to access the subnets.

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").

Here's how Vpc.fromLookup() can be used:

const vpc = ec2.Vpc.fromLookup(stack, 'VPC', {
  // This imports the default VPC but you can also
  // specify a 'vpcName' or 'tags'.
  isDefault: true
});

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:

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:

// Allow connections from anywhere
loadBalancer.connections.allowFromAnyIpv4(ec2.Port.tcp(443), 'Allow inbound HTTPS');

// The same, but an explicit IP address
loadBalancer.connections.allowFrom(ec2.Peer.ipv4('1.2.3.4/32'), ec2.Port.tcp(443), 'Allow inbound HTTPS');

// Allow connection between AutoScalingGroups
appFleet.connections.allowTo(dbFleet, ec2.Port.tcp(443), 'App can call database');

Connection Peers

There are various classes that implement the connection peer part:

// Simple connection peers
let peer = ec2.Peer.ipv4("10.0.0.0/16");
let peer = ec2.Peer.anyIpv4();
let peer = ec2.Peer.ipv6("::0/0");
let peer = ec2.Peer.anyIpv6();
let peer = ec2.Peer.prefixList("pl-12345");
fleet.connections.allowTo(peer, ec2.Port.tcp(443), 'Allow outbound HTTPS');

Any object that has a security group can itself be used as a connection peer:

// These automatically create appropriate ingress and egress rules in both security groups
fleet1.connections.allowTo(fleet2, ec2.Port.tcp(80), 'Allow between fleets');

fleet.connections.allowFromAnyIpv4(ec2.Port.tcp(80), 'Allow from load balancer');

Port Ranges

The connections that are allowed are specified by port ranges. A number of classes provide the connection specifier:

ec2.Port.tcp(80)
ec2.Port.tcpRange(60000, 65535)
ec2.Port.allTcp()
ec2.Port.allTraffic()

NOTE: This set is not complete yet; for example, there is no library support for ICMP at the moment. However, you can write your own classes to implement those.

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:

// Port implicit in listener
listener.connections.allowDefaultPortFromAnyIpv4('Allow public');

// Port implicit in peer
fleet.connections.allowDefaultPortTo(rdsDatabase, 'Fleet can access database');

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.

The latest version of Amazon Linux and Microsoft Windows images are selectable by instantiating one of these classes:

// Pick a Windows edition to use
const windows = new ec2.WindowsImage(ec2.WindowsVersion.WINDOWS_SERVER_2019_ENGLISH_FULL_BASE);

// Pick the right Amazon Linux edition. All arguments shown are optional
// and will default to these values when omitted.
const amznLinux = new ec2.AmazonLinuxImage({
  generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX,
  edition: ec2.AmazonLinuxEdition.STANDARD,
  virtualization: ec2.AmazonLinuxVirt.HVM,
  storage: ec2.AmazonLinuxStorage.GENERAL_PURPOSE,
});

// For other custom (Linux) images, instantiate a `GenericLinuxImage` with
// a map giving the AMI to in for each region:

const linux = new ec2.GenericLinuxImage({
    'us-east-1': 'ami-97785bed',
    'eu-west-1': 'ami-12345678',
    // ...
});

// For other custom (Windows) images, instantiate a `GenericWindowsImage` with
// a map giving the AMI to in for each region:

const genericWindows = new ec2.GenericWindowsImage({
  'us-east-1': 'ami-97785bed',
  'eu-west-1': 'ami-12345678',
  // ...
});

NOTE: The Amazon Linux images selected will be cached in your cdk.json, so that your AutoScalingGroups don't automatically change out from under you when you're making unrelated changes. To update to the latest version of Amazon Linux, remove the cache entry from the context section of your cdk.json.

We will add command-line options to make this step easier in the future.

VPN connections to a VPC

Create your VPC with VPN connections by specifying the vpnConnections props (keys are construct ids):

const vpc = new ec2.Vpc(stack, 'MyVpc', {
  vpnConnections: {
    dynamic: { // Dynamic routing (BGP)
      ip: '1.2.3.4'
    },
    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:

const vpc = new ec2.Vpc(stack, 'MyVpc', {
  vpnGateway: true
});

VPN connections can then be added:

vpc.addVpnConnection('Dynamic', {
  ip: '1.2.3.4'
});

Routes will be propagated on the route tables associated with the private subnets.

VPN connections expose metrics (cloudwatch.Metric) across all tunnels in the account/region and per connection:

// Across all tunnels in the account/region
const allDataOut = 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.

    // Add gateway endpoints when creating the VPC
    const vpc = new ec2.Vpc(this, 'MyVpc', {
      gatewayEndpoints: {
        S3: {
          service: ec2.GatewayVpcEndpointAwsService.S3
        }
      }
    });

    // Alternatively gateway endpoints can be added on the VPC
    const dynamoDbEndpoint = vpc.addGatewayEndpoint('DynamoDbEndpoint', {
      service: ec2.GatewayVpcEndpointAwsService.DYNAMODB
    });

    // This allows to customize the endpoint policy
    dynamoDbEndpoint.addToPolicy(
      new iam.PolicyStatement({ // Restrict to listing and describing tables
        principals: [new iam.AnyPrincipal()],
        actions: ['dynamodb:DescribeTable', 'dynamodb:ListTables'],
        resources: ['*'],
    }));

    // Add an interface endpoint
    const ecrDockerEndpoint = vpc.addInterfaceEndpoint('EcrDockerEndpoint', {
      service: ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER
    });

    // When working with an interface endpoint, use the connections object to
    // allow traffic to flow to the endpoint.
    ecrDockerEndpoint.connections.allowDefaultPortFromAnyIpv4();

Bastion Hosts

A bastion host functions as an instance used to access servers and resources in a VPC without open up the complete VPC on a network level. You can use bastion hosts using a standard SSH connection targetting port 22 on the host. As an alternative, you can connect the SSH connection feature of AWS Systems Manager Session Manager, which does not need an opened security group. (https://aws.amazon.com/about-aws/whats-new/2019/07/session-manager-launches-tunneling-support-for-ssh-and-scp/)

A default bastion host for use via SSM can be configured like:

const host = new ec2.BastionHostLinux(this, 'BastionHost', { vpc });

If you want to connect from the internet using SSH, you need to place the host into a public subnet. You can then configure allowed source hosts.

const host = new ec2.BastionHostLinux(this, 'BastionHost', {
  vpc,
  subnetSelection: { subnetType: SubnetType.PUBLIC },
});
host.allowSshAccessFrom(Peer.ipv4('1.2.3.4/32'));

As there are no SSH public keys deployed on this machine, you need to use EC2 Instance Connect with the command aws ec2-instance-connect send-ssh-public-key to provide your SSH public key.

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aws-cdk.aws-ec2-1.12.0.tar.gz (596.4 kB view hashes)

Uploaded Source

Built Distribution

aws_cdk.aws_ec2-1.12.0-py3-none-any.whl (586.4 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page