The creation and management of Amazon S3 storage through Terraform represents a convergence of declarative infrastructure as code and AWS storage primitives. Amazon S3 is described as a storage service offered by AWS for storing data lakes, websites, mobile applications, backups and restores, archives, and enterprise applications. The S3 stands for Simple Storage Service, which can be scaled based on individual or organization needs. In addition to providing storage, Amazon S3 also offers comprehensive access management, which can help you set up very granular permissions. When Terraform is used to interact with this service, the workflow moves from local configuration files to provider initialization, plan preview, and applied state in the AWS account. The blog post context shows how to create and manage an AWS S3 bucket with Terraform using modern AWS provider patterns.
The main Terraform resources used in this guide are:
awss3bucket
awss3object
awss3bucketpublicaccessblock
awss3bucketownership_controls
These resources let you create a bucket, upload objects, block public access, and manage bucket ownership behavior. The operational sequence documented is first initialization, then planning, then applying. The note that screenshots reflect an earlier version of the example is repeated across sections, and the updated code in this article uses current Terraform AWS provider patterns, so resource names and plan/apply output may differ slightly. This versioning awareness is central to interpreting examples, because the Terraform AWS provider has evolved toward separating bucket creation from configuration concerns.
Initialization and Provider Configuration
The first command in the documented workflow is initialization.
$ terraform init
This is the first command we are going to run. It initializes the working directory and downloads the AWS provider. The impact of this step is that without a provider plugin present locally, Terraform cannot translate HCL configuration into AWS API calls. The technical layer is that terraform init downloads the necessary provider plugins and sets up the backend for storing the state file. Terraform is not limited to a specific cloud; rather, 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. The contextual layer is that initialization is a prerequisite for every subsequent operation and establishes the state backend that will later track the bucket, public access block, and ownership controls that Terraform is about to create.
The provider configuration example provided is:
provider "aws" {
region = "us-east-1"
}
Using the resource block, we've defined a new resource of type awss3bucket. This tells Terraform that we want to create a new S3 bucket in our AWS account. Inside the awss3bucket block, we've specified the name of our bucket using the bucket field. In this case, we've named it terraform-experiments, but you can choose any name that meets the requirements for S3 bucket names. It's worth noting that while the bucket field is optional, it's considered a best practice to give your resources descriptive names to help with organization and management.
The TL;DR guidance states: Create the bucket with the awss3bucket resource after configuring the AWS provider. In modern Terraform, keep the bucket definition minimal and manage settings like versioning or encryption with separate resources when needed.
A minimal bucket definition example is:
resource "aws_s3_bucket" "example" {
bucket = "my-unique-bucket-name-12345"
}
Then run terraform init, terraform plan, and terraform apply.
The second command would be to run a Terraform plan.
$ terraform plan
The second command would be to run a Terraform plan. This command shows the bucket, public access block, and ownership controls that Terraform is about to create. To see a preview of the resource that will be created, you can run the terraform plan command in your terminal. This will show you what changes Terraform will make to your infrastructure based on the configuration in your main.tf file. The plan output is the impact layer for operators, because it surfaces drift and unexpected changes before state mutation. The technical layer is that plan evaluates the current state file against the desired configuration. The contextual layer is that plan is the safety check that precedes apply in both creation and destruction workflows.
The third command is application.
$ terraform apply
Apply the Terraform configuration using the terraform apply command, which will eventually create an S3 bucket in AWS. After the apply completes, you should see a new S3 bucket named spacelift-test1-s3 in your AWS account. Note: These screenshots reflect an earlier version of the example. The updated code in this article uses current Terraform AWS provider patterns, so resource names and plan/apply output may differ slightly. In the updated configuration, Terraform may also show additional S3 resources such as public access blocking and ownership controls. Note: This apply screenshot is illustrative and may not exactly match the current code sample. The latest version of this guide uses a more modern S3 configuration, so the number of resources created may be different.
Core Resources and Object Upload
The core resource set for bucket creation and object management is awss3bucket and awss3object. The resource block defines a new resource of type awss3bucket. This tells Terraform that we want to create a new S3 bucket in our AWS account. Inside the awss3bucket block, we've specified the name of our bucket using the bucket field.
In the previous step, we created an S3 bucket with Terraform. In this step, we will upload files to that bucket using the awss3object resource. If you are working from older examples, you may still see awss3bucket_object, but that resource has been deprecated.
The impact of using awss3object for uploads is that Terraform can manage a small number of supporting files along with the infrastructure itself. This approach works well when you want Terraform to manage a small number of supporting files along with the infrastructure itself. For very large numbers of files or frequent bulk uploads, use a data transfer or deployment tool rather than Terraform. The technical layer is that object resources are state-tracked, so changes to file content trigger diffs and updates. The contextual layer is that the deprecation of awss3bucketobject signals a provider shift toward awss3_object as the canonical resource for object management.
Public Access Block and Ownership Controls
- Block public access and enforce bucket ownership
Now that the bucket and objects are in place, the next step is to make sure the bucket follows the modern S3 access model. Older Terraform examples often set acl = "private" on the bucket and then discuss ACLs as the main way to manage access. That is no longer the best default for new buckets.
The modern model introduces awss3bucketpublicaccessblock and awss3bucketownership_controls. The guide lists these as main Terraform resources used in this guide. The resources let you create a bucket, upload objects, block public access, and manage bucket ownership behavior. The impact is reduced risk of unintended public exposure. The technical layer is that public access block settings are applied at the bucket level and ownership controls determine who owns newly created objects. The contextual layer is that the shift away from ACLs as the primary access mechanism aligns with AWS best practices and is reflected in the module defaults.
Terraform AWS S3 Bucket Module Features
Terraform module which creates S3 bucket on AWS with all (or almost all) features provided by Terraform AWS provider.
These features of S3 bucket configurations are supported:
- static web-site hosting
- access logging
- versioning
- CORS
- lifecycle rules
- server-side encryption
- object locking
- Cross-Region Replication (CRR)
- 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 module source is consistently terraform-aws-modules/s3-bucket/aws.
Example module for a general bucket:
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
}
}
Example module for logs with ELB delivery:
```
module "s3bucketfor_logs" {
source = "terraform-aws-modules/s3-bucket/aws"
bucket = "my-s3-bucket-for-logs"
acl = "log-delivery-write"
Allow deletion of non-empty bucket
forcedestroy = true
controlobjectownership = true
objectownership = "ObjectWriter"
attachelblogdeliverypolicy = true
}
```
A variant for logs with additional load balancer policies:
```
module "s3bucketfor_logs" {
source = "terraform-aws-modules/s3-bucket/aws"
bucket = "my-s3-bucket-for-logs"
Allow deletion of non-empty bucket
forcedestroy = true
controlobjectownership = true
objectownership = "ObjectWriter"
attachelblogdeliverypolicy = true # Required for ALB logs
attachlblogdeliverypolicy = true # Required for ALB/NLB logs
}
```
The module for WAF logs begins:
module "s3_bucket_for_waf_logs" {
source = "terraform-aws-modules/s3-bucket/aws"
bucket =
The impact of using a community module is accelerated adoption of supported features such as static web-site hosting, access logging, versioning, CORS, lifecycle rules, server-side encryption, object locking, Cross-Region Replication, and log delivery bucket policies for ELB, ALB/NLB, and WAF. The technical layer is that the module encapsulates resource interdependencies and policy documents. The contextual layer is that the module supports S3 Directory Bucket, S3 Table Bucket, and S3 Vectors, indicating coverage of newer S3 data types beyond classic object storage.
Policy Placeholders and Conditional Creation
To keep bucket policy with correct S3 bucket and AWS account properties, you can use the placeholders S3BUCKETID, S3BUCKETARN, and AWSACCOUNTID in the policy document. Those values will be replaced with the actual values during the policy attachment. This is especially useful when using bucket prefixes.
The impact is reduced manual templating errors when policies reference dynamic identifiers. The technical layer is placeholder substitution occurs at module apply time. The contextual layer is that this pattern supports reusable policy templates across environments.
Sometimes you need to have a way to create S3 resources conditionally but Terraform does not allow to use count inside module block, so the solution is to specify argument create_bucket.
```
This S3 bucket will not be created
module "s3bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
createbucket = false
..
```
The impact is that infrastructure can be toggled without removing module code. The technical layer is that create_bucket = false prevents resource creation while keeping configuration present. The contextual layer is that this addresses a Terraform language limitation regarding count inside module blocks.
Destroy Lifecycle and Ordering
Delete S3 Bucket using Terraform
When you are done working with the bucket, Terraform can remove all of the resources it created by running terraform destroy.
$ terraform destroy
Terraform will delete the managed S3 objects first and then delete the bucket itself. This ordering matters because S3 buckets must be empty before AWS allows them to be removed. As always, review the destroy plan carefully before confirming the operation, especially if you are working in a shared or production AWS account.
Note: The destroy output shown here was generated from an earlier version of the example. In the updated configuration, Terraform may also remove public access block and ownership control resources. As you can see in the screenshot, Terraform has deleted the resources in reverse-chronological order, starting with test2.txt, then test2.txt, and finally the bucket spacelift-test1-s3.
The impact of destroy ordering is prevention of API errors due to non-empty bucket constraints. The technical layer is that Terraform evaluates dependency graph and removes objects before bucket. The contextual layer is that destroy plans must be reviewed carefully in shared accounts to avoid accidental data loss.
Workflow Summary Table
A structured view of the documented commands and resources helps relate the workflow steps to the outcomes.
| Step | Command | Purpose | Resources Affected |
| terraform init | terraform init | Initializes working directory and downloads AWS provider | Provider plugin, state backend |
| terraform plan | terraform plan | Preview changes | awss3bucket, awss3bucketpublicaccessblock, awss3bucketownership_controls |
| terraform apply | terraform apply | Create infrastructure | S3 bucket named spacelift-test1-s3, objects |
| terraform destroy | terraform destroy | Remove infrastructure | Managed S3 objects first, then bucket |
Module Configuration Attributes Table
| Attribute | Example Value | Context |
| source | terraform-aws-modules/s3-bucket/aws | Module location |
| bucket | my-s3-bucket | Bucket name |
| acl | private | Legacy access control |
| controlobjectownership | true | Enables ownership controls |
| objectownership | ObjectWriter | Ownership mode |
| versioning.enabled | true | Versioning state |
| forcedestroy | true | Allows deletion of non-empty bucket |
| attachelblogdeliverypolicy | true | ELB log policy attachment |
| attachlblogdeliverypolicy | true | ALB/NLB log policy attachment |
Conclusion
The documented approach to AWS S3 bucket management with Terraform centers on an explicit three-step command sequence, a minimal bucket resource definition, and separation of concerns for access control and ownership. Initialization establishes the provider and state backend, planning surfaces the bucket, public access block, and ownership controls that will be created, and applying materializes the bucket, potentially named spacelift-test1-s3, and associated objects. The shift from awss3bucketobject to awss3_object reflects provider modernization, while the guidance to keep the bucket definition minimal and manage settings like versioning or encryption with separate resources when needed aligns with current AWS provider patterns.
The community module terraform-aws-modules/s3-bucket/aws expands the base resource into a comprehensive feature set that includes static web-site hosting, access logging, versioning, CORS, lifecycle rules, server-side encryption, object locking, Cross-Region Replication, ELB, ALB/NLB, and WAF log delivery bucket policies, plus Account-level Public Access Block, S3 Directory Bucket, S3 Table Bucket, and S3 Vectors. Placeholder substitution for S3BUCKETID, S3BUCKETARN, and AWSACCOUNTID supports dynamic policy generation, and the create_bucket argument provides conditional creation without using count inside a module block.
Public access blocking and ownership controls replace ACL-centric defaults, with controlobjectownership and object_ownership settings demonstrating the modern S3 access model. Destruction follows a dependency-aware order, deleting managed S3 objects first and then the bucket itself, because S3 buckets must be empty before AWS allows them to be removed. Review of destroy plans remains critical in shared or production accounts. The overall pattern is that Terraform orchestrates S3 provisioning declaratively while the AWS provider and community modules handle evolving resource types and best practice defaults.