diff --git a/README.md b/README.md index b644ea3..56c173f 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,128 @@ module "cdn" { } ``` +### CloudFront distribution with CloudFront Functions + +```hcl +module "cdn" { + source = "terraform-aws-modules/cloudfront/aws" + + aliases = ["cdn.example.com"] + + comment = "CloudFront with Functions" + enabled = true + is_ipv6_enabled = true + price_class = "PriceClass_All" + retain_on_delete = false + wait_for_deployment = false + + # Enable CloudFront Functions + create_cloudfront_function = true + + cloudfront_functions = { + viewer-request-function = { + runtime = "cloudfront-js-2.0" + comment = "Function to add security headers and modify requests" + code = file("${path.module}/functions/viewer-request.js") + publish = true + } + + viewer-response-function = { + runtime = "cloudfront-js-2.0" + comment = "Function to add security response headers" + code = file("${path.module}/functions/viewer-response.js") + publish = true + # Optional: Associate with CloudFront KeyValueStore + key_value_store_associations = ["arn:aws:cloudfront::123456789012:key-value-store/example-store"] + } + } + + origin = { + s3_bucket = { + domain_name = "my-bucket.s3.amazonaws.com" + s3_origin_config = { + origin_access_identity = "s3_bucket" + } + } + } + + default_cache_behavior = { + target_origin_id = "s3_bucket" + viewer_protocol_policy = "redirect-to-https" + + allowed_methods = ["GET", "HEAD", "OPTIONS"] + cached_methods = ["GET", "HEAD"] + compress = true + query_string = true + + # Associate CloudFront Functions with cache behavior + # Option 1: Direct ARN reference (recommended for external functions) + # function_association = { + # viewer-request = { + # function_arn = aws_cloudfront_function.external.arn + # } + # } + + # Option 2: Dynamic reference to module-managed functions by name + function_association = { + viewer-request = { + function_name = "viewer-request-function" + } + viewer-response = { + function_name = "viewer-response-function" + } + } + } + + viewer_certificate = { + acm_certificate_arn = "arn:aws:acm:us-east-1:135367859851:certificate/1032b155-22da-4ae0-9f69-e206f825458b" + ssl_support_method = "sni-only" + } +} +``` + +**CloudFront Functions Features:** + +- **Lightweight JavaScript execution** at CloudFront edge locations +- **Sub-millisecond execution** for viewer request/response modifications +- **Runtime options**: `cloudfront-js-1.0` (10KB limit) or `cloudfront-js-2.0` (30KB limit, default) +- **Event types**: viewer-request, viewer-response (not origin-request/response) +- **Key-Value Store integration**: Associate functions with CloudFront KeyValueStore (max 1 per function) +- **Cost-effective**: Lower cost than Lambda@Edge for simple transformations + +**Common use cases:** + +- URL redirects and rewrites +- Request/response header manipulation +- Access control and authentication +- A/B testing and feature flags +- Cache key normalization + +**Usage Pattern Note:** + +The module supports two flexible patterns for associating CloudFront Functions with cache behaviors: + +1. **Direct ARN Reference** (`function_arn`): Pass the ARN directly from external `aws_cloudfront_function` resources + + ```hcl + function_association = { + viewer-request = { + function_arn = aws_cloudfront_function.external.arn + } + } + ``` + +2. **Dynamic Name Reference** (`function_name`): Reference module-managed functions by their map key + ```hcl + function_association = { + viewer-request = { + function_name = "viewer-request-function" # Key from cloudfront_functions map + } + } + ``` + +The module automatically resolves function ARNs using Terraform's `try()` function, checking for `function_arn` first, then falling back to `function_name` lookup in module-created functions. This eliminates circular dependency issues while maintaining flexibility. + ## Examples - [Complete](https://github.com/terraform-aws-modules/terraform-aws-cloudfront/tree/master/examples/complete) - Complete example which creates AWS CloudFront distribution and integrates it with other [terraform-aws-modules](https://github.com/terraform-aws-modules) to create additional resources: S3 buckets, Lambda Functions, CloudFront Functions, VPC Origins, ACM Certificate, Route53 Records. @@ -124,9 +246,11 @@ No modules. | Name | Type | |------|------| | [aws_cloudfront_distribution.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_distribution) | resource | +| [aws_cloudfront_function.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_function) | resource | | [aws_cloudfront_monitoring_subscription.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_monitoring_subscription) | resource | | [aws_cloudfront_origin_access_control.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_origin_access_control) | resource | | [aws_cloudfront_origin_access_identity.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_origin_access_identity) | resource | +| [aws_cloudfront_response_headers_policy.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_response_headers_policy) | resource | | [aws_cloudfront_vpc_origin.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_vpc_origin) | resource | | [aws_cloudfront_cache_policy.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/cloudfront_cache_policy) | data source | | [aws_cloudfront_origin_request_policy.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/cloudfront_origin_request_policy) | data source | @@ -137,12 +261,15 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [aliases](#input\_aliases) | Extra CNAMEs (alternate domain names), if any, for this distribution. | `list(string)` | `null` | no | +| [cloudfront\_functions](#input\_cloudfront\_functions) | Map of CloudFront Function configurations. Key is used as default function name if 'name' not specified. |
map(object({
name = optional(string)
runtime = optional(string, "cloudfront-js-2.0")
comment = optional(string)
publish = optional(bool, true)
code_path = string
key_value_store_associations = optional(list(string), null)
})) | `{}` | no |
| [comment](#input\_comment) | Any comments you want to include about the distribution. | `string` | `null` | no |
| [continuous\_deployment\_policy\_id](#input\_continuous\_deployment\_policy\_id) | Identifier of a continuous deployment policy. This argument should only be set on a production distribution. | `string` | `null` | no |
+| [create\_cloudfront\_function](#input\_create\_cloudfront\_function) | Controls if CloudFront Functions should be created | `bool` | `false` | no |
| [create\_distribution](#input\_create\_distribution) | Controls if CloudFront distribution should be created | `bool` | `true` | no |
| [create\_monitoring\_subscription](#input\_create\_monitoring\_subscription) | If enabled, the resource for monitoring subscription will created. | `bool` | `false` | no |
| [create\_origin\_access\_control](#input\_create\_origin\_access\_control) | Controls if CloudFront origin access control should be created | `bool` | `false` | no |
| [create\_origin\_access\_identity](#input\_create\_origin\_access\_identity) | Controls if CloudFront origin access identity should be created | `bool` | `false` | no |
+| [create\_response\_headers\_policy](#input\_create\_response\_headers\_policy) | Controls if CloudFront response headers policies should be created | `bool` | `false` | no |
| [create\_vpc\_origin](#input\_create\_vpc\_origin) | If enabled, the resource for VPC origin will be created. | `bool` | `false` | no |
| [custom\_error\_response](#input\_custom\_error\_response) | One or more custom error response elements | `any` | `{}` | no |
| [default\_cache\_behavior](#input\_default\_cache\_behavior) | The default cache behavior for this distribution | `any` | `null` | no |
@@ -159,6 +286,7 @@ No modules.
| [origin\_group](#input\_origin\_group) | One or more origin\_group for this distribution (multiples allowed). | `any` | `{}` | no |
| [price\_class](#input\_price\_class) | The price class for this distribution. One of PriceClass\_All, PriceClass\_200, PriceClass\_100 | `string` | `null` | no |
| [realtime\_metrics\_subscription\_status](#input\_realtime\_metrics\_subscription\_status) | A flag that indicates whether additional CloudWatch metrics are enabled for a given CloudFront distribution. Valid values are `Enabled` and `Disabled`. | `string` | `"Enabled"` | no |
+| [response\_headers\_policy](#input\_response\_headers\_policy) | Map of CloudFront response headers policies with their configurations | map(object({
name = optional(string)
comment = optional(string)
cors_config = optional(object({
access_control_allow_credentials = bool
origin_override = bool
access_control_allow_headers = object({
items = list(string)
})
access_control_allow_methods = object({
items = list(string)
})
access_control_allow_origins = object({
items = list(string)
})
access_control_expose_headers = optional(object({
items = list(string)
}))
access_control_max_age_sec = optional(number)
}))
custom_headers_config = optional(object({
items = list(object({
header = string
override = bool
value = string
}))
}))
remove_headers_config = optional(object({
items = list(object({
header = string
}))
}))
security_headers_config = optional(object({
content_security_policy = optional(object({
content_security_policy = string
override = bool
}))
content_type_options = optional(object({
override = bool
}))
frame_options = optional(object({
frame_option = string
override = bool
}))
referrer_policy = optional(object({
referrer_policy = string
override = bool
}))
strict_transport_security = optional(object({
access_control_max_age_sec = number
override = bool
include_subdomains = optional(bool)
preload = optional(bool)
}))
xss_protection = optional(object({
mode_block = bool
override = bool
protection = bool
report_uri = optional(string)
}))
}))
server_timing_headers_config = optional(object({
enabled = bool
sampling_rate = number
}))
})) | `{}` | no |
| [retain\_on\_delete](#input\_retain\_on\_delete) | Disables the distribution instead of deleting it when destroying the resource through Terraform. If this is set, the distribution needs to be deleted manually afterwards. | `bool` | `false` | no |
| [staging](#input\_staging) | Whether the distribution is a staging distribution. | `bool` | `false` | no |
| [tags](#input\_tags) | A map of tags to assign to the resource. | `map(string)` | `null` | no |
@@ -183,12 +311,19 @@ No modules.
| [cloudfront\_distribution\_status](#output\_cloudfront\_distribution\_status) | The current status of the distribution. Deployed if the distribution's information is fully propagated throughout the Amazon CloudFront system. |
| [cloudfront\_distribution\_tags](#output\_cloudfront\_distribution\_tags) | Tags of the distribution's |
| [cloudfront\_distribution\_trusted\_signers](#output\_cloudfront\_distribution\_trusted\_signers) | List of nested attributes for active trusted signers, if the distribution is set up to serve private content with signed URLs |
+| [cloudfront\_function\_arns](#output\_cloudfront\_function\_arns) | The ARNs of the CloudFront Functions created |
+| [cloudfront\_function\_etags](#output\_cloudfront\_function\_etags) | The ETags of the CloudFront Functions (DEVELOPMENT stage) |
+| [cloudfront\_function\_live\_stage\_etags](#output\_cloudfront\_function\_live\_stage\_etags) | The ETags of the CloudFront Functions (LIVE stage) |
+| [cloudfront\_function\_status](#output\_cloudfront\_function\_status) | The deployment status of the CloudFront Functions |
| [cloudfront\_monitoring\_subscription\_id](#output\_cloudfront\_monitoring\_subscription\_id) | The ID of the CloudFront monitoring subscription, which corresponds to the `distribution_id`. |
| [cloudfront\_origin\_access\_controls](#output\_cloudfront\_origin\_access\_controls) | The origin access controls created |
| [cloudfront\_origin\_access\_controls\_ids](#output\_cloudfront\_origin\_access\_controls\_ids) | The IDS of the origin access identities created |
| [cloudfront\_origin\_access\_identities](#output\_cloudfront\_origin\_access\_identities) | The origin access identities created |
| [cloudfront\_origin\_access\_identity\_iam\_arns](#output\_cloudfront\_origin\_access\_identity\_iam\_arns) | The IAM arns of the origin access identities created |
| [cloudfront\_origin\_access\_identity\_ids](#output\_cloudfront\_origin\_access\_identity\_ids) | The IDS of the origin access identities created |
+| [cloudfront\_response\_headers\_policies](#output\_cloudfront\_response\_headers\_policies) | The response headers policies created |
+| [cloudfront\_response\_headers\_policy\_etags](#output\_cloudfront\_response\_headers\_policy\_etags) | The ETags of the response headers policies created |
+| [cloudfront\_response\_headers\_policy\_ids](#output\_cloudfront\_response\_headers\_policy\_ids) | The IDs of the response headers policies created |
| [cloudfront\_vpc\_origin\_ids](#output\_cloudfront\_vpc\_origin\_ids) | The IDS of the VPC origin created |
diff --git a/examples/complete/README.md b/examples/complete/README.md
index d2b2039..4530b97 100644
--- a/examples/complete/README.md
+++ b/examples/complete/README.md
@@ -1,15 +1,37 @@
# Complete CloudFront distribution with most of supported features enabled
Configuration in this directory creates CloudFront distribution which demos such capabilities:
+
- access logging
- origins and origin groups
- caching behaviours
- Origin Access Identities (with S3 bucket policy)
+- Origin Access Control (recommended over OAI)
- Lambda@Edge
+- **CloudFront Functions** (lightweight JavaScript execution at edge locations)
+- Response Headers Policies
- ACM certificate
- Route53 record
- VPC Origins
+## CloudFront Functions
+
+This example demonstrates CloudFront Functions integration with the module:
+
+**Functions included:**
+
+- `viewer-request-security.js` - Security headers and cache key normalization
+- `viewer-response-headers.js` - Add security response headers
+- `ab-testing.js` - A/B testing with path rewriting
+- `kvstore-redirect.js` - Example with CloudFront KeyValueStore integration (commented)
+
+**Features demonstrated:**
+
+- Module-managed CloudFront Functions creation
+- Function association with cache behaviors
+- Runtime selection (cloudfront-js-2.0)
+- KeyValueStore association pattern
+
## Usage
To run this example you need to execute:
diff --git a/examples/complete/ab-testing.js b/examples/complete/ab-testing.js
new file mode 100644
index 0000000..7d4f54d
--- /dev/null
+++ b/examples/complete/ab-testing.js
@@ -0,0 +1,46 @@
+function handler(event) {
+ // A/B testing function using CloudFront Functions
+ // Assigns users to test groups and routes to different origin paths
+
+ var request = event.request;
+ var headers = request.headers;
+ var cookies = request.cookies;
+
+ // Check if user already has an A/B test cookie
+ var abTestGroup = null;
+
+ if (cookies['ab-test-group']) {
+ abTestGroup = cookies['ab-test-group'].value;
+ } else {
+ // Assign new users to a test group (50/50 split)
+ // Use CloudFront viewer ID for consistent assignment
+ var viewerId = event.viewer.address;
+ var hash = 0;
+
+ for (var i = 0; i < viewerId.length; i++) {
+ hash = ((hash << 5) - hash) + viewerId.charCodeAt(i);
+ hash = hash & hash; // Convert to 32bit integer
+ }
+
+ abTestGroup = (Math.abs(hash) % 2 === 0) ? 'A' : 'B';
+
+ // Note: CloudFront Functions cannot set cookies
+ // You would set this cookie in the response (using viewer-response function)
+ // or via JavaScript on the client side
+ }
+
+ // Route to different paths based on test group
+ var uri = request.uri;
+
+ if (abTestGroup === 'B' && !uri.startsWith('/variant-b/')) {
+ // Rewrite path for variant B users
+ request.uri = '/variant-b' + uri;
+ }
+
+ // Add header for origin to know which variant was served
+ headers['x-ab-test-group'] = {
+ value: abTestGroup
+ };
+
+ return request;
+}
diff --git a/examples/complete/kvstore-redirect.js b/examples/complete/kvstore-redirect.js
new file mode 100644
index 0000000..f7b2437
--- /dev/null
+++ b/examples/complete/kvstore-redirect.js
@@ -0,0 +1,46 @@
+function handler(event) {
+ // CloudFront Function with KeyValueStore integration
+ // Uses KV store for dynamic URL redirects and feature flags
+
+ var request = event.request;
+ var uri = request.uri;
+
+ // Note: To use KeyValueStore, associate the KV store ARN with this function
+ // Example: key_value_store_associations = ["arn:aws:cloudfront::123456789012:key-value-store/redirects"]
+
+ // Uncomment when KV store is associated:
+ /*
+ var kvsHandle = event.context.kvs;
+
+ // Look up redirect mapping in KeyValueStore
+ var redirectTarget = kvsHandle.get(uri);
+
+ if (redirectTarget) {
+ // Redirect to target URL from KV store
+ var response = {
+ statusCode: 301,
+ statusDescription: 'Moved Permanently',
+ headers: {
+ 'location': { value: redirectTarget },
+ 'cache-control': { value: 'max-age=3600' }
+ }
+ };
+ return response;
+ }
+
+ // Check feature flags in KV store
+ var featureFlags = kvsHandle.get('feature-flags');
+ if (featureFlags) {
+ var flags = JSON.parse(featureFlags);
+
+ // Add feature flag headers for origin
+ if (flags.enableNewUI) {
+ request.headers['x-feature-new-ui'] = { value: 'true' };
+ }
+ }
+ */
+
+ // For now, return request as-is
+ // When KV store is configured, uncomment the code above
+ return request;
+}
diff --git a/examples/complete/main.tf b/examples/complete/main.tf
index 76bc588..74429ba 100644
--- a/examples/complete/main.tf
+++ b/examples/complete/main.tf
@@ -176,13 +176,28 @@ module "cloudfront" {
cache_policy_name = "Managed-CachingOptimized"
origin_request_policy_name = "Managed-UserAgentRefererHeaders"
response_headers_policy_name = "Managed-SimpleCORS"
+ # using a response header policy you're dynamically creating below
+ # response_header_policy: "cors_policy"
function_association = {
# Valid keys: viewer-request, viewer-response
+
+ # Option 1: Direct ARN reference to standalone resource
viewer-request = {
function_arn = aws_cloudfront_function.example.arn
}
+ # Option 2: Dynamic reference to module-managed function by name
+ # Uncomment to use module-managed functions instead:
+ # viewer-request = {
+ # function_name = "viewer-request-security"
+ # }
+
+ # viewer-response = {
+ # function_name = "viewer-response-headers"
+ # }
+
+ # For this example, using standalone function for both
viewer-response = {
function_arn = aws_cloudfront_function.example.arn
}
@@ -231,6 +246,112 @@ module "cloudfront" {
locations = ["NO", "UA", "US", "GB"]
}
+ # CloudFront Functions - module managed
+ create_cloudfront_function = true
+ cloudfront_functions = {
+ viewer-request-security = {
+ runtime = "cloudfront-js-2.0"
+ comment = "Security headers and cache key normalization"
+ code_path = "functions/viewer-request-security.js"
+ publish = true
+ }
+ viewer-response-headers = {
+ runtime = "cloudfront-js-2.0"
+ comment = "Add security response headers"
+ code_path = "functions/viewer-response-headers.js"
+ publish = true
+ }
+ ab-testing = {
+ runtime = "cloudfront-js-2.0"
+ comment = "A/B testing function"
+ code_path = "functions//ab-testing.js"
+ publish = true
+ }
+ # Example with KeyValueStore association (uncomment and provide actual KV store ARN)
+ # kvstore-redirect = {
+ # runtime = "cloudfront-js-2.0"
+ # comment = "Function using CloudFront KeyValueStore for dynamic redirects"
+ # code = file("${path.module}/kvstore-redirect.js")
+ # publish = true
+ # key_value_store_associations = [
+ # "arn:aws:cloudfront::123456789012:key-value-store/example-redirects"
+ # ]
+ # }
+ }
+
+ create_response_headers_policy = true
+ response_headers_policy = {
+ cors_policy = {
+ name = "CORSPolicy"
+ comment = "CORS configuration for API"
+
+ cors_config = {
+ access_control_allow_credentials = true
+ origin_override = true
+
+ access_control_allow_headers = {
+ items = ["*"]
+ }
+
+ access_control_allow_methods = {
+ items = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
+ }
+
+ access_control_allow_origins = {
+ items = ["https://example.com", "https://app.example.com"]
+ }
+
+ access_control_expose_headers = {
+ items = ["X-Custom-Header", "X-Request-Id"]
+ }
+
+ access_control_max_age_sec = 3600
+ }
+ }
+ custom_headers = {
+ name = "CustomHeadersPolicy"
+ comment = "Add custom response headers"
+
+ custom_headers_config = {
+ items = [
+ {
+ header = "X-Powered-By"
+ override = true
+ value = "MyApp/1.0"
+ },
+ {
+ header = "X-API-Version"
+ override = false
+ value = "v2"
+ },
+ {
+ header = "Cache-Control"
+ override = true
+ value = "public, max-age=3600"
+ }
+ ]
+ }
+ }
+ remove_headers = {
+ name = "RemoveHeadersPolicy"
+ comment = "Remove unwanted headers from origin"
+
+ remove_headers_config = {
+ items = [
+ {
+ header = "x-robots-tag"
+ },
+ {
+ header = "server"
+ },
+ {
+ header = "x-powered-by"
+ }
+ ]
+ }
+ }
+ }
+
}
######
diff --git a/examples/complete/viewer-request-security.js b/examples/complete/viewer-request-security.js
new file mode 100644
index 0000000..6fa5a53
--- /dev/null
+++ b/examples/complete/viewer-request-security.js
@@ -0,0 +1,35 @@
+function handler(event) {
+ // Viewer request function to add security headers and normalize cache keys
+ // This function runs before CloudFront checks the cache
+
+ var request = event.request;
+ var headers = request.headers;
+
+ // Normalize Host header for consistent caching
+ if (headers.host) {
+ headers.host.value = headers.host.value.toLowerCase();
+ }
+
+ // Remove query parameters that don't affect content (for better cache hit ratio)
+ var uri = request.uri;
+ var querystring = request.querystring;
+
+ // Example: Remove tracking parameters but keep content-affecting ones
+ var allowedParams = ['id', 'page', 'category'];
+ var newQuerystring = {};
+
+ for (var param in querystring) {
+ if (allowedParams.includes(param)) {
+ newQuerystring[param] = querystring[param];
+ }
+ }
+
+ request.querystring = newQuerystring;
+
+ // Add custom header for logging/debugging
+ headers['x-viewer-country'] = {
+ value: event.viewer.country || 'unknown'
+ };
+
+ return request;
+}
diff --git a/examples/complete/viewer-response-headers.js b/examples/complete/viewer-response-headers.js
new file mode 100644
index 0000000..69ccebc
--- /dev/null
+++ b/examples/complete/viewer-response-headers.js
@@ -0,0 +1,42 @@
+function handler(event) {
+ // Viewer response function to add security and performance headers
+ // This function runs after CloudFront receives the response from origin
+
+ var response = event.response;
+ var headers = response.headers;
+
+ // Add security headers
+ headers['strict-transport-security'] = {
+ value: 'max-age=63072000; includeSubdomains; preload'
+ };
+
+ headers['x-content-type-options'] = {
+ value: 'nosniff'
+ };
+
+ headers['x-frame-options'] = {
+ value: 'DENY'
+ };
+
+ headers['x-xss-protection'] = {
+ value: '1; mode=block'
+ };
+
+ headers['referrer-policy'] = {
+ value: 'strict-origin-when-cross-origin'
+ };
+
+ // Add cache control for static assets
+ if (event.request.uri.match(/\.(jpg|jpeg|png|gif|ico|css|js|woff|woff2)$/)) {
+ headers['cache-control'] = {
+ value: 'public, max-age=31536000, immutable'
+ };
+ }
+
+ // Add custom header to identify CloudFront Functions processing
+ headers['x-cloudfront-function'] = {
+ value: 'viewer-response-headers'
+ };
+
+ return response;
+}
diff --git a/main.tf b/main.tf
index f773d7e..e5cde4a 100644
--- a/main.tf
+++ b/main.tf
@@ -1,7 +1,160 @@
locals {
- create_origin_access_identity = var.create_origin_access_identity && length(keys(var.origin_access_identities)) > 0
- create_origin_access_control = var.create_origin_access_control && length(keys(var.origin_access_control)) > 0
- create_vpc_origin = var.create_vpc_origin && length(keys(var.vpc_origin)) > 0
+ create_origin_access_identity = var.create_origin_access_identity && length(keys(var.origin_access_identities)) > 0
+ create_origin_access_control = var.create_origin_access_control && length(keys(var.origin_access_control)) > 0
+ create_vpc_origin = var.create_vpc_origin && length(keys(var.vpc_origin)) > 0
+ create_response_headers_policy = var.create_response_headers_policy && length(keys(var.response_headers_policy)) > 0
+ create_cloudfront_function = var.create_cloudfront_function && length(keys(var.cloudfront_functions)) > 0
+}
+
+resource "aws_cloudfront_response_headers_policy" "this" {
+ for_each = local.create_response_headers_policy ? var.response_headers_policy : {}
+
+ name = each.value.name != null ? each.value.name : each.key
+ comment = each.value.comment
+
+ dynamic "cors_config" {
+ for_each = each.value.cors_config != null ? [each.value.cors_config] : []
+
+ content {
+ access_control_allow_credentials = cors_config.value.access_control_allow_credentials
+ origin_override = cors_config.value.origin_override
+ access_control_max_age_sec = cors_config.value.access_control_max_age_sec != null ? cors_config.value.access_control_max_age_sec : null
+
+ access_control_allow_headers {
+ items = cors_config.value.access_control_allow_headers.items
+ }
+
+ access_control_allow_methods {
+ items = cors_config.value.access_control_allow_methods.items
+ }
+
+ access_control_allow_origins {
+ items = cors_config.value.access_control_allow_origins.items
+ }
+
+ dynamic "access_control_expose_headers" {
+ for_each = cors_config.value.access_control_expose_headers != null ? [cors_config.value.access_control_expose_headers] : []
+
+ content {
+ items = access_control_expose_headers.value.items
+ }
+ }
+ }
+ }
+
+ dynamic "custom_headers_config" {
+ for_each = each.value.custom_headers_config != null ? [each.value.custom_headers_config] : []
+
+ content {
+ dynamic "items" {
+ for_each = custom_headers_config.value.items
+
+ content {
+ header = items.value.header
+ override = items.value.override
+ value = items.value.value
+ }
+ }
+ }
+ }
+
+ dynamic "remove_headers_config" {
+ for_each = each.value.remove_headers_config != null ? [each.value.remove_headers_config] : []
+
+ content {
+ dynamic "items" {
+ for_each = remove_headers_config.value.items
+
+ content {
+ header = items.value.header
+ }
+ }
+ }
+ }
+
+ dynamic "security_headers_config" {
+ for_each = each.value.security_headers_config != null ? [each.value.security_headers_config] : []
+
+ content {
+ dynamic "content_security_policy" {
+ for_each = security_headers_config.value.content_security_policy != null ? [security_headers_config.value.content_security_policy] : []
+
+ content {
+ content_security_policy = content_security_policy.value.content_security_policy
+ override = content_security_policy.value.override
+ }
+ }
+
+ dynamic "content_type_options" {
+ for_each = security_headers_config.value.content_type_options != null ? [security_headers_config.value.content_type_options] : []
+
+ content {
+ override = content_type_options.value.override
+ }
+ }
+
+ dynamic "frame_options" {
+ for_each = security_headers_config.value.frame_options != null ? [security_headers_config.value.frame_options] : []
+
+ content {
+ frame_option = frame_options.value.frame_option
+ override = frame_options.value.override
+ }
+ }
+
+ dynamic "referrer_policy" {
+ for_each = security_headers_config.value.referrer_policy != null ? [security_headers_config.value.referrer_policy] : []
+
+ content {
+ referrer_policy = referrer_policy.value.referrer_policy
+ override = referrer_policy.value.override
+ }
+ }
+
+ dynamic "strict_transport_security" {
+ for_each = security_headers_config.value.strict_transport_security != null ? [security_headers_config.value.strict_transport_security] : []
+
+ content {
+ access_control_max_age_sec = strict_transport_security.value.access_control_max_age_sec
+ override = strict_transport_security.value.override
+ include_subdomains = strict_transport_security.value.include_subdomains
+ preload = strict_transport_security.value.preload
+ }
+ }
+
+ dynamic "xss_protection" {
+ for_each = security_headers_config.value.xss_protection != null ? [security_headers_config.value.xss_protection] : []
+
+ content {
+ mode_block = xss_protection.value.mode_block
+ override = xss_protection.value.override
+ protection = xss_protection.value.protection
+ report_uri = xss_protection.value.report_uri
+ }
+ }
+ }
+ }
+
+ dynamic "server_timing_headers_config" {
+ for_each = each.value.server_timing_headers_config != null ? [each.value.server_timing_headers_config] : []
+
+ content {
+ enabled = server_timing_headers_config.value.enabled
+ sampling_rate = server_timing_headers_config.value.sampling_rate
+ }
+ }
+}
+
+resource "aws_cloudfront_function" "this" {
+ for_each = local.create_cloudfront_function ? var.cloudfront_functions : {}
+
+ name = each.value.name != null ? each.value.name : each.key
+ runtime = each.value.runtime
+ comment = each.value.comment
+ publish = each.value.publish
+ code = file("${path.module}/${each.value.code_path}")
+
+ key_value_store_associations = each.value.key_value_store_associations
}
resource "aws_cloudfront_origin_access_identity" "this" {
@@ -67,6 +220,11 @@ resource "aws_cloudfront_distribution" "this" {
web_acl_id = var.web_acl_id
tags = var.tags
+ # Ensure CloudFront Functions are created before the distribution
+ depends_on = [
+ aws_cloudfront_function.this
+ ]
+
dynamic "logging_config" {
for_each = length(keys(var.logging_config)) == 0 ? [] : [var.logging_config]
@@ -217,7 +375,7 @@ resource "aws_cloudfront_distribution" "this" {
content {
event_type = f.key
- function_arn = f.value.function_arn
+ function_arn = lookup(f.value, "function_arn", try(aws_cloudfront_function.this[lookup(f.value, "function_name", "")].arn, null))
}
}
@@ -289,7 +447,7 @@ resource "aws_cloudfront_distribution" "this" {
content {
event_type = f.key
- function_arn = f.value.function_arn
+ function_arn = lookup(f.value, "function_arn", try(aws_cloudfront_function.this[lookup(f.value, "function_name", "")].arn, null))
}
}
diff --git a/outputs.tf b/outputs.tf
index 29e7642..36d836d 100644
--- a/outputs.tf
+++ b/outputs.tf
@@ -87,3 +87,38 @@ output "cloudfront_vpc_origin_ids" {
description = "The IDS of the VPC origin created"
value = local.create_vpc_origin ? [for v in aws_cloudfront_vpc_origin.this : v.id] : []
}
+
+output "cloudfront_response_headers_policies" {
+ description = "The response headers policies created"
+ value = local.create_response_headers_policy ? { for k, v in aws_cloudfront_response_headers_policy.this : k => v } : {}
+}
+
+output "cloudfront_response_headers_policy_ids" {
+ description = "The IDs of the response headers policies created"
+ value = local.create_response_headers_policy ? { for k, v in aws_cloudfront_response_headers_policy.this : k => v.id } : {}
+}
+
+output "cloudfront_response_headers_policy_etags" {
+ description = "The ETags of the response headers policies created"
+ value = local.create_response_headers_policy ? { for k, v in aws_cloudfront_response_headers_policy.this : k => v.etag } : {}
+}
+
+output "cloudfront_function_arns" {
+ description = "The ARNs of the CloudFront Functions created"
+ value = local.create_cloudfront_function ? { for k, v in aws_cloudfront_function.this : k => v.arn } : {}
+}
+
+output "cloudfront_function_status" {
+ description = "The deployment status of the CloudFront Functions"
+ value = local.create_cloudfront_function ? { for k, v in aws_cloudfront_function.this : k => v.status } : {}
+}
+
+output "cloudfront_function_etags" {
+ description = "The ETags of the CloudFront Functions (DEVELOPMENT stage)"
+ value = local.create_cloudfront_function ? { for k, v in aws_cloudfront_function.this : k => v.etag } : {}
+}
+
+output "cloudfront_function_live_stage_etags" {
+ description = "The ETags of the CloudFront Functions (LIVE stage)"
+ value = local.create_cloudfront_function ? { for k, v in aws_cloudfront_function.this : k => v.live_stage_etag } : {}
+}
diff --git a/variables.tf b/variables.tf
index afeec33..da8e739 100644
--- a/variables.tf
+++ b/variables.tf
@@ -210,3 +210,112 @@ variable "vpc_origin_timeouts" {
type = map(string)
default = {}
}
+
+variable "create_response_headers_policy" {
+ description = "Controls if CloudFront response headers policies should be created"
+ type = bool
+ default = false
+}
+
+variable "response_headers_policy" {
+ description = "Map of CloudFront response headers policies with their configurations"
+ type = map(object({
+ name = optional(string)
+ comment = optional(string)
+
+ cors_config = optional(object({
+ access_control_allow_credentials = bool
+ origin_override = bool
+ access_control_allow_headers = object({
+ items = list(string)
+ })
+ access_control_allow_methods = object({
+ items = list(string)
+ })
+ access_control_allow_origins = object({
+ items = list(string)
+ })
+ access_control_expose_headers = optional(object({
+ items = list(string)
+ }))
+ access_control_max_age_sec = optional(number)
+ }))
+
+ custom_headers_config = optional(object({
+ items = list(object({
+ header = string
+ override = bool
+ value = string
+ }))
+ }))
+
+ remove_headers_config = optional(object({
+ items = list(object({
+ header = string
+ }))
+ }))
+
+ security_headers_config = optional(object({
+ content_security_policy = optional(object({
+ content_security_policy = string
+ override = bool
+ }))
+ content_type_options = optional(object({
+ override = bool
+ }))
+ frame_options = optional(object({
+ frame_option = string
+ override = bool
+ }))
+ referrer_policy = optional(object({
+ referrer_policy = string
+ override = bool
+ }))
+ strict_transport_security = optional(object({
+ access_control_max_age_sec = number
+ override = bool
+ include_subdomains = optional(bool)
+ preload = optional(bool)
+ }))
+ xss_protection = optional(object({
+ mode_block = bool
+ override = bool
+ protection = bool
+ report_uri = optional(string)
+ }))
+ }))
+
+ server_timing_headers_config = optional(object({
+ enabled = bool
+ sampling_rate = number
+ }))
+ }))
+ default = {}
+}
+
+variable "create_cloudfront_function" {
+ description = "Controls if CloudFront Functions should be created"
+ type = bool
+ default = false
+}
+
+variable "cloudfront_functions" {
+ description = "Map of CloudFront Function configurations. Key is used as default function name if 'name' not specified."
+ type = map(object({
+ name = optional(string)
+ runtime = optional(string, "cloudfront-js-2.0")
+ comment = optional(string)
+ publish = optional(bool, true)
+ code_path = string
+ key_value_store_associations = optional(list(string), null)
+ }))
+ default = {}
+
+ validation {
+ condition = alltrue([
+ for k, v in var.cloudfront_functions :
+ contains(["cloudfront-js-1.0", "cloudfront-js-2.0"], v.runtime)
+ ])
+ error_message = "Runtime must be 'cloudfront-js-1.0' or 'cloudfront-js-2.0'. Provided runtime is invalid."
+ }
+}
diff --git a/wrappers/main.tf b/wrappers/main.tf
index 750d27e..510cf10 100644
--- a/wrappers/main.tf
+++ b/wrappers/main.tf
@@ -4,12 +4,15 @@ module "wrapper" {
for_each = var.items
aliases = try(each.value.aliases, var.defaults.aliases, null)
+ cloudfront_functions = try(each.value.cloudfront_functions, var.defaults.cloudfront_functions, {})
comment = try(each.value.comment, var.defaults.comment, null)
continuous_deployment_policy_id = try(each.value.continuous_deployment_policy_id, var.defaults.continuous_deployment_policy_id, null)
+ create_cloudfront_function = try(each.value.create_cloudfront_function, var.defaults.create_cloudfront_function, false)
create_distribution = try(each.value.create_distribution, var.defaults.create_distribution, true)
create_monitoring_subscription = try(each.value.create_monitoring_subscription, var.defaults.create_monitoring_subscription, false)
create_origin_access_control = try(each.value.create_origin_access_control, var.defaults.create_origin_access_control, false)
create_origin_access_identity = try(each.value.create_origin_access_identity, var.defaults.create_origin_access_identity, false)
+ create_response_headers_policy = try(each.value.create_response_headers_policy, var.defaults.create_response_headers_policy, false)
create_vpc_origin = try(each.value.create_vpc_origin, var.defaults.create_vpc_origin, false)
custom_error_response = try(each.value.custom_error_response, var.defaults.custom_error_response, {})
default_cache_behavior = try(each.value.default_cache_behavior, var.defaults.default_cache_behavior, null)
@@ -33,6 +36,7 @@ module "wrapper" {
origin_group = try(each.value.origin_group, var.defaults.origin_group, {})
price_class = try(each.value.price_class, var.defaults.price_class, null)
realtime_metrics_subscription_status = try(each.value.realtime_metrics_subscription_status, var.defaults.realtime_metrics_subscription_status, "Enabled")
+ response_headers_policy = try(each.value.response_headers_policy, var.defaults.response_headers_policy, {})
retain_on_delete = try(each.value.retain_on_delete, var.defaults.retain_on_delete, false)
staging = try(each.value.staging, var.defaults.staging, false)
tags = try(each.value.tags, var.defaults.tags, null)