Build EC2 with Terraform
Provision an EC2 instance on AWS using Terraform
Read the article covering the lab end to end.
Build EC2 with Terraform: From Zero to Running Instance
Why this lab matters
Provisioning an EC2 instance through the AWS console takes about ninety seconds of clicking. Doing it with Terraform takes ten minutes the first time, and thirty seconds every time after. The point is not the instance. The point is the pattern: declarative compute, version-controlled, reproducible, and the loop of init then plan then apply then destroy that you will repeat across every cloud resource you ever touch.
This is the foundation stage of the application platform. Every AWS lab that follows assumes you can do this in your sleep.
What this lab builds
Two tightly scoped resources in us-east-1, defined declaratively:
- An EC2 instance named tier1-ec2-instance, a t3.micro running Amazon Linux 2, launched into the account default VPC with identifying tags so you can find it again.
- A security group named tier1-ec2-sg that permits inbound SSH on port 22 and allows all outbound traffic.
The instance references the security group, so Terraform creates the group first and the instance second. Small surface area, high muscle-memory return.
Get the code
The code lives in the Code tab on this lab page. The main.tf there holds both resource definitions, with versions.tf, provider.tf, variables.tf, and outputs.tf separating version pins, provider config, inputs, and exported values. Read main.tf before you apply, then copy the files into a local lab directory.
Run the apply loop
From the lab directory, run terraform init, then terraform plan, then terraform apply.
init downloads the AWS provider and writes the lock file. plan shows you the two resources Terraform intends to create, and you read this output carefully every time. apply creates them and asks for confirmation. When apply finishes, you have a running t3.micro instance. Verify in the AWS console under EC2 then Instances, or with aws ec2 describe-instances.
What to notice while you're doing it
Several deliberate choices in this module are worth pausing on, because each is a question a senior engineer will ask:
- The AMI is hardcoded, not resolved by a data source. A pinned AMI ID produces a fully deterministic plan: the same image every run. A data aws_ami lookup with most_recent resolves at plan time and can silently pull a newer image, causing an unexpected instance replacement. The trade is that the ID is region-specific and must be updated by hand for another region or a newer OS.
- The provider is pinned to major version 5. The versions.tf constraint is ~> 5.0, which accepts 5.x patches and minors but never 6.0.0. Skipping this works today and breaks on a Tuesday in 2027 when the provider hits a major bump. The lock file records the exact provider hash so every machine and pipeline runs the identical binary.
- The instance launches into the default VPC. That keeps the module self-contained with zero network dependencies, which is right for a foundation exercise. It is also why this is not production shape: the default VPC is shared and uncontrolled, and a real workload uses a purpose-built VPC with private subnets.
- No key pair is attached. The instance is provisioned to validate the IaC pattern, not to be logged into, so there is no key material to generate, store, or rotate. If you later need access, the production-grade answer is AWS Systems Manager Session Manager, which needs no key pair and no open inbound port.
- The tags are not decorative. Name, Environment, and Project are how you find this instance in three months when you have forgotten you created it, and how cost allocation and policy targeting work at scale.
Destroy
This is the most important step. Free tier covers 750 hours of t3.micro per month, but you are going to forget. Run terraform destroy and type yes. The instance terminates, the security group is removed, and the bill stops. This is the loop you want burned into reflex.
Common gotchas
- UnauthorizedOperation or AccessDenied on apply. The executing identity lacks ec2:RunInstances or a related EC2 permission. Confirm who you are with aws sts get-caller-identity, then attach the EC2 actions the module needs.
- InvalidAMIID.NotFound. The AMI ami-0c02fb55956c7d316 exists only in us-east-1. Either keep aws_region as us-east-1 or look up the equivalent Amazon Linux 2 AMI for your region in the EC2 console under Launch Instance.
- No default VPC for this user. Some accounts have no default VPC, so the instance has nowhere to launch. Recreate it with aws ec2 create-default-vpc, or pass an explicit subnet_id in main.tf.
- SSH open to the world. The security group allows port 22 from 0.0.0.0/0, which is fine for a short demo and a known risk for anything longer. Restrict it to your own IP with a /32 CIDR, or remove SSH entirely in favour of SSM Session Manager.
- State file confusion. terraform.tfstate is created locally and maps your config to the real resources. Delete it and Terraform loses track of the instance, leaving you to clean up by hand in the console. For real work, use remote state in S3 with DynamoDB locking. For this lab, just do not delete it.
Interview-style questions to think through
Hold these while you run the lab. They are the questions a screen will probe:
- Why pin the provider version, and what breaks if you do not?
- What is the difference between terraform plan and terraform apply, and why does plan matter on a team?
- What lives in terraform.tfstate, and why is keeping it local a problem in production?
- If two engineers run terraform apply against the same local state at once, what happens?
- Why is the AMI hardcoded instead of looked up, and what is the cost of that choice?
- How would you take SSH off the public internet without losing the ability to reach the instance?
If you can answer these out loud without looking them up, you have earned the lab.
What's next
Once init then plan then apply then destroy feels routine, the next steps tighten this same instance toward production: a key pair and SSM access, a custom VPC with private subnets in place of the default, remote state in S3 with DynamoDB locking, and an IAM instance profile so the instance calls AWS APIs without stored credentials. The progression mirrors how a real platform team hardens compute: start with an instance that applies and destroys cleanly, then layer network isolation, state safety, and identity on top.
Finished the article? Test what stuck with a quick check.
Take the Article Check →