Terraform Attachment Of AWS S3 Bucket Policies And Module Patterns

The intersection of AWS S3 bucket policy enforcement and Terraform declarative configuration produces a control surface where policy documents are treated as immutable artifacts that are fully replaced on each apply. The reference implementations demonstrate retrieval of existing policies, import into Terraform state, construction of aws_s3_bucket_policy resources, and the use of the community terraform-aws-modules/terraform-aws-s3-bucket/aws module to encapsulate bucket creation alongside policy attachment. The provider configuration binds authentication profile and region, while two distinct policy authoring patterns emerge: raw JSON via heredoc and programmatic generation via aws_iam_policy_document. The deprecation of the inline policy parameter on aws_s3_bucket forces migration to the standalone aws_s3_bucket_policy resource. Module capabilities span static web hosting, access logging, versioning, CORS, lifecycle rules, server-side encryption, object locking, Cross-Region Replication, ELB/ALB/NLB/WAF log delivery policies, account-level Public Access Block, and specialized bucket types including S3 Directory Bucket, S3 Table Bucket, and S3 Vectors. Placeholder tokens _S3_BUCKET_ID_, _S3_BUCKET_ARN_, and _AWS_ACCOUNT_ID_ enable policy templating for prefix-based deployments. Conditional creation is achieved through the create_bucket argument because Terraform does not permit count inside a module block.

Updating Existing S3 Bucket Policies With Terraform

The operational workflow for modifying an already existing bucket policy begins with retrieval of the current policy. The reference material specifies first retrieving the existing bucket policy using a data source or by importing the existing policy into Terraform state.

The impact for operators is that without an accurate state representation, Terraform will interpret the bucket as having no managed policy and will replace the entire document on the first apply, potentially removing permissions that were applied outside Terraform. Importing the existing policy into state creates a baseline that Terraform can diff against.

The contextual layer ties this step to the complete replacement behavior of aws_s3_bucket_policy. Terraform does not perform merge operations on policy JSON; it applies the exact document supplied in the policy attribute. Therefore the retrieval step is not optional for preservation of existing statements.

The configuration pattern is to define aws_s3_bucket_policy referencing the existing bucket.

resource "aws_s3_bucket_policy" "bucket_policy" { bucket = aws_s3_bucket.your_bucket.id policy = jsonencode({ Version = "2012-10-17" Statement = [ { // Existing policy statements here }, { // New policy statement to add Sid = "NewPolicyStatement" Effect = "Allow" Principal = { Service = "some-aws-service.amazonaws.com" } Action = ["s3:SomeAction"] Resource = ["${aws_s3_bucket.your_bucket.arn}/*"] } ] }) }

The bucket attribute binds the policy to the bucket identified by aws_s3_bucket.your_bucket.id. The policy attribute uses jsonencode to produce a valid policy document. The statement array contains a placeholder for existing statements and a new statement with Sid, Effect, Principal, Action, and Resource. The Resource uses interpolation of the bucket ARN.

The impact for practitioners is that the placeholder comment must be replaced with the actual existing policy statements extracted from AWS. If the comment remains, the policy will be syntactically valid but will lose existing permissions.

Contextually, this pattern aligns with the module approach where policy attachment is separated from bucket creation, and with the deprecation notice that the policy parameter on aws_s3_bucket is no longer recommended.

Terraform will replace the entire bucket policy when you apply this configuration. This is a direct statement from the reference material. The consequence is that any policy statements not explicitly included in the Statement array will be removed from AWS after apply. Operators must therefore maintain a complete superset of required permissions in the configuration, not incremental deltas.

Module Architecture For S3 Buckets

The Terraform module which creates S3 bucket on AWS with all or almost all features provided by Terraform AWS provider.

The module is referenced via source terraform-aws-modules/s3-bucket/aws. The module centralizes arguments for bucket creation, ownership control, and policy attachment.

A basic module invocation is:

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 } }

The bucket argument sets the bucket name. The acl argument sets private. The control_object_ownership boolean enables explicit ownership control. The object_ownership string is set to ObjectWriter. The versioning block enables versioning.

Impact: Using the module reduces repetitive resource definitions and ensures consistent ownership settings across environments. The versioning block enables object versioning, which changes delete behavior and storage cost.

Contextually, this module is the preferred vehicle for the log delivery buckets shown later, where additional policy attachments are required.

Log delivery bucket example:

module "s3_bucket_for_logs" { source = "terraform-aws-modules/s3-bucket/aws" bucket = "my-s3-bucket-for-logs" acl = "log-delivery-write" # Allow deletion of non-empty bucket force_destroy = true control_object_ownership = true object_ownership = "ObjectWriter" attach_elb_log_delivery_policy = true }

The acl is log-delivery-write to allow ELB log delivery. force_destroy = true permits Terraform to delete a non-empty bucket during destroy. The attach_elb_log_delivery_policy flag triggers attachment of the ELB log delivery bucket policy.

A variant for ALB/NLB logs:

module "s3_bucket_for_logs" { source = "terraform-aws-modules/s3-bucket/aws" bucket = "my-s3-bucket-for-logs" # Allow deletion of non-empty bucket force_destroy = true control_object_ownership = true object_ownership = "ObjectWriter" attach_elb_log_delivery_policy = true attach_lb_log_delivery_policy = true }

Here attach_lb_log_delivery_policy = true is required for ALB/NLB logs, in addition to ELB policy.

Impact: Enabling both flags ensures the bucket accepts logs from multiple load balancer types without manual policy authoring.

Contextually, this demonstrates the module's ability to encapsulate multiple policy attachments behind boolean flags.

The module supports creation of a WAF logs bucket:

module "s3_bucket_for_waf_logs" { source = "terraform-aws-modules/s3-bucket/aws" bucket =

The snippet is truncated in the reference material, indicating the pattern continues with bucket name assignment and similar flags.

Feature Support Matrix Of The S3 Bucket Module

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 breadth of support means the module can replace numerous individual resources. The impact for teams is reduced drift and standardized configurations for complex requirements such as replication and object locking.

A tabular view clarifies the scope.

Feature Supported
static web-site hosting yes
access logging yes
versioning yes
CORS yes
lifecycle rules yes
server-side encryption yes
object locking yes
Cross-Region Replication yes
ELB log delivery bucket policy yes
ALB/NLB log delivery bucket policy yes
WAF log delivery bucket policy yes
Account-level Public Access Block yes
S3 Directory Bucket yes
S3 Table Bucket yes
S3 Vectors yes

The presence of S3 Directory Bucket, S3 Table Bucket, and S3 Vectors indicates coverage of newer S3 storage classes beyond classic buckets.

Placeholder Substitution In Bucket Policies

To keep bucket policy with correct S3 bucket and AWS account properties, you can use the placeholders _S3_BUCKET_ID_, _S3_BUCKET_ARN_, and _AWS_ACCOUNT_ID_ 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.

Impact: Placeholders allow a single policy template to be reused across multiple buckets with different names and ARNs without manual string replacement. This reduces copy-paste errors when deploying to multiple accounts.

Contextually, this technique complements the module's prefix support and the jsonencode pattern where interpolation is used for ARNs.

Conditional Creation With create_bucket Argument

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"
create
bucket = false
# ..
}
```

Setting create_bucket = false prevents bucket creation while still allowing the module to be evaluated. The impact is that dependent resources can be conditionally skipped without using count on the module.

Contextually, this is useful in feature-flagged environments where bucket creation is gated.

Provider And Terraform Configuration Block Patterns

The reference material shows a provider configuration used across examples.

terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 3.27" } } required_version = ">= 0.14.9" }

required_providers pins the AWS provider source to hashicorp/aws with version ~> 3.27. required_version enforces Terraform >= 0.14.9.

Provider block:

provider "aws" { profile = "default" region = "ap-south-1" }

profile = "default" specifies the credential profile used to authenticate to AWS. region = "ap-south-1" defines the region in which resources will be created by default.

Impact: Hardcoding profile and region ensures consistent deployment targets. Changing region requires modification of this block and re-apply.

Contextually, this configuration precedes bucket and policy resources in all examples.

Heredoc Policy Definition Pattern

An example of S3 bucket policy using heredoc string format:

```
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.27"
}
}
}
provider "aws" {
profile = "default"
region = "ap-south-1"
}

S3 Bucket on Which we will add policy

resource "awss3bucket" "demo-bucket"{
bucket = "ck-demo-bucket-04th"
}

Resource to attach a bucket policy to a bucket

resource "awss3bucketpolicy" "publicreadaccess" {
bucket = aws
s3bucket.demo-bucket.id
policy = < {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": [ "s3:*" ],
"Resource": [
"${aws
s3bucket.demo-bucket.arn}",
"${aws
s3_bucket.demo-bucket.arn}/*"
]
}
]
}
EOF
}
```

The bucket resource creates ck-demo-bucket-04th. The policy resource attaches a policy allowing s3:* for principal * on both bucket ARN and bucket ARN with /*.

Impact: Heredoc allows direct JSON authoring with interpolation. The broad s3:* action grants full access, which is high risk in production.

Contextually, this pattern is shown as an alternative to aws_iam_policy_document.

IAM Policy Document Data Source Pattern

A reusable pattern uses aws_iam_policy_document data source.

```
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.27"
}
}
}
provider "aws" {
profile = "default"
region = "ap-south-1"
}

S3 Bucket on Which we will add policy

resource "awss3bucket" "demo-bucket"{
bucket = "ck-demo-bucket-04th"
}

Resource to add bucket policy to a

```

The final configuration file expands this pattern:

```
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.27"
}
}
}
provider "aws" {
profile = "default"
region = "ap-south-1"
}

S3 Bucket on Which we will add policy

resource "awss3bucket" "demo-bucket"{
bucket = "ck-demo-bucket-04th"
}

Resource to add bucket policy to a bucket

resource "awss3bucketpolicy" "publicreadaccess" {
bucket = aws
s3bucket.demo-bucket.id
policy = data.aws
iampolicydocument.publicreadaccess.json
}

DataSource to generate a policy document

data "awsiampolicydocument" "publicreadaccess" {
statement {
principals {
type = "*"
identifiers = ["*"]
}
actions = [
"s3:GetObject",
"s3:ListBucket",
]
resources = [
aws
s3bucket.demo-bucket.arn,
"${aws
s3_bucket.demo-bucket.arn}/*",
]
}
}
```

The data source generates a policy document JSON via data.aws_iam_policy_document.public_read_access.json. The statement defines principals type * with identifiers ["*"], actions s3:GetObject and s3:ListBucket, resources include bucket ARN and ARN with /*.

Impact: Using the data source provides validation of policy syntax by Terraform and allows composition of statements via code rather than raw JSON.

Contextually, this is the recommended approach alongside the aws_s3_bucket_policy resource.

Prerequisite Conditions For Terraform S3 Operations

The reference material lists prerequisites:

  • An AWS Account: Setup Free Tier Account on AWS In Right Way
  • Required Permission to Create S3 Bucket and Policy
  • AWS CLI
  • An Editor Like Notepad or VS Code

Impact: Missing permissions will cause aws_s3_bucket_policy creation to fail with AccessDenied. AWS CLI is required for authentication and state operations. An editor is needed for configuration authoring.

Contextually, these prerequisites precede the steps for creating a bucket policy.

Creation Workflow And Verification

The article notes clicking on your bucket name and clicking on the Permissions tab as shown below screenshot. Scroll down to the Bucket policy section and you will see our public read-only policy got attached to our bucket.

Impact: Manual verification in the AWS console confirms Terraform apply succeeded. The Permissions tab displays the effective policy document.

Contextually, this verification step closes the loop between Terraform configuration and AWS reality.

Cleanup And Destruction Workflow

Finally, if you are doing this exercise for learning purposes, you can clean up by destroying the created resource.

terraform destroy

Type yes, and hit enter

Once you hit enter, your resources get destroyed. Once done, this is how you see destruction complete message.

Impact: Destroying resources prevents ongoing charges for buckets with storage. force_destroy = true on modules is required for non-empty buckets to allow destroy.

Contextually, cleanup is essential for ephemeral test environments.

Deprecation Of Inline Policy Parameter

How to Create S3 Bucket Policy using Terraform

When it comes to creating an S3 bucket policy using terraform, there are two ways in which you can do it:

  • Using policy parameter in the resource awss3bucket
  • Creating a awss3bucket_policy resource(recommended)

As of now, specifying policy in the awss3bucket resource is the old way of doing it and is already deprecated.

Conclusion
In this article, we learnt How to Create S3 Bucket Policy using Terraform. You can use the standalone resource awss3bucketpolicy to create a policy or use policy parameter in the resource awss3_bucket.

However, using policy parameter on bucket resource is deprecated and it is recommended to use awss3bucket_policy resource to attach an s3 bucket policy to your bucket.

Impact: Continuing to use the deprecated policy argument on aws_s3_bucket risks future incompatibility and loss of support. Migration to aws_s3_bucket_policy is required for long-term stability.

Contextually, this deprecation aligns with Terraform AWS provider design where bucket creation and policy attachment are separate concerns.

Conclusion

The material establishes a comprehensive control plane for AWS S3 bucket policies expressed through Terraform. The retrieval and import of existing policies into state, followed by full replacement via aws_s3_bucket_policy, creates a deterministic lifecycle where drift is eliminated but requires explicit preservation of all statements. The community module encapsulates a wide feature set spanning static hosting, logging, versioning, CORS, lifecycle, encryption, object locking, replication, and specialized bucket types, with boolean flags for ELB, ALB/NLB, and WAF log delivery policies. Placeholder tokens enable reusable policy templates across prefixes and accounts, while the create_bucket argument provides conditional creation without module-level count. Provider configuration pins AWS provider version and enforces region and profile consistency. Policy authoring can be expressed as raw JSON via heredoc or generated programmatically via aws_iam_policy_document, the latter offering syntax validation and composability. Prerequisites of AWS credentials, permissions, CLI, and editor support the workflow, and destruction via terraform destroy provides cleanup. The deprecation of the inline policy parameter on aws_s3_bucket reinforces the separation of bucket lifecycle and policy lifecycle, making aws_s3_bucket_policy the durable pattern for policy management.

Sources

  1. How to update existing S3 bucket policy using Terraform
  2. terraform-aws-modules/terraform-aws-s3-bucket
  3. How to Create S3 Bucket Policy using Terraform

Related Posts