Skip to content

Commit

Permalink
feat(route53-targets): s3 bucket website target support (#3618)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Jimmy Gaussen authored and mergify[bot] committed Aug 12, 2019
1 parent 3ba14c8 commit bccc11f
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 4 deletions.
7 changes: 7 additions & 0 deletions packages/@aws-cdk/aws-route53-targets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ This library contains Route53 Alias Record targets for:
target: route53.RecordTarget.fromAlias(new alias.CloudFrontTarget(distribution)),
});
```
* S3 Bucket WebSite
```ts
new route53.ARecord(this, 'AliasRecord', {
zone,
target: route53.RecordTarget.fromAlias(new alias.BucketWebsiteTarget(bucket)),
});
```
* ELBv2 load balancers
```ts
new route53.ARecord(this, 'AliasRecord', {
Expand Down
35 changes: 35 additions & 0 deletions packages/@aws-cdk/aws-route53-targets/lib/bucket-website-target.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import route53 = require('@aws-cdk/aws-route53');
import s3 = require('@aws-cdk/aws-s3');
import {Stack, Token} from '@aws-cdk/core';
import {RegionInfo} from '@aws-cdk/region-info';

/**
* Use a S3 as an alias record target
*/
export class BucketWebsiteTarget implements route53.IAliasRecordTarget {
constructor(private readonly bucket: s3.Bucket) {
}

public bind(_record: route53.IRecordSet): route53.AliasRecordTargetConfig {
const {region} = Stack.of(this.bucket.stack);

if (Token.isUnresolved(region)) {
throw new Error([
'Cannot use an S3 record alias in region-agnostic stacks.',
'You must specify a specific region when you define the stack',
'(see https://docs.aws.amazon.com/cdk/latest/guide/environments.html)'
].join(' '));
}

const hostedZoneId = RegionInfo.get(region).s3StaticWebsiteHostedZoneId;

if (!hostedZoneId) {
throw new Error(`Bucket website target is not supported for the "${region}" region`);
}

return {
hostedZoneId,
dnsName: this.bucket.bucketWebsiteUrl,
};
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-route53-targets/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './api-gateway-domain-name';
export * from './bucket-website-target';
export * from './classic-load-balancer-target';
export * from './cloudfront-target';
export * from './load-balancer-target';
9 changes: 7 additions & 2 deletions packages/@aws-cdk/aws-route53-targets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"@aws-cdk/aws-ec2": "^1.3.0",
"@aws-cdk/aws-lambda": "^1.3.0",
"@aws-cdk/aws-s3": "^1.3.0",
"@aws-cdk/region-info": "^1.3.0",
"cdk-build-tools": "file:../../../tools/cdk-build-tools",
"cdk-integ-tools": "file:../../../tools/cdk-integ-tools",
"cfn2ts": "file:../../../tools/cfn2ts",
Expand All @@ -88,7 +89,9 @@
"@aws-cdk/aws-elasticloadbalancing": "^1.3.0",
"@aws-cdk/aws-iam": "^1.3.0",
"@aws-cdk/aws-route53": "^1.3.0",
"@aws-cdk/core": "^1.3.0"
"@aws-cdk/aws-s3": "^1.3.0",
"@aws-cdk/core": "^1.3.0",
"@aws-cdk/region-info": "^1.3.0"
},
"homepage": "https://github.com/aws/aws-cdk",
"peerDependencies": {
Expand All @@ -98,7 +101,9 @@
"@aws-cdk/aws-elasticloadbalancing": "^1.3.0",
"@aws-cdk/aws-iam": "^1.3.0",
"@aws-cdk/aws-route53": "^1.3.0",
"@aws-cdk/core": "^1.3.0"
"@aws-cdk/aws-s3": "^1.3.0",
"@aws-cdk/core": "^1.3.0",
"@aws-cdk/region-info": "^1.3.0"
},
"engines": {
"node": ">= 8.10.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import '@aws-cdk/assert/jest';
import route53 = require('@aws-cdk/aws-route53');
import s3 = require('@aws-cdk/aws-s3');
import { App, Stack } from '@aws-cdk/core';
import targets = require('../lib');

test('use S3 bucket website as record target', () => {
// GIVEN
const app = new App();
const stack = new Stack(app, 'test', {env: {region: 'us-east-1'}});

const bucketWebsite = new s3.Bucket(stack, 'Bucket');

// WHEN
const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'test.public' });
new route53.ARecord(zone, 'Alias', {
zone,
recordName: '_foo',
target: route53.RecordTarget.fromAlias(new targets.BucketWebsiteTarget(bucketWebsite))
});

// THEN
expect(stack).toHaveResource('AWS::Route53::RecordSet', {
AliasTarget: {
DNSName: { "Fn::GetAtt": [ "Bucket83908E77", "WebsiteURL"] },
HostedZoneId: "Z3AQBSTGFYJSTF"
},
});
});

test('throws if region agnostic', () => {
// GIVEN
const stack = new Stack();

const bucketWebsite = new s3.Bucket(stack, 'Bucket');

// WHEN
const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'test.public' });

// THEN
expect(() => {
new route53.ARecord(zone, 'Alias', {
zone,
recordName: '_foo',
target: route53.RecordTarget.fromAlias(new targets.BucketWebsiteTarget(bucketWebsite))
});
}).toThrow(/Cannot use an S3 record alias in region-agnostic stacks/);
});

test('throws if bucket website hosting is unavailable (cn-northwest-1)', () => {
// GIVEN
const app = new App();
const stack = new Stack(app, 'test', {env: {region: 'cn-northwest-1'}});

const bucketWebsite = new s3.Bucket(stack, 'Bucket');

// WHEN
const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'test.public' });

// THEN
expect(() => {
new route53.ARecord(zone, 'Alias', {
zone,
recordName: '_foo',
target: route53.RecordTarget.fromAlias(new targets.BucketWebsiteTarget(bucketWebsite))
});
}).toThrow(/Bucket website target is not supported/);
});
33 changes: 31 additions & 2 deletions packages/@aws-cdk/region-info/build-tools/generate-static-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,33 @@ async function main(): Promise<void> {
'sa-east-1',
]);

/**
* The hosted zone Id if using an alias record in Route53.
*
* @see https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_website_region_endpoints
*/
const ROUTE_53_BUCKET_WEBSITE_ZONE_IDS: { [region: string]: string } = {
'us-east-2': 'Z2O1EMRO9K5GLX',
'us-east-1': 'Z3AQBSTGFYJSTF',
'us-west-1': 'Z2F56UZL2M1ACD',
'us-west-2': 'Z3BJ6K6RIION7M',
'ap-east-1': 'ZNB98KWMFR0R6',
'ap-south-1': 'Z11RGJOFQNVJUP',
'ap-northeast-3': 'Z2YQB5RD63NC85',
'ap-northeast-2': 'Z3W03O7B5YMIYP',
'ap-southeast-1': 'Z3O0J2DXBE1FTB',
'ap-southeast-2': 'Z1WCIGYICN2BYD',
'ap-northeast-1': 'Z2M4EHUR26P7ZW',
'ca-central-1': 'Z1QDHH18159H29',
'eu-central-1': 'Z21DNDUVLTQW6Q',
'eu-west-1': 'Z1BKCTXD74EZPE',
'eu-west-2': 'Z3GKZC51ZF0DB4',
'eu-west-3': 'Z3R1K369G5AVDG',
'eu-north-1': 'Z3BAZG2TWCNX0D',
'sa-east-1': 'Z7KQH4QJS55SO',
'me-south-1': 'Z1MPMWCPA7YB62',
};

for (const region of AWS_REGIONS) {
const partition = region.startsWith('cn-') ? 'aws-cn' : 'aws';
registerFact(region, 'PARTITION', partition);
Expand All @@ -65,8 +92,10 @@ async function main(): Promise<void> {
registerFact(region, 'CDK_METADATA_RESOURCE_AVAILABLE', AWS_CDK_METADATA.has(region) ? 'YES' : 'NO');

registerFact(region, 'S3_STATIC_WEBSITE_ENDPOINT', AWS_OLDER_REGIONS.has(region)
? `s3-website-${region}.${domainSuffix}`
: `s3-website.${region}.${domainSuffix}`);
? `s3-website-${region}.${domainSuffix}`
: `s3-website.${region}.${domainSuffix}`);

registerFact(region, 'S3_STATIC_WEBSITE_ZONE_53_HOSTED_ZONE_ID', ROUTE_53_BUCKET_WEBSITE_ZONE_IDS[region] || '');

for (const service of AWS_SERVICES) {
registerFact(region, ['servicePrincipal', service], Default.servicePrincipal(service, region, domainSuffix));
Expand Down
5 changes: 5 additions & 0 deletions packages/@aws-cdk/region-info/lib/fact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ export class FactName {
*/
public static readonly S3_STATIC_WEBSITE_ENDPOINT = 's3-static-website:endpoint';

/**
* The endpoint used for aliasing S3 static websites in Route 53
*/
public static readonly S3_STATIC_WEBSITE_ZONE_53_HOSTED_ZONE_ID = 's3-static-website:route-53-hosted-zone-id';

/**
* The name of the regional service principal for a given service.
*
Expand Down
7 changes: 7 additions & 0 deletions packages/@aws-cdk/region-info/lib/region-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ export class RegionInfo {
return Fact.find(this.name, FactName.S3_STATIC_WEBSITE_ENDPOINT);
}

/**
* The hosted zone ID used by Route 53 to alias a S3 static website in this region (e.g: Z2O1EMRO9K5GLX)
*/
public get s3StaticWebsiteHostedZoneId(): string | undefined {
return Fact.find(this.name, FactName.S3_STATIC_WEBSITE_ZONE_53_HOSTED_ZONE_ID);
}

/**
* The name of the service principal for a given service in this region.
* @param service the service name (e.g: s3.amazonaws.com)
Expand Down

0 comments on commit bccc11f

Please sign in to comment.