Skip to content

Commit 301fb52

Browse files
skinny85Curtis Eppel
authored and
Curtis Eppel
committed
feat(cloudfront): add support for Origin Groups (aws#9360)
Fixes aws#9109 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 89b5dae commit 301fb52

File tree

9 files changed

+445
-2
lines changed

9 files changed

+445
-2
lines changed

packages/@aws-cdk/aws-cloudfront-origins/README.md

+21
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,24 @@ new cloudfront.Distribution(this, 'myDist', {
7676
```
7777

7878
See the documentation of `@aws-cdk/aws-cloudfront` for more information.
79+
80+
## Failover Origins (Origin Groups)
81+
82+
You can set up CloudFront with origin failover for scenarios that require high availability.
83+
To get started, you create an origin group with two origins: a primary and a secondary.
84+
If the primary origin is unavailable, or returns specific HTTP response status codes that indicate a failure,
85+
CloudFront automatically switches to the secondary origin.
86+
You achieve that behavior in the CDK using the `OriginGroup` class:
87+
88+
```ts
89+
new cloudfront.Distribution(this, 'myDist', {
90+
defaultBehavior: {
91+
origin: new origins.OriginGroup({
92+
primaryOrigin: new origins.S3Origin(myBucket),
93+
fallbackOrigin: new origins.HttpOrigin('www.example.com'),
94+
// optional, defaults to: 500, 502, 503 and 504
95+
fallbackStatusCodes: [404],
96+
}),
97+
},
98+
});
99+
```
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './http-origin';
22
export * from './load-balancer-origin';
33
export * from './s3-origin';
4+
export * from './origin-group';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as cloudfront from '@aws-cdk/aws-cloudfront';
2+
import { Construct } from '@aws-cdk/core';
3+
4+
/** Construction properties for {@link OriginGroup}. */
5+
export interface OriginGroupProps {
6+
/**
7+
* The primary origin that should serve requests for this group.
8+
*/
9+
readonly primaryOrigin: cloudfront.IOrigin;
10+
11+
/**
12+
* The fallback origin that should serve requests when the primary fails.
13+
*/
14+
readonly fallbackOrigin: cloudfront.IOrigin;
15+
16+
/**
17+
* The list of HTTP status codes that,
18+
* when returned from the primary origin,
19+
* would cause querying the fallback origin.
20+
*
21+
* @default - 500, 502, 503 and 504
22+
*/
23+
readonly fallbackStatusCodes?: number[];
24+
}
25+
26+
/**
27+
* An Origin that represents a group.
28+
* Consists of a primary Origin,
29+
* and a fallback Origin called when the primary returns one of the provided HTTP status codes.
30+
*/
31+
export class OriginGroup implements cloudfront.IOrigin {
32+
public constructor(private readonly props: OriginGroupProps) {
33+
}
34+
35+
public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig {
36+
const primaryOriginConfig = this.props.primaryOrigin.bind(scope, options);
37+
if (primaryOriginConfig.failoverConfig) {
38+
throw new Error('An OriginGroup cannot use an Origin with its own failover configuration as its primary origin!');
39+
}
40+
41+
return {
42+
originProperty: primaryOriginConfig.originProperty,
43+
failoverConfig: {
44+
failoverOrigin: this.props.fallbackOrigin,
45+
statusCodes: this.props.fallbackStatusCodes,
46+
},
47+
};
48+
}
49+
}

packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts

-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ export class S3Origin implements cloudfront.IOrigin {
4242
public bind(scope: cdk.Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig {
4343
return this.origin.bind(scope, options);
4444
}
45-
4645
}
4746

4847
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
{
2+
"Resources": {
3+
"Bucket83908E77": {
4+
"Type": "AWS::S3::Bucket",
5+
"UpdateReplacePolicy": "Delete",
6+
"DeletionPolicy": "Delete"
7+
},
8+
"BucketPolicyE9A3008A": {
9+
"Type": "AWS::S3::BucketPolicy",
10+
"Properties": {
11+
"Bucket": {
12+
"Ref": "Bucket83908E77"
13+
},
14+
"PolicyDocument": {
15+
"Statement": [
16+
{
17+
"Action": [
18+
"s3:GetObject*",
19+
"s3:GetBucket*",
20+
"s3:List*"
21+
],
22+
"Effect": "Allow",
23+
"Principal": {
24+
"CanonicalUser": {
25+
"Fn::GetAtt": [
26+
"DistributionOrigin1S3Origin5F5C0696",
27+
"S3CanonicalUserId"
28+
]
29+
}
30+
},
31+
"Resource": [
32+
{
33+
"Fn::GetAtt": [
34+
"Bucket83908E77",
35+
"Arn"
36+
]
37+
},
38+
{
39+
"Fn::Join": [
40+
"",
41+
[
42+
{
43+
"Fn::GetAtt": [
44+
"Bucket83908E77",
45+
"Arn"
46+
]
47+
},
48+
"/*"
49+
]
50+
]
51+
}
52+
]
53+
}
54+
],
55+
"Version": "2012-10-17"
56+
}
57+
}
58+
},
59+
"DistributionOrigin1S3Origin5F5C0696": {
60+
"Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity",
61+
"Properties": {
62+
"CloudFrontOriginAccessIdentityConfig": {
63+
"Comment": "Allows CloudFront to reach the bucket"
64+
}
65+
}
66+
},
67+
"DistributionCFDistribution882A7313": {
68+
"Type": "AWS::CloudFront::Distribution",
69+
"Properties": {
70+
"DistributionConfig": {
71+
"DefaultCacheBehavior": {
72+
"ForwardedValues": {
73+
"QueryString": false
74+
},
75+
"TargetOriginId": "cloudfrontorigingroupDistributionOrigin137659A54",
76+
"ViewerProtocolPolicy": "allow-all"
77+
},
78+
"Enabled": true,
79+
"OriginGroups": {
80+
"Items": [
81+
{
82+
"FailoverCriteria": {
83+
"StatusCodes": {
84+
"Items": [
85+
500,
86+
502,
87+
503,
88+
504
89+
],
90+
"Quantity": 4
91+
}
92+
},
93+
"Id": "cloudfrontorigingroupDistributionOriginGroup10B57F1D1",
94+
"Members": {
95+
"Items": [
96+
{
97+
"OriginId": "cloudfrontorigingroupDistributionOrigin137659A54"
98+
},
99+
{
100+
"OriginId": "cloudfrontorigingroupDistributionOrigin2CCE5D500"
101+
}
102+
],
103+
"Quantity": 2
104+
}
105+
}
106+
],
107+
"Quantity": 1
108+
},
109+
"Origins": [
110+
{
111+
"DomainName": {
112+
"Fn::GetAtt": [
113+
"Bucket83908E77",
114+
"RegionalDomainName"
115+
]
116+
},
117+
"Id": "cloudfrontorigingroupDistributionOrigin137659A54",
118+
"S3OriginConfig": {
119+
"OriginAccessIdentity": {
120+
"Fn::Join": [
121+
"",
122+
[
123+
"origin-access-identity/cloudfront/",
124+
{
125+
"Ref": "DistributionOrigin1S3Origin5F5C0696"
126+
}
127+
]
128+
]
129+
}
130+
}
131+
},
132+
{
133+
"CustomOriginConfig": {
134+
"OriginProtocolPolicy": "https-only"
135+
},
136+
"DomainName": "www.example.com",
137+
"Id": "cloudfrontorigingroupDistributionOrigin2CCE5D500"
138+
}
139+
]
140+
}
141+
}
142+
}
143+
}
144+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as cloudfront from '@aws-cdk/aws-cloudfront';
2+
import * as s3 from '@aws-cdk/aws-s3';
3+
import * as cdk from '@aws-cdk/core';
4+
import * as origins from '../lib';
5+
6+
const app = new cdk.App();
7+
const stack = new cdk.Stack(app, 'cloudfront-origin-group');
8+
9+
const bucket = new s3.Bucket(stack, 'Bucket', {
10+
removalPolicy: cdk.RemovalPolicy.DESTROY,
11+
});
12+
13+
const originGroup = new origins.OriginGroup({
14+
primaryOrigin: new origins.S3Origin(bucket),
15+
fallbackOrigin: new origins.HttpOrigin('www.example.com'),
16+
});
17+
18+
new cloudfront.Distribution(stack, 'Distribution', {
19+
defaultBehavior: { origin: originGroup },
20+
});
21+
22+
app.synth();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import '@aws-cdk/assert/jest';
2+
import * as cloudfront from '@aws-cdk/aws-cloudfront';
3+
import * as s3 from '@aws-cdk/aws-s3';
4+
import { Stack } from '@aws-cdk/core';
5+
import * as origins from '../lib';
6+
7+
let stack: Stack;
8+
let bucket: s3.IBucket;
9+
let primaryOrigin: cloudfront.IOrigin;
10+
beforeEach(() => {
11+
stack = new Stack();
12+
bucket = new s3.Bucket(stack, 'Bucket');
13+
primaryOrigin = new origins.S3Origin(bucket);
14+
});
15+
16+
describe('Origin Groups', () => {
17+
test('correctly render the OriginGroups property of DistributionConfig', () => {
18+
const failoverOrigin = new origins.S3Origin(s3.Bucket.fromBucketName(stack, 'ImportedBucket', 'imported-bucket'));
19+
const originGroup = new origins.OriginGroup({
20+
primaryOrigin,
21+
fallbackOrigin: failoverOrigin,
22+
fallbackStatusCodes: [500],
23+
});
24+
25+
new cloudfront.Distribution(stack, 'Distribution', {
26+
defaultBehavior: { origin: originGroup },
27+
});
28+
29+
const primaryOriginId = 'DistributionOrigin13547B94F';
30+
const failoverOriginId = 'DistributionOrigin2C85CC43B';
31+
expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', {
32+
DistributionConfig: {
33+
Origins: [
34+
{
35+
Id: primaryOriginId,
36+
DomainName: {
37+
'Fn::GetAtt': ['Bucket83908E77', 'RegionalDomainName'],
38+
},
39+
S3OriginConfig: {
40+
OriginAccessIdentity: {
41+
'Fn::Join': ['', [
42+
'origin-access-identity/cloudfront/',
43+
{ Ref: 'DistributionOrigin1S3Origin5F5C0696' },
44+
]],
45+
},
46+
},
47+
},
48+
{
49+
Id: failoverOriginId,
50+
DomainName: {
51+
'Fn::Join': ['', [
52+
'imported-bucket.s3.',
53+
{ Ref: 'AWS::Region' },
54+
'.',
55+
{ Ref: 'AWS::URLSuffix' },
56+
]],
57+
},
58+
S3OriginConfig: {
59+
OriginAccessIdentity: {
60+
'Fn::Join': ['', [
61+
'origin-access-identity/cloudfront/',
62+
{ Ref: 'DistributionOrigin2S3OriginE484D4BF' },
63+
]],
64+
},
65+
},
66+
},
67+
],
68+
OriginGroups: {
69+
Items: [
70+
{
71+
FailoverCriteria: {
72+
StatusCodes: {
73+
Items: [500],
74+
Quantity: 1,
75+
},
76+
},
77+
Id: 'DistributionOriginGroup1A1A31B49',
78+
Members: {
79+
Items: [
80+
{ OriginId: primaryOriginId },
81+
{ OriginId: failoverOriginId },
82+
],
83+
Quantity: 2,
84+
},
85+
},
86+
],
87+
Quantity: 1,
88+
},
89+
},
90+
});
91+
});
92+
93+
test('cannot have an Origin with their own failover configuration as the primary Origin', () => {
94+
const failoverOrigin = new origins.S3Origin(s3.Bucket.fromBucketName(stack, 'ImportedBucket', 'imported-bucket'));
95+
const originGroup = new origins.OriginGroup({
96+
primaryOrigin,
97+
fallbackOrigin: failoverOrigin,
98+
});
99+
const groupOfGroups = new origins.OriginGroup({
100+
primaryOrigin: originGroup,
101+
fallbackOrigin: failoverOrigin,
102+
});
103+
104+
expect(() => {
105+
new cloudfront.Distribution(stack, 'Distribution', {
106+
defaultBehavior: { origin: groupOfGroups },
107+
});
108+
}).toThrow(/An OriginGroup cannot use an Origin with its own failover configuration as its primary origin!/);
109+
});
110+
111+
test('cannot have an Origin with their own failover configuration as the fallback Origin', () => {
112+
const originGroup = new origins.OriginGroup({
113+
primaryOrigin,
114+
fallbackOrigin: new origins.S3Origin(s3.Bucket.fromBucketName(stack, 'ImportedBucket', 'imported-bucket')),
115+
});
116+
const groupOfGroups = new origins.OriginGroup({
117+
primaryOrigin,
118+
fallbackOrigin: originGroup,
119+
});
120+
121+
expect(() => {
122+
new cloudfront.Distribution(stack, 'Distribution', {
123+
defaultBehavior: { origin: groupOfGroups },
124+
});
125+
}).toThrow(/An Origin cannot use an Origin with its own failover configuration as its fallback origin!/);
126+
});
127+
128+
test('cannot have an empty array of fallbackStatusCodes', () => {
129+
const failoverOrigin = new origins.S3Origin(s3.Bucket.fromBucketName(stack, 'ImportedBucket', 'imported-bucket'));
130+
const originGroup = new origins.OriginGroup({
131+
primaryOrigin,
132+
fallbackOrigin: failoverOrigin,
133+
fallbackStatusCodes: [],
134+
});
135+
136+
expect(() => {
137+
new cloudfront.Distribution(stack, 'Distribution', {
138+
defaultBehavior: { origin: originGroup },
139+
});
140+
}).toThrow(/fallbackStatusCodes cannot be empty/);
141+
});
142+
});

0 commit comments

Comments
 (0)