Agentic AI with Java: Live Cohort
TerraformTerraform Workflow & State

First Terraform Script on Linux VM

Write a main.tf with a provider and an aws_instance resource on the Linux VM, fix a real validate error, then run init, fmt, validate, plan, apply and destroy.

Time to stop talking and create something. In this lesson we write a small Terraform file on the Linux VM that creates one EC2 instance, the same thing we clicked through in the console in the first lesson. Then we run the full workflow from the previous lesson and finally delete the instance again.

If you work on Windows with VS Code instead, read this lesson anyway for the explanation, then follow First Terraform Script locally on Windows for the Windows specific steps.

Step 1: Connect and set credentials

Start the terraform-vm instance, connect over SSH, and export your credentials as shown in Terraform Security Credentials. Remember that export does not survive a new SSH session, so you do this every time you reconnect (unless you attached an IAM role to the VM).

export AWS_ACCESS_KEY_ID="AKIAxxxxxxxxxxxxxxxx"
export AWS_SECRET_ACCESS_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Step 2: Create a project folder

Every Terraform project lives in its own folder, because Terraform reads all .tf files in the current folder together, and it stores its state there too.

mkdir 01-tf-ec2
cd 01-tf-ec2
vi main.tf

main.tf is just a convention. Terraform would read abc.tf the same way, but everyone uses main.tf for the main resources, so stick with it. Press i in vi to enter insert mode. If vi is new to you, the Linux course covers it.

Step 3: Write the code

A Terraform file for AWS needs two things:

  1. Which provider to use, and its settings such as the region
  2. Which resources to create with that provider

Here is the file from the lecture, after formatting:

main.tf
provider "aws" {
  region = "ap-south-1"
}

resource "aws_instance" "linux-VM" {
  ami             = "ami-062f0cc54dbfd8ef1"
  instance_type   = "t2.micro"
  key_name        = "terraform_keypair"
  security_groups = ["default"]

  tags = {
    Name = "Telusko_Terraform_Linux_VM"
  }
}

Let us read it line by line.

The provider block

provider "aws" {
  region = "ap-south-1"
}

provider "aws" tells Terraform we are working with AWS. region is where resources are created. ap-south-1 is Mumbai; pick the region shown at the top right of your AWS console. We do not write any keys here. The provider finds them in the environment variables.

The resource block

resource "aws_instance" "linux-VM" {

Every resource block has the same shape: resource "<TYPE>" "<NAME>".

PartMeaning
resourceKeyword. We want Terraform to create and manage something
"aws_instance"The resource type, defined by the AWS provider. It means an EC2 instance. You cannot invent this name; it comes from the provider documentation
"linux-VM"The local name, chosen by you. Terraform uses it to refer to this resource (aws_instance.linux-VM). It is not the name you see in the AWS console

Inside the block are the arguments, the same choices you made in the console:

ArgumentConsole equivalentValue used
amiApplication and OS ImagesAn Amazon Linux AMI ID copied from the Launch instance page
instance_typeInstance typet2.micro (free tier eligible)
key_nameKey pairThe key pair created earlier, terraform_keypair
security_groupsSecurity groupThe VPC's default security group. It is a list, so you could give several
tagsName and tagsName is the tag the console shows as the instance name

AMI IDs are per region

An AMI ID such as ami-062f0cc54dbfd8ef1 only exists in one region, and Amazon publishes new AMIs regularly. Do not copy the ID from this page. Open EC2 > Launch instance in your own region, select Amazon Linux, and copy the AMI ID shown there. A wrong ID gives InvalidAMIID.NotFound during apply.

The lecture also mentions that adding count = 10 inside the resource block creates ten identical instances. That is true, and it shows the real power of code over clicking. Do not try it on a free tier account, though: ten instances eat through your free allowance ten times faster.

Save with Esc then :wq.

Step 4: init

terraform init
Initializing the backend...
Initializing provider plugins...
- Finding latest version of hashicorp/aws...
- Installing hashicorp/aws v5.97.0...
- Installed hashicorp/aws v5.97.0 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above.

Terraform has been successfully initialized!

Terraform saw provider "aws", downloaded the latest AWS provider into .terraform/, and recorded the version in .terraform.lock.hcl. In the recording that was v5.97.0; today you will get a 6.x version.

Step 5: fmt and validate, and a real error

The lecture ran terraform validate and got an error. This is a mistake almost everyone makes once, so it is worth seeing:

Error: Unsupported block type

  on main.tf line 13, in resource "aws_instance" "linux-VM":
  13:   tags {

Blocks of type "tags" are not expected here. Did you mean to define argument "tags"?
If so, use the equals sign to assign it a value.

The code had tags { instead of tags = {. In HCL there is a difference:

SyntaxWhat it isExample
name { ... }A nested block, defined by the resource schemaroot_block_device { ... }
name = { ... }An argument whose value is a maptags = { Name = "web" }

tags is an argument that takes a map, so it needs =. Fix it in vi, then:

terraform fmt
cat main.tf
terraform validate

terraform fmt prints the names of files it changed and aligns all the = signs. terraform validate now says:

Success! The configuration is valid.

Step 6: plan

terraform plan

The plan lists every attribute of the new instance. Values you set (ami, instance_type, key_name) are shown; everything else is (known after apply) because AWS decides it, such as the instance ID and the public IP. The last line is the important one:

Plan: 1 to add, 0 to change, 0 to destroy.

One resource to add, nothing changed, nothing destroyed. That is exactly what we expect.

Step 7: apply

terraform apply

Terraform shows the plan again and asks:

Do you want to perform these actions?
  Enter a value: yes

Type yes (only the exact word yes is accepted). After about a minute:

aws_instance.linux-VM: Creation complete after 32s [id=i-0abc...]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Refresh EC2 > Instances in the console. A new instance called Telusko_Terraform_Linux_VM is running. You did not click a single button to create it.

terraform apply -auto-approve skips the question. It is convenient in practice sessions, but get used to reading the plan first.

Step 8: destroy

terraform destroy -auto-approve

Terraform refreshes its state, plans to delete the one instance it created, and deletes it. In the console the instance moves to Terminated. Your terraform-vm, which you created by hand, is not touched, because Terraform only manages what is in its state.

Two improvements to adopt now

The lecture's file works, but two small changes make it more reliable. You will see them in the rest of this course.

Pin the provider version. Without it, init always takes the newest provider, and a future major version could break your code.

main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

Use vpc_security_group_ids instead of security_groups. The security_groups argument takes security group names and only works in the default VPC. With vpc_security_group_ids you pass IDs, which works in every VPC. We will use it in Creating EC2 VM with user Data using Terraform.

Common errors

ErrorCauseFix
No valid credential sources foundCredentials not exported in this SSH sessionExport them again, or attach an IAM role to the VM
InvalidAMIID.NotFoundAMI ID from another region, or an old IDCopy the AMI ID from your own region's console
InvalidKeyPair.NotFoundKey pair name wrong or in another regionCheck EC2 > Key Pairs in the same region
Unsupported block typetags { instead of tags = {Add the =

Try It Yourself

  1. Create 01-tf-ec2/main.tf on the VM with your own region's AMI ID, and run init, fmt, validate, plan and apply.
  2. Remove the = after tags on purpose and run terraform validate to see the error yourself.
  3. Change the Name tag, run terraform plan, and notice that it shows 1 to change (~), not a new instance.
  4. Run ls -a in the folder and find .terraform/, .terraform.lock.hcl and terraform.tfstate.
  5. Finish with terraform destroy and confirm in the console that the instance is terminated.

Official references


Written By: Shramik Masti

How is this guide?

Last updated on