Skip to content
Infrastructure as Code iac providers 4 min read

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.

ArgumentPurpose
regionTarget AWS region, e.g. eu-west-1. Required (or via AWS_REGION).
profileNamed profile from ~/.aws/credentials / ~/.aws/config.
assume_roleBlock to assume an IAM role before making API calls.
default_tagsTags applied to every taggable resource managed by the provider.
access_key / secret_keyStatic credentials (avoid — prefer profiles or roles).
endpointsOverride 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:

  1. Static access_key / secret_key set in the provider block (discouraged).
  2. Environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN.
  3. A shared profile named via profile or AWS_PROFILE.
  4. 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 .tf files. Use profile, environment variables, or — best of all — assume_role with 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.hcl so every environment resolves the same plugin version.
  • Authenticate with assume_role plus OIDC in CI rather than long-lived access keys.
  • Use default_tags to 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 region explicitly in the provider block (or via AWS_REGION) so behavior never depends on a developer’s local default.
  • Validate the active identity with aws_caller_identity before applying to production accounts to avoid deploying to the wrong place.
Last updated June 14, 2026
Was this helpful?