AMIs with a contract: anatomy of EC2's new instance-type block
Listen to article
generated on playGenerated only on first play
Powered by Amazon Polly + OmniVoice
On September 4, 2026 EC2 started accepting an `InstanceTypeSpecification` on an AMI: two lists, evaluated at `RunInstances`, that block launches on incompatible instance types. The pattern moves compatibility from the caller to the artifact — and it ships with a trap most teams will only find the hard way: the specification matches strings, not hardware capabilities.
Um AMI sempre foi um contrato verbal. O artefato sabe exatamente de que hardware ele precisa — driver ENA, NVMe, arquitetura do binário, extensão de CPU que a JVM compilou — e nada disso estava escrito em lugar nenhum que o RunInstances pudesse ler. A partir de 4 de setembro de 2026 dá para escrever: o dono do AMI declara SupportedInstanceTypes, UnsupportedInstanceTypes ou os dois, e o EC2 bloqueia o lançamento fora da lista. Sem custo adicional, em todas as regiões. A leitura fácil é "menos launch quebrado". A leitura que interessa é outra — o ponto de enforcement mudou de lugar, e com ele mudou quem paga a conta da manutenção.
The problem: the contract existed, it just wasn't executable
Every platform team that publishes golden AMIs has lived this scene. The hardened image was built for x86_64, with a statically compiled EDR agent and a kernel that only loads its network driver on a Nitro host. Someone creates an Auto Scaling group with attribute-based instance type selection asking for 4 vCPUs and 16 GiB, leaves Spot diversification wide — which is the correct recommendation for filling capacity — and EC2 picks a Graviton family because it matches the attributes and is the cheapest right now.
The instance comes up. That detail is what makes the failure mode hurt. It doesn't die at RunInstances; it dies later, with no console, no network, nothing but a health check running out. The ASG waits the default 300-second grace period, marks the instance unhealthy, terminates it and tries again — and since the allocation decision hasn't changed, it picks the same pool. You get a launch/terminate loop billed at a 60-second minimum per instance created, an alarm firing at 2 AM, and a graph that looks exactly like a capacity shortage.
Every defense available lived on the caller side: an ec2:InstanceType condition in an IAM policy, an SCP at the Organization, ExcludedInstanceTypes inside a fleet's InstanceRequirements. All correct, all sharing one structural defect — they describe what that principal may launch, not what that image can survive. Share the AMI with 40 accounts and you replicate the rule 40 times, in 40 places that drift apart.
The contract travels with the artifact — and fails at the gate, not at the health check
Reference design: the pipeline derives the list from a capability query, writes the specification onto the AMI, and RunInstances becomes the single evaluation point for every account that received the image.
- EC2 Image Builder · pipeline do golden AMI
- DescribeInstanceTypes · filtro por arquitetura + hypervisor
- ReplaceImageInstanceTypeSpecification · lista explícita, gerada
- AMI · InstanceTypeSpecification
- CopyImage / share · especificação preservada
- RunInstances · avalia tipo × contrato
- Auto Scaling group · ABIS + Spot diversificado
- Launch template · versão do AMI fixada
- Instância lançada · tipo dentro do contrato
- InvalidParameterCombination · lançamento bloqueado
- CloudTrail · errorCode em RunInstances
- Scaling activity · falha registrada no ASG
- CloudWatch · metric filter por imageId
Anatomy: two lists, one evaluation order, zero state
The attribute is InstanceTypeSpecification and it carries SupportedInstanceTypes and UnsupportedInstanceTypes. The logic is short enough to fit in an ADR: with no specification, everything passes (that's the default, and that's why nothing broke on the 4th); with only the unsupported list, everything passes except what matches; with the supported list set, the type must match the supported list and must not match the unsupported one. Denial always wins.
You write it with aws ec2 replace-image-instance-type-specification --image-id ami-... --instance-type-specification '{"SupportedInstanceTypes": ["t3.","a2."], "UnsupportedInstanceTypes": ["t3.micro"]}'. Omitting the parameter removes the restriction. You read it back through describe-images, which returns the InstanceTypeSpecification field with the patterns as stored. A blocked launch returns InvalidParameterCombination from RunInstances, with a message pointing at DescribeImages as the place to look.
Four properties change the design, and none of them are in the announcement:
Replace, not Modify: the operation swaps the whole specification. There is no add or remove — it's read-modify-write with no conditional version. Two pipelines patching the same image in parallel produce last-writer-wins silently, and the loser only finds out when a legitimate launch gets blocked.
Only the owner writes. An account that received the shared AMI consumes the contract and does not negotiate it.
CopyImage preserves the specification. That's what makes the pattern worth it in a multi-Region topology: you declare once and the sa-east-1 replica is born with the contract.
Marketplace is mutually exclusive. You can't set a specification on an AMI listed in AWS Marketplace, and you can't list an AMI that already has one. If you're an ISV shipping an appliance, this feature does not exist for you.
Three ways to constrain instance type — and what each one actually covers
| IAM / SCP `ec2:InstanceType` condition | `ExcludedInstanceTypes` in ABIS | `InstanceTypeSpecification` on the AMI | |
|---|---|---|---|
| Where the rule lives | In the principal's policy, per account or OU | In the fleet or ASG configuration | On the artifact, once, in the owning account |
| Applies to launches from another account | No — that policy is theirs, not yours | No — the consumer configures the fleet | Yes — EC2 evaluates it regardless of caller |
| Survives CopyImage and sharing | Irrelevant — it doesn't follow the image | Irrelevant — it doesn't follow the image | Yes, preserved on copy |
| When the failure shows up | UnauthorizedOperation at launch | Never — the type simply isn't chosen | InvalidParameterCombination at launch |
| Maintenance cost | High: replicate and audit across N accounts | Medium: one edit per fleet that exists | Low per place, high per hardware generation |
Where the glob betrays you: a string is not an architecture predicate
Both lists accept . The docs give the examples: t3. matches the whole t3 family, g5. matches g5, .12xlarge matches any 12xlarge in any family, *xlarge matches any type xlarge or larger. It looks expressive. This is where I'd stop and ask the question that matters: are you describing hardware, or are you describing names?
What the AMI actually requires is a capability — arm64 versus x86_64, Nitro versus Xen hypervisor, presence of a GPU, presence of local NVMe. What the specification evaluates is a pattern over the instance type's string. The two coincide today because AWS names Graviton families with a g and local-storage families with a d. They coincide by convention, and a naming convention is not a contract: c6gn, c7gd, m7i-flex, u7in-32tb and mac2-m2pro already show a scheme full of exceptions, suffixes and infixes that no simple glob covers. A g. written to exclude Graviton is a heuristic — and a hand-written heuristic, in a file nobody revisits, is debt that accrues interest.
The second trap is temporal, and it's the expensive one. An explicit allow-list blocks every future generation by construction. New families reach new Regions constantly; the day the next generation shows up in your Spot pool, the fleet you deliberately left wide — because wide diversification is the correct practice for Spot capacity — will skip exactly the cheapest, most available family. You get no error: the ASG simply doesn't choose it, price creeps up, interruption rate creeps up, and nobody connects that to a JSON written eight months ago. It's the same tension behind OnDemandMaxPricePercentageOverLowestPrice, which ships with a 20% default headroom precisely so it doesn't narrow your pool without telling you.
Generate the list, don't write the list
The only way to keep the contract honest is to derive it from a capability query in the same build that produced the image: aws ec2 describe-instance-types --filters Name=processor-info.supported-architectures,Values=x86_64 Name=hypervisor,Values=nitro --query 'InstanceTypes[].InstanceType', feeding the result straight into replace-image-instance-type-specification. For fleets that already think in attributes, GetInstanceTypesFromInstanceRequirements plays the same role and lets you preview the set before writing it. The list ends up explicit, verbose and ugly — which is exactly what you want, because it is regenerated on every build, and your lag behind the AWS catalog becomes your pipeline cadence instead of the memory of whoever wrote the glob.
The hard block on incident day — and the signal nobody instruments
The documentation is explicit: EC2 enforces the specification as a hard block, and launch templates and Auto Scaling groups that reference the AMI can fail if the configured instance type isn't allowed. That isn't an implementation detail — it's the pattern's central trade. You converted a slow, expensive failure (zombie instance, health check, loop) into a fast, cheap one (InvalidParameterCombination at RunInstances). Fast and cheap wins on 95% of days. On the other 5%, you're in the middle of a zonal capacity event, price-capacity-optimized has finally found a pool with room, and your contract says no.
In financial systems that has a name: you traded correctness risk for availability risk, and you did it on purpose. When the AMI genuinely won't boot on that hardware, the trade is trivially good — the instance was never going to serve traffic. When the list is merely stale, you invented an unavailability mode that didn't exist before, in exactly the window where elasticity matters. That's why a generated allow-list and a minimal deny-list aren't aesthetic preferences: one errs toward letting through, the other errs toward blocking.
The hole is observability. The specification doesn't affect running instances, emits no metric of its own, and shows up on no dashboard by default. The auditable signal is the RunInstances event in CloudTrail carrying an errorCode — build a metric filter grouped by requestParameters.imageId and alarm on any block, because in steady state the expected rate is zero. A recurring block isn't the control working; it's a launch template pointing at an AMI whose contract changed underneath it. And add the read side to the runbook: describe-images with its InstanceTypeSpecification field is the first thing to check when a launch fails on a parameter — the error message itself sends you there, and whoever is on call at 2 AM needs to know that before opening a support case.
How to get this wrong
- Applying the contract to an already-shared AMI without an inventory of consumers: you can't see the launch templates in the other 40 accounts, and the block only surfaces on their next scale-out — which is probably a traffic spike, not a deploy.
- Hand-writing the list: a human-curated glob is a snapshot of the instance catalog on the day it was written, and the catalog changes every week.
- Two pipelines writing the same AMI's specification:
ReplaceImageInstanceTypeSpecificationreplaces everything and has no conditional write; the second one erases the first with no error. - Using this as a cost control: blocking
*.24xlargeto contain spend is FinOps policy disguised as compatibility — that belongs in an SCP withec2:InstanceTypeor inOnDemandMaxPricePercentageOverLowestPrice, where exceptions have an owner and a process. - Mistaking this for pipeline validation: the contract fails at launch, not at build. It does not replace a test that boots the image on each allowed family before promoting the AMI to production.
Reading the pattern through the pillars
Operational excellence
The contract becomes generated code in the same pipeline as the AMI, versioned with it. Add describe-images to the runbook: the error message points there, and it's the first diagnostic step.
Security
Constraining the list is neither isolation nor least privilege — only the owner writes it and the rule follows the image, but it does not replace an IAM condition or an SCP for governing who launches what.
Reliability
A real gain by killing the launch/terminate loop from incompatible AMIs; a real risk of failing to scale during a capacity event if the list is stale. An alarm with an expected rate of zero is what separates the two.
Cost optimization
The feature costs nothing and cuts the waste of instances that booted only to die at the health check — but a narrow allow-list pushes the fleet into pricier pools while emitting no signal at all.
I'd turn this on first for the AMIs that carry a genuine physical limit — an arm64 image, an image whose driver only exists on Nitro, an inference image that assumes a GPU — and leave general-purpose AMIs alone, where the permissive default is already right. On every image I restrict, I'd generate the list from describe-instance-types inside the build and fail the pipeline if it comes back empty, because an empty list from a broken query blocks everything silently. And I'd put a zero-expected-rate alarm on the RunInstances errorCode in CloudTrail the same day — not next sprint. The lesson that cost me, on financial-grade platforms: any control that blocks something and doesn't count how often it blocked eventually becomes a root cause nobody can name. The block is the easy part; the counter is what saves you at 2 AM.
References
Verdict
Adopt it — narrowly scoped, with the list generated. Use InstanceTypeSpecification when three conditions hold together: the AMI has a real hardware incompatibility rather than a cost preference, you don't control every account that launches the image, and the pipeline that builds the AMI can derive the list from a capability query on every build. Missing any one of them, an ec2:InstanceType condition in an SCP remains the right tool — it errs toward people who know how to ask for an exception. Don't use this as a FinOps control, don't hand-write globs, and don't enable it on a shared AMI before running describe-images against the launch templates already pointing at it. The feature costs nothing and the permissive default guarantees nothing breaks until you act; the price is entirely maintenance, and it comes due every time AWS ships a new family. If your team has nowhere to hang that maintenance, the pattern is still correct and the answer is still no.
Architecture, AWS, AI and market deep dives — straight to your inbox. Free.
No spam · unsubscribe anytime
Ask Fernando about this
Get a focused answer about this article from my AI assistant, grounded in my work.
Join the conversation
Sign in to comment
Verify your email to join in — you'll also get the newsletter. No password.
Keep reading
Architecture intelligence, in your inbox
Curated signals and original analysis on AWS, AI, distributed systems and the market — the way a solutions architect reads them.
- Curated AWS · AI · architecture · market signals
- New architecture studies & deep-dives when they ship
- Sharp summaries — depth without the noise
- No spam · double opt-in · unsubscribe anytime