Using Terraform with AWS: The Complete Guide
Using Terraform with AWS: The Complete Guide
Infrastructure management has evolved from manual console clicks and fragile scripts to declarative code that can be versioned, reviewed, and shared. For teams building on Amazon Web Services, Terraform has become the standard for infrastructure as code (IaC)—and knowing how to use terraform with aws effectively is now a core skill for cloud engineers. This guide walks you through the complete workflow, from setting up the AWS provider to deploying real resources with production-grade practices.
What You'll Learn
By the end of this guide, you'll understand the complete Terraform workflow on AWS—from provider configuration and resource definition to state management and team collaboration. You'll be able to deploy, update, and destroy AWS infrastructure confidently using Terraform's declarative approach, and you'll walk away with a clear mental model for structuring your own projects. You'll also learn practical safeguards to prevent costly mistakes like deploying to the wrong AWS account.
Understanding the Core Concepts: Providers, Resources, and State
Before writing any configuration, it helps to understand Terraform's fundamental building blocks. Unlike AWS CloudFormation—which is native to AWS and assumes resources are being created within an AWS account—Terraform is cloud-agnostic and requires you to explicitly declare providers that interact with specific APIs .
What Is a Provider?
A provider is a plugin that acts as the bridge between Terraform and a target platform. The AWS Provider is what allows Terraform to call AWS APIs to create, read, update, and delete resources . Because Terraform supports multiple providers—AWS, Kubernetes, Helm, and even third-party SaaS tools—you can provision an Amazon EKS cluster and deploy Helm charts into it within the same configuration .
Resources and Data Sources
A resource defines a component of your infrastructure: an EC2 instance, an S3 bucket, or a security group. Resources are declared declaratively—you specify what you want, and Terraform figures out how to create it .
A data source, on the other hand, queries AWS for information about existing resources. For example, instead of hardcoding an AMI ID that might change or vary by region, you can use a data source to fetch the most recent Amazon Linux 2023 AMI dynamically .
State: The Source of Truth
Terraform maintains a state file that tracks the resources it manages. This state maps your configuration to real infrastructure and is essential for planning updates, detecting drift, and destroying resources cleanly . For team environments, storing state in a remote backend like Amazon S3 is highly recommended to prevent conflicts and ensure consistency .
Getting Started: Installing Tools and Configuring AWS Credentials
Prerequisites
Before you write a single line of configuration, set up your local environment:
- Terraform CLI 1.5 or later – Download the binary and add it to your PATH .
- AWS CLI – Install and configure it with IAM credentials that have sufficient permissions .
- An AWS account – Use the Free Tier for practice to avoid unexpected charges .
Authenticating the AWS Provider
Terraform's AWS provider authenticates using the same methods as the AWS CLI. The simplest approach is to set environment variables with your IAM credentials:
export AWS_ACCESS_KEY_ID=<your_access_key>
export AWS_SECRET_ACCESS_KEY=<your_secret_key>
Alternatively, Terraform can read from the default credential file at ~/.aws/credentials .
Step 1: Write Your First Terraform Configuration
Create a new directory for your project and define the provider and resources. Terraform configuration files use HashiCorp Configuration Language (HCL) and end with .tf .
The terraform Block
The terraform block configures Terraform itself, including provider sources and version constraints. Pinning provider versions prevents unexpected drift when the provider is updated .
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
required_version = ">= 1.2"
}
The source argument points to the provider in the Terraform Registry. HashiCorp maintains the AWS provider, so its source is hashicorp/aws .
The provider Block
The provider block configures the AWS provider, including the default region:
provider "aws" {
region = "us-west-2"
}
Define an EC2 Instance
A resource block declares an infrastructure component. Here's how to define an EC2 instance using a data source to fetch the latest Ubuntu AMI:
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]
}
}
resource "aws_instance" "app_server" {
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
tags = {
Name = "terraform-demo"
}
}
Using a data source for the AMI keeps your configuration dynamic and portable across regions .
Step 2: Initialize, Plan, and Apply
The Terraform workflow consists of three core commands :
terraform init
Initialize the working directory. This downloads the required provider plugins and sets up the backend:
terraform init
Terraform reads the required_providers block and installs the AWS provider version you specified .
terraform plan
Preview the changes Terraform will make to your infrastructure:
terraform plan
The plan shows additions, modifications, and deletions—this is your opportunity to catch mistakes before applying .
terraform apply
Apply the configuration to create resources. Terraform will show the plan again and prompt for confirmation:
terraform apply
Type yes to proceed. Terraform will provision the EC2 instance and output the results .
terraform destroy
When you're done, clean up to avoid charges:
terraform destroy
Terraform calculates what to delete based on the state file and prompts for confirmation .
Step 3: Add a Security Group and Provisioning Logic
A server is useless without network access. Add a security group to control inbound and outbound traffic :
resource "aws_security_group" "web" {
name = "web-sg"
description = "Allow HTTP and SSH"
vpc_id = var.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"] # Restrict SSH
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
Attach the security group to your EC2 instance by referencing aws_security_group.web.id in the vpc_security_group_ids argument.
Automating Software Installation with User Data
Use EC2 user data to install software when the instance first boots. For example, to install Nginx on Amazon Linux 2023 :
resource "aws_instance" "web" {
ami = data.aws_ami.al2023.id
instance_type = "t3.micro"
vpc_security_group_ids = [aws_security_group.web.id]
key_name = var.key_name
user_data = <<-EOF
#!/bin/bash
dnf install -y nginx
systemctl start nginx
echo "<h1>Hello from Terraform</h1>" > /usr/share/nginx/html/index.html
EOF
}
User data runs during the initial boot and is a simple way to bootstrap instances without additional configuration management tools .
Step 4: Use Variables, Outputs, and Modules
Variables for Reusability
Hardcoding values makes configurations brittle. Define variables in a variables.tf file :
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t2.micro"
}
variable "aws_region" {
description = "AWS region"
type = string
default = "us-west-2"
}
Reference variables in your configuration with var.instance_type.
Outputs to Display Information
Outputs let you retrieve information after applying, such as the instance's public IP:
output "instance_public_ip" {
description = "Public IP of the EC2 instance"
value = aws_instance.app_server.public_ip
}
Modules for Reusable Components
Modules are containers for multiple resources that can be shared across projects. The Terraform Registry provides thousands of community modules, and you can also write your own . Using modules keeps your root configuration clean and promotes reusability.
Step 5: Remote State and Team Collaboration
For any team environment, storing state locally is dangerous—it can become outdated or conflict with other users' changes. Configure a remote backend, typically using Amazon S3 with DynamoDB for state locking :
terraform {
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "prod/terraform.tfstate"
region = "us-west-2"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
This configuration ensures state is shared and locked during apply operations, preventing concurrent modifications that could corrupt the state file .
Step 6: Security Best Practices
Restrict Provider to Specific AWS Accounts
One of the most dangerous mistakes is running Terraform against the wrong AWS account—for example, intending to deploy to development but accidentally using production credentials. The allowed_account_ids setting in the provider block creates a safety net :
provider "aws" {
region = "us-west-2"
allowed_account_ids = ["123456789012"] # Only this account
}
If you attempt to run Terraform with credentials from a different account, the plan will error immediately, preventing costly mistakes .
Use IAM Roles, Not Static Keys
For EC2 instances, always use IAM instance profiles rather than embedding static credentials. This improves security and simplifies credential rotation .
Version Control and Review
All Terraform configurations should be stored in version control. Pull requests for infrastructure changes should be reviewed just like application code—this is the core value of IaC .
How to Use Terraform with AWS in CI/CD Pipelines
Integrating Terraform into CI/CD pipelines automates deployments and enforces consistency. A typical GitHub Actions workflow :
name: Terraform Apply
on:
push:
branches: [ main ]
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: hashicorp/setup-terraform@v1
- name: Terraform Init
run: terraform init
- name: Terraform Plan
run: terraform plan
- name: Terraform Apply
run: terraform apply -auto-approve
This approach eliminates manual apply steps and ensures every main branch merge deploys infrastructure that has been reviewed and tested .
Frequently Asked Questions
1. What is the difference between Terraform and AWS CloudFormation?
Terraform is cloud-agnostic and supports multiple providers—AWS, Azure, Google Cloud, and hundreds of others—while CloudFormation is AWS-only. Terraform also offers a more flexible workflow with plan/apply stages and supports a wider range of third-party resources .
2. Where does Terraform store the state of my infrastructure?
By default, Terraform stores state in a local terraform.tfstate file. For production and team use, you should configure a remote backend like Amazon S3 to store state centrally with locking to prevent conflicts .
3. What are Terraform modules and why should I use them?
Modules are containers for multiple resources that can be reused across projects. Using modules reduces duplication, enforces consistency, and lets you share infrastructure patterns across teams via the Terraform Registry .
4. How do I avoid accidentally deploying to the wrong AWS account?
Use the allowed_account_ids setting in your AWS provider block to restrict Terraform to specific account IDs. If the credentials don't match the allowed list, Terraform will error before making any changes .
5. What is the difference between terraform plan and terraform apply?
terraform plan previews the changes Terraform would make to your infrastructure. terraform apply executes those changes. Always run plan first to catch mistakes before modifying resources .
Sources
- Terrateam, "How to deploy Nginx with Terraform on AWS" (2026)
- AWS Prescriptive Guidance, "Understanding Terraform resources and providers" (2024)
- AWS Prescriptive Guidance, "Terraform AWS Provider best practices" (2024)
- HashiCorp Developer, "Create infrastructure" (2025)
- Refine, "Getting Started with Terraform on AWS" (2024)
- DevelopersIO, "Terraform AWS Provider allowed_account_ids" (2025)
- GitHub, "terraform-basics-aws" repository
— Editorial Team
No comments yet.