# terraform-aws-ec2-instance: one module for single, many and spot instances

> The terraform-aws-ec2-instance module wraps an EC2 instance with the resources it usually needs, and its README spends more space on what Terraform cannot do than on what the module does. The interesting parts are the for_each pattern, the conditional creation flags, and an honest account of the AMI replacement problem.

**terraform-aws-modules/terraform-aws-ec2-instance** — Terraform module to create AWS EC2 instance(s) resources 🇺🇦

- Repository: https://github.com/terraform-aws-modules/terraform-aws-ec2-instance
- Website: https://registry.terraform.io/modules/terraform-aws-modules/ec2-instance/aws
- Stars: 814 · Forks: 1,961
- Language: HCL
- License: Apache-2.0
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/terraform-aws-modules-terraform-aws-ec2-instance

## What the module actually wraps

This is a Terraform module that creates an EC2 instance on AWS, published to the Terraform Registry under the name terraform-aws-modules/ec2-instance/aws. The licence is Apache-2.0, the code is HCL, and the last push to the repository was on 2026-09-25, with v6.4.1 published on 2026-09-18 after v6.4.0 in March 2026 and v6.3.0 in February 2026.

The resources it manages are the ones that normally surround an instance. The generated documentation lists aws_ebs_volume.this, aws_ec2_tag.spot_instance, aws_eip.this, aws_iam_instance_profile.this and aws_iam_role.this, and the module has no nested modules of its own, so everything happens in one flat set of resources rather than a tree of calls.

That flatness is the design point. Most of the value is not in the EC2 instance itself, which is a dozen lines of arguments in any Terraform configuration, but in the surrounding resources that people tend to get subtly wrong or forget: the instance profile and its role, the security group, the optional elastic IP, the optional EBS volume, and the tagging of a spot instance.

The audience is a team with more than one environment and more than one instance shape, where consistency matters more than the last line of customisation. A single instance in a personal sandbox does not need a module for this.

## The single instance call is short

The basic usage is a module block with a source, a name, an instance type, a key pair, a subnet and some tags:

```hcl
module "ec2_instance" {
  source  = "terraform-aws-modules/ec2-instance/aws"

  name = "single-instance"

  instance_type = "t3.micro"
  key_name      = "user1"
  monitoring    = true
  subnet_id     = "subnet-eddcdzz4"

  tags = {
    Terraform   = "true"
    Environment = "dev"
  }
}
```

Two details in that block are worth noticing because they are conventions rather than requirements. The source line has no version constraint, so a fresh call tracks the latest release of the module, which for a module with breaking changes in its minor versions is a decision to make deliberately rather than by default. The second is the tag with Terraform set to true, which exists so that a later sweep can find and destroy resources that were created by Terraform.

The registry entry is where the full input list lives, and the module does not document a required version pin in the README itself. Pinning is one argument, and the compliance.tf section later in the README explains how the source line changes when rules are applied, which is worth reading before you copy any source line from elsewhere.

## for_each, not count, for more than one instance

The multiple instance example uses for_each with a set of keys, and derives the name from each key:

```hcl
  for_each = toset(["one", "two", "three"])

  name = "instance-${each.key}"

  instance_type = "t3.micro"
  key_name      = "user1"
  monitoring    = true
  subnet_id     = "subnet-eddcdzz4"

  tags = {
    Terraform   = "true"
    Environment = "dev"
  }
```

The toset call matters more than it looks. Terraform's for_each over a list gives you integer indexes, which means that removing the first element from the list renumbers every subsequent instance and the state addresses shift, so Terraform wants to destroy and recreate resources that did not really change. Over a set of string keys, the address of each instance is tied to the key itself, so you can add and remove members without the rest moving.

The spot instance variant is the same block with three extra arguments, create_spot_instance set to true, spot_price set to a quoted string, and spot_type set to persistent:

```hcl
  name = "spot-instance"

  create_spot_instance = true
  spot_price           = "0.60"
  spot_type            = "persistent"

  instance_type = "t3.micro"
  key_name      = "user1"
  monitoring    = true
  subnet_id     = "subnet-eddcdzz4"
```

Note that spot_price is a string rather than a number, which is the sort of detail that fails at apply time rather than at plan time if you guess it.

## The AMI replacement problem, and who owns it

The most substantial part of the README is a section titled Known limitations, and it opens by saying these requests cannot be implemented by this module, or by any module.

The underlying constraint is that Terraform requires lifecycle arguments to be literal values inside the resource block, which means a lifecycle setting cannot be computed from a variable. The README cites the long-running upstream issues, hashicorp/terraform#18367 open since 2018, #22544 since 2019, and opentofu/opentofu#1329 as the same request in OpenTofu.

The concrete symptom most teams hit is this: Terraform wants to replace the EC2 instance because the AMI changed. The native options given are to pin the AMI id, or to set ignore_ami_changes = true, with a warning that an existing instance moves to a new state address, so you add a moved block.

The module's answer to this is compliance.tf, a service that serves the module with those rules applied at download time. The important operational detail is that inputs and outputs do not change and only the source line does, and that you pin a release by dropping the version argument and adding &version= with the release number to the URL:

```hcl
source = "https://registry.compliance.tf/terraform-aws-modules/ec2-instance/aws?add_rules=lifecycle_ignore_ami_changes"
```

There is a rules playground for previewing the diff without an account, and the README discloses that the known limitations text was written by the module's maintainer, who also builds compliance.tf. The disclosure is there because it is a commercial product being recommended by the person who maintains the module, and reading it is part of evaluating the recommendation rather than an attack on it.

## Encrypted AMIs need a separate recipe

One feature is explicitly out of scope, and the README handles it honestly.

The module does not support encrypted AMIs out of the box. The suggested workaround is to generate one, and the example that follows creates an encrypted image from the latest Ubuntu 20.04 base image. The recipe has three parts: a data source that finds the most recent Ubuntu image for a given owner with a name filter and a virtualization-type filter, an aws_ami_copy resource that copies it with encrypted set to true and a source_ami_region, and a second data source that looks the copy up again by owner self.

```hcl
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["679593333241"]

  filter {
    name   = "name"
    values = ["ubuntu-minimal/images/hvm-ssd/ubuntu-focal-20.04-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}
```

The aws_ami_copy step is the part that creates the encrypted artefact, with a description built from the source image id, a name of ubuntu-encrypted-ami, and its own tag. A team with a compliance requirement for encrypted volumes therefore has two modules to compose, and a second data source lookup, rather than one switch to flip.

## Conditional creation is a set of flags, not a count

The module has a family of create_ inputs, and the conditional creation section shows the combinations that are supported.

There is a top level create flag that disables creation of the EC2 instance and all resources, and then separate flags for the spot instance, the IAM instance profile, the security group and the elastic IP. The point of having them separately is that a common pattern is a create = false switch for a single environment in a shared configuration, with the rest of the resources still created for that environment.

The example block shows create set to false, create_spot_instance set to true, create_iam_instance_profile set to true, create_security_group set to false, and create_eip set to true, with the rest of the block omitted from the documentation.

The two complete examples linked from the README are the complete EC2 instance example, which is where the full input surface is visible in practice, and an example of EC2 instance with private network access via Session Manager, which covers the case of an instance that should not need inbound SSH at all.

The requirements are stated in the generated documentation: Terraform 1.5.7 or newer and the AWS provider at 6.37 or newer. That provider floor is a meaningful constraint, since it is a recent major version of the AWS provider and teams on older ones cannot simply bump it without a plan of their own.

## Two notes that will cost you an afternoon if you skip them

The Notes section is short and both items are the kind of constraint that only shows up as a failed apply.

The first is that network_interface cannot be specified together with vpc_security_group_ids, associate_public_ip_address and subnet_id. That is a mutual exclusion baked into how the underlying resource behaves, and the README points at the complete example for the details of the supported combinations. If you need a custom network interface with a different set of security groups, that combination is not available through this module, and the fix is either to change your network design or to not use the module for that instance.

The second is about spot instances and encryption. You must grant the AWSServiceRoleForEC2Spot service-linked role access to any custom KMS keys, otherwise the spot request and the instances fail with bad parameters. The README also gives you the debugging step, using the awscli with aws ec2 describe-spot-instance-requests to see why a request failed, which is a good reminder that a bare error string from a spot request usually means a permission problem rather than a capacity problem.

As for maintenance, the module is released with semantic release, evidenced by the .releaserc.json and the CHANGELOG.md in the tree, and the six minor version bumps a year are the visible cost of a module that tracks a fast moving provider. The repository also carries a .pre-commit-config.yaml, a docs directory, a wrappers directory and the two examples, and the licence is Apache-2.0 with the text in the LICENSE file.

## Conclusion

terraform-aws-ec2-instance suits a team that wants EC2 instances with IAM profile, security group, elastic IP and optional EBS volume created consistently from one call, and it is the wrong choice if you need encrypted AMIs out of the box, since the module does not support them and the README shows a separate recipe for building one. Verify first which Terraform and AWS provider versions you are on, because the module requires Terraform 1.5.7 or newer and the AWS provider 6.37 or newer, and decide how you will handle the AMI replacement problem before your first apply, since that is the failure mode the module's own documentation spends the most space on.

## FAQ

### How do I create several EC2 instances with terraform-aws-ec2-instance?

Use for_each over a set of string keys, for example for_each = toset(["one", "two", "three"]), and derive the name from each.key. A set of keys rather than a list keeps the state address of each instance tied to its key, so adding or removing one does not renumber the others.

### How do I stop Terraform from replacing my instance when the AMI changes?

Pin the AMI id, or set ignore_ami_changes = true, in which case an existing instance moves to a new state address and you need to add a moved block. The module's README also documents serving the module through compliance.tf with the lifecycle_ignore_ami_changes rule applied at download time.

### Does terraform-aws-ec2-instance support encrypted AMIs?

Not out of the box. The README shows a separate recipe that finds the base image with an aws_ami data source, copies it with aws_ami_copy and encrypted set to true, and then looks the copy up again with a second data source filtered on owner self.

### Which Terraform and AWS provider versions does the module need?

The generated documentation lists Terraform 1.5.7 or newer and the AWS provider at 6.37 or newer. Recent releases include v6.4.1 on 2026-09-18, v6.4.0 on 2026-03-26 and v6.3.0 on 2026-02-27.

### Why does my spot instance request fail with bad parameters?

The README states that you must grant the AWSServiceRoleForEC2Spot service-linked role access to any custom KMS keys, otherwise the spot request and the instances fail with bad parameters. You can inspect the reason with the awscli using aws ec2 describe-spot-instance-requests.

## Sources

- [License: Apache-2.0](https://github.com/terraform-aws-modules/terraform-aws-ec2-instance/blob/master/LICENSE)
- [Project website](https://registry.terraform.io/modules/terraform-aws-modules/ec2-instance/aws)
- [README](https://github.com/terraform-aws-modules/terraform-aws-ec2-instance/blob/master/README.md)
- [Releases](https://github.com/terraform-aws-modules/terraform-aws-ec2-instance/releases)
- [terraform-aws-modules/terraform-aws-ec2-instance on GitHub](https://github.com/terraform-aws-modules/terraform-aws-ec2-instance)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/terraform-aws-modules-terraform-aws-ec2-instance
