EC2

Amazon EC2 — the full 807-operation control plane. VPCs, subnets, security groups, instances, EBS, AMIs, transit gateways, VPN, IPAM, Verified Access, and the entire networking long tail at 100% Smithy conformance.

fakecloud implements 807 of 807 AWS EC2 operations at 100% Smithy conformance — the complete control plane for the largest service surface in AWS. Request/response shapes, flattened ec2Query XML lists, field names, enum validation, and integer/length bounds are checked against AWS's own Smithy model on every commit.

Supported features

  • Core networking — VPCs (+ secondary CIDRs, tenancy), DHCP option sets, subnets (+ CIDR reservations), security groups (rules, references, VPC associations, per-interface quota validation), route tables, internet / egress-only / NAT gateways, and elastic IPs with transfer/move flows.
  • Compute — RunInstances and the full instance lifecycle (start/stop/reboot/terminate/monitor), instance attributes, credit specifications, metadata + maintenance options, instance types, and topology. Key pairs and placement groups. Instances are backed by real Docker/Podman containers — RunInstances boots a container per instance, runs your user-data at boot, and maps start/stop/reboot/terminate onto the container lifecycle (falling back to a metadata-only control plane when no container runtime is present). BlockDeviceMapping EBS entries create attached volumes (size, type, source snapshot and encryption as asked), reported under the instance's blockDeviceMapping; terminating the instance deletes the DeleteOnTermination ones and detaches the rest. RunInstances with a LaunchTemplate (by LaunchTemplateId or LaunchTemplateName) resolves the version ($Default when none is given, $Latest, or a version number; an unknown template or version is InvalidLaunchTemplateId.NotFound / InvalidLaunchTemplateName.NotFoundException / InvalidLaunchTemplateId.VersionNotFound) and merges its LaunchTemplateData under the request's own parameters, as EC2 does: anything the request sets wins, a list the request sets (block-device mappings, network interfaces, security groups) replaces the template's, tags merge per resource type with the request's value winning, and an instance-level subnet or security group applies to the template's primary network interface. Every field the launch path honors comes from the template too (image, type, key pair, security groups, network interface subnet / private IP / groups / public IP, user data, IAM instance profile, tags, metadata, monitoring, placement, EBS optimization, credit specification, CPU options, DNS name options, termination / stop protection, block-device mappings as real volumes), and the instance carries the aws:ec2launchtemplate:id / aws:ec2launchtemplate:version tags. Network interfaces beyond the primary one are created (or an existing one attached) at their device index, and deleted with the instance (or detached) on termination; InstanceMarketOptions Spot launches report instanceLifecycle spot; a fixed PrivateIpAddress needs a single-instance launch and an address not already in use (InvalidIPAddress.InUse). Auto Scaling and CloudFormation launch through the same path.
  • Storage — EBS volumes (+ modifications, recycle bin), snapshots (+ copy, tier, lock, fast restores, block-public-access), AMIs (register/copy/deprecate/deregistration-protection), and EBS encryption defaults. An encrypted volume with no named key uses the account's EBS default key: the AWS-managed aws/ebs key for the region (a real KMS key, reported by ARN, as is GetEbsDefaultKmsKeyId) unless ModifyEbsDefaultKmsKeyId set another. EnableEbsEncryptionByDefault encrypts every new volume; snapshots inherit their volume's key, volumes created from an encrypted snapshot inherit its encryption, key and size, and CopySnapshot keeps the source key in-region or uses the default key when it newly encrypts.
  • Seeded public AMI catalogue — every account starts with a small catalogue of public Amazon-owned (Amazon Linux 2, Amazon Linux 2023 x86_64/arm64, Windows Server) and Canonical-owned (Ubuntu 22.04/24.04) AMIs, just like a real account. DescribeImages honours the Owner param (amazon / aws-marketplace / self / account id) and name filters with * wildcards, so the standard Terraform data "aws_ami" pattern (most_recent = true, owners = ["amazon"], a name glob) resolves to a real image instead of returning empty — and anything that chains off it (an aws_instance, an ELBv2 target-group attachment) can be planned.
  • Interfaces — elastic network interfaces with attachments, permissions, IPv4/IPv6 address assignment, and prefix lists.
  • Edge / advanced networking — network ACLs, VPC peering, VPC endpoints + PrivateLink (services, connection notifications), flow logs, launch templates (+ versions: SourceVersion, SetDefaultVersion, real version deletion, $Latest / $Default / number selection and MinVersion / MaxVersion on describe, and GetLaunchTemplateData derived from a running instance), spot requests / fleets / EC2 fleets, capacity reservations (+ fleets, cancellation and date-change quotes), reserved instances, dedicated hosts.
  • Transit Gateway — the complete 74-op surface: gateways, attachments, route tables (+ associations / propagations / prefix-list refs), peering, Connect + Connect peers, policy tables, route-table announcements, multicast domains (+ group members/sources), metering policies, and Client-VPN attachments.
  • Site-to-Site & Client VPN — customer gateways, virtual private gateways, VPN connections (+ routes / tunnels / device configs), VPN concentrators, and full Client VPN (endpoints, routes, authorization rules, Cedar authorization policies + device posture, target networks, connections, certificate/config export-import).
  • IPAM — IPAMs, scopes, pools, pool CIDRs + allocations, resource CIDRs, address history, resource discovery (+ associations, discovered getters), BYOASN, BYOIP-to-IPAM, external resource verification tokens, policies (+ allocation rules / org targets), and prefix-list resolvers (+ targets / rules / versions).
  • Verified Access — instances, trust providers (+ attach/detach), groups, endpoints, policies (group + endpoint), logging configuration, and client-config export.
  • Network Insights — reachability paths + analyses and access scopes + scope analyses (content, findings).
  • Outpost / hybrid — carrier gateways, CoIP pools + CIDRs, local-gateway route tables, routes, VPC + virtual-interface-group associations, virtual interfaces, and groups.
  • Access & diagnostics — EC2 Instance Connect endpoints, fast launch, serial-console access, console output / screenshot, and password data. GetConsoleOutput returns the instance container's real console log (its combined stdout/stderr, including anything user-data printed at boot), base64-encoded as on AWS.
  • Cross-cutting — tag specifications on create, CreateTags / DeleteTags / DescribeTags, Filter.N filtering, and MaxResults / NextToken pagination on every operation the model paginates (all 215 Describe* / Get* / Search* / List* operations with a NextToken input): MaxResults is checked against the model's range, nextToken is opaque and returned only when items remain, and a token fakecloud did not mint is rejected with InvalidParameterValue.

Protocol

EC2 uses the ec2Query protocol: form-encoded requests and flattened-XML responses (no <member> wrapper, lower-camel element names, lowercase <requestId>, no <Result> envelope). Because EC2 declares no per-operation error shapes, fakecloud is lenient on not-found and validates only wire-observable negatives — missing required scalars, invalid enum values, out-of-range integers / MaxResults, and bad lengths.

Introspection

  • GET /_fakecloud/ec2/instances — list every fakecloud-managed EC2 instance across all account partitions with its control-plane metadata (instanceId, imageId, instanceType, state, privateIp, publicIp, subnetId, vpcId, keyName, securityGroupIds, availabilityZone, launchTime) plus containerId — the backing Docker container id or Kubernetes Pod name, or null when the instance runs metadata-only (no container runtime). Lets tests assert on instance state (and which container backs it) without re-parsing DescribeInstances XML. Exposed through every fakecloud introspection SDK — fc.ec2().getInstances() (Go/Java/TS/PHP), fc.ec2.get_instances() (Python), fc.ec2().get_instances() (Rust).
  • GET /_fakecloud/ec2/instance-networks — inspect each instance's real backing network: the Docker/Podman network (fakecloud-subnet-<id>) or k8s NetworkPolicy (fakecloud-ec2-<id>) that backs it, its container IP, the isolationBackend (docker/podman/kubernetes/none), the securityGroupEnforcement mechanism (nftables/networkpolicy/disabled), and whether enforcement is currently enforcementActive. A debugging aid for "why can't X reach Y". Exposed through every introspection SDK — fc.ec2().getInstanceNetworks() (Go/Java/TS/PHP), fc.ec2.get_instance_networks() (Python), fc.ec2().get_instance_networks() (Rust).

Instance metadata service (IMDS)

fakecloud serves the EC2 instance metadata service on /latest/* so an app that resolves credentials through IMDS (rather than static keys) runs unmodified. Point the SDK's IMDS client at fakecloud with AWS_EC2_METADATA_SERVICE_ENDPOINT=http://<host>:<port>/ and the default credential chain resolves locally with no code change.

Both IMDSv1 (plain GET) and IMDSv2 (PUT /latest/api/token then GET with X-aws-ec2-metadata-token) are supported; fakecloud hands out a token but does not enforce it. Served paths: meta-data/iam/security-credentials/ (role name) and meta-data/iam/security-credentials/{role} (credential JSON, from the same registered-credential cache as /_fakecloud/credentials, so it verifies under --verify-sigv4), meta-data/iam/info, meta-data/instance-id, meta-data/placement/region, meta-data/placement/availability-zone, and dynamic/instance-identity/document. The reported instance ID is set with --imds-instance-id. See the Run an app unmodified guide.

Inside an instance. Code running in an instance's backing container reaches IMDS at the real address, http://169.254.169.254, with that instance's own answers: its instance ID, AMI ID, instance type, private IP (meta-data/local-ipv4), Availability Zone, identity document, and credentials for the role of the instance profile associated with it (IamInstanceProfile on RunInstances, or AssociateIamInstanceProfile; an instance without a profile has no iam/ paths, as on EC2). A sidecar sharing the instance's network namespace redirects 169.254.169.254:80 to a proxy that forwards to fakecloud's per-instance IMDS (/_fakecloud/ec2/imds/<instance-id>/latest/...), the way the ECS runtime routes 169.254.170.2. User-data waits for it (up to 60 seconds) before running, so a boot script that reads IMDS first thing finds it. The Kubernetes backend adds the same sidecar to the instance Pod. If the helper image can't be built or pulled, the instance instead gets AWS_EC2_METADATA_SERVICE_ENDPOINT pointing at its per-instance IMDS on fakecloud. The helper image is overridable with FAKECLOUD_EC2_HELPER_IMAGE.

Load balancer targets. An ELBv2 target group that routes to i-...:<port> reaches the port the instance opened, even though it opened it after boot: on first use fakecloud starts a small forwarder that publishes that port on the host, and the load balancer data plane and health checks connect through it. Forwarders go away when the instance stops or terminates, and nothing is forwarded to a stopped instance. With security-group enforcement on, a forwarder counts as a member of the load balancer's security groups, so an instance rule that admits only the load balancer's group admits it.

Network isolation

VPC/subnet/security-group/NACL metadata isn't just stored — fakecloud gives instances real network isolation backed by the container runtime:

  • Default VPC — every account+region ships a default VPC (172.31.0.0/16) with an internet gateway, a main route table, one default subnet per AZ, a default security group, and a default NACL, exactly like AWS. RunInstances with no SubnetId lands in the default subnet and attaches the default security group.

  • L3 isolation (Docker/Podman) — each subnet gets its own daemon network (fakecloud-subnet-<id>); instances in the same subnet share a bridge and can talk, while instances in different VPCs/subnets land on different bridges and cannot route to each other. Private subnets (no 0.0.0.0/0 → igw route) back onto --internal networks with no NAT to the host.

  • Security-group + NACL enforcement (Docker/Podman) — when enabled, security-group and NACL rules are translated into an nftables ruleset applied on the host, so SG rules actually block/allow traffic. As on AWS, a packet between two instances must be allowed by the sender's egress rules and the receiver's ingress rules: the two are evaluated as independent gates (separate nftables base chains), so the default security group's allow-all egress never lets traffic past the receiver's ingress default-deny. This needs CAP_NET_ADMIN + nft, so it is opt-in via FAKECLOUD_EC2_SG_ENFORCEMENT=1 and degrades gracefully: without the capability (CI, Docker Desktop, rootless podman) the rules are tracked but not enforced, with a one-time startup warning — L3 isolation still holds. The published image ships nft (plus kmod/procps, which it needs to load and enable bridge netfilter so same-subnet traffic is actually filtered); you only need to grant the capability and set the env var. With docker run:

    docker run -e FAKECLOUD_EC2_SG_ENFORCEMENT=1 --cap-add=NET_ADMIN \
      -p 4566:4566 -v /var/run/docker.sock:/var/run/docker.sock \
      ghcr.io/faiscadev/fakecloud:latest

    or in docker-compose.yml, uncomment the cap_add: [NET_ADMIN] and FAKECLOUD_EC2_SG_ENFORCEMENT lines in the shipped compose. Confirm it took effect via GET /_fakecloud/ec2/instance-networks — securityGroupEnforcement reads nftables and enforcementActive is true when active.

  • Kubernetes — on the k8s backend, isolation is expressed as NetworkPolicy objects (one per instance, derived from its security groups) and enforced by the cluster CNI. fakecloud detects the CNI (Calico/Cilium enforce; kindnet does not) and warns when NetworkPolicy won't be enforced, but always creates the policies.

  • Compose interop — to let your own containers reach an instance by its private IP, attach them to the instance's subnet network: docker network connect fakecloud-subnet-<id> <your-container> (find <id> via /_fakecloud/ec2/instance-networks). Cross-VPC traffic stays isolated by design.

Known limitations

  • Instances run as containers, not VMs — RunInstances boots a real container per instance (Amazon Linux by default, overridable via FAKECLOUD_EC2_DEFAULT_IMAGE; a cached image is used as is, and a missing one is pulled first with transient registry failures such as 429 Too Many Requests retried) and runs user-data at boot, but it is a container, not a full virtual machine: no kernel modules, no nested virtualization, and the instance type / EBS sizing is metadata only. The container runs as a local Docker/Podman container by default, or as a native Kubernetes Pod when FAKECLOUD_EC2_BACKEND=k8s (or the global FAKECLOUD_CONTAINER_BACKEND=k8s) is set — mirroring the Lambda/ECS/RDS/ElastiCache backends, so fakecloud running inside Kubernetes needs no Docker daemon. On the k8s backend a stopped instance's Pod is deleted and recreated on start (instances are not persistent disks). When no container runtime is available at all, the control plane degrades to a metadata-only instance so every API call still succeeds.
  • A handful of model operations are absent from the vendored AWS SDK (DescribeIpamPoolAllocations, ModifyIpamPoolAllocation, the capacity-reservation cancellation-quote and date-change-quote pairs, and the AttachImageWatermark/DetachImageWatermark AMI watermark pair). They are implemented and conformance-probed via raw ec2Query, and graduate to typed SDK calls on the next SDK refresh.
  • Security-group / NACL packet filtering needs host privileges. Real enforcement (above) requires CAP_NET_ADMIN + nft on Docker/Podman, or a NetworkPolicy-enforcing CNI on Kubernetes. Where neither is available the rules are stored and returned faithfully but tracked-only; L3 (per-subnet/VPC) isolation still applies. ICMP and IPv6 rules are modeled best-effort. On Docker/Podman, fakecloud enables bridge netfilter (net.bridge.bridge-nf-call-iptables) so same-subnet traffic is actually filtered.
  • Enforced security-group source matching uses real container IPs, not the AWS address space. A backing container has a Docker/Podman bridge IP, not an address in the VPC's CIDR. So an enforced ingress rule keyed on a specific AWS CIDR (e.g. 10.0.0.0/8) won't match a peer instance's real source IP — only 0.0.0.0/0 (anywhere) and referenced security groups (which resolve to member instances' real IPs, like the default group's allow-from-self) match as intended. Metadata (DescribeSecurityGroups) is unaffected; this only concerns the optional packet-filtering path.