# Converting a QCOW2 image to an AWS AMI

> A step-by-step guide to turning a QCOW2 disk built under QEMU/KVM into a bootable EC2 AMI using VM Import/Export — with the IAM, format, and regional gotchas that trip people up.

_1 July 2026 · The Primcoat Team_

If you build VM images on Linux, you almost certainly build them in [QCOW2](https://primcoat.app/glossary/qcow2) — it is QEMU's native format and what the whole KVM toolchain produces. If you deploy on AWS, you need an [AMI](https://primcoat.app/glossary/ami). Getting from one to the other is not hard, but it has enough sharp edges that it is worth walking through end to end.

## AWS does not import QCOW2

The first thing to know is that **AWS VM Import/Export does not accept QCOW2**. The supported disk formats are RAW, VHD, and VMDK (and OVA for full appliances). So the first step is always a conversion.

QEMU ships the tool for it:

```bash
qemu-img convert -f qcow2 -O raw image.qcow2 image.raw
```

RAW is the safest target for import. It is an unstructured, byte-for-byte disk dump — large on disk, but there is nothing for the import process to misinterpret. If disk size or upload time matters, VHD is a more compact option (`-O vpc` in `qemu-img`, which is the format's historical name in QEMU).

## Step 1: the vmimport IAM role

This is the step that stops most first attempts. VM Import/Export runs as an AWS service that needs permission to read your uploaded disk from S3 and create an EBS snapshot from it. That permission is granted through a specifically named IAM role called `vmimport`, with a trust policy that lets the VM Import service assume it.

You create the role once per account, with a trust relationship scoped to `vmie.amazonaws.com`, and a policy granting it read access to your import bucket and the EC2 snapshot and import actions. AWS documents the exact JSON; the thing to internalize is that a `disk validation failed` or `permission` error on import almost always traces back to this role being missing or too narrowly scoped, not to the disk.

## Step 2: upload to S3

```bash
aws s3 cp image.raw s3://my-import-bucket/image.raw
```

The bucket must be in the same region you intend to import into. Cross-region adds cost and latency for no benefit.

## Step 3: import the disk

Point `import-image` at the uploaded object. A small JSON file describes the container:

```json
[
  {
    "Description": "hardened-ubuntu-2404",
    "Format": "raw",
    "UserBucket": {
      "S3Bucket": "my-import-bucket",
      "S3Key": "image.raw"
    }
  }
]
```

```bash
aws ec2 import-image \
  --description "hardened-ubuntu-2404" \
  --disk-containers file://containers.json
```

This returns an `ImportTaskId`. The import is asynchronous and not fast — a large disk takes a while to convert and snapshot. Poll it:

```bash
aws ec2 describe-import-image-tasks --import-task-ids import-ami-0abc123
```

When `Status` reaches `completed`, the task output includes the new AMI ID.

## The gotchas that actually cost time

**Imports are rate-limited.** AWS allows a default of 5 concurrent import tasks per region, raisable to 20 through a support request. If you are publishing an image to many regions, you cannot fire all the imports at once; they have to queue against that limit. Any automation you build around this has to respect it, or AWS will reject the excess.

**AMIs are regional.** The AMI that import produces exists in exactly one region. To run it elsewhere you `copy-image` to each target region, and every copy gets a *different* AMI ID. Anything downstream that references the AMI — a launch template, an autoscaling group, a Terraform module — has to be parameterized per region. This surprises people who expect an image to be a global object; on AWS it is not.

**UEFI vs BIOS boot mode.** Modern images often expect UEFI. `import-image` infers a boot mode, but you can set it explicitly with `--boot-mode uefi`. A mismatch produces an AMI that imports fine and then will not boot, which is a slow way to discover the problem.

**Windows is a one-way door.** If you import a Windows image, AWS applies its own licensing on the way in, and the result **cannot be exported back out of EC2**. That makes EC2 a terminal destination for a Windows build, not a staging point you re-publish from. Build Windows once and publish outward to each destination — do not plan to move it cloud-to-cloud afterward.

## Doing it repeatably

For a single image, the sequence above is an afternoon. The friction shows up when it becomes routine: every new build re-runs the conversion, the upload, the import, and the multi-region copy, all while staying under the concurrency limit and keeping track of which AMI ID landed in which region. And the AMI is only half the artifact — the [SBOM](https://primcoat.app/glossary/sbom), the [CVE](https://primcoat.app/glossary/cve) scan, and the [signature](https://primcoat.app/glossary/cosign) that describe what is actually in it live somewhere else unless you deliberately keep them together.

That publish-and-track loop is what [Primcoat's AWS integration](https://primcoat.app/integrations/aws) handles: it converts the hardened master to RAW, imports it, copies it to every region you target while respecting the import limit, and keeps the evidence attached to the build that produced each AMI. The commands here are worth understanding either way — but if you are running them on every release, that is a pipeline, not a task.

---

Source: https://primcoat.app/blog/qcow2-to-aws-ami
Primcoat — a managed VM image factory. https://primcoat.app
