Skip to content

Commit bccc11f

Browse files
Jimmy Gaussenmergify[bot]
authored andcommitted
feat(route53-targets): s3 bucket website target support (#3618)
* feat(route53-targets): s3 bucket website target support * adds region-info S3_STATIC_WEBSITE_ZONE_53_HOSTED_ZONE_ID * chore: tslint fixes * chore: fix indent * chore: change unresolved region error message * chore: RegionInfo.get refactor * chore: fix tests
1 parent 3ba14c8 commit bccc11f

File tree

8 files changed

+161
-4
lines changed

8 files changed

+161
-4
lines changed

packages/@aws-cdk/aws-route53-targets/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ This library contains Route53 Alias Record targets for:
2525
target: route53.RecordTarget.fromAlias(new alias.CloudFrontTarget(distribution)),
2626
});
2727
```
28+
* S3 Bucket WebSite
29+
```ts
30+
new route53.ARecord(this, 'AliasRecord', {
31+
zone,
32+
target: route53.RecordTarget.fromAlias(new alias.BucketWebsiteTarget(bucket)),
33+
});
34+
```
2835
* ELBv2 load balancers
2936
```ts
3037
new route53.ARecord(this, 'AliasRecord', {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import route53 = require('@aws-cdk/aws-route53');
2+
import s3 = require('@aws-cdk/aws-s3');
3+
import {Stack, Token} from '@aws-cdk/core';
4+
import {RegionInfo} from '@aws-cdk/region-info';
5+
6+
/**
7+
* Use a S3 as an alias record target
8+
*/
9+
export class BucketWebsiteTarget implements route53.IAliasRecordTarget {
10+
constructor(private readonly bucket: s3.Bucket) {
11+
}
12+
13+
public bind(_record: route53.IRecordSet): route53.AliasRecordTargetConfig {
14+
const {region} = Stack.of(this.bucket.stack);
15+
16+
if (Token.isUnresolved(region)) {
17+
throw new Error([
18+
'Cannot use an S3 record alias in region-agnostic stacks.',
19+
'You must specify a specific region when you define the stack',
20+
'(see https://docs.aws.amazon.com/cdk/latest/guide/environments.html)'
21+
].join(' '));
22+
}
23+
24+
const hostedZoneId = RegionInfo.get(region).s3StaticWebsiteHostedZoneId;
25+
26+
if (!hostedZoneId) {
27+
throw new Error(`Bucket website target is not supported for the "${region}" region`);
28+
}
29+
30+
return {
31+
hostedZoneId,
32+
dnsName: this.bucket.bucketWebsiteUrl,
33+
};
34+
}
35+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './api-gateway-domain-name';
2+
export * from './bucket-website-target';
23
export * from './classic-load-balancer-target';
34
export * from './cloudfront-target';
45
export * from './load-balancer-target';

packages/@aws-cdk/aws-route53-targets/package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"@aws-cdk/aws-ec2": "^1.3.0",
7676
"@aws-cdk/aws-lambda": "^1.3.0",
7777
"@aws-cdk/aws-s3": "^1.3.0",
78+
"@aws-cdk/region-info": "^1.3.0",
7879
"cdk-build-tools": "file:../../../tools/cdk-build-tools",
7980
"cdk-integ-tools": "file:../../../tools/cdk-integ-tools",
8081
"cfn2ts": "file:../../../tools/cfn2ts",
@@ -88,7 +89,9 @@
8889
"@aws-cdk/aws-elasticloadbalancing": "^1.3.0",
8990
"@aws-cdk/aws-iam": "^1.3.0",
9091
"@aws-cdk/aws-route53": "^1.3.0",
91-
"@aws-cdk/core": "^1.3.0"
92+
"@aws-cdk/aws-s3": "^1.3.0",
93+
"@aws-cdk/core": "^1.3.0",
94+
"@aws-cdk/region-info": "^1.3.0"
9295
},
9396
"homepage": "https://github.com/aws/aws-cdk",
9497
"peerDependencies": {
@@ -98,7 +101,9 @@
98101
"@aws-cdk/aws-elasticloadbalancing": "^1.3.0",
99102
"@aws-cdk/aws-iam": "^1.3.0",
100103
"@aws-cdk/aws-route53": "^1.3.0",
101-
"@aws-cdk/core": "^1.3.0"
104+
"@aws-cdk/aws-s3": "^1.3.0",
105+
"@aws-cdk/core": "^1.3.0",
106+
"@aws-cdk/region-info": "^1.3.0"
102107
},
103108
"engines": {
104109
"node": ">= 8.10.0"
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import '@aws-cdk/assert/jest';
2+
import route53 = require('@aws-cdk/aws-route53');
3+
import s3 = require('@aws-cdk/aws-s3');
4+
import { App, Stack } from '@aws-cdk/core';
5+
import targets = require('../lib');
6+
7+
test('use S3 bucket website as record target', () => {
8+
// GIVEN
9+
const app = new App();
10+
const stack = new Stack(app, 'test', {env: {region: 'us-east-1'}});
11+
12+
const bucketWebsite = new s3.Bucket(stack, 'Bucket');
13+
14+
// WHEN
15+
const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'test.public' });
16+
new route53.ARecord(zone, 'Alias', {
17+
zone,
18+
recordName: '_foo',
19+
target: route53.RecordTarget.fromAlias(new targets.BucketWebsiteTarget(bucketWebsite))
20+
});
21+
22+
// THEN
23+
expect(stack).toHaveResource('AWS::Route53::RecordSet', {
24+
AliasTarget: {
25+
DNSName: { "Fn::GetAtt": [ "Bucket83908E77", "WebsiteURL"] },
26+
HostedZoneId: "Z3AQBSTGFYJSTF"
27+
},
28+
});
29+
});
30+
31+
test('throws if region agnostic', () => {
32+
// GIVEN
33+
const stack = new Stack();
34+
35+
const bucketWebsite = new s3.Bucket(stack, 'Bucket');
36+
37+
// WHEN
38+
const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'test.public' });
39+
40+
// THEN
41+
expect(() => {
42+
new route53.ARecord(zone, 'Alias', {
43+
zone,
44+
recordName: '_foo',
45+
target: route53.RecordTarget.fromAlias(new targets.BucketWebsiteTarget(bucketWebsite))
46+
});
47+
}).toThrow(/Cannot use an S3 record alias in region-agnostic stacks/);
48+
});
49+
50+
test('throws if bucket website hosting is unavailable (cn-northwest-1)', () => {
51+
// GIVEN
52+
const app = new App();
53+
const stack = new Stack(app, 'test', {env: {region: 'cn-northwest-1'}});
54+
55+
const bucketWebsite = new s3.Bucket(stack, 'Bucket');
56+
57+
// WHEN
58+
const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'test.public' });
59+
60+
// THEN
61+
expect(() => {
62+
new route53.ARecord(zone, 'Alias', {
63+
zone,
64+
recordName: '_foo',
65+
target: route53.RecordTarget.fromAlias(new targets.BucketWebsiteTarget(bucketWebsite))
66+
});
67+
}).toThrow(/Bucket website target is not supported/);
68+
});

packages/@aws-cdk/region-info/build-tools/generate-static-data.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,33 @@ async function main(): Promise<void> {
5555
'sa-east-1',
5656
]);
5757

58+
/**
59+
* The hosted zone Id if using an alias record in Route53.
60+
*
61+
* @see https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_website_region_endpoints
62+
*/
63+
const ROUTE_53_BUCKET_WEBSITE_ZONE_IDS: { [region: string]: string } = {
64+
'us-east-2': 'Z2O1EMRO9K5GLX',
65+
'us-east-1': 'Z3AQBSTGFYJSTF',
66+
'us-west-1': 'Z2F56UZL2M1ACD',
67+
'us-west-2': 'Z3BJ6K6RIION7M',
68+
'ap-east-1': 'ZNB98KWMFR0R6',
69+
'ap-south-1': 'Z11RGJOFQNVJUP',
70+
'ap-northeast-3': 'Z2YQB5RD63NC85',
71+
'ap-northeast-2': 'Z3W03O7B5YMIYP',
72+
'ap-southeast-1': 'Z3O0J2DXBE1FTB',
73+
'ap-southeast-2': 'Z1WCIGYICN2BYD',
74+
'ap-northeast-1': 'Z2M4EHUR26P7ZW',
75+
'ca-central-1': 'Z1QDHH18159H29',
76+
'eu-central-1': 'Z21DNDUVLTQW6Q',
77+
'eu-west-1': 'Z1BKCTXD74EZPE',
78+
'eu-west-2': 'Z3GKZC51ZF0DB4',
79+
'eu-west-3': 'Z3R1K369G5AVDG',
80+
'eu-north-1': 'Z3BAZG2TWCNX0D',
81+
'sa-east-1': 'Z7KQH4QJS55SO',
82+
'me-south-1': 'Z1MPMWCPA7YB62',
83+
};
84+
5885
for (const region of AWS_REGIONS) {
5986
const partition = region.startsWith('cn-') ? 'aws-cn' : 'aws';
6087
registerFact(region, 'PARTITION', partition);
@@ -65,8 +92,10 @@ async function main(): Promise<void> {
6592
registerFact(region, 'CDK_METADATA_RESOURCE_AVAILABLE', AWS_CDK_METADATA.has(region) ? 'YES' : 'NO');
6693

6794
registerFact(region, 'S3_STATIC_WEBSITE_ENDPOINT', AWS_OLDER_REGIONS.has(region)
68-
? `s3-website-${region}.${domainSuffix}`
69-
: `s3-website.${region}.${domainSuffix}`);
95+
? `s3-website-${region}.${domainSuffix}`
96+
: `s3-website.${region}.${domainSuffix}`);
97+
98+
registerFact(region, 'S3_STATIC_WEBSITE_ZONE_53_HOSTED_ZONE_ID', ROUTE_53_BUCKET_WEBSITE_ZONE_IDS[region] || '');
7099

71100
for (const service of AWS_SERVICES) {
72101
registerFact(region, ['servicePrincipal', service], Default.servicePrincipal(service, region, domainSuffix));

packages/@aws-cdk/region-info/lib/fact.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ export class FactName {
9696
*/
9797
public static readonly S3_STATIC_WEBSITE_ENDPOINT = 's3-static-website:endpoint';
9898

99+
/**
100+
* The endpoint used for aliasing S3 static websites in Route 53
101+
*/
102+
public static readonly S3_STATIC_WEBSITE_ZONE_53_HOSTED_ZONE_ID = 's3-static-website:route-53-hosted-zone-id';
103+
99104
/**
100105
* The name of the regional service principal for a given service.
101106
*

packages/@aws-cdk/region-info/lib/region-info.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ export class RegionInfo {
4343
return Fact.find(this.name, FactName.S3_STATIC_WEBSITE_ENDPOINT);
4444
}
4545

46+
/**
47+
* The hosted zone ID used by Route 53 to alias a S3 static website in this region (e.g: Z2O1EMRO9K5GLX)
48+
*/
49+
public get s3StaticWebsiteHostedZoneId(): string | undefined {
50+
return Fact.find(this.name, FactName.S3_STATIC_WEBSITE_ZONE_53_HOSTED_ZONE_ID);
51+
}
52+
4653
/**
4754
* The name of the service principal for a given service in this region.
4855
* @param service the service name (e.g: s3.amazonaws.com)

0 commit comments

Comments
 (0)