Terraform encodes AWS S3 bucket creation and lifecycle management through the HashiCorp AWS provider and the community module terraform-aws-modules/s3-bucket/aws. The module abstracts a large surface of S3 configuration options into a single module invocation while Terraform 4.x and later splits the native awss3bucket resource into discrete resources for versioning, encryption, public access, and ACL ownership. The interaction between provider initialization, resource declaration, backend state storage in S3, and IAM permissions for state access forms the operational core of real deployments. The reference material documents both module usage patterns and direct resource patterns, with specific emphasis on production-ready configuration, log delivery policies, and common naming and access mistakes.
The Terraform AWS provider uses a resource block of type awss3bucket to declare a new S3 bucket in an AWS account. Inside the awss3bucket block the bucket field specifies the name. The example name used in the reference material is terraform-experiments, but any name that meets S3 naming requirements can be chosen. The bucket field is optional in the schema but is considered best practice to give resources descriptive names to help with organization and management. Initialization of the Terraform module is performed with terraform init in the terminal. This command downloads necessary provider plugins and sets up the backend for storing the state file. Terraform is not limited to a specific cloud; it uses provider plugins to facilitate infrastructure management across all major cloud providers. Additionally, there are community plugins available for other popular services, such as Cloudflare. A preview of the resource that will be created can be obtained with terraform plan in the terminal. This will show what changes Terraform will make to the infrastructure based on the configuration in the main.tf file.
Prerequisites for direct S3 bucket creation with Terraform include Terraform 1.5+ installed and AWS CLI configured with aws configure. An AWS account with S3 permissions is required. S3 stands for Simple Storage Service. S3 buckets are cloud storage services by Amazon Web Service. It is used to store objects. It consists of data in any format like documents, images, videos, and application code. These are highly scalable.
Module Feature Coverage
The terraform-aws-modules/s3-bucket/aws module creates an S3 bucket on AWS with all or almost all features provided by Terraform AWS provider. The supported configuration features are listed explicitly in the reference material.
- static web-site hosting
- access logging
- versioning
- CORS
- lifecycle rules
- server-side encryption
- object locking
- Cross-Region Replication
- ELB log delivery bucket policy
- ALB/NLB log delivery bucket policy
- WAF log delivery bucket policy
- Account-level Public Access Block
- S3 Directory Bucket
- S3 Table Bucket
- S3 Vectors
The presence of these features in a single module reduces the need to manually compose multiple resources. Impact for operators is a reduction in drift and duplication because the module encapsulates recommended defaults for security and logging. Contextually, static web-site hosting, versioning, and server-side encryption are also addressed as separate resources in the native Terraform 4.x split, meaning the module provides a convenience layer over the split resource model.
Example module invocation for a private bucket with versioning enabled:
module "s3_bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
bucket = "my-s3-bucket"
acl = "private"
control_object_ownership = true
object_ownership = "ObjectWriter"
versioning = {
enabled = true
}
}
A log delivery bucket example shows force_destroy and ownership control:
module "s3_bucket_for_logs" {
source = "terraform-aws-modules/s3-bucket/aws"
bucket = "my-s3-bucket-for-logs"
acl = "log-delivery-write"
force_destroy = true
control_object_ownership = true
object_ownership = "ObjectWriter"
attach_elb_log_delivery_policy = true
}
A variant for ALB/NLB logs adds both ELB and LB policy attachment:
module "s3_bucket_for_logs" {
source = "terraform-aws-modules/s3-bucket/aws"
bucket = "my-s3-bucket-for-logs"
force_destroy = true
control_object_ownership = true
object_ownership = "ObjectWriter"
attach_elb_log_delivery_policy = true
attach_lb_log_delivery_policy = true
}
The module source is consistently terraform-aws-modules/s3-bucket/aws. The bucket argument sets the name. The acl argument controls canned ACL. controlobjectownership and objectownership govern ownership control behavior. The forcedestroy flag allows deletion of non-empty bucket. The attachelblogdeliverypolicy and attachlblogdeliverypolicy flags enable automatic policy attachment required for ALB/NLB logs.
Provider Initialization and Resource Declaration
Direct resource creation starts with a provider block and a resource block.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "4.64.0"
}
}
}
provider "aws" {}
resource "aws_s3_bucket" "s3" {
bucket = "terraform-experiments"
}
In just three lines of HCL, a new S3 bucket is defined. Using the resource block with type awss3bucket tells Terraform to create a new S3 bucket in the AWS account. Inside the awss3bucket block the bucket field specifies the name.
Project scaffolding can be performed with shell commands:
mkdir terraform-s3 && touch terraform-s3/main.tf
The file main.tf holds Terraform configurations. Opening main.tf in a preferred text editor allows follow along with basic provider configuration and creation of a new S3 bucket.
Terraform 4.x split S3 configuration into individual resources for better modularity. The quick answer in the reference material is to create an S3 bucket with awss3bucket, then add versioning, encryption, and public access blocks as separate resources. Common mistakes are documented as a table.
The mistake table includes:
- Inline versioning in awss3bucket | Use separate awss3bucket_versioning resource
- Bucket name with underscores | Use hyphens — underscores break DNS
- Missing public access block | Always add — blocks accidental public exposure
- Hardcoded bucket name | Add environment suffix for uniqueness
These corrections map directly to production-ready patterns.
Production-Ready Resource Splitting
Production-ready configuration requires versioning, encryption, public access blocks, and lifecycle rules, all as separate Terraform resources. Use bucket policies to enforce SSL and restrict access. Always use unique bucket names with environment prefixes.
Prerequisites listed are Terraform 1.5+ installed, AWS CLI configured with aws configure, and an AWS account with S3 permissions.
A provider configuration from the reference material uses version 4.33.0 and region us-east-1:
terraform {
required_providers{
aws={
source = "hashicorp/aws"
version = "4.33.0"
}
}
}
provider "aws"{
region = "us-east-1"
}
After defining provider, terraform init is executed to initialize Terraform. The Terraform init command prepares the working directory for use with Terraform. It initialises the backend, any child module installation and any plugin installation.
A main.tf file is created in the same directory where provider.tf file is located. In this file the resource and bucket name are defined. The bucket name should be unique. The example bucket name is my-s3-test-bucket02.
Versioning is a separate resource. Server-side encryption is a separate resource. Block public access is a separate resource. Lifecycle rules are a separate resource. Bucket policy is a separate resource. Static website hosting is a separate configuration.
ACL Ownership and Public Access Patterns
By default, objects uploaded to an S3 bucket are private and not accessible to the public. This occurs because, by default, objects uploaded to an S3 bucket are private.
To grant public access to a recently uploaded text file, modifications to ACL settings are made for the bucket and file.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "4.64.0"
}
}
}
resource "aws_s3_bucket" "s3" {
bucket = "terraform-experiments"
}
resource "aws_s3_bucket_ownership_controls" "ownership" {
bucket = aws_s3_bucket.s3.id
rule {
object_ownership = "BucketOwnerPreferred"
}
}
resource "aws_s3_bucket_public_access_block" "pb" {
bucket = aws_s3_bucket.s3.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
resource "aws_s3_bucket_acl" "acl" {
depends_on = [aws_s3_bucket_ownership_controls.ownership]
bucket = aws_s3_bucket.s3.id
acl = "private"
}
resource "aws_s3_bucket_object" "object" {
bucket = aws_s3_bucket.s3.bucket
key = "document.txt"
source = "./document.txt"
acl = "public-read"
}
A new awss3bucket_object resource is added with an acl field with value public-read.
Ownership control can be set to ObjectWriter as shown in module examples. The ownership control resource uses objectownership = "BucketOwnerPreferred" in the direct resource example. The module examples use objectownership = "ObjectWriter" with controlobjectownership = true.
Public access block settings control blockpublicacls, blockpublicpolicy, ignorepublicacls, and restrictpublicbuckets. Setting them to false enables public access for the example.
Backend S3 State Configuration and IAM Permissions
When using S3 as a Terraform backend, the state file is stored in an S3 bucket and optionally DynamoDB for locking. The table must have a partition key named LockID with a type of String.
When not using workspaces or when only using the default workspace, Terraform will need the following AWS IAM permissions on the target backend bucket:
- s3:ListBucket on arn:aws:s3:::mybucket
- s3:GetObject on arn:aws:s3:::mybucket/path/to/my/key
- s3:PutObject on arn:aws:s3:::mybucket/path/to/my/key
At a minimum, this must be able to list the path where the state is stored.
If use_lockfile is set, s3:GetObject, s3:PutObject, and s3:DeleteObject are required on the lock file, e.g., arn:aws:s3:::mybucket/path/to/my/key.tflock.
Note: s3:DeleteObject is not required on the state file, as Terraform does not delete it.
The IAM statement example is:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::mybucket",
"Condition": {
"StringEquals": {
"s3:prefix": "mybucket/path/to/my/key"
}
}
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": [
"arn:aws:s3:::mybucket/path/to/my/key"
]
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": [
"arn:aws:s3:::mybucket/path/to/my/key.tflock"
]
}
]
}
When using workspaces, Terraform will also need permissions to create, list, read, update, and delete the workspace state file:
- s3:ListBucket on arn:aws:s3:::mybucket
This permission requirement links backend security to bucket policy design. Misconfigured IAM leads to state read failures and lock contention.
Log Delivery and Specialized Bucket Policies
Log delivery buckets require specific ACL and policy attachments. The module supports attachelblogdeliverypolicy and attachlblogdeliverypolicy. The acl for log buckets is often log-delivery-write.
The module example for WAF logs starts with bucket = and is truncated in the reference material, indicating further configuration for WAF log delivery.
Cross-Region Replication, object locking, and S3 Directory Bucket and S3 Table Bucket and S3 Vectors are listed as supported features in the module. These features expand the bucket beyond simple object storage to compliance and analytics use cases.
Prerequisites and Workspace Setup
Step 1 is to open the cmd if in Windows and configure aws using the aws configure command.
aws configure
Step 2 is to open a code editor and make a file called provider.tf. .tf is the file extension for the terraform file. This file will store information about the cloud provider, version, and region for the s3 bucket.
After this, open terminal where this file is located and type terraform init to initialize terraform.
Step 3 is to define provider, then create S3 bucket, make a file called main.tf in the same directory where provider.tf file is located. In this file define the resource, bucket name. The bucket name should be unique.
Common Configuration Mistakes and Corrections
The reference material summarizes mistakes and fixes.
A table of common mistakes:
- Inline versioning in awss3bucket | Use separate awss3bucket_versioning resource
- Bucket name with underscores | Use hyphens — underscores break DNS
- Missing public access block | Always add — blocks accidental public exposure
- Hardcoded bucket name | Add environment suffix for uniqueness
The impact of using underscores is DNS incompatibility. The impact of missing public access block is accidental public exposure. The impact of inline versioning is loss of modularity and incompatibility with Terraform 4.x split.
Related articles mentioned are AWS ElastiCache Redis with Terraform - Complete Guide and AWS Kinesis Data Streams with Terraform.
Conclusion
The Terraform AWS S3 bucket landscape consists of a high-level module that bundles versioning, encryption, CORS, lifecycle, object locking, replication, log delivery policies, public access block, and specialized bucket types, and a lower-level native resource model that splits configuration into separate resources for better modularity. Provider initialization with requiredproviders version pinning, explicit provider region configuration, and terraform init establishes the execution environment. Resource declaration with awss3_bucket sets the bucket name, while ownership controls, ACL resources, public access blocks, and bucket objects manage access semantics. Backend state storage in S3 imposes specific IAM permissions for ListBucket, GetObject, PutObject, and conditional DeleteObject on lock files, with additional workspace permissions. Production readiness is achieved by separating versioning, encryption, public access blocks, and lifecycle rules into distinct resources, enforcing unique names with environment suffixes and hyphens instead of underscores, and attaching log delivery policies for ELB, ALB/NLB, and WAF. The module provides a concise abstraction for these patterns, while direct resource usage offers granular control over each configuration surface. Understanding both approaches allows operators to select module convenience for standard workloads and native resource splitting for fine-tuned compliance and state management requirements.