Invalidate CloudFront with Lambda and S3 events

Bla bla bla... You're here for the solution, not to hear me talk about it. See code example. Improvise, adapt and overcome.

One thing though, unless you have a shit metric ton of objects that you want to keep all hot and sizzling in cache I suggest you just invalidate the entire path, and not per object. Amazon has this weird pricing model where wildcard invalidations are priced as a single path.

lambda.py

import os
import time
import boto3

def lambda_handler(event, context):
    client = boto3.client('cloudfront')
    invalidation = client.create_invalidation(
        DistributionId=os.environ['DISTRIBUTION_ID'],
        InvalidationBatch={
            'Paths': {
                'Quantity': 1,
                'Items': [
                    '/*',
                ]
            },
            'CallerReference': str(time.time())
        }
    )

terraform.tf

data "archive_file" "lambda" {
  type        = "zip"
  source_file = "lambda_function.py"
  output_path = "lambda.zip"
}

data "aws_iam_policy_document" "role" {
  statement {
    actions = [
      "sts:AssumeRole",
    ]

    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "this" {
  name               = "${local.prefix}-lambda"
  assume_role_policy = "${data.aws_iam_policy_document.role.json}"
}

data "aws_iam_policy_document" "policy" {
  statement {
    actions = [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents",
    ]

    resources = ["arn:aws:logs:*:*:*"]
  }

  statement {
    actions = [
      "cloudfront:CreateInvalidation",
    ]

    # You'd assume that Amazon lets you specify the ARN of the CloudFront
    # distribution. But na-ah!
    resources = ["*"]
  }
}

resource "aws_iam_role_policy" "this" {
  name   = "${local.prefix}-lambda"
  policy = "${data.aws_iam_policy_document.policy.json}"
  role   = "${aws_iam_role.this.name}"
}

resource "aws_lambda_function" "this" {
  runtime          = "python3.6"
  filename         = "lambda.zip"
  function_name    = "${local.prefix}-cloudfront-invalidation"
  role             = "${aws_iam_role.this.arn}"
  handler          = "lambda_function.lambda_handler"
  source_code_hash = "${data.archive_file.lambda.output_base64sha256}"

  environment {
    variables = {
      DISTRIBUTION_ID = "${aws_cloudfront_distribution.this.id}"
    }
  }
}

resource "aws_lambda_permission" "this" {
  action        = "lambda:InvokeFunction"
  function_name = "${aws_lambda_function.this.arn}"
  principal     = "s3.amazonaws.com"
  source_arn    = "${aws_s3_bucket.this.arn}"
}

resource "aws_s3_bucket_notification" "this" {
  # This would be the bucket that CloudFront fetches the objects from.
  bucket = "${aws_s3_bucket.this.id}"

  lambda_function {
    lambda_function_arn = "${aws_lambda_function.this.arn}"
    events              = ["s3:ObjectCreated:*"]
    filter_prefix       = "index.html"
  }
}