Setting up CloudFront Proxy Configuration for PostHog

A drop-in Terraform file to configure the PostHog proxy for your project.

Redirect street sign (by Stefan Heinemann)

It seems every time I use Google Analytics, nothing works! Maybe it's just me, but it seems an extraordinary pain to get everything configured correctly so that events are received correctly.

In addition, Google Analytics is very generously sharing data with Google of course ...

Thus, I decided to give an alternative a try and after some research decided on PostHog.

The setup was quite straightforward but it was a bit tricky to get the CloudFront distribution for the proxy relay configured using Terraform.

So I though I share the Terraform configuration here for anyone who is interested to set up a proxy for PostHog using CloudFront & Terraform.

This is based on the official PostHog documentation for Setting up AWS CloudFront as a reverse proxy.

Variables

Here the variables required (variables.tf):

variable "aws_region" {
  description = "AWS region for resources"
  type        = string
  default     = "us-east-1"
}

variable "hosted_zone_domain" {
  description = "Domain of the Route 53 hosted zone this website domain should be added to"
  type        = string
}

variable "website_domain" {
  description = "Main website domain"
  type        = string
}

CloudFront Distribution

And here the CloudFront configuration (posthog.tf):

# Standalone PostHog CloudFront Distribution
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# Variables
variable "aws_region" {
  description = "AWS region for resources"
  type        = string
  default     = "us-east-1"
}

variable "hosted_zone_domain" {
  description = "Domain of the Route 53 hosted zone"
  type        = string
}

variable "website_domain" {
  description = "Main website domain"
  type        = string
}

# Data sources
data "aws_route53_zone" "main" {
  name         = var.hosted_zone_domain
  private_zone = false
}

data "aws_acm_certificate" "wildcard_website" {
  domain      = var.website_domain
  statuses    = ["ISSUED"]
  most_recent = true
}

# Random ID for unique resource names
resource "random_id" "id" {
  byte_length = 8
}

# PostHog CloudFront Distribution for reverse proxy
# Cache policy for CORS-enabled requests
resource "aws_cloudfront_cache_policy" "posthog_origin_cors" {
  name = "posthog-origin-cors-${random_id.id.hex}"

  default_ttl = 0
  max_ttl     = 31536000
  min_ttl     = 0

  parameters_in_cache_key_and_forwarded_to_origin {
    cookies_config {
      cookie_behavior = "none"
    }

    headers_config {
      header_behavior = "whitelist"
      headers {
        items = ["Origin", "Authorization"]
      }
    }

    query_strings_config {
      query_string_behavior = "all"
    }

    enable_accept_encoding_brotli = true
    enable_accept_encoding_gzip   = true
  }
}

# Origin request policy for static assets
resource "aws_cloudfront_origin_request_policy" "posthog_origin_request" {
  name = "posthog-origin-request-policy-${random_id.id.hex}"

  cookies_config {
    cookie_behavior = "none"
  }

  headers_config {
    header_behavior = "whitelist"
    headers {
      items = ["Origin"]
    }
  }

  query_strings_config {
    query_string_behavior = "all"
  }
}

# CloudFront distribution for PostHog reverse proxy
resource "aws_cloudfront_distribution" "posthog_relay" {
  enabled = true
  aliases = ["relay-ph.${var.website_domain}"]

  # Primary origin for PostHog API
  origin {
    domain_name = "us.i.posthog.com"
    origin_id   = "posthog-api"

    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "https-only"
      origin_ssl_protocols   = ["TLSv1.2"]
    }
  }

  # Secondary origin for PostHog assets
  origin {
    domain_name = "us-assets.i.posthog.com"
    origin_id   = "posthog-assets"

    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "https-only"
      origin_ssl_protocols   = ["TLSv1.2"]
    }
  }

  # Default cache behavior for API requests
  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
    cached_methods   = ["GET", "HEAD", "OPTIONS"]
    target_origin_id = "posthog-api"

    cache_policy_id          = aws_cloudfront_cache_policy.posthog_origin_cors.id
    origin_request_policy_id = "59781a5b-3903-41f3-afcb-af62929ccde1"

    viewer_protocol_policy = "redirect-to-https"
    compress               = true
  }

  # Ordered cache behavior for static assets
  ordered_cache_behavior {
    path_pattern     = "/static/*"
    allowed_methods  = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
    cached_methods   = ["GET", "HEAD", "OPTIONS"]
    target_origin_id = "posthog-assets"

    cache_policy_id          = aws_cloudfront_cache_policy.posthog_origin_cors.id
    origin_request_policy_id = aws_cloudfront_origin_request_policy.posthog_origin_request.id

    response_headers_policy_id = "eaab4381-ed33-4a86-88ca-d9558dc6cd63"

    viewer_protocol_policy = "redirect-to-https"
    compress               = true
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    acm_certificate_arn = data.aws_acm_certificate.wildcard_website.arn
    ssl_support_method  = "sni-only"
  }

  tags = {
    ManagedBy = "terraform"
    Changed   = formatdate("YYYY-MM-DD hh:mm ZZZ", timestamp())
    Purpose   = "PostHog Reverse Proxy"
  }

  lifecycle {
    ignore_changes = [tags]
  }
}

# Route 53 record for the PostHog relay domain
resource "aws_route53_record" "posthog_relay_record" {
  zone_id = data.aws_route53_zone.main.zone_id
  name    = "relay-ph.${var.website_domain}"
  type    = "A"

  alias {
    name                   = aws_cloudfront_distribution.posthog_relay.domain_name
    zone_id                = aws_cloudfront_distribution.posthog_relay.hosted_zone_id
    evaluate_target_health = false
  }
}

# Output
output "posthog_relay_domain" {
  description = "PostHog CloudFront relay domain"
  value       = aws_cloudfront_distribution.posthog_relay.domain_name
}

Combining with Goldstack Template

This Terraform configuration can easily be used with a Goldstack template, such as Next.js + Tailwind.

For this, simply download the template and then drop in the CloudFront Distribution Terraform configuration from above.

You won't need the Variables declaration then, since that is already provided by the template.