Skip to content
AWS aws deployment 6 min read

Using Terraform with AWS

Terraform is an open-source tool from HashiCorp for Infrastructure as Code (IaC) — describing your servers, networks, and databases in text files you commit to Git instead of clicking buttons in a console. Unlike CloudFormation, which only works with AWS, Terraform is multi-cloud: the same tool provisions resources on AWS, Azure, Google Cloud, and hundreds of other services through plug-ins called providers. This matters because many teams run on more than one cloud and want one workflow and one language for all of them. The catch is that Terraform tracks what it built in a separate state file you must store and protect carefully — the single biggest source of trouble for newcomers.

What Terraform actually is

You write configuration in HCL (HashiCorp Configuration Language), a readable declarative format. You declare the desired end state — “I want one S3 bucket and one EC2 instance” — and Terraform figures out the API calls needed to get there. It talks to AWS through the AWS provider, a plug-in that maps HCL resource types like aws_s3_bucket to real AWS API calls.

The core loop has two commands. terraform plan shows you exactly what will change before anything happens — a dry run. terraform apply then makes those changes. Between runs, Terraform records every resource it created in a state file (terraform.tfstate), a JSON file mapping your HCL to real resource IDs like i-0a1b2c3d4e5f. State is how Terraform knows what already exists so it can compute the difference on the next plan.

When to use Terraform (and when not to)

When to use it: you deploy to more than one cloud or use third-party services (Datadog, Cloudflare, GitHub) alongside AWS, you want one consistent tool and language everywhere, or your team already knows HCL. The Terraform CLI is free and open source; you pay only for the AWS resources it creates.

When NOT to use it: you are AWS-only and want zero extra tooling to install or state to manage — CloudFormation is fully managed by AWS and keeps no local state. If your team prefers a real programming language with loops and classes, the AWS CDK may fit better. And if managing a remote state backend feels like overhead you do not want, stay with CloudFormation.

Terraform vs CloudFormation vs CDK — when to use which

DimensionTerraformCloudFormationAWS CDK
Clouds supportedAWS + 1000s of providersAWS onlyAWS only
LanguageHCL (declarative)YAML/JSON (declarative)TypeScript, Python, Java, Go, C#
StateSelf-managed state file (you store it)Managed by AWS (no state file)Managed by AWS (synthesizes to CFN)
Preview changesterraform planChange setscdk diff
CostFree CLI; HCP Terraform paid tiersFreeFree
Best forMulti-cloud, mixed providersAWS-only, zero toolingAWS-only, code-driven infra

Getting started

Install the Terraform CLI, then confirm it runs. On macOS you can use Homebrew; downloads for every OS are on the HashiCorp site.

brew install hashicorp/tap/terraform
terraform version

Output:

Terraform v1.9.5
on darwin_arm64

Terraform uses your existing AWS credentials — the same ones the AWS CLI uses (set with aws configure or an AWS_PROFILE). It needs the AWS Command Line Interface (CLI) credentials but does not call the aws binary itself.

A minimal configuration

Create a file main.tf. The provider block tells Terraform to use AWS in a Region; the resource block declares one EC2 instance (a virtual server) and a tag.

provider "aws" {
  region = "us-east-1"
}

resource "aws_instance" "web" {
  ami           = "ami-0abcdef1234567890"
  instance_type = "t3.micro"

  tags = {
    Name = "devcraftly-web"
  }
}

Run the three core commands. init downloads the AWS provider plug-in, plan previews, and apply builds.

terraform init
terraform plan
terraform apply

Output:

Terraform will perform the following actions:

  # aws_instance.web will be created
  + resource "aws_instance" "web" {
      + ami           = "ami-0abcdef1234567890"
      + instance_type = "t3.micro"
      + id            = (known after apply)
    }

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

After you type yes, Terraform creates the instance and records i-0a1b2c3d4e5f in terraform.tfstate. To tear everything down, run terraform destroy.

The state file — the big gotcha

Terraform’s state file is the one thing CloudFormation users do not expect, and getting it wrong corrupts your infrastructure. By default the state lands in a local terraform.tfstate file. That is fine for a solo experiment but dangerous for a team: it can contain secrets in plain text, it is not shared, and two people running apply at once will clobber each other and create duplicate or orphaned resources.

The fix is a remote backend: store state in an Amazon S3 bucket (object storage) and use a DynamoDB table (a key-value database) for state locking so only one apply runs at a time.

terraform {
  backend "s3" {
    bucket         = "devcraftly-tfstate"
    key            = "prod/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

You can also create the backing resources from the console:

  1. Open the AWS Management Console and go to S3.
  2. Click Create bucket, name it (e.g. devcraftly-tfstate), enable Bucket Versioning, enable default encryption, and create it.
  3. Go to DynamoDB, click Create table, name it terraform-locks, and set the Partition key to LockID (type String).

Or with the AWS CLI v2:

aws s3api create-bucket --bucket devcraftly-tfstate --region us-east-1
aws s3api put-bucket-versioning --bucket devcraftly-tfstate \
  --versioning-configuration Status=Enabled
aws dynamodb create-table --table-name terraform-locks \
  --attribute-definitions AttributeName=LockID,AttributeType=S \
  --key-schema AttributeName=LockID,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST

Output:

{
    "TableDescription": {
        "TableName": "terraform-locks",
        "TableStatus": "CREATING"
    }
}

A PAY_PER_REQUEST locks table costs effectively nothing for normal use (a few cents a month), and S3 storage for a small state file is well under 1 USD per month — cheap insurance against corruption.

Never edit a Terraform-managed resource by hand in the console. If you do, Terraform’s state no longer matches reality, and the next apply may revert or destroy your change. This mismatch is called drift; check for it with terraform plan, which shows any out-of-band changes.

Protect the state file like a password. It can hold secrets (database passwords, keys) in plain text. Always enable S3 encryption and versioning, restrict bucket access with IAM, and never commit terraform.tfstate to Git. A lost or deleted state file means Terraform forgets what it owns — and a re-apply will create duplicate resources you now pay for twice.

Best practices

  • Always run and read terraform plan before terraform apply — never apply blind.
  • Use a remote S3 backend with DynamoDB locking from day one on any shared project, so two people cannot corrupt state.
  • Enable S3 bucket versioning and encryption on the state bucket; restrict it with tight IAM permissions.
  • Add terraform.tfstate* and .terraform/ to .gitignore so secrets never reach your repository.
  • Pin the AWS provider version (required_providers) so a surprise upgrade does not change behavior.
  • Never modify Terraform-managed resources manually; make every change through HCL to avoid drift.
  • Use terraform fmt and terraform validate in CI to keep configurations clean and catch errors early.
Last updated June 15, 2026
Was this helpful?