IaC AWS

Terraform Module: AWS CloudFront — a secure CDN distribution with OAC, managed policies, and TLS

Quick take — A reusable hashicorp/aws ~> 5.0 module for aws_cloudfront_distribution: S3 Origin Access Control, custom + S3 origins, managed cache/origin-request policies, ACM TLS, WAF, and custom error responses. New here? Jump to the Quickstart below to deploy it in minutes; read on for how it works and when to reach for it.

Quickstart (copy-paste)

Minimal, runnable configuration — drop this in a .tf file and fill in the "..." placeholders (each required input is commented):

provider "aws" {
  region = "us-east-1"
}

module "cloudfront" {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-cloudfront?ref=v1.0.0"

  name                   = "..."           # Logical name; used in comment, OAC name, and `Name` tag.
  origins                = ["...", "..."]  # Origins to fetch from; `use_oac` for private S3, `custo…
  default_cache_behavior = {}              # Catch-all behavior; `cache_policy_name` is an AWS-manag…
}

Then terraform init && terraform apply. Every other input has a sensible default — see Inputs below to override behaviour.

What this module is

Amazon CloudFront is AWS’s global content delivery network (CDN). It puts your content on hundreds of edge locations so that a viewer in Mumbai, Frankfurt, or São Paulo terminates TLS and is served close to home, while CloudFront fetches from your origin (an S3 bucket, an ALB, an API Gateway, or any custom HTTP server) only on a cache miss. Beyond raw caching it is also a security perimeter: it terminates HTTPS with an ACM certificate, can sit in front of a private S3 bucket via Origin Access Control (OAC) so the bucket is never public, attaches an AWS WAF web ACL, and enforces geo-restrictions — all at the edge, before traffic ever reaches your account.

aws_cloudfront_distribution is one of the gnarlier resources in the AWS provider. It nests origin, default_cache_behavior, ordered_cache_behavior, viewer_certificate, restrictions, custom_error_response, and logging_config blocks, each with its own required-vs-optional minefield. Three things bite people repeatedly: the ACM certificate must live in us-east-1 no matter where the rest of your stack runs; you should drive caching with managed cache and origin-request policies rather than the deprecated forwarded_values block; and CloudFront-to-S3 access should use OAC (the modern replacement for Origin Access Identity). This module wraps all of that into one var-driven building block that bakes in TLS-1.2 minimums, optional OAC wiring, managed-policy lookups, and consistent tagging — so every distribution your org ships is secure and uniform by default.

When to use it

If you only need object storage with no edge caching, you do not need this — serve from S3 directly. If your “CDN” is purely internal east-west traffic, CloudFront is the wrong tool.

Module structure

terraform-module-aws-cloudfront/
├── versions.tf      # provider + Terraform version pins
├── main.tf          # OAC, managed-policy lookups, aws_cloudfront_distribution
├── variables.tf     # var-driven inputs with validations
└── outputs.tf       # id, arn, domain_name, hosted_zone_id, OAC id
# versions.tf
terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}
# main.tf

locals {
  tags = merge(
    var.tags,
    {
      Name      = var.name
      ManagedBy = "terraform"
    },
  )

  # Create an OAC only when at least one origin opts into it.
  use_oac = length([for o in var.origins : o if o.use_oac]) > 0
}

# ---------------------------------------------------------------------------
# Origin Access Control — modern, SigV4-signed access to private S3 origins.
# Replaces the legacy Origin Access Identity. The S3 bucket policy must grant
# cloudfront.amazonaws.com read access scoped to this distribution's ARN.
# ---------------------------------------------------------------------------
resource "aws_cloudfront_origin_access_control" "this" {
  count = local.use_oac ? 1 : 0

  name                              = "${var.name}-oac"
  description                       = "OAC for ${var.name}"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

# ---------------------------------------------------------------------------
# Managed policy lookups — drive caching declaratively instead of the
# deprecated forwarded_values block. Names map to AWS-managed policies, e.g.
# "Managed-CachingOptimized", "Managed-CachingDisabled", "Managed-AllViewer".
# ---------------------------------------------------------------------------
data "aws_cloudfront_cache_policy" "default" {
  name = var.default_cache_behavior.cache_policy_name
}

data "aws_cloudfront_origin_request_policy" "default" {
  count = var.default_cache_behavior.origin_request_policy_name != null ? 1 : 0
  name  = var.default_cache_behavior.origin_request_policy_name
}

data "aws_cloudfront_cache_policy" "ordered" {
  for_each = { for b in var.ordered_cache_behaviors : b.path_pattern => b }
  name     = each.value.cache_policy_name
}

data "aws_cloudfront_origin_request_policy" "ordered" {
  for_each = {
    for b in var.ordered_cache_behaviors :
    b.path_pattern => b if b.origin_request_policy_name != null
  }
  name = each.value.origin_request_policy_name
}

resource "aws_cloudfront_distribution" "this" {
  enabled             = var.enabled
  is_ipv6_enabled     = var.is_ipv6_enabled
  comment             = var.comment
  default_root_object = var.default_root_object
  aliases             = var.aliases
  price_class         = var.price_class
  web_acl_id          = var.web_acl_id
  http_version        = var.http_version
  retain_on_delete    = var.retain_on_delete
  wait_for_deployment = var.wait_for_deployment

  dynamic "origin" {
    for_each = { for o in var.origins : o.origin_id => o }
    content {
      origin_id                = origin.value.origin_id
      domain_name              = origin.value.domain_name
      origin_path              = origin.value.origin_path
      connection_attempts      = origin.value.connection_attempts
      connection_timeout       = origin.value.connection_timeout
      origin_access_control_id = origin.value.use_oac ? aws_cloudfront_origin_access_control.this[0].id : null

      # Custom (non-S3) origins: ALB, API Gateway, any HTTP server.
      dynamic "custom_origin_config" {
        for_each = origin.value.custom_origin_config != null ? [origin.value.custom_origin_config] : []
        content {
          http_port              = custom_origin_config.value.http_port
          https_port             = custom_origin_config.value.https_port
          origin_protocol_policy = custom_origin_config.value.origin_protocol_policy
          origin_ssl_protocols   = custom_origin_config.value.origin_ssl_protocols
        }
      }

      dynamic "custom_header" {
        for_each = origin.value.custom_headers
        content {
          name  = custom_header.value.name
          value = custom_header.value.value
        }
      }
    }
  }

  # -------------------------------------------------------------------------
  # Default behavior — matches anything not caught by an ordered behavior.
  # -------------------------------------------------------------------------
  default_cache_behavior {
    target_origin_id         = var.default_cache_behavior.target_origin_id
    viewer_protocol_policy   = var.default_cache_behavior.viewer_protocol_policy
    allowed_methods          = var.default_cache_behavior.allowed_methods
    cached_methods           = var.default_cache_behavior.cached_methods
    compress                 = var.default_cache_behavior.compress
    cache_policy_id          = data.aws_cloudfront_cache_policy.default.id
    origin_request_policy_id = try(data.aws_cloudfront_origin_request_policy.default[0].id, null)
    response_headers_policy_id = var.default_cache_behavior.response_headers_policy_id

    dynamic "function_association" {
      for_each = var.default_cache_behavior.function_associations
      content {
        event_type   = function_association.value.event_type
        function_arn = function_association.value.function_arn
      }
    }
  }

  # -------------------------------------------------------------------------
  # Ordered behaviors — first match wins, e.g. /api/* to the ALB origin.
  # -------------------------------------------------------------------------
  dynamic "ordered_cache_behavior" {
    for_each = { for b in var.ordered_cache_behaviors : b.path_pattern => b }
    content {
      path_pattern             = ordered_cache_behavior.value.path_pattern
      target_origin_id         = ordered_cache_behavior.value.target_origin_id
      viewer_protocol_policy   = ordered_cache_behavior.value.viewer_protocol_policy
      allowed_methods          = ordered_cache_behavior.value.allowed_methods
      cached_methods           = ordered_cache_behavior.value.cached_methods
      compress                 = ordered_cache_behavior.value.compress
      cache_policy_id          = data.aws_cloudfront_cache_policy.ordered[ordered_cache_behavior.key].id
      origin_request_policy_id = try(data.aws_cloudfront_origin_request_policy.ordered[ordered_cache_behavior.key].id, null)
      response_headers_policy_id = ordered_cache_behavior.value.response_headers_policy_id
    }
  }

  # SPA-friendly rewrites and friendly error pages.
  dynamic "custom_error_response" {
    for_each = { for e in var.custom_error_responses : tostring(e.error_code) => e }
    content {
      error_code            = custom_error_response.value.error_code
      response_code         = custom_error_response.value.response_code
      response_page_path    = custom_error_response.value.response_page_path
      error_caching_min_ttl = custom_error_response.value.error_caching_min_ttl
    }
  }

  # -------------------------------------------------------------------------
  # TLS — a custom-domain cert (ACM, us-east-1) OR the default *.cloudfront.net.
  # -------------------------------------------------------------------------
  viewer_certificate {
    cloudfront_default_certificate = var.acm_certificate_arn == null
    acm_certificate_arn            = var.acm_certificate_arn
    ssl_support_method             = var.acm_certificate_arn != null ? "sni-only" : null
    minimum_protocol_version       = var.acm_certificate_arn != null ? var.minimum_protocol_version : "TLSv1"
  }

  restrictions {
    geo_restriction {
      restriction_type = var.geo_restriction.restriction_type
      locations        = var.geo_restriction.locations
    }
  }

  dynamic "logging_config" {
    for_each = var.logging_config != null ? [var.logging_config] : []
    content {
      bucket          = logging_config.value.bucket
      prefix          = logging_config.value.prefix
      include_cookies = logging_config.value.include_cookies
    }
  }

  tags = local.tags
}
# variables.tf

variable "name" {
  description = "Logical name for the distribution; used in the comment, OAC name, and Name tag."
  type        = string

  validation {
    condition     = can(regex("^[a-zA-Z0-9._-]{1,128}$", var.name))
    error_message = "name must be 1-128 chars of letters, digits, dots, hyphens or underscores."
  }
}

variable "enabled" {
  description = "Whether the distribution accepts end-user requests for content."
  type        = bool
  default     = true
}

variable "comment" {
  description = "Free-text comment shown in the console (<= 128 chars)."
  type        = string
  default     = "Managed by Terraform"

  validation {
    condition     = length(var.comment) <= 128
    error_message = "comment must be 128 characters or fewer (AWS limit)."
  }
}

variable "aliases" {
  description = "Custom domain names (CNAMEs) served by this distribution, e.g. [\"cdn.example.com\"]. Each must be covered by the ACM certificate."
  type        = list(string)
  default     = []
}

variable "default_root_object" {
  description = "Object returned for requests to the root URL, typically index.html for static sites."
  type        = string
  default     = null
}

variable "is_ipv6_enabled" {
  description = "Serve content over IPv6 in addition to IPv4."
  type        = bool
  default     = true
}

variable "http_version" {
  description = "Maximum HTTP version viewers can use: http1.1, http2, http2and3, or http3."
  type        = string
  default     = "http2and3"

  validation {
    condition     = contains(["http1.1", "http2", "http2and3", "http3"], var.http_version)
    error_message = "http_version must be one of: http1.1, http2, http2and3, http3."
  }
}

variable "price_class" {
  description = "Edge-location footprint: PriceClass_100 (NA/EU), PriceClass_200 (+Asia/ME/Africa), PriceClass_All (everywhere)."
  type        = string
  default     = "PriceClass_100"

  validation {
    condition     = contains(["PriceClass_100", "PriceClass_200", "PriceClass_All"], var.price_class)
    error_message = "price_class must be PriceClass_100, PriceClass_200, or PriceClass_All."
  }
}

variable "web_acl_id" {
  description = "ARN of an AWS WAFv2 web ACL (must be CLOUDFRONT scope, created in us-east-1). Null to skip WAF."
  type        = string
  default     = null
}

variable "origins" {
  description = <<-EOT
    Origins CloudFront fetches from. Set use_oac = true for a PRIVATE S3 bucket
    (domain_name is the bucket's regional REST endpoint). For ALB/API Gateway/HTTP
    origins, set custom_origin_config and leave use_oac = false.
  EOT
  type = list(object({
    origin_id           = string
    domain_name         = string
    origin_path         = optional(string)
    use_oac             = optional(bool, false)
    connection_attempts = optional(number, 3)
    connection_timeout  = optional(number, 10)
    custom_headers = optional(list(object({
      name  = string
      value = string
    })), [])
    custom_origin_config = optional(object({
      http_port              = optional(number, 80)
      https_port             = optional(number, 443)
      origin_protocol_policy = optional(string, "https-only")
      origin_ssl_protocols   = optional(list(string), ["TLSv1.2"])
    }))
  }))

  validation {
    condition     = length(var.origins) > 0
    error_message = "Provide at least one origin."
  }

  validation {
    condition     = length(distinct([for o in var.origins : o.origin_id])) == length(var.origins)
    error_message = "Every origin must have a unique origin_id."
  }

  validation {
    # OAC is for S3 only; an origin cannot be both OAC and a custom HTTP origin.
    condition = alltrue([
      for o in var.origins : !(o.use_oac && o.custom_origin_config != null)
    ])
    error_message = "An origin cannot set both use_oac = true and custom_origin_config."
  }
}

variable "default_cache_behavior" {
  description = "Behavior for any request not matched by an ordered behavior. cache_policy_name references an AWS-managed cache policy."
  type = object({
    target_origin_id           = string
    cache_policy_name          = optional(string, "Managed-CachingOptimized")
    origin_request_policy_name = optional(string)
    response_headers_policy_id = optional(string)
    viewer_protocol_policy     = optional(string, "redirect-to-https")
    allowed_methods            = optional(list(string), ["GET", "HEAD", "OPTIONS"])
    cached_methods             = optional(list(string), ["GET", "HEAD"])
    compress                   = optional(bool, true)
    function_associations = optional(list(object({
      event_type   = string
      function_arn = string
    })), [])
  })

  validation {
    condition     = contains(["allow-all", "https-only", "redirect-to-https"], var.default_cache_behavior.viewer_protocol_policy)
    error_message = "viewer_protocol_policy must be allow-all, https-only, or redirect-to-https."
  }
}

variable "ordered_cache_behaviors" {
  description = "Path-pattern behaviors evaluated in order (e.g. /api/* to a dynamic origin with caching disabled)."
  type = list(object({
    path_pattern               = string
    target_origin_id           = string
    cache_policy_name          = optional(string, "Managed-CachingDisabled")
    origin_request_policy_name = optional(string)
    response_headers_policy_id = optional(string)
    viewer_protocol_policy     = optional(string, "redirect-to-https")
    allowed_methods            = optional(list(string), ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"])
    cached_methods             = optional(list(string), ["GET", "HEAD"])
    compress                   = optional(bool, true)
  }))
  default = []

  validation {
    condition     = length(distinct([for b in var.ordered_cache_behaviors : b.path_pattern])) == length(var.ordered_cache_behaviors)
    error_message = "Each ordered_cache_behaviors entry must have a unique path_pattern."
  }
}

variable "custom_error_responses" {
  description = "Custom error handling, e.g. map 403/404 to /index.html for SPAs with response_code 200."
  type = list(object({
    error_code            = number
    response_code         = optional(number)
    response_page_path    = optional(string)
    error_caching_min_ttl = optional(number, 10)
  }))
  default = []
}

variable "acm_certificate_arn" {
  description = "ARN of an ACM certificate IN us-east-1 covering all aliases. Null uses the default *.cloudfront.net certificate."
  type        = string
  default     = null

  validation {
    condition     = var.acm_certificate_arn == null || can(regex("^arn:aws:acm:us-east-1:[0-9]{12}:certificate/", var.acm_certificate_arn))
    error_message = "acm_certificate_arn must be an ACM certificate ARN in us-east-1 (CloudFront requirement)."
  }
}

variable "minimum_protocol_version" {
  description = "Minimum TLS version for viewer connections when using a custom certificate."
  type        = string
  default     = "TLSv1.2_2021"

  validation {
    condition = contains(
      ["TLSv1.2_2018", "TLSv1.2_2019", "TLSv1.2_2021"],
      var.minimum_protocol_version
    )
    error_message = "minimum_protocol_version must be TLSv1.2_2018, TLSv1.2_2019, or TLSv1.2_2021 (no TLS 1.0/1.1)."
  }
}

variable "geo_restriction" {
  description = "Geo restriction. type 'none', 'whitelist' (allow listed), or 'blacklist' (deny listed); locations are ISO 3166-1 alpha-2 codes."
  type = object({
    restriction_type = optional(string, "none")
    locations        = optional(list(string), [])
  })
  default = {}

  validation {
    condition     = contains(["none", "whitelist", "blacklist"], var.geo_restriction.restriction_type)
    error_message = "geo_restriction.restriction_type must be none, whitelist, or blacklist."
  }
}

variable "logging_config" {
  description = "Standard access logging to an S3 bucket (bucket must be the bucket's domain name, e.g. logs.s3.amazonaws.com). Null disables logging."
  type = object({
    bucket          = string
    prefix          = optional(string, "")
    include_cookies = optional(bool, false)
  })
  default = null
}

variable "retain_on_delete" {
  description = "Disable rather than delete the distribution on terraform destroy (CloudFront deletes can be slow)."
  type        = bool
  default     = false
}

variable "wait_for_deployment" {
  description = "Block terraform apply until the distribution finishes deploying to all edges (can take 5-15 min)."
  type        = bool
  default     = true
}

variable "tags" {
  description = "Tags applied to the distribution."
  type        = map(string)
  default     = {}
}
# outputs.tf

output "id" {
  description = "The distribution ID (e.g. E2QWRUHEXAMPLE); use for invalidations and WAF associations."
  value       = aws_cloudfront_distribution.this.id
}

output "arn" {
  description = "The ARN of the distribution (reference in S3 bucket policies for OAC scoping)."
  value       = aws_cloudfront_distribution.this.arn
}

output "domain_name" {
  description = "The CloudFront domain (e.g. d111111abcdef8.cloudfront.net); the alias target for Route 53."
  value       = aws_cloudfront_distribution.this.domain_name
}

output "hosted_zone_id" {
  description = "CloudFront's fixed hosted zone ID for Route 53 alias records (Z2FDTNDATAQYW2)."
  value       = aws_cloudfront_distribution.this.hosted_zone_id
}

output "status" {
  description = "Current distribution status (Deployed or InProgress)."
  value       = aws_cloudfront_distribution.this.status
}

output "etag" {
  description = "Current version identifier of the distribution's configuration."
  value       = aws_cloudfront_distribution.this.etag
}

output "origin_access_control_id" {
  description = "ID of the created Origin Access Control, or null when no origin uses OAC."
  value       = try(aws_cloudfront_origin_access_control.this[0].id, null)
}

How to use it

This example fronts a private S3 bucket (OAC, SPA error rewrites) and routes /api/* to an ALB origin with caching disabled, on a custom domain with an ACM certificate from us-east-1.

# CloudFront's ACM certificate MUST be in us-east-1, regardless of your app region.
provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"
}

module "cloudfront" {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-cloudfront?ref=v1.0.0"

  name                = "web-prod"
  comment             = "Production web app + API edge"
  aliases             = ["cdn.example.com"]
  default_root_object = "index.html"
  price_class         = "PriceClass_200"
  web_acl_id          = aws_wafv2_web_acl.cloudfront.arn

  origins = [
    {
      origin_id   = "s3-static"
      domain_name = aws_s3_bucket.site.bucket_regional_domain_name
      use_oac     = true
    },
    {
      origin_id   = "alb-api"
      domain_name = aws_lb.api.dns_name
      custom_origin_config = {
        origin_protocol_policy = "https-only"
        origin_ssl_protocols   = ["TLSv1.2"]
      }
    },
  ]

  default_cache_behavior = {
    target_origin_id  = "s3-static"
    cache_policy_name = "Managed-CachingOptimized"
  }

  ordered_cache_behaviors = [
    {
      path_pattern               = "/api/*"
      target_origin_id           = "alb-api"
      cache_policy_name          = "Managed-CachingDisabled"
      origin_request_policy_name = "Managed-AllViewerExceptHostHeader"
    },
  ]

  # SPA: serve index.html (200) for client-side routes instead of S3's 403/404.
  custom_error_responses = [
    { error_code = 403, response_code = 200, response_page_path = "/index.html" },
    { error_code = 404, response_code = 200, response_page_path = "/index.html" },
  ]

  acm_certificate_arn = aws_acm_certificate.cdn.arn # validated, in us-east-1

  logging_config = {
    bucket = "${aws_s3_bucket.cdn_logs.bucket_domain_name}"
    prefix = "web-prod/"
  }

  tags = {
    Environment = "prod"
    Team        = "web-platform"
  }
}

# Downstream: point the custom domain at the distribution via a Route 53 alias.
resource "aws_route53_record" "cdn" {
  zone_id = var.public_zone_id
  name    = "cdn.example.com"
  type    = "A"

  alias {
    name                   = module.cloudfront.domain_name
    zone_id                = module.cloudfront.hosted_zone_id
    evaluate_target_health = false
  }
}

# Downstream: bucket policy that lets ONLY this distribution read the S3 origin.
data "aws_iam_policy_document" "s3_oac" {
  statement {
    actions   = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.site.arn}/*"]

    principals {
      type        = "Service"
      identifiers = ["cloudfront.amazonaws.com"]
    }

    condition {
      test     = "StringEquals"
      variable = "AWS:SourceArn"
      values   = [module.cloudfront.arn]
    }
  }
}

resource "aws_s3_bucket_policy" "site" {
  bucket = aws_s3_bucket.site.id
  policy = data.aws_iam_policy_document.s3_oac.json
}

With Terragrunt

Terragrunt keeps this module DRY across environments — define the backend and provider once in a root config, then a thin terragrunt.hcl per environment supplies only the inputs that differ.

1. Root configlive/terragrunt.hcl (inherited by every module):

remote_state {
  backend = "s3"
  generate = { path = "backend.tf", if_exists = "overwrite" }
  config = {
    # ...s3 state bucket/container + key per path...
  }
}

2. Module configlive/prod/cloudfront/terragrunt.hcl:

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-cloudfront?ref=v1.0.0"
}

inputs = {
  name = "..."
  origins = ["...", "..."]
  default_cache_behavior = {}
}

3. Deploy one environment, or roll out all modules together:

cd live/prod/cloudfront && terragrunt apply        # this module
terragrunt run-all apply                      # every module under live/prod

Why Terragrunt here: the backend and provider live in one place instead of being copy-pasted into every module; inputs is overridden per environment (dev / stage / prod) without forking the module; and run-all orchestrates dependencies across modules. Reach for it once you have more than one environment or more than a handful of modules — for a single stack, the plain Quickstart above is enough.

Inputs

Name Type Default Required Description
name string Yes Logical name; used in comment, OAC name, and Name tag.
enabled bool true No Whether the distribution accepts viewer requests.
comment string "Managed by Terraform" No Console comment (≤128 chars).
aliases list(string) [] No Custom CNAMEs; each must be covered by the ACM cert.
default_root_object string null No Root object (e.g. index.html).
is_ipv6_enabled bool true No Serve over IPv6 as well as IPv4.
http_version string "http2and3" No Max HTTP version: http1.1/http2/http2and3/http3.
price_class string "PriceClass_100" No Edge footprint: _100/_200/_All.
web_acl_id string null No WAFv2 web ACL ARN (CLOUDFRONT scope, us-east-1).
origins list(object) Yes Origins to fetch from; use_oac for private S3, custom_origin_config for HTTP.
default_cache_behavior object Yes Catch-all behavior; cache_policy_name is an AWS-managed policy.
ordered_cache_behaviors list(object) [] No Path-pattern behaviors evaluated in order.
custom_error_responses list(object) [] No Error mappings (e.g. 403/404 → /index.html for SPAs).
acm_certificate_arn string null No ACM cert ARN in us-east-1; null = default cert.
minimum_protocol_version string "TLSv1.2_2021" No Minimum viewer TLS (custom cert only).
geo_restriction object {} No none/whitelist/blacklist + ISO-3166 alpha-2 codes.
logging_config object null No S3 standard access logging; null disables.
retain_on_delete bool false No Disable instead of delete on destroy.
wait_for_deployment bool true No Block apply until edges finish deploying.
tags map(string) {} No Tags applied to the distribution.

Outputs

Name Description
id Distribution ID; used for invalidations and WAF associations.
arn Distribution ARN; reference in S3 bucket policies for OAC scoping.
domain_name CloudFront domain (d…​.cloudfront.net); Route 53 alias target.
hosted_zone_id CloudFront’s fixed hosted zone ID (Z2FDTNDATAQYW2) for alias records.
status Current status (Deployed or InProgress).
etag Current configuration version identifier.
origin_access_control_id Created OAC ID, or null when no origin uses OAC.

Enterprise scenario

A retail company serves its React storefront and checkout API to customers across India, the EU, and the Middle East. They instantiate this module once per environment: the static SPA lives in a private S3 bucket reachable only through OAC (the bucket has zero public access), /api/* is routed to a regional ALB with Managed-CachingDisabled, and a CLOUDFRONT-scope WAF web ACL with rate limiting and managed rule groups is attached via web_acl_id. They pick PriceClass_200 so Mumbai and Dubai edges are in play without paying for South America, enforce TLSv1.2_2021, ship access logs to a central S3 logging bucket, and front it all with cdn.shop.example.com via a Route 53 alias — giving security a private origin and an edge WAF, and finance a single tagged, right-sized distribution per environment.

Best practices

TerraformAWSCloudFrontModuleIaC
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading