The AWS Provider
The AWS provider is by far the most widely used Terraform provider, exposing thousands of resources and data sources that map onto Amazon Web Services APIs. Configuring it correctly — region, credentials, and (often) assumed roles — is the foundation for every AWS deployment. This page covers how to declare the provider, how it authenticates, the resources you will reach for most, and how data sources let you query existing infrastructure.
Declaring the provider
Like every modern provider, AWS should be pinned in a required_providers block so that builds are reproducible across machines and CI. The provider is published under the official hashicorp/aws namespace and works identically with both Terraform 1.5+ and OpenTofu.
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.60"
}
}
}
provider "aws" {
region = "us-east-1"
}
The ~> 5.60 constraint allows patch and minor upgrades (5.61, 5.62, …) but blocks the breaking 6.x major version. Run terraform init to download the plugin and record the exact resolved version in .terraform.lock.hcl.
Configuration options
The provider "aws" block accepts many arguments. The ones you will use most are summarized below.
| Argument | Purpose |
|---|---|
region | Target AWS region, e.g. eu-west-1. Required (or via AWS_REGION). |
profile | Named profile from ~/.aws/credentials / ~/.aws/config. |
assume_role | Block to assume an IAM role before making API calls. |
default_tags | Tags applied to every taggable resource managed by the provider. |
access_key / secret_key | Static credentials (avoid — prefer profiles or roles). |
endpoints | Override service endpoints, useful for LocalStack testing. |
A production-style configuration usually combines a profile, default tags, and an assumed role:
provider "aws" {
region = "us-east-1"
profile = "platform-admin"
assume_role {
role_arn = "arn:aws:iam::123456789012:role/terraform-deployer"
session_name = "terraform-ci"
}
default_tags {
tags = {
ManagedBy = "terraform"
Environment = "production"
}
}
}
Authentication
The AWS provider resolves credentials using the same default chain as the AWS CLI and SDKs, checked in order:
- Static
access_key/secret_keyset in the provider block (discouraged). - Environment variables:
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN. - A shared profile named via
profileorAWS_PROFILE. - EC2 instance profiles, ECS task roles, or EKS IRSA / Pod Identity.
For CI runners, the strongest option is OIDC federation: the runner exchanges a short-lived token for AWS credentials via assume_role_with_web_identity, so no long-lived secrets are stored anywhere.
Tip: Never commit static access keys to version control or hard-code them in
.tffiles. Useprofile, environment variables, or — best of all —assume_rolewith OIDC for keyless authentication.
You can confirm which identity Terraform is using with the aws_caller_identity data source before applying anything.
Common resources
Most AWS estates are built from a handful of core building blocks. The example below provisions a VPC, an S3 bucket, and an IAM role — the kind of foundation almost every stack needs.
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = { Name = "main-vpc" }
}
resource "aws_s3_bucket" "assets" {
bucket = "devcraftly-assets-prod"
}
resource "aws_s3_bucket_versioning" "assets" {
bucket = aws_s3_bucket.assets.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_iam_role" "app" {
name = "app-runtime"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = { Service = "ec2.amazonaws.com" }
}]
})
}
EC2 instances tie these together. You typically look up the AMI dynamically rather than hard-coding an ID that varies by region.
Data sources
Data sources read existing AWS state without managing it. Two are nearly universal: aws_caller_identity (who am I?) and aws_ami (the latest image for a region).
data "aws_caller_identity" "current" {}
data "aws_ami" "al2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.al2023.id
instance_type = "t3.micro"
subnet_id = aws_subnet.public.id
tags = { Name = "web-server" }
}
output "account_id" {
value = data.aws_caller_identity.current.account_id
}
Worked example: plan and apply
After writing the configuration, initialize and plan:
terraform init
terraform plan
Output:
data.aws_caller_identity.current: Reading...
data.aws_ami.al2023: Reading...
data.aws_caller_identity.current: Read complete after 0s [id=123456789012]
data.aws_ami.al2023: Read complete after 1s [id=ami-0abcd1234ef567890]
Terraform will perform the following actions:
# aws_instance.web will be created
+ resource "aws_instance" "web" {
+ ami = "ami-0abcd1234ef567890"
+ instance_type = "t3.micro"
+ id = (known after apply)
}
Plan: 5 to add, 0 to change, 0 to destroy.
Apply the change once the plan looks correct:
terraform apply -auto-approve
Output:
aws_instance.web: Creating...
aws_instance.web: Creation complete after 22s [id=i-0fae9c1b2d3e4f567]
Apply complete! Resources: 5 added, 0 changed, 0 destroyed.
Outputs:
account_id = "123456789012"
Best Practices
- Pin the provider with a
~>constraint and commit.terraform.lock.hclso every environment resolves the same plugin version. - Authenticate with
assume_roleplus OIDC in CI rather than long-lived access keys. - Use
default_tagsto enforce ownership and cost-allocation tags across the whole stack automatically. - Look up AMIs and account context with data sources instead of hard-coding region-specific IDs.
- Set
regionexplicitly in the provider block (or viaAWS_REGION) so behavior never depends on a developer’s local default. - Validate the active identity with
aws_caller_identitybefore applying to production accounts to avoid deploying to the wrong place.