Skip to content

Commit 812763b

Browse files
feat: Cloudfront function support (#175)
Co-authored-by: Bryant Biggs <[email protected]>
1 parent 7190f25 commit 812763b

File tree

13 files changed

+352
-10
lines changed

13 files changed

+352
-10
lines changed

README.md

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,86 @@ 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 key/name
143+
function_association = {
144+
viewer-request = {
145+
function_key = "viewer-request-function"
146+
}
147+
viewer-response = {
148+
function_key = "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+
80160
## Examples
81161

82162
- [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.
@@ -86,7 +166,7 @@ module "cdn" {
86166
- `Error: updating CloudFront Distribution (ETXXXXXXXXXXXX): InvalidArgument: The parameter ForwardedValues cannot be used when a cache policy is associated to the cache behavior.`
87167
- When defining a behavior in `ordered_cache_behavior` and `default_cache_behavior` with a cache policy, you must specify `use_forwarded_values = false`.
88168

89-
```
169+
```hcl
90170
ordered_cache_behavior = [{
91171
path_pattern = "/my/path"
92172
target_origin_id = "my-origin"
@@ -124,6 +204,7 @@ No modules.
124204
| Name | Type |
125205
|------|------|
126206
| [aws_cloudfront_distribution.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_distribution) | resource |
207+
| [aws_cloudfront_function.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_function) | resource |
127208
| [aws_cloudfront_monitoring_subscription.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_monitoring_subscription) | resource |
128209
| [aws_cloudfront_origin_access_control.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_origin_access_control) | resource |
129210
| [aws_cloudfront_origin_access_identity.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_origin_access_identity) | resource |
@@ -138,8 +219,10 @@ No modules.
138219
| Name | Description | Type | Default | Required |
139220
|------|-------------|------|---------|:--------:|
140221
| <a name="input_aliases"></a> [aliases](#input\_aliases) | Extra CNAMEs (alternate domain names), if any, for this distribution. | `list(string)` | `null` | no |
222+
| <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)<br/> code = string<br/> key_value_store_associations = optional(list(string))<br/> }))</pre> | `null` | no |
141223
| <a name="input_comment"></a> [comment](#input\_comment) | Any comments you want to include about the distribution. | `string` | `null` | no |
142224
| <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 |
225+
| <a name="input_create_cloudfront_function"></a> [create\_cloudfront\_function](#input\_create\_cloudfront\_function) | Controls if CloudFront Functions should be created | `bool` | `false` | no |
143226
| <a name="input_create_distribution"></a> [create\_distribution](#input\_create\_distribution) | Controls if CloudFront distribution should be created | `bool` | `true` | no |
144227
| <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 |
145228
| <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 +269,7 @@ No modules.
186269
| <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. |
187270
| <a name="output_cloudfront_distribution_tags"></a> [cloudfront\_distribution\_tags](#output\_cloudfront\_distribution\_tags) | Tags of the distribution's |
188271
| <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 |
272+
| <a name="output_cloudfront_functions"></a> [cloudfront\_functions](#output\_cloudfront\_functions) | The CloudFront Functions created |
189273
| <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`. |
190274
| <a name="output_cloudfront_origin_access_controls"></a> [cloudfront\_origin\_access\_controls](#output\_cloudfront\_origin\_access\_controls) | The origin access controls created |
191275
| <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/.gitignore

Lines changed: 0 additions & 1 deletion
This file was deleted.

examples/complete/README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
# Complete CloudFront distribution with most of supported features enabled
1+
# Complete CloudFront Distribution
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
12+
- Response Headers Policies
913
- ACM certificate
1014
- Route53 record
1115
- VPC Origins
@@ -15,9 +19,9 @@ Configuration in this directory creates CloudFront distribution which demos such
1519
To run this example you need to execute:
1620

1721
```bash
18-
$ terraform init
19-
$ terraform plan
20-
$ terraform apply
22+
terraform init
23+
terraform plan
24+
terraform apply
2125
```
2226

2327
Note that this example may create resources which cost money. Run `terraform destroy` when you don't need these resources.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
function handler(event) {
2+
// A/B testing function using CloudFront Functions
3+
// Assigns users to test groups and routes to different origin paths
4+
5+
var request = event.request;
6+
var headers = request.headers;
7+
var cookies = request.cookies;
8+
9+
// Check if user already has an A/B test cookie
10+
var abTestGroup = null;
11+
12+
if (cookies['ab-test-group']) {
13+
abTestGroup = cookies['ab-test-group'].value;
14+
} else {
15+
// Assign new users to a test group (50/50 split)
16+
// Use CloudFront viewer ID for consistent assignment
17+
var viewerId = event.viewer.address;
18+
var hash = 0;
19+
20+
for (var i = 0; i < viewerId.length; i++) {
21+
hash = ((hash << 5) - hash) + viewerId.charCodeAt(i);
22+
hash = hash & hash; // Convert to 32bit integer
23+
}
24+
25+
abTestGroup = (Math.abs(hash) % 2 === 0) ? 'A' : 'B';
26+
27+
// Note: CloudFront Functions cannot set cookies
28+
// You would set this cookie in the response (using viewer-response function)
29+
// or via JavaScript on the client side
30+
}
31+
32+
// Route to different paths based on test group
33+
var uri = request.uri;
34+
35+
if (abTestGroup === 'B' && !uri.startsWith('/variant-b/')) {
36+
// Rewrite path for variant B users
37+
request.uri = '/variant-b' + uri;
38+
}
39+
40+
// Add header for origin to know which variant was served
41+
headers['x-ab-test-group'] = {
42+
value: abTestGroup
43+
};
44+
45+
return request;
46+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
function handler(event) {
2+
// CloudFront Function with KeyValueStore integration
3+
// Uses KV store for dynamic URL redirects and feature flags
4+
5+
var request = event.request;
6+
var uri = request.uri;
7+
8+
// Note: To use KeyValueStore, associate the KV store ARN with this function
9+
// Example: key_value_store_associations = ["arn:aws:cloudfront::123456789012:key-value-store/redirects"]
10+
11+
// Uncomment when KV store is associated:
12+
/*
13+
var kvsHandle = event.context.kvs;
14+
15+
// Look up redirect mapping in KeyValueStore
16+
var redirectTarget = kvsHandle.get(uri);
17+
18+
if (redirectTarget) {
19+
// Redirect to target URL from KV store
20+
var response = {
21+
statusCode: 301,
22+
statusDescription: 'Moved Permanently',
23+
headers: {
24+
'location': { value: redirectTarget },
25+
'cache-control': { value: 'max-age=3600' }
26+
}
27+
};
28+
return response;
29+
}
30+
31+
// Check feature flags in KV store
32+
var featureFlags = kvsHandle.get('feature-flags');
33+
if (featureFlags) {
34+
var flags = JSON.parse(featureFlags);
35+
36+
// Add feature flag headers for origin
37+
if (flags.enableNewUI) {
38+
request.headers['x-feature-new-ui'] = { value: 'true' };
39+
}
40+
}
41+
*/
42+
43+
// For now, return request as-is
44+
// When KV store is configured, uncomment the code above
45+
return request;
46+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
function handler(event) {
2+
// Viewer request function to add security headers and normalize cache keys
3+
// This function runs before CloudFront checks the cache
4+
5+
var request = event.request;
6+
var headers = request.headers;
7+
8+
// Normalize Host header for consistent caching
9+
if (headers.host) {
10+
headers.host.value = headers.host.value.toLowerCase();
11+
}
12+
13+
// Remove query parameters that don't affect content (for better cache hit ratio)
14+
var uri = request.uri;
15+
var querystring = request.querystring;
16+
17+
// Example: Remove tracking parameters but keep content-affecting ones
18+
var allowedParams = ['id', 'page', 'category'];
19+
var newQuerystring = {};
20+
21+
for (var param in querystring) {
22+
if (allowedParams.includes(param)) {
23+
newQuerystring[param] = querystring[param];
24+
}
25+
}
26+
27+
request.querystring = newQuerystring;
28+
29+
// Add custom header for logging/debugging
30+
headers['x-viewer-country'] = {
31+
value: event.viewer.country || 'unknown'
32+
};
33+
34+
return request;
35+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
function handler(event) {
2+
// Viewer response function to add security and performance headers
3+
// This function runs after CloudFront receives the response from origin
4+
5+
var response = event.response;
6+
var headers = response.headers;
7+
8+
// Add security headers
9+
headers['strict-transport-security'] = {
10+
value: 'max-age=63072000; includeSubdomains; preload'
11+
};
12+
13+
headers['x-content-type-options'] = {
14+
value: 'nosniff'
15+
};
16+
17+
headers['x-frame-options'] = {
18+
value: 'DENY'
19+
};
20+
21+
headers['x-xss-protection'] = {
22+
value: '1; mode=block'
23+
};
24+
25+
headers['referrer-policy'] = {
26+
value: 'strict-origin-when-cross-origin'
27+
};
28+
29+
// Add cache control for static assets
30+
if (event.request.uri.match(/\.(jpg|jpeg|png|gif|ico|css|js|woff|woff2)$/)) {
31+
headers['cache-control'] = {
32+
value: 'public, max-age=31536000, immutable'
33+
};
34+
}
35+
36+
// Add custom header to identify CloudFront Functions processing
37+
headers['x-cloudfront-function'] = {
38+
value: 'viewer-response-headers'
39+
};
40+
41+
return response;
42+
}

0 commit comments

Comments
 (0)