Ever since AWS announced CloudFront supporting Origin Access Control (OAC) for Lambda function URL origins I was thinking of improving the image optimization system used by one of my projects that was based on the infrastructure described here.
The problem with this approach was that it's security was based on a secret key in a Custom origin header, which is validated in the Lambda function before processing the image. This approach was triggering the AWS Security rules to flag the Lambda Public Access rule. At the moment of publishing this post this has been already been fixed by the team maintaining the aws-samples although the official blog post still mentions the Custom origin header.
Anyway, as I was not much into CDK and JavaScript I also decided to rewrite the CDK code used to deploy the stack with Terraform and make it available to the public via Githbub terraform-cloudfront-image-optimizatio repository.
This is the sample architecture the image-optimization stack creates.
So let's start.
We have a couple of components in this:
- Original images S3 bucket
- Transformed images S3 bucket
- Cloudfront
- Cloudfront funtion
- Image optimization Lambda function
Setting up AWS Terraform provider
Let's set up the AWS Terraform provider first. I ussualy do it by creating a file called versions.tf but I have seen also configurations using providers.tf
It is done using the following block
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0" #Allow only the right-most version component to increment
}
}
}
provider "aws" {
region = var.aws_region
}
Defining necessary Terraform variables
Let's define the required variables
variable "aws_region" {
description = "The AWS region to deploy resources."
default = "eu-west-1"
}
variable "create_origin_bucket" {
description = "Create an S3 bucket for original images"
type = bool
default = false #Assume there is already a bucket used to serve images
}
variable "original_image_bucket_name" {
description = "Name of the original image bucket"
type = string
}
variable "transformed_image_bucket_name" {
description = "Name of the transformed image bucket"
type = string
}
variable "cloudfront_log_bucket_name" {
description = "S3 bucket for CloudFront logs"
type = string
}
variable "min_ttl" {
description = "Minimum TTL for CloudFront cache"
type = number
default = 86400
}
variable "default_ttl" {
description = "Default TTL for CloudFront cache"
type = number
default = 604800
}
variable "max_ttl" {
description = "Maximum TTL for CloudFront cache"
type = number
default = 2592000
}
variable "max_image_size" {
description = "Maximum image size in bytes"
type = number
default = 4700000
}
variable "image_cache_ttl" {
description = "TTL for transformed images in seconds"
type = string
default = "max-age=31622400"
}
variable "lambda_layer_arn" {
description = "ARN of the Lambda layer" #used for adding the Pillow library layer
type = string
}
Creating Cloudfront distribution
To create the Cloudfront distribution we use the terraform-aws-modules/cloudfront/aws from AWS Hero Anton Babenko.
For an easy distinction of the distribution let's set comment = "Image Optimization CloudFront with Failover"
, obviously this can be changed to the description of your wish.
As we will use Cloudfront Origin Access Control (OAC) we will enable it using following configuration block
origin_access_control = {
s3_oac = {
description = "CloudFront access to S3"
origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
lambda_oac = {
description = "CloudFront access to Lambda"
origin_type = "lambda"
signing_behavior = "always"
signing_protocol = "sigv4"
}
}
Because the cloudfront module doesn't have the posibility to create the cache policy and response header policy we will use Terraform resources
resource "aws_cloudfront_cache_policy" "image_optimization_cache_policy" {
name = "image-optimization-cache-policy"
comment = "Cache policy for image optimization"
default_ttl = var.default_ttl
min_ttl = var.min_ttl
max_ttl = var.max_ttl
parameters_in_cache_key_and_forwarded_to_origin {
cookies_config {
cookie_behavior = "none"
}
headers_config {
header_behavior = "none"
}
query_strings_config {
query_string_behavior = "none"
}
}
}
resource "aws_cloudfront_response_headers_policy" "image_optimization_response_header_policy" {
name = "image-optimization-response-header-policy"
comment = "Response header policy for image optimization"
cors_config {
access_control_allow_credentials = false
access_control_allow_headers {
items = ["*"]
}
access_control_allow_methods {
items = ["GET"]
}
access_control_allow_origins {
items = ["*"]
}
access_control_expose_headers {
items = ["-"]
}
access_control_max_age_sec = 600
origin_override = true
}
custom_headers_config {
items {
header = "x-aws-image-optimization"
override = true
value = "v1.0"
}
items {
header = "vary"
override = true
value = "accept"
}
}
}
Now let's return to the cloudfront module and add the origins and the origin group
origin = {
s3 = {
domain_name = module.transformed_s3_bucket.s3_bucket_bucket_regional_domain_name
origin_access_control = "s3_oac"
origin_shield = {
enabled = true
origin_shield_region = var.aws_region
}
}
lambda = {
domain_name = "${module.image_optimization_lambda.lambda_function_url_id}.lambda-url.${data.aws_region.current.name}.on.aws"
origin_access_control = "lambda_oac"
custom_origin_config = {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1.2"]
}
origin_shield = {
enabled = true
origin_shield_region = var.aws_region
}
}
}
To make this work we will also need the default cache behavior and the Cloudfront function association
default_cache_behavior = {
target_origin_id = "lambda_failover"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
use_forwarded_values = false
cache_policy_id = aws_cloudfront_cache_policy.image_optimization_cache_policy.id
response_headers_policy_id = aws_cloudfront_response_headers_policy.image_optimization_response_header_policy.id
function_association = {
# Valid keys: viewer-request, viewer-response
viewer-request = {
function_arn = aws_cloudfront_function.cloudfront_url_rewrite.arn
}
}
Last pieces of Cloudfront configurations are logging and viewer certificate which we will use the default and also we don't need any geo restrictions and will setup a dependency on the lambda function.
logging_config = {
include_cookies = false
bucket = module.cloudfront_logs.s3_bucket_bucket_domain_name
prefix = "cloudfront-logs/"
}
geo_restriction = {
restriction_type = "none"
}
viewer_certificate = {
cloudfront_default_certificate = true
}
}
depends_on = [module.image_optimization_lambda]
In the next part we will deploy the Cloudfront function and Lambda
Top comments (0)