Host a static website in AWS S3 and serve it through CloudFlare
This is a guide on how to host a static website in AWS S3 and serve it through CloudFlare.
If you are lazy to read, you can watch the youtube video instead.
Today we are going to host a website in an S3 bucket with CloudFlare to act as a CDN, DNS, Reserve proxy, and SSL certificate provider.
Architecture
With S3 buckets, you can host files but you can also activate the static website function to host a website as long as you have only static files like HTML, CSS and images.
Serving our website from CloudFlare comes with a lot of perks like caching your static content in the edge locations from Cloudflare and also inherits the advantage of the WAF/DDOS protection from CloudFlare and encrypted in-transit SSL Certificate.
and ALL of Cloudflare part is FREE!
You will pay for hosting files in S3, but if you are still in the Free Tier and your amount of data is less than 5GB, it will be free for the first year in AWS. Furthermore, even if I don’t have the Free Tier anymore, my AWS bill for running websites in s3 is about 0.01$/month.
This is the equivalent of running AWS CloudFront, the AWS WAF, and ACM (Amazon Certificate Manager) in front of our s3 bucket, which is going to cost you more… still if you have the Free Tier, you won’t probably pay for CloudFront, and the ACM service is free for providing any public certificates even outside of the Free Tier.
CloudFlare IP Ranges
If we just enable the static website in s3, the bucket will be open to everyone with a bucket policy.
This is what I would like to avoid as we are using CloudFlare as a Reverse Proxy, with the “Proxied” option, there is no need of exposing the bucket directly to the entire internet. In this example today, I would like to secure this, so only Cloudflare would be able to reach my public s3 bucket and serve it only through our domain name.
To achieve this, we need a bucket policy that restrains our bucket only to the CloudFlare IPs.
You can find these IP public ranges from Cloudflare on their website. https://www.cloudflare.com/en-gb/ips/
As these IPs could potentially change over time, we are going to create an AWS Lambda function to pull these IPs from the CloudFlare API and then generate a bucket policy and push it to our s3 bucket.
This way, we will run it one time from Lambda… but to run this Lambda function in an automated manner, we will create a rule in EventBridge to run it once a day so if there are new IPs from Cloudflare… it’s going to be added to our bucket policy automatically once a day.
Your domain name needs to be served by CloudFlare. If don’t have a domain name you can register one directly in CloudFlare.
If you have a domain that is not served by CloudFlare, you can “Add Site” in CloudFlare, then in your current registrar, you need to point the nameservers to the ones CloudFlare will provide. it might take a few minutes for the domain to be imported into Cloudflare.
Once your domain is ready in CloudFlare, we can head to s3 and name a bucket as your domain is going to look at the end.
Activate the Static website hosting in your bucket.
Properties section »
For now, we are not going to use the lambda yet and check if our s3 bucket is working with static website enabled.
Create the bucket policy, Permissions tab »
Add the policy :
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadForGetBucketObjects",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::YOUR_BUCKET/*"
}
]
}
Go back to the object section, and upload some static files, you can also use this static website if you don’t know what to put there.
Then go back to Properties tab, at the bottom, you will find the link of your website.
If you click on it, you should see your website hosted in AWS S3!
Copy this link, head to Cloudflare, go to DNS and add a CNAME record.
Set the root or subdomain of your choice, and paste the website link without the http:// part.
Save, and then check the entry you have created. Your website should be served by CloudFlare and hosted on AWS!
Bonus section : Create a lambda function to pull automatically the CloudFlare IPs
As I said before, we could make this a bit more secure and avoid having your S3 bucket open on the internet.
Let’s head to Lambda in AWS.
Create a function from scratch, Engine should be the latest one from Python.
Here is the lambda function to paste :
import boto3
import json
import urllib3
import os
def lambda_handler(event, context):
# CloudFlare connection
http = urllib3.PoolManager()
r = http.request('GET', 'https://api.cloudflare.com/client/v4/ips')
json_content = json.loads(r.data)
ipv4 = json_content["result"]["ipv4_cidrs"]
ipv6 = json_content["result"]["ipv6_cidrs"]
ip_list = ipv4 + ipv6
# S3 connection
s3 = boto3.client('s3')
bucket_name = os.environ['BUCKET_NAME']
bucket_policy = {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadForGetBucketObjects",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": f"arn:aws:s3:::{bucket_name}/*",
"Condition": {
"IpAddress": {
"aws:SourceIp": ip_list
}
}
}
]
}
bucket_policy = json.dumps(bucket_policy)
s3.put_bucket_policy(Bucket=bucket_name, Policy=bucket_policy)
Hit Deploy.
Add an environment variable, it needs to be equal to your bucket name :
Key : BUCKET_NAME
Value : YOUR BUCKET NAME!!!!!
Don’t leave this page, go to the permission section on the left side.
Click on the Role.
Click on the Policies section on the left side, and create a policy.
in JSON, here is the policy to create :
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "s3:PutBucketPolicy",
"Resource": "arn:aws:s3:::startpage.cloudscalr.com"
}
]
}
Go back to your lambda role, and attach the policy.
You can then, come back to the Lambda function, and create a test with a “Test” name, Save.
Run the Lambda function with the Test.
Then now, your S3 bucket policy should be updated with a condition on the IP ranges from Cloudflare!
To automate this, you can head to EventBridge, then Rule, Create rule.
Select schedule, select the second option :
A schedule that runs at a regular rate, such as every 10 minutes.
rate (1, Days)
Next, Target type : AWS Function, select your lambda function.
!!! EventBridge redirects time to time to another region of your lambda function, don’t forget to select the right region to find your Lambda function!!!
That’s it! You got a Bucket policy updated automatically from a Lambda function through EventBridge regularly!
Cheers!
Reference links
Sample website : github.com/KasteM34/startpage
Lambda function : github.com/KasteM34/lambda-pull-cloudflare-ips
S3 Bucket policy read from everywhere : S3-bucket-policy-read-from-everywhere.json
S3 Bucket policy read from cloudflare : S3-bucket-policy-read-from-cloudflare.json