Terraform AWS S3 Bucket Policy Management With Module And Direct Resource Patterns

Introduction

Terraform configuration for AWS S3 bucket policy represents a persistent operational challenge for infrastructure teams who must reconcile existing cloud assets with declarative state management. The problem space centers on the awss3bucketpolicy resource, the terraform-aws-modules/terraform-aws-s3-bucket module, and the provider configuration patterns that govern authentication and region targeting. The reference material establishes two primary creation paths for policy attachment, a module-based approach that encapsulates S3 bucket features, and a direct resource approach that exposes the awss3bucket and awss3bucketpolicy resources in user-authored configuration.

The material emphasizes that Terraform will replace the entire bucket policy when the awss3bucket_policy resource is applied, which creates an impact layer for operators who manage legacy buckets. The consequence of replacement is total policy overwrite, meaning existing statements that are not explicitly redeclared in the Terraform configuration will be removed from the bucket. This behavior drives the requirement to retrieve the existing bucket policy using a data source or by importing the existing policy into Terraform state before defining the resource.

The module approach supports 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, and S3 Vectors. The module exposes arguments such as bucket, acl, controlobjectownership, objectownership, versioning, forcedestroy, attachelblogdeliverypolicy, attachlblogdeliverypolicy, and createbucket. The direct resource approach documents a provider block with profile = "default" and region = "ap-south-1", requiredproviders aws source "hashicorp/aws" version "~> 3.27", and requiredversion ">= 0.14.9". The direct approach also demonstrates policy definition via heredoc string format and via reusable awsiampolicydocument data source.

The operational context includes prerequisites of an AWS Account, required permission to create S3 bucket and policy, AWS CLI, and an editor like Notepad or VS Code. Cleanup is performed with terraform destroy and confirmation with yes. The article notes that specifying policy in the awss3bucket resource is deprecated and that the recommended pattern is to create a awss3bucket_policy resource to attach an S3 bucket policy to a bucket.

Updating Existing Bucket Policy With Terraform State Import

Retrieving an existing bucket policy before codification is a required first step when the bucket is already present in the AWS account but not yet represented in Terraform state. The reference material states that first, retrieve the existing bucket policy using a data source or by importing the existing policy into your Terraform state.

Once retrieval is completed, define the awss3bucketpolicy resource in your Terraform configuration, referencing the existing bucket. In the policy attribute of this resource, include both the existing policy statements and the new policy you want to add. The example code shows a resource named awss3bucketpolicy.bucketpolicy with bucket = awss3bucket.yourbucket.id and 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 = ["${awss3bucket.your_bucket.arn}/*"] } ] }).

The impact of this pattern is that the operator must maintain fidelity between the live policy and the declared policy. If the existing statements are omitted from the jsonencode block, Terraform will remove them on apply because Terraform will replace the entire bucket policy when you apply this configuration. The contextual layer connects this replacement behavior to the module approach where attachelblogdeliverypolicy and attachlblogdeliverypolicy manage pre-defined policy statements for log delivery, reducing manual statement merging.

The code fragment provided for update is:

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 placeholder comments must be replaced with actual existing policy statements and the new policy statement to add. The consequence for the user is a need for manual policy audit before each apply.

Module-Based S3 Bucket Provisioning And Policy Attachment

The terraform-aws-modules/terraform-aws-s3-bucket module creates S3 bucket on AWS with all or almost all features provided by Terraform AWS provider. The supported features are enumerated as 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, and S3 Vectors.

The features can be structured for comparison:

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

The module example for a standard bucket 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 impact of controlobjectownership and object_ownership settings is to enforce explicit ownership controls, which prevents unexpected ACL behavior when multiple accounts interact with the bucket.

The module example for logs with ELB policy is:

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 }

The force_destroy argument allows deletion of non-empty bucket, which has real-world consequence of data loss if applied unintentionally.

A variant for ALB and NLB logs shows:

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 comment notes attachelblogdeliverypolicy is required for ALB logs and attachlblogdeliverypolicy is required for ALB/NLB logs. The contextual layer ties this to the update pattern where entire policy replacement would otherwise require manual merging of log delivery statements.

Placeholder substitution is supported to keep bucket policy with correct S3 bucket and AWS account properties. Placeholders S3BUCKETID, S3BUCKETARN, and AWSACCOUNTID in the policy document will be replaced with the actual values during the policy attachment. This is especially useful when using bucket prefixes.

Conditional creation is addressed because Terraform does not allow to use count inside module block. The solution is to specify argument create_bucket.

module "s3_bucket" { source = "terraform-aws-modules/s3-bucket/aws" create_bucket = false }

The comment states this S3 bucket will not be created. The impact is that module inputs can be used for feature toggles without module duplication.

Direct Resource Policy Creation Patterns

The direct resource pattern begins with provider configuration. The reference material specifies:

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

Provider profile and region in which all resources will create:

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

The provider profile default is used to authenticate to AWS and region ap-south-1 is used as default.

Prerequisites listed are An AWS Account with Free Tier setup, Required Permission to Create S3 Bucket and Policy, AWS CLI, and An Editor Like Notepad or VS Code.

Two ways to create S3 bucket policy using terraform are documented:

  • 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.

S3 Bucket Policy using heredoc string format example:

terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 3.27" } } } provider "aws" { profile = "default" region = "ap-south-1" } resource "aws_s3_bucket" "demo-bucket"{ bucket = "ck-demo-bucket-04th" } resource "aws_s3_bucket_policy" "public_read_access" { bucket = aws_s3_bucket.demo-bucket.id policy = <<EOF { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": "*", "Action": [ "s3:*" ], "Resource": [ "${aws_s3_bucket.demo-bucket.arn}", "${aws_s3_bucket.demo-bucket.arn}/*" ] } ] } EOF }

The impact of heredoc is readability for static JSON but lack of validation before apply.

S3 Bucket Policy using reusable awsiampolicy_document example:

terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 3.27" } } } provider "aws" { profile = "default" region = "ap-south-1" } resource "aws_s3_bucket" "demo-bucket"{ bucket = "ck-demo-bucket-04th" } resource "aws_s3_bucket_policy" "public_read_access" { bucket = aws_s3_bucket.demo-bucket.id policy = data.aws_iam_policy_document.public_read_access.json }

Data source to generate policy document:

data "aws_iam_policy_document" "public_read_access" { statement { principals { type = "*" identifiers = ["*"] } actions = [ "s3:GetObject", "s3:ListBucket", ] resources = [ aws_s3_bucket.demo-bucket.arn, "${aws_s3_bucket.demo-bucket.arn}/*", ] } }

The final configuration file to create S3 bucket policy using Terraform combines these elements. The impact of using data.awsiampolicy_document is compile-time validation and reusability across statements.

The material notes that you can see the destruction complete message after terraform destroy and type yes and hit enter.

The conclusion reiterates that you can use the standalone resource awss3bucketpolicy to create a policy or use policy parameter in the resource awss3bucket. However, using policy parameter on bucket resource is deprecated and it is recommended to use awss3bucketpolicy resource to attach an s3 bucket policy to your bucket.

Verification in AWS Console involves clicking on bucket name and click 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.

Policy Placeholder Substitution And Conditional Creation

Placeholder substitution with S3BUCKETID, S3BUCKETARN, and AWSACCOUNTID ensures policy documents remain portable across environments. The impact is reduced drift when bucket prefixes are used. Conditional creation via create_bucket = false prevents module execution while preserving module configuration in code. This avoids Terraform errors about count inside module block.

Conclusion

The reference material establishes a complete operational map for Terraform AWS S3 bucket policy management that spans updating existing policies, module-based provisioning, and direct resource authoring. The update workflow requires explicit retrieval and merging of statements because Terraform replaces entire policy on apply. The module approach provides a feature-rich abstraction with pre-built policies for ELB, ALB/NLB, and WAF log delivery, plus placeholder substitution for bucket identifiers. The direct resource approach documents provider configuration with profile default and region ap-south-1, deprecation of policy parameter on awss3bucket, and recommendation of awss3bucketpolicy with either heredoc JSON or data.awsiampolicydocument generation. The prerequisites of AWS account, permissions, CLI, and editor, and the cleanup via terraform destroy complete the lifecycle. The persistent theme is that policy replacement behavior forces careful statement management, and that module arguments such as createbucket, forcedestroy, controlobjectownership, and attachelblogdeliverypolicy provide safe toggles for production deployments.

Sources

  1. Source Name
  2. Source Name
  3. Source Name

Related Posts