Skip to content

Commit 1a392f4

Browse files
committed
Add Cloudfront Function support
1 parent f06304f commit 1a392f4

File tree

7 files changed

+265
-2
lines changed

7 files changed

+265
-2
lines changed

README.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,128 @@ module "cdn" {
7777
}
7878
```
7979

80+
### CloudFront distribution with CloudFront Functions
81+
82+
```hcl
83+
module "cdn" {
84+
source = "terraform-aws-modules/cloudfront/aws"
85+
86+
aliases = ["cdn.example.com"]
87+
88+
comment = "CloudFront with Functions"
89+
enabled = true
90+
is_ipv6_enabled = true
91+
price_class = "PriceClass_All"
92+
retain_on_delete = false
93+
wait_for_deployment = false
94+
95+
# Enable CloudFront Functions
96+
create_cloudfront_function = true
97+
98+
cloudfront_functions = {
99+
viewer-request-function = {
100+
runtime = "cloudfront-js-2.0"
101+
comment = "Function to add security headers and modify requests"
102+
code = file("${path.module}/functions/viewer-request.js")
103+
publish = true
104+
}
105+
106+
viewer-response-function = {
107+
runtime = "cloudfront-js-2.0"
108+
comment = "Function to add security response headers"
109+
code = file("${path.module}/functions/viewer-response.js")
110+
publish = true
111+
# Optional: Associate with CloudFront KeyValueStore
112+
key_value_store_associations = ["arn:aws:cloudfront::123456789012:key-value-store/example-store"]
113+
}
114+
}
115+
116+
origin = {
117+
s3_bucket = {
118+
domain_name = "my-bucket.s3.amazonaws.com"
119+
s3_origin_config = {
120+
origin_access_identity = "s3_bucket"
121+
}
122+
}
123+
}
124+
125+
default_cache_behavior = {
126+
target_origin_id = "s3_bucket"
127+
viewer_protocol_policy = "redirect-to-https"
128+
129+
allowed_methods = ["GET", "HEAD", "OPTIONS"]
130+
cached_methods = ["GET", "HEAD"]
131+
compress = true
132+
query_string = true
133+
134+
# Associate CloudFront Functions with cache behavior
135+
# Option 1: Direct ARN reference (recommended for external functions)
136+
# function_association = {
137+
# viewer-request = {
138+
# function_arn = aws_cloudfront_function.external.arn
139+
# }
140+
# }
141+
142+
# Option 2: Dynamic reference to module-managed functions by name
143+
function_association = {
144+
viewer-request = {
145+
function_name = "viewer-request-function"
146+
}
147+
viewer-response = {
148+
function_name = "viewer-response-function"
149+
}
150+
}
151+
}
152+
153+
viewer_certificate = {
154+
acm_certificate_arn = "arn:aws:acm:us-east-1:135367859851:certificate/1032b155-22da-4ae0-9f69-e206f825458b"
155+
ssl_support_method = "sni-only"
156+
}
157+
}
158+
```
159+
160+
**CloudFront Functions Features:**
161+
162+
- **Lightweight JavaScript execution** at CloudFront edge locations
163+
- **Sub-millisecond execution** for viewer request/response modifications
164+
- **Runtime options**: `cloudfront-js-1.0` (10KB limit) or `cloudfront-js-2.0` (30KB limit, default)
165+
- **Event types**: viewer-request, viewer-response (not origin-request/response)
166+
- **Key-Value Store integration**: Associate functions with CloudFront KeyValueStore (max 1 per function)
167+
- **Cost-effective**: Lower cost than Lambda@Edge for simple transformations
168+
169+
**Common use cases:**
170+
171+
- URL redirects and rewrites
172+
- Request/response header manipulation
173+
- Access control and authentication
174+
- A/B testing and feature flags
175+
- Cache key normalization
176+
177+
**Usage Pattern Note:**
178+
179+
The module supports two flexible patterns for associating CloudFront Functions with cache behaviors:
180+
181+
1. **Direct ARN Reference** (`function_arn`): Pass the ARN directly from external `aws_cloudfront_function` resources
182+
183+
```hcl
184+
function_association = {
185+
viewer-request = {
186+
function_arn = aws_cloudfront_function.external.arn
187+
}
188+
}
189+
```
190+
191+
2. **Dynamic Name Reference** (`function_name`): Reference module-managed functions by their map key
192+
```hcl
193+
function_association = {
194+
viewer-request = {
195+
function_name = "viewer-request-function" # Key from cloudfront_functions map
196+
}
197+
}
198+
```
199+
200+
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.
201+
80202
## Examples
81203

82204
- [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,6 +246,7 @@ No modules.
124246
| Name | Type |
125247
|------|------|
126248
| [aws_cloudfront_distribution.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_distribution) | resource |
249+
| [aws_cloudfront_function.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_function) | resource |
127250
| [aws_cloudfront_monitoring_subscription.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_monitoring_subscription) | resource |
128251
| [aws_cloudfront_origin_access_control.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_origin_access_control) | resource |
129252
| [aws_cloudfront_origin_access_identity.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_origin_access_identity) | resource |
@@ -138,8 +261,10 @@ No modules.
138261
| Name | Description | Type | Default | Required |
139262
|------|-------------|------|---------|:--------:|
140263
| <a name="input_aliases"></a> [aliases](#input\_aliases) | Extra CNAMEs (alternate domain names), if any, for this distribution. | `list(string)` | `null` | no |
264+
| <a name="input_cloudfront_functions"></a> [cloudfront\_functions](#input\_cloudfront\_functions) | Map of CloudFront Function configurations. Key is used as default function name if 'name' not specified. | <pre>map(object({<br/> name = optional(string)<br/> runtime = optional(string, "cloudfront-js-2.0")<br/> comment = optional(string)<br/> publish = optional(bool, true)<br/> code = string<br/> key_value_store_associations = optional(list(string), null)<br/> }))</pre> | `{}` | no |
141265
| <a name="input_comment"></a> [comment](#input\_comment) | Any comments you want to include about the distribution. | `string` | `null` | no |
142266
| <a name="input_continuous_deployment_policy_id"></a> [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 |
267+
| <a name="input_create_cloudfront_function"></a> [create\_cloudfront\_function](#input\_create\_cloudfront\_function) | Controls if CloudFront Functions should be created | `bool` | `false` | no |
143268
| <a name="input_create_distribution"></a> [create\_distribution](#input\_create\_distribution) | Controls if CloudFront distribution should be created | `bool` | `true` | no |
144269
| <a name="input_create_monitoring_subscription"></a> [create\_monitoring\_subscription](#input\_create\_monitoring\_subscription) | If enabled, the resource for monitoring subscription will created. | `bool` | `false` | no |
145270
| <a name="input_create_origin_access_control"></a> [create\_origin\_access\_control](#input\_create\_origin\_access\_control) | Controls if CloudFront origin access control should be created | `bool` | `false` | no |
@@ -186,6 +311,10 @@ No modules.
186311
| <a name="output_cloudfront_distribution_status"></a> [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. |
187312
| <a name="output_cloudfront_distribution_tags"></a> [cloudfront\_distribution\_tags](#output\_cloudfront\_distribution\_tags) | Tags of the distribution's |
188313
| <a name="output_cloudfront_distribution_trusted_signers"></a> [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 |
314+
| <a name="output_cloudfront_function_arns"></a> [cloudfront\_function\_arns](#output\_cloudfront\_function\_arns) | The ARNs of the CloudFront Functions created |
315+
| <a name="output_cloudfront_function_etags"></a> [cloudfront\_function\_etags](#output\_cloudfront\_function\_etags) | The ETags of the CloudFront Functions (DEVELOPMENT stage) |
316+
| <a name="output_cloudfront_function_live_stage_etags"></a> [cloudfront\_function\_live\_stage\_etags](#output\_cloudfront\_function\_live\_stage\_etags) | The ETags of the CloudFront Functions (LIVE stage) |
317+
| <a name="output_cloudfront_function_status"></a> [cloudfront\_function\_status](#output\_cloudfront\_function\_status) | The deployment status of the CloudFront Functions |
189318
| <a name="output_cloudfront_monitoring_subscription_id"></a> [cloudfront\_monitoring\_subscription\_id](#output\_cloudfront\_monitoring\_subscription\_id) | The ID of the CloudFront monitoring subscription, which corresponds to the `distribution_id`. |
190319
| <a name="output_cloudfront_origin_access_controls"></a> [cloudfront\_origin\_access\_controls](#output\_cloudfront\_origin\_access\_controls) | The origin access controls created |
191320
| <a name="output_cloudfront_origin_access_controls_ids"></a> [cloudfront\_origin\_access\_controls\_ids](#output\_cloudfront\_origin\_access\_controls\_ids) | The IDS of the origin access identities created |

examples/complete/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,37 @@
11
# Complete CloudFront distribution with most of supported features enabled
22

33
Configuration in this directory creates CloudFront distribution which demos such capabilities:
4+
45
- access logging
56
- origins and origin groups
67
- caching behaviours
78
- Origin Access Identities (with S3 bucket policy)
9+
- Origin Access Control (recommended over OAI)
810
- Lambda@Edge
11+
- **CloudFront Functions** (lightweight JavaScript execution at edge locations)
12+
- Response Headers Policies
913
- ACM certificate
1014
- Route53 record
1115
- VPC Origins
1216

17+
## CloudFront Functions
18+
19+
This example demonstrates CloudFront Functions integration with the module:
20+
21+
**Functions included:**
22+
23+
- `viewer-request-security.js` - Security headers and cache key normalization
24+
- `viewer-response-headers.js` - Add security response headers
25+
- `ab-testing.js` - A/B testing with path rewriting
26+
- `kvstore-redirect.js` - Example with CloudFront KeyValueStore integration (commented)
27+
28+
**Features demonstrated:**
29+
30+
- Module-managed CloudFront Functions creation
31+
- Function association with cache behaviors
32+
- Runtime selection (cloudfront-js-2.0)
33+
- KeyValueStore association pattern
34+
1335
## Usage
1436

1537
To run this example you need to execute:

examples/complete/main.tf

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,23 @@ module "cloudfront" {
181181

182182
function_association = {
183183
# Valid keys: viewer-request, viewer-response
184+
185+
# Option 1: Direct ARN reference to standalone resource
184186
viewer-request = {
185187
function_arn = aws_cloudfront_function.example.arn
186188
}
187189

190+
# Option 2: Dynamic reference to module-managed function by name
191+
# Uncomment to use module-managed functions instead:
192+
# viewer-request = {
193+
# function_name = "viewer-request-security"
194+
# }
195+
196+
# viewer-response = {
197+
# function_name = "viewer-response-headers"
198+
# }
199+
200+
# For this example, using standalone function for both
188201
viewer-response = {
189202
function_arn = aws_cloudfront_function.example.arn
190203
}
@@ -233,6 +246,39 @@ module "cloudfront" {
233246
locations = ["NO", "UA", "US", "GB"]
234247
}
235248

249+
# CloudFront Functions - module managed
250+
create_cloudfront_function = true
251+
cloudfront_functions = {
252+
viewer-request-security = {
253+
runtime = "cloudfront-js-2.0"
254+
comment = "Security headers and cache key normalization"
255+
code = file("${path.module}/viewer-request-security.js")
256+
publish = true
257+
}
258+
viewer-response-headers = {
259+
runtime = "cloudfront-js-2.0"
260+
comment = "Add security response headers"
261+
code = file("${path.module}/viewer-response-headers.js")
262+
publish = true
263+
}
264+
ab-testing = {
265+
runtime = "cloudfront-js-2.0"
266+
comment = "A/B testing function"
267+
code = file("${path.module}/ab-testing.js")
268+
publish = true
269+
}
270+
# Example with KeyValueStore association (uncomment and provide actual KV store ARN)
271+
# kvstore-redirect = {
272+
# runtime = "cloudfront-js-2.0"
273+
# comment = "Function using CloudFront KeyValueStore for dynamic redirects"
274+
# code = file("${path.module}/kvstore-redirect.js")
275+
# publish = true
276+
# key_value_store_associations = [
277+
# "arn:aws:cloudfront::123456789012:key-value-store/example-redirects"
278+
# ]
279+
# }
280+
}
281+
236282
create_response_headers_policy = true
237283
response_headers_policy = {
238284
cors_policy = {

main.tf

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ locals {
33
create_origin_access_control = var.create_origin_access_control && length(keys(var.origin_access_control)) > 0
44
create_vpc_origin = var.create_vpc_origin && length(keys(var.vpc_origin)) > 0
55
create_response_headers_policy = var.create_response_headers_policy && length(keys(var.response_headers_policy)) > 0
6+
create_cloudfront_function = var.create_cloudfront_function && length(keys(var.cloudfront_functions)) > 0
67
}
78

89
resource "aws_cloudfront_response_headers_policy" "this" {
@@ -144,6 +145,17 @@ resource "aws_cloudfront_response_headers_policy" "this" {
144145
}
145146
}
146147

148+
resource "aws_cloudfront_function" "this" {
149+
for_each = local.create_cloudfront_function ? var.cloudfront_functions : {}
150+
151+
name = each.value.name != null ? each.value.name : each.key
152+
runtime = each.value.runtime
153+
comment = each.value.comment
154+
publish = each.value.publish
155+
code = each.value.code
156+
157+
key_value_store_associations = each.value.key_value_store_associations
158+
}
147159

148160
resource "aws_cloudfront_origin_access_identity" "this" {
149161
for_each = local.create_origin_access_identity ? var.origin_access_identities : {}
@@ -208,6 +220,11 @@ resource "aws_cloudfront_distribution" "this" {
208220
web_acl_id = var.web_acl_id
209221
tags = var.tags
210222

223+
# Ensure CloudFront Functions are created before the distribution
224+
depends_on = [
225+
aws_cloudfront_function.this
226+
]
227+
211228
dynamic "logging_config" {
212229
for_each = length(keys(var.logging_config)) == 0 ? [] : [var.logging_config]
213230

@@ -358,7 +375,7 @@ resource "aws_cloudfront_distribution" "this" {
358375

359376
content {
360377
event_type = f.key
361-
function_arn = f.value.function_arn
378+
function_arn = try(f.value.function_arn, aws_cloudfront_function.this[f.value.function_name].arn, null)
362379
}
363380
}
364381

@@ -430,7 +447,7 @@ resource "aws_cloudfront_distribution" "this" {
430447

431448
content {
432449
event_type = f.key
433-
function_arn = f.value.function_arn
450+
function_arn = try(f.value.function_arn, aws_cloudfront_function.this[f.value.function_name].arn, null)
434451
}
435452
}
436453

outputs.tf

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,23 @@ output "cloudfront_response_headers_policy_etags" {
102102
description = "The ETags of the response headers policies created"
103103
value = local.create_response_headers_policy ? { for k, v in aws_cloudfront_response_headers_policy.this : k => v.etag } : {}
104104
}
105+
106+
output "cloudfront_function_arns" {
107+
description = "The ARNs of the CloudFront Functions created"
108+
value = local.create_cloudfront_function ? { for k, v in aws_cloudfront_function.this : k => v.arn } : {}
109+
}
110+
111+
output "cloudfront_function_status" {
112+
description = "The deployment status of the CloudFront Functions"
113+
value = local.create_cloudfront_function ? { for k, v in aws_cloudfront_function.this : k => v.status } : {}
114+
}
115+
116+
output "cloudfront_function_etags" {
117+
description = "The ETags of the CloudFront Functions (DEVELOPMENT stage)"
118+
value = local.create_cloudfront_function ? { for k, v in aws_cloudfront_function.this : k => v.etag } : {}
119+
}
120+
121+
output "cloudfront_function_live_stage_etags" {
122+
description = "The ETags of the CloudFront Functions (LIVE stage)"
123+
value = local.create_cloudfront_function ? { for k, v in aws_cloudfront_function.this : k => v.live_stage_etag } : {}
124+
}

variables.tf

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,3 +292,30 @@ variable "response_headers_policy" {
292292
}))
293293
default = {}
294294
}
295+
296+
variable "create_cloudfront_function" {
297+
description = "Controls if CloudFront Functions should be created"
298+
type = bool
299+
default = false
300+
}
301+
302+
variable "cloudfront_functions" {
303+
description = "Map of CloudFront Function configurations. Key is used as default function name if 'name' not specified."
304+
type = map(object({
305+
name = optional(string)
306+
runtime = optional(string, "cloudfront-js-2.0")
307+
comment = optional(string)
308+
publish = optional(bool, true)
309+
code = string
310+
key_value_store_associations = optional(list(string), null)
311+
}))
312+
default = {}
313+
314+
validation {
315+
condition = alltrue([
316+
for k, v in var.cloudfront_functions :
317+
contains(["cloudfront-js-1.0", "cloudfront-js-2.0"], v.runtime)
318+
])
319+
error_message = "Runtime must be 'cloudfront-js-1.0' or 'cloudfront-js-2.0'. Provided runtime is invalid."
320+
}
321+
}

wrappers/main.tf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ module "wrapper" {
44
for_each = var.items
55

66
aliases = try(each.value.aliases, var.defaults.aliases, null)
7+
cloudfront_functions = try(each.value.cloudfront_functions, var.defaults.cloudfront_functions, {})
78
comment = try(each.value.comment, var.defaults.comment, null)
89
continuous_deployment_policy_id = try(each.value.continuous_deployment_policy_id, var.defaults.continuous_deployment_policy_id, null)
10+
create_cloudfront_function = try(each.value.create_cloudfront_function, var.defaults.create_cloudfront_function, false)
911
create_distribution = try(each.value.create_distribution, var.defaults.create_distribution, true)
1012
create_monitoring_subscription = try(each.value.create_monitoring_subscription, var.defaults.create_monitoring_subscription, false)
1113
create_origin_access_control = try(each.value.create_origin_access_control, var.defaults.create_origin_access_control, false)

0 commit comments

Comments
 (0)