Prerequisites
Why This is Needed
Amazon CloudFront + Amazon S3 is an essential configuration for delivering frontend content on AWS.
Additionally, A/B testing is an indispensable method for improving web services.
However, implementing A/B testing with traditional configurations requires implementation in frontend code or APIs.
This adds complexity to application logic since it requires modifications to the application layer.
In April 2025, there was an update that enabled origin switching with CloudFront Functions.
This time, we'll explore how to easily implement A/B testing by switching between S3 buckets using the selectRequestOriginById() method in CloudFront Functions.
Benefits of Using CloudFront Functions
- High-speed processing at the edge: Instant decisions at locations closest to users
- Simple implementation: Concise JavaScript code that's easy to deploy
- Infrastructure separation: Separates application code from A/B testing logic, improving maintainability
This approach enables efficient A/B testing without sacrificing performance.
Required Knowledge
- Basic understanding of AWS CDK (Cloud Development Kit)
- Basic knowledge of TypeScript/JavaScript
- Basic concepts of AWS CloudFront and S3
- Understanding of A/B testing concepts
Required Environment
- Node.js (v18 or higher recommended)
- AWS CLI configured
- AWS CDK CLI (
npm install -g aws-cdk) - TypeScript environment
AWS Services Used
- Amazon S3: Static website hosting (two buckets)
- Amazon CloudFront: CDN distribution and A/B testing logic
- CloudFront Function: Request routing control
- Origin Access Control (OAC): Secure access control to S3 buckets
Setup
1. Project Initialization
# Create CDK project
mkdir cloudfront-ab-testing
cd cloudfront-ab-testing
cdk init app --language typescript
2. Project Structure Setup
cloudfront-ab-testing/
├── lib/
│ ├── cloudfront-ab-testing.ts # Main stack
│ └── ab-test-function.js # CloudFront Function
├── assets/
│ ├── site-a/
│ │ └── index.html # Variant A
│ └── site-b/
│ └── index.html # Variant B
└── package.json
Implementation Steps
1. CloudFront Function Implementation
Create a CloudFront Function that handles A/B testing logic:
// lib/ab-test-function.js
import cf from 'cloudfront';
function handler(event) {
var request = event.request;
var headers = request.headers;
// Check X-AB-Test header
if (headers['x-ab-test'] && headers['x-ab-test'].value === 'variant-b') {
// Select Origin B
cf.selectRequestOriginById('BUCKET_B_ORIGIN_ID');
} else {
// Select Origin A by default (variant-a or no header)
cf.selectRequestOriginById('BUCKET_A_ORIGIN_ID');
}
return request;
}
2. S3 Bucket Creation
Create two S3 buckets to host different content:
// S3 Bucket A (for variant-a)
const bucketA = new s3.Bucket(this, 'BucketA', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
// S3 Bucket B (for variant-b)
const bucketB = new s3.Bucket(this, 'BucketB', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
3. Origin Access Control (OAC) Configuration
Configure OAC to ensure secure access to S3 buckets:
const originAccessControl = new cloudfront.CfnOriginAccessControl(this, 'OriginAccessControl', {
originAccessControlConfig: {
name: 'ABTestOAC',
originAccessControlOriginType: 's3',
signingBehavior: 'always',
signingProtocol: 'sigv4',
description: 'OAC for A/B testing CloudFront distribution',
},
});
4. Custom Cache Policy Creation
Create a custom policy that includes A/B test headers in the cache key:
const customCachePolicy = new cloudfront.CfnCachePolicy(this, 'ABTestCachePolicy', {
cachePolicyConfig: {
name: 'ABTestCachePolicy',
comment: 'Cache policy that includes x-ab-test header as cache key',
defaultTtl: 86400, // 1 day
maxTtl: 31536000, // 1 year
minTtl: 0,
parametersInCacheKeyAndForwardedToOrigin: {
enableAcceptEncodingGzip: true,
enableAcceptEncodingBrotli: true,
headersConfig: {
headerBehavior: 'whitelist',
headers: ['x-ab-test'],
},
queryStringsConfig: {
queryStringBehavior: 'none',
},
cookiesConfig: {
cookieBehavior: 'none',
},
},
},
});
5. CloudFront Distribution Configuration
Configure distribution with multiple origins and CloudFront Function:
const distribution = new cloudfront.CfnDistribution(this, 'CloudFrontDistribution', {
distributionConfig: {
enabled: true,
priceClass: 'PriceClass_100',
origins: [
{
id: bucketAOriginId,
domainName: bucketA.bucketDomainName,
s3OriginConfig: {
originAccessIdentity: '',
},
originAccessControlId: originAccessControl.attrId,
},
{
id: bucketBOriginId,
domainName: bucketB.bucketDomainName,
s3OriginConfig: {
originAccessIdentity: '',
},
originAccessControlId: originAccessControl.attrId,
},
],
defaultCacheBehavior: {
targetOriginId: bucketAOriginId,
viewerProtocolPolicy: 'redirect-to-https',
cachePolicyId: customCachePolicy.ref,
compress: true,
functionAssociations: [
{
eventType: 'viewer-request',
functionArn: abTestFunction.functionArn,
},
],
},
},
});
6. S3 Bucket Policy Configuration
Configure access permissions from CloudFront using OAC:
const bucketAPolicyStatement = new iam.PolicyStatement({
actions: ['s3:GetObject'],
resources: [bucketA.arnForObjects('*')],
principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')],
conditions: {
StringEquals: {
'AWS:SourceArn': `arn:aws:cloudfront::${this.account}:distribution/${distribution.ref}`,
},
},
});
bucketA.addToResourcePolicy(bucketAPolicyStatement);
7. Content Deployment
Deploy static files to S3 buckets:
// Upload Site A files to BucketA
new s3deploy.BucketDeployment(this, 'DeploySiteA', {
sources: [s3deploy.Source.asset('./assets/site-a')],
destinationBucket: bucketA,
});
// Upload Site B files to BucketB
new s3deploy.BucketDeployment(this, 'DeploySiteB', {
sources: [s3deploy.Source.asset('./assets/site-b')],
destinationBucket: bucketB,
});
8. Complete Stack Implementation
The complete main stack implementation, integrating all the previous steps, is as follows:
import * as cdk from 'aws-cdk-lib/core';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
import * as fs from 'fs';
import * as path from 'path';
export class CloudfrontAbTestingStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// S3 Bucket A (for variant-a)
const bucketA = new s3.Bucket(this, 'BucketA', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
// S3 Bucket B (for variant-b)
const bucketB = new s3.Bucket(this, 'BucketB', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
// Origin Access Control (OAC) - New recommended approach
const originAccessControl = new cloudfront.CfnOriginAccessControl(this, 'OriginAccessControl', {
originAccessControlConfig: {
name: 'ABTestOAC',
originAccessControlOriginType: 's3',
signingBehavior: 'always',
signingProtocol: 'sigv4',
description: 'OAC for A/B testing CloudFront distribution',
},
});
// Define Origin IDs as constants
const bucketAOriginId = 'BucketAOrigin';
const bucketBOriginId = 'BucketBOrigin';
// CloudFront Function for A/B testing
// Load Function code from external file
const functionCodePath = path.join(__dirname, 'ab-test-function.js');
let functionCode = fs.readFileSync(functionCodePath, 'utf8');
// Replace placeholders with actual values
functionCode = functionCode
.replaceAll('BUCKET_A_ORIGIN_ID', bucketAOriginId)
.replaceAll('BUCKET_B_ORIGIN_ID', bucketBOriginId);
const abTestFunction = new cloudfront.Function(this, 'ABTestFunction', {
code: cloudfront.FunctionCode.fromInline(functionCode),
comment: 'A/B testing function to route requests based on X-AB-Test header',
runtime: cloudfront.FunctionRuntime.JS_2_0, // Specify JavaScript Runtime 2.0
});
// Custom cache policy (include x-ab-test header in cache key)
const customCachePolicy = new cloudfront.CfnCachePolicy(this, 'ABTestCachePolicy', {
cachePolicyConfig: {
name: 'ABTestCachePolicy',
comment: 'Cache policy that includes x-ab-test header as cache key',
defaultTtl: 86400, // 1 day
maxTtl: 31536000, // 1 year
minTtl: 0,
parametersInCacheKeyAndForwardedToOrigin: {
enableAcceptEncodingGzip: true,
enableAcceptEncodingBrotli: true,
headersConfig: {
headerBehavior: 'whitelist',
headers: ['x-ab-test'],
},
queryStringsConfig: {
queryStringBehavior: 'none',
},
cookiesConfig: {
cookieBehavior: 'none',
},
},
},
});
// CloudFront Distribution (using low-level API for precise control)
const distribution = new cloudfront.CfnDistribution(this, 'CloudFrontDistribution', {
distributionConfig: {
enabled: true,
priceClass: 'PriceClass_100',
origins: [
{
id: bucketAOriginId,
domainName: bucketA.bucketDomainName,
s3OriginConfig: {
originAccessIdentity: '', // Empty string when using OAC
},
originAccessControlId: originAccessControl.attrId,
},
{
id: bucketBOriginId,
domainName: bucketB.bucketDomainName,
s3OriginConfig: {
originAccessIdentity: '', // Empty string when using OAC
},
originAccessControlId: originAccessControl.attrId,
},
],
defaultCacheBehavior: {
targetOriginId: bucketAOriginId,
viewerProtocolPolicy: 'redirect-to-https',
cachePolicyId: customCachePolicy.ref, // Use custom cache policy
compress: true,
functionAssociations: [
{
eventType: 'viewer-request',
functionArn: abTestFunction.functionArn,
},
],
},
},
});
// Bucket A policy (for OAC) - Set after Distribution creation
const bucketAPolicyStatement = new iam.PolicyStatement({
actions: ['s3:GetObject'],
resources: [bucketA.arnForObjects('*')],
principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')],
conditions: {
StringEquals: {
'AWS:SourceArn': `arn:aws:cloudfront::${this.account}:distribution/${distribution.ref}`,
},
},
});
bucketA.addToResourcePolicy(bucketAPolicyStatement);
// Bucket B policy (for OAC) - Set after Distribution creation
const bucketBPolicyStatement = new iam.PolicyStatement({
actions: ['s3:GetObject'],
resources: [bucketB.arnForObjects('*')],
principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')],
conditions: {
StringEquals: {
'AWS:SourceArn': `arn:aws:cloudfront::${this.account}:distribution/${distribution.ref}`,
},
},
});
bucketB.addToResourcePolicy(bucketBPolicyStatement);
// Deploy HTML files to S3 buckets
// Upload Site A files to BucketA
new s3deploy.BucketDeployment(this, 'DeploySiteA', {
sources: [s3deploy.Source.asset('./assets/site-a')],
destinationBucket: bucketA,
});
// Upload Site B files to BucketB
new s3deploy.BucketDeployment(this, 'DeploySiteB', {
sources: [s3deploy.Source.asset('./assets/site-b')],
destinationBucket: bucketB,
});
// Stack outputs
new cdk.CfnOutput(this, 'CloudFrontDomainName', {
value: distribution.attrDomainName,
description: 'CloudFront Distribution domain name',
});
new cdk.CfnOutput(this, 'BucketAName', {
value: bucketA.bucketName,
description: 'S3 Bucket A name',
});
new cdk.CfnOutput(this, 'BucketBName', {
value: bucketB.bucketName,
description: 'S3 Bucket B name',
});
new cdk.CfnOutput(this, 'ABTestHeaderExample', {
value: 'X-AB-Test: variant-a or X-AB-Test: variant-b',
description: 'A/B test header usage examples',
});
}
}
Full sample project is below:
tacck
/
cloudfront-ab-testing
Check A/B testing on CloudFront
Welcome to your CDK TypeScript project
This is a blank project for CDK development with TypeScript.
The cdk.json file tells the CDK Toolkit how to execute your app.
Useful commands
-
npm run buildcompile typescript to js -
npm run watchwatch for changes and compile -
npm run testperform the jest unit tests -
npx cdk deploydeploy this stack to your default AWS account/region -
npx cdk diffcompare deployed stack with current state -
npx cdk synthemits the synthesized CloudFormation template
Permison Boundly
Bootstraping
cdk bootstrap --custom-permissions-boundary builder-permissions-boundary
https://dev.classmethod.jp/articles/cdk-minimum-deploy-policy/
Check
curl -i https://[YOUR_CLOUDFRONT_SITE]/index.html
curl -i -H 'x-ab-test: variant-a' https://[YOUR_CLOUDFRONT_SITE]/index.html
curl -i -H 'x-ab-test: variant-b' https://[YOUR_CLOUDFRONT_SITE]/index.html
9. Deployment and Testing
# Deploy CDK stack
cdk deploy
# Test A/B functionality
# Default (Variant A)
curl -i https://[YOUR_CLOUDFRONT_DOMAIN]/index.html
# Variant A (explicit specification)
curl -i -H 'X-AB-Test: variant-a' https://[YOUR_CLOUDFRONT_DOMAIN]/index.html
# Variant B
curl -i -H 'X-AB-Test: variant-b' https://[YOUR_CLOUDFRONT_DOMAIN]/index.html
Summary
This A/B testing system combines the following technical elements:
Key Features
- CloudFront Function: Dynamic origin selection based on request headers
- Origin Access Control (OAC): Latest security best practices
- Custom Cache Policy: Cache strategy considering A/B test headers
- Infrastructure as Code: Reproducible infrastructure construction with CDK
Benefits
- Low Latency: CloudFront Functions execute at the edge for high speed
- Cost Efficiency: 1 million executions free, then $0.10 USD per million executions
- Scalable: Leverages CloudFront's global network
- Secure: Proper access control with OAC
Application Possibilities
- Regional content distribution
- Device-specific optimization
- Gradual feature releases (canary deployment)
- Query parameter-based A/B testing
- Personalization
This system can serve as a foundation for implementing more complex A/B testing and traffic distribution logic.
Using CDK makes infrastructure changes easy to manage, enabling continuous improvement and testing.
Top comments (0)