s3-bucket
Creates a hardened S3 bucket with encryption, versioning, lifecycle rules, access logging, and optional cross-region replication.
Provisions an S3 bucket with all public access blocked by default, server-side encryption (aws:kms or AES256), object versioning, configurable lifecycle transitions and expirations, access log delivery to a target bucket, and optional cross-region replication with per-rule destination encryption keys.
Usage
Minimal
The smallest valid call. Versioning, SSE-KMS with the AWS-managed key, BucketOwnerEnforced ownership, and a fully-blocking public access block are all applied by default.
module "minimal_bucket" {
source = "hcassc.jfrog.io/iac-tf-modules-virtual/storage/s3-bucket/aws"
version = "0.1.0"
bucket_name = "my-org-minimal-bucket"
}
Customer-managed KMS key
Recommended for most HCA workloads. Passing an explicit kms_key_id also means the module's kms_key_id output actually returns a usable ARN — see the Known limitations note below on why the AWS-managed key case differs.
module "data_bucket" {
source = "hcassc.jfrog.io/iac-tf-modules-virtual/storage/s3-bucket/aws"
version = "0.1.0"
bucket_name = "my-org-data-bucket"
kms_key_id = module.kms.key_arn
lifecycle_rules = {
archive = {
transitions = [
{ days = 90, storage_class = "STANDARD_IA" },
{ days = 365, storage_class = "GLACIER" },
]
}
}
}
Lifecycle rules with expiration
Each transition's days must be less than the rule's expiration_days when both are set — a transition scheduled to happen after the object has already expired is rejected at terraform validate, not silently accepted as dead configuration.
module "logs_bucket" {
source = "hcassc.jfrog.io/iac-tf-modules-virtual/storage/s3-bucket/aws"
version = "0.1.0"
bucket_name = "my-org-app-logs"
lifecycle_rules = {
standard_tiering = {
prefix = "logs/"
abort_incomplete_multipart_upload_days = 7
noncurrent_version_expiration_days = 90
expiration_days = 365
transitions = [
{ days = 30, storage_class = "STANDARD_IA" },
{ days = 90, storage_class = "GLACIER" },
{ days = 180, storage_class = "DEEP_ARCHIVE" },
]
}
short_lived_temp = {
prefix = "tmp/"
expiration_days = 14
# transitions omitted -> object simply expires at day 14, no tiering
}
}
}
Access logging
module "app_bucket" {
source = "hcassc.jfrog.io/iac-tf-modules-virtual/storage/s3-bucket/aws"
version = "0.1.0"
bucket_name = "my-org-app-bucket"
logging_target_bucket = "my-org-access-logs"
logging_target_prefix = "app-bucket-logs/"
}
logging_target_bucket must already exist and accept log delivery writes — this module does not validate its format, existence, or permissions. See Known limitations.
Cross-region / cross-account replication
Replication requires versioning_enabled = true (enforced at terraform validate) and a pre-existing IAM role with the standard S3 replication trust policy and permissions.
module "replicated_bucket" {
source = "hcassc.jfrog.io/iac-tf-modules-virtual/storage/s3-bucket/aws"
version = "0.1.0"
bucket_name = "my-org-primary-bucket"
versioning_enabled = true
replication_role_arn = "arn:aws:iam::123456789012:role/s3-replication-role"
replication_rules = {
same_region_dr = {
destination_bucket_arn = "arn:aws:s3:::my-org-dr-bucket"
priority = 1
}
cross_account_archive = {
destination_bucket_arn = "arn:aws:s3:::my-org-archive-bucket"
destination_account_id = "234567890123"
destination_kms_key_id = "arn:aws:kms:us-east-1:234567890123:key/abcd1234-ef56-7890-ab12-cd34ef567890"
priority = 2
}
}
}
Every rule needs a unique priority — duplicate priorities across rules in the same replication_rules map are rejected at terraform validate.
Object Lock (compliance / WORM)
object_lock_enabled can only be set at bucket creation and can never be disabled afterward. It also requires versioning_enabled = true.
module "compliance_bucket" {
source = "hcassc.jfrog.io/iac-tf-modules-virtual/storage/s3-bucket/aws"
version = "0.1.0"
bucket_name = "my-org-compliance-records"
versioning_enabled = true
object_lock_enabled = true
object_lock_default_retention = {
mode = "GOVERNANCE"
days = 365
}
}
⚠️ Warning
A bucket with Object Lock enabled and objects written to it cannot be destroyed until each object's retention period expires (
GOVERNANCEmode can be bypassed withs3:BypassGovernanceRetentionpermission and explicit action;COMPLIANCEmode cannot be bypassed at all, by anyone, including the account root). An empty Object Lock bucket can still be destroyed normally. Do not apply this configuration against a bucket you intend to tear down casually — test with a plain bucket first.
Restrictive public access overrides
All four public access block settings default to true (fully blocking). Override only when you have a specific, reviewed reason — for example, a bucket serving public static assets behind CloudFront with OAC, where the bucket policy (not ACLs) governs access.
module "public_assets_bucket" {
source = "hcassc.jfrog.io/iac-tf-modules-virtual/storage/s3-bucket/aws"
version = "0.1.0"
bucket_name = "my-org-public-assets"
object_ownership = "BucketOwnerEnforced"
block_public_acls = true
block_public_policy = false
ignore_public_acls = true
restrict_public_buckets = false
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Sid = "PublicReadGetObject"
Effect = "Allow"
Principal = "*"
Action = "s3:GetObject"
Resource = "arn:aws:s3:::my-org-public-assets/*"
}]
})
}
Full example
Combines encryption, lifecycle, logging, tags, and a custom object ownership mode in one call.
module "full_example" {
source = "hcassc.jfrog.io/iac-tf-modules-virtual/storage/s3-bucket/aws"
version = "0.1.0"
bucket_name = "my-org-full-example"
versioning_enabled = true
sse_algorithm = "aws:kms"
kms_key_id = module.kms.key_arn
bucket_key_enabled = true
object_ownership = "BucketOwnerEnforced"
logging_target_bucket = "my-org-access-logs"
logging_target_prefix = "full-example/"
lifecycle_rules = {
standard_tiering = {
prefix = "data/"
abort_incomplete_multipart_upload_days = 7
noncurrent_version_expiration_days = 90
expiration_days = 730
transitions = [
{ days = 30, storage_class = "STANDARD_IA" },
{ days = 180, storage_class = "GLACIER" },
]
}
}
tags = {
environment = "production"
owner = "platform-team"
}
}
Known limitations
kms_key_idoutput is empty when using the AWS-managed key. Whenkms_key_idis left unset (the default), AWS automatically encrypts objects with the account's defaultaws/s3managed key — but the AWS provider does not read that resolved key ARN back into Terraform state, so the module'skms_key_idoutput returns an empty string in that case. It only returns a usable value when you supply your ownkms_key_idexplicitly. If a downstream consumer needs the key ARN (for example, to grantkms:Decryptto another principal), pass a customer-managed key rather than relying on the AWS-managed default.mfa_deleteis not exposed. Enabling MFA Delete requires a live MFA-authenticated session (root or IAM user), which is incompatible with this platform's OIDC/assumed-role pipeline authentication. If MFA Delete is required on a specific bucket, it must be enabled manually, out-of-band, by someone with root or IAM-user credentials and a physical/virtual MFA device — Terraform will not manage that attribute.logging_target_bucketis not format- or existence-validated. The module does not check that the target bucket exists, is in a compatible region, or has the correct log-delivery permissions. These are AWS API-level failures atapplytime, not something format validation can catch, so none is applied.- Replication priority uniqueness is stricter than AWS technically requires. AWS only needs unique priorities among rules whose prefixes can overlap on the same object. This module enforces uniqueness across all rules in a
replication_rulesmap regardless of prefix overlap, since prefix-overlap analysis isn't validated either. If you have a legitimate case for two non-overlapping rules sharing a priority, this module will currently reject it.
Requirements
| Name | Version |
|---|---|
| terraform | >= 1.15.0, < 2.0.0 |
| aws | >= 6.0, < 7.0 |
Providers
| Name | Version |
|---|---|
| aws | >= 6.0, < 7.0 |
Resources
| Name | Type |
|---|---|
| aws_s3_bucket.this | resource |
| aws_s3_bucket_lifecycle_configuration.this | resource |
| aws_s3_bucket_logging.this | resource |
| aws_s3_bucket_object_lock_configuration.this | resource |
| aws_s3_bucket_ownership_controls.this | resource |
| aws_s3_bucket_policy.this | resource |
| aws_s3_bucket_public_access_block.this | resource |
| aws_s3_bucket_replication_configuration.this | resource |
| aws_s3_bucket_server_side_encryption_configuration.this | resource |
| aws_s3_bucket_versioning.this | resource |
Inputs
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| bucket_name | Globally unique S3 bucket name | string |
n/a | yes |
| block_public_acls | Block public ACLs | bool |
true |
no |
| block_public_policy | Block public bucket policies | bool |
true |
no |
| bucket_key_enabled | Enable S3 bucket key to reduce KMS API calls | bool |
true |
no |
| force_destroy | Allow destruction of non-empty bucket | bool |
false |
no |
| ignore_public_acls | Ignore public ACLs | bool |
true |
no |
| kms_key_id | KMS key ARN for server-side encryption (null uses aws:kms with managed key) | string |
null |
no |
| lifecycle_rules | Map of lifecycle rules to apply | map(object({ enabled = optional(bool, true) prefix = optional(string, null) abort_incomplete_multipart_upload_days = optional(number, 7) expiration_days = optional(number, null) noncurrent_version_expiration_days = optional(number, 90) transitions = optional(list(object({ days = number storage_class = string })), []) })) | {} |
no |
| logging_target_bucket | S3 bucket name to receive access logs (optional) | string |
null |
no |
| logging_target_prefix | Prefix for access log objects | string |
"logs/" |
no |
| object_lock_default_retention | Default retention rule applied to objects when Object Lock isenabled. mode must be 'GOVERNANCE' or 'COMPLIANCE'. Only usedwhen object_lock_enabled is true. | object({ mode = string days = number }) | null |
no |
| object_lock_enabled | Enable S3 Object Lock on the bucket. Can only be set at bucketcreation, cannot be enabled on an existing bucket. Requiresversioning_enabled = true. | bool |
false |
no |
| object_ownership | Object ownership | string |
"BucketOwnerEnforced" |
no |
| policy | JSON bucket policy document (optional) | string |
null |
no |
| replication_role_arn | IAM role ARN for S3 replication (required when replication_rules is set) | string |
null |
no |
| replication_rules | Map of replication rules | map(object({ destination_bucket_arn = string destination_kms_key_id = optional(string, null) destination_account_id = optional(string, null) prefix = optional(string, "") priority = optional(number, 1) delete_marker_replication = optional(bool, true) })) | {} |
no |
| restrict_public_buckets | Restrict public bucket access | bool |
true |
no |
| sse_algorithm | SSE algorithm (aws:kms or AES256) | string |
"aws:kms" |
no |
| tags | Resource tags to apply to all resources | map(string) |
{} |
no |
| versioning_enabled | Enable object versioning | bool |
true |
no |
Outputs
| Name | Description |
|---|---|
| arn | S3 bucket ARN |
| bucket_domain_name | Bucket-style domain name |
| bucket_regional_domain_name | Regional domain name (for CloudFront origins) |
| hosted_zone_id | Route 53 hosted zone ID for the bucket's region |
| id | S3 bucket name |
| kms_key_id | KMS key ARN actually applied for SSE-KMS encryption |
| object_lock_enabled | Whether S3 Object Lock is enabled on the bucket |
| replication_configuration_id | Replication configuration ID, if replication is configured |
| versioning_status | Effective versioning status of the bucket |