Skip to content
This repository has been archived by the owner on Jun 13, 2024. It is now read-only.

Commit

Permalink
feat(cloudfront): add support for Origin Groups (aws#9360)
Browse files Browse the repository at this point in the history
Fixes aws#9109

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
skinny85 authored Aug 3, 2020
1 parent 375ed9b commit 11e146c
Show file tree
Hide file tree
Showing 9 changed files with 445 additions and 2 deletions.
21 changes: 21 additions & 0 deletions packages/@aws-cdk/aws-cloudfront-origins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,24 @@ new cloudfront.Distribution(this, 'myDist', {
```

See the documentation of `@aws-cdk/aws-cloudfront` for more information.

## Failover Origins (Origin Groups)

You can set up CloudFront with origin failover for scenarios that require high availability.
To get started, you create an origin group with two origins: a primary and a secondary.
If the primary origin is unavailable, or returns specific HTTP response status codes that indicate a failure,
CloudFront automatically switches to the secondary origin.
You achieve that behavior in the CDK using the `OriginGroup` class:

```ts
new cloudfront.Distribution(this, 'myDist', {
defaultBehavior: {
origin: new origins.OriginGroup({
primaryOrigin: new origins.S3Origin(myBucket),
fallbackOrigin: new origins.HttpOrigin('www.example.com'),
// optional, defaults to: 500, 502, 503 and 504
fallbackStatusCodes: [404],
}),
},
});
```
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-cloudfront-origins/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './http-origin';
export * from './load-balancer-origin';
export * from './s3-origin';
export * from './origin-group';
49 changes: 49 additions & 0 deletions packages/@aws-cdk/aws-cloudfront-origins/lib/origin-group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as cloudfront from '@aws-cdk/aws-cloudfront';
import { Construct } from '@aws-cdk/core';

/** Construction properties for {@link OriginGroup}. */
export interface OriginGroupProps {
/**
* The primary origin that should serve requests for this group.
*/
readonly primaryOrigin: cloudfront.IOrigin;

/**
* The fallback origin that should serve requests when the primary fails.
*/
readonly fallbackOrigin: cloudfront.IOrigin;

/**
* The list of HTTP status codes that,
* when returned from the primary origin,
* would cause querying the fallback origin.
*
* @default - 500, 502, 503 and 504
*/
readonly fallbackStatusCodes?: number[];
}

/**
* An Origin that represents a group.
* Consists of a primary Origin,
* and a fallback Origin called when the primary returns one of the provided HTTP status codes.
*/
export class OriginGroup implements cloudfront.IOrigin {
public constructor(private readonly props: OriginGroupProps) {
}

public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig {
const primaryOriginConfig = this.props.primaryOrigin.bind(scope, options);
if (primaryOriginConfig.failoverConfig) {
throw new Error('An OriginGroup cannot use an Origin with its own failover configuration as its primary origin!');
}

return {
originProperty: primaryOriginConfig.originProperty,
failoverConfig: {
failoverOrigin: this.props.fallbackOrigin,
statusCodes: this.props.fallbackStatusCodes,
},
};
}
}
1 change: 0 additions & 1 deletion packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export class S3Origin implements cloudfront.IOrigin {
public bind(scope: cdk.Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig {
return this.origin.bind(scope, options);
}

}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
{
"Resources": {
"Bucket83908E77": {
"Type": "AWS::S3::Bucket",
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete"
},
"BucketPolicyE9A3008A": {
"Type": "AWS::S3::BucketPolicy",
"Properties": {
"Bucket": {
"Ref": "Bucket83908E77"
},
"PolicyDocument": {
"Statement": [
{
"Action": [
"s3:GetObject*",
"s3:GetBucket*",
"s3:List*"
],
"Effect": "Allow",
"Principal": {
"CanonicalUser": {
"Fn::GetAtt": [
"DistributionOrigin1S3Origin5F5C0696",
"S3CanonicalUserId"
]
}
},
"Resource": [
{
"Fn::GetAtt": [
"Bucket83908E77",
"Arn"
]
},
{
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"Bucket83908E77",
"Arn"
]
},
"/*"
]
]
}
]
}
],
"Version": "2012-10-17"
}
}
},
"DistributionOrigin1S3Origin5F5C0696": {
"Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity",
"Properties": {
"CloudFrontOriginAccessIdentityConfig": {
"Comment": "Allows CloudFront to reach the bucket"
}
}
},
"DistributionCFDistribution882A7313": {
"Type": "AWS::CloudFront::Distribution",
"Properties": {
"DistributionConfig": {
"DefaultCacheBehavior": {
"ForwardedValues": {
"QueryString": false
},
"TargetOriginId": "cloudfrontorigingroupDistributionOrigin137659A54",
"ViewerProtocolPolicy": "allow-all"
},
"Enabled": true,
"OriginGroups": {
"Items": [
{
"FailoverCriteria": {
"StatusCodes": {
"Items": [
500,
502,
503,
504
],
"Quantity": 4
}
},
"Id": "cloudfrontorigingroupDistributionOriginGroup10B57F1D1",
"Members": {
"Items": [
{
"OriginId": "cloudfrontorigingroupDistributionOrigin137659A54"
},
{
"OriginId": "cloudfrontorigingroupDistributionOrigin2CCE5D500"
}
],
"Quantity": 2
}
}
],
"Quantity": 1
},
"Origins": [
{
"DomainName": {
"Fn::GetAtt": [
"Bucket83908E77",
"RegionalDomainName"
]
},
"Id": "cloudfrontorigingroupDistributionOrigin137659A54",
"S3OriginConfig": {
"OriginAccessIdentity": {
"Fn::Join": [
"",
[
"origin-access-identity/cloudfront/",
{
"Ref": "DistributionOrigin1S3Origin5F5C0696"
}
]
]
}
}
},
{
"CustomOriginConfig": {
"OriginProtocolPolicy": "https-only"
},
"DomainName": "www.example.com",
"Id": "cloudfrontorigingroupDistributionOrigin2CCE5D500"
}
]
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as cloudfront from '@aws-cdk/aws-cloudfront';
import * as s3 from '@aws-cdk/aws-s3';
import * as cdk from '@aws-cdk/core';
import * as origins from '../lib';

const app = new cdk.App();
const stack = new cdk.Stack(app, 'cloudfront-origin-group');

const bucket = new s3.Bucket(stack, 'Bucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
});

const originGroup = new origins.OriginGroup({
primaryOrigin: new origins.S3Origin(bucket),
fallbackOrigin: new origins.HttpOrigin('www.example.com'),
});

new cloudfront.Distribution(stack, 'Distribution', {
defaultBehavior: { origin: originGroup },
});

app.synth();
142 changes: 142 additions & 0 deletions packages/@aws-cdk/aws-cloudfront-origins/test/origin-group.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import '@aws-cdk/assert/jest';
import * as cloudfront from '@aws-cdk/aws-cloudfront';
import * as s3 from '@aws-cdk/aws-s3';
import { Stack } from '@aws-cdk/core';
import * as origins from '../lib';

let stack: Stack;
let bucket: s3.IBucket;
let primaryOrigin: cloudfront.IOrigin;
beforeEach(() => {
stack = new Stack();
bucket = new s3.Bucket(stack, 'Bucket');
primaryOrigin = new origins.S3Origin(bucket);
});

describe('Origin Groups', () => {
test('correctly render the OriginGroups property of DistributionConfig', () => {
const failoverOrigin = new origins.S3Origin(s3.Bucket.fromBucketName(stack, 'ImportedBucket', 'imported-bucket'));
const originGroup = new origins.OriginGroup({
primaryOrigin,
fallbackOrigin: failoverOrigin,
fallbackStatusCodes: [500],
});

new cloudfront.Distribution(stack, 'Distribution', {
defaultBehavior: { origin: originGroup },
});

const primaryOriginId = 'DistributionOrigin13547B94F';
const failoverOriginId = 'DistributionOrigin2C85CC43B';
expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', {
DistributionConfig: {
Origins: [
{
Id: primaryOriginId,
DomainName: {
'Fn::GetAtt': ['Bucket83908E77', 'RegionalDomainName'],
},
S3OriginConfig: {
OriginAccessIdentity: {
'Fn::Join': ['', [
'origin-access-identity/cloudfront/',
{ Ref: 'DistributionOrigin1S3Origin5F5C0696' },
]],
},
},
},
{
Id: failoverOriginId,
DomainName: {
'Fn::Join': ['', [
'imported-bucket.s3.',
{ Ref: 'AWS::Region' },
'.',
{ Ref: 'AWS::URLSuffix' },
]],
},
S3OriginConfig: {
OriginAccessIdentity: {
'Fn::Join': ['', [
'origin-access-identity/cloudfront/',
{ Ref: 'DistributionOrigin2S3OriginE484D4BF' },
]],
},
},
},
],
OriginGroups: {
Items: [
{
FailoverCriteria: {
StatusCodes: {
Items: [500],
Quantity: 1,
},
},
Id: 'DistributionOriginGroup1A1A31B49',
Members: {
Items: [
{ OriginId: primaryOriginId },
{ OriginId: failoverOriginId },
],
Quantity: 2,
},
},
],
Quantity: 1,
},
},
});
});

test('cannot have an Origin with their own failover configuration as the primary Origin', () => {
const failoverOrigin = new origins.S3Origin(s3.Bucket.fromBucketName(stack, 'ImportedBucket', 'imported-bucket'));
const originGroup = new origins.OriginGroup({
primaryOrigin,
fallbackOrigin: failoverOrigin,
});
const groupOfGroups = new origins.OriginGroup({
primaryOrigin: originGroup,
fallbackOrigin: failoverOrigin,
});

expect(() => {
new cloudfront.Distribution(stack, 'Distribution', {
defaultBehavior: { origin: groupOfGroups },
});
}).toThrow(/An OriginGroup cannot use an Origin with its own failover configuration as its primary origin!/);
});

test('cannot have an Origin with their own failover configuration as the fallback Origin', () => {
const originGroup = new origins.OriginGroup({
primaryOrigin,
fallbackOrigin: new origins.S3Origin(s3.Bucket.fromBucketName(stack, 'ImportedBucket', 'imported-bucket')),
});
const groupOfGroups = new origins.OriginGroup({
primaryOrigin,
fallbackOrigin: originGroup,
});

expect(() => {
new cloudfront.Distribution(stack, 'Distribution', {
defaultBehavior: { origin: groupOfGroups },
});
}).toThrow(/An Origin cannot use an Origin with its own failover configuration as its fallback origin!/);
});

test('cannot have an empty array of fallbackStatusCodes', () => {
const failoverOrigin = new origins.S3Origin(s3.Bucket.fromBucketName(stack, 'ImportedBucket', 'imported-bucket'));
const originGroup = new origins.OriginGroup({
primaryOrigin,
fallbackOrigin: failoverOrigin,
fallbackStatusCodes: [],
});

expect(() => {
new cloudfront.Distribution(stack, 'Distribution', {
defaultBehavior: { origin: originGroup },
});
}).toThrow(/fallbackStatusCodes cannot be empty/);
});
});
Loading

0 comments on commit 11e146c

Please sign in to comment.