Skip to content

AWS.EC2 reference

Source: src/AWS/EC2/AuthorizeSecurityGroupIngress.ts

Runtime binding for the AuthorizeSecurityGroupIngress operation scoped to the bound SecurityGroup (IAM action ec2:AuthorizeSecurityGroupIngress on the security group ARN).

Adds an inbound rule to the group at runtime — the classic dynamic IP-allowlisting Lambda that opens a port for an operator’s current address. Pair with RevokeSecurityGroupIngress to remove the rule afterwards. Provide the implementation with Effect.provide(AWS.EC2.AuthorizeSecurityGroupIngressHttp).

AuthorizeSecurityGroupIngress: Dynamic Security Group Rules

Section titled “AuthorizeSecurityGroupIngress: Dynamic Security Group Rules”
// init — bind the operation to the security group
const authorizeIngress = yield* AWS.EC2.AuthorizeSecurityGroupIngress(group);
// runtime — open port 22 for the caller's address
yield* authorizeIngress({
IpProtocol: "tcp",
FromPort: 22,
ToPort: 22,
CidrIp: "203.0.113.7/32",
});

Source: src/AWS/EC2/ClientVpnAuthorizationRule.ts

Authorizes Client VPN clients to access a destination network. Authorization does not create a route: configure a target association and any required routes separately. Specify either accessGroupId or authorizeAllGroups: true.

Every property is immutable, including description. Description-only changes revoke the old rule before creating its replacement. This resource manages the declared endpoint/CIDR/group identity, including an existing rule discovered without cached state. Rules have no independent ownership markers. Revoking an all-groups rule leaves group-specific rules untouched.

Readiness waits default to 30 minutes. Set AWS_CLIENT_VPN_TIMEOUT to a positive finite duration, such as 45 minutes, to override this deadline.

ClientVpnAuthorizationRule: Authorizing All Clients

Section titled “ClientVpnAuthorizationRule: Authorizing All Clients”
const rule = yield* AWS.EC2.ClientVpnAuthorizationRule("VpnVpcAccess", {
clientVpnEndpointId: endpoint.clientVpnEndpointId,
targetNetworkCidr: "10.0.0.0/16",
authorizeAllGroups: true,
description: "Access to the VPC",
});

ClientVpnAuthorizationRule: Authorizing One Group

Section titled “ClientVpnAuthorizationRule: Authorizing One Group”
const rule = yield* AWS.EC2.ClientVpnAuthorizationRule("VpnAdminAccess", {
clientVpnEndpointId: endpoint.clientVpnEndpointId,
targetNetworkCidr: "10.0.1.0/24",
accessGroupId: "S-1-5-21-123456789-123456789-123456789-1234",
});

Source: src/AWS/EC2/ClientVpnEndpoint.ts

An AWS Client VPN endpoint for authenticated remote access to a VPC. Create target network associations, authorization rules, and routes separately. An endpoint is usable only after a target network has finished associating. Client address range, authentication, transport, address families, and VPC changes replace the endpoint. Other settings update in place; omitted optional settings restore the documented defaults rather than leaving drift unmanaged.

Readiness waits default to 30 minutes. Set AWS_CLIENT_VPN_TIMEOUT to a positive finite duration, such as 45 minutes, to override this deadline.

const endpoint = yield* AWS.EC2.ClientVpnEndpoint("Vpn", {
clientCidrBlock: "172.20.0.0/22",
serverCertificateArn: certificate.certificateArn,
authenticationOptions: [{
type: "certificate-authentication",
mutualAuthentication: {
clientRootCertificateChainArn: certificate.certificateArn,
},
}],
splitTunnel: true,
vpcId: vpc.vpcId,
});
const endpoint = yield* AWS.EC2.ClientVpnEndpoint("LoggedVpn", {
clientCidrBlock: "172.20.0.0/22",
serverCertificateArn: certificate.certificateArn,
authenticationOptions: [{
type: "directory-service-authentication",
activeDirectory: { directoryId: "d-0123456789" },
}],
connectionLogOptions: {
enabled: true,
cloudwatchLogGroup: logGroup.logGroupName,
},
});

Source: src/AWS/EC2/ClientVpnRoute.ts

Adds a destination route through a Client VPN target subnet. The target must already be associated; reference the association’s subnetId output, not the subnet resource directly, to preserve deployment and teardown ordering. Authorization rules are configured separately and are also required for access.

All property changes require replacement. Description-only changes delete the old route first because AWS has no modify-route API. AWS-created local routes cannot be managed or deleted with this resource. This resource manages the declared endpoint/destination/subnet identity, including an existing manual route discovered without cached state. Routes have no independent ownership markers.

Readiness waits default to 30 minutes. Set AWS_CLIENT_VPN_TIMEOUT to a positive finite duration, such as 45 minutes, to override this deadline.

const target = yield* AWS.EC2.ClientVpnTargetNetworkAssociation("VpnTarget", {
clientVpnEndpointId: endpoint.clientVpnEndpointId,
subnetId: publicSubnet.subnetId,
});
const route = yield* AWS.EC2.ClientVpnRoute("VpnInternetRoute", {
clientVpnEndpointId: target.clientVpnEndpointId,
targetVpcSubnetId: target.subnetId,
destinationCidrBlock: "0.0.0.0/0",
description: "Internet egress",
});

The subnet’s VPC routing must provide the required egress path. For multiple target associations, add the same destination through each target subnet so clients have consistent access regardless of which association serves them.

Source: src/AWS/EC2/ClientVpnTargetNetworkAssociation.ts

Associates a VPC subnet with a Client VPN endpoint and waits for it to become associated. AWS automatically adds the VPC’s local route; do not manage that automatic route with ClientVpnRoute. Security groups are endpoint-wide settings. Association and disassociation can take several minutes. The provider waits for AWS to finish, fails on association errors, and supports cancellation.

All property changes replace the association. Replacement within the same endpoint deletes first because AWS permits only one subnet per Availability Zone. Removing the last association disconnects clients. This resource manages the declared endpoint/subnet pair, including an existing association discovered without cached state. Associations have no independent ownership markers.

Readiness waits default to 30 minutes. Set AWS_CLIENT_VPN_TIMEOUT to a positive finite duration, such as 45 minutes, to override this deadline.

ClientVpnTargetNetworkAssociation: Associating a Target Network

Section titled “ClientVpnTargetNetworkAssociation: Associating a Target Network”
const target = yield* AWS.EC2.ClientVpnTargetNetworkAssociation("VpnTarget", {
clientVpnEndpointId: endpoint.clientVpnEndpointId,
subnetId: privateSubnet.subnetId,
});
const route = yield* AWS.EC2.ClientVpnRoute("VpnInternet", {
clientVpnEndpointId: target.clientVpnEndpointId,
targetVpcSubnetId: target.subnetId,
destinationCidrBlock: "0.0.0.0/0",
});

Source: src/AWS/EC2/CreateSnapshot.ts

Runtime binding for the CreateSnapshot operation scoped to the bound Volume (IAM actions ec2:CreateSnapshot + ec2:CreateTags on the volume ARN and the region-wide snapshot wildcard — snapshot creation authorizes against both the source volume and the new snapshot).

Creates a point-in-time EBS snapshot of the volume — e.g. a Lambda that takes an application-consistent backup before a risky migration. The snapshot is created pending and completes asynchronously. Provide the implementation with Effect.provide(AWS.EC2.CreateSnapshotHttp).

// init — bind the operation to the volume
const createSnapshot = yield* AWS.EC2.CreateSnapshot(volume);
// runtime — take a point-in-time backup
const snapshot = yield* createSnapshot({
Description: "pre-migration backup",
});
console.log(snapshot.SnapshotId, snapshot.State);

Source: src/AWS/EC2/DefaultSecurityGroup.ts

Declaratively manages the rules of the AWS-created default security group in one VPC. AWS creates this group named default whenever it creates a VPC; Alchemy looks it up by VPC ID and name, never creates it, and never deletes it.

Inline rules use the same defaults as SecurityGroup: omitted or undefined ingress means no inline inbound rules, and omitted or undefined egress means IPv4 allow-all outbound. Passing [] means no inline rules in that direction, not unmanaged rules. Defaults apply on initial management, property removal, adoption, and drift repair; AWS’s initial self-ingress rule is removed, not restored.

Current standalone SecurityGroupRule declarations in the same stack and stage retain ownership of their persisted physical rule IDs. Other rules are removed even if they carry Alchemy tags. This resource manages rules, not ownership of the AWS-created group; do not also manage this group with a SecurityGroup resource or a second DefaultSecurityGroup manager.

Removing this Alchemy resource leaves both the default group and its last applied rules unchanged. Deleting its VPC removes the group as part of AWS’s VPC lifecycle.

DefaultSecurityGroup: Default Inline Rules

Section titled “DefaultSecurityGroup: Default Inline Rules”

Remove AWS self-ingress and allow outbound IPv4

const vpc = yield* AWS.EC2.Vpc("Vpc", { cidrBlock: "10.0.0.0/16" });
const group = yield* AWS.EC2.DefaultSecurityGroup("DefaultSecurityGroup", {
vpcId: vpc.vpcId,
});

Explicit undefined has the same meaning as omission

yield* AWS.EC2.DefaultSecurityGroup("DefaultSecurityGroup", {
vpcId: vpc.vpcId,
ingress: undefined,
egress: undefined,
});

DefaultSecurityGroup: Closing the Default Security Group

Section titled “DefaultSecurityGroup: Closing the Default Security Group”
const vpc = yield* AWS.EC2.Vpc("Vpc", { cidrBlock: "10.0.0.0/16" });
yield* AWS.EC2.DefaultSecurityGroup("DefaultSecurityGroup", {
vpcId: vpc.vpcId,
ingress: [],
egress: [],
});
yield* AWS.EC2.DefaultSecurityGroup("DefaultSecurityGroup", {
vpcId: vpc.vpcId,
ingress: [{
ipProtocol: "tcp",
fromPort: 443,
toPort: 443,
cidrIpv4: "10.0.0.0/16",
description: "Internal HTTPS",
}],
egress: [],
});

Unchanged deployments repair missing rules and remove undeclared rules. Updates leave unrelated rule IDs untouched; description changes update in place, and removing a description clears it. Equivalent protocol names, canonical CIDRs, rule order, and duplicate rules do not cause rule churn. Duplicates with conflicting descriptions are rejected before any writes. A rule with several source fields expands into one AWS rule per source.

DefaultSecurityGroup: Restoring Default Inline Rules

Section titled “DefaultSecurityGroup: Restoring Default Inline Rules”
yield* AWS.EC2.DefaultSecurityGroup("DefaultSecurityGroup", {
vpcId: vpc.vpcId,
ingress: [],
egress: [],
});

This still denies inline inbound access but restores IPv4 allow-all outbound. Removing these properties is different from removing the manager itself: deleting the manager leaves its last-applied rules, with no baseline restore.

DefaultSecurityGroup: Composing Standalone Rules

Section titled “DefaultSecurityGroup: Composing Standalone Rules”
const group = yield* AWS.EC2.DefaultSecurityGroup("DefaultSecurityGroup", {
vpcId: vpc.vpcId,
ingress: [],
egress: [],
});
yield* AWS.EC2.SecurityGroupRule("HttpsEgress", {
group: group,
type: "egress",
ipProtocol: "tcp",
fromPort: 443,
toPort: 443,
cidrIpv4: "10.0.0.0/16",
});

Pass the whole manager as group so rule creation and updates wait for its inline reconciliation. Only group.groupId is consumed by the rule provider; unrelated manager attributes do not trigger rule updates. The stable ID is still available for replacement planning. The ID-only groupId form cannot enforce this ordering when the manager updates without changing its ID. Standalone ingress composes the same way. Omitting egress instead would retain the default allow-all rule alongside this standalone rule. A current declaration and its persisted physical ID establish ownership, not cloud tags. Removing a declaration ends that protection; its provider handles deletion. Cross-stack or cross-stage rule ownership is unsupported. Inline and standalone rules must have distinct identities. If a standalone rule owns IPv4 allow-all egress, set egress: [] to disable the inline default.

yield* AWS.EC2.DefaultSecurityGroup("DefaultSecurityGroup", {
vpcId: otherVpc.vpcId,
ingress: [],
egress: [],
});

Changing vpcId replaces the Alchemy resource, not either AWS-owned group, including when the destination VPC is created or replaced in the same deploy. The previous group’s last-applied rules remain unchanged. Keep the old VPC declared until replacement finishes, then remove it separately if desired.

Source: src/AWS/EC2/DescribeInstance.ts

Runtime binding for the DescribeInstances operation scoped to the bound Instance (IAM action ec2:DescribeInstances; Describe* actions do not support resource-level permissions, so the grant is on *).

Returns the bound instance’s live description — state, addresses, block device mappings — e.g. a Lambda that reports whether a dev box is running. Provide the implementation with Effect.provide(AWS.EC2.DescribeInstanceHttp).

// init — bind the operation to the instance
const describeInstance = yield* AWS.EC2.DescribeInstance(instance);
// runtime — read the live description
const live = yield* describeInstance();
console.log(live?.State?.Name, live?.PrivateIpAddress);

Source: src/AWS/EC2/DescribeInstanceStatus.ts

Runtime binding for the DescribeInstanceStatus operation scoped to the bound Instance (IAM action ec2:DescribeInstanceStatus; Describe* actions do not support resource-level permissions, so the grant is on *).

Reads the instance’s system/instance status checks and scheduled events — e.g. a Lambda that alerts on failed reachability checks. Pass IncludeAllInstances: true to also see the status while the instance is stopped. Provide the implementation with Effect.provide(AWS.EC2.DescribeInstanceStatusHttp).

DescribeInstanceStatus: Observing Instances

Section titled “DescribeInstanceStatus: Observing Instances”
// init — bind the operation to the instance
const describeStatus = yield* AWS.EC2.DescribeInstanceStatus(instance);
// runtime — read status checks (even while stopped)
const result = yield* describeStatus({ IncludeAllInstances: true });
console.log(result.InstanceStatuses?.[0]?.InstanceStatus?.Status);

Source: src/AWS/EC2/DhcpOptions.ts

A DHCP options set configures the DHCP parameters (domain name, DNS servers, NTP servers, NetBIOS settings) that a VPC hands out to the instances launched inside it. Attach a custom set to a VPC to override the AWS defaults — for example to point instances at your own DNS or an internal search domain.

A DHCP options set is immutable: AWS provides no edit API, so changing any DHCP parameter replaces the set. Setting vpcId associates the set with a VPC; clearing it (or deleting the resource) re-associates the VPC with the account’s default options set, since a set must be disassociated from every VPC before it can be deleted.

Custom DNS and Search Domain

const dhcp = yield* AWS.EC2.DhcpOptions("CorpDhcp", {
domainName: "corp.internal",
domainNameServers: ["10.0.0.2", "AmazonProvidedDNS"],
vpcId: myVpc.vpcId,
});

Creates the options set and associates it with the VPC in one step. Instances launched into the VPC receive the corp.internal search domain and the listed DNS servers.

NTP and NetBIOS Configuration

const dhcp = yield* AWS.EC2.DhcpOptions("Dhcp", {
ntpServers: ["169.254.169.123"],
netbiosNameServers: ["10.0.0.5"],
netbiosNodeType: "2",
});

Creates an unassociated options set that you can associate later by setting vpcId.

Source: src/AWS/EC2/EgressOnlyInternetGateway.ts

An egress-only internet gateway is the IPv6 counterpart to a NAT gateway: it lets instances in a VPC initiate outbound IPv6 traffic to the internet while preventing the internet from initiating inbound connections to them. Use it to give private, IPv6-addressed resources outbound-only internet access.

Unlike a NAT gateway it is free, has no bandwidth charges, and does not require an Elastic IP — but it works for IPv6 only. It always belongs to a VPC (vpcId is required); the gateway must be paired with an IPv6 Route to actually carry traffic.

EgressOnlyInternetGateway: Creating an Egress-Only Internet Gateway

Section titled “EgressOnlyInternetGateway: Creating an Egress-Only Internet Gateway”

The gateway is created and attached to vpcId in a single step. Because the attachment is intrinsic, changing vpcId replaces the gateway rather than moving it.

Basic Egress-Only Internet Gateway

const egressOnlyIgw = yield* AWS.EC2.EgressOnlyInternetGateway("EgressOnlyIgw", {
vpcId: myVpc.vpcId,
});

Creates the gateway in the VPC. The resulting egressOnlyInternetGatewayId (prefixed eigw-) is referenced from a route’s egressOnlyInternetGatewayId target.

Egress-Only Internet Gateway with Tags

const egressOnlyIgw = yield* AWS.EC2.EgressOnlyInternetGateway("EgressOnlyIgw", {
vpcId: myVpc.vpcId,
tags: { Name: "production-eigw" },
});

The tags map is merged with the alchemy auto-tags and can be updated in place without replacing the gateway.

EgressOnlyInternetGateway: Routing IPv6 Egress Traffic

Section titled “EgressOnlyInternetGateway: Routing IPv6 Egress Traffic”

A gateway alone does nothing until a private route table sends IPv6 traffic to it. Pair it with a ::/0 Route so private, IPv6-addressed instances can reach the internet outbound-only.

const egressOnlyIgw = yield* AWS.EC2.EgressOnlyInternetGateway("EgressOnlyIgw", {
vpcId: myVpc.vpcId,
});
const ipv6EgressRoute = yield* AWS.EC2.Route("Ipv6EgressRoute", {
routeTableId: privateRouteTable.routeTableId,
destinationIpv6CidrBlock: "::/0",
egressOnlyInternetGatewayId: egressOnlyIgw.egressOnlyInternetGatewayId,
});

Instances in subnets associated with privateRouteTable can now make outbound IPv6 connections (updates, API calls) while remaining unreachable from the public internet.

Source: src/AWS/EC2/EIP.ts

An Elastic IP address — a static, public IPv4 address allocated to your AWS account that you can attach to instances, network interfaces, or NAT gateways.

Allocating an EIP reserves the address; you then reference its allocationId from the resource that should use it (for example a public NatGateway). The address is released back to AWS when the resource is destroyed. The pool-related properties (publicIpv4Pool, networkBorderGroup, customerOwnedIpv4Pool) are immutable and replace the address when changed.

By default an Elastic IP is allocated for use within a VPC (domain: "vpc"), which is the only domain available to modern accounts.

const eip = yield* AWS.EC2.EIP("MyEip", {
domain: "vpc",
tags: { Name: "app-eip" },
});

This reserves a standard, Amazon-owned public IPv4 address scoped to your VPC; domain defaults to "vpc", so it can be omitted, and tags help you find the address in the console and on the bill.

If you have onboarded an address range to AWS (BYOIP) or use Outposts, you can draw the address from a specific pool instead of Amazon’s general pool.

Allocate from a Public IPv4 (BYOIP) Pool

const eip = yield* AWS.EC2.EIP("ByoipEip", {
publicIpv4Pool: "ipv4pool-ec2-0abcdef1234567890",
networkBorderGroup: "us-east-1",
});

publicIpv4Pool selects an address from a pool you own rather than a random Amazon address, and networkBorderGroup restricts which zone group AWS advertises it from (useful for Local and Wavelength Zones).

Allocate from a Customer-Owned Pool (Outposts)

const eip = yield* AWS.EC2.EIP("CoIpEip", {
customerOwnedIpv4Pool: "ipv4pool-coip-0abcdef1234567890",
});

customerOwnedIpv4Pool pulls a customer-owned IP (CoIP) from an Outposts-associated pool, for workloads that must use your own on-premises address space.

const eip = yield* AWS.EC2.EIP("NatEip", {});
const natGateway = yield* AWS.EC2.NatGateway("NatGateway", {
subnetId: publicSubnet.subnetId,
allocationId: eip.allocationId,
});

Downstream resources consume the reserved address through its allocationId; here the EIP becomes the fixed public IP of a NAT gateway.

Source: src/AWS/EC2/FlowLog.ts

A flow log captures information about the IP traffic going to and from a VPC, subnet, or network interface, and publishes it to CloudWatch Logs, S3, or a Kinesis Data Firehose delivery stream. Use it for network monitoring, traffic analysis, and troubleshooting security-group / NACL rules.

A flow log is immutable: every property except tags is fixed at creation, so changing the monitored resource, destination, or traffic type replaces the flow log. For CloudWatch Logs delivery you must supply a logGroupName and a deliverLogsPermissionArn — an IAM role that EC2’s vpc-flow-logs service principal can assume to write to the group.

VPC Flow Log to CloudWatch Logs

const logGroup = yield* AWS.Logs.LogGroup("FlowLogs", {});
const role = yield* AWS.IAM.Role("FlowLogRole", {
assumeRolePolicyDocument: {
Version: "2012-10-17",
Statement: [{
Effect: "Allow",
Principal: { Service: "vpc-flow-logs.amazonaws.com" },
Action: "sts:AssumeRole",
}],
},
inlinePolicies: {
deliver: {
Version: "2012-10-17",
Statement: [{
Effect: "Allow",
Action: [
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams",
],
Resource: "*",
}],
},
},
});
const flowLog = yield* AWS.EC2.FlowLog("VpcFlowLog", {
resourceType: "VPC",
resourceId: myVpc.vpcId,
logGroupName: logGroup.logGroupName,
deliverLogsPermissionArn: role.roleArn,
});

Captures all traffic for the VPC and delivers it to the CloudWatch Logs group via the delivery role.

S3 Flow Log for Rejected Traffic

const flowLog = yield* AWS.EC2.FlowLog("RejectedTraffic", {
resourceType: "Subnet",
resourceId: mySubnet.subnetId,
trafficType: "REJECT",
logDestinationType: "s3",
logDestination: bucket.bucketArn,
});

Delivers only rejected-traffic records for a subnet directly to an S3 bucket (no IAM role required for S3 delivery).

Source: src/AWS/EC2/GetAmi.ts

The DescribeImages-backed AMI lookup (IAM action ec2:DescribeImages; Describe* actions do not support resource-level permissions, so the runtime grant is on *). Returns the newest available image matching the filters, or undefined when nothing matches.

As a data source, invoke it at plan time via getAmi — the result is an Output resolved during plan/deploy and inert inside deployed bundles. As a runtime binding, bind it inside a Function to look images up at runtime with the IAM grant attached automatically. Provide the implementation with Effect.provide(AWS.EC2.GetAmiHttp) (already registered by AWS.providers() for plan-time use).

Plan-time lookup (data source)

const instance = yield* AWS.EC2.Instance("web", {
imageId: AWS.EC2.getAmi({
owners: ["amazon"],
name: ["al2023-ami-2023.*"],
}).ImageId.as<string>(),
instanceType: "t3.micro",
subnetId: subnet.subnetId,
});

Runtime lookup inside a Function

// init — bind the operation
const getAmi = yield* AWS.EC2.GetAmi({
owners: ["amazon"],
name: ["al2023-ami-2023.*"],
});
// runtime — read the newest matching image
const latest = yield* getAmi();
console.log(latest?.ImageId, latest?.CreationDate);

Source: src/AWS/EC2/GetConsoleOutput.ts

Runtime binding for the GetConsoleOutput operation scoped to the bound Instance (IAM action ec2:GetConsoleOutput on the instance ARN).

Fetches the instance’s serial console output (base64-encoded) — e.g. a diagnostics Lambda that captures boot logs when a host fails its health checks. Pass Latest: true for the most recent output on supported instance types. Provide the implementation with Effect.provide(AWS.EC2.GetConsoleOutputHttp).

// init — bind the operation to the instance
const getConsoleOutput = yield* AWS.EC2.GetConsoleOutput(instance);
// runtime — fetch the (base64-encoded) boot log
const result = yield* getConsoleOutput({ Latest: true });
const log = Buffer.from(result.Output ?? "", "base64").toString("utf8");

Source: src/AWS/EC2/GetPasswordData.ts

Runtime binding for the GetPasswordData operation scoped to the bound Instance (IAM action ec2:GetPasswordData on the instance ARN).

Retrieves the encrypted Windows administrator password for the instance. The PasswordData field is sensitive and surfaces as Redacted.Redacted<string> — decrypt it with the launch key pair’s private key. Linux instances return an empty value. Provide the implementation with Effect.provide(AWS.EC2.GetPasswordDataHttp).

// init — bind the operation to the instance
const getPasswordData = yield* AWS.EC2.GetPasswordData(instance);
// runtime — the ciphertext is Redacted; unwrap explicitly to decrypt
const result = yield* getPasswordData();
const ciphertext = result.PasswordData; // Redacted<string>

Source: src/AWS/EC2/Instance.ts

An EC2 instance that can either act as a low-level compute primitive or run a bundled long-lived Effect program directly on the machine.

const instance = yield* AWS.EC2.Instance("AppInstance", {
imageId: AWS.EC2.amazonLinux2023(),
instanceType: "t3.micro",
subnetId: subnet.subnetId,
});
const api = yield* Effect.gen(function* () {
yield* Http.serve(
HttpServerResponse.json({ ok: true }),
);
return {
main: import.meta.url,
imageId: AWS.EC2.amazonLinux2023(),
instanceType: "t3.small",
subnetId: subnet.subnetId,
securityGroupIds: [securityGroup.groupId],
associatePublicIpAddress: true,
port: 3000,
};
}).pipe(
Effect.provide(AWS.EC2.HttpServer),
AWS.EC2.Instance("ApiInstance"),
);

main is bundled with rolldown at deploy time. Unused code is tree-shaken. effect, alchemy, and @distilled.cloud are marked pure so unused parts prune more aggressively. Your app is not marked pure.

Mark additional packages as pure

Only list packages with no top-level side effects.

{
main: import.meta.url,
build: {
pure: { packages: ["my-lib", "@my-scope/*"] },
},
}

Turn it off

{
main: import.meta.url,
build: { pure: false },
}

Source: src/AWS/EC2/InternetGateway.ts

An internet gateway provides a target for internet-routable traffic in a VPC, enabling bidirectional IPv4 and IPv6 connectivity between resources in your VPC and the public internet. A VPC can have at most one internet gateway attached at a time.

The only inputs are the optional vpcId to attach to and tags. Attaching a gateway is not enough on its own to make a subnet public — you also need a 0.0.0.0/0 Route pointing at the gateway and a RouteTableAssociation binding the subnet to that route table.

InternetGateway: Creating an Internet Gateway

Section titled “InternetGateway: Creating an Internet Gateway”

Pass vpcId to create and attach the gateway in one step, or omit it to create a standalone gateway and attach it later by setting the prop. Updating vpcId moves the gateway between VPCs (detach then attach) without recreating it.

Internet Gateway Attached to a VPC

const internetGateway = yield* AWS.EC2.InternetGateway("InternetGateway", {
vpcId: myVpc.vpcId,
});

Creates the gateway and attaches it to the VPC immediately. The resulting internetGatewayId (prefixed igw-) is what you reference from a route’s gatewayId.

Detached Internet Gateway

const internetGateway = yield* AWS.EC2.InternetGateway("InternetGateway", {});

Omitting vpcId creates an unattached gateway. This is occasionally useful when the VPC is provisioned separately; add the vpcId prop later to attach it.

Internet Gateway with Tags

const internetGateway = yield* AWS.EC2.InternetGateway("InternetGateway", {
vpcId: myVpc.vpcId,
tags: { Name: "production-igw" },
});

The tags map is merged with the alchemy auto-tags and can be changed in place. A Name tag makes the gateway easy to identify in the AWS console.

InternetGateway: Enabling Public Internet Access

Section titled “InternetGateway: Enabling Public Internet Access”

An internet gateway only carries traffic once a route table sends traffic to it and a subnet is associated with that table. The full pattern below makes a subnet public.

const internetGateway = yield* AWS.EC2.InternetGateway("InternetGateway", {
vpcId: myVpc.vpcId,
});
const publicRouteTable = yield* AWS.EC2.RouteTable("PublicRouteTable", {
vpcId: myVpc.vpcId,
});
const internetRoute = yield* AWS.EC2.Route("InternetRoute", {
routeTableId: publicRouteTable.routeTableId,
destinationCidrBlock: "0.0.0.0/0",
gatewayId: internetGateway.internetGatewayId,
});

With the default route in place, any subnet associated with publicRouteTable can send and receive internet traffic. Add an analogous route with destinationIpv6CidrBlock: "::/0" to enable IPv6.

Source: src/AWS/EC2/KeyPair.ts

An EC2 key pair used to grant SSH access to instances launched with its keyName.

By default Alchemy asks EC2 to generate the key pair and captures the private key (returned only once, at create time) as a secret in state. Pass KeyPairProps.publicKeyMaterial to import your own public key instead, in which case no private key is stored.

Generated key pair

const keyPair = yield* AWS.EC2.KeyPair("DeployKey", {
keyType: "ed25519",
});
// keyPair.keyName -> pass to AWS.EC2.Instance({ keyName })
// keyPair.privateKey -> Redacted<string> (the PEM private key)

Imported public key

const keyPair = yield* AWS.EC2.KeyPair("ImportedKey", {
publicKeyMaterial: "ssh-ed25519 AAAAC3Nz... user@host",
});

Source: src/AWS/EC2/NatGateway.ts

A NAT gateway that lets instances in a private subnet reach the internet (and other AWS services) while preventing unsolicited inbound connections.

The gateway lives in the subnet given by subnetId, and its connectivityType decides how it connects: a "public" gateway must sit in a public subnet and requires an Elastic IP via allocationId, while a "private" gateway has no public address and is used for VPC-to-VPC routing. A NAT gateway only carries traffic once a Route sends 0.0.0.0/0 from the private subnet’s route table to it. Core properties (subnetId, connectivityType, allocationId) are immutable, so changing them replaces the gateway.

Public gateways translate private addresses to a stable public IP, so they must be placed in a public subnet (one with a route to an internet gateway) and given an Elastic IP allocation.

const eip = yield* AWS.EC2.EIP("NatEip", {});
const natGateway = yield* AWS.EC2.NatGateway("NatGateway", {
subnetId: publicSubnet.subnetId,
allocationId: eip.allocationId,
connectivityType: "public",
tags: { Name: "production-nat" },
});

Allocating the EIP first and passing its allocationId gives the gateway a fixed public IP. connectivityType defaults to "public", so it can be omitted; this is the standard way to give private instances outbound internet access.

Private gateways have no public IP and route traffic between VPCs or to on-premises networks without exposing it to the internet.

Private NAT Gateway with a Fixed Private IP

const natGateway = yield* AWS.EC2.NatGateway("PrivateNat", {
subnetId: privateSubnet.subnetId,
connectivityType: "private",
privateIpAddress: "10.0.10.10",
});

Omitting allocationId and setting connectivityType: "private" creates a gateway with no public address; privateIpAddress pins it to a specific address in the subnet instead of letting AWS choose one automatically.

Private NAT Gateway with Secondary Addresses

const natGateway = yield* AWS.EC2.NatGateway("ScaledNat", {
subnetId: privateSubnet.subnetId,
connectivityType: "private",
secondaryPrivateIpAddressCount: 3,
});

Secondary private addresses — via secondaryPrivateIpAddressCount, secondaryPrivateIpAddresses, or secondaryAllocationIds — raise the number of simultaneous connections a private gateway can sustain to busy destinations, which is only valid for private gateways.

const natRoute = yield* AWS.EC2.Route("NatRoute", {
routeTableId: privateRouteTable.routeTableId,
destinationCidrBlock: "0.0.0.0/0",
natGatewayId: natGateway.natGatewayId,
});

Without a route the gateway is inert; this entry sends all outbound traffic from the private subnet’s route table through the gateway so private instances can reach the internet.

Source: src/AWS/EC2/Network.ts

Creates a production-shaped VPC network from the low-level EC2 primitives.

Network is the ergonomic entry point for users who want a ready-to-use VPC layout without manually creating route tables, internet gateways, NAT gateways, and subnet associations by hand.

The helper still returns the underlying canonical resources so callers can keep composing with raw AWS.EC2.* APIs when they need more control.

Minimal network

const network = yield* AWS.EC2.Network("AppNetwork", {
cidrBlock: "10.42.0.0/16",
});

ECS-ready network with shared NAT

const network = yield* AWS.EC2.Network("AppNetwork", {
cidrBlock: "10.42.0.0/16",
availabilityZones: 2,
nat: "single",
gatewayEndpoints: ["s3"],
});
yield* AWS.ECS.Service("ApiService", {
cluster,
task: apiTask,
vpcId: network.vpcId,
subnets: network.publicSubnetIds,
assignPublicIp: true,
});

Source: src/AWS/EC2/NetworkAcl.ts

A network ACL — a stateless firewall that controls inbound and outbound traffic at the subnet level, evaluated as an ordered list of numbered allow/deny rules.

Unlike security groups (which are stateful and attach to interfaces), a network ACL is associated with subnets and evaluates return traffic independently, so you typically pair each inbound rule with a matching ephemeral-port outbound rule. The ACL itself only takes vpcId and tags; the actual rules live in NetworkAclEntry resources and subnet attachments in NetworkAclAssociation resources. Changing vpcId replaces the ACL.

const acl = yield* AWS.EC2.NetworkAcl("PrivateNetworkAcl", {
vpcId: vpc.vpcId,
tags: { Name: "private-nacl" },
});

This creates an empty custom ACL in the VPC — it starts with only the implicit default-deny rules, so until you add entries it blocks all traffic on any subnet you associate with it.

NetworkAcl: Composing Rules and Associations

Section titled “NetworkAcl: Composing Rules and Associations”

A network ACL is only useful once you attach rules and point subnets at it. The typical pattern is one NetworkAcl, several NetworkAclEntry rules, and one NetworkAclAssociation per subnet.

const acl = yield* AWS.EC2.NetworkAcl("PrivateNetworkAcl", {
vpcId: vpc.vpcId,
});
const allowVpc = yield* AWS.EC2.NetworkAclEntry("AllowVpc", {
networkAclId: acl.networkAclId,
ruleNumber: 100,
protocol: "-1",
ruleAction: "allow",
egress: false,
cidrBlock: "10.0.0.0/16",
});
const association = yield* AWS.EC2.NetworkAclAssociation("SubnetAssoc", {
networkAclId: acl.networkAclId,
subnetId: privateSubnet.subnetId,
});

The entry allows all traffic from within the VPC CIDR and the association makes the subnet use this ACL instead of the VPC default. Build up the full rule set by adding more NetworkAclEntry resources with increasing ruleNumbers.

Source: src/AWS/EC2/NetworkAclAssociation.ts

Associates a subnet with a NetworkAcl, replacing whichever ACL the subnet currently uses (every subnet is always associated with exactly one network ACL — the VPC’s default until you point it at a custom one).

Changing subnetId replaces the association, while changing only networkAclId re-points the same subnet at a different ACL in place. On delete, the subnet is reverted to the VPC’s default network ACL so it is never left without one.

NetworkAclAssociation: Associating Subnets

Section titled “NetworkAclAssociation: Associating Subnets”

A subnet starts out attached to the VPC’s default ACL; this resource moves it onto a custom ACL so the rules you defined with NetworkAclEntry take effect for that subnet.

const association = yield* AWS.EC2.NetworkAclAssociation("PrivateSubnetNaclAssoc", {
networkAclId: privateNetworkAcl.networkAclId,
subnetId: privateSubnet.subnetId,
});

This detaches the subnet from the default ACL and attaches it to your custom ACL; destroying the association automatically reverts the subnet to the default ACL, which is the safe way to “remove” a custom ACL from a subnet.

Source: src/AWS/EC2/NetworkAclEntry.ts

A single rule in a NetworkAcl — a numbered, stateless allow/deny entry that matches traffic by protocol, CIDR (IPv4 or IPv6), and, for TCP/UDP, a port range.

Each entry is identified by its (networkAclId, ruleNumber, egress) triple, and changing any of those three replaces the entry. Rules are evaluated from the lowest ruleNumber upward and the first match wins, so leave gaps between numbers to make room for future rules. Because NACLs are stateless, always add a matching ephemeral-port rule for return traffic.

Inbound rules (egress: false) match traffic entering the subnet. A common pattern is to allow trusted source ranges plus the ephemeral ports needed for return traffic.

Allow Inbound Traffic from the VPC CIDR

const allowVpc = yield* AWS.EC2.NetworkAclEntry("AllowVpc", {
networkAclId: acl.networkAclId,
ruleNumber: 100,
protocol: "-1",
ruleAction: "allow",
egress: false,
cidrBlock: "10.0.0.0/16",
});

protocol: "-1" matches all protocols and cidrBlock scopes the rule to the VPC’s IPv4 range; the low ruleNumber (100) makes it take precedence over higher-numbered rules.

Allow Inbound Ephemeral Ports (NAT Return Traffic)

const allowEphemeral = yield* AWS.EC2.NetworkAclEntry("AllowEphemeral", {
networkAclId: acl.networkAclId,
ruleNumber: 200,
protocol: "6",
ruleAction: "allow",
egress: false,
cidrBlock: "0.0.0.0/0",
portRange: { from: 1024, to: 65535 },
});

Because the ACL is stateless, responses to outbound requests arrive on ephemeral ports and need their own inbound rule; protocol: "6" is TCP and portRange restricts the match to the ephemeral port range.

Deny a Specific IPv6 Range

const denyRange = yield* AWS.EC2.NetworkAclEntry("DenyBadActor", {
networkAclId: acl.networkAclId,
ruleNumber: 50,
protocol: "-1",
ruleAction: "deny",
egress: false,
ipv6CidrBlock: "2001:db8:1234::/48",
});

ruleAction: "deny" with a very low ruleNumber blocks an IPv6 range before any allow rule can match it; use ipv6CidrBlock instead of cidrBlock to target IPv6 traffic.

Outbound rules (egress: true) match traffic leaving the subnet and are numbered in their own sequence, independent of the inbound rules.

const allowEgress = yield* AWS.EC2.NetworkAclEntry("AllowEgress", {
networkAclId: acl.networkAclId,
ruleNumber: 100,
protocol: "-1",
ruleAction: "allow",
egress: true,
cidrBlock: "0.0.0.0/0",
});

Setting egress: true makes this an outbound rule; allowing all protocols to 0.0.0.0/0 is typical when you want the subnet to initiate connections freely.

const allowPing = yield* AWS.EC2.NetworkAclEntry("AllowPing", {
networkAclId: acl.networkAclId,
ruleNumber: 300,
protocol: "1",
ruleAction: "allow",
egress: false,
cidrBlock: "10.0.0.0/16",
icmpTypeCode: { type: 8, code: -1 },
});

ICMP (protocol: "1") has no ports, so icmpTypeCode selects the message type instead — type 8 is echo request and code: -1 matches all codes.

Source: src/AWS/EC2/NetworkInterface.ts

An Elastic Network Interface (ENI) — a virtual network card in a VPC subnet with its own private IPs, MAC address, and security groups. Attach one to an instance via a NetworkInterfaceAttachment for stable-IP and multi-homing patterns.

Changing subnetId or the primary privateIpAddress replaces the interface. description, securityGroupIds, and sourceDestCheck are applied in place.

NetworkInterface: Creating a Network Interface

Section titled “NetworkInterface: Creating a Network Interface”
const eni = yield* AWS.EC2.NetworkInterface("AppEni", {
subnetId: subnet.subnetId,
description: "stable IP for the app server",
securityGroupIds: [securityGroup.groupId],
});

The interface gets a private IP from the subnet’s range. Its IP survives instance replacement — detach it from a failed instance and attach it to a new one to keep the same address.

const eni = yield* AWS.EC2.NetworkInterface("FixedIpEni", {
subnetId: subnet.subnetId,
privateIpAddress: "10.0.1.50",
securityGroupIds: [securityGroup.groupId],
});

Pinning privateIpAddress gives the interface a predictable address — useful for appliances and services other resources reference by IP.

const eni = yield* AWS.EC2.NetworkInterface("NatEni", {
subnetId: subnet.subnetId,
sourceDestCheck: false,
securityGroupIds: [securityGroup.groupId],
});

Disable sourceDestCheck when the interface belongs to a NAT instance, firewall, or router that forwards packets not addressed to itself.

Source: src/AWS/EC2/NetworkInterfaceAttachment.ts

Attaches an NetworkInterface (ENI) to an EC2 Instance at a device index. The interface and instance must be in the same Availability Zone. On delete the interface is detached (force-detached as a fallback) before the resource is removed.

This is an existence-style resource — its identity is the networkInterfaceId/instanceId/deviceIndex triple. Changing any of them replaces the attachment.

NetworkInterfaceAttachment: Attaching a Network Interface

Section titled “NetworkInterfaceAttachment: Attaching a Network Interface”
const attachment = yield* AWS.EC2.NetworkInterfaceAttachment("SecondaryEni", {
networkInterfaceId: eni.networkInterfaceId,
instanceId: instance.instanceId,
deviceIndex: 1,
});

Device index 0 is the instance’s primary interface, so secondary interfaces use index 1 and up. The ENI’s IPs and security groups now apply to the instance on that interface.

Source: src/AWS/EC2/PrefixList.ts

A managed prefix list is a named, reusable set of CIDR blocks that you reference by ID from security group rules and route tables. Instead of duplicating the same IP ranges across many rules, you maintain them in one place and every rule that references the list picks up changes automatically.

The list is versioned: each entry modification bumps version, and AWS limits a list to maxEntries CIDRs (you provision headroom up front and can only grow it, never shrink it, in place). addressFamily fixes whether the list holds IPv4 or IPv6 CIDRs and is immutable.

Basic IPv4 Prefix List

const corpNetworks = yield* AWS.EC2.PrefixList("CorpNetworks", {
maxEntries: 10,
entries: [
{ cidr: "10.0.0.0/16", description: "vpc-a" },
{ cidr: "10.1.0.0/16", description: "vpc-b" },
],
});

Creates a prefix list with two IPv4 CIDRs. The resulting prefixListId (prefixed pl-) can be referenced from security group rules and routes.

IPv6 Prefix List

const ipv6List = yield* AWS.EC2.PrefixList("Ipv6List", {
addressFamily: "IPv6",
maxEntries: 5,
entries: [{ cidr: "2001:db8::/32" }],
});

addressFamily: "IPv6" makes the list accept IPv6 CIDRs. Because the family is intrinsic to the list, changing it later replaces the resource.

PrefixList: Referencing a Prefix List from a Security Group Rule

Section titled “PrefixList: Referencing a Prefix List from a Security Group Rule”
const corpNetworks = yield* AWS.EC2.PrefixList("CorpNetworks", {
maxEntries: 10,
entries: [{ cidr: "10.0.0.0/16" }],
});
const rule = yield* AWS.EC2.SecurityGroupRule("AllowCorp", {
groupId: sg.groupId,
type: "ingress",
ipProtocol: "tcp",
fromPort: 443,
toPort: 443,
prefixListId: corpNetworks.prefixListId,
});

The rule allows HTTPS from every CIDR in the list. Editing the list’s entries updates what the rule permits without touching the rule itself.

Source: src/AWS/EC2/RebootInstance.ts

Runtime binding for the RebootInstances operation scoped to the bound Instance (IAM action ec2:RebootInstances on the instance ARN).

Requests an asynchronous reboot of the instance — e.g. a remediation Lambda that bounces a wedged host after a failed health check. Provide the implementation with Effect.provide(AWS.EC2.RebootInstanceHttp).

RebootInstance: Instance Lifecycle Control

Section titled “RebootInstance: Instance Lifecycle Control”
// init — bind the operation to the instance
const rebootInstance = yield* AWS.EC2.RebootInstance(instance);
// runtime — request the reboot (asynchronous)
yield* rebootInstance();

Source: src/AWS/EC2/RevokeSecurityGroupIngress.ts

Runtime binding for the RevokeSecurityGroupIngress operation scoped to the bound SecurityGroup (IAM action ec2:RevokeSecurityGroupIngress on the security group ARN).

Removes an inbound rule from the group at runtime — the cleanup half of the dynamic IP-allowlisting pattern (see AuthorizeSecurityGroupIngress). Provide the implementation with Effect.provide(AWS.EC2.RevokeSecurityGroupIngressHttp).

RevokeSecurityGroupIngress: Dynamic Security Group Rules

Section titled “RevokeSecurityGroupIngress: Dynamic Security Group Rules”
// init — bind the operation to the security group
const revokeIngress = yield* AWS.EC2.RevokeSecurityGroupIngress(group);
// runtime — close port 22 for the address again
yield* revokeIngress({
IpProtocol: "tcp",
FromPort: 22,
ToPort: 22,
CidrIp: "203.0.113.7/32",
});

Source: src/AWS/EC2/Route.ts

A single route entry inside a RouteTable. A route maps a destination to exactly one target, telling the VPC where to send packets whose address falls within the destination range.

Every route has two halves:

  • Destination — exactly one of destinationCidrBlock (IPv4), destinationIpv6CidrBlock (IPv6), or destinationPrefixListId (a managed prefix list, e.g. for an AWS service).
  • Target — exactly one of gatewayId (internet/virtual private gateway), natGatewayId, instanceId (NAT instance), networkInterfaceId, vpcPeeringConnectionId, transitGatewayId, localGatewayId (Outposts), carrierGatewayId (Wavelength), egressOnlyInternetGatewayId (IPv6), coreNetworkArn (Cloud WAN), or vpcEndpointId (Gateway Load Balancer).

Changing the routeTableId or the destination replaces the route, whereas changing only the target is applied in place via ReplaceRoute.

Use an IPv4 destinationCidrBlock — most commonly 0.0.0.0/0 for the default route, or a narrower CIDR to route specific traffic.

Default Route to an Internet Gateway

const internetRoute = yield* AWS.EC2.Route("InternetRoute", {
routeTableId: publicRouteTable.routeTableId,
destinationCidrBlock: "0.0.0.0/0",
gatewayId: internetGateway.internetGatewayId,
});

Sends all outbound IPv4 traffic to the internet gateway, which is what makes a subnet “public”. Attach this route table to any subnet that needs inbound and outbound internet connectivity.

Default Route to a NAT Gateway

const natRoute = yield* AWS.EC2.Route("NatRoute", {
routeTableId: privateRouteTable.routeTableId,
destinationCidrBlock: "0.0.0.0/0",
natGatewayId: natGateway.natGatewayId,
});

Lets private subnets reach the internet for outbound traffic (package updates, API calls) while blocking unsolicited inbound connections. The NAT gateway itself lives in a public subnet.

Route to a VPC Peering Connection

const peeringRoute = yield* AWS.EC2.Route("PeeringRoute", {
routeTableId: routeTable.routeTableId,
destinationCidrBlock: "10.1.0.0/16",
vpcPeeringConnectionId: "pcx-0abc1234",
});

Routes traffic destined for the peer VPC’s CIDR across a VPC peering connection. Use a narrow destination matching the remote VPC rather than 0.0.0.0/0 so only cross-VPC traffic is affected.

Route to a Transit Gateway

const transitRoute = yield* AWS.EC2.Route("TransitRoute", {
routeTableId: routeTable.routeTableId,
destinationCidrBlock: "172.16.0.0/12",
transitGatewayId: "tgw-0abc1234",
});

Hands traffic to a transit gateway, the hub used to connect many VPCs and on-premises networks. The destination CIDR should cover the address space reachable through the transit gateway.

Route to a Network Interface or NAT Instance

const applianceRoute = yield* AWS.EC2.Route("ApplianceRoute", {
routeTableId: routeTable.routeTableId,
destinationCidrBlock: "0.0.0.0/0",
networkInterfaceId: "eni-0abc1234",
});

Forwards traffic to a specific elastic network interface — for example a firewall or NAT instance appliance. Use instanceId instead when targeting a NAT instance that has exactly one network interface attached.

IPv6 routes use destinationIpv6CidrBlock (e.g. ::/0 for the IPv6 default route). For outbound-only IPv6 access from private subnets, target an EgressOnlyInternetGateway.

IPv6 Egress Route to an Egress-Only Internet Gateway

const ipv6EgressRoute = yield* AWS.EC2.Route("Ipv6EgressRoute", {
routeTableId: privateRouteTable.routeTableId,
destinationIpv6CidrBlock: "::/0",
egressOnlyInternetGatewayId: egressOnlyIgw.egressOnlyInternetGatewayId,
});

Gives IPv6-addressed instances outbound internet access while blocking inbound connections — the IPv6 equivalent of routing IPv4 through a NAT gateway.

IPv6 Internet Route to an Internet Gateway

const ipv6InternetRoute = yield* AWS.EC2.Route("Ipv6InternetRoute", {
routeTableId: publicRouteTable.routeTableId,
destinationIpv6CidrBlock: "::/0",
gatewayId: internetGateway.internetGatewayId,
});

Provides full bidirectional IPv6 connectivity for a public subnet, since an internet gateway (unlike an egress-only gateway) allows inbound IPv6 traffic.

Route: Routing to AWS Services via Prefix Lists

Section titled “Route: Routing to AWS Services via Prefix Lists”

Instead of a raw CIDR, a route can match a managed prefix list — useful for AWS service ranges (e.g. an S3 gateway endpoint) where the underlying CIDRs change over time.

const prefixListRoute = yield* AWS.EC2.Route("S3PrefixRoute", {
routeTableId: privateRouteTable.routeTableId,
destinationPrefixListId: "pl-0abc1234",
vpcEndpointId: "vpce-0abc1234",
});

Routes traffic for every CIDR in the prefix list to a Gateway Load Balancer VPC endpoint. AWS keeps the prefix list current, so you don’t have to update the route when the service’s address ranges change.

Source: src/AWS/EC2/RouteTable.ts

A VPC route table holds a set of routes that determine where network traffic from associated subnets (or gateways) is directed. Create one route table per routing domain — typically a “public” table whose default route points at an InternetGateway, and one or more “private” tables whose default route points at a NAT gateway.

A route table is little more than a container: it owns a vpcId and tags, while the actual routing behaviour is supplied by separate Route resources and applied to subnets by RouteTableAssociation resources.

The only required input is the vpcId the table belongs to. Changing vpcId later replaces the route table, since a table cannot move between VPCs.

Basic Route Table

const routeTable = yield* AWS.EC2.RouteTable("PublicRouteTable", {
vpcId: myVpc.vpcId,
});

Creates an empty route table in the given VPC. It starts with only the implicit local route (managed by AWS) until you add your own Route resources.

Route Table with Tags

const routeTable = yield* AWS.EC2.RouteTable("PrivateRouteTable", {
vpcId: myVpc.vpcId,
tags: { Name: "private-rt", Tier: "private" },
});

The tags map is merged with the alchemy auto-tags (alchemy::stack, alchemy::stage, alchemy::id) and can be updated in place without replacing the table. Use the Name tag to label the table in the AWS console.

RouteTable: Building a Public Routing Domain

Section titled “RouteTable: Building a Public Routing Domain”

A route table only directs traffic once you attach routes to it and associate it with subnets. The pattern below wires a public subnet to the internet: an InternetGateway, a default Route pointing at it, and a RouteTableAssociation binding the subnet to the table.

const internetGateway = yield* AWS.EC2.InternetGateway("InternetGateway", {
vpcId: myVpc.vpcId,
});
const publicRouteTable = yield* AWS.EC2.RouteTable("PublicRouteTable", {
vpcId: myVpc.vpcId,
});
const internetRoute = yield* AWS.EC2.Route("InternetRoute", {
routeTableId: publicRouteTable.routeTableId,
destinationCidrBlock: "0.0.0.0/0",
gatewayId: internetGateway.internetGatewayId,
});
const association = yield* AWS.EC2.RouteTableAssociation("PublicSubnetAssociation", {
routeTableId: publicRouteTable.routeTableId,
subnetId: publicSubnet.subnetId,
});

Any subnet associated with this table now reaches the public internet via the 0.0.0.0/0 route. Multiple subnets can share the same route table by declaring additional associations — a common way to give every public subnet in a VPC identical routing.

Source: src/AWS/EC2/RouteTableAssociation.ts

Associates a RouteTable with a subnet (or a gateway), making that route table govern traffic for the associated resource. A subnet can be associated with exactly one route table at a time; multiple subnets may share the same route table.

Provide exactly one of subnetId or gatewayId. Changing the subnet or gateway replaces the association, whereas pointing an existing association at a different route table is applied in place via ReplaceRouteTableAssociation.

RouteTableAssociation: Associating Subnets

Section titled “RouteTableAssociation: Associating Subnets”

Associating a subnet overrides the VPC’s main route table for that subnet. This is how you make a subnet “public” (associate it with a table that has an internet-gateway route) or “private” (associate it with a NAT-gateway table).

Associate a Subnet with a Route Table

const association = yield* AWS.EC2.RouteTableAssociation("PublicSubnetAssociation", {
routeTableId: publicRouteTable.routeTableId,
subnetId: publicSubnet.subnetId,
});

Binds a single subnet to the route table so its instances follow that table’s routes. The returned associationId (prefixed rtbassoc-) can be used to track or replace the association.

Share One Route Table Across Multiple Subnets

const subnet1Association = yield* AWS.EC2.RouteTableAssociation("PublicSubnet1Association", {
routeTableId: publicRouteTable.routeTableId,
subnetId: publicSubnet1.subnetId,
});
const subnet2Association = yield* AWS.EC2.RouteTableAssociation("PublicSubnet2Association", {
routeTableId: publicRouteTable.routeTableId,
subnetId: publicSubnet2.subnetId,
});

Declaring multiple associations against the same routeTableId gives every listed subnet identical routing — a concise way to apply one public (or private) routing policy across all subnets in a tier.

RouteTableAssociation: Associating Gateways (Edge Routing)

Section titled “RouteTableAssociation: Associating Gateways (Edge Routing)”

Instead of a subnet, an association can target an internet gateway or virtual private gateway via gatewayId. This “gateway route table association” enables edge routing, where inbound traffic is inspected or redirected (e.g. to a firewall appliance) as it enters the VPC.

const edgeAssociation = yield* AWS.EC2.RouteTableAssociation("EdgeAssociation", {
routeTableId: ingressRouteTable.routeTableId,
gatewayId: internetGateway.internetGatewayId,
});

Attaches the route table at the gateway rather than at a subnet, so traffic arriving from the internet is steered by this table — typically toward an inspection appliance before reaching its destination subnet.

Source: src/AWS/EC2/SecurityGroup.ts

An EC2 security group — a stateful virtual firewall that controls inbound (ingress) and outbound (egress) traffic for resources in a VPC. Rules can allow traffic from CIDR ranges, IPv6 ranges, managed prefix lists, or other security groups. Because it is stateful, return traffic for an allowed connection is permitted automatically regardless of the opposite-direction rules.

If no egress rules are specified, all outbound traffic is allowed by default. Changing the vpcId or groupName replaces the security group. Inline rules are authoritative: undeclared rules are removed even if they carry Alchemy tags. Standalone SecurityGroupRule resources declared in the same stack and stage retain ownership of their persisted physical rule IDs; their own providers manage their updates and deletion. Cloud tags alone do not establish ownership.

Every security group belongs to a VPC. groupName and description are optional — alchemy generates a deterministic name and a default description when they are omitted. Both the VPC and the name are immutable, so changing either replaces the group.

Empty Security Group

const sg = yield* AWS.EC2.SecurityGroup("AppSg", {
vpcId: vpc.vpcId,
});

With no rules, this group denies all inbound traffic and (since no egress is given) allows all outbound. It’s a useful starting point you attach rules to later, or a target other groups can reference.

Named group with a description

const sg = yield* AWS.EC2.SecurityGroup("AppSg", {
vpcId: vpc.vpcId,
groupName: "app-tier",
description: "Application tier security group",
});

Set an explicit groupName when you need a stable, human-readable identifier (for example to reference the group by name elsewhere). The description is shown in the EC2 console and cannot be changed after creation.

Inbound rules are declared inline via ingress. Each rule specifies an ipProtocol (tcp, udp, icmp, or -1 for all), an optional port range (fromPort/toPort), and a source — most commonly an IPv4 cidrIpv4.

const webSg = yield* AWS.EC2.SecurityGroup("WebSecurityGroup", {
vpcId: vpc.vpcId,
description: "Web tier security group",
ingress: [
{
ipProtocol: "tcp",
fromPort: 80,
toPort: 80,
cidrIpv4: "0.0.0.0/0",
description: "Allow HTTP",
},
{
ipProtocol: "tcp",
fromPort: 443,
toPort: 443,
cidrIpv4: "0.0.0.0/0",
description: "Allow HTTPS",
},
],
tags: { Name: "web-sg" },
});

Two rules open the standard web ports to the whole internet (0.0.0.0/0). Setting fromPort equal to toPort opens a single port; widen the range to open a contiguous span.

Outbound traffic is governed by egress. If you omit it entirely, the group enforces the default “allow all outbound” rule. Supplying egress replaces that default with exactly the rules you list — so you must re-add an allow-all rule if you still want unrestricted outbound.

const lockedSg = yield* AWS.EC2.SecurityGroup("LockedSg", {
vpcId: vpc.vpcId,
description: "Outbound restricted to HTTPS",
egress: [
{
ipProtocol: "tcp",
fromPort: 443,
toPort: 443,
cidrIpv4: "0.0.0.0/0",
description: "Allow outbound HTTPS",
},
],
});

This locks egress down to port 443 only — useful for instances that should only call out to HTTPS APIs. Any other outbound traffic (DNS, NTP, etc.) would need explicit rules added here.

Changing a rule description updates the existing physical rule in place. Adding or removing a rule leaves unrelated rule IDs unchanged. Redeploying unchanged code repairs missing rules and removes undeclared rules.

const sg = yield* AWS.EC2.SecurityGroup("AppSg", {
vpcId: vpc.vpcId,
ingress: [{
ipProtocol: "tcp",
fromPort: 443,
toPort: 443,
cidrIpv4: "10.0.0.0/16",
description: "HTTPS",
description: "Internal HTTPS",
}],
});

Removing description resets it to an empty description without replacing the rule.

Omitting ingress restores no inbound rules. Omitting egress restores the default IPv4 allow-all outbound rule; egress: [] disables outbound traffic. These defaults apply on creation, property removal, and drift repair.

const sg = yield* AWS.EC2.SecurityGroup("AppSg", {
vpcId: vpc.vpcId,
egress: [{
ipProtocol: "tcp",
fromPort: 443,
toPort: 443,
cidrIpv4: "0.0.0.0/0",
}],
});

Standalone rules must be declared in the same stack and stage as this group. Pass the whole resource as group to order creation and updates after inline reconciliation. The ID-only groupId form remains supported, but a stable ID alone does not order concurrent inline updates. Ownership is verified against each current declaration’s persisted physical rule ID. Removing a declaration ends that ownership; tags alone do not protect rules. Cross-stack or cross-stage rule ownership is unsupported: this group removes rules declared elsewhere when reconciling its authoritative configuration. Inline and standalone rules must have distinct identities. If a standalone rule owns IPv4 allow-all egress, set egress: [] to disable the inline default.

yield* AWS.EC2.SecurityGroupRule("HttpsIngress", {
group: sg,
type: "ingress",
ipProtocol: "tcp",
fromPort: 443,
toPort: 443,
cidrIpv4: "10.0.0.0/16",
});

Instead of a CIDR, a rule’s source can be another security group via referencedGroupId. This is the idiomatic way to express tier-to-tier trust (“the database accepts connections from anything in the app tier”) without pinning IP addresses.

const dbSg = yield* AWS.EC2.SecurityGroup("DbSecurityGroup", {
vpcId: vpc.vpcId,
description: "Database tier security group",
ingress: [
{
ipProtocol: "tcp",
fromPort: 5432,
toPort: 5432,
referencedGroupId: webSg.groupId,
description: "Allow PostgreSQL from web tier",
},
],
tags: { Name: "db-sg" },
});

Only instances in webSg can reach PostgreSQL on this group, regardless of their IPs. As the web tier scales up and down, the rule keeps working without any change.

Beyond IPv4 CIDRs, a rule source can be an IPv6 range (cidrIpv6) or a managed prefix list (prefixListId). For ICMP, set ipProtocol: "icmp" and use fromPort/toPort as the ICMP type and code (-1 for all).

const sg = yield* AWS.EC2.SecurityGroup("EdgeSg", {
vpcId: vpc.vpcId,
description: "Edge security group",
ingress: [
{
ipProtocol: "tcp",
fromPort: 443,
toPort: 443,
cidrIpv6: "::/0",
description: "Allow HTTPS over IPv6",
},
{
ipProtocol: "tcp",
fromPort: 22,
toPort: 22,
prefixListId: "pl-0123456789abcdef0",
description: "Allow SSH from corporate prefix list",
},
{
ipProtocol: "icmp",
fromPort: -1,
toPort: -1,
cidrIpv4: "10.0.0.0/16",
description: "Allow all ICMP from within the VPC",
},
],
});

Prefix lists let you reference a centrally-maintained set of CIDRs (e.g. your corporate egress IPs) by ID, so the rule updates automatically as the list changes. The ICMP rule with type/code -1 permits ping and other ICMP within the VPC.

Source: src/AWS/EC2/SecurityGroupRule.ts

A single ingress or egress rule attached to an existing security group, managed as a standalone resource. Use this when you want to manage a security group’s rules independently of its inline rules, or to add rules to a group not managed by an Alchemy SecurityGroup resource.

Declare the group and rule in the same stack and stage, passing the whole resource as group to order rule operations after inline reconciliation. groupId remains supported for ID-only callers, but its stable value alone does not order concurrent inline updates. Specify exactly one input form; switching forms with the same physical ID does not replace the rule. The group recognizes the current declaration and its persisted physical rule ID, not cloud tags. Rules in another stack or stage are not protected from the group’s reconciliation; cross-stack ownership is unsupported.

Changes to protocol, ports, source, or type props replace the rule. External edits to protocol, ports, source, description, and tags are repaired on unchanged deployment. Description and tag prop changes update in place.

The type field decides the direction: "ingress" for inbound rules and "egress" for outbound. Everything else (protocol, ports, source) is shared between the two directions.

Inbound HTTPS from anywhere

const httpsRule = yield* AWS.EC2.SecurityGroupRule("HttpsIngress", {
group: sg,
type: "ingress",
ipProtocol: "tcp",
fromPort: 443,
toPort: 443,
cidrIpv4: "0.0.0.0/0",
description: "Allow HTTPS",
});

Opens TCP 443 inbound from the entire internet on the target group. A single port is expressed by setting fromPort and toPort to the same value.

Outbound to a database port

const egressRule = yield* AWS.EC2.SecurityGroupRule("DbEgress", {
group: sg,
type: "egress",
ipProtocol: "tcp",
fromPort: 5432,
toPort: 5432,
cidrIpv4: "10.0.0.0/16",
description: "Allow PostgreSQL to the VPC",
});

This rule allows outbound PostgreSQL within the VPC CIDR. Set egress: [] on the parent group to remove its default allow-all-outbound rule; adding a standalone egress rule does not remove other rules.

A rule’s source (for ingress) or destination (for egress) is exactly one of: an IPv4 CIDR (cidrIpv4), an IPv6 CIDR (cidrIpv6), another security group (referencedGroupId), or a managed prefix list (prefixListId).

Allow traffic from another security group

const dbFromWeb = yield* AWS.EC2.SecurityGroupRule("DbFromWeb", {
group: dbSg,
type: "ingress",
ipProtocol: "tcp",
fromPort: 5432,
toPort: 5432,
referencedGroupId: webSg.groupId,
description: "Allow PostgreSQL from web tier",
});

Referencing webSg rather than a CIDR means any instance in the web tier can reach the database, even as the tier’s IPs change. This is the preferred way to wire trust between tiers.

Allow an IPv6 range

const ipv6Rule = yield* AWS.EC2.SecurityGroupRule("HttpsIpv6", {
group: sg,
type: "ingress",
ipProtocol: "tcp",
fromPort: 443,
toPort: 443,
cidrIpv6: "::/0",
description: "Allow HTTPS over IPv6",
});

Use cidrIpv6 for dual-stack workloads; ::/0 is the IPv6 equivalent of 0.0.0.0/0. IPv4 and IPv6 are separate rules — you’d pair this with a cidrIpv4 rule to cover both.

Allow from a managed prefix list

const sshRule = yield* AWS.EC2.SecurityGroupRule("SshFromCorp", {
group: sg,
type: "ingress",
ipProtocol: "tcp",
fromPort: 22,
toPort: 22,
prefixListId: "pl-0123456789abcdef0",
description: "Allow SSH from the corporate prefix list",
});

A prefixListId references a centrally-managed set of CIDRs by ID, so the rule’s effective ranges update automatically whenever the prefix list does.

ipProtocol accepts tcp, udp, icmp/icmpv6, a protocol number, or -1 for all protocols. For ICMP, fromPort is the ICMP type and toPort is the ICMP code, with -1 meaning “all”.

Allow all traffic from a trusted CIDR

const allRule = yield* AWS.EC2.SecurityGroupRule("AllFromVpc", {
group: sg,
type: "ingress",
ipProtocol: "-1",
cidrIpv4: "10.0.0.0/16",
description: "Allow all protocols from within the VPC",
});

With ipProtocol: "-1", ports are ignored and every protocol is permitted — appropriate only for fully trusted sources such as your own VPC CIDR.

Allow ICMP echo (ping)

const icmpRule = yield* AWS.EC2.SecurityGroupRule("AllowPing", {
group: sg,
type: "ingress",
ipProtocol: "icmp",
fromPort: 8,
toPort: 0,
cidrIpv4: "10.0.0.0/16",
description: "Allow ICMP echo request",
});

For ICMP the port fields carry the type and code: type 8 / code 0 is an echo request (ping). Use fromPort: -1, toPort: -1 to allow every ICMP type/code instead.

Source: src/AWS/EC2/Snapshot.ts

A point-in-time backup of an EBS Volume, stored in S3. Snapshots are incremental and immutable — you create new Volumes from them via snapshotId.

A snapshot is immutable once created; changing the source volumeId replaces it. Creation is asynchronous — the resource waits for the snapshot to reach the completed state before returning.

const snapshot = yield* AWS.EC2.Snapshot("DailyBackup", {
volumeId: volume.volumeId,
description: "nightly backup of the data volume",
});

The snapshot captures the volume’s state at creation time. Because snapshots are incremental, only blocks changed since the previous snapshot of the same volume are stored.

const restored = yield* AWS.EC2.Volume("Restored", {
availabilityZone: "us-east-1a",
snapshotId: snapshot.snapshotId,
});

Pass a snapshot’s snapshotId to Volume to provision a new volume pre-populated with the snapshot’s data — the standard backup/restore and clone-across-AZ pattern.

Source: src/AWS/EC2/StartInstance.ts

Runtime binding for the StartInstances operation scoped to the bound Instance (IAM action ec2:StartInstances on the instance ARN).

Starts the stopped instance — the classic scheduled Lambda that powers dev boxes on in the morning. Starting an already-running instance succeeds without effect. Provide the implementation with Effect.provide(AWS.EC2.StartInstanceHttp).

// init — bind the operation to the instance
const startInstance = yield* AWS.EC2.StartInstance(instance);
// runtime — power the instance on
const result = yield* startInstance();
console.log(result.StartingInstances?.[0]?.CurrentState?.Name);

Source: src/AWS/EC2/StopInstance.ts

Runtime binding for the StopInstances operation scoped to the bound Instance (IAM action ec2:StopInstances on the instance ARN).

Stops the running instance — the other half of the scheduled start/stop-Lambda pattern that powers dev fleets off overnight. Pass Hibernate: true for hibernation-enabled instances. Provide the implementation with Effect.provide(AWS.EC2.StopInstanceHttp).

// init — bind the operation to the instance
const stopInstance = yield* AWS.EC2.StopInstance(instance);
// runtime — power the instance off
const result = yield* stopInstance();
console.log(result.StoppingInstances?.[0]?.CurrentState?.Name);

Source: src/AWS/EC2/Subnet.ts

A subnet within a VPC — a range of IP addresses bound to a single Availability Zone where you place instances and other resources. Create public subnets (with mapPublicIpOnLaunch) for internet-facing resources and private subnets for internal ones.

Changing the vpcId, cidrBlock, availability zone, or an IPAM/IPv6 pool replaces the subnet.

A subnet carves a smaller CIDR range out of its parent VPC’s block. The cidrBlock must be a subset of the VPC CIDR and must not overlap any sibling subnet. You can also let AWS IPAM allocate the range via ipv4IpamPoolId + ipv4NetmaskLength.

const subnet = yield* AWS.EC2.Subnet("TestSubnet", {
vpcId: vpc.vpcId,
cidrBlock: "10.0.1.0/24",
});

The minimal subnet: a /24 (256 addresses) inside the VPC. Without an explicit availabilityZone, AWS picks one for you.

Each subnet lives in exactly one AZ. Pin it with availabilityZone (the zone name, e.g. us-east-1a) or availabilityZoneId (the stable zone ID, e.g. use1-az1) to spread tiers across zones for high availability.

const subnet = yield* AWS.EC2.Subnet("Az1Subnet", {
vpcId: vpc.vpcId,
cidrBlock: "10.0.1.0/24",
availabilityZone: "us-east-1a",
});

Pinning the AZ lets you place a matching subnet in us-east-1b and run resources redundantly across zones. Use availabilityZoneId instead when you need the physical zone to line up across different AWS accounts.

const publicSubnet = yield* AWS.EC2.Subnet("PublicSubnet", {
vpcId: vpc.vpcId,
cidrBlock: "10.0.1.0/24",
availabilityZone: "us-east-1a",
mapPublicIpOnLaunch: true,
tags: { Name: "public-1a", Tier: "public" },
});

mapPublicIpOnLaunch: true makes this a “public” subnet — instances launched here automatically get a public IPv4 address. Combine it with an internet gateway route so those instances can reach the internet.

For dual-stack VPCs, give the subnet an IPv6 cidrBlock, auto-assign IPv6 addresses on launch with assignIpv6AddressOnCreation, and optionally enable enableDns64 so the Amazon DNS resolver synthesizes IPv6 addresses for IPv4-only destinations (NAT64).

const subnet = yield* AWS.EC2.Subnet("Ipv6Subnet", {
vpcId: vpc.vpcId,
cidrBlock: "10.0.1.0/24",
ipv6CidrBlock: "2600:1f18:abcd:1234::/64",
assignIpv6AddressOnCreation: true,
enableDns64: true,
});

Instances launched here receive an IPv6 address automatically, and enableDns64 lets them reach IPv4-only services through a NAT gateway. The IPv6 /64 must come from the parent VPC’s IPv6 block.

Control what hostnames instances receive on launch. hostnameType chooses between IP-based names (ip-name) and resource-based names (resource-name), and the enableResourceNameDnsARecordOnLaunch / enableResourceNameDnsAAAARecordOnLaunch flags register A / AAAA records for resource-name hosts.

const subnet = yield* AWS.EC2.Subnet("ResourceNameSubnet", {
vpcId: vpc.vpcId,
cidrBlock: "10.0.1.0/24",
hostnameType: "resource-name",
enableResourceNameDnsARecordOnLaunch: true,
enableResourceNameDnsAAAARecordOnLaunch: true,
});

Resource-name hostnames are derived from the instance ID rather than its IP, so they stay stable across stop/start. Enabling the A/AAAA records makes those names resolvable over IPv4 and IPv6.

const vpc = yield* AWS.EC2.Vpc("MyVpc", {
cidrBlock: "10.0.0.0/16",
enableDnsSupport: true,
enableDnsHostnames: true,
});
const publicSubnet = yield* AWS.EC2.Subnet("PublicSubnet", {
vpcId: vpc.vpcId,
cidrBlock: "10.0.1.0/24",
availabilityZone: "us-east-1a",
mapPublicIpOnLaunch: true,
});
const privateSubnet = yield* AWS.EC2.Subnet("PrivateSubnet", {
vpcId: vpc.vpcId,
cidrBlock: "10.0.10.0/24",
availabilityZone: "us-east-1a",
});

The canonical two-tier pattern: a public subnet (auto public IPs, routed to an internet gateway) for load balancers and a private subnet (no public IPs) for application and database instances. Both share the same AZ here, but in production you’d replicate the pair across AZs.

Source: src/AWS/EC2/Volume.ts

An Elastic Block Store (EBS) volume — durable block storage you attach to an EC2 instance via a VolumeAttachment. Volumes live in a single Availability Zone and persist independently of any instance.

Changing availabilityZone, encrypted, kmsKeyId, snapshotId, or shrinking size replaces the volume. Growing size and changing iops, throughput, or volumeType are applied in place via modifyVolume (note AWS enforces a 6-hour cooldown between volume modifications).

const volume = yield* AWS.EC2.Volume("DataVolume", {
availabilityZone: "us-east-1a",
size: 20,
volumeType: "gp3",
});

The minimal volume: a 20 GiB general-purpose gp3 volume in one AZ. It must be in the same AZ as the instance you attach it to.

const fast = yield* AWS.EC2.Volume("FastVolume", {
availabilityZone: "us-east-1a",
size: 100,
volumeType: "gp3",
iops: 6000,
throughput: 250,
});

gp3 decouples IOPS and throughput from size, so you can provision up to 16,000 IOPS and 1,000 MiB/s independently. Use io2 for the highest durability and IOPS ceilings.

const secure = yield* AWS.EC2.Volume("SecureVolume", {
availabilityZone: "us-east-1a",
size: 20,
encrypted: true,
kmsKeyId: "alias/my-app-key",
});

Setting kmsKeyId implies encryption. Omit it while setting encrypted: true to use the account’s default EBS KMS key.

const restored = yield* AWS.EC2.Volume("RestoredVolume", {
availabilityZone: "us-east-1a",
snapshotId: snapshot.snapshotId,
});

When you create a volume from a snapshot, size defaults to the snapshot’s size and can only be grown, never shrunk.

Source: src/AWS/EC2/VolumeAttachment.ts

Attaches an EBS Volume to an EC2 Instance at a device name. The volume and instance must be in the same Availability Zone. On delete the volume is detached (and force-detached as a fallback) before the resource is removed.

This is an existence-style resource — its identity is the volumeId/instanceId/device triple. Changing any of them replaces the attachment.

const attachment = yield* AWS.EC2.VolumeAttachment("DataAttachment", {
volumeId: volume.volumeId,
instanceId: instance.instanceId,
device: "/dev/sdf",
});

The volume appears to the instance as a block device at device. On modern Linux AMIs the kernel may rename /dev/sdf to /dev/xvdf — check lsblk inside the instance. The volume and instance must share an AZ.

Source: src/AWS/EC2/Vpc.ts

An Amazon VPC (Virtual Private Cloud) — an isolated virtual network that is the root of any custom AWS networking topology. Subnets, route tables, gateways, security groups, and instances are all created inside a VPC.

Changing the cidrBlock, instanceTenancy, or an IPAM/IPv6 pool replaces the VPC.

A VPC is defined by a private IPv4 address range (cidrBlock). Pick a block from the RFC 1918 private space (e.g. 10.0.0.0/16) that is large enough to subdivide into subnets across your Availability Zones.

Basic VPC

const vpc = yield* AWS.EC2.Vpc("MyVpc", {
cidrBlock: "10.0.0.0/16",
});

A /16 gives you 65,536 addresses to carve into subnets — enough headroom for a multi-AZ, multi-tier network. This is the minimal config every other networking resource builds on.

Allocating IPv4 from an IPAM pool

const vpc = yield* AWS.EC2.Vpc("MyVpc", {
ipv4IpamPoolId: "ipam-pool-0123456789abcdef0",
ipv4NetmaskLength: 16,
});

Instead of hard-coding cidrBlock, let AWS IPAM hand out a non-overlapping range of the requested size. Use this when an organization centrally manages address space to avoid CIDR collisions between accounts.

Two independent toggles control DNS behavior inside the VPC. enableDnsSupport lets instances resolve names via the Amazon DNS server; enableDnsHostnames additionally assigns public DNS hostnames to instances with public IPs.

const vpc = yield* AWS.EC2.Vpc("MyVpc", {
cidrBlock: "10.0.0.0/16",
enableDnsSupport: true,
enableDnsHostnames: true,
});

Enable both when instances need public DNS names or when you rely on private hosted zones and VPC endpoints, which require DNS resolution to function.

const vpc = yield* AWS.EC2.Vpc("MyVpc", {
cidrBlock: "10.0.0.0/16",
instanceTenancy: "dedicated",
});

Forcing "dedicated" tenancy ensures every instance launched in the VPC runs on single-tenant hardware — required by some compliance regimes, but more expensive than the "default" shared tenancy. This property cannot be changed after creation without replacing the VPC.

A VPC can carry an IPv6 /56 block alongside its IPv4 range. The block can come from Amazon’s pool, an IPAM pool, or your own BYOIP pool (ipv6CidrBlock + ipv6Pool, optionally scoped to a ipv6CidrBlockNetworkBorderGroup).

Amazon-provided IPv6 block

const vpc = yield* AWS.EC2.Vpc("MyVpc", {
cidrBlock: "10.0.0.0/16",
amazonProvidedIpv6CidrBlock: true,
});

Requests an Amazon-assigned IPv6 /56, the simplest way to make a VPC dual-stack. Pair it with IPv6-enabled subnets and an egress-only internet gateway for outbound-only IPv6 connectivity.

IPv6 from an IPAM pool

const vpc = yield* AWS.EC2.Vpc("MyVpc", {
cidrBlock: "10.0.0.0/16",
ipv6IpamPoolId: "ipam-pool-0fedcba9876543210",
ipv6NetmaskLength: 56,
});

Draws the IPv6 block from a centrally-managed IPAM pool instead of Amazon’s pool, giving you deterministic, organization-governed IPv6 ranges.

const vpc = yield* AWS.EC2.Vpc("MyVpc", {
cidrBlock: "10.0.0.0/16",
enableDnsSupport: true,
enableDnsHostnames: true,
});
const subnet = yield* AWS.EC2.Subnet("PublicSubnet", {
vpcId: vpc.vpcId,
cidrBlock: "10.0.1.0/24",
availabilityZone: "us-east-1a",
mapPublicIpOnLaunch: true,
});

Passing vpc.vpcId into a Subnet is how you build out a topology — the subnet’s CIDR must fall within the VPC’s cidrBlock. Add route tables, gateways, and security groups the same way.

const vpc = yield* AWS.EC2.Vpc("MyVpc", {
cidrBlock: "10.0.0.0/16",
tags: {
Name: "production-vpc",
Environment: "production",
},
});

User tags are merged with alchemy’s auto-tags (alchemy::stack, alchemy::stage, alchemy::id), which brand the VPC as managed by your stack. The Name tag is what surfaces in the EC2 console.

Source: src/AWS/EC2/VpcEndpoint.ts

A VPC endpoint that connects your VPC privately to an AWS service (or a service behind a Gateway Load Balancer) without traversing the public internet, a NAT gateway, or an internet gateway.

The vpcEndpointType selects how the connection is realized:

  • "Gateway" — for S3 and DynamoDB; traffic is directed by adding routes to the route tables in routeTableIds (no hourly cost).
  • "Interface" — for most other AWS services; provisions elastic network interfaces in subnetIds, guarded by securityGroupIds, and optionally resolves the service’s public DNS name privately via privateDnsEnabled.
  • "GatewayLoadBalancer" — routes traffic through a third-party appliance fleet fronted by a Gateway Load Balancer.

Changing vpcId, serviceName, or vpcEndpointType replaces the endpoint; route tables, subnets, security groups, DNS, and the policy update in place.

Gateway endpoints target S3 and DynamoDB and work by injecting a prefix-list route into each route table you list, so requests to the service stay on the AWS network.

const s3Endpoint = yield* AWS.EC2.VpcEndpoint("S3Endpoint", {
vpcId: vpc.vpcId,
serviceName: "com.amazonaws.us-east-1.s3",
vpcEndpointType: "Gateway",
routeTableIds: [privateRouteTable.routeTableId],
tags: { Name: "s3-endpoint" },
});

Listing the private subnets’ route tables in routeTableIds lets those subnets reach S3 directly, removing NAT data-processing charges for S3 traffic and keeping it off the public internet.

Interface endpoints place an ENI in each chosen subnet and are reached over private IPs; enabling private DNS lets existing SDK calls resolve to the endpoint transparently.

const secretsEndpoint = yield* AWS.EC2.VpcEndpoint("SecretsEndpoint", {
vpcId: vpc.vpcId,
serviceName: "com.amazonaws.us-east-1.secretsmanager",
vpcEndpointType: "Interface",
subnetIds: [privateSubnet.subnetId],
securityGroupIds: [endpointSecurityGroup.groupId],
privateDnsEnabled: true,
ipAddressType: "ipv4",
dnsOptions: {
dnsRecordIpType: "ipv4",
},
});

The endpoint gets an interface in each subnetIds entry, securityGroupIds controls who may reach those interfaces, and privateDnsEnabled: true makes the service’s default DNS name resolve to the endpoint; ipAddressType and dnsOptions tune the IP family used for the interfaces and their DNS records.

VpcEndpoint: Restricting Access with a Policy

Section titled “VpcEndpoint: Restricting Access with a Policy”
const s3Endpoint = yield* AWS.EC2.VpcEndpoint("RestrictedS3Endpoint", {
vpcId: vpc.vpcId,
serviceName: "com.amazonaws.us-east-1.s3",
vpcEndpointType: "Gateway",
routeTableIds: [privateRouteTable.routeTableId],
policyDocument: JSON.stringify({
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Principal: "*",
Action: ["s3:GetObject"],
Resource: ["arn:aws:s3:::my-bucket/*"],
},
],
}),
});

policyDocument attaches an endpoint policy (JSON) that constrains which service actions and resources can be reached through the endpoint; omit it to allow full access to the service.

Source: src/AWS/EC2/VpcPeeringConnection.ts

A VPC peering connection links two VPCs so resources in each can communicate using private IP addresses, as if they were on the same network. The two VPCs can be in the same account or different accounts, and the same Region or different Regions. Their CIDR blocks must not overlap.

A peering connection is a two-sided handshake: a requester VPC creates the request and an accepter VPC accepts it. For same-account, same-Region peering alchemy accepts the request for you automatically (autoAccept defaults to true); for cross-account or cross-Region peering the connection is left in pending-acceptance for the peer to accept out of band. Once active, add Routes on both sides pointing the peer CIDR at the connection to actually carry traffic.

VpcPeeringConnection: Creating a Peering Connection

Section titled “VpcPeeringConnection: Creating a Peering Connection”

Same-Account Peering (auto-accepted)

const vpcA = yield* AWS.EC2.Vpc("VpcA", { cidrBlock: "10.0.0.0/16" });
const vpcB = yield* AWS.EC2.Vpc("VpcB", { cidrBlock: "10.1.0.0/16" });
const peering = yield* AWS.EC2.VpcPeeringConnection("Peering", {
vpcId: vpcA.vpcId,
peerVpcId: vpcB.vpcId,
});

Because both VPCs are in the same account and Region, the request is accepted automatically and the connection reaches the active state.

Cross-Account Peering (accepted out of band)

const peering = yield* AWS.EC2.VpcPeeringConnection("Peering", {
vpcId: myVpc.vpcId,
peerVpcId: "vpc-0abc123",
peerOwnerId: "123456789012",
});

With a different peerOwnerId the connection stays in pending-acceptance until the peer account accepts it.

VpcPeeringConnection: Routing Traffic Across the Peering

Section titled “VpcPeeringConnection: Routing Traffic Across the Peering”
const peering = yield* AWS.EC2.VpcPeeringConnection("Peering", {
vpcId: vpcA.vpcId,
peerVpcId: vpcB.vpcId,
});
const routeAtoB = yield* AWS.EC2.Route("RouteAtoB", {
routeTableId: vpcARouteTable.routeTableId,
destinationCidrBlock: "10.1.0.0/16",
vpcPeeringConnectionId: peering.vpcPeeringConnectionId,
});

Each side needs a route pointing the other VPC’s CIDR at the peering connection; only then can instances reach each other over private IPs.