Skip to content

EC2

Protocol: EC2 Query (XML) — POST http://localhost:4566/ with Action= parameter

Instance Execution Model

RunInstances launches a real Docker container for each instance. By default, the container is kept alive with tail -f /dev/null so any base image works regardless of its default CMD. Catalog entries that opt into the systemd guest runtime start /sbin/init instead, with the Docker mounts needed for a systemd-based cloud-image guest.

EC2 state Docker operation
pending → running Container created and started
running → stopping → stopped docker stop (30 s timeout, then SIGKILL)
stopped → pending → running docker start
running → shutting-down → terminated docker rm -f
Reboot docker restart

Terminated instances remain queryable for 1 hour (matching real EC2 tombstone behavior) before being pruned.

AMI to Docker Image Mapping

Floci resolves AMI IDs to Docker images from the EC2 image catalog at src/main/resources/ec2/image-catalog.yaml. The same catalog stores the fallback Docker image, per-AMI Docker image mappings, and DescribeImages metadata.

AMI ID Aliases Docker image
ami-0abcdef1234567890 ami-amazonlinux2 public.ecr.aws/amazonlinux/amazonlinux:2
ami-0abcdef1234567891 ami-amazonlinux2023 public.ecr.aws/amazonlinux/amazonlinux:2023
ami-0abcdef1234567892 ami-ubuntu2004 public.ecr.aws/docker/library/ubuntu:20.04
ami-ubuntu2204 public.ecr.aws/docker/library/ubuntu:22.04
ami-ubuntu2404-arm64 ami-ubuntu2404 public.ecr.aws/docker/library/ubuntu:24.04
ami-ubuntu2404-amd64 public.ecr.aws/docker/library/ubuntu:24.04
ami-ubuntu2404-cloud-arm64 ami-ubuntu2404-cloud floci/ami-ubuntu:24.04-arm64
ami-debian12 public.ecr.aws/docker/library/debian:12
ami-alpine public.ecr.aws/docker/library/alpine:latest
ami-0abcdef1234567893 public.ecr.aws/amazonlinux/amazonlinux:2023

Any unrecognized AMI ID (including real AWS AMI IDs like ami-0abc12345678) falls back to the catalog defaultDockerImage (public.ecr.aws/amazonlinux/amazonlinux:2023 by default).

Cloud-image-derived AMI guests

The ami-ubuntu2404-cloud entry is an experimental Ubuntu 24.04 guest image built from Canonical cloud-image artifacts, not from the Docker-library ubuntu:24.04 image. It is intended for EC2 workflows that need packages such as systemd and cloud-init to match a real Ubuntu cloud image more closely.

This mode is opt-in by AMI selection, not by a global configuration switch. Existing catalog entries, including ami-ubuntu2404, keep their current Docker-library image mapping and default tail -f /dev/null container lifecycle. The cloud-image-derived entry is a separate AMI ID and alias, so DescribeImages can advertise it while existing callers continue to get the old behavior unless they choose ami-ubuntu2404-cloud-arm64 or the ami-ubuntu2404-cloud alias.

The Java metadata-driven builder lives at io.github.hectorvent.floci.tools.ami.AmiImageTool. Its recipe is checked in at docker/ec2/ami-images/image-build-metadata.yaml, and generated context/provenance defaults to target/ami-images/<image-id>/.

./mvnw -q -DskipTests compile exec:java \
  -Dexec.mainClass=io.github.hectorvent.floci.tools.ami.AmiImageTool \
  -Dexec.args="plan --image-id ubuntu-24.04-arm64"

./mvnw -q -DskipTests compile exec:java \
  -Dexec.mainClass=io.github.hectorvent.floci.tools.ami.AmiImageTool \
  -Dexec.args="generate --image-id ubuntu-24.04-arm64"

./mvnw -q -DskipTests compile exec:java \
  -Dexec.mainClass=io.github.hectorvent.floci.tools.ami.AmiImageTool \
  -Dexec.args="build --image-id ubuntu-24.04-arm64"

./mvnw -q -DskipTests compile exec:java \
  -Dexec.mainClass=io.github.hectorvent.floci.tools.ami.AmiImageTool \
  -Dexec.args="smoke --image-id ubuntu-24.04-arm64"

SSH Key Injection

If KeyName is specified at launch, Floci looks up the stored key pair's public key (generated by CreateKeyPair or supplied to ImportKeyPair) and copies it into /root/.ssh/authorized_keys inside the container at boot. It then attempts to start sshd if present. The SSH port (container port 22) is mapped to a host port from the configured range (default 2200–2299).

Key pairs created with CreateKeyPair return real RSA private key material, and the matching public key is injected at launch, so either a created or an imported key pair works for SSH.

Security Group Port Publishing

With FLOCI_NETWORK_SECURITY_GROUP_ENFORCEMENT_ENABLED=true, Floci prepares a separate Linux network namespace and an nftables default-deny policy before starting each Docker-backed instance. It checks new managed connections against both the sender's egress rules and the receiver's ingress rules, using emulated ENI addresses and security-group membership before translating to Docker addresses. Established replies use connection tracking. This filters packets but does not emulate VPC routing, NACLs, NAT gateways, or peering. Rootless Docker and Windows containers are unsupported.

Security-group permissions on a host-published port use the source visible inside Docker. Docker Desktop may replace the original external-client address. The legacy socat application publisher is disabled while enforcement is enabled because it obscures that source. SSH is published directly by the protected namespace. Application host ports remain available through the legacy publisher only when enforcement is explicitly disabled. Direct managed container traffic uses the logical ENI identity.

When enforcement is disabled, an instance's security groups can open a TCP port to a CIDR source and Floci publishes that port on the host so you can reach the app from localhost. For each opened port Floci starts a small alpine/socat sidecar container that binds an allocated host port (default range 30000–30999) and forwards it to the instance container's IP. This works both for rules present at launch and for rules added later with authorize-security-group-ingress; revoking the rule removes the forward. The mapping (app port -> host port) is written to the logs:

Published EC2 instance i-0abc... app port 80 on host port 30000 (socat -> 172.17.0.3:80)

Notes and limitations:

  • The app inside the instance must listen on 0.0.0.0 (not 127.0.0.1) for the forward to reach it.
  • Only CIDR-sourced TCP rules are published. A port opened only to a referenced security group (or via a prefix list) is not published, matching AWS: those grant reachability from the referenced group's private IPs, not from the host. The source CIDR value itself is not enforced, so a CIDR-sourced port is reachable whether the rule is 0.0.0.0/0 or narrower.
  • Ports are aggregated across all of the instance's security groups, SSH (22) is never re-forwarded, and any single rule whose port span exceeds max-published-ports-per-instance (default 20) is skipped so an allow-all range cannot spawn thousands of sidecars. The total published per instance is capped at the same limit.
  • Stopping an instance tears down its forwards; starting it again does not automatically restore them (re-run authorize-security-group-ingress, or recreate the instance).
  • With enforcement disabled, set publish-security-group-ports: false (FLOCI_SERVICES_EC2_PUBLISH_SECURITY_GROUP_PORTS=false) to keep security groups as metadata only.

UserData

UserData must be base64-encoded in the request (matching the AWS wire format). Floci decodes it, copies the script into /var/lib/user-data.sh inside the container, and executes the script directly after SSH key injection so the script shebang selects the interpreter. Output is captured and logged.

EC2 containers receive AWS_EC2_METADATA_SERVICE_ENDPOINT for IMDS and AWS_ENDPOINT_URL for AWS service API calls back to Floci.

Instance Metadata Service (IMDS)

Floci runs an IMDS-compatible HTTP server on port 9169 of the host. Each launched container receives the environment variable AWS_EC2_METADATA_SERVICE_ENDPOINT pointing to this server.

Both IMDSv1 (no token) and IMDSv2 (token-based) flows are supported:

# IMDSv2 — get a token first
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
  -H "x-aws-ec2-metadata-token-ttl-seconds: 21600")

# Then use the token for metadata requests
curl -s -H "x-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/instance-id

Supported IMDS endpoints

Endpoint Returns
GET /latest/meta-data/instance-id Instance ID
GET /latest/meta-data/ami-id Image ID
GET /latest/meta-data/instance-type Instance type
GET /latest/meta-data/local-ipv4 Private IP
GET /latest/meta-data/public-ipv4 Public IP — the container IP where that is routable from the host, 127.0.0.1 otherwise
GET /latest/meta-data/public-hostname Public hostname
GET /latest/meta-data/local-hostname Private DNS name
GET /latest/meta-data/hostname Private DNS name
GET /latest/meta-data/mac MAC address of first ENI
GET /latest/meta-data/security-groups Security group names
GET /latest/meta-data/placement/availability-zone AZ
GET /latest/meta-data/placement/region Region
GET /latest/meta-data/iam/info IAM instance profile info
GET /latest/meta-data/iam/security-credentials/ Role name list
GET /latest/meta-data/iam/security-credentials/{role} Temporary credentials
GET /latest/user-data UserData script
GET /latest/dynamic/instance-identity/document Identity document JSON

IAM credentials are served when the instance has an IamInstanceProfile.Arn that resolves to an existing instance profile with one role in the profile's account. The role list uses the role name, which can differ from the profile name. Missing profiles, missing roles, and requests for another role return 404.

Each registered instance receives its own temporary IAM session. Credentials last one hour and refresh on retrieval during the final five minutes; the previous generation remains valid until expiration. Floci revokes tracked sessions when the instance is unregistered or IMDS shuts down, and discards persisted EC2 sessions when Floci restarts. Restored guests obtain fresh credentials after metadata registration is rebuilt.

Guests with an instance profile receive endpoint and region settings without Floci's static test credentials, allowing the standard SDK credential chain to reach IMDS. Guests without an instance profile retain the existing local test credential environment. Custom images can still override the SDK chain with their own credentials. This does not add EKS access-entry authorization or change Floci's global IAM enforcement configuration.

IMDS and SSM managed instances

IMDS only knows about containers that Floci itself launched through RunInstances. That launch is what installs the link-local 169.254.169.254 proxy inside the container and maps the container's IP to its instance record.

Registering a container with SSM is independent of this: an SSM agent that calls UpdateInstanceInformation becomes a managed instance, but that does not create an EC2 instance record, does not install the IMDS proxy, and does not register the container with IMDS. From such a container, curl http://169.254.169.254/... fails outright (no proxy), and a request sent straight to the host IMDS port returns 404 with a message explaining that no EC2 instance is registered for the source IP. Floci also logs a warning when an SSM agent registers from a container that is not backed by a Floci EC2 instance.

To get a container that both answers IMDS (including instance profile credentials) and executes SendCommand directly, launch it with RunInstances first, then register that same container's SSM agent as a managed instance. Do not register an arbitrary or pre-existing container directly with SSM and expect IMDS to work. See SSM for the SendCommand side of this.

Default Resources

Floci seeds the following resources on first use in each region so Terraform, the AWS CLI, and SDK clients work out of the box without any setup:

Resource ID Details
Default VPC vpc-default CIDR 172.31.0.0/16
Default Subnet (AZ a) subnet-default-a CIDR 172.31.0.0/20
Default Subnet (AZ b) subnet-default-b CIDR 172.31.16.0/20
Default Subnet (AZ c) subnet-default-c CIDR 172.31.32.0/20
Default Security Group sg-default groupName=default, all-traffic egress
Default Internet Gateway igw-default Attached to default VPC
Main Route Table rtb-default Associated with default VPC
Default Network ACL acl-default Allow-all, associated with the default subnets

Supported Actions

Instances

Action Description
RunInstances Creates one or more local EC2 instances, starting Docker-backed runtime when not in mock mode.
CreateFleet Creates an instant local fleet from launch-template configurations and on-demand or spot overrides. DryRun returns the AWS-compatible DryRunOperation error without launching instances.
DescribeInstances Lists or returns stored EC2 instances.
TerminateInstances Terminates instances and updates their stored lifecycle state.
StartInstances Starts stopped instances and their local runtime when applicable.
StopInstances Stops running instances and updates their stored lifecycle state.
RebootInstances Reboots instances through the local EC2 service model.
DescribeInstanceStatus Returns status records for stored instances.
DescribeInstanceCreditSpecifications Returns the CPU credit option of burstable performance instances. A named instance id reports the option the instance acquired at launch, either the explicit CreditSpecification.CpuCredits or the family default, standard for t2 and unlimited for t3, t3a and t4g. An id that is not a burstable performance instance reports standard, and only an unknown id is an error. Naming no id returns the instances on the unlimited option, including one that kept unlimited after a resize onto a non-burstable type. Filter.N supports the modeled instance-id and narrows whichever set the request selected. MaxResults accepts 5 through 1000 and cannot be combined with instance ids. DryRun=true returns DryRunOperation.
DescribeInstanceAttribute Returns a supported attribute for an instance.
ModifyInstanceAttribute Updates supported mutable attributes for an instance.
ModifyInstanceMetadataOptions Updates an instance's IMDS options, changing only the fields the request names.

VPCs

Action Description
CreateVpc Creates a VPC with the requested CIDR block.
DescribeVpcs Lists or returns stored VPCs.
DeleteVpc Deletes a VPC from the local EC2 store, together with its default security group and rules, main route table and default network ACL. Fails with DependencyViolation while the VPC still has a subnet, a security group, route table or network ACL other than those defaults, a VPC endpoint, or an attached internet gateway. Instances, NAT gateways and other subnet-resident resources are not checked.
ModifyVpcAttribute Updates supported VPC attributes.
DescribeVpcAttribute Returns a supported VPC attribute.
DescribeVpcEndpointServices Returns an empty local VPC endpoint service catalog.
CreateVpcEndpoint Creates a VPC endpoint record, including its PolicyDocument and the per-subnet IPv4 and IPv6 addresses named by SubnetConfiguration.N. An Ipv4 value outside the named subnet's CIDR, or among the first four or the last address AWS reserves in it, is rejected with InvalidParameterValue.
DescribeVpcEndpoints Lists or returns stored VPC endpoints.
ModifyVpcEndpoint Associates or disassociates route tables, subnets and security groups, and sets or resets the endpoint policy. SubnetConfiguration.N replaces the addresses pinned for a subnet, under the same address validation as CreateVpcEndpoint. DnsOptions and IpAddressType are accepted and ignored.
DeleteVpcEndpoints Deletes VPC endpoint records.
DescribeVpnGateways Validates filters and returns empty discovery results; explicit IDs return not-found errors.
DescribeEgressOnlyInternetGateways Validates filters and pagination parameters and returns an empty set, including for explicit IDs, as AWS does.
CreateDefaultVpc Creates or returns the default VPC for the region.
AssociateVpcCidrBlock Adds a secondary CIDR block association to a VPC.
DisassociateVpcCidrBlock Removes a secondary CIDR block association from a VPC.

The two describe-only network actions above provide discovery compatibility when no resources exist. Egress-only gateway discovery validates its pagination parameters before returning an empty page. Neither models virtual private gateway or egress-only internet gateway lifecycles.

Subnets

Action Description
CreateSubnet Creates a subnet in a VPC.
DescribeSubnets Lists or returns stored subnets.
DeleteSubnet Deletes a subnet from the local EC2 store.
ModifySubnetAttribute Updates supported subnet attributes.

Security Groups

Action Description
CreateSecurityGroup Creates a security group in a VPC.
DescribeSecurityGroups Lists or returns stored security groups.
DeleteSecurityGroup Deletes a security group from the local EC2 store.
AuthorizeSecurityGroupIngress Adds inbound permissions. Sources may be IPv4 ranges, IPv6 ranges, or another security group (UserIdGroupPairs, sent on the wire as Groups); prefix list sources are not stored. One rule is stored per source, each carrying its own description.
AuthorizeSecurityGroupEgress Adds outbound permissions, with the same source types as the inbound call.
RevokeSecurityGroupIngress Removes inbound permissions. Matches on protocol and port range only, so it removes every permission on that port regardless of source.
RevokeSecurityGroupEgress Removes outbound permissions, matched the same way as the inbound call.
GetSecurityGroupsForVpc Lists the security groups belonging to one VPC, with the same filters as the describe call.
DescribeSecurityGroupRules Lists stored security group rules.
ModifySecurityGroupRules Updates supported fields on security group rules.
UpdateSecurityGroupRuleDescriptionsIngress Updates descriptions on matching inbound security group rules.
UpdateSecurityGroupRuleDescriptionsEgress Updates descriptions on matching outbound security group rules.

Key Pairs

Action Description
CreateKeyPair Creates and stores a local key pair.
DescribeKeyPairs Lists or returns stored key pairs.
DeleteKeyPair Deletes a key pair from the local EC2 store.
ImportKeyPair Imports a public key as a local key pair.

AMIs

Action Description
DescribeImages Returns AMI metadata known to the local EC2 service.
CreateImage Captures an instance as a new AMI. Reboots the source unless NoReboot=true.
RegisterImage Registers an AMI from supplied metadata and block device mappings.

Every resource EC2 creates is owned by the account the request resolves to, the same account STS reports for those credentials, and that account is what ownerId and the resource ARN carry. So DescribeImages with --owners <your account id> matches the AMIs that account registered, and --owners self resolves to the same account. This is what lets a Terraform aws_ami data source pin owners to the account under test instead of the emulator's default 000000000000. The amazon and aws-marketplace aliases still resolve to the AWS-owned accounts that publish those images.

Tags

Action Description
CreateTags Adds tags to supported EC2 resources.
DeleteTags Removes tags from supported EC2 resources.
DescribeTags Lists tags stored for EC2 resources.

Internet Gateways

Action Description
CreateInternetGateway Creates an internet gateway.
DescribeInternetGateways Lists or returns stored internet gateways.
DeleteInternetGateway Deletes an internet gateway.
AttachInternetGateway Attaches an internet gateway to a VPC.
DetachInternetGateway Detaches an internet gateway from a VPC.

Route Tables

Action Description
CreateRouteTable Creates a route table in a VPC.
DescribeRouteTables Lists or returns stored route tables.
DeleteRouteTable Deletes a route table from the local EC2 store.
AssociateRouteTable Associates a route table with a subnet.
DisassociateRouteTable Removes a route table association.
CreateRoute Adds a route to a route table. Accepts VpcPeeringConnectionId as a target (alongside GatewayId/NatGatewayId/EgressOnlyInternetGatewayId) and reports it back on DescribeRouteTables.
ReplaceRoute Replaces the target of an existing route.
DeleteRoute Removes a route from a route table.

VPC Peering Connections

Action Description
CreateVpcPeeringConnection Creates a peering connection between the local VPC and a peer VPC, in status pending-acceptance.
AcceptVpcPeeringConnection Transitions a pending-acceptance connection to active.
DescribeVpcPeeringConnections Lists or returns stored peering connections.
ModifyVpcPeeringConnectionOptions Sets allow_remote_vpc_dns_resolution independently per side.
DeleteVpcPeeringConnection Deletes a peering connection from the local EC2 store.

Real AWS never auto-accepts a connection, same-account or not: every CreateVpcPeeringConnection starts pending-acceptance and stays there until an explicit AcceptVpcPeeringConnection. The auto_accept convenience on Terraform's aws_vpc_peering_connection and aws_vpc_peering_connection_accepter resources is implemented by the provider, which simply issues that second call itself — so this emulator does not special-case same-account peers.

A connection is stored keyed by its id alone, not region::id like every other EC2 resource here. It is meaningfully addressable from both the requester's and the accepter's side, which can be a different region (peer_region/accepter_region); region-scoped storage would leave the accepter's AcceptVpcPeeringConnection/DescribeVpcPeeringConnections calls unable to find a connection created under the requester's region key. RejectVpcPeeringConnection is not implemented — no Gruntwork VPC-peering example exercises it (they use auto_accept, not manual rejection).

The accepter VPC named by PeerVpcId may belong to another account or region and not be modelled in this store at all (a cross-account or "external" peer). Its cidrBlock is reported only when that VPC happens to exist locally; the request still succeeds either way, and no CIDR is fabricated.

A connection's storage entry lives under whichever account's request created it, but lookups (AcceptVpcPeeringConnection, DescribeVpcPeeringConnections, ModifyVpcPeeringConnectionOptions, DeleteVpcPeeringConnection) resolve it across every account's partition, the same pattern used for RAM-shared IPAM resources. AcceptVpcPeeringConnection additionally enforces that the caller is the connection's accepter — reporting a connection it cannot see as absent, not as a permission error, matching how AWS itself responds.

Network ACLs

Action Description
CreateNetworkAcl Creates a network ACL in a VPC.
DescribeNetworkAcls Lists or returns stored network ACLs.
DeleteNetworkAcl Deletes a network ACL from the local EC2 store.
CreateNetworkAclEntry Adds an entry to a network ACL.
ReplaceNetworkAclEntry Replaces an entry in a network ACL.
DeleteNetworkAclEntry Removes an entry from a network ACL.
ReplaceNetworkAclAssociation Replaces the network ACL associated with a subnet.

Prefix Lists

Action Description
DescribePrefixLists Returns prefix lists known to the local EC2 service.
CreateManagedPrefixList Creates a customer-managed prefix list with its initial entries.
DescribeManagedPrefixLists Lists customer-managed and AWS-managed prefix lists.
GetManagedPrefixListEntries Returns the entries of a prefix list, optionally at an earlier version.
ModifyManagedPrefixList Adds or removes entries, renames the list, or raises its entry limit.
DeleteManagedPrefixList Deletes a customer-managed prefix list.

Two AWS-managed prefix lists exist in every region without being created — com.amazonaws.<region>.s3 (pl-63a5400a) and com.amazonaws.<region>.dynamodb (pl-02cd2c6b) — matching the gateway endpoint services on AWS. They are owned by AWS, read-only, and served by both DescribePrefixLists and DescribeManagedPrefixLists; modifying or deleting one returns UnsupportedOperation.

A customer-managed list may not take a name AWS reserves for its own: com.amazonaws., com.amazon. or com.aws., each including the trailing dot. CreateManagedPrefixList rejects those with InvalidParameterValue; a name that merely resembles one, such as com.amazonaws-internal, is allowed.

Entries are versioned. A prefix list starts at version 1, and each ModifyManagedPrefixList that adds or removes entries stores a new version and bumps the counter, so GetManagedPrefixListEntries can serve an earlier TargetVersion. Renaming the list or changing MaxEntries does not create a version. Passing CurrentVersion makes the modification conditional: a stale value returns PrefixListVersionMismatch. Removals are applied before additions, so one call can replace an entry's description by removing and re-adding the CIDR.

Creation is synchronous: a new list is returned as create-complete rather than passing through create-in-progress, since nothing about it is slow locally.

A security group rule can take a prefix list as its source instead of a CIDR. Pass it as IpPermissions.N.PrefixListIds.M.PrefixListId, optionally with a Description; AWS emits one rule per source, so a permission naming both CIDRs and prefix lists expands to a rule for each. The resulting rule carries prefixListId in place of cidrIpv4, and DescribeSecurityGroups nests the reference under the permission as prefixListIds. Authorizing against a list that does not exist returns InvalidPrefixListID.NotFound, so a typo cannot leave a rule pointing at nothing.

Transit Gateways

Action Description
CreateTransitGateway Creates a transit gateway, applying AWS's option defaults and minting its default route table.
DescribeTransitGateways Lists or returns stored transit gateways.
ModifyTransitGateway Updates a transit gateway's description, options and CIDR blocks.
DeleteTransitGateway Deletes a transit gateway and the default route table created with it.

Transit gateway metadata only: nothing routes packets, and the value is in ids that later resources can reference and describes that round-trip so plans converge.

Options left out of CreateTransitGateway take the same defaults AWS applies — amazonSideAsn 64512, dnsSupport, vpnEcmpSupport, defaultRouteTableAssociation and defaultRouteTablePropagation enabled, and autoAcceptSharedAttachments, securityGroupReferencingSupport and multicastSupport disabled. transitGatewayCidrBlocks is omitted from the response entirely when no blocks are set, rather than sent empty.

Creating a gateway with either default-route-table option enabled also creates the route table AWS creates, and reports its id as associationDefaultRouteTableId and propagationDefaultRouteTableId. Both name the same table. Disabling both leaves the ids absent. The actions that operate on transit gateway route tables directly — creating them, associating attachments, enabling propagation — are not implemented yet, and neither are attachments.

State is reported settled rather than transitional: AWS returns a new gateway as pending and reaches available roughly a minute later, and reports deleting before deleted. Nothing here is slow, so callers see available and deleted immediately. ModifyTransitGateway and DeleteTransitGateway echo the gateway without its tagSet, matching AWS; CreateTransitGateway and DescribeTransitGateways include it.

Transit Gateway VPC Attachments

Action Description
CreateTransitGatewayVpcAttachment Attaches a VPC to a transit gateway through one subnet per availability zone.
DescribeTransitGatewayVpcAttachments Lists or returns VPC attachments with their subnets and options.
DescribeTransitGatewayAttachments Returns the same attachments in the resource-agnostic shape, including the route table association.
DescribeTransitGatewayConnects Always returns an empty list, since Connect attachments cannot be created yet; requested ids are still validated.
ModifyTransitGatewayVpcAttachment Adds or removes attachment subnets and updates its options.
DeleteTransitGatewayVpcAttachment Deletes a VPC attachment.

An attachment's option defaults are its own rather than the gateway's: dnsSupport and securityGroupReferencingSupport enabled, ipv6Support and applianceModeSupport disabled. Note that securityGroupReferencingSupport is enabled here while a transit gateway defaults it to disabled.

The attachment is associated with the gateway's default route table only when the gateway carries defaultRouteTableAssociation enabled; a gateway created without it produces an attachment with no association. That association is reported by DescribeTransitGatewayAttachments alone — the VPC-specific describe does not carry it, and the resource-agnostic one carries neither the subnets nor the options in exchange.

Subnets must belong to the VPC being attached and no two may share an availability zone; one from another VPC is reported as InvalidSubnetID.NotFound rather than as a mismatch. A VPC can be attached to a given gateway once, so a second attempt returns DuplicateTransitGatewayAttachment. Removing every subnet returns InsufficientSubnetsException, and a gateway with a live attachment cannot be deleted — IncorrectState, naming the attachments.

As with the gateway itself, state is reported settled rather than transitional, and the echoes are trimmed the way AWS trims them: modify omits the tagSet, and delete omits both the tagSet and the subnets. Ipv6Support is accepted without checking that the subnets carry IPv6 CIDRs, which real AWS rejects; Floci does not model subnet IPv6 allocation.

Transit Gateway Route Tables

Action Description
CreateTransitGatewayRouteTable Creates a route table on a transit gateway.
DescribeTransitGatewayRouteTables Lists or returns stored transit gateway route tables.
DeleteTransitGatewayRouteTable Deletes a route table, along with its propagations and static routes.
AssociateTransitGatewayRouteTable Associates an attachment with a route table.
DisassociateTransitGatewayRouteTable Removes an attachment's association.
GetTransitGatewayRouteTableAssociations Lists the attachments associated with a route table.
EnableTransitGatewayRouteTablePropagation Propagates an attachment's routes into a route table.
DisableTransitGatewayRouteTablePropagation Stops an attachment propagating into a route table.
GetTransitGatewayRouteTablePropagations Lists the propagations into a route table.
CreateTransitGatewayRoute Adds a static or blackhole route to a route table.
DeleteTransitGatewayRoute Removes a static route.
ReplaceTransitGatewayRoute Points an existing route at a different target, or writes it if absent.
SearchTransitGatewayRoutes Returns a route table's routes, static and propagated, filtered.
ExportTransitGatewayRoutes Reports the S3 object a route-table export would be written to.

A route table asked for by name is never a default one; only the table a gateway mints for itself carries defaultAssociationRouteTable or defaultPropagationRouteTable. Deleting a route table is refused with IncorrectState while it is a gateway's default association table, and again while attachments are still associated with it; the two cases carry different messages. Once it does go, its propagations and static routes go with it.

An attachment is associated with exactly one route table at a time, so associating a second time returns Resource.AlreadyAssociated rather than moving it — disassociate first. Association is recorded on the attachment itself, which is why GetTransitGatewayRouteTableAssociations reports the attachment's VPC as the associated resource.

Propagation is separate: one attachment may propagate into several route tables. Enabling twice returns TransitGatewayRouteTablePropagation.Duplicate. Unlike association, which reports associating and disassociating, propagation reports the settled enabled or disabled at once — that is what the live API does rather than a shortcut taken here.

ReplaceTransitGatewayRoute is an upsert rather than an update: replacing a destination the table has never held writes it instead of reporting it missing, which is what the live API does. The target moves as a unit, so a route turned into a blackhole keeps no attachment and one pointed back at an attachment regains all of its fields.

SearchTransitGatewayRoutes serves both kinds of route. Static routes are stored as written; a blackhole is a static route in the blackhole state rather than a type of its own, and carries no attachment. Propagated routes are derived when searched, from each enabled propagation joined to the attached VPC's CIDR blocks, so a VPC's CIDRs changing cannot leave a stale route behind. A route table's own listings drop the route table id that the mutating calls include, matching AWS.

Route table ids follow the live API's own inconsistency: an id that does not exist is InvalidRouteTableID.NotFound, while one of the wrong shape is InvalidRouteTableId.Malformed.

ExportTransitGatewayRoutes validates the route table and requires S3Bucket, then returns the s3:// object key the export would occupy. No object is written and nothing is uploaded — the value is a caller that needs the call to succeed and the key to look right, not a readable export.

NAT Gateways

Action Description
CreateNatGateway Creates a NAT gateway record.
DescribeNatGateways Lists or returns stored NAT gateways.
DeleteNatGateway Deletes a NAT gateway record.

Capacity Reservations

Action Description
CreateCapacityReservation Reserves EC2 instance capacity in a specific Availability Zone.
DescribeCapacityReservations Lists or returns stored Capacity Reservations.
ModifyCapacityReservation Updates InstanceCount, EndDate, EndDateType or InstanceMatchCriteria in place.
CancelCapacityReservation Marks a Capacity Reservation cancelled and its available count 0, matching real AWS's retain-but-cancel behaviour rather than deleting the record.

InstanceType, InstancePlatform and InstanceCount are required, matching the AWS API, and one of AvailabilityZone or AvailabilityZoneId must be given; a request missing any of these is rejected with MissingParameter. InstanceCount must be greater than 0 on create and on modify, otherwise InvalidParameterValue. InstanceMatchCriteria defaults to open, Tenancy defaults to default and EndDateType defaults to unlimited. Creation is synchronous: the reservation comes back active on the create response rather than passing through payment-pending/assessing. DescribeCapacityReservations supports the availability-zone, end-date-type, instance-match-criteria, instance-platform, instance-type, state and tenancy filters alongside the shared tag:, tag-key and tag-value filters.

Elastic IPs

Action Description
AllocateAddress Allocates an Elastic IP address record.
DescribeAddresses Lists or returns stored Elastic IP address records.
DescribeAddressesAttribute Returns allocation ID and public IP attributes for Elastic IP addresses.
AssociateAddress Associates an Elastic IP address with a resource.
DisassociateAddress Removes an Elastic IP address association.
ReleaseAddress Releases an Elastic IP address record.

An allocated Elastic IP's 54.x.x.x address is invented and routes nowhere, so associating it re-points it at the address the instance is actually reachable on — the same value DescribeInstances reports. This is a deliberate deviation from AWS, where an EIP's public IP is fixed from allocation: without it, aws_eip.x.public_ip hands every caller a dead address. The allocation ID, association ID and domain are unaffected, and disassociating restores the allocated address.

Availability Zones & Regions

Action Description
DescribeAvailabilityZones Returns the configured local availability zones.
DescribeRegions Returns the regions known to the local EC2 service.
DescribeAccountAttributes Returns local account-level EC2 attributes.

Instance Types

Action Description
DescribeInstanceTypes Returns instance type metadata known to the local EC2 service, including burstablePerformanceSupported for the T families.
DescribeInstanceTypeOfferings Returns instance type offerings for the requested location filters.

Launch Templates

Action Description
CreateLaunchTemplate Creates a launch template with an initial version.
CreateLaunchTemplateVersion Creates a new launch template version, optionally from a source version.
DescribeLaunchTemplates Lists or returns stored launch templates.
DescribeLaunchTemplateVersions Lists versions stored for a launch template.
ModifyLaunchTemplate Updates launch template metadata such as the default version.
DeleteLaunchTemplate Deletes a launch template and its versions.

Launch templates store versioned launch data. New template versions can be created from an existing source version, and ModifyLaunchTemplate updates the default version used by later launches.

Launch template data members

These members of RequestLaunchTemplateData are stored and read back unchanged by DescribeLaunchTemplateVersions:

ImageId, InstanceType, KeyName, UserData, KernelId, RamDiskId, SecurityGroupIds, IamInstanceProfile, BlockDeviceMappings, NetworkInterfaces, TagSpecifications, MetadataOptions, Monitoring, Placement, CpuOptions, CreditSpecification, EnclaveOptions, HibernationOptions, MaintenanceOptions, PrivateDnsNameOptions, CapacityReservationSpecification, InstanceMarketOptions, InstanceRequirements, EbsOptimized, DisableApiTermination, DisableApiStop, InstanceInitiatedShutdownBehavior.

InstanceRequirements carries every member of the service model's InstanceRequirementsRequest, including the nested BaselinePerformanceFactors.Cpu.References. Within NetworkInterfaces, ConnectionTrackingSpecification is stored and read back with all three of its timeout members.

Both blocks are validated by CreateLaunchTemplate and CreateLaunchTemplateVersion before anything is stored. An InstanceRequirements block must carry VCpuCount and MemoryMiB, each with its Min, which the service model declares required; a request missing either fails with MissingParameter. A connection tracking timeout outside the range its member documents fails with InvalidParameterValue: TcpEstablishedTimeout runs from 60 to 432000 seconds, UdpTimeout from 30 to 60, and UdpStreamTimeout from 60 to 180.

Three combinations the service model documents in prose rather than in its constraints are rejected with InvalidParameterCombination:

  • InstanceRequirements and InstanceType together. A launch template selects instance types by attribute or by name, not by both.
  • AllowedInstanceTypes and ExcludedInstanceTypes together inside InstanceRequirements.
  • SpotMaxPricePercentageOverLowestPrice and MaxSpotPriceAsPercentageOfOptimalOnDemandPrice together inside InstanceRequirements.

CreateLaunchTemplateVersion applies these three to the merged version as well as to the request it received, because the merged data is what the version stores. A version that names only InstanceType against a source version carrying InstanceRequirements is therefore rejected: the merge inherits the requirements block and cannot express its removal. Omitting SourceVersion starts from empty data and is how a template moves between the two selection modes.

Two behaviours worth calling out, because they are what Terraform reads back:

  • IamInstanceProfile keeps the form it was given. A profile submitted as Name reads back as Name, not rewritten to Arn. At launch time, Floci resolves that name against IAM in the caller's account and preserves the profile's full path in its ARN. A name missing from that account is rejected with InvalidParameterValue. This also applies to direct RunInstances requests and CreateFleet launches, so aws_launch_template.iam_instance_profile.name converges.
  • NetworkInterfaces stays a NetworkInterfaces block. Its Groups are not hoisted into top-level SecurityGroupIds; on AWS the two are mutually exclusive. A launch from the template resolves its security groups from whichever of the two is populated.

Launch template versions

CreateLaunchTemplateVersion accepts two members outside LaunchTemplateData itself:

  • SourceVersion controls inheritance, and omitting it means no inheritance. A request that names a source version — including $Latest or $Default — layers its own fields onto that version's data, so only the fields it restates change. A request that omits SourceVersion entirely does not fall back to the latest version: the new version starts from an empty LaunchTemplateData, populated only by whatever fields the request itself supplies. This matches the AWS-documented behavior; it does not merge onto any prior version.
  • VersionDescription is stored and read back by DescribeLaunchTemplateVersions. It is a version-level field on LaunchTemplateVersion, not a member of RequestLaunchTemplateData / ResponseLaunchTemplateData, so it is tracked per version alongside VersionNumber and CreateTime rather than inside the launch template data payload. CreateLaunchTemplate accepts the same field for the initial version it creates.

Members the service model declares that are accepted and ignored rather than stored: LicenseSpecifications, ElasticGpuSpecifications, ElasticInferenceAccelerators, NetworkPerformanceOptions, Operator, SecondaryInterfaces and SecurityGroups (security groups by name, where resolving names to IDs would need lookup machinery, including ambiguity handling across VPCs, that no other EC2 action here has either; RunInstances itself only accepts SecurityGroupId). Within NetworkInterfaces, the IPv4/IPv6 address and prefix lists, EnaSrdSpecification, PrimaryIpv6 and EnaQueueCount are likewise ignored.

IAM Instance Profiles

Action Description
DescribeIamInstanceProfileAssociations Lists IAM instance profile associations known to the local EC2 service.

Network Interfaces

Action Description
CreateNetworkInterface Creates a standalone elastic network interface (ENI) in a subnet, unattached.
DescribeNetworkInterfaces Lists network interfaces known to the local EC2 service, both an instance's implicit primary interface and standalone ENIs created via CreateNetworkInterface.
AttachNetworkInterface Attaches an available standalone ENI to a running or stopped instance at a device index.
DetachNetworkInterface Detaches a standalone ENI by attachment ID, returning it to available.
DeleteNetworkInterface Deletes a standalone ENI. Fails while the ENI is still attached, matching AWS.

A standalone ENI created via CreateNetworkInterface can also be handed to RunInstances as an instance's primary interface (NetworkInterface.1.NetworkInterfaceId / NetworkInterface.1.DeviceIndex) instead of letting the instance create its own implicit one, the pattern Terraform's aws_instance resource uses for network_interface { network_interface_id = ... }. AWS only allows this for a single instance per launch call; RunInstances rejects it otherwise with InvalidParameterCombination.

ModifyNetworkInterfaceAttribute is not implemented: no example in the corpus that needed CreateNetworkInterface was found to need it. A route table's CreateRoute with a NetworkInterfaceId target is accepted but not recorded, since Route does not yet model an ENI target; a subsequent plan against such a route may show drift.

Volumes

Action Description
CreateVolume Creates an EBS volume record.
DescribeVolumes Lists or returns stored EBS volume records.
DeleteVolume Deletes an EBS volume record.

EBS Encryption Defaults

Action Description
EnableEbsEncryptionByDefault Turns on default encryption for new volumes in the region.
DisableEbsEncryptionByDefault Turns default encryption back off.
GetEbsEncryptionByDefault Reports whether default encryption is on.
ModifyEbsDefaultKmsKeyId Sets the KMS key used when a volume names none.
GetEbsDefaultKmsKeyId Reports the current default KMS key.
ResetEbsDefaultKmsKeyId Restores the AWS-managed default key.

These are account-level settings scoped per region, not per volume, and nothing here encrypts anything — no volume's stored bytes change. LZA's SecurityStack drives them through its Custom::EnableEbsEncryptionByDefault Lambda, which calls enable plus ModifyEbsDefaultKmsKeyId on create and disable on delete, then reads the state back with the two Get calls.

An account that has never set a key reports alias/aws/ebs, the AWS-managed EBS key every account starts with, rather than an empty value — the module runner fails hard on a missing KmsKeyId, so the fallback is what keeps it running. ResetEbsDefaultKmsKeyId returns to that same alias. ModifyEbsDefaultKmsKeyId requires KmsKeyId and rejects a blank one with MissingParameter; the key is stored as given and is not checked against KMS.

Snapshot Block Public Access

Action Description
EnableSnapshotBlockPublicAccess Sets the region's snapshot sharing block to block-all-sharing or block-new-sharing.
DisableSnapshotBlockPublicAccess Returns the region to unblocked.
GetSnapshotBlockPublicAccessState Reads the region's current state.

This is an account-level setting scoped per region, not a resource, so there is no id and nothing to tag. A region that was never configured reads back unblocked. EnableSnapshotBlockPublicAccess accepts only block-all-sharing and block-new-sharing, and rejects unblocked with InvalidParameterValue the way AWS does: disabling goes through DisableSnapshotBlockPublicAccess, which returns the resulting unblocked rather than the prior state. A missing State is rejected with MissingParameter.

Only GetSnapshotBlockPublicAccessState returns managedBy, and it always reports account because Floci has no declarative-policy layer that could take the setting over. Nothing here changes snapshot permissions: no snapshot's createVolumePermission is rewritten when the block goes on or off.

All three actions honor DryRun. A request that would otherwise succeed returns DryRunOperation with HTTP 412 and leaves the stored state untouched. EnableSnapshotBlockPublicAccess validates State first, so an invalid or missing State is still rejected on its own error even when DryRun=true is set.

IPAM

Action Description
EnableIpamOrganizationAdminAccount Delegates IPAM administration to a member account.
DisableIpamOrganizationAdminAccount Removes the IPAM delegated administrator.
CreateIpam Creates an IPAM with its default private and public scopes.
DescribeIpams Lists or returns stored IPAMs.
ModifyIpam Updates an IPAM's description, tier, metered account and operating regions.
DeleteIpam Deletes an IPAM and, leniently, the pools that belong to it.
CreateIpamPool Creates a pool under a scope, optionally sourced from a parent pool.
DescribeIpamPools Lists or returns stored pools.
ModifyIpamPool Updates a pool's description, auto-import flag and netmask-length bounds.
DeleteIpamPool Deletes a pool.
ProvisionIpamPoolCidr Provisions a CIDR onto a pool, validated against its source pool.
GetIpamPoolCidrs Returns a pool's provisioned CIDRs.
AllocateIpamPoolCidr Allocates a CIDR from a pool, by explicit CIDR or by netmask length.
ReleaseIpamPoolAllocation Releases an allocation, returning its space to the pool.
GetIpamPoolAllocations Returns a pool's live allocations.
AssociateIpamByoasn Associates a BYOASN with a CIDR.
DisassociateIpamByoasn Removes a BYOASN association.
DescribeIpamByoasn Lists BYOASN associations in the region.

This is what LZA needs end to end: the Organization stage delegates the IPAM admin through Custom::EnableIpamOrganizationAdminAccount, the Network stages build the IPAM and pool hierarchy through CloudFormation, and the get-ipam-subnet-cidr custom-resource Lambda allocates subnet CIDRs from pools at Deploy time.

Allocation is real rather than recorded. AllocateIpamPoolCidr by netmask length hands out the first free block that fits, skipping both live allocations and any space already provisioned onward to child pools, and reports InsufficientCidrBlocks when nothing fits. An explicit Cidr must fall inside a provisioned CIDR and must not overlap an existing allocation. ProvisionIpamPoolCidr on a pool with a source pool requires the CIDR to sit inside one of the parent's provisioned CIDRs. Releasing an allocation returns its space, so the next allocation of the same size reuses it.

CreateIpam, CreateIpamPool, ProvisionIpamPoolCidr and AllocateIpamPoolCidr honour ClientToken — the four IPAM operations that model it. A replay returns what the first call produced rather than creating a second resource: the same pool, the same provisioned CIDR, the same allocation id and CIDR, with pool consumption unchanged. This matters most on AllocateIpamPoolCidr, which LZA's get-ipam-subnet-cidr Lambda retries; without it each retry would burn another distinct CIDR out of the pool. Parameter differences on a replay are ignored rather than rejected as IdempotentParameterMismatch, and the token is not echoed in the response, matching the AWS output shapes. A token is scoped to the account that used it.

Pool lookups deliberately fall back to an id-only scan across accounts, so a RAM-shared pool resolves from a workload account and region. That fallback covers reads and allocation only: AllocateIpamPoolCidr from an account that does not own the pool succeeds and writes the allocation back to the owner's partition rather than forking a copy into the caller's. Mutations of the pool itself — ModifyIpamPool, DeleteIpamPool, ProvisionIpamPoolCidr — are owner-only, as are ModifyIpam and DeleteIpam, and a non-owner gets InvalidIpamPoolId.NotFound or InvalidIpamId.NotFound, which is what AWS returns for a resource you cannot act on. ReleaseIpamPoolAllocation stays on the cross-account path, since no per-allocation caller is tracked to check ownership against.

The delegated administrator is stored organization-wide rather than per account, so every member account reads the same value and delegating a second, different account conflicts with InvalidParameterValue no matter which account asks.

Omitting a required identifier is a modeled MissingParameter rather than a not-found: this covers IpamPoolId on every pool operation, and IpamScopeId on CreateIpamPool. A CreateIpamPool naming a scope no IPAM owns is rejected with InvalidIpamScopeId.NotFound instead of storing a pool with a null ipamId.

State is reported settled rather than transitional, as elsewhere in this service: IPAMs and pools come back create-complete immediately and delete-complete on deletion, with no intermediate states. DeleteIpam cascades to the IPAM's pools, which real AWS requires --cascade to do. BYOASN associations are stored and echoed but nothing validates the ASN or advertises it.

Configuration

Environment variable Default Description
FLOCI_SERVICES_EC2_IMDS_PORT 9169 Host port for the IMDS server
FLOCI_SERVICES_EC2_SSH_PORT_RANGE_START 2200 Start of SSH host port range
FLOCI_SERVICES_EC2_SSH_PORT_RANGE_END 2299 End of SSH host port range
FLOCI_SERVICES_EC2_PUBLISH_SECURITY_GROUP_PORTS true Publish security-group TCP ingress ports on the host via socat sidecars
FLOCI_SERVICES_EC2_APP_PORT_RANGE_START 30000 Start of the host-port range for published app ports
FLOCI_SERVICES_EC2_APP_PORT_RANGE_END 30999 End of the host-port range for published app ports
FLOCI_SERVICES_EC2_MAX_PUBLISHED_PORTS_PER_INSTANCE 20 Max published ports per instance; also the widest single-rule span published
FLOCI_SERVICES_EC2_SOCAT_IMAGE alpine/socat Image used for the port-forwarding sidecar
FLOCI_SERVICES_EC2_MOCK false Skip Docker; instances jump directly to final state (useful for tests)
FLOCI_SERVICES_EC2_AWS_FAITHFUL_PRIVATE_IP false Report the CFN/subnet-allocated private IP instead of the container bridge IP; routing and IMDS are unaffected
FLOCI_SERVICES_EC2_CONTAINER_IPS_ROUTABLE auto-detect Whether an instance's container IP is reachable from the machines consuming Floci's API (Terraform, Terratest, your shell). When it is, DescribeInstances and DescribeAddresses report the container IP, so port 22 really is port 22; when it is not, they report 127.0.0.1 and reachability goes through the published high host ports. Detected by a throwaway TCP connect; set explicitly when Floci itself runs as a container
FLOCI_SERVICES_EC2_RECONCILE_CONTAINERS_ON_STARTUP true On startup, remove instance containers this Floci left on the daemon whose record did not survive the restart or came back terminated; stopped instances are never swept
FLOCI_SERVICES_EC2_VPC_NETWORKS_ENABLED true Back each VPC with a real Docker network (see VPC Docker networks)
FLOCI_SERVICES_EC2_VPC_NETWORKS_FALLBACK_POOL 10.240.0.0/12 RFC 1918 pool that substituted CIDRs are drawn from
FLOCI_SERVICES_EC2_VPC_NETWORKS_FALLBACK_PREFIX_LENGTH 16 Prefix length of each block handed out of that pool
FLOCI_SERVICES_EC2_VPC_NETWORKS_RECONCILE_ON_STARTUP true Remove VPC networks left behind by a previous run of this same emulator
FLOCI_SERVICES_EC2_VPC_NETWORKS_DRIVER bridge Docker network driver used for VPC networks

VPC Docker networks

Each VPC is backed by a real Docker network, created lazily when the first instance in that VPC launches. An instance's reported private IP is then an address its container actually holds, drawn from the subnet CIDR the caller declared, not a plausible-looking number. Instances in the same VPC reach each other at those addresses; instances in different VPCs sit on different bridges.

One network per VPC, not per subnet: subnets inside a VPC route to each other in AWS, so a network per subnet would manufacture a partition AWS does not have. Per-subnet addressing is kept anyway: the network's IPAM pool is the whole VPC CIDR and each subnet allocates static addresses out of its own slice of it.

The declared CIDR is used verbatim whenever it can be. It cannot be when it is absent, malformed, outside RFC 1918, or already claimed on the Docker daemon, including by another Floci VPC, since two VPCs may legally declare the same CIDR in AWS but one daemon cannot route two identical ranges. Only then is an equivalent block taken from fallback-pool, and the substitution is logged at WARN: a reported private IP that does not mean what the caller declared is either true or it is in the log.

Limits worth knowing before reading a passing test as evidence:

  • Security groups and NACLs are not enforced by this. Within one Docker network every container reaches every other on every port.
  • Between-VPC isolation is the daemon's, not Floci's. It comes from Docker's own DOCKER-ISOLATION-STAGE rules. OrbStack does not apply them: measured on OrbStack 29.4.0, two containers on separate networks reach each other in both directions, --internal included. On such a host the VPC boundary is an addressing boundary only.
  • On Docker Desktop for macOS and Windows container addresses do not answer from the host. The address is real and reachable container-to-container, but a host-side client cannot dial it. This is why SSH keeps its published host port.

Set FLOCI_SERVICES_EC2_VPC_NETWORKS_ENABLED=false to go back to synthesised private addresses and the shared default bridge. It is also off whenever mock is on.

Requirements

EC2 requires the Docker socket to be accessible (same as Lambda, ECS, and other container services):

services:
  floci:
    image: floci/floci:latest
    ports:
      - "4566:4566"
      - "9169:9169"   # IMDS — expose if containers need to reach it externally
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

The IMDS port (9169) only needs to be published if you are running EC2 containers outside the default Docker bridge network.

Examples

export AWS_ENDPOINT_URL=http://localhost:4566

# Import an SSH key pair for injection at launch
aws ec2 import-key-pair \
  --key-name my-key \
  --public-key-material fileb://~/.ssh/id_rsa.pub \
  --endpoint-url $AWS_ENDPOINT_URL

# Launch a real Docker container instance with UserData
aws ec2 run-instances \
  --image-id ami-amazonlinux2023 \
  --instance-type t2.micro \
  --min-count 1 \
  --max-count 1 \
  --key-name my-key \
  --user-data '#!/bin/bash
yum install -y nginx
systemctl start nginx' \
  --endpoint-url $AWS_ENDPOINT_URL

# Launch with an IAM instance profile (credentials served via IMDS)
aws ec2 run-instances \
  --image-id ami-amazonlinux2023 \
  --instance-type t2.micro \
  --min-count 1 \
  --max-count 1 \
  --iam-instance-profile Arn=arn:aws:iam::000000000000:instance-profile/my-app-role \
  --endpoint-url $AWS_ENDPOINT_URL

# Describe running instances
aws ec2 describe-instances \
  --filters "Name=instance-state-name,Values=running" \
  --endpoint-url $AWS_ENDPOINT_URL

# Stop and start an instance
aws ec2 stop-instances --instance-ids i-XXXXX --endpoint-url $AWS_ENDPOINT_URL
aws ec2 start-instances --instance-ids i-XXXXX --endpoint-url $AWS_ENDPOINT_URL

# Terminate an instance
aws ec2 terminate-instances --instance-ids i-XXXXX --endpoint-url $AWS_ENDPOINT_URL

# Create a VPC and subnet
aws ec2 create-vpc --cidr-block 10.0.0.0/16 --endpoint-url $AWS_ENDPOINT_URL
aws ec2 create-subnet --vpc-id vpc-XXXXX --cidr-block 10.0.1.0/24 --endpoint-url $AWS_ENDPOINT_URL

# Create and configure a security group
aws ec2 create-security-group \
  --group-name my-sg \
  --description "My security group" \
  --vpc-id vpc-XXXXX \
  --endpoint-url $AWS_ENDPOINT_URL

aws ec2 authorize-security-group-ingress \
  --group-id sg-XXXXX \
  --protocol tcp \
  --port 22 \
  --cidr 0.0.0.0/0 \
  --endpoint-url $AWS_ENDPOINT_URL

# Allocate and associate an Elastic IP
aws ec2 allocate-address --domain vpc --endpoint-url $AWS_ENDPOINT_URL
aws ec2 associate-address \
  --allocation-id eipalloc-XXXXX \
  --instance-id i-XXXXX \
  --endpoint-url $AWS_ENDPOINT_URL

Notes

  • DescribeImages returns AMIs from the EC2 image catalog, including common AMIs and Floci-native AMI IDs.
  • Security group rules are not enforced as a firewall (Docker bridge networking handles routing), but TCP ingress rules opened to a CIDR source are published on the host via socat sidecars so the instance's app is reachable from localhost — see Security Group Port Publishing.
  • The IMDS server identifies which instance is calling via IMDSv2 tokens (mapped at token issuance time) or by the container's bridge IP for IMDSv1.