Customizing CloudFront with Terraform: Redirecting by Country
In this blog post, we’ll explore how to use Terraform to create a custom CloudFront function that redirects traffic based on the viewer’s country. We’ll also set up a CloudFront distribution with the custom function and configure it to work with our desired origin.
The CloudFront Function
Our custom function, myredirect_redirect_function, is written in CloudFront’s JavaScript runtime (cloudfront-js-1.0). It takes in the event object, which contains information about the request, including the viewer’s IP address and headers. The function checks the cloudfront-viewer-country header to determine the viewer’s country and then redirects the traffic accordingly.
The function also includes logic to check if the IP address is whitelisted, and if so, allows the request to proceed. Additionally, it checks if the request is for a specific type of content (e.g., HTML) and blocks it if not.
The CloudFront Distribution
Our CloudFront distribution, mysite, is configured to work with our origin (origin.kmerkuri.de) and has the following settings:
- Aliases:
www.kmerkuri.de - Comment:
cloudfront for mypage - Enabled:
true - Is IPv6 enabled:
true - Wait for deployment:
true
The default cache behavior is configured to cache GET and HEAD requests for 1 hour, and compress responses. We’ve also associated our custom function with the viewer-request event, which will trigger the function to run on every request.
Configuring Forwarded Values
We’re forwarding several headers from the origin to the viewer, including Accept-Language, Host, User-Agent, CloudFront-Viewer-Country, Origin, Accept, Access-Control-Request-Method, Access-Control-Request-Headers, and Referer. We’re also forwarding cookies with the forward setting set to "none".
Terraform Configuration
Here’s the complete Terraform configuration file:
resource "aws_cloudfront_function" "myredirect_redirect_function" {
name = "redirectByCountry"
runtime = "cloudfront-js-1.0"
publish = true
code = <<EOF
function handler(event) {
var request = event.request;
var headers = request.headers;
var clientIP = event.viewer.ip;
var country = headers['cloudfront-viewer-country'].value;
var userAgent = headers['user-agent'] ? headers['user-agent'].value : '';
request.headers['x-forwarded-for'] = {value: clientIP};
var accept = headers['accept'] ? headers['accept'].value : '';
var xAppPlatform = headers['x-app-platform'] ? headers['x-app-platform'].value : '';
// Define your predefined IP block
var isWhitelisted = checkIP(clientIP, whitelistedIPRanges);
if (!accept.includes('text/html')) {
return request;
}
if (botUserAgents.some(function(bot) { return userAgent.includes(bot); })) {
return request;
}
if (xAppPlatform.includes('native')) { // 'native ios' or 'native android'
return request;
}
if (isWhitelisted) {
// Code to execute if the IP address is whitelisted
return request;
}
else if (country === 'AT') {
return {
statusCode: 302,
statusDescription: 'Found',
headers: {
'location': {
value: 'https://www.klevimerkuri.com'
}
},
};}
else if (country === 'BE') {
return {
statusCode: 302,
statusDescription: 'Found',
headers: {
'location': {
value: 'https://www.kmerkuri.be'
}
},
};}
else if (country === 'DK') {
return {
statusCode: 302,
statusDescription: 'Found',
headers: {
'location': {
value: 'https://www.kmerkuri.dk'
}
},
};
}
else if (country === 'RS') {
return {
statusCode: 302,
statusDescription: 'Found',
headers: {
'location': {
value: 'https://www.kmerkuri.rs'
}
},
};
}
else if (country === 'CZ') {
return {
statusCode: 302,
statusDescription: 'Found',
headers: {
'location': {
value: 'https://www.kmerkuri.cz'
}
},
};
}else {
return request;
}
}
function checkIP(clientIP, whitelistedIPRanges) {
for (var i = 0; i < whitelistedIPRanges.length; i++) {
var ipRange = whitelistedIPRanges[i];
if (ipRange.includes('/')) {
var ipAndSubnet = ipRange.split('/');
var ip = ipAndSubnet[0];
var subnet = ipAndSubnet[1];
var subnetMask = 32 - parseInt(subnet, 10);
var ipInt = ipToInt(clientIP);
var subnetInt = ipToInt(ip);
var maskInt = 0xffffffff << subnetMask;
if ((ipInt & maskInt) === (subnetInt & maskInt)) {
return true;
}
} else {
if (clientIP === ipRange) {
return true;
}
}
}
return false;
}
function ipToInt(ip) {
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0);
}
var whitelistedIPRanges = [
'192.168.1.0/24',
'192.168.2.124/32',
];
var botUserAgents = [
'Googlebot',
'Bingbot',
'Slurp',
'DuckDuckBot',
'Baiduspider',
'YandexBot',
'Sogou',
'Exabot',
'Facebot',
'facebookexternalhit',
'ia_archiver',
'pingdom',
'twitterbot',
'crawler',
'spider',
'robot'
];
EOF
}
resource "aws_cloudfront_distribution" "mysite" {
aliases = [
"www.kmerkuri.de",
]
comment = "cloudfront for mypage"
enabled = true
is_ipv6_enabled = true
wait_for_deployment = true
default_cache_behavior {
allowed_methods = [
"GET",
"HEAD",
]
cached_methods = [
"GET",
"HEAD",
]
compress = true
target_origin_id = "origin.kmerkuri.de"
function_association {
event_type = "viewer-request"
function_arn = aws_cloudfront_function.myredirect_redirect_function.arn
}
viewer_protocol_policy = "redirect-to-https"
forwarded_values {
headers = [
"Accept-Language",
"Host",
"User-Agent",
"CloudFront-Viewer-Country",
"Origin",
"Accept",
"Access-Control-Request-Method",
"Access-Control-Request-Headers",
"Referer",
"X-App-Platform",
]
query_string = false
query_string_cache_keys = []
cookies {
forward = "none"
whitelisted_names = []
}
}
}
}
Conclusion
In this blog post, we’ve seen how to use Terraform to create a custom CloudFront function that redirects traffic based on the viewer’s country. We’ve also set up a CloudFront distribution with the custom function and configured it to work with our desired origin. This configuration allows us to customize the behavior of our CloudFront distribution and provide a better experience for our users.