Forem

Deploying a Cloudfront Lambda Image Optimization stack with Terraform Part 1 - Cloudfront

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.
Sample image-optimization architecture

Image from AWS Networking & Content Delivery blog

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
      }
    }
  }

Enter fullscreen mode Exit fullscreen mode

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
      }
    }
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

In the next part we will deploy the Cloudfront function and Lambda

Top comments (0)