| 1 | --- |
| 2 | title: EC2 |
| 3 | description: Launch virtual machines with the Instance resource — as a raw compute primitive, or hosting a bundled long-lived Effect program served straight off the box. |
| 4 | --- |
| 5 | |
| 6 | **EC2** is for when you need the machine itself: an OS you |
| 7 | control, GPUs, custom daemons, software that expects a host, or |
| 8 | predictable dedicated capacity. The |
| 9 | [`Instance`](/providers/aws/ec2/instance) resource plays two |
| 10 | roles — a **low-level compute primitive** (launch an AMI, attach |
| 11 | networking, done), or a **host for a bundled Effect program** |
| 12 | that Alchemy deploys onto the machine and keeps running. |
| 13 | |
| 14 | This is the lowest-level of Alchemy's four runtimes: unlike |
| 15 | [Lambda](/aws/compute/lambda), [ECS](/aws/compute/ecs), and |
| 16 | [EKS](/aws/compute/eks), you own patching, |
| 17 | scaling, and availability. Reach for it deliberately — see |
| 18 | [Choosing a runtime](/aws/compute/choosing-a-runtime). |
| 19 | |
| 20 | ## Launch an instance |
| 21 | |
| 22 | The minimal instance is an AMI plus an instance type. AMI IDs |
| 23 | are per-region, so resolve one with Alchemy's image helpers |
| 24 | instead of hard-coding — `amazonLinux()` finds the latest Amazon |
| 25 | Linux AMI (there are also `amazonLinux2023`, `amazonLinux2`, |
| 26 | `ubuntu2404`, `ubuntu2204`, an id-only `image()` finder, and the |
| 27 | underlying `getAmi()` data source returning the full image |
| 28 | description). Each returns an `Output` resolved at deploy time: |
| 29 | |
| 30 | ```typescript |
| 31 | const instance = yield* AWS.EC2.Instance("AppInstance", { |
| 32 | imageId: AWS.EC2.amazonLinux(), |
| 33 | instanceType: "t3.micro", |
| 34 | subnetId: subnet.subnetId, |
| 35 | }); |
| 36 | ``` |
| 37 | |
| 38 | The resolved instance exposes `instanceId`, `publicIpAddress`, |
| 39 | `privateIpAddress`, DNS names, and the rest of the observed |
| 40 | state — see the [`Instance` reference](/providers/aws/ec2/instance) |
| 41 | for every prop and attribute. |
| 42 | |
| 43 | ## Host an Effect program |
| 44 | |
| 45 | Give the instance a `main` entrypoint and it becomes a runtime, |
| 46 | same class pattern as Lambda and ECS. Because instance props |
| 47 | (the network, the AMI) are resolved by the stack, the props slot |
| 48 | takes an `Effect.gen` block. The composition is also re-executed |
| 49 | inside the deployed instance's bundle — that's safe as-is: |
| 50 | resource yields resolve to references at runtime, and the AMI |
| 51 | helper returns an `Output` that only resolves at deploy time: |
| 52 | |
| 53 | ```typescript |
| 54 | // src/server.ts |
| 55 | import * as AWS from "alchemy/AWS"; |
| 56 | import * as Effect from "effect/Effect"; |
| 57 | import { HttpServerRequest } from "effect/http/HttpServerRequest"; |
| 58 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 59 | |
| 60 | export default class Server extends AWS.EC2.Instance<Server>()( |
| 61 | "Server", |
| 62 | Effect.gen(function* () { |
| 63 | const network = yield* AWS.EC2.Network("AppNetwork", { |
| 64 | cidrBlock: "10.81.0.0/16", |
| 65 | availabilityZones: 1, |
| 66 | }); |
| 67 | const securityGroup = yield* AWS.EC2.SecurityGroup("AppSg", { |
| 68 | vpcId: network.vpcId, |
| 69 | description: "app ingress", |
| 70 | ingress: [ |
| 71 | { |
| 72 | ipProtocol: "tcp", |
| 73 | fromPort: 3000, |
| 74 | toPort: 3000, |
| 75 | cidrIpv4: "0.0.0.0/0", |
| 76 | description: "app", |
| 77 | }, |
| 78 | ], |
| 79 | egress: [ |
| 80 | { |
| 81 | ipProtocol: "-1", |
| 82 | cidrIpv4: "0.0.0.0/0", |
| 83 | description: "all outbound", |
| 84 | }, |
| 85 | ], |
| 86 | }); |
| 87 | |
| 88 | return { |
| 89 | main: import.meta.url, |
| 90 | imageId: AWS.EC2.amazonLinux(), |
| 91 | instanceType: "t3.small", |
| 92 | subnetId: network.publicSubnetIds[0], |
| 93 | securityGroupIds: [securityGroup.groupId], |
| 94 | associatePublicIpAddress: true, |
| 95 | port: 3000, |
| 96 | }; |
| 97 | }), |
| 98 | Effect.gen(function* () { |
| 99 | return { |
| 100 | fetch: Effect.gen(function* () { |
| 101 | const request = yield* HttpServerRequest; |
| 102 | const url = new URL(request.url, "http://instance"); |
| 103 | if (url.pathname === "/health") { |
| 104 | return yield* HttpServerResponse.json({ ok: true }); |
| 105 | } |
| 106 | return HttpServerResponse.text("hello from EC2"); |
| 107 | }), |
| 108 | }; |
| 109 | }), |
| 110 | ) {} |
| 111 | ``` |
| 112 | |
| 113 | At deploy time Alchemy bundles the program, uploads it to S3, |
| 114 | and boots the instance with a user-data script that installs |
| 115 | Bun and writes a **systemd unit**. The unit syncs the bundle and |
| 116 | environment from S3 on every start and runs with |
| 117 | `Restart=always`, so a flaky network install self-heals and the |
| 118 | service survives reboots. The `{ fetch }` handler is served by |
| 119 | the instance's HTTP server on `port`; `ServerHost` + `host.run` |
| 120 | background loops work exactly as they do on [ECS](/aws/compute/ecs). |
| 121 | |
| 122 | Hosted instances also get an Alchemy-managed IAM role and |
| 123 | instance profile. Bindings use the same contract as Lambda — |
| 124 | environment variables plus IAM policy statements attached to |
| 125 | that role — and you can widen it with managed policies: |
| 126 | |
| 127 | ```typescript |
| 128 | // e.g. make the instance manageable via SSM Session Manager |
| 129 | roleManagedPolicyArns: [ |
| 130 | "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore", |
| 131 | ], |
| 132 | ``` |
| 133 | |
| 134 | ## SSH access |
| 135 | |
| 136 | For direct access, create an Alchemy-managed |
| 137 | [`KeyPair`](/providers/aws/ec2/keypair) and pass its name to the |
| 138 | instance. The generated private key comes back as a `Redacted` |
| 139 | output on the resource: |
| 140 | |
| 141 | ```typescript |
| 142 | const key = yield* AWS.EC2.KeyPair("AppKeyPair", { |
| 143 | keyType: "ed25519", |
| 144 | }); |
| 145 | |
| 146 | // then on the instance props: |
| 147 | // keyName: key.keyName |
| 148 | ``` |
| 149 | |
| 150 | ## Networking |
| 151 | |
| 152 | An instance is only as reachable as the network you launch it |
| 153 | into. The example above leans on the |
| 154 | [`Network`](/providers/aws/ec2/network) helper for a |
| 155 | public-subnet VPC; when you need explicit control — private |
| 156 | subnets, NAT, VPC endpoints, custom route tables — compose the |
| 157 | primitives directly. [VPC & networking](/aws/networking) walks |
| 158 | through the whole set. |
| 159 | |
| 160 | ## Where next |
| 161 | |
| 162 | - [Choosing a runtime](/aws/compute/choosing-a-runtime) — Lambda first, |
| 163 | ECS for containers, EKS for Kubernetes, EC2 when you need the |
| 164 | machine. |
| 165 | - [VPC & networking](/aws/networking) — the `Network` helper and |
| 166 | the VPC primitives an instance lives in. |
| 167 | - [`Instance` reference](/providers/aws/ec2/instance), |
| 168 | [`KeyPair` reference](/providers/aws/ec2/keypair) — every prop |
| 169 | and attribute. |