TL;DR: Amazon WorkSpaces Applications (formerly AppStream 2.0) will only import a Windows Server 2025 AMI that has UEFI boot and NitroTPM v2.0 – and EC2 Image Builder cannot produce one, because NitroTPM can only be set when an AMI is first registered. This post shows you how to close that gap with an event-driven chain of SSM Automations, so a single pipeline run ends with a hardened, streaming-ready image shared to every account that needs it. The Terraform is on GitHub.
Table of Contents
The challenge: streaming images are drifting from SOEs
Most organisations running virtual desktops or streamed applications on AWS end up maintaining two golden images that are supposed to be the same image.
There is the EC2 Standard Operating Environment: hardened, patched, encrypted, built by a pipeline, shared across the AWS Organization, and audited by someone who cares a great deal about CIS benchmarks. Then there is the WorkSpaces Applications base image, which – if you have ever built one – was probably created by launching an image builder in the console, RDP-ing in, installing things by hand, and clicking “Snapshot”. It works. It is also undocumented, unreproducible, and three patch cycles behind the SOE by the time anyone notices.
That gap is not laziness. It exists because the WorkSpaces Applications image path genuinely does not fit into a normal AMI factory, and the reason is a single line in the AWS documentation.
Reason: a TPM support that EC2 image builder pipeline doesn’t natively support
To import an existing EC2 AMI into WorkSpaces Applications, AWS requires the image to be Windows Server 2022 or 2025 Full Base, unencrypted, account-owned (Marketplace AMIs won’t work), on a single gp2 or gp3 volume of 500 GB or less, with ports 8000, 8300 and 8443 free – and, for Windows Server 2025, UEFI boot mode with NitroTPM v2.0. Read more here.
If those are new to you: UEFI is the modern replacement for the legacy BIOS firmware that starts the machine, and NitroTPM is AWS’s virtual Trusted Platform Module which is the tamper-resistant key store Windows uses for features like BitLocker, Credential Guard and measured boot. NitroTPM only runs on UEFI images, which is why the two are always quoted as a pair.
That last requirement is where automation falls over, for two reasons:
- NitroTPM can only be set at RegisterImage time. The AWS documentation is unambiguous: “You must configure your AMI with NitroTPM support when you register it. You can’t configure NitroTPM support later on.” There is no ModifyImageAttribute call that adds it.
- EC2 Image Builder has no TPM setting. CreateImageRecipe and AmiDistributionConfiguration doesn’t support tpmSupport or bootMode anywhere. Image Builder’s output AMI inherits TPM support from its parent image, and when this post was written, no configuration changes that.
The obvious workaround – start from a TPM-enabled base – sounds fine until you try it: if your base is an AWS Marketplace AMI, its snapshots are vendor-owned and cannot be copied, so you cannot re-register anything from them. The logical order has to be inverted. Build first, then re-register.
Solution: one module, one chain, no clicking
The pattern we landed on is a single Terraform module that builds both images, plus three event-driven stages that turn the streaming build into a shareable WorkSpaces Applications image. Nothing is scheduled, nothing polls, and no human touches the console after the pipeline runs.
The shape looks like this:
- Build. An EC2 Image Builder pipeline builds Windows Server 2025 from the AWS-managed base, applies the same STIG hardening component as your EC2 SOE, a recipe to install agents, org certs and common libraries, then writes the output AMI ID into an SSM parameter.
- Re-register. Writing that parameter emits a Parameter Store Change event. An EventBridge rule catches it and starts an SSM Automation that copies the root snapshot and calls RegisterImage with BootMode = uefi and TpmSupport = v2.0. It writes the new AMI ID to a second parameter and deletes the pre-TPM source.
- Import and share. A second rule on that parameter starts another Automation, which calls CreateImportedImage, waits for AVAILABLE, shares the image with your consumer accounts via UpdateImagePermissions, publishes the image name for discovery, and prunes older images.
- Alert. A third rule watches for automation failures and pushes them to an SNS topic and out to email.
Each stage’s output parameter is the next stage’s trigger. There is no orchestrator, no Lambda, and no state machine to maintain – just three small Automations that each do one thing.
Outcomes: What you actually get
One hardening baseline, two outputs. Both Windows pipelines call the same module and run the same EC2 image pipeline recipe. When your security team asks whether the streaming desktops are hardened to the same standard as the servers, the answer is a Git diff rather than a promise.
Images that rebuild themselves. Point the pipeline at a schedule and every stage downstream does the rest. A patch Tuesday rebuild flows all the way through to a shared, dated Workspaces Applications image ready for consumption by application teams.
Streaming images are available through the same channels as the SOEs. Each stage publishes to a well-known SSM parameter. Member accounts read the latest SOE AMI ID from a RAM-shared parameter by ARN, and the latest streaming image name from a discovery parameter – no AMI name-matching, no wiki page of image IDs.
Retention policies. Image Builder’s native lifecycle policy cleans up the pipeline’s own images. The TPM and streaming images automations implements custom retention policies to clean up images in the same way EC2 image builder does.
How the chain works, stage by stage
The build module knows what AppStream needs
The interesting design decision is that the module takes a pipeline_type of either “ec2” or “appstream”, and that one variable enforces every constraint the import step will later impose. Setting it to “appstream” forces encryption off – imported AMIs cannot be encrypted – turns off organisation-wide sharing, restricts the recipe to a single root volume, and appends a prerequisites component that disables automatic Windows updates, removes the Remote Desktop Services roles, and validates the required ports, .NET version and EC2Launch v2 from inside the image.
Terraform preconditions catch the rest at plan time rather than forty minutes into a build:
precondition {
condition = !local.is_appstream || local.volume_size_gb <= 500
error_message = "WorkSpaces Applications import requires a root
volume of 500 GB or less."
} The TPM stage is a snapshot copy and a re-register
The Automation resolves the AMI ID from the parameter, copies its root snapshot (which is why the source must be account-owned rather than a Marketplace image), waits for the copy, and registers a new AMI from it with UEFI and TPM v2.0 set. It then publishes the new AMI ID and, by default, deletes the pre-TPM source so the account does not fill with intermediate artifacts.
Because it reads the parameter at runtime rather than at plan time, you can deploy this stage before the pipeline has ever run. It simply sits idle until the first build fires the event.
The import stage does what the console does
CreateImportedImage is the API behind the console’s “Import Image” button. The Automation calls it with a dated name, waits for the image to reach AVAILABLE, then shares it with the target accounts using allowImageBuilder = true and allowFleet = false – consumers build their own images on top of the shared base rather than streaming from it directly, which keeps fleet promotion a deliberate decision in each account.
Limitations
Parameter Store Change delivery is best-effort. AWS does not guarantee the event, so build a manual re-run path into your runbook and do not treat a missing image as a code bug until you have checked the EventBridge invocation metrics.
A failed import looks like a timeout. The aws:waitForAwsResourceProperty step only waits for the state you asked for. If the import fails, the step keeps waiting until it times out – so set the timeout to something you are willing to wait for, and make sure the failure alert is wired up.
EBS default encryption. If you’re using AWS LZA, you would know that EBS default encryption is enabled by default in all AWS accounts. Without turning it off, EC2 image piplelines will always produce encrypted AMIs using the account default KMS key. You should turn it off in the image builder account. So the best practice here is to have a dedicated image builder account.
Wrapping up
The gap between “we have an image pipeline” and “our streaming images come out of it too” is usually one awkward constraint, not an architectural problem. Once I realised that NitroTPM has to be applied after the build rather than during it, the rest is four small pieces of Terraform and three EventBridge rules.
What you get back is worth the effort: one hardened baseline instead of two, a rebuild that costs a pipeline run instead of an afternoon of RDP, and an audit answer you can point at rather than describe. If your organisation is running Workspaces Applications alongside an EC2 SOE, the two should come out of the same factory.
The full Terraform stack is on GitHub – clone it, read the module, and copy whatever is useful.
Frequently asked questions
Why can’t EC2 Image Builder just build a TPM-enabled AMI?
Because NitroTPM is only settable through the RegisterImage API, and Image Builder does not expose it in either the image recipe or the distribution configuration. The output AMI inherits TPM support from its parent image, so unless your parent already has it, the only way to get there is to re-register the output AMI from a copy of its snapshot.
Can I skip the re-registration by starting from a TPM-enabled base image?
Sometimes. If you own the base AMI and it already has UEFI and NitroTPM, Image Builder’s output will inherit both. But if your base is an AWS Marketplace image – a CIS-hardened base, for example – its snapshots are vendor-owned and cannot be copied, so you cannot produce a TPM-enabled derivative from it. Building first and re-registering afterwards works in both cases.
What are the actual requirements for importing an AMI into WorkSpaces Applications?
Windows Server 2022 or 2025 Full Base (Server Core and 2019 are not supported), unencrypted, owned by your account, x86_64, a single gp2 or gp3 root volume of 500 GB or less, ports 8000/8300/8443 free, .NET Framework 4.8 or later, EC2Launch v2 installed, and the Remote Desktop Services Session Host and Licensing roles not installed. Windows Server 2025 additionally requires UEFI boot mode and NitroTPM v2.0. Read more here.
Why use SSM parameters as the handoff between stages instead of Step Functions?
Because EC2 Image Builder already writes its output AMI ID to a Parameter Store parameter natively as part of distribution, and a parameter write is an EventBridge event. Using it as the trigger means the chain has no orchestration layer to deploy, version or debug – and the same parameters double as the discovery mechanism for consumer accounts. This design also makes room for further flexibility to branch out the automation at any stage. Read more here.
Is AppStream 2.0 the same thing as WorkSpaces Applications?
Yes. AWS rebranded Amazon AppStream 2.0 as Amazon WorkSpaces Applications in late 2025. The API, CLI and IAM namespaces are still “appstream”, so you will see both names in the console, the documentation and any Terraform you write.
Mostafa Mabrouk leads the Cloud Evolution practice at Cevo, helping enterprises build secure, scalable AWS platforms and migrate and modernise their workloads. He is passionate about enhancing developer experience through enabling self-services and creating repeatable solutions to complex cloud challenges.



