EC2
EC2 gives you a virtual machine, called an instance. You choose its size, its operating system and its disk, and it runs until you stop it. Everything on it is yours to control: the kernel, the daemons, the GPUs, the software that expects a host. So is everything that keeps it healthy: patching, restarts, and replacing it when it fails.
An instance is an image, a type and a subnet
Section titled “An instance is an image, a type and a subnet”An image (AMI) is the disk an instance boots from: an operating system and whatever is installed on it. An instance type is its size: t3.micro has 2 vCPUs and 1 GB of memory, g5.xlarge has a GPU. A subnet is where it sits on your network, and security groups decide what can reach it.
- type
- t3.small · 2 vCPU · 2 GB
- image
- Amazon Linux (AMI)
You pay for an instance by the second while it runs, busy or idle.
Launch an instance
Section titled “Launch an instance”Image ids differ by region, so look one up instead of writing it down. amazonLinux() finds the latest Amazon Linux image at deploy time. ubuntu2404() and the others work the same way:
const instance = yield* AWS.EC2.Instance("Box", { imageId: AWS.EC2.amazonLinux(), instanceType: "t3.micro", subnetId: network.publicSubnetIds[0],});The instance’s instanceId, publicIpAddress and privateIpAddress are on the result.
Run your program on it
Section titled “Run your program on it”Give an instance main, and it runs your Effect program as a service. The program serves fetch on port, and binds what it uses exactly as a Lambda Function or an ECS task does:
export default class Server extends AWS.EC2.Instance<Server>()( "Server", Effect.gen(function* () { const network = yield* AWS.EC2.Network("AppNetwork", { cidrBlock: "10.81.0.0/16", availabilityZones: 1 }); const web = yield* AWS.EC2.SecurityGroup("Web", { vpcId: network.vpcId, ingress: [{ ipProtocol: "tcp", fromPort: 3000, toPort: 3000, cidrIpv4: "0.0.0.0/0" }], egress: [{ ipProtocol: "-1", cidrIpv4: "0.0.0.0/0" }], }); return { main: import.meta.url, imageId: AWS.EC2.amazonLinux(), instanceType: "t3.small", subnetId: network.publicSubnetIds[0], securityGroupIds: [web.groupId], associatePublicIpAddress: true, port: 3000, }; }), Effect.gen(function* () { return { fetch: Effect.succeed(HttpServerResponse.text("hello from EC2")), }; }),) {}The props are an Effect here because they depend on the network and security group declared inside it.
Deploy bundles the program, uploads it, and boots the instance with a script that installs Bun and a systemd service. The service fetches the latest bundle and runs it on every start, and restarts it if it exits, so the program survives reboots and crashes. Background loops with ServerHost work as they do on ECS.
Give it permissions
Section titled “Give it permissions”An instance that runs your program gets its own IAM role, attached through an instance profile. Each binding adds its statement to that role, and the program’s AWS calls use the role’s credentials.
Add AWS managed policies for tools that run on the box, like Session Manager:
roleManagedPolicyArns: ["arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"],Connect to it
Section titled “Connect to it”With the Session Manager policy above, open a shell from the AWS console or CLI, with no open port and no key. For SSH, declare a key pair and give its name to the instance:
const key = yield* AWS.EC2.KeyPair("BoxKey", { keyType: "ed25519" });Pass keyName: key.keyName in the instance’s props. The private key comes back Redacted on the key pair, and port 22 must be open in its security group.
Place it on the network
Section titled “Place it on the network”An instance is as reachable as its subnet and security groups make it. In a public subnet with associatePublicIpAddress, it has an internet address. In a private subnet, only things inside the VPC can reach it, and it reaches out through a NAT gateway.
Open only the ports you serve, to only the sources that need them. See VPC & networking.