Terraform Best Practice | Split Terraform Code into Multiple Files | EC2 with Variables & Outputs

By Raees Qazi | DevOps Engineer | Learner | Mentor | Creator In the last blog, we created an EC2 instance in AWS using Terraform. At that time, we wrote the whole code in a single file. While this works, it’s not the best practice because keeping everything in one file becomes messy and harder to manage as your infrastructure grows. In today’s blog, we’ll improve our code by splitting it into separate files. This makes the code cleaner, more reusable, and easier to handle.

👉 Before we start, I assume:

  • You already have the provider file created.
  • Terraform is initialized.
  • AWS CLI is configured with an IAM user.

Why Split Terraform Files?

Terraform doesn’t care how many .tf files you have. It looks at the whole directory as a single configuration.

So instead of dumping everything in main.tf (or ec2.tf), we create multiple files like:

  • ec2.tf → EC2-related resources
  • variables.tf → Input variables
  • output.tf → Outputs like IPs or DNS names
  • provider.tf → Provider details

This way, your code is more structured and modular.

Old Code (Single File)

Earlier, we defined everything inside one file. Example snippet:

# Create SSH Key
resource "aws_key_pair" "my_ssh_key" {
key_name = "terra-key-auto"
public_key = file("/home/ubuntu/terra-key-auto.pub")
}
# Create Default VPC
resource "aws_default_vpc" "default" {}
# Security Group
resource "aws_security_group" "my_sg" {
name = "TWS-SG"
description = "this is a sooper se ooper upper security group"
vpc_id = aws_default_vpc.default.id
ingress = {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "this is for ssh access"
}
ingress = {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "this is for http access"
}
egress = {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "this is for outside world requests"
}
}
# EC2 Instance
resource "aws_instance" "my_instance" {
ami = "ami-xxxxxxx"
instance_type = "t2.micro"
key_name = aws_key_pair.my_ssh_key.key_name
security_groups = [aws_security_group.my_sg.name]
tags = {
Name = "My-Auto-server"
}
}

Improved Code (Multiple Files with Best Practices)

variables.tf

Here we define our variables instead of hardcoding values.
✅ Change #1 → Replaced hardcoded AMI ID & Instance Type with variables.

variable "ec2_ami_id" {
default = "ami-021eda" # Replace with correct AMI for your region
description = "This is AMI ID for EC2 instance"
type = string
}
variable "ec2_instance_type" {
default = "t2.micro"
description = "This is the instance type for EC2 instance"
type = string
}

output.tf

Here we define outputs like the public IP of our instance.
✅ Change #2 → Added an output file to display EC2 public IP after deployment.

output "my_ec2_ip" {
value = aws_instance.my_instance.public_ip
}

ec2.tf

Now we rewrite our EC2 code, but instead of hardcoding values, we use variables.
✅ Change #3 → Used var.ec2_ami_id and var.ec2_instance_type.
✅ Change #4 → Code split across separate files (variables.tfoutput.tfec2.tf).

# Create SSH Key
resource "aws_key_pair" "my_ssh_key" {
key_name = "terra-key-auto"
public_key = file("/home/ubuntu/terra-key-auto.pub")
}
# Create Default VPC
resource "aws_default_vpc" "default" {}
# Security Group
resource "aws_security_group" "my_sg" {
name = "TWS-SG"
description = "this is a sooper se ooper upper security group"
vpc_id = aws_default_vpc.default.id
ingress = {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "this is for ssh access"
}
ingress = {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "this is for http access"
}
egress = {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "this is for outside world requests"
}
}
# EC2 Instance
resource "aws_instance" "my_instance" {
ami = var.ec2_ami_id # ✅ Using variable instead of hardcoded value
instance_type = var.ec2_instance_type # ✅ Using variable instead of hardcoded value
key_name = aws_key_pair.my_ssh_key.key_name
security_groups = [aws_security_group.my_sg.name]
tags = {
Name = "My-Auto-server"
}
}
# Manage EC2 Instance State
resource "aws_ec2_instance_state" "my_state" {
instance_id = aws_instance.my_instance.id
state = "stopped"
}

Terraform Execution Flow (Step by Step)

Once you have the files ready (provider.tfvariables.tfec2.tfoutput.tf), run the following commands:

# 1. Initialize Terraform (downloads provider plugins)
terraform init
# 2. Check what Terraform will create (dry run)
terraform plan
# 3. Apply changes and create resources
terraform apply -auto-approve
# 4. Check the outputs (like EC2 public IP)
terraform output
# 5. Destroy resources when not needed
terraform destroy -auto-approve

Final Thoughts

We made our Terraform code:

  • Modular (separate files).
  • Reusable (variables instead of hardcoded values).
  • Clearer (outputs for visibility).

This approach is not just a best practice but also makes your Terraform code scalable and maintainable.

In the next blog, we’ll move one step ahead — maybe creating multiple environments (dev, staging, prod) using the same Terraform code. 🚀

🌐 Online References

✍️ By Raees Qazi
DevOps Engineer | Learner | Mentor | Creator

Comments

Popular posts from this blog

📘 Understanding Prometheus in a Simple Way-Part 3 (For DevOps Beginners)

Grafana Setup & Dashboard Creation (Part-5)— Explained by Raees Yaqoob Qazi

My First Python Program: A Simple Calculator