VPC & networking
A VPC is a private network of your own inside an AWS region. Containers, virtual machines, databases and caches run inside one, and the VPC decides what can reach them: from the internet, from each other, and from AWS’s own services.
Who needs a VPC
Section titled “Who needs a VPC”Serverless services don’t live in your VPC. A Lambda Function, a Bucket, a Table and a Queue are reached over AWS’s own endpoints, and a Function serves HTTP from its Function URL. An app made only of those needs no network at all.
A VPC comes in when something needs a network interface of its own: an ECS task, an EKS node, an EC2 instance, an Aurora database, a cache. A Lambda Function joins a VPC too when it has to reach one of those privately.
Subnets split the network across zones
Section titled “Subnets split the network across zones”A VPC owns a range of private addresses, like 10.42.0.0/16. Subnets divide it into smaller ranges, and each subnet sits in one Availability Zone. Put a subnet in each of at least two zones, so losing a data center leaves the app running in the other.
What makes a subnet public or private is its route table. A public subnet routes internet traffic to an internet gateway, so things in it can have public addresses and be reached from outside. A private subnet has no route in from the internet. If it routes outbound traffic to a NAT gateway in a public subnet, things in it can call out, to download packages or call an API, and still can’t be reached. Load balancers go in public subnets. Databases, and usually containers, go in private ones.
Security groups are firewalls around each resource
Section titled “Security groups are firewalls around each resource”A security group is a set of rules around each network interface: which ports accept traffic, and from where. It’s stateful, so the reply to an allowed request is always allowed back.
A rule can name another security group instead of an address. “Port 5432, from anything in the App group” lets your containers reach the database however many there are and whatever addresses they get:
const database = yield* AWS.EC2.SecurityGroup("Database", { vpcId: network.vpcId, ingress: [{ ipProtocol: "tcp", fromPort: 5432, toPort: 5432, referencedGroupId: app.groupId }],});Build the usual layout
Section titled “Build the usual layout”AWS.EC2.Network builds a VPC with an internet gateway, one public and one private subnet in each of two zones, and the route tables between them:
import * as AWS from "@alchemy.run/aws";
export const AppNetwork = AWS.EC2.Network("AppNetwork", { cidrBlock: "10.42.0.0/16" });The subnet ranges are worked out from the VPC’s. Pass vpcId, publicSubnetIds and privateSubnetIds from the result to whatever runs in it:
const network = yield* AppNetwork;
yield* AWS.ECS.Service("Api", { cluster, task: apiTask, vpcId: network.vpcId, subnets: network.publicSubnetIds, assignPublicIp: true, loadBalancer: true,});The tasks and their load balancer share these subnets. Public subnets with public addresses let the tasks pull their image without a NAT gateway. To keep the tasks private, add NAT, below, and put them in privateSubnetIds.
Let private subnets reach out
Section titled “Let private subnets reach out”Private subnets are isolated by default. Add NAT when what runs in them needs the internet:
export const AppNetwork = AWS.EC2.Network("AppNetwork", { cidrBlock: "10.42.0.0/16", nat: "single", gatewayEndpoints: ["s3", "dynamodb"],});"single" shares one NAT gateway between the zones. "per-az" puts one in each, so losing a zone doesn’t cut the others off, at the cost of a gateway each. NAT gateways are billed by the hour and by the gigabyte.
Gateway endpoints route traffic for S3 and DynamoDB over AWS’s network instead, for free. With them, a private subnet reaches its Buckets and Tables without NAT at all.
Build it by hand
Section titled “Build it by hand”Network returns every resource it creates, so you can keep building on it. When you need a different layout, declare the pieces yourself. A VPC and its internet gateway:
const vpc = yield* AWS.EC2.Vpc("AppVpc", { cidrBlock: "10.80.0.0/16", enableDnsSupport: true, enableDnsHostnames: true,});const igw = yield* AWS.EC2.InternetGateway("AppIgw", { vpcId: vpc.vpcId });A subnet in a zone:
const publicA = yield* AWS.EC2.Subnet("PublicA", { vpcId: vpc.vpcId, cidrBlock: "10.80.1.0/24", availabilityZone: "us-east-1a", mapPublicIpOnLaunch: true,});A route table that makes it public:
const publicRoutes = yield* AWS.EC2.RouteTable("PublicRoutes", { vpcId: vpc.vpcId });
yield* AWS.EC2.Route("PublicInternet", { routeTableId: publicRoutes.routeTableId, destinationCidrBlock: "0.0.0.0/0", gatewayId: igw.internetGatewayId,});
yield* AWS.EC2.RouteTableAssociation("PublicA", { routeTableId: publicRoutes.routeTableId, subnetId: publicA.subnetId,});And a NAT gateway for the private subnets, with a static address, in a public subnet:
const eip = yield* AWS.EC2.EIP("NatEip", { domain: "vpc" });const nat = yield* AWS.EC2.NatGateway("Nat", { subnetId: publicA.subnetId, allocationId: eip.allocationId, connectivityType: "public",});
yield* AWS.EC2.Route("PrivateInternet", { routeTableId: privateRoutes.routeTableId, destinationCidrBlock: "0.0.0.0/0", natGatewayId: nat.natGatewayId,});Network ACLs, peering, interface endpoints and IPv6 gateways are in the EC2 reference.