Sitemap

Deploy a Static Web App to AWS (S3 + CloudFront) with CloudFormation + Cloudflare DNS

5 min readJun 6, 2025

--

Press enter or click to view image in full size

This tutorial guides you through deploying a secure and scalable static web application on AWS using Amazon S3, CloudFront, Cloudflare DNS, and a custom ACM certificate via a CloudFormation-driven workflow. It covers end-to-end infrastructure provisioning using Infrastructure-as-Code principles, eliminating manual setup. This approach helps you achieve secure HTTPS delivery, custom domain support, and cost efficiency — especially when avoiding non-free AWS services like Route 53 or AWS WAF.

Key highlights:

  • Setting up S3 for static website hosting
  • Creating a CloudFront distribution with HTTPS and custom domains
  • Using Cloudflare for DNS instead of Route 53
  • Automatically requesting and validating a certificate using a custom Lambda-based provider

1. Provision an ACM certificate with DNS validation

Using a custom CloudFormation provider (Lambda-backed) hosted in an S3 bucket. The custom Lambda is from binxio/cfn-certificate-provider, which automates certificate validation via DNS.

Step 1: Define Parameters

Parameters:
S3BucketName:
Type: String
Description: S3 bucket containing the zipped Lambda custom provider

CFNCustomProviderZipFileName:
Type: String
Default: 'cfn-helper/cfn-certificate-provider-1.0.0-beta.zip'
Description: Path to the custom provider zip file in S3

RetentionInDays:
Type: Number
Default: 30
Description: How long to retain CloudWatch logs for the Lambda function CFNCustomProviderZipFileName:
Type: String
Default: 'cfn-helper/cfn-certificate-provider-1.0.0-beta.zip'
Description: Path to the custom provider zip file in S3 RetentionInDays:
Type: Number
Default: 30
Description: How long to retain CloudWatch logs for the Lambda function

Explanation:

  • S3BucketName: where you uploaded the Lambda zip.
  • CFNCustomProviderZipFileName: location of the Lambda zip in that bucket.
  • RetentionInDays: controls CloudWatch log retention.

Step 2: Lambda Role & Policy

Resources:
LambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: [lambda.amazonaws.com]
Action: sts:AssumeRole
LambdaPolicy:
Type: AWS::IAM::Policy
DependsOn: [LambdaRole]
Properties:
PolicyName: CFNCertificateDomainResourceRecordProvider
Roles: [!Ref LambdaRole]
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- acm:RequestCertificate
- acm:DescribeCertificate
- acm:UpdateCertificateOptions
- acm:DeleteCertificate
Resource: '*'
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource: !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:binxio-cfn-certificate-provider
- Effect: Allow
Action: logs:*
Resource: arn:aws:logs:*:*:*

Explanation:

This Lambda role and policy allow the custom provider to:

  • Manage ACM certificates.
  • Log to CloudWatch.
  • Optionally invoke itself for resource coordination.

Step 3: Lambda Function Definition

CFNCustomProvider:
Type: AWS::Lambda::Function
DependsOn: [LambdaRole]
Properties:
Description: Custom provider to automate ACM DNS validation
FunctionName: binxio-cfn-certificate-provider
Handler: provider.handler
Runtime: python3.9
MemorySize: 128
Timeout: 300
Role: !GetAtt LambdaRole.Arn
Code:
S3Bucket: !Ref S3BucketName
S3Key: !Ref CFNCustomProviderZipFileName

Explanation:

  • This is the Lambda function implementing the logic to provision and validate ACM certificates via DNS.
  • You need to upload the cfn-certificate-provider zip file into your S3 bucket before deployment.

Step 4: CloudWatch Logs

CFNCustomProviderLogGroup:
Type: AWS::Logs::LogGroup
DependsOn: [CFNCustomProvider]
Properties:
LogGroupName: !Sub /aws/lambda/${CFNCustomProvider}
RetentionInDays: !Ref RetentionInDays

Explanation:

  • Creates a dedicated log group for the Lambda function for monitoring and debugging.
  • Retention is configurable via parameter.

Step 5: Use the Custom Resource to Request and Validate the Certificate

Certificate:
Type: Custom::Certificate
Properties:
DomainName: !Sub '${SubdomainName}.${BaseDomainName}'
ValidationMethod: DNS
KeyAlgorithm: EC_prime256v1
ServiceToken: !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:binxio-cfn-certificate-provider
CertificateDNSRecord:
Type: Custom::CertificateDNSRecord
Properties:
CertificateArn: !Ref Certificate
DomainName: !Sub '${SubdomainName}.${BaseDomainName}'
ServiceToken: !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:binxio-cfn-certificate-provider

Explanation:

  • Custom::Certificate: creates a new ACM certificate using DNS validation.
  • You must manually add the DNS CNAME record to your DNS provider (like Cloudflare or Route 53).
  • Custom::CertificateDNSRecord: fetches the DNS validation record, which you can extract via outputs to guide the DNS configuration.

Additional Recommendations

  • You can define Outputs to print the validation DNS name and value for DNS configuration.
  • Ensure the Lambda is deployed in us-east-1 if used with CloudFront.
  • Certificates won’t be issued until DNS validation is completed and propagated.

Put all above yaml code snippet in a CloudFormation Template (acm-cert.yml)

CLI:

aws cloudformation deploy \
--template-file acm-cert.yml \
--stack-name webapp-acm-cert \
--region us-east-1 \
--parameter-overrides DomainName=webapp.example.com \
--capabilities CAPABILITY_NAMED_IAM

2. Register a Domain Name

💡 Why: You need a domain (e.g., example.com) to route users to your CloudFront distribution. Use any registrar like Namecheap or GoDaddy.

3. Set Up Cloudflare as DNS Provider

💡 Why: Cloudflare will host your DNS records and provide free CDN + security features like WAF.

Steps:

  • Go to Cloudflare, add your domain.
  • Cloudflare will give you nameservers (e.g., ada.ns.cloudflare.com, hank.ns.cloudflare.com)
  • Go to your registrar (e.g., Namecheap), and update your domain’s nameservers.

4. Validate ACM Certificate via DNS

💡 Why: DNS validation proves to AWS you control the domain name.

How:

  • In ACM console, copy the DNS CNAME Name and Value (e.g., _xxxx.webapp.example.com)
  • In Cloudflare DNS:
  • Add CNAME record:
  • Name: _xxxx.webapp(Important: omit the domain part!)
  • Target: the ACM-provided target (e.g., _xyz.acm-validations.aws)
  • Proxy Status: DNS Only (Gray Cloud)

Verify:

Use https://dnschecker.org to check propagation:

  • Enter _xxxx.webapp.example.com
  • Choose CNAME type

✅ Wait until ACM cert status changes to ISSUED.

5. Create an S3 Bucket for Web App Content

💡 Why: This is where your static HTML/CSS/JS files will be stored.

CloudFormation Template (s3-bucket.yml)

Resources:
WebAppBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${AppName}-webapp-${AWS::AccountId}
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
Parameters:
AppName:
Type: String

CLI:

aws cloudformation deploy \
--template-file s3-bucket.yml \
--stack-name webapp-s3 \
--parameter-overrides AppName=webapp \
--capabilities CAPABILITY_NAMED_IAM

6. Create a CloudFront Distribution with SSL

💡 Why: CloudFront will serve your site globally with HTTPS, performance, and caching.

CloudFormation Template (cloudfront.yml)

Parameters:
DomainName:
Type: String
BucketName:
Type: String
CertificateArn:
Type: String
Resources:
CFDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Enabled: true
Aliases:
- !Ref DomainName
DefaultRootObject: index.html
ViewerCertificate:
AcmCertificateArn: !Ref CertificateArn
SslSupportMethod: sni-only
DefaultCacheBehavior:
TargetOriginId: s3origin
ViewerProtocolPolicy: redirect-to-https
AllowedMethods: [GET, HEAD]
ForwardedValues:
QueryString: false
Origins:
- Id: s3origin
DomainName: !Sub "${BucketName}.s3.amazonaws.com"
S3OriginConfig: {}

CLI:

aws cloudformation deploy \
--template-file cloudfront.yml \
--stack-name webapp-cloudfront \
--parameter-overrides \
DomainName=webapp.example.com \
BucketName=webapp-webapp-123456789012 \
CertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/xxx-xxx \
--capabilities CAPABILITY_NAMED_IAM

7. Upload Static Web App Content to S3

💡 Why: This is the actual website users will see.

aws s3 sync ./dist s3://webapp-webapp-123456789012 --delete

📦 Replace ./dist with your local build directory.

8. (Optional) Invalidate CloudFront Cache

💡 Why: After each update, invalidate to ensure CloudFront serves the latest version.

aws cloudfront create-invalidation \
--distribution-id YOUR_DISTRIBUTION_ID \
--paths "/*"

9. Connect Domain to CloudFront in Cloudflare

💡 Why: Route traffic from webapp.example.com to CloudFront.

In Cloudflare DNS:

  • Type: CNAME
  • Name: webapp
  • Target: xxxx.cloudfront.net (from CloudFront)
  • Proxy: Enabled (orange cloud ✅)

10. Enable Basic WAF (Cloudflare)

💡 Why: Save money by using Cloudflare’s free WAF instead of AWS WAF.

Go to Security > WAF in Cloudflare dashboard:

  • You’ll see:

“Free customers are receiving protection from the Cloudflare Free Managed Ruleset…”

💰 This is enough for most static sites — no need to pay for AWS WAF.

Checklist

Step

  • Request ACM Cert (DNS validated)

aws cloudformation deploy ... acm-cert.yml

  • Register Domain Manual (Namecheap, GoDaddy)
  • Setup Cloudflare DNS Manual
  • Validate ACM DNS record in Cloudflare
  • Create S3 Bucket

aws cloudformation deploy ... s3-bucket.yml

  • Create CloudFront

aws cloudformation deploy ... cloudfront.yml

  • Upload Web Files

aws s3 sync ./dist ...

  • Invalidate Cache

aws cloudfront create-invalidation

  • Point DNS to CloudFront CNAME in Cloudflare
  • Use Free WAF Enabled by default in Cloudflare

Summary

By following this tutorial, you now have:

  • A CloudFormation-powered deployment workflow for static websites
  • A custom SSL certificate provisioned and validated through DNS
  • Your domain managed by Cloudflare and integrated with AWS infrastructure
  • A secure and performant static site deployed with minimal ongoing cost

This setup leverages AWS best practices and avoids additional charges from services like Route 53 and AWS WAF — thanks to Cloudflare’s free DNS and WAF features.

--

--