Deploy a Static Web App to AWS (S3 + CloudFront) with CloudFormation + Cloudflare DNS
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 functionExplanation:
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 CFNCustomProviderZipFileNameExplanation:
- This is the Lambda function implementing the logic to provision and validate ACM certificates via DNS.
- You need to upload the
cfn-certificate-providerzip 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 RetentionInDaysExplanation:
- 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-providerExplanation:
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
Outputsto print the validation DNS name and value for DNS configuration. - Ensure the Lambda is deployed in
us-east-1if 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_IAM2. 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: StringCLI:
aws cloudformation deploy \
--template-file s3-bucket.yml \
--stack-name webapp-s3 \
--parameter-overrides AppName=webapp \
--capabilities CAPABILITY_NAMED_IAM6. 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_IAM7. 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.
