Setting up CloudFront Proxy Configuration for PostHog
A drop-in Terraform file to configure the PostHog proxy for your project.
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.