Insights

How to Connect S3 with Cloudflare with Terraform to Serve Static Content

Author
Kuba Czaplicki
Published
October 10, 2025
Last update
October 10, 2025

Table of Contents

EXCLUSIVE LAUNCH
AI Implementation in Healthcare Masterclass
Start the course

Key Takeaways

Is Your HealthTech Product Built for Success in Digital Health?

Download the Playbook

Overview

This guide shows you how to combine AWS S3’s storage with Cloudflare’s global CDN network to create a simple, cost-effective solution.

I’m not a big fan of setting up CloudFront via Terraform, but I really like this simple and tested configuration for connecting S3 with Cloudflare and its proxy. It provides super-fast content loading, and with Cloudflare’s generous free tier, you can serve plain static content like avatars and public documents, as well as SPA applications deployed to S3.

Implementation

Firstly we create private S3:

locals {
  cdn_bucket_name = "${local.name_prefix}-${local.env}-s3"
}

resource "aws_s3_bucket" "cdn" {
  bucket = local.cdn_bucket_name
  tags   = local.tags
}

resource "aws_s3_bucket_public_access_block" "cdn" {
  bucket = aws_s3_bucket.cdn.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_policy" "cdn" {
  bucket = aws_s3_bucket.cdn.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "PublicReadGetObject"
        Effect    = "Allow"
        Principal = "*"
        Action    = "s3:GetObject"
        Resource = [
          aws_s3_bucket.cdn.arn,
          "${aws_s3_bucket.cdn.arn}/*",
        ]
        # We want to restrict bucket to be accessed only from Cloudflare #
        Condition = {
          # Cloudflare IPs https://www.cloudflare.com/ips-v4/# #
          IpAddress = {
            "aws:SourceIp" = [
              "173.245.48.0/20",
              "103.21.244.0/22",
              "103.22.200.0/22",
              "103.31.4.0/22",
              "141.101.64.0/18",
              "108.162.192.0/18",
              "190.93.240.0/20",
              "188.114.96.0/20",
              "197.234.240.0/22",
              "198.41.128.0/17",
              "162.158.0.0/15",
              "104.16.0.0/13",
              "104.24.0.0/14",
              "172.64.0.0/13",
              "131.0.72.0/22",
            ]
          }
        }
      },
    ]
  })

  depends_on = [aws_s3_bucket_public_access_block.cdn]
}


resource "aws_s3_bucket_website_configuration" "cdn" {
  bucket = aws_s3_bucket.cdn.id

  index_document {
    suffix = "index.html"
  }

  error_document {
    key = "index.html" # Using index.html for errors to support SPA routing
  }
}

Then create domain like this:

locals {
  cloudflare_zone_id = "123abcd456efg"
  cdn_domain         = "cdn.domain.com" // REMEMBER ! you can have only one domain nested from your root domain - some.cdn.domain.com WON'T WORK
}


resource "cloudflare_dns_record" "app_cname" {
  zone_id = local.cloudflare_zone_id
  name    = local.cdn_domain

  type    = "CNAME"
  ttl     = 1
  content = aws_s3_bucket_website_configuration.cdn.website_endpoint # must have s3-website.*.amazonaws.com
  proxied = true
}

Here’s the cool part.

Cloudflare has introduced a special service called Cloudflare Connector that streamlines the process of setting up host headers, creating rules, and so on, consolidating everything under one service. Documentation is available here

Terrafrom resource:

resource "cloudflare_cloud_connector_rules" "app" {
  zone_id = local.cloudflare_zone_id

  rules = [
    {
      description = "cdn.domain.com"
      enabled     = true
      expression  = "(http.request.full_uri wildcard \"https://cdn.domain.com/*\")"
      parameters = {
        host = "some-cdn-s3.s3-website.<aws_region>.amazonaws.com" # must have s3-website.*.amazonaws.com
      }
      provider = "aws_s3"
    }
  ]
}

…and that’s all. You can access your S3 by your domain through Cloudflare proxy.

Frequently Asked Questions

No items found.

Let's Create the Future of Health Together

Looking for a partner who not only understands your challenges but anticipates your future needs? Get in touch, and let’s build something extraordinary in the world of digital health.

Written by Kuba Czaplicki

Platform Engineer
Kuba designs infrastructure that keeps digital health products secure, compliant, and built to last. With a background in DevOps and a passion for clean, reliable systems, he brings deep technical insight to every project—ensuring security isn’t an afterthought, but a foundation.

See related articles

Newsletter

Kuba Czaplicki