terraform-aws-s3-bucket: the S3 module most Terraform setups end up using
Terraform module to create AWS S3 resources 🇺🇦
At a glance
- What is it?
- The terraform-aws-s3-bucket module creates an S3 bucket with nearly every feature the AWS provider exposes, plus a family of sub-modules for objects, notifications, table buckets, vectors and account-level public access blocks. Its most instructive documentation is the notes section, which lists the behaviours that will replace resources rather than update them.
- Who is it for?
- Adopt terraform-aws-s3-bucket if you want one well-trodden module for S3 buckets with versioning, encryption, logging and lifecycle rules rather than hand-rolling provider resources, and you are prepared to read the notes section before changing file system configuration. Do not adopt it if you want a minimal bucket with a handful of arguments, since the module's surface is large, and check the version constraints in versions.tf before pinning a version.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 12 days ago.
- What is it written in?
- Mainly HCL, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the module covers, and where the boundaries are
The module creates an S3 bucket on AWS with all, or almost all, the features provided by the Terraform AWS provider. The feature list in the README is the scope statement: static web-site hosting, access logging, versioning, CORS, lifecycle rules, server-side encryption, object locking, Cross-Region Replication, Elastic Load Balancing and AWS WAF log delivery bucket policies, file system access to bucket data with Amazon S3 Files, and Account-level Public Access Block.
That is a lot of surface, and it is the reason this module exists rather than a hand-written aws_s3_bucket resource. The cost of that breadth is that the argument list is long, which is visible in the examples directory. There are working examples for account-public-access, acl, bucket-policies, complete, directory-bucket, file-system, inventory-and-analytics, notification, object, s3-replication, table-bucket and vectors, and the README directs you to that directory for working examples to reference.
The repository layout is the standard Terraform module arrangement with one addition. There is main.tf, variables.tf, outputs.tf and versions.tf at the root, then a modules/ directory holding the sub-modules, an examples/ directory, a docs/ directory and a wrappers/ directory.
The sub-modules are where the module is genuinely a family rather than a single thing. There is account-public-access for the account-level Public Access Block that applies to every bucket in the account, file-system for an Amazon S3 Files file system on a bucket the module does not manage, notification for bucket notifications to Lambda, SQS, SNS and EventBridge, object for objects placed in a bucket, table-bucket for table buckets and the Iceberg tables in them, and vectors for vector buckets and vector indexes.
There are also three bucket types the repository manages beyond the general purpose case. An S3 Directory Bucket is created by the root module when is_directory_bucket is true, while the S3 Table Bucket and S3 Vectors are reached through the table-bucket and vectors sub-modules.
A first bucket, and the two arguments that decide the ownership model
The private bucket with versioning enabled example is the shortest complete usage, and it shows the module call shape:
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
}
}Two things here are worth understanding rather than copying.
First, the module is referenced by the short source form, terraform-aws-modules/s3-bucket/aws, which is the Terraform registry form for a community module. The homepage is the registry page for the same module.
Second, the ownership pair. control_object_ownership and object_ownership go together, and they exist because the S3 default behaviour of objects being owned by the bucket owner has caused a great deal of friction for applications that need to write objects with their own credentials. Setting control_object_ownership to true and object_ownership to ObjectWriter is the combination that allows that.
Note also that versioning is a block rather than a boolean. The version argument is a map, and the module has evolved to take structured configuration for the features that have grown options of their own. That pattern recurs through the module, and it is a reason the examples are worth reading rather than the variable list alone.
The releases show a project that moves steadily: v5.17.0 on 2026-10-11, v5.16.2 on 2026-10-07 and v5.16.1 on 2026-09-18, with the last push on 2026-10-11. The major version is 5, so the module is well past the churn of its early years but still shipping several releases a month.
Log delivery policies, and the custom policy placeholder trick
Log delivery is handled with a set of attach flags, and the module merges every enabled flag into a single bucket policy rather than creating several. The example shows three in one bucket:
module "s3_bucket_for_logs" {
source = "terraform-aws-modules/s3-bucket/aws"
bucket = "aws-waf-logs-my-s3-bucket-for-logs"
# Application and Classic Load Balancer access logs
attach_elb_log_delivery_policy = true
# Network Load Balancer access logs
attach_lb_log_delivery_policy = true
# AWS WAF logs, which require a bucket name that starts with `aws-waf-logs-`
attach_waf_log_delivery_policy = true
}The comment on the WAF flag records a service requirement rather than a style choice, because AWS requires a bucket name that starts with aws-waf-logs- for WAF log delivery. The bucket name in the example follows that convention.
The custom policy path is more interesting technically. When you need to attach your own policy, you use the policy argument together with attach_policy. The placeholders _S3_BUCKET_ID_, _S3_BUCKET_ARN_ and _AWS_ACCOUNT_ID_ are replaced with the real values when the policy is attached:
module "s3_bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
bucket_prefix = "my-s3-bucket-"
attach_policy = true
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Sid = "DenyInsecureTransport"
Effect = "Deny"
Principal = "*"
Action = "s3:*"
Resource = ["_S3_BUCKET_ARN_", "_S3_BUCKET_ARN_/*"]
Condition = {
Bool = { "aws:SecureTransport" = "false" }
}
}]
})
}The reason the placeholders exist is given: they are what makes a policy work against a bucket whose name is generated from bucket_prefix. When the bucket name is known at plan time you could reference the attribute directly, but with a prefix the final name is not known until apply, so a literal reference would not resolve. Substituting placeholders at attach time sidesteps that.
The example policy itself is the standard deny-insecure-transport statement, denying all S3 actions when aws:SecureTransport is false, and the Resource list covers both the bucket itself and its objects with the /* suffix.
S3 file systems, and the notes that tell you what gets replaced
The file system example is the most complex in the README, and the notes section below it is the most valuable part of the documentation for anyone running this in production.
S3 Files requires versioning on the bucket. The module creates an IAM role for each file system and a security group shared by the mount targets. A principal listed in an access point's read_access_arns or read_write_access_arns can mount that file system only through the access points that list it.
The configuration is a nested map of file systems, each with a prefix and a set of mount targets keyed by Availability Zone:
module "s3_bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
bucket = "my-s3-bucket"
versioning = {
enabled = true
}
file_system_security_group_vpc_id = "vpc-1234556abcdef"
file_system_security_group_ingress_rules = {
clients = {
referenced_security_group_id = "sg-1234556abcdef"
}
}
file_systems = {
datasets = {
prefix = "datasets/"
mount_targets = {
eu-west-1a = { subnet_id = "subnet-abcde012" }
eu-west-1b = { subnet_id = "subnet-bcde012a" }
}
}
}
}The notes then spell out the consequences, and they are the kind of thing you want to know before rather than after an apply.
A bucket carrying an S3 file system needs versioning enabled and either SSE-S3 or SSE-KMS encryption. S3 Files is not available on a directory bucket, and the module says so rather than ignoring file_systems.
Changing a file system's prefix, its KMS key, or its IAM role replaces the file system, along with its mount targets and access points. An access point cannot be edited either, so any change to one replaces it and gives it a new ARN. That last point has a knock-on effect for permissions, since anything that referred to the old ARN has to be updated.
Removing a synchronization_configuration leaves the file system expiring data after 365 days, not the 30 days AWS applies to a file system that never had one.
And the constraint that bites hardest is the last one. Key mount_targets by something known at plan time, such as the Availability Zone, because Terraform cannot create resources whose map keys come from values that only exist after an apply. That single rule prevents a class of plan failure that is otherwise very confusing, because the error appears as a dependency cycle rather than as a naming problem.
Conditional creation, and what the repository does not tell you
Terraform does not allow count inside a module block, which is a real constraint when you want a bucket to exist only in some environments. The module's answer is the create_bucket argument, described in the README as the solution for creating S3 resources conditionally. The example that follows shows a module block for a bucket that will not be created.
That is a small design point, but it is one of the reasons community modules exist at all, since a resource that only sometimes exists has to be expressed at the module boundary rather than inside it.
What the README does not cover is worth listing, because it affects how you evaluate the module. There is no discussion of state migration between module versions, so an upgrade that changes a resource address will show up as a destroy and create in the plan. There is no compatibility statement about Terraform CLI versions or provider versions in the visible text, so you should read versions.tf for the constraints, which is where Terraform modules conventionally put them. There is no performance discussion and no cost analysis.
What the README does give you is unusual for a module of this size: a notes section that states the destructive behaviours plainly, including that a change replaces a resource and issues a new ARN, and that removing a synchronization configuration changes the expiry from 30 days to 365. Those are the facts that cause production incidents, and most module documentation leaves them implicit.
For the Apache-2.0 licence, the practical points are that you can modify and redistribute the module, and that redistribution has to keep the licence and the notice. The repository also carries a CHANGELOG.md, which is where you should look before a version bump, and a .releaserc.json at the root, which indicates the releases are cut by tooling rather than by hand.
Editorial conclusion
Adopt terraform-aws-s3-bucket if you want one well-trodden module for S3 buckets with versioning, encryption, logging and lifecycle rules rather than hand-rolling provider resources, and you are prepared to read the notes section before changing file system configuration. Do not adopt it if you want a minimal bucket with a handful of arguments, since the module's surface is large, and check the version constraints in versions.tf before pinning a version. Before you deploy a bucket with an S3 file system, read the notes about versioning, encryption and the 365 day expiry, and key mount_targets by something known at plan time such as the Availability Zone, because Terraform cannot create resources whose map keys come from values that only exist after an apply.
Frequently asked questions
What does the terraform-aws-s3-bucket module create?
It creates an S3 bucket with all or almost all the features provided by the Terraform AWS provider, including static web-site hosting, access logging, versioning, CORS, lifecycle rules, server-side encryption, object locking, Cross-Region Replication, load balancer and WAF log delivery policies, S3 file system access, and an account-level Public Access Block.
Which sub-modules does the S3 bucket module ship with?
Six: account-public-access for the account-level Public Access Block, file-system for an Amazon S3 Files file system on a bucket the module does not manage, notification for notifications to Lambda, SQS, SNS and EventBridge, object for objects placed in a bucket, table-bucket for table buckets and Iceberg tables, and vectors for vector buckets and indexes.
How do I attach a custom bucket policy when the bucket name uses a prefix?
Set attach_policy to true and pass the policy with the placeholders _S3_BUCKET_ID_, _S3_BUCKET_ARN_ and _AWS_ACCOUNT_ID_. The module replaces them with the real values when the policy is attached, which is what makes the policy work against a bucket whose name is generated from bucket_prefix.
What are the constraints for an S3 file system in this module?
The bucket needs versioning enabled and either SSE-S3 or SSE-KMS encryption, and S3 Files is not available on a directory bucket. Changing a file system's prefix, KMS key or IAM role replaces the file system along with its mount targets and access points, and mount_targets must be keyed by something known at plan time such as the Availability Zone.
How do I create an S3 bucket conditionally with this module?
Use the create_bucket argument. Terraform does not allow count inside a module block, so the module takes a flag instead, and an example in the README shows a module block for a bucket that will not be created.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/terraform-aws-modules-terraform-aws-s3-bucket)Was this useful?