Terraform Create EC2 Instance on AWS - Step by Step (Provider v6, aws_ami Data Source, Key Pair, Security Group, user_data and Provisioners)



This guide will help you to create your first AWS EC2 instance using Terraform.

When it comes to IaC (Infrastructure as Code) Terraform is still the first choice of most DevOps teams, although there are alternatives such as Ansible, Pulumi, CloudFormation and the open-source fork OpenTofu, because -

  1. Terraform is really easy to install
  2. The provider documentation on the Terraform Registry is excellent
  3. It is widely adopted in the DevOps community, so every problem you hit has been hit before
  4. It has first-class support for every cloud - AWS, Google Cloud, Azure - with the same language

I wrote the first version of this post in 2021 against Terraform 0.14 and the AWS provider 3.32. In 2026 the workflow is the same, but a few things in the original were plain bad practice that I do not want anyone copying any more - root user access keys pasted into main.tf, a hard-coded AMI ID that only worked in one region, and a provisioner block floating outside its resource. All of that is fixed below, and the code is tested with Terraform 1.16 and AWS provider 6.x.

(To know how to do the same on Google Cloud - How to setup a Virtual Machine on Google Cloud Platform using Terraform)



Prerequisite

  1. You must install Terraform - any 1.x version works, I am using 1.16.
  2. The AWS CLI - not strictly required by Terraform, but aws configure is the easiest way to store credentials, and aws sts get-caller-identity is the quickest way to check them.

Table of Content

  1. Setup an AWS account
  2. Create an IAM user and access keys (not the root user)
  3. Configure the AWS credentials for Terraform (outside the code)
  4. Create your first Terraform infrastructure (main.tf)
  5. Terraform commands - init, plan, apply, destroy
  6. Add an SSH key pair and a security group so you can log in
  7. Setup a custom startup script for the EC2 instance with user_data
  8. Copy and execute a script with the file and remote-exec provisioners
  9. Common errors when creating an EC2 instance with Terraform
  10. Conclusion

The Terraform workflow for an EC2 instance - write main.tf, init, plan, apply, destroy


1. Setup an AWS account

As the aim of this article is to set up an EC2 instance on AWS, the first step is an AWS account.

If you are a beginner, the AWS Free Tier has changed since this post was first written. New accounts opened from July 2025 get a credit-based Free plan - up to $200 of credits ($100 straight away, another $100 for completing a few activities), valid for six months, after which the account closes unless you upgrade to the Paid plan. On the Free plan you cannot be billed, which makes it ideal for learning Terraform. The old "750 hours of t2.micro per month for 12 months" offer is gone, but a t3.micro running for an evening costs cents against those credits - and section 5 shows how terraform destroy makes sure you stop paying.

1.1 Sign up for an AWS account

Go to https://aws.amazon.com/free/ and click Create a free account

AWS sign up page

Choose the Free plan when asked.

AWS Free plan

Amazon will ask for your credit card number to complete the sign-up and makes a small temporary authorisation to verify the card. On the Free plan it is not charged.

1.2 After sign-up, log in and look around

After sign-up you log in as the root user - the email address you registered with. Once you log into your AWS account you will see the console home; I know it can be a little overwhelming for a first-time user, but do not worry, we are going to take one item at a time.

AWS console after login

The root user is for account-level tasks only (billing, closing the account). For day-to-day work, including Terraform, you create an IAM user next - that is what the AWS IAM best practices say, and they add that you should not create root access keys at all.



2. Create an IAM user and access keys (not the root user)

Terraform installed on your laptop needs to call the AWS APIs, so it has to authenticate. For that we need access keys (an access key ID and a secret access key), and they should belong to an IAM user with only the permissions it needs, not to the root user.

  1. In the console search for IAM, go to Users → Create user and name it terraform. Leave "Provide user access to the AWS Management Console" unticked - this user is for the CLI and Terraform only.

  2. On the permissions page choose Attach policies directly and attach AmazonEC2FullAccess. (For this tutorial that is enough; for a real project you would write a narrower policy, which I cover in Terraform AWS IAM - users, roles and policies.) Click Create user.

  3. Open the user, go to the Security credentials tab, Access keys → Create access key, choose Command Line Interface (CLI), tick the confirmation and create it.

  4. Copy the Access key ID and the Secret access key (Note:- you can see the secret only once, so copy it now or download the .csv).

If your company uses IAM Identity Center (SSO), you do not create access keys at all - aws configure sso and aws sso login give you short-lived credentials, and Terraform picks them up the same way as below. That is the recommended setup for teams, and the one I use in Terraform and AWS multi-account setup.



3. Configure the AWS credentials for Terraform (outside the code)

Here is the rule - the access key never goes into a .tf file. Files end up in Git, Git ends up on GitHub, and leaked keys get found by bots within minutes. The AWS provider authentication docs list the places Terraform looks instead, in order. Use one of these -

Option 1 - the shared credentials file (easiest). Run aws configure once and paste the keys. It writes ~/.aws/credentials and ~/.aws/config (%USERPROFILE%\.aws\ on Windows) -

1aws configure
2# AWS Access Key ID [None]: AKIA................
3# AWS Secret Access Key [None]: ....................
4# Default region name [None]: eu-central-1
5# Default output format [None]: json

Option 2 - environment variables. Good for CI pipelines and for switching accounts in one terminal -

1export AWS_ACCESS_KEY_ID="AKIA................"
2export AWS_SECRET_ACCESS_KEY="...................."
3export AWS_REGION="eu-central-1"

Option 3 - a named profile in ~/.aws/credentials, selected with profile = "jhooq" in the provider block or export AWS_PROFILE=jhooq.

Whichever you pick, verify it before touching Terraform -

1aws sts get-caller-identity
2# {
3#     "UserId": "AIDA................",
4#     "Account": "123456789012",
5#     "Arn": "arn:aws:iam::123456789012:user/terraform"
6# }

I have gone through all the options, including IAM roles on EC2 and GitHub Actions OIDC, in Terraform and AWS credentials handling.


4. Create your first Terraform infrastructure (main.tf)

Before we start writing Terraform code, the first thing to learn is - "You save your configuration in files with the .tf extension", written in HCL (HashiCorp Configuration Language). Terraform reads every .tf file in the directory, so for now one main.tf is enough.

Create an empty directory and an empty main.tf in it -

1mkdir terraform-ec2 && cd terraform-ec2
2touch main.tf

4.1 terraform block and provider

The first block pins the Terraform and provider versions. Without required_providers, terraform init still works (it assumes hashicorp/aws), but pinning ~> 6.0 means a provider upgrade with breaking changes can never surprise you -

 1terraform {
 2  required_version = ">= 1.5"
 3
 4  required_providers {
 5    aws = {
 6      source  = "hashicorp/aws"
 7      version = "~> 6.0"
 8    }
 9  }
10}

Then the provider block tells Terraform which cloud we are talking to - AWS, Google or Azure - and in which region. Notice it has only the region. Credentials come from section 3.

1provider "aws" {
2  region = "eu-central-1"
3}


4.2 data source - "aws_ami" (how to find the AMI)

Before we provision the EC2 instance we need two decisions -

  1. ami - which AMI (Amazon Machine Image) the instance boots from - Ubuntu, Amazon Linux, CentOS Stream, Windows ...
  2. instance_type - the size of the machine - t3.micro, t3.small, m7i.large ...

The 2021 version of this post told you to copy an AMI ID from the console. You can still do that - EC2 → Images → AMIs → Public images, search "ubuntu", copy the ID -

AWS AMIs option in the left navigation

Search public Ubuntu AMIs

Copy the AMI ID

But an AMI ID is different in every region and Canonical publishes a new one every few weeks, so a hard-coded ami-0767046d1677be5a0 stops working the moment you change region or the image is deprecated. The right way is an aws_ami data source that looks up the newest Ubuntu 24.04 image in whatever region you are in -

 1data "aws_ami" "ubuntu" {
 2  most_recent = true
 3  owners      = ["099720109477"] # Canonical's AWS account
 4
 5  filter {
 6    name   = "name"
 7    values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]
 8  }
 9
10  filter {
11    name   = "virtualization-type"
12    values = ["hvm"]
13  }
14}

099720109477 is Canonical's account ID, the same in every region, and noble is Ubuntu 24.04 (use jammy for 22.04, or al2023-ami-2023* with owner amazon for Amazon Linux 2023). If the filter matches nothing you get the error in section 9. The Ubuntu AMI locator is handy for checking names. Data sources in general are explained in how to use Terraform data sources.

4.3 How to find the correct instance_type

You can find the right instance_type on the EC2 instance types page. Since I am looking for a very basic instance for learning and not a production server, I choose t3.micro - 2 vCPUs, 1 GiB, burstable, and the cheapest x86 option in most regions (the older t2.micro from the original post is not even offered in the newer regions). If you want the absolute cheapest, t4g.micro is ARM (Graviton) - then change the AMI filter to arm64.

4.4 resource - "aws_instance"

Now after defining the provider and the data source, we define the resource - the thing we are going to provision on AWS. The syntax of every resource is -

1resource "<PROVIDER>_<TYPE>" "<NAME>" {
2  [CONFIG ...]
3}
  1. <PROVIDER>_<TYPE> - aws_instance, aws_s3_bucket, google_compute_instance ... - the list is in the provider docs
  2. <NAME> - a name you choose, used only inside Terraform to refer to this resource

Here is the aws_instance configuration, and an output so that Terraform prints the public IP when it is done -

 1resource "aws_instance" "ec2_example" {
 2  ami           = data.aws_ami.ubuntu.id
 3  instance_type = "t3.micro"
 4
 5  tags = {
 6    Name = "Terraform EC2"
 7  }
 8}
 9
10output "public_ip" {
11  description = "Public IP of the EC2 instance"
12  value       = aws_instance.ec2_example.public_ip
13}

This is how the complete main.tf looks -

 1terraform {
 2  required_version = ">= 1.5"
 3  required_providers {
 4    aws = {
 5      source  = "hashicorp/aws"
 6      version = "~> 6.0"
 7    }
 8  }
 9}
10
11provider "aws" {
12  region = "eu-central-1"
13}
14
15data "aws_ami" "ubuntu" {
16  most_recent = true
17  owners      = ["099720109477"]
18
19  filter {
20    name   = "name"
21    values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]
22  }
23
24  filter {
25    name   = "virtualization-type"
26    values = ["hvm"]
27  }
28}
29
30resource "aws_instance" "ec2_example" {
31  ami           = data.aws_ami.ubuntu.id
32  instance_type = "t3.micro"
33
34  tags = {
35    Name = "Terraform EC2"
36  }
37}
38
39output "public_ip" {
40  description = "Public IP of the EC2 instance"
41  value       = aws_instance.ec2_example.public_ip
42}

Nothing says which VPC or subnet - the instance goes into the default VPC of the region, which every new account has. That is fine for learning; a real project creates its own VPC, subnets and the security group of section 6.



5. Terraform commands - init, plan, apply, destroy

Alright, now we have completed all the prerequisites for provisioning our first EC2 instance on AWS. Four commands do everything.

5.1 terraform init

The first command we are going to run is -

1terraform init
 1Initializing the backend...
 2Initializing provider plugins...
 3- Finding hashicorp/aws versions matching "~> 6.0"...
 4- Installing hashicorp/aws v6.68.0...
 5- Installed hashicorp/aws v6.68.0 (signed by HashiCorp)
 6Terraform has created a lock file .terraform.lock.hcl to record the provider
 7selections it made above. Include this file in your version control repository
 8so that Terraform can guarantee to make the same selections by default when
 9you run "terraform init" in the future.
10
11Terraform has been successfully initialized!

terraform init downloads the AWS provider into a .terraform/ directory and writes .terraform.lock.hcl, which pins the exact provider version. Commit the lock file, ignore the .terraform/ directory. It makes no calls to your AWS account.

5.2 terraform plan

This command shows you what Terraform would do - how many resources it is going to add, change or destroy - without doing it.

1terraform plan
 1data.aws_ami.ubuntu: Reading...
 2data.aws_ami.ubuntu: Read complete after 1s [id=ami-0a1b2c3d4e5f67890]
 3
 4Terraform used the selected providers to generate the following execution
 5plan. Resource actions are indicated with the following symbols:
 6  + create
 7
 8Terraform will perform the following actions:
 9
10  # aws_instance.ec2_example will be created
11  + resource "aws_instance" "ec2_example" {
12      + ami                                  = "ami-0a1b2c3d4e5f67890"
13      + arn                                  = (known after apply)
14      + associate_public_ip_address          = (known after apply)
15      + availability_zone                    = (known after apply)
16      + id                                   = (known after apply)
17      + instance_state                       = (known after apply)
18      + instance_type                        = "t3.micro"
19      + key_name                             = (known after apply)
20      + public_ip                            = (known after apply)
21      + subnet_id                            = (known after apply)
22      + tags                                 = {
23          + "Name" = "Terraform EC2"
24        }
25      + tags_all                             = {
26          + "Name" = "Terraform EC2"
27        }
28      + vpc_security_group_ids               = (known after apply)
29      # (many attributes omitted)
30    }
31
32Plan: 1 to add, 0 to change, 0 to destroy.
33
34Changes to Outputs:
35  + public_ip = (known after apply)
36
37─────────────────────────────────────────────────────────────────────────────
38
39Note: You didn't use the -out option to save this plan, so Terraform can't
40guarantee to take exactly these actions if you run "terraform apply" now.

Two things to notice - the data source was read and resolved the AMI ID for you, and the summary line Plan: 1 to add, 0 to change, 0 to destroy. Read that line every single time before you apply. The (known after apply) values are things AWS decides when the instance is created. terraform plan does need working credentials, so this is where authentication errors show up.

(Note:- this command is not going to start your t3.micro instance)

5.3 terraform apply

This command does the real work on AWS. It shows the same plan, asks for confirmation, and then creates the instance.

1terraform apply
 1Plan: 1 to add, 0 to change, 0 to destroy.
 2
 3Do you want to perform these actions?
 4  Terraform will perform the actions described above.
 5  Only 'yes' will be accepted to approve.
 6
 7  Enter a value: yes
 8
 9aws_instance.ec2_example: Creating...
10aws_instance.ec2_example: Still creating... [10s elapsed]
11aws_instance.ec2_example: Creation complete after 13s [id=i-0a948ac635a2010f1]
12
13Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
14
15Outputs:
16
17public_ip = "3.70.123.45"

As you can see from the output, it created the t3.micro instance and printed its public IP. Terraform also wrote a terraform.tfstate file in the directory - that is how it remembers what it created. Never edit it by hand and never commit it to Git; for anything beyond a tutorial, store it in an S3 remote backend. In CI you run terraform apply -auto-approve to skip the prompt.

5.4 Verify the EC2 setup

Let's verify the setup by going back to the AWS console - Services → EC2 → Instances. You should see one instance running (make sure the console is set to the same region as the provider, eu-central-1 / Frankfurt).

AWS EC2 running instance

AWS EC2 running instance with details

You can also see the Tag Name - Terraform EC2 which we set in the Terraform code. Or without leaving the terminal -

1aws ec2 describe-instances --filters "Name=tag:Name,Values=Terraform EC2" \
2  --query "Reservations[].Instances[].[InstanceId,State.Name,PublicIpAddress]" --output table

5.5 terraform destroy

Now we have seen how to write the Terraform code and how to provision the EC2 instance. Let's see how to remove everything from AWS - this is the command that keeps your free credits intact.

1terraform destroy
 1Plan: 0 to add, 0 to change, 1 to destroy.
 2
 3Do you really want to destroy all resources?
 4  Terraform will destroy all your managed infrastructure, as shown above.
 5  There is no undo. Only 'yes' will be accepted to confirm.
 6
 7  Enter a value: yes
 8
 9aws_instance.ec2_example: Destroying... [id=i-0a948ac635a2010f1]
10aws_instance.ec2_example: Still destroying... [id=i-0a948ac635a2010f1, 10s elapsed]
11aws_instance.ec2_example: Still destroying... [id=i-0a948ac635a2010f1, 20s elapsed]
12aws_instance.ec2_example: Destruction complete after 31s
13
14Destroy complete! Resources: 1 destroyed.

terraform destroy removes every resource recorded in the state file. If you only want to remove one thing, terraform destroy -target=aws_instance.ec2_example - explained in how to run a specific Terraform resource.

The previous steps were the very basic Terraform example for setting up an EC2 instance. In a real project you need a little more than that - you need to log in to the machine, and you want it to configure itself on boot. The remaining part of this blog post covers that.


6. Add an SSH key pair and a security group so you can log in

Right now the instance has no SSH key and uses the default security group, which allows no inbound traffic, so you cannot connect to it. Three more resources fix that.

1. Generate an SSH key on your laptop (skip if you already have one) -

1ssh-keygen -t ed25519 -f ~/.ssh/aws_key -N ""
2# creates ~/.ssh/aws_key (private) and ~/.ssh/aws_key.pub (public)

2. Register the public key as an aws_key_pair - the private key stays on your machine -

1resource "aws_key_pair" "deployer" {
2  key_name   = "aws_key"
3  public_key = file("~/.ssh/aws_key.pub")
4}

3. Create a security group with SSH (and HTTP, for the next section) open. The original post used the inline ingress = [ { ... } ] list syntax; the AWS provider now recommends the separate aws_vpc_security_group_ingress_rule resources, one per rule, which avoids the rule-drift problems the inline blocks had -

 1data "aws_vpc" "default" {
 2  default = true
 3}
 4
 5resource "aws_security_group" "ec2_sg" {
 6  name        = "terraform-ec2-sg"
 7  description = "Allow SSH and HTTP"
 8  vpc_id      = data.aws_vpc.default.id
 9}
10
11resource "aws_vpc_security_group_ingress_rule" "ssh" {
12  security_group_id = aws_security_group.ec2_sg.id
13  cidr_ipv4         = "0.0.0.0/0"   # better: "<your-public-ip>/32"
14  from_port         = 22
15  to_port           = 22
16  ip_protocol       = "tcp"
17}
18
19resource "aws_vpc_security_group_ingress_rule" "http" {
20  security_group_id = aws_security_group.ec2_sg.id
21  cidr_ipv4         = "0.0.0.0/0"
22  from_port         = 80
23  to_port           = 80
24  ip_protocol       = "tcp"
25}
26
27resource "aws_vpc_security_group_egress_rule" "all" {
28  security_group_id = aws_security_group.ec2_sg.id
29  cidr_ipv4         = "0.0.0.0/0"
30  ip_protocol       = "-1"           # all traffic out
31}

4. Attach both to the instance -

 1resource "aws_instance" "ec2_example" {
 2  ami                    = data.aws_ami.ubuntu.id
 3  instance_type          = "t3.micro"
 4  key_name               = aws_key_pair.deployer.key_name
 5  vpc_security_group_ids = [aws_security_group.ec2_sg.id]
 6
 7  tags = {
 8    Name = "Terraform EC2"
 9  }
10}

Run terraform apply again (Plan: 4 to add, 1 to change - Terraform updates the existing instance in place for the security group, and replaces it for the key pair; either is fine here) and connect -

1ssh -i ~/.ssh/aws_key ubuntu@$(terraform output -raw public_ip)

ubuntu is the default user on Ubuntu AMIs (ec2-user on Amazon Linux). If SSH complains about the key file permissions, see how to fix WARNING: UNPROTECTED PRIVATE KEY FILE. Terraform can also generate the key pair for you with tls_private_key, and I have a full post on SSH into EC2 and GCP VMs with Terraform.


7. Setup a custom startup script for the EC2 instance with user_data

To run a script when the instance boots - install packages, write config, start a service - you use the user_data argument of aws_instance. The script is handed to cloud-init on the instance and runs once, as root, on the first boot.

In this example we are going to -

  1. Set up the EC2 instance
  2. Install the Apache web server
  3. Deploy a very basic HTML page on it
 1resource "aws_instance" "ec2_example" {
 2  ami                    = data.aws_ami.ubuntu.id
 3  instance_type          = "t3.micro"
 4  key_name               = aws_key_pair.deployer.key_name
 5  vpc_security_group_ids = [aws_security_group.ec2_sg.id]
 6
 7  user_data = <<-EOT
 8    #!/bin/bash
 9    apt-get update
10    apt-get install -y apache2
11    systemctl enable --now apache2
12    echo "<html><body><h1>Hello, this custom page was built with Terraform user_data</h1></body></html>" > /var/www/html/index.html
13  EOT
14
15  user_data_replace_on_change = true
16
17  tags = {
18    Name = "Terraform EC2"
19  }
20}

Three details that were wrong or missing in the old version - the script runs as root, so no sudo and no chown $USER (there is no $USER); the shebang #!/bin/bash must be the very first line; and user_data_replace_on_change = true tells Terraform to recreate the instance when you edit the script, because otherwise the new script never runs (user_data only runs on first boot).

After terraform apply, open http://<public_ip> in the browser - port 80 is open thanks to the security group - and you should see the page. If not, the cloud-init log on the instance tells you why - sudo cat /var/log/cloud-init-output.log.

For a longer script, keep it in a file and load it - user_data = file("${path.module}/startup.sh"), or templatefile() if it needs variables. More patterns in what is user_data in Terraform.


8. Copy and execute a script with the file and remote-exec provisioners

user_data is the right tool for first-boot configuration. The alternative, when you need Terraform itself to copy files to the machine and run commands over SSH, is a provisioner - file to copy, remote-exec to run. HashiCorp calls provisioners a last resort, because they need network access from where Terraform runs and their failures are harder to recover from - but they do have their uses, and this is how they are written correctly (in the 2021 version the remote-exec block had slipped outside the resource, which is a syntax error).

Both provisioners and the connection block go inside the aws_instance resource -

 1resource "aws_instance" "ec2_example" {
 2  ami                    = data.aws_ami.ubuntu.id
 3  instance_type          = "t3.micro"
 4  key_name               = aws_key_pair.deployer.key_name
 5  vpc_security_group_ids = [aws_security_group.ec2_sg.id]
 6
 7  tags = {
 8    Name = "Terraform EC2"
 9  }
10
11  # connection - how Terraform SSHes into the instance for the provisioners below
12  connection {
13    type        = "ssh"
14    host        = self.public_ip
15    user        = "ubuntu"
16    private_key = file("~/.ssh/aws_key")
17    timeout     = "4m"
18  }
19
20  # file provisioner - copies startup.sh from your laptop to the instance
21  provisioner "file" {
22    source      = "${path.module}/startup.sh"
23    destination = "/home/ubuntu/startup.sh"
24  }
25
26  # remote-exec provisioner - runs the copied script
27  provisioner "remote-exec" {
28    inline = [
29      "chmod +x /home/ubuntu/startup.sh",
30      "sudo /home/ubuntu/startup.sh",
31    ]
32  }
33}

And startup.sh next to main.tf -

1#!/bin/bash
2apt-get update
3apt-get install -y apache2
4systemctl enable --now apache2
5echo "<h1>Deployed with the Terraform remote-exec provisioner</h1>" > /var/www/html/index.html

self.public_ip refers to the instance being created. The aws_key_pair and aws_security_group resources are the ones from section 6, and SSH must be open from your IP for this to work. During terraform apply you will see Provisioning with 'file'... and the script output streamed to your terminal.

Provisioners run only at creation time, so changing the script does not re-run it - that is one of the reasons the user_data approach, or a proper configuration tool like Ansible, is usually better. All the details, including local-exec and on_failure, are in what is a Terraform provisioner.


9. Common errors when creating an EC2 instance with Terraform

1. Error: No valid credential sources found / failed to refresh cached credentials, no EC2 IMDS role found - Terraform could not find credentials in any of the places from section 3. Run aws sts get-caller-identity; if that fails, so will Terraform.

2. Error: configuring Terraform AWS Provider: validating provider credentials: ... SignatureDoesNotMatch - The secret key is wrong or has a trailing space. Full write-up in error configuring Terraform AWS Provider.

3. UnauthorizedOperation: You are not authorized to perform this operation - The IAM user lacks a permission, usually ec2:RunInstances or ec2:CreateSecurityGroup. Attach AmazonEC2FullAccess for the tutorial, or read the encoded message with aws sts decode-authorization-message.

4. Error: Your query returned no results. Please change your search criteria and try again. - The aws_ami filter matched nothing - a typo in the name pattern, or the wrong owners. Fix explained here.

5. InvalidAMIID.NotFound: The image id '[ami-...]' does not exist - A hard-coded AMI from another region. Use the data source.

6. VPCIdNotSpecified: No default VPC for this user - Someone deleted the default VPC. Create one in VPC → Actions → Create default VPC, or define aws_vpc and aws_subnet resources and set subnet_id on the instance.

7. InvalidKeyPair.NotFound - key_name refers to a key pair that does not exist in that region. Reference aws_key_pair.deployer.key_name so Terraform creates it first.

8. Unsupported: The requested configuration is currently not supported / InstanceLimitExceeded - The instance type is not offered in that AZ or you hit the vCPU quota of a new account. Try t3.micro or another region; new accounts can request a limit increase in Service Quotas.

9. timeout - last error: dial tcp <ip>:22: i/o timeout during provisioning - The security group does not allow port 22 from your IP, or the instance has no public IP (private subnet).

10. Error: Provider produced inconsistent final plan or lock file complaints after upgrading - Run terraform init -upgrade to update the lock file after changing the version constraint.


10. Conclusion

That is the whole journey - an AWS account, an IAM user with its own access keys, credentials configured outside the code, a main.tf with the AWS provider v6, an aws_ami data source and an aws_instance, and the init → plan → apply → destroy loop. Then the real-project additions - a key pair and security group so you can SSH in, user_data to configure the instance on boot, and the file/remote-exec provisioners for the cases where you need Terraform to push files over SSH.

The next posts in the series pick up exactly from here - Terraform variables to stop hard-coding the region and instance type, variables.tf and terraform.tfvars, and managing Terraform state before you share the project with anyone. HashiCorp's own version of this tutorial is the Build infrastructure guide, and the complete resource reference is the aws_instance documentation.


Read More - Terragrunt -

  1. How to use Terragrunt?