Quick Answer
Every Terraform AWS project needs three services: IAM (who can do what), S3 (store state files), and DynamoDB (lock state for team safety). This guide shows how to set up all three.
AWS IAM for Terraform
IAM controls who can create, modify, and delete AWS resources.
Create a Terraform IAM User
# Create user
aws iam create-user --user-name terraform
# Create access keys
aws iam create-access-key --user-name terraform
# Attach administrator policy (for dev/learning)
aws iam attach-user-policy \
--user-name terraform \
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess
Least-Privilege Policy (Production)
data "aws_iam_policy_document" "terraform" {
# S3 state bucket access
statement {
effect = "Allow"
actions = [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket",
]
resources = [
"arn:aws:s3:::my-terraform-state",
"arn:aws:s3:::my-terraform-state/*",
]
}
# DynamoDB locking
statement {
effect = "Allow"
actions = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem",
]
resources = ["arn:aws:dynamodb:*:*:table/terraform-locks"]
}
# EC2, VPC, etc. — add per project needs
statement {
effect = "Allow"
actions = ["ec2:*", "vpc:*"]
resources = ["*"]
condition {
test = "StringEquals"
variable = "aws:RequestedRegion"
values = ["us-east-1"]
}
}
}
Use IAM Roles (Best Practice)
# For CI/CD — use OIDC instead of access keys
resource "aws_iam_role" "terraform_ci" {
name = "terraform-ci"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/token.actions.githubusercontent.com"
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
}
}]
})
}
S3 for Terraform State
S3 stores your Terraform state file remotely so teams can collaborate.
Create the State Bucket
resource "aws_s3_bucket" "state" {
bucket = "mycompany-terraform-state"
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "state" {
bucket = aws_s3_bucket.state.id
versioning_configuration {
status = "Enabled" # Recover from bad applies
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "state" {
bucket = aws_s3_bucket.state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
resource "aws_s3_bucket_public_access_block" "state" {
bucket = aws_s3_bucket.state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
DynamoDB for State Locking
Prevents two people from applying at the same time.
resource "aws_dynamodb_table" "locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST" # No capacity planning needed
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
Configure the Backend
terraform {
backend "s3" {
bucket = "mycompany-terraform-state"
key = "production/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
Summary
| Service | Purpose | Terraform Use |
|---|---|---|
| IAM | Authentication & authorization | Who can run Terraform |
| S3 | Object storage | Remote state file storage |
| DynamoDB | Key-value database | State locking for teams |
Related Articles
Conclusion
Set up IAM with least-privilege policies (not AdministratorAccess), S3 with versioning and encryption for state, and DynamoDB for locking. Use IAM roles with OIDC for CI/CD instead of access keys. This foundation supports safe, team-friendly Terraform workflows.




