From 33de062302f0ab74a425b4d1d163dc935e51db8c Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Fri, 26 Jun 2020 13:31:57 +0100 Subject: [PATCH 01/15] Initial "hello world" working example Enables creating a basic, non-customizable distribution from an S3 bucket. Example: new Distribution(this, 'dist', { origin: Origin.fromBucket(websiteBucket), }); --- .../aws-cloudfront/lib/distribution.ts | 125 +++++++++++++++++- packages/@aws-cdk/aws-cloudfront/lib/index.ts | 1 + .../@aws-cdk/aws-cloudfront/lib/origin.ts | 88 ++++++++++++ .../aws-cloudfront/lib/web_distribution.ts | 2 - packages/@aws-cdk/aws-cloudfront/package.json | 2 + 5 files changed, 213 insertions(+), 5 deletions(-) create mode 100644 packages/@aws-cdk/aws-cloudfront/lib/origin.ts diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index 9b124dd37d722..aa1ab3611b0ad 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -1,14 +1,133 @@ +import * as s3 from '@aws-cdk/aws-s3'; +import { Construct, IConstruct, Lazy } from '@aws-cdk/core'; +import { CfnDistribution } from './cloudfront.generated'; +import { IOrigin, Origin } from './origin'; +import { ViewerProtocolPolicy } from './web_distribution'; + /** * Interface for CloudFront distributions + * + * Note: IDistribution should extend IResource, not IConstruct, but this breaks backwards compatibility + * with the CloudFrontWebDistribution. + */ +export interface IDistribution extends IConstruct { + /** + * The domain name of the Distribution, such as d111111abcdef8.cloudfront.net. + * + * @attribute + */ + readonly domainName: string; + + /** + * The distribution ID for this distribution. + * + * @attribute + */ + readonly distributionId: string; +} + +// TODO - Do we need a BaseDistribution? + +/** + * Properties for a Distribution + */ +export interface DistributionProps { + /** + * The primary origin for the distribution. + */ + readonly origin: IOrigin; +} + +/** + * Attributes used to import a Distribution. */ -export interface IDistribution { +export interface DistributionAttributes { /** - * The domain name of the distribution + * The generated domain name of the Distribution, such as d111111abcdef8.cloudfront.net. + * + * @attribute */ readonly domainName: string; /** * The distribution ID for this distribution. + * + * @attribute */ readonly distributionId: string; -} \ No newline at end of file +} + +/** + * A CloudFront distribution with associated origin(s) and caching behavior(s). + */ +export class Distribution extends Construct implements IDistribution { + + /** + * Creates a Distribution construct that represents an external (imported) distribution. + */ + public static fromDistributionAttributes(scope: Construct, id: string, attrs: DistributionAttributes): IDistribution { + return new class extends Construct implements IDistribution { + public readonly domainName: string; + public readonly distributionId: string; + + constructor() { + super(scope, id); + this.domainName = attrs.domainName; + this.distributionId = attrs.distributionId; + } + }(); + } + + /** + * Creates a Distribution for an S3 Bucket, where the bucket has not been configured for website hosting. + * + * This creates a single-origin distribution with a single default behavior. + */ + public static forBucket(scope: Construct, id: string, bucket: s3.IBucket): Distribution { + return new Distribution(scope, id, { + origin: Origin.fromBucket(bucket), + }); + } + + /** + * Creates a Distribution for an S3 bucket, where the bucket has been configured for website hosting. + * + * This creates a single-origin distribution with a single default behavior. + */ + public static forWebsiteBucket(scope: Construct, id: string, bucket: s3.IBucket): Distribution { + return new Distribution(scope, id, { + origin: Origin.fromWebsiteBucket(bucket), + }); + } + + /** + * Default origin of the distribution. + */ + public readonly origin: IOrigin; + public readonly domainName: string; + public readonly distributionId: string; + + constructor(scope: Construct, id: string, props: DistributionProps) { + super(scope, id); + + this.origin = props.origin; + + const distribution = new CfnDistribution(this, 'CFDistribution', { distributionConfig: { + enabled: true, + origins: Lazy.anyValue({ produce: () => this.renderOrigins() }), + defaultCacheBehavior: { + forwardedValues: { queryString: false }, + viewerProtocolPolicy: ViewerProtocolPolicy.ALLOW_ALL, + targetOriginId: this.origin.id, + }, + } }); + + this.domainName = distribution.attrDomainName; + this.distributionId = distribution.ref; + } + + private renderOrigins(): CfnDistribution.OriginProperty[] { + return [this.origin.renderOrigin()]; + } + +} diff --git a/packages/@aws-cdk/aws-cloudfront/lib/index.ts b/packages/@aws-cdk/aws-cloudfront/lib/index.ts index 85b081a3f9e9a..bf106211657c9 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/index.ts @@ -1,5 +1,6 @@ export * from './distribution'; export * from './web_distribution'; +export * from './origin'; export * from './origin_access_identity'; // AWS::CloudFront CloudFormation Resources: diff --git a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts new file mode 100644 index 0000000000000..2362058667f46 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts @@ -0,0 +1,88 @@ +import { IBucket } from '@aws-cdk/aws-s3'; +import { Token } from '@aws-cdk/core'; +import { CfnDistribution } from './cloudfront.generated'; + +/** + * Represents a CloudFront Origin and its behaviors. + */ +export interface IOrigin { + /** + * A unique identifier for the origin. This value must be unique within the distribution. + */ + readonly id: string; + + /** + * The domain name for the origin. + */ + readonly domainName: string; + + /** + * Creates and returns the CloudFormation representation of this origin. + */ + renderOrigin(): CfnDistribution.OriginProperty; +} + +/** + * Properties to be used to create an Origin. Prefer to use one of the Origin.from* factory methods rather than + * instantiating an Origin directly from these properties. + */ +export interface OriginProps { + /** + * A unique identifier for the origin. This value must be unique within the distribution. + * + * @default - Assigned automatically. + */ + readonly id?: string; + + /** + * The domain name of the Amazon S3 bucket or HTTP server origin. + */ + readonly domainName: string; +} + +/** + * Represents a distribution origin, that describes the Amazon S3 bucket, HTTP server (for example, a web server), + * Amazon MediaStore, or other server from which CloudFront gets your files. + */ +export class Origin implements IOrigin { + + /** + * Creates a pre-configured origin for a S3 bucket. + * If this bucket has been configured for static website hosting, then `fromWebsiteBucket` should be used instead. + * + * An Origin Access Identity will be created and granted read access to the bucket, unless **TODO**. + * + * @param bucket the bucket to act as an origin. + */ + public static fromBucket(bucket: IBucket): Origin { + return new Origin({ domainName: bucket.bucketRegionalDomainName, id: bucket.bucketName }); + } + + /** + * Creates a pre-configured origin for a S3 bucket, where the bucket has been configured for website hosting. + * + * An Origin Access Identity will be created and granted read access to the bucket, unless **TODO**. + * + * @param bucket the bucket to act as an origin. + */ + public static fromWebsiteBucket(bucket: IBucket): Origin { + return new Origin({ domainName: bucket.bucketWebsiteDomainName, id: bucket.bucketName }); + } + + public readonly domainName: string; + public readonly id: string; + + constructor(props: OriginProps) { + this.domainName = props.domainName; + this.id = props.id || Token.asString(undefined); + } + + public renderOrigin(): CfnDistribution.OriginProperty { + return { + domainName: this.domainName, + id: this.id, + s3OriginConfig: {}, + }; + } + +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts index f6c9c0fe89019..7615efd9df120 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts @@ -687,8 +687,6 @@ interface BehaviorWithOrigin extends Behavior { * This will create a CloudFront distribution that uses your S3Bucket as it's origin. * * You can customize the distribution using additional properties from the CloudFrontWebDistributionProps interface. - * - * */ export class CloudFrontWebDistribution extends cdk.Construct implements IDistribution { /** diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index 19b3be311679b..ada0c21957561 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -97,6 +97,8 @@ "maturity": "experimental", "awslint": { "exclude": [ + "props-physical-name:@aws-cdk/aws-cloudfront.Distribution", + "props-physical-name:@aws-cdk/aws-cloudfront.DistributionProps", "props-physical-name:@aws-cdk/aws-cloudfront.OriginAccessIdentityProps", "docs-public-apis:@aws-cdk/aws-cloudfront.OriginProtocolPolicy", "docs-public-apis:@aws-cdk/aws-cloudfront.ViewerProtocolPolicy.ALLOW_ALL", From 5379655301b11ff671d3962f9291694b9a98be83 Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Wed, 8 Jul 2020 14:05:19 +0100 Subject: [PATCH 02/15] Origin access identifies, S3/Custom origin props and classes, and certs --- .../aws-cloudfront/lib/distribution.ts | 36 +++++- .../@aws-cdk/aws-cloudfront/lib/origin.ts | 115 ++++++++++++++++-- .../aws-cloudfront/test/distribution.test.ts | 56 +++++++++ .../aws-cloudfront/test/origin.test.ts | 70 +++++++++++ 4 files changed, 260 insertions(+), 17 deletions(-) create mode 100644 packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts create mode 100644 packages/@aws-cdk/aws-cloudfront/test/origin.test.ts diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index aa1ab3611b0ad..5f1d8e3ee7f1b 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -1,5 +1,6 @@ +import * as acm from '@aws-cdk/aws-certificatemanager'; import * as s3 from '@aws-cdk/aws-s3'; -import { Construct, IConstruct, Lazy } from '@aws-cdk/core'; +import { Construct, IConstruct, Lazy, Stack, Token } from '@aws-cdk/core'; import { CfnDistribution } from './cloudfront.generated'; import { IOrigin, Origin } from './origin'; import { ViewerProtocolPolicy } from './web_distribution'; @@ -36,6 +37,13 @@ export interface DistributionProps { * The primary origin for the distribution. */ readonly origin: IOrigin; + + /** + * A certificate to associate with the distribution. The certificate must be located in N. Virginia (us-east-1). + * + * @default the CloudFront wildcard certificate (*.cloudfront.net) will be used. + */ + readonly certificate?: acm.ICertificate; } /** @@ -85,7 +93,7 @@ export class Distribution extends Construct implements IDistribution { */ public static forBucket(scope: Construct, id: string, bucket: s3.IBucket): Distribution { return new Distribution(scope, id, { - origin: Origin.fromBucket(bucket), + origin: Origin.fromBucket(scope, 'SingleOriginBucket', bucket), }); } @@ -96,21 +104,34 @@ export class Distribution extends Construct implements IDistribution { */ public static forWebsiteBucket(scope: Construct, id: string, bucket: s3.IBucket): Distribution { return new Distribution(scope, id, { - origin: Origin.fromWebsiteBucket(bucket), + origin: Origin.fromWebsiteBucket(scope, 'SingleOriginWebsiteBucket', bucket), }); } + public readonly domainName: string; + public readonly distributionId: string; + /** * Default origin of the distribution. */ public readonly origin: IOrigin; - public readonly domainName: string; - public readonly distributionId: string; + /** + * Certificate associated with the distribution, if any. + */ + public readonly certificate?: acm.ICertificate; constructor(scope: Construct, id: string, props: DistributionProps) { super(scope, id); + if (props.certificate) { + const certificateRegion = Stack.of(this).parseArn(props.certificate.certificateArn).region; + if (!Token.isUnresolved(certificateRegion) && certificateRegion !== 'us-east-1') { + throw new Error('Distribution certificates must be in the us-east-1 region.'); + } + } + this.origin = props.origin; + this.certificate = props.certificate; const distribution = new CfnDistribution(this, 'CFDistribution', { distributionConfig: { enabled: true, @@ -120,6 +141,7 @@ export class Distribution extends Construct implements IDistribution { viewerProtocolPolicy: ViewerProtocolPolicy.ALLOW_ALL, targetOriginId: this.origin.id, }, + viewerCertificate: this.renderViewerCertificate(), } }); this.domainName = distribution.attrDomainName; @@ -130,4 +152,8 @@ export class Distribution extends Construct implements IDistribution { return [this.origin.renderOrigin()]; } + private renderViewerCertificate(): CfnDistribution.ViewerCertificateProperty | undefined { + return this.certificate ? { acmCertificateArn: this.certificate.certificateArn } : undefined; + } + } diff --git a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts index 2362058667f46..eb90e47605711 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts @@ -1,11 +1,13 @@ import { IBucket } from '@aws-cdk/aws-s3'; -import { Token } from '@aws-cdk/core'; +import { Construct, IConstruct } from '@aws-cdk/core'; import { CfnDistribution } from './cloudfront.generated'; +import { OriginAccessIdentity } from './origin_access_identity'; +import { OriginProtocolPolicy } from './web_distribution'; /** * Represents a CloudFront Origin and its behaviors. */ -export interface IOrigin { +export interface IOrigin extends IConstruct { /** * A unique identifier for the origin. This value must be unique within the distribution. */ @@ -44,7 +46,7 @@ export interface OriginProps { * Represents a distribution origin, that describes the Amazon S3 bucket, HTTP server (for example, a web server), * Amazon MediaStore, or other server from which CloudFront gets your files. */ -export class Origin implements IOrigin { +export abstract class Origin extends Construct implements IOrigin { /** * Creates a pre-configured origin for a S3 bucket. @@ -54,35 +56,124 @@ export class Origin implements IOrigin { * * @param bucket the bucket to act as an origin. */ - public static fromBucket(bucket: IBucket): Origin { - return new Origin({ domainName: bucket.bucketRegionalDomainName, id: bucket.bucketName }); + public static fromBucket(scope: Construct, id: string, bucket: IBucket): Origin { + return new S3Origin(scope, id, { + domainName: bucket.bucketRegionalDomainName, + id, + bucket, + }); } /** * Creates a pre-configured origin for a S3 bucket, where the bucket has been configured for website hosting. * - * An Origin Access Identity will be created and granted read access to the bucket, unless **TODO**. - * * @param bucket the bucket to act as an origin. */ - public static fromWebsiteBucket(bucket: IBucket): Origin { - return new Origin({ domainName: bucket.bucketWebsiteDomainName, id: bucket.bucketName }); + public static fromWebsiteBucket(scope: Construct, id: string, bucket: IBucket): Origin { + return new HttpOrigin(scope, id, { + domainName: bucket.bucketWebsiteDomainName, + id, + protocolPolicy: OriginProtocolPolicy.HTTP_ONLY, // S3 only supports HTTP for website buckets + }); } public readonly domainName: string; public readonly id: string; - constructor(props: OriginProps) { + constructor(scope: Construct, id: string, props: OriginProps) { + super(scope, id); this.domainName = props.domainName; - this.id = props.id || Token.asString(undefined); + this.id = props.id || id; } public renderOrigin(): CfnDistribution.OriginProperty { + const s3OriginConfig = this.renderS3OriginConfig(); + const customOriginConfig = this.renderCustomOriginConfig(); + + if (!s3OriginConfig && !customOriginConfig) { + throw new Error('Subclass must override and provide either s3OriginConfig or customOriginConfig'); + } + return { domainName: this.domainName, id: this.id, - s3OriginConfig: {}, + s3OriginConfig, + customOriginConfig, }; } + // Overridden by sub-classes to provide S3 origin config. + protected renderS3OriginConfig(): CfnDistribution.S3OriginConfigProperty | undefined { + return undefined; + } + + // Overridden by sub-classes to provide custom origin config. + protected renderCustomOriginConfig(): CfnDistribution.CustomOriginConfigProperty | undefined { + return undefined; + } + +} + +/** + * Properties for an Origin backed by an S3 bucket + */ +export interface S3OriginProps extends OriginProps { + /** + * The bucket to use as an origin. + */ + readonly bucket: IBucket; +} + +/** + * An Origin specific to a S3 bucket (not configured for website hosting). + * + * Contains additional logic around bucket permissions and origin access identities. + */ +export class S3Origin extends Origin { + private readonly originAccessIdentity: OriginAccessIdentity; + + constructor(scope: Construct, id: string, props: S3OriginProps) { + super(scope, id, props); + + this.originAccessIdentity = new OriginAccessIdentity(scope, 'S3OriginIdentity'); + props.bucket.grantRead(this.originAccessIdentity); + } + + protected renderS3OriginConfig(): CfnDistribution.S3OriginConfigProperty | undefined { + return { originAccessIdentity: `origin-access-identity/cloudfront/${this.originAccessIdentity.originAccessIdentityName}` }; + } +} + +/** + * Properties for an Origin backed by an S3 website-configured bucket, load balancer, or custom HTTP server. + */ +export interface HttpOriginProps extends OriginProps { + /** + * Specifies the protocol (HTTP or HTTPS) that CloudFront uses to connect to the origin. + * + * @default OriginProtocolPolicy.HTTPS_ONLY + */ + readonly protocolPolicy?: OriginProtocolPolicy; +} + +/** + * An Origin specific to a S3 bucket (not configured for website hosting). + * + * Contains additional logic around bucket permissions and origin access identities. + */ +export class HttpOrigin extends Origin { + + private readonly protocolPolicy?: OriginProtocolPolicy; + + constructor(scope: Construct, id: string, props: HttpOriginProps) { + super(scope, id, props); + + this.protocolPolicy = props.protocolPolicy; + } + + protected renderCustomOriginConfig(): CfnDistribution.CustomOriginConfigProperty | undefined { + return { + originProtocolPolicy: this.protocolPolicy ?? OriginProtocolPolicy.HTTPS_ONLY, + }; + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts new file mode 100644 index 0000000000000..9b771625dee42 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts @@ -0,0 +1,56 @@ +import '@aws-cdk/assert/jest'; +import * as acm from '@aws-cdk/aws-certificatemanager'; +import * as s3 from '@aws-cdk/aws-s3'; +import { App, Stack } from '@aws-cdk/core'; +import { Distribution, Origin } from '../lib'; + +let app: App; +let stack: Stack; + +beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { + env: { account: '1234', region: 'testregion' }, + }); +}); + +test('minimal example renders correctly', () => { + const origin = Origin.fromBucket(stack, 'MyOrigin', new s3.Bucket(stack, 'Bucket')); + new Distribution(stack, 'MyDist', { origin }); + + expect(stack).toHaveResource('AWS::CloudFront::Distribution', { + DistributionConfig: { + DefaultCacheBehavior: { + ForwardedValues: { QueryString: false }, + TargetOriginId: 'MyOrigin', + ViewerProtocolPolicy: 'allow-all', + }, + Enabled: true, + Origins: [{ + DomainName: { 'Fn::GetAtt': [ 'Bucket83908E77', 'RegionalDomainName' ] }, + Id: 'MyOrigin', + S3OriginConfig: { + OriginAccessIdentity: { 'Fn::Join': [ '', + [ 'origin-access-identity/cloudfront/', { Ref: 'S3OriginIdentityBB010E4C' } ], + ]}, + }, + }], + }, + }); +}); + +describe('certificates', () => { + + test('should fail if using an imported certificate from outside of us-east-1', () => { + const origin = Origin.fromBucket(stack, 'Origin', new s3.Bucket(stack, 'Bucket')); + const certificate = acm.Certificate.fromCertificateArn(stack, 'Cert', 'arn:aws:acm:eu-west-1:123456789012:certificate/12345678-1234-1234-1234-123456789012'); + + expect(() => { + new Distribution(stack, 'Dist', { + origin, + certificate, + }); + }).toThrow(/Distribution certificates must be in the us-east-1 region/); + }); + +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts new file mode 100644 index 0000000000000..f17f501da10d3 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts @@ -0,0 +1,70 @@ +import '@aws-cdk/assert/jest'; +import * as s3 from '@aws-cdk/aws-s3'; +import { App, Stack } from '@aws-cdk/core'; +import { Distribution, Origin } from '../lib'; + +let app: App; +let stack: Stack; + +beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { + env: { account: '1234', region: 'testregion' }, + }); +}); + +describe('fromBucket', () => { + + test('renders all properties, including S3Origin config', () => { + const bucket = new s3.Bucket(stack, 'Bucket'); + + const origin = Origin.fromBucket(stack, 'MyOrigin', bucket); + + expect(origin.renderOrigin()).toEqual({ + id: 'MyOrigin', + domainName: bucket.bucketRegionalDomainName, + s3OriginConfig: { + originAccessIdentity: 'origin-access-identity/cloudfront/${Token[TOKEN.26]}', + }, + }); + }); + + test('creates an OriginAccessIdentity and grants read permissions on the bucket', () => { + const bucket = new s3.Bucket(stack, 'Bucket'); + + const origin = Origin.fromBucket(stack, 'Origin', bucket); + new Distribution(stack, 'Dist', { origin }); + + expect(stack).toHaveResourceLike('AWS::CloudFront::CloudFrontOriginAccessIdentity', { + CloudFrontOriginAccessIdentityConfig: { + Comment: 'Allows CloudFront to reach the bucket', + }, + }); + expect(stack).toHaveResourceLike('AWS::S3::BucketPolicy', { + PolicyDocument: { + Statement: [{ + Principal: { + CanonicalUser: { 'Fn::GetAtt': [ 'S3OriginIdentityBB010E4C', 'S3CanonicalUserId' ] }, + }, + }], + }, + }); + }); + +}); + +test('fromWebsiteBucket renders all properties, including custom origin config', () => { + const bucket = new s3.Bucket(stack, 'Bucket', { + websiteIndexDocument: 'index.html', + }); + + const origin = Origin.fromWebsiteBucket(stack, 'MyOrigin', bucket); + + expect(origin.renderOrigin()).toEqual({ + id: 'MyOrigin', + domainName: bucket.bucketWebsiteDomainName, + customOriginConfig: { + originProtocolPolicy: 'http-only', + }, + }); +}); \ No newline at end of file From 32796952c23ae8856c37fd5d5286e5d673f6bd38 Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Thu, 9 Jul 2020 11:10:23 +0100 Subject: [PATCH 03/15] Convert IDistribution, Distribution, and CloudFrontWebDistribution to Resources (instead of Constructs) --- .../@aws-cdk/aws-cloudfront/lib/distribution.ts | 11 ++++------- .../aws-cloudfront/lib/web_distribution.ts | 4 +++- packages/@aws-cdk/aws-cloudfront/package.json | 2 ++ .../aws-cloudfront/test/distribution.test.ts | 16 ++++++++++++++++ 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index 5f1d8e3ee7f1b..65afb3a145109 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -1,17 +1,14 @@ import * as acm from '@aws-cdk/aws-certificatemanager'; import * as s3 from '@aws-cdk/aws-s3'; -import { Construct, IConstruct, Lazy, Stack, Token } from '@aws-cdk/core'; +import { Construct, IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; import { CfnDistribution } from './cloudfront.generated'; import { IOrigin, Origin } from './origin'; import { ViewerProtocolPolicy } from './web_distribution'; /** * Interface for CloudFront distributions - * - * Note: IDistribution should extend IResource, not IConstruct, but this breaks backwards compatibility - * with the CloudFrontWebDistribution. */ -export interface IDistribution extends IConstruct { +export interface IDistribution extends IResource { /** * The domain name of the Distribution, such as d111111abcdef8.cloudfront.net. * @@ -68,13 +65,13 @@ export interface DistributionAttributes { /** * A CloudFront distribution with associated origin(s) and caching behavior(s). */ -export class Distribution extends Construct implements IDistribution { +export class Distribution extends Resource implements IDistribution { /** * Creates a Distribution construct that represents an external (imported) distribution. */ public static fromDistributionAttributes(scope: Construct, id: string, attrs: DistributionAttributes): IDistribution { - return new class extends Construct implements IDistribution { + return new class extends Resource implements IDistribution { public readonly domainName: string; public readonly distributionId: string; diff --git a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts index 7615efd9df120..bca7d53db5db7 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts @@ -687,8 +687,10 @@ interface BehaviorWithOrigin extends Behavior { * This will create a CloudFront distribution that uses your S3Bucket as it's origin. * * You can customize the distribution using additional properties from the CloudFrontWebDistributionProps interface. + * + * @resource AWS::CloudFront::Distribution */ -export class CloudFrontWebDistribution extends cdk.Construct implements IDistribution { +export class CloudFrontWebDistribution extends cdk.Resource implements IDistribution { /** * The logging bucket for this CloudFront distribution. * If logging is not enabled for this distribution - this property will be undefined. diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index ada0c21957561..b243c416c46db 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -99,6 +99,8 @@ "exclude": [ "props-physical-name:@aws-cdk/aws-cloudfront.Distribution", "props-physical-name:@aws-cdk/aws-cloudfront.DistributionProps", + "props-physical-name:@aws-cdk/aws-cloudfront.CloudFrontWebDistribution", + "props-physical-name:@aws-cdk/aws-cloudfront.CloudFrontWebDistributionProps", "props-physical-name:@aws-cdk/aws-cloudfront.OriginAccessIdentityProps", "docs-public-apis:@aws-cdk/aws-cloudfront.OriginProtocolPolicy", "docs-public-apis:@aws-cdk/aws-cloudfront.ViewerProtocolPolicy.ALLOW_ALL", diff --git a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts index 9b771625dee42..01207f39b1738 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts @@ -53,4 +53,20 @@ describe('certificates', () => { }).toThrow(/Distribution certificates must be in the us-east-1 region/); }); + test('adding a certificate renders the correct ViewerCertificate property', () => { + const certificate = acm.Certificate.fromCertificateArn(stack, 'Cert', 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012'); + + new Distribution(stack, 'Dist', { + origin: Origin.fromBucket(stack, 'Origin', new s3.Bucket(stack, 'Bucket')), + certificate, + }); + + expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + DistributionConfig: { + ViewerCertificate: { + AcmCertificateArn: 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012', + }, + }, + }); + }); }); \ No newline at end of file From aab812200db8059739e4a87dca0ac882ef133df8 Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Thu, 9 Jul 2020 13:53:16 +0100 Subject: [PATCH 04/15] Removing IOrigin, adding back-reference to Distribution --- .../aws-cloudfront/lib/distribution.ts | 10 +-- .../@aws-cdk/aws-cloudfront/lib/origin.ts | 65 +++++++++++-------- .../aws-cloudfront/test/origin.test.ts | 4 +- 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index 65afb3a145109..e51919faed145 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -2,7 +2,7 @@ import * as acm from '@aws-cdk/aws-certificatemanager'; import * as s3 from '@aws-cdk/aws-s3'; import { Construct, IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; import { CfnDistribution } from './cloudfront.generated'; -import { IOrigin, Origin } from './origin'; +import { Origin } from './origin'; import { ViewerProtocolPolicy } from './web_distribution'; /** @@ -33,7 +33,7 @@ export interface DistributionProps { /** * The primary origin for the distribution. */ - readonly origin: IOrigin; + readonly origin: Origin; /** * A certificate to associate with the distribution. The certificate must be located in N. Virginia (us-east-1). @@ -111,7 +111,7 @@ export class Distribution extends Resource implements IDistribution { /** * Default origin of the distribution. */ - public readonly origin: IOrigin; + public readonly origin: Origin; /** * Certificate associated with the distribution, if any. */ @@ -130,6 +130,8 @@ export class Distribution extends Resource implements IDistribution { this.origin = props.origin; this.certificate = props.certificate; + this.origin._attachDistribution(this); + const distribution = new CfnDistribution(this, 'CFDistribution', { distributionConfig: { enabled: true, origins: Lazy.anyValue({ produce: () => this.renderOrigins() }), @@ -146,7 +148,7 @@ export class Distribution extends Resource implements IDistribution { } private renderOrigins(): CfnDistribution.OriginProperty[] { - return [this.origin.renderOrigin()]; + return [this.origin._renderOrigin()]; } private renderViewerCertificate(): CfnDistribution.ViewerCertificateProperty | undefined { diff --git a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts index eb90e47605711..714b02542b080 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts @@ -1,29 +1,10 @@ import { IBucket } from '@aws-cdk/aws-s3'; -import { Construct, IConstruct } from '@aws-cdk/core'; +import { Construct } from '@aws-cdk/core'; import { CfnDistribution } from './cloudfront.generated'; +import { Distribution } from './distribution'; import { OriginAccessIdentity } from './origin_access_identity'; import { OriginProtocolPolicy } from './web_distribution'; -/** - * Represents a CloudFront Origin and its behaviors. - */ -export interface IOrigin extends IConstruct { - /** - * A unique identifier for the origin. This value must be unique within the distribution. - */ - readonly id: string; - - /** - * The domain name for the origin. - */ - readonly domainName: string; - - /** - * Creates and returns the CloudFormation representation of this origin. - */ - renderOrigin(): CfnDistribution.OriginProperty; -} - /** * Properties to be used to create an Origin. Prefer to use one of the Origin.from* factory methods rather than * instantiating an Origin directly from these properties. @@ -40,13 +21,21 @@ export interface OriginProps { * The domain name of the Amazon S3 bucket or HTTP server origin. */ readonly domainName: string; + + /** + * The Distribution this Origin will be associated with. + * [disable-awslint:ref-via-interface] + * + * @default - This will be set when the Origin is added to the Distribution. + */ + readonly distribution?: Distribution; } /** * Represents a distribution origin, that describes the Amazon S3 bucket, HTTP server (for example, a web server), * Amazon MediaStore, or other server from which CloudFront gets your files. */ -export abstract class Origin extends Construct implements IOrigin { +export abstract class Origin extends Construct { /** * Creates a pre-configured origin for a S3 bucket. @@ -77,16 +66,32 @@ export abstract class Origin extends Construct implements IOrigin { }); } + /** + * The Distribution this Origin is associated with. + */ + public distribution?: Distribution; + /** + * The domain name of the origin. + */ public readonly domainName: string; + /** + * A unique identifier for the origin. This value must be unique within the distribution. + */ public readonly id: string; constructor(scope: Construct, id: string, props: OriginProps) { super(scope, id); + this.distribution = props.distribution; this.domainName = props.domainName; this.id = props.id || id; } - public renderOrigin(): CfnDistribution.OriginProperty { + /** + * Creates and returns the CloudFormation representation of this origin. + * + * @internal + */ + public _renderOrigin(): CfnDistribution.OriginProperty { const s3OriginConfig = this.renderS3OriginConfig(); const customOriginConfig = this.renderCustomOriginConfig(); @@ -102,6 +107,16 @@ export abstract class Origin extends Construct implements IOrigin { }; } + /** + * Internal API used by `Distribution` to keep an inventory of origins for the + * purposes of keeping track of behaviors as they're created and added. + * + * @internal + */ + public _attachDistribution(distribution: Distribution) { + this.distribution = distribution; + } + // Overridden by sub-classes to provide S3 origin config. protected renderS3OriginConfig(): CfnDistribution.S3OriginConfigProperty | undefined { return undefined; @@ -157,9 +172,7 @@ export interface HttpOriginProps extends OriginProps { } /** - * An Origin specific to a S3 bucket (not configured for website hosting). - * - * Contains additional logic around bucket permissions and origin access identities. + * An Origin for an HTTP server or S3 bucket configured for website hosting. */ export class HttpOrigin extends Origin { diff --git a/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts index f17f501da10d3..d295656a1a2d7 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts @@ -20,7 +20,7 @@ describe('fromBucket', () => { const origin = Origin.fromBucket(stack, 'MyOrigin', bucket); - expect(origin.renderOrigin()).toEqual({ + expect(origin._renderOrigin()).toEqual({ id: 'MyOrigin', domainName: bucket.bucketRegionalDomainName, s3OriginConfig: { @@ -60,7 +60,7 @@ test('fromWebsiteBucket renders all properties, including custom origin config', const origin = Origin.fromWebsiteBucket(stack, 'MyOrigin', bucket); - expect(origin.renderOrigin()).toEqual({ + expect(origin._renderOrigin()).toEqual({ id: 'MyOrigin', domainName: bucket.bucketWebsiteDomainName, customOriginConfig: { From 253f7978791cbfe129c72bcca0abc7b1313d6849 Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Thu, 9 Jul 2020 16:32:56 +0100 Subject: [PATCH 05/15] Initial implementation of Behaviors --- .../@aws-cdk/aws-cloudfront/lib/behavior.ts | 90 +++++++++++++++++++ .../aws-cloudfront/lib/distribution.ts | 45 ++++++++-- packages/@aws-cdk/aws-cloudfront/lib/index.ts | 1 + .../@aws-cdk/aws-cloudfront/lib/origin.ts | 48 +++++++++- 4 files changed, 171 insertions(+), 13 deletions(-) create mode 100644 packages/@aws-cdk/aws-cloudfront/lib/behavior.ts diff --git a/packages/@aws-cdk/aws-cloudfront/lib/behavior.ts b/packages/@aws-cdk/aws-cloudfront/lib/behavior.ts new file mode 100644 index 0000000000000..a3e3972b2a24a --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/lib/behavior.ts @@ -0,0 +1,90 @@ +import { CfnDistribution } from './cloudfront.generated'; +import { Origin } from './origin'; +import { ViewerProtocolPolicy } from './web_distribution'; + +/** + * The HTTP methods that the Behavior will accept requests on. + */ +export class AllowedMethods { + /** HEAD and GET */ + public static readonly ALLOW_GET_HEAD = new AllowedMethods(['GET' ,'HEAD']); + /** HEAD, GET, and OPTIONS */ + public static readonly ALLOW_GET_HEAD_OPTIONS = new AllowedMethods(['GET', 'HEAD', 'OPTIONS']); + /** All supported HTTP methods */ + public static readonly ALLOW_ALL = new AllowedMethods(['GET', 'HEAD', 'OPTIONS', 'PUT', 'PATCH', 'POST', 'DELETE']); + + /** HTTP methods supported */ + public readonly methods: string[]; + + private constructor(methods: string[]) { this.methods = methods; } +} + +/** + * Options for adding a behavior to an Origin. + */ +export interface CacheBehaviorOptions { + /** + * HTTP methods to allow for this behavior. + * + * @default GET and HEAD + */ + readonly allowedMethods?: AllowedMethods; +} + +/** + * Properties for specifying custom behaviors for origins. + */ +export interface CacheBehaviorProps extends CacheBehaviorOptions { + /** + * The pattern (e.g., `images/*.jpg`) that specifies which requests to apply the behavior to. + * There must be exactly one behavior associated with each `Distribution` that has a path pattern + * of '*', which acts as the catch-all default behavior. + */ + readonly pathPattern: string; + + /** + * The origin that you want CloudFront to route requests to when they match this cache behavior. + */ + readonly origin: Origin; +} + +/** + * Allows configuring a variety of CloudFront functionality for a given URL path pattern. + * + * Note: This really should simply by called 'Behavior', but this name is already taken by the legacy + * CloudFrontWebDistribution implementation. + */ +export class CacheBehavior { + + /** + * The pattern (e.g., `images/*.jpg`) that specifies which requests to apply the behavior to. + */ + public readonly pathPattern: string; + private readonly origin: Origin; + + constructor(private readonly props: CacheBehaviorProps) { + this.origin = props.origin; + this.pathPattern = props.pathPattern; + + this.origin._attachBehavior(this); + } + + /** + * Creates and returns the CloudFormation representation of this behavior. + * This renders as a "CacheBehaviorProperty" regardless of if this is a default + * cache behavior or not, as the two are identical except that the pathPattern + * is omitted for the default cache behavior. + * + * @internal + */ + public _renderBehavior(): CfnDistribution.CacheBehaviorProperty { + return { + targetOriginId: this.origin.id, + pathPattern: this.pathPattern, + allowedMethods: this.props.allowedMethods?.methods ?? undefined, + forwardedValues: { queryString: false }, + viewerProtocolPolicy: ViewerProtocolPolicy.ALLOW_ALL, + }; + } + +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index e51919faed145..ed26bfd01136f 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -1,9 +1,9 @@ import * as acm from '@aws-cdk/aws-certificatemanager'; import * as s3 from '@aws-cdk/aws-s3'; import { Construct, IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; +import { CacheBehavior } from './behavior'; import { CfnDistribution } from './cloudfront.generated'; import { Origin } from './origin'; -import { ViewerProtocolPolicy } from './web_distribution'; /** * Interface for CloudFront distributions @@ -117,6 +117,9 @@ export class Distribution extends Resource implements IDistribution { */ public readonly certificate?: acm.ICertificate; + private defaultBehavior?: CacheBehavior; + private readonly behaviors: CacheBehavior[] = []; + constructor(scope: Construct, id: string, props: DistributionProps) { super(scope, id); @@ -135,24 +138,48 @@ export class Distribution extends Resource implements IDistribution { const distribution = new CfnDistribution(this, 'CFDistribution', { distributionConfig: { enabled: true, origins: Lazy.anyValue({ produce: () => this.renderOrigins() }), - defaultCacheBehavior: { - forwardedValues: { queryString: false }, - viewerProtocolPolicy: ViewerProtocolPolicy.ALLOW_ALL, - targetOriginId: this.origin.id, - }, - viewerCertificate: this.renderViewerCertificate(), + defaultCacheBehavior: Lazy.anyValue({ produce: () => this.renderDefaultBehavior() }), + cacheBehaviors: Lazy.anyValue({ produce: () => this.renderCacheBehaviors() }), + viewerCertificate: this.certificate ? { acmCertificateArn: this.certificate.certificateArn } : undefined, } }); this.domainName = distribution.attrDomainName; this.distributionId = distribution.ref; } + /** + * Internal API used by `Origin` to keep an inventory of behaviors associated with + * this distribution, for the sake of ordering behaviors. + * + * @internal + */ + public _attachBehavior(behavior: CacheBehavior) { + if (behavior.pathPattern === '*') { + if (this.defaultBehavior) { + throw new Error('Distributions may only have one Behavior with the default path pattern (*)'); + } + this.defaultBehavior = behavior; + } else { + this.behaviors.push(behavior); + } + } + private renderOrigins(): CfnDistribution.OriginProperty[] { return [this.origin._renderOrigin()]; } - private renderViewerCertificate(): CfnDistribution.ViewerCertificateProperty | undefined { - return this.certificate ? { acmCertificateArn: this.certificate.certificateArn } : undefined; + private renderDefaultBehavior(): CfnDistribution.DefaultCacheBehaviorProperty | undefined { + if (!this.defaultBehavior) { + this.defaultBehavior = new CacheBehavior({ + origin: this.origin, + pathPattern: '*', + }); + } + return this.defaultBehavior._renderBehavior(); + } + + private renderCacheBehaviors(): CfnDistribution.CacheBehaviorProperty[] | undefined { + return this.behaviors.length === 0 ? undefined : this.behaviors.map(b => b._renderBehavior()); } } diff --git a/packages/@aws-cdk/aws-cloudfront/lib/index.ts b/packages/@aws-cdk/aws-cloudfront/lib/index.ts index bf106211657c9..5063f0bdd218a 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/index.ts @@ -1,3 +1,4 @@ +export * from './behavior'; export * from './distribution'; export * from './web_distribution'; export * from './origin'; diff --git a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts index 714b02542b080..34b890d5a9858 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts @@ -1,5 +1,6 @@ import { IBucket } from '@aws-cdk/aws-s3'; import { Construct } from '@aws-cdk/core'; +import { CacheBehavior, CacheBehaviorOptions, CacheBehaviorProps } from './behavior'; import { CfnDistribution } from './cloudfront.generated'; import { Distribution } from './distribution'; import { OriginAccessIdentity } from './origin_access_identity'; @@ -45,12 +46,16 @@ export abstract class Origin extends Construct { * * @param bucket the bucket to act as an origin. */ - public static fromBucket(scope: Construct, id: string, bucket: IBucket): Origin { - return new S3Origin(scope, id, { + public static fromBucket(scope: Construct, id: string, bucket: IBucket, behaviorProps?: CacheBehaviorProps): Origin { + const origin = new S3Origin(scope, id, { domainName: bucket.bucketRegionalDomainName, id, bucket, }); + if (behaviorProps) { + origin.addBehavior(behaviorProps.pathPattern, behaviorProps); + } + return origin; } /** @@ -58,12 +63,16 @@ export abstract class Origin extends Construct { * * @param bucket the bucket to act as an origin. */ - public static fromWebsiteBucket(scope: Construct, id: string, bucket: IBucket): Origin { - return new HttpOrigin(scope, id, { + public static fromWebsiteBucket(scope: Construct, id: string, bucket: IBucket, behaviorProps?: CacheBehaviorProps): Origin { + const origin = new HttpOrigin(scope, id, { domainName: bucket.bucketWebsiteDomainName, id, protocolPolicy: OriginProtocolPolicy.HTTP_ONLY, // S3 only supports HTTP for website buckets }); + if (behaviorProps) { + origin.addBehavior(behaviorProps.pathPattern, behaviorProps); + } + return origin; } /** @@ -79,6 +88,8 @@ export abstract class Origin extends Construct { */ public readonly id: string; + private readonly _behaviors: CacheBehavior[] = []; + constructor(scope: Construct, id: string, props: OriginProps) { super(scope, id); this.distribution = props.distribution; @@ -86,6 +97,20 @@ export abstract class Origin extends Construct { this.id = props.id || id; } + /** + * Creates a new Behavior from the given pathPattern and options, and associates it with this origin. + * + * @param pathPattern the pattern that specifies which requests to apply the behavior to; may be '*' if this is the default behavior. + * @param options the behavior options + */ + public addBehavior(pathPattern: string, options: CacheBehaviorOptions): CacheBehavior { + return new CacheBehavior({ + origin: this, + pathPattern, + ...options, + }); + } + /** * Creates and returns the CloudFormation representation of this origin. * @@ -115,6 +140,21 @@ export abstract class Origin extends Construct { */ public _attachDistribution(distribution: Distribution) { this.distribution = distribution; + // Register all existing behaviors with the distribution. New behaviors will be attached as they are created. + this._behaviors.forEach(b => distribution._attachBehavior(b)); + } + + /** + * Internal API used by `Behavior` to keep an inventory of behaviors associated with + * this origin, for the sake of ordering behaviors based on creation order. + * + * @internal + */ + public _attachBehavior(behavior: CacheBehavior) { + this._behaviors.push(behavior); + if (this.distribution) { + this.distribution._attachBehavior(behavior); + } } // Overridden by sub-classes to provide S3 origin config. From d6eeeb5dba4a321bd6a6357bccdce8506f90e010 Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Fri, 10 Jul 2020 14:47:58 +0100 Subject: [PATCH 06/15] Refactored to make Behavior the top-level entity (over Origin) This change also extends the set of properties available on Distribution, Behavior, and Origin, and adds support for multi-origin and multi-behavior setups. --- .../@aws-cdk/aws-cloudfront/lib/behavior.ts | 48 ++-- .../aws-cloudfront/lib/distribution.ts | 164 ++++++++---- .../@aws-cdk/aws-cloudfront/lib/origin.ts | 83 +----- .../aws-cloudfront/lib/web_distribution.ts | 26 +- .../aws-cloudfront/test/behavior.test.ts | 49 ++++ .../aws-cloudfront/test/distribution.test.ts | 238 +++++++++++++++++- .../aws-cloudfront/test/origin.test.ts | 4 +- 7 files changed, 437 insertions(+), 175 deletions(-) create mode 100644 packages/@aws-cdk/aws-cloudfront/test/behavior.test.ts diff --git a/packages/@aws-cdk/aws-cloudfront/lib/behavior.ts b/packages/@aws-cdk/aws-cloudfront/lib/behavior.ts index a3e3972b2a24a..c1ed113456280 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/behavior.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/behavior.ts @@ -1,13 +1,13 @@ import { CfnDistribution } from './cloudfront.generated'; +import { ViewerProtocolPolicy } from './distribution'; import { Origin } from './origin'; -import { ViewerProtocolPolicy } from './web_distribution'; /** * The HTTP methods that the Behavior will accept requests on. */ export class AllowedMethods { /** HEAD and GET */ - public static readonly ALLOW_GET_HEAD = new AllowedMethods(['GET' ,'HEAD']); + public static readonly ALLOW_GET_HEAD = new AllowedMethods(['GET', 'HEAD']); /** HEAD, GET, and OPTIONS */ public static readonly ALLOW_GET_HEAD_OPTIONS = new AllowedMethods(['GET', 'HEAD', 'OPTIONS']); /** All supported HTTP methods */ @@ -20,15 +20,37 @@ export class AllowedMethods { } /** - * Options for adding a behavior to an Origin. + * Options for creating a new behavior. */ export interface CacheBehaviorOptions { + /** + * The origin that you want CloudFront to route requests to when they match this cache behavior. + */ + readonly origin: Origin; + /** * HTTP methods to allow for this behavior. * * @default GET and HEAD */ readonly allowedMethods?: AllowedMethods; + + /** + * Whether CloudFront will forward query strings to the origin. + * If this is set to true, CloudFront will forward all query parameters to the origin, and cache + * based on all parameters. See `forwardQueryStringCacheKeys` for a way to limit the query parameters + * CloudFront caches on. + * + * @default false + */ + readonly forwardQueryString?: boolean; + + /** + * A set of query string parameter names to use for caching if `forwardQueryString` is set to true. + * + * @default empty list + */ + readonly forwardQueryStringCacheKeys?: string[]; } /** @@ -41,11 +63,6 @@ export interface CacheBehaviorProps extends CacheBehaviorOptions { * of '*', which acts as the catch-all default behavior. */ readonly pathPattern: string; - - /** - * The origin that you want CloudFront to route requests to when they match this cache behavior. - */ - readonly origin: Origin; } /** @@ -57,16 +74,12 @@ export interface CacheBehaviorProps extends CacheBehaviorOptions { export class CacheBehavior { /** - * The pattern (e.g., `images/*.jpg`) that specifies which requests to apply the behavior to. + * Origin that this behavior will route traffic to. */ - public readonly pathPattern: string; - private readonly origin: Origin; + public readonly origin: Origin; constructor(private readonly props: CacheBehaviorProps) { this.origin = props.origin; - this.pathPattern = props.pathPattern; - - this.origin._attachBehavior(this); } /** @@ -79,10 +92,13 @@ export class CacheBehavior { */ public _renderBehavior(): CfnDistribution.CacheBehaviorProperty { return { + pathPattern: this.props.pathPattern, targetOriginId: this.origin.id, - pathPattern: this.pathPattern, allowedMethods: this.props.allowedMethods?.methods ?? undefined, - forwardedValues: { queryString: false }, + forwardedValues: { + queryString: this.props.forwardQueryString ?? false, + queryStringCacheKeys: this.props.forwardQueryStringCacheKeys, + }, viewerProtocolPolicy: ViewerProtocolPolicy.ALLOW_ALL, }; } diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index ed26bfd01136f..b8443a50d2787 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -1,7 +1,7 @@ import * as acm from '@aws-cdk/aws-certificatemanager'; import * as s3 from '@aws-cdk/aws-s3'; import { Construct, IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; -import { CacheBehavior } from './behavior'; +import { CacheBehavior, CacheBehaviorOptions } from './behavior'; import { CfnDistribution } from './cloudfront.generated'; import { Origin } from './origin'; @@ -24,16 +24,40 @@ export interface IDistribution extends IResource { readonly distributionId: string; } -// TODO - Do we need a BaseDistribution? +/** + * Attributes used to import a Distribution. + */ +export interface DistributionAttributes { + /** + * The generated domain name of the Distribution, such as d111111abcdef8.cloudfront.net. + * + * @attribute + */ + readonly domainName: string; + + /** + * The distribution ID for this distribution. + * + * @attribute + */ + readonly distributionId: string; +} /** * Properties for a Distribution */ export interface DistributionProps { /** - * The primary origin for the distribution. + * The default behavior for the distribution. + */ + readonly defaultBehavior: CacheBehaviorOptions; + + /** + * Additional behaviors for the distribution, mapped by the pathPattern that specifies which requests to apply the behavior to. + * + * @default no additional behaviors are added. */ - readonly origin: Origin; + readonly additionalBehaviors?: Record; /** * A certificate to associate with the distribution. The certificate must be located in N. Virginia (us-east-1). @@ -41,25 +65,24 @@ export interface DistributionProps { * @default the CloudFront wildcard certificate (*.cloudfront.net) will be used. */ readonly certificate?: acm.ICertificate; -} -/** - * Attributes used to import a Distribution. - */ -export interface DistributionAttributes { /** - * The generated domain name of the Distribution, such as d111111abcdef8.cloudfront.net. + * The price class that corresponds with the maximum price that you want to pay for CloudFront service. + * If you specify PriceClass_All, CloudFront responds to requests for your objects from all CloudFront edge locations. + * If you specify a price class other than PriceClass_All, CloudFront serves your objects from the CloudFront edge location + * that has the lowest latency among the edge locations in your price class. * - * @attribute + * @default PRICE_CLASS_ALL */ - readonly domainName: string; + readonly priceClass?: PriceClass; /** - * The distribution ID for this distribution. + * How CloudFront should handle requests that are not successful (e.g., PageNotFound). * - * @attribute + * + * @default - No custom error configuration. */ - readonly distributionId: string; + readonly errorConfigurations?: CfnDistribution.CustomErrorResponseProperty[]; } /** @@ -90,7 +113,7 @@ export class Distribution extends Resource implements IDistribution { */ public static forBucket(scope: Construct, id: string, bucket: s3.IBucket): Distribution { return new Distribution(scope, id, { - origin: Origin.fromBucket(scope, 'SingleOriginBucket', bucket), + defaultBehavior: { origin: Origin.fromBucket(scope, 'SingleOriginBucket', bucket) }, }); } @@ -101,24 +124,18 @@ export class Distribution extends Resource implements IDistribution { */ public static forWebsiteBucket(scope: Construct, id: string, bucket: s3.IBucket): Distribution { return new Distribution(scope, id, { - origin: Origin.fromWebsiteBucket(scope, 'SingleOriginWebsiteBucket', bucket), + defaultBehavior: { origin: Origin.fromWebsiteBucket(scope, 'SingleOriginWebsiteBucket', bucket) }, }); } public readonly domainName: string; public readonly distributionId: string; - /** - * Default origin of the distribution. - */ - public readonly origin: Origin; - /** - * Certificate associated with the distribution, if any. - */ - public readonly certificate?: acm.ICertificate; + private readonly defaultBehavior: CacheBehavior; + private readonly additionalBehaviors: CacheBehavior[] = []; - private defaultBehavior?: CacheBehavior; - private readonly behaviors: CacheBehavior[] = []; + private readonly errorConfigurations: CfnDistribution.CustomErrorResponseProperty[]; + private readonly certificate?: acm.ICertificate; constructor(scope: Construct, id: string, props: DistributionProps) { super(scope, id); @@ -130,17 +147,24 @@ export class Distribution extends Resource implements IDistribution { } } - this.origin = props.origin; - this.certificate = props.certificate; + this.defaultBehavior = new CacheBehavior({ pathPattern: '*', ...props.defaultBehavior }); + if (props.additionalBehaviors) { + Object.entries(props.additionalBehaviors).forEach(([pathPattern, behaviorOptions]) => { + this.addBehavior(pathPattern, behaviorOptions); + }); + } - this.origin._attachDistribution(this); + this.certificate = props.certificate; + this.errorConfigurations = props.errorConfigurations ?? []; const distribution = new CfnDistribution(this, 'CFDistribution', { distributionConfig: { enabled: true, origins: Lazy.anyValue({ produce: () => this.renderOrigins() }), - defaultCacheBehavior: Lazy.anyValue({ produce: () => this.renderDefaultBehavior() }), + defaultCacheBehavior: this.defaultBehavior._renderBehavior(), cacheBehaviors: Lazy.anyValue({ produce: () => this.renderCacheBehaviors() }), viewerCertificate: this.certificate ? { acmCertificateArn: this.certificate.certificateArn } : undefined, + customErrorResponses: this.renderCustomErrorResponses(), + priceClass: props.priceClass ?? undefined, } }); this.domainName = distribution.attrDomainName; @@ -148,38 +172,72 @@ export class Distribution extends Resource implements IDistribution { } /** - * Internal API used by `Origin` to keep an inventory of behaviors associated with - * this distribution, for the sake of ordering behaviors. + * Adds a new behavior to this distribution for the given pathPattern. * - * @internal + * @param pathPattern the path pattern (e.g., 'images/*') that specifies which requests to apply the behavior to. + * @param behaviorOptions the options for the behavior at this path. */ - public _attachBehavior(behavior: CacheBehavior) { - if (behavior.pathPattern === '*') { - if (this.defaultBehavior) { - throw new Error('Distributions may only have one Behavior with the default path pattern (*)'); - } - this.defaultBehavior = behavior; - } else { - this.behaviors.push(behavior); + public addBehavior(pathPattern: string, behaviorOptions: CacheBehaviorOptions) { + if (pathPattern === '*') { + throw new Error('Only the default behavior can have a path pattern of \'*\''); } + this.additionalBehaviors.push(new CacheBehavior({ pathPattern, ...behaviorOptions })); } private renderOrigins(): CfnDistribution.OriginProperty[] { - return [this.origin._renderOrigin()]; - } + const origins = new Set(); + origins.add(this.defaultBehavior.origin); + this.additionalBehaviors.forEach(behavior => origins.add(behavior.origin)); - private renderDefaultBehavior(): CfnDistribution.DefaultCacheBehaviorProperty | undefined { - if (!this.defaultBehavior) { - this.defaultBehavior = new CacheBehavior({ - origin: this.origin, - pathPattern: '*', - }); - } - return this.defaultBehavior._renderBehavior(); + const renderedOrigins: CfnDistribution.OriginProperty[] = []; + origins.forEach(origin => renderedOrigins.push(origin._renderOrigin())); + return renderedOrigins; } private renderCacheBehaviors(): CfnDistribution.CacheBehaviorProperty[] | undefined { - return this.behaviors.length === 0 ? undefined : this.behaviors.map(b => b._renderBehavior()); + if (this.additionalBehaviors.length === 0) { return undefined; } + return this.additionalBehaviors.map(behavior => behavior._renderBehavior()); } + private renderCustomErrorResponses(): CfnDistribution.CustomErrorResponseProperty[] | undefined { + if (this.errorConfigurations.length === 0) { return undefined; } + function validateCustomErrorResponse(errorResponse: CfnDistribution.CustomErrorResponseProperty) { + if (errorResponse.responsePagePath && !errorResponse.responseCode) { + throw new Error('\'responseCode\' must be provided if \'responsePagePath\' is defined'); + } + if (!errorResponse.responseCode && !errorResponse.errorCachingMinTtl) { + throw new Error('A custom error response without either a \'responseCode\' or \'errorCachingMinTtl\' is not valid.'); + } + } + this.errorConfigurations.forEach(e => validateCustomErrorResponse(e)); + return this.errorConfigurations; + } + +} + +/** + * The price class determines how many edge locations CloudFront will use for your distribution. + */ +export enum PriceClass { + PRICE_CLASS_100 = 'PriceClass_100', + PRICE_CLASS_200 = 'PriceClass_200', + PRICE_CLASS_ALL = 'PriceClass_All' +} + +/** + * How HTTPs should be handled with your distribution. + */ +export enum ViewerProtocolPolicy { + HTTPS_ONLY = 'https-only', + REDIRECT_TO_HTTPS = 'redirect-to-https', + ALLOW_ALL = 'allow-all' +} + +/** + * Defines what protocols CloudFront will use to connect to an origin. + */ +export enum OriginProtocolPolicy { + HTTP_ONLY = 'http-only', + MATCH_VIEWER = 'match-viewer', + HTTPS_ONLY = 'https-only', } diff --git a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts index 34b890d5a9858..a920e92803e6d 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts @@ -1,23 +1,14 @@ import { IBucket } from '@aws-cdk/aws-s3'; import { Construct } from '@aws-cdk/core'; -import { CacheBehavior, CacheBehaviorOptions, CacheBehaviorProps } from './behavior'; import { CfnDistribution } from './cloudfront.generated'; -import { Distribution } from './distribution'; +import { Distribution, OriginProtocolPolicy } from './distribution'; import { OriginAccessIdentity } from './origin_access_identity'; -import { OriginProtocolPolicy } from './web_distribution'; /** * Properties to be used to create an Origin. Prefer to use one of the Origin.from* factory methods rather than * instantiating an Origin directly from these properties. */ export interface OriginProps { - /** - * A unique identifier for the origin. This value must be unique within the distribution. - * - * @default - Assigned automatically. - */ - readonly id?: string; - /** * The domain name of the Amazon S3 bucket or HTTP server origin. */ @@ -42,20 +33,15 @@ export abstract class Origin extends Construct { * Creates a pre-configured origin for a S3 bucket. * If this bucket has been configured for static website hosting, then `fromWebsiteBucket` should be used instead. * - * An Origin Access Identity will be created and granted read access to the bucket, unless **TODO**. + * An Origin Access Identity will be created and granted read access to the bucket. * * @param bucket the bucket to act as an origin. */ - public static fromBucket(scope: Construct, id: string, bucket: IBucket, behaviorProps?: CacheBehaviorProps): Origin { - const origin = new S3Origin(scope, id, { + public static fromBucket(scope: Construct, id: string, bucket: IBucket): Origin { + return new S3Origin(scope, id, { domainName: bucket.bucketRegionalDomainName, - id, bucket, }); - if (behaviorProps) { - origin.addBehavior(behaviorProps.pathPattern, behaviorProps); - } - return origin; } /** @@ -63,52 +49,26 @@ export abstract class Origin extends Construct { * * @param bucket the bucket to act as an origin. */ - public static fromWebsiteBucket(scope: Construct, id: string, bucket: IBucket, behaviorProps?: CacheBehaviorProps): Origin { - const origin = new HttpOrigin(scope, id, { + public static fromWebsiteBucket(scope: Construct, id: string, bucket: IBucket): Origin { + return new HttpOrigin(scope, id, { domainName: bucket.bucketWebsiteDomainName, - id, protocolPolicy: OriginProtocolPolicy.HTTP_ONLY, // S3 only supports HTTP for website buckets }); - if (behaviorProps) { - origin.addBehavior(behaviorProps.pathPattern, behaviorProps); - } - return origin; } - /** - * The Distribution this Origin is associated with. - */ - public distribution?: Distribution; /** * The domain name of the origin. */ public readonly domainName: string; /** - * A unique identifier for the origin. This value must be unique within the distribution. + * The unique id of the origin. */ public readonly id: string; - private readonly _behaviors: CacheBehavior[] = []; - constructor(scope: Construct, id: string, props: OriginProps) { super(scope, id); - this.distribution = props.distribution; this.domainName = props.domainName; - this.id = props.id || id; - } - - /** - * Creates a new Behavior from the given pathPattern and options, and associates it with this origin. - * - * @param pathPattern the pattern that specifies which requests to apply the behavior to; may be '*' if this is the default behavior. - * @param options the behavior options - */ - public addBehavior(pathPattern: string, options: CacheBehaviorOptions): CacheBehavior { - return new CacheBehavior({ - origin: this, - pathPattern, - ...options, - }); + this.id = this.node.id; } /** @@ -132,31 +92,6 @@ export abstract class Origin extends Construct { }; } - /** - * Internal API used by `Distribution` to keep an inventory of origins for the - * purposes of keeping track of behaviors as they're created and added. - * - * @internal - */ - public _attachDistribution(distribution: Distribution) { - this.distribution = distribution; - // Register all existing behaviors with the distribution. New behaviors will be attached as they are created. - this._behaviors.forEach(b => distribution._attachBehavior(b)); - } - - /** - * Internal API used by `Behavior` to keep an inventory of behaviors associated with - * this origin, for the sake of ordering behaviors based on creation order. - * - * @internal - */ - public _attachBehavior(behavior: CacheBehavior) { - this._behaviors.push(behavior); - if (this.distribution) { - this.distribution._attachBehavior(behavior); - } - } - // Overridden by sub-classes to provide S3 origin config. protected renderS3OriginConfig(): CfnDistribution.S3OriginConfigProperty | undefined { return undefined; @@ -190,7 +125,7 @@ export class S3Origin extends Origin { constructor(scope: Construct, id: string, props: S3OriginProps) { super(scope, id, props); - this.originAccessIdentity = new OriginAccessIdentity(scope, 'S3OriginIdentity'); + this.originAccessIdentity = new OriginAccessIdentity(this, 'S3OriginIdentity'); props.bucket.grantRead(this.originAccessIdentity); } diff --git a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts index bca7d53db5db7..56c42c0846d24 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts @@ -4,7 +4,7 @@ import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; import { CfnDistribution } from './cloudfront.generated'; -import { IDistribution } from './distribution'; +import { IDistribution, OriginProtocolPolicy, PriceClass, ViewerProtocolPolicy } from './distribution'; import { IOriginAccessIdentity } from './origin_access_identity'; export enum HttpVersion { @@ -12,24 +12,6 @@ export enum HttpVersion { HTTP2 = 'http2' } -/** - * The price class determines how many edge locations CloudFront will use for your distribution. - */ -export enum PriceClass { - PRICE_CLASS_100 = 'PriceClass_100', - PRICE_CLASS_200 = 'PriceClass_200', - PRICE_CLASS_ALL = 'PriceClass_All' -} - -/** - * How HTTPs should be handled with your distribution. - */ -export enum ViewerProtocolPolicy { - HTTPS_ONLY = 'https-only', - REDIRECT_TO_HTTPS = 'redirect-to-https', - ALLOW_ALL = 'allow-all' -} - /** * Configuration for custom domain names * @@ -247,12 +229,6 @@ export enum OriginSslPolicy { TLS_V1_2 = 'TLSv1.2', } -export enum OriginProtocolPolicy { - HTTP_ONLY = 'http-only', - MATCH_VIEWER = 'match-viewer', - HTTPS_ONLY = 'https-only', -} - /** * S3 origin configuration for CloudFront */ diff --git a/packages/@aws-cdk/aws-cloudfront/test/behavior.test.ts b/packages/@aws-cdk/aws-cloudfront/test/behavior.test.ts new file mode 100644 index 0000000000000..ed673d0296592 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/behavior.test.ts @@ -0,0 +1,49 @@ +import '@aws-cdk/assert/jest'; +import * as s3 from '@aws-cdk/aws-s3'; +import { App, Stack } from '@aws-cdk/core'; +import { AllowedMethods, CacheBehavior, Origin } from '../lib'; + +let app: App; +let stack: Stack; + +beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { + env: { account: '1234', region: 'testregion' }, + }); +}); + +test('renders the minimum template with an origin and path specified', () => { + const behavior = new CacheBehavior({ + origin: Origin.fromBucket(stack, 'MyOrigin', new s3.Bucket(stack, 'MyBucket')), + pathPattern: '*', + }); + + expect(behavior._renderBehavior()).toEqual({ + targetOriginId: behavior.origin.id, + pathPattern: '*', + forwardedValues: { queryString: false }, + viewerProtocolPolicy: 'allow-all', + }); +}); + +test('renders with all properties specified', () => { + const behavior = new CacheBehavior({ + origin: Origin.fromBucket(stack, 'MyOrigin', new s3.Bucket(stack, 'MyBucket')), + pathPattern: '*', + allowedMethods: AllowedMethods.ALLOW_ALL, + forwardQueryString: true, + forwardQueryStringCacheKeys: ['user_id', 'auth'], + }); + + expect(behavior._renderBehavior()).toEqual({ + targetOriginId: behavior.origin.id, + pathPattern: '*', + allowedMethods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'PATCH', 'POST', 'DELETE'], + forwardedValues: { + queryString: true, + queryStringCacheKeys: ['user_id', 'auth'], + }, + viewerProtocolPolicy: 'allow-all', + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts index 01207f39b1738..f9fb050da8a2c 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts @@ -2,7 +2,7 @@ import '@aws-cdk/assert/jest'; import * as acm from '@aws-cdk/aws-certificatemanager'; import * as s3 from '@aws-cdk/aws-s3'; import { App, Stack } from '@aws-cdk/core'; -import { Distribution, Origin } from '../lib'; +import { Distribution, Origin, PriceClass } from '../lib'; let app: App; let stack: Stack; @@ -16,7 +16,7 @@ beforeEach(() => { test('minimal example renders correctly', () => { const origin = Origin.fromBucket(stack, 'MyOrigin', new s3.Bucket(stack, 'Bucket')); - new Distribution(stack, 'MyDist', { origin }); + new Distribution(stack, 'MyDist', { defaultBehavior: { origin } }); expect(stack).toHaveResource('AWS::CloudFront::Distribution', { DistributionConfig: { @@ -31,7 +31,7 @@ test('minimal example renders correctly', () => { Id: 'MyOrigin', S3OriginConfig: { OriginAccessIdentity: { 'Fn::Join': [ '', - [ 'origin-access-identity/cloudfront/', { Ref: 'S3OriginIdentityBB010E4C' } ], + [ 'origin-access-identity/cloudfront/', { Ref: 'MyOriginS3OriginIdentityBEF16CC0' } ], ]}, }, }], @@ -39,6 +39,158 @@ test('minimal example renders correctly', () => { }); }); +describe('multiple behaviors', () => { + + test('a second behavior can\'t be specified with the catch-all path pattern', () => { + const origin = Origin.fromBucket(stack, 'MyOrigin', new s3.Bucket(stack, 'Bucket')); + + expect(() => { + new Distribution(stack, 'MyDist', { + defaultBehavior: { origin }, + additionalBehaviors: { + '*': { origin }, + }, + }); + }).toThrow(/Only the default behavior can have a path pattern of \'*\'/); + }); + + test('a second behavior can be added to the original origin', () => { + const origin = Origin.fromBucket(stack, 'MyOrigin', new s3.Bucket(stack, 'Bucket')); + new Distribution(stack, 'MyDist', { + defaultBehavior: { origin }, + additionalBehaviors: { + 'api/*': { origin }, + }, + }); + + expect(stack).toHaveResource('AWS::CloudFront::Distribution', { + DistributionConfig: { + DefaultCacheBehavior: { + ForwardedValues: { QueryString: false }, + TargetOriginId: 'MyOrigin', + ViewerProtocolPolicy: 'allow-all', + }, + CacheBehaviors: [{ + PathPattern: 'api/*', + ForwardedValues: { QueryString: false }, + TargetOriginId: 'MyOrigin', + ViewerProtocolPolicy: 'allow-all', + }], + Enabled: true, + Origins: [{ + DomainName: { 'Fn::GetAtt': [ 'Bucket83908E77', 'RegionalDomainName' ] }, + Id: 'MyOrigin', + S3OriginConfig: { + OriginAccessIdentity: { 'Fn::Join': [ '', + [ 'origin-access-identity/cloudfront/', { Ref: 'MyOriginS3OriginIdentityBEF16CC0' } ], + ]}, + }, + }], + }, + }); + }); + + test('a second behavior can be added to a secondary origin', () => { + const origin = Origin.fromBucket(stack, 'MyOrigin', new s3.Bucket(stack, 'Bucket')); + const origin2 = Origin.fromBucket(stack, 'MyOrigin2', new s3.Bucket(stack, 'Bucket2')); + new Distribution(stack, 'MyDist', { + defaultBehavior: { origin }, + additionalBehaviors: { + 'api/*': { origin: origin2 }, + }, + }); + + expect(stack).toHaveResource('AWS::CloudFront::Distribution', { + DistributionConfig: { + DefaultCacheBehavior: { + ForwardedValues: { QueryString: false }, + TargetOriginId: 'MyOrigin', + ViewerProtocolPolicy: 'allow-all', + }, + CacheBehaviors: [{ + PathPattern: 'api/*', + ForwardedValues: { QueryString: false }, + TargetOriginId: 'MyOrigin2', + ViewerProtocolPolicy: 'allow-all', + }], + Enabled: true, + Origins: [{ + DomainName: { 'Fn::GetAtt': [ 'Bucket83908E77', 'RegionalDomainName' ] }, + Id: 'MyOrigin', + S3OriginConfig: { + OriginAccessIdentity: { 'Fn::Join': [ '', + [ 'origin-access-identity/cloudfront/', { Ref: 'MyOriginS3OriginIdentityBEF16CC0' } ], + ]}, + }, + }, + { + DomainName: { 'Fn::GetAtt': [ 'Bucket25524B414', 'RegionalDomainName' ] }, + Id: 'MyOrigin2', + S3OriginConfig: { + OriginAccessIdentity: { 'Fn::Join': [ '', + [ 'origin-access-identity/cloudfront/', { Ref: 'MyOrigin2S3OriginIdentityB67B10D6' } ], + ]}, + }, + }], + }, + }); + }); + + test('behavior creation order is preserved', () => { + const origin = Origin.fromBucket(stack, 'MyOrigin', new s3.Bucket(stack, 'Bucket')); + const origin2 = Origin.fromBucket(stack, 'MyOrigin2', new s3.Bucket(stack, 'Bucket2')); + const dist = new Distribution(stack, 'MyDist', { + defaultBehavior: { origin }, + additionalBehaviors: { + 'api/1*': { origin: origin2 }, + }, + }); + dist.addBehavior('api/2*', { origin }); + + expect(stack).toHaveResource('AWS::CloudFront::Distribution', { + DistributionConfig: { + DefaultCacheBehavior: { + ForwardedValues: { QueryString: false }, + TargetOriginId: 'MyOrigin', + ViewerProtocolPolicy: 'allow-all', + }, + CacheBehaviors: [{ + PathPattern: 'api/1*', + ForwardedValues: { QueryString: false }, + TargetOriginId: 'MyOrigin2', + ViewerProtocolPolicy: 'allow-all', + }, + { + PathPattern: 'api/2*', + ForwardedValues: { QueryString: false }, + TargetOriginId: 'MyOrigin', + ViewerProtocolPolicy: 'allow-all', + }], + Enabled: true, + Origins: [{ + DomainName: { 'Fn::GetAtt': [ 'Bucket83908E77', 'RegionalDomainName' ] }, + Id: 'MyOrigin', + S3OriginConfig: { + OriginAccessIdentity: { 'Fn::Join': [ '', + [ 'origin-access-identity/cloudfront/', { Ref: 'MyOriginS3OriginIdentityBEF16CC0' } ], + ]}, + }, + }, + { + DomainName: { 'Fn::GetAtt': [ 'Bucket25524B414', 'RegionalDomainName' ] }, + Id: 'MyOrigin2', + S3OriginConfig: { + OriginAccessIdentity: { 'Fn::Join': [ '', + [ 'origin-access-identity/cloudfront/', { Ref: 'MyOrigin2S3OriginIdentityB67B10D6' } ], + ]}, + }, + }], + }, + }); + }); + +}); + describe('certificates', () => { test('should fail if using an imported certificate from outside of us-east-1', () => { @@ -47,7 +199,7 @@ describe('certificates', () => { expect(() => { new Distribution(stack, 'Dist', { - origin, + defaultBehavior: { origin }, certificate, }); }).toThrow(/Distribution certificates must be in the us-east-1 region/); @@ -57,7 +209,7 @@ describe('certificates', () => { const certificate = acm.Certificate.fromCertificateArn(stack, 'Cert', 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012'); new Distribution(stack, 'Dist', { - origin: Origin.fromBucket(stack, 'Origin', new s3.Bucket(stack, 'Bucket')), + defaultBehavior: { origin: Origin.fromBucket(stack, 'Origin', new s3.Bucket(stack, 'Bucket')) }, certificate, }); @@ -69,4 +221,80 @@ describe('certificates', () => { }, }); }); +}); + +describe('custom error responses', () => { + + test('should fail if responsePagePath is defined but responseCode is not', () => { + const origin = Origin.fromBucket(stack, 'Origin', new s3.Bucket(stack, 'Bucket')); + + expect(() => { + new Distribution(stack, 'Dist', { + defaultBehavior: { origin }, + errorConfigurations: [{ + errorCode: 404, + responsePagePath: '/errors/404.html', + }], + }); + }).toThrow(/\'responseCode\' must be provided if \'responsePagePath\' is defined/); + }); + + test('should fail if only the error code is provided', () => { + const origin = Origin.fromBucket(stack, 'Origin', new s3.Bucket(stack, 'Bucket')); + + expect(() => { + new Distribution(stack, 'Dist', { + defaultBehavior: { origin }, + errorConfigurations: [{ errorCode: 404 }], + }); + }).toThrow(/A custom error response without either a \'responseCode\' or \'errorCachingMinTtl\' is not valid./); + }); + + test('should render the array of error configs if provided', () => { + const origin = Origin.fromBucket(stack, 'Origin', new s3.Bucket(stack, 'Bucket')); + new Distribution(stack, 'Dist', { + defaultBehavior: { origin }, + errorConfigurations: [{ + errorCode: 404, + responseCode: 404, + responsePagePath: '/errors/404.html', + }, + { + errorCode: 500, + errorCachingMinTtl: 2, + }], + }); + + expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + DistributionConfig: { + CustomErrorResponses: [ + { + ErrorCode: 404, + ResponseCode: 404, + ResponsePagePath: '/errors/404.html', + }, + { + ErrorCachingMinTTL: 2, + ErrorCode: 500, + }, + ], + }, + }); + }); + +}); + +test('price class is included if provided', () => { + const origin = Origin.fromBucket(stack, 'Origin', new s3.Bucket(stack, 'Bucket')); + new Distribution(stack, 'Dist', { + defaultBehavior: { origin }, + priceClass: PriceClass.PRICE_CLASS_200, + }); + + expect(stack).toHaveResourceLike('AWS::CloudFront::Distribution', { + DistributionConfig: { + PriceClass: 'PriceClass_200', + }, + }); + }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts index d295656a1a2d7..b5adcb4b18404 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts @@ -33,7 +33,7 @@ describe('fromBucket', () => { const bucket = new s3.Bucket(stack, 'Bucket'); const origin = Origin.fromBucket(stack, 'Origin', bucket); - new Distribution(stack, 'Dist', { origin }); + new Distribution(stack, 'Dist', { defaultBehavior: { origin } }); expect(stack).toHaveResourceLike('AWS::CloudFront::CloudFrontOriginAccessIdentity', { CloudFrontOriginAccessIdentityConfig: { @@ -44,7 +44,7 @@ describe('fromBucket', () => { PolicyDocument: { Statement: [{ Principal: { - CanonicalUser: { 'Fn::GetAtt': [ 'S3OriginIdentityBB010E4C', 'S3CanonicalUserId' ] }, + CanonicalUser: { 'Fn::GetAtt': [ 'OriginS3OriginIdentity1E4900C6', 'S3CanonicalUserId' ] }, }, }], }, From d02fd0a91226d9113ad0f5d48c75ba7241f5c380 Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Fri, 10 Jul 2020 15:00:22 +0100 Subject: [PATCH 07/15] Moved CacheBehavior to private/ and stopped exporting it --- .../@aws-cdk/aws-cloudfront/lib/behavior.ts | 106 ------------------ .../aws-cloudfront/lib/distribution.ts | 59 +++++++++- packages/@aws-cdk/aws-cloudfront/lib/index.ts | 1 - .../lib/private/cache-behavior.ts | 55 +++++++++ .../cache-behavior.test.ts} | 3 +- 5 files changed, 112 insertions(+), 112 deletions(-) delete mode 100644 packages/@aws-cdk/aws-cloudfront/lib/behavior.ts create mode 100644 packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts rename packages/@aws-cdk/aws-cloudfront/test/{behavior.test.ts => private/cache-behavior.test.ts} (92%) diff --git a/packages/@aws-cdk/aws-cloudfront/lib/behavior.ts b/packages/@aws-cdk/aws-cloudfront/lib/behavior.ts deleted file mode 100644 index c1ed113456280..0000000000000 --- a/packages/@aws-cdk/aws-cloudfront/lib/behavior.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { CfnDistribution } from './cloudfront.generated'; -import { ViewerProtocolPolicy } from './distribution'; -import { Origin } from './origin'; - -/** - * The HTTP methods that the Behavior will accept requests on. - */ -export class AllowedMethods { - /** HEAD and GET */ - public static readonly ALLOW_GET_HEAD = new AllowedMethods(['GET', 'HEAD']); - /** HEAD, GET, and OPTIONS */ - public static readonly ALLOW_GET_HEAD_OPTIONS = new AllowedMethods(['GET', 'HEAD', 'OPTIONS']); - /** All supported HTTP methods */ - public static readonly ALLOW_ALL = new AllowedMethods(['GET', 'HEAD', 'OPTIONS', 'PUT', 'PATCH', 'POST', 'DELETE']); - - /** HTTP methods supported */ - public readonly methods: string[]; - - private constructor(methods: string[]) { this.methods = methods; } -} - -/** - * Options for creating a new behavior. - */ -export interface CacheBehaviorOptions { - /** - * The origin that you want CloudFront to route requests to when they match this cache behavior. - */ - readonly origin: Origin; - - /** - * HTTP methods to allow for this behavior. - * - * @default GET and HEAD - */ - readonly allowedMethods?: AllowedMethods; - - /** - * Whether CloudFront will forward query strings to the origin. - * If this is set to true, CloudFront will forward all query parameters to the origin, and cache - * based on all parameters. See `forwardQueryStringCacheKeys` for a way to limit the query parameters - * CloudFront caches on. - * - * @default false - */ - readonly forwardQueryString?: boolean; - - /** - * A set of query string parameter names to use for caching if `forwardQueryString` is set to true. - * - * @default empty list - */ - readonly forwardQueryStringCacheKeys?: string[]; -} - -/** - * Properties for specifying custom behaviors for origins. - */ -export interface CacheBehaviorProps extends CacheBehaviorOptions { - /** - * The pattern (e.g., `images/*.jpg`) that specifies which requests to apply the behavior to. - * There must be exactly one behavior associated with each `Distribution` that has a path pattern - * of '*', which acts as the catch-all default behavior. - */ - readonly pathPattern: string; -} - -/** - * Allows configuring a variety of CloudFront functionality for a given URL path pattern. - * - * Note: This really should simply by called 'Behavior', but this name is already taken by the legacy - * CloudFrontWebDistribution implementation. - */ -export class CacheBehavior { - - /** - * Origin that this behavior will route traffic to. - */ - public readonly origin: Origin; - - constructor(private readonly props: CacheBehaviorProps) { - this.origin = props.origin; - } - - /** - * Creates and returns the CloudFormation representation of this behavior. - * This renders as a "CacheBehaviorProperty" regardless of if this is a default - * cache behavior or not, as the two are identical except that the pathPattern - * is omitted for the default cache behavior. - * - * @internal - */ - public _renderBehavior(): CfnDistribution.CacheBehaviorProperty { - return { - pathPattern: this.props.pathPattern, - targetOriginId: this.origin.id, - allowedMethods: this.props.allowedMethods?.methods ?? undefined, - forwardedValues: { - queryString: this.props.forwardQueryString ?? false, - queryStringCacheKeys: this.props.forwardQueryStringCacheKeys, - }, - viewerProtocolPolicy: ViewerProtocolPolicy.ALLOW_ALL, - }; - } - -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index b8443a50d2787..1ec6439c2f862 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -1,9 +1,9 @@ import * as acm from '@aws-cdk/aws-certificatemanager'; import * as s3 from '@aws-cdk/aws-s3'; import { Construct, IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; -import { CacheBehavior, CacheBehaviorOptions } from './behavior'; import { CfnDistribution } from './cloudfront.generated'; import { Origin } from './origin'; +import { CacheBehavior } from './private/cache-behavior'; /** * Interface for CloudFront distributions @@ -50,14 +50,14 @@ export interface DistributionProps { /** * The default behavior for the distribution. */ - readonly defaultBehavior: CacheBehaviorOptions; + readonly defaultBehavior: BehaviorOptions; /** * Additional behaviors for the distribution, mapped by the pathPattern that specifies which requests to apply the behavior to. * * @default no additional behaviors are added. */ - readonly additionalBehaviors?: Record; + readonly additionalBehaviors?: Record; /** * A certificate to associate with the distribution. The certificate must be located in N. Virginia (us-east-1). @@ -177,7 +177,7 @@ export class Distribution extends Resource implements IDistribution { * @param pathPattern the path pattern (e.g., 'images/*') that specifies which requests to apply the behavior to. * @param behaviorOptions the options for the behavior at this path. */ - public addBehavior(pathPattern: string, behaviorOptions: CacheBehaviorOptions) { + public addBehavior(pathPattern: string, behaviorOptions: BehaviorOptions) { if (pathPattern === '*') { throw new Error('Only the default behavior can have a path pattern of \'*\''); } @@ -241,3 +241,54 @@ export enum OriginProtocolPolicy { MATCH_VIEWER = 'match-viewer', HTTPS_ONLY = 'https-only', } + +/** + * The HTTP methods that the Behavior will accept requests on. + */ +export class AllowedMethods { + /** HEAD and GET */ + public static readonly ALLOW_GET_HEAD = new AllowedMethods(['GET', 'HEAD']); + /** HEAD, GET, and OPTIONS */ + public static readonly ALLOW_GET_HEAD_OPTIONS = new AllowedMethods(['GET', 'HEAD', 'OPTIONS']); + /** All supported HTTP methods */ + public static readonly ALLOW_ALL = new AllowedMethods(['GET', 'HEAD', 'OPTIONS', 'PUT', 'PATCH', 'POST', 'DELETE']); + + /** HTTP methods supported */ + public readonly methods: string[]; + + private constructor(methods: string[]) { this.methods = methods; } +} + +/** + * Options for creating a new behavior. + */ +export interface BehaviorOptions { + /** + * The origin that you want CloudFront to route requests to when they match this cache behavior. + */ + readonly origin: Origin; + + /** + * HTTP methods to allow for this behavior. + * + * @default GET and HEAD + */ + readonly allowedMethods?: AllowedMethods; + + /** + * Whether CloudFront will forward query strings to the origin. + * If this is set to true, CloudFront will forward all query parameters to the origin, and cache + * based on all parameters. See `forwardQueryStringCacheKeys` for a way to limit the query parameters + * CloudFront caches on. + * + * @default false + */ + readonly forwardQueryString?: boolean; + + /** + * A set of query string parameter names to use for caching if `forwardQueryString` is set to true. + * + * @default empty list + */ + readonly forwardQueryStringCacheKeys?: string[]; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/lib/index.ts b/packages/@aws-cdk/aws-cloudfront/lib/index.ts index 5063f0bdd218a..bf106211657c9 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/index.ts @@ -1,4 +1,3 @@ -export * from './behavior'; export * from './distribution'; export * from './web_distribution'; export * from './origin'; diff --git a/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts b/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts new file mode 100644 index 0000000000000..2ffcdf5b1f576 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts @@ -0,0 +1,55 @@ +import { CfnDistribution } from '../cloudfront.generated'; +import { BehaviorOptions, ViewerProtocolPolicy } from '../distribution'; +import { Origin } from '../origin'; + +/** + * Properties for specifying custom behaviors for origins. + */ +export interface CacheBehaviorProps extends BehaviorOptions { + /** + * The pattern (e.g., `images/*.jpg`) that specifies which requests to apply the behavior to. + * There must be exactly one behavior associated with each `Distribution` that has a path pattern + * of '*', which acts as the catch-all default behavior. + */ + readonly pathPattern: string; +} + +/** + * Allows configuring a variety of CloudFront functionality for a given URL path pattern. + * + * Note: This really should simply by called 'Behavior', but this name is already taken by the legacy + * CloudFrontWebDistribution implementation. + */ +export class CacheBehavior { + + /** + * Origin that this behavior will route traffic to. + */ + public readonly origin: Origin; + + constructor(private readonly props: CacheBehaviorProps) { + this.origin = props.origin; + } + + /** + * Creates and returns the CloudFormation representation of this behavior. + * This renders as a "CacheBehaviorProperty" regardless of if this is a default + * cache behavior or not, as the two are identical except that the pathPattern + * is omitted for the default cache behavior. + * + * @internal + */ + public _renderBehavior(): CfnDistribution.CacheBehaviorProperty { + return { + pathPattern: this.props.pathPattern, + targetOriginId: this.origin.id, + allowedMethods: this.props.allowedMethods?.methods ?? undefined, + forwardedValues: { + queryString: this.props.forwardQueryString ?? false, + queryStringCacheKeys: this.props.forwardQueryStringCacheKeys, + }, + viewerProtocolPolicy: ViewerProtocolPolicy.ALLOW_ALL, + }; + } + +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/test/behavior.test.ts b/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts similarity index 92% rename from packages/@aws-cdk/aws-cloudfront/test/behavior.test.ts rename to packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts index ed673d0296592..d49f41b316b38 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/behavior.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts @@ -1,7 +1,8 @@ import '@aws-cdk/assert/jest'; import * as s3 from '@aws-cdk/aws-s3'; import { App, Stack } from '@aws-cdk/core'; -import { AllowedMethods, CacheBehavior, Origin } from '../lib'; +import { AllowedMethods, Origin } from '../../lib'; +import { CacheBehavior } from '../../lib/private/cache-behavior'; let app: App; let stack: Stack; From 799f5d8cbccfe1d7d9b60c1af1c688e0159dd15f Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Fri, 10 Jul 2020 15:34:16 +0100 Subject: [PATCH 08/15] README updates, plus @experimental tags throughout --- packages/@aws-cdk/aws-cloudfront/README.md | 128 +++++++++++++++++- .../aws-cloudfront/lib/distribution.ts | 8 ++ .../@aws-cdk/aws-cloudfront/lib/origin.ts | 12 ++ 3 files changed, 147 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-cloudfront/README.md b/packages/@aws-cdk/aws-cloudfront/README.md index 04488d3670742..180cf2a5c7dfb 100644 --- a/packages/@aws-cdk/aws-cloudfront/README.md +++ b/packages/@aws-cdk/aws-cloudfront/README.md @@ -1,4 +1,5 @@ ## Amazon CloudFront Construct Library + --- @@ -13,6 +14,15 @@ --- +Amazon CloudFront is a web service that speeds up distribution of your static and dynamic web content, such as .html, .css, .js, and image files, to +your users. CloudFront delivers your content through a worldwide network of data centers called edge locations. When a user requests content that +you're serving with CloudFront, the user is routed to the edge location that provides the lowest latency, so that content is delivered with the best +possible performance. + +## CloudFrontWebDistribution API - Stable + +![cdk-constructs: Stable](https://img.shields.io/badge/cdk--constructs-stable-success.svg?style=for-the-badge) + A CloudFront construct - for setting up the AWS CDN with ease! Example usage: @@ -75,6 +85,7 @@ CloudFront supports adding restrictions to your distribution. See [Restricting the Geographic Distribution of Your Content](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/georestrictions.html) in the CloudFront User Guide. Example: + ```ts new cloudfront.CloudFrontWebDistribution(stack, 'MyDistribution', { //... @@ -82,7 +93,7 @@ new cloudfront.CloudFrontWebDistribution(stack, 'MyDistribution', { }); ``` -### Connection behaviors between CloudFront and your origin. +### Connection behaviors between CloudFront and your origin CloudFront provides you even more control over the connection behaviors between CloudFront and your origin. You can now configure the number of connection attempts CloudFront will make to your origin and the origin connection timeout for each attempt. @@ -103,3 +114,118 @@ const distribution = new CloudFrontWebDistribution(this, 'MyDistribution', { ] }); ``` + +## Distribution API - Experimental + +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +In addition to the APIs listed above, a new construct (`Distribution`) is under active development as a newer, friendlier way to define CloudFront +distributions. + +### Creating a distribution + +CloudFront distributions deliver your content from one or more origins; an origin is the location where you store the original version of your +content. Origins can be created from S3 buckets or a custom origin (HTTP server). Each distribution has a default behavior which applies to all +requests to that distribution, and routes requests to a primary origin. + +#### From an S3 Bucket + +An S3 bucket can be added as an origin. If the bucket is configured as a website endpoint, the distribution can use S3 redirects and S3 custom error +documents. + +```ts +import * as cloudfront from '@aws-cdk/aws-cloudfront'; + +// Creates a distribution for a S3 bucket. +const myBucket = new s3.Bucket(...); +new cloudfront.Distribution(this, 'myDist', { + defaultBehavior: { origin: cloudfront.Origin.fromBucket(myBucket) }, +}); + +// Equivalent to the above +const myBucket = new s3.Bucket(...); +cloudfront.Distribution.forBucket(this, 'myDist', myBucket); + +// Creates a distribution for a S3 bucket that has been configured for website hosting. +const myWebsiteBucket = new s3.Bucket(...); +new cloudfront.Distribution(this, 'myDist', { + defaultBehavior: { origin: cloudfront.Origin.fromWebsiteBucket(myBucket) }, +}); + +// Equivalent to the above +const myBucket = new s3.Bucket(...); +cloudfront.Distribution.forWebsiteBucket(this, 'myDist', myBucket); +``` + +The `forBucket` options will automatically create an origin access identity and grant it access to the underlying bucket. This can be used in +conjunction with a bucket that is not public to require that your users access your content using CloudFront URLs and not S3 URLs directly. + +### Domain Names and Certificates + +When you create a distribution, CloudFront returns a domain name for the distribution, for example: `d111111abcdef8.cloudfront.net`. CloudFront +distributions use a default certificate (`*.cloudfront.net`) to support HTTPS by default. If you want to use your own domain name, such as +`www.example.com`, you must associate a certificate with your distribution that contains your domain name. The certificate must be present in the AWS +Certificate Manager (ACM) service in the US East (N. Virginia) region; the certificate may either be created by ACM, or created elsewhere and +imported into ACM. + +```ts +const myCertificate = new acm.DnsValidatedCertificate(this, 'mySiteCert', { + domainName: 'www.example.com', + hostedZone, +}); +new cloudfront.Distribution(this, 'myDist', { + defaultBehavior: { origin: cloudfront.Origin.fromBucket(myBucket) }, + certificate: myCertificate, +}); +``` + +### Multiple Behaviors & Origins + +Each distribution has a default behavior which applies to all requests to that distribution; additional behaviors may be specified for a +given URL path pattern. Behaviors allow routing with multiple origins, controlling which HTTP methods to support, whether to require users to +use HTTPS, and what query strings or cookies to forward to your origin, among others. + +The properties of the default behavior can be adjusted as part of the distribution creation. The following example shows configuring the HTTP +methods and viewer protocol policy of the cache. + +```ts +const myWebDistribution = new cloudfront.Distribution(this, 'myDist', { + defaultBehavior: { + origin: cloudfront.Origin.fromBucket(myBucket), + allowedMethods: AllowedMethods.ALLOW_ALL, + viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + } +}); +``` + +Additional cache behaviors can be specified at creation, or added after the initial creation. Each additional behavior is associated with an origin, +and enable customization for a specific set of resources based on a URL path pattern. For example, we can add a behavior to `myWebDistribution` to +override the default time-to-live (TTL) for all of the images. + +```ts +myWebDistribution.addBehavior('/images/*.jpg', { + origin: cloudfront.Origin.fromBucket(myOtherBucket), + viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + defaultTtl: cdk.Duration.days(7), +}); +``` + +These behaviors can also be specified at distribution creation time. + +```ts +const bucketOrigin = cloudfront.Origin.fromBucket(myBucket); +new cloudfront.Distribution(this, 'myDist', { + defaultBehavior: { + origin: bucketOrigin, + allowedMethods: AllowedMethods.ALLOW_ALL, + viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + }, + additionalBehaviors: [{ + '/images/*.jpg', { + origin: bucketOrigin, + viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + defaultTtl: cdk.Duration.days(7), + }, + }] +}); +``` diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index 1ec6439c2f862..c1bda91daa6a6 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -26,6 +26,8 @@ export interface IDistribution extends IResource { /** * Attributes used to import a Distribution. + * + * @experimental */ export interface DistributionAttributes { /** @@ -45,6 +47,8 @@ export interface DistributionAttributes { /** * Properties for a Distribution + * + * @experimental */ export interface DistributionProps { /** @@ -87,6 +91,8 @@ export interface DistributionProps { /** * A CloudFront distribution with associated origin(s) and caching behavior(s). + * + * @experimental */ export class Distribution extends Resource implements IDistribution { @@ -261,6 +267,8 @@ export class AllowedMethods { /** * Options for creating a new behavior. + * + * @experimental */ export interface BehaviorOptions { /** diff --git a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts index a920e92803e6d..53a772bd80171 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts @@ -7,6 +7,8 @@ import { OriginAccessIdentity } from './origin_access_identity'; /** * Properties to be used to create an Origin. Prefer to use one of the Origin.from* factory methods rather than * instantiating an Origin directly from these properties. + * + * @experimental */ export interface OriginProps { /** @@ -26,6 +28,8 @@ export interface OriginProps { /** * Represents a distribution origin, that describes the Amazon S3 bucket, HTTP server (for example, a web server), * Amazon MediaStore, or other server from which CloudFront gets your files. + * + * @experimental */ export abstract class Origin extends Construct { @@ -106,6 +110,8 @@ export abstract class Origin extends Construct { /** * Properties for an Origin backed by an S3 bucket + * + * @experimental */ export interface S3OriginProps extends OriginProps { /** @@ -118,6 +124,8 @@ export interface S3OriginProps extends OriginProps { * An Origin specific to a S3 bucket (not configured for website hosting). * * Contains additional logic around bucket permissions and origin access identities. + * + * @experimental */ export class S3Origin extends Origin { private readonly originAccessIdentity: OriginAccessIdentity; @@ -136,6 +144,8 @@ export class S3Origin extends Origin { /** * Properties for an Origin backed by an S3 website-configured bucket, load balancer, or custom HTTP server. + * + * @experimental */ export interface HttpOriginProps extends OriginProps { /** @@ -148,6 +158,8 @@ export interface HttpOriginProps extends OriginProps { /** * An Origin for an HTTP server or S3 bucket configured for website hosting. + * + * @experimental */ export class HttpOrigin extends Origin { From aa14763d0f6dc752989d58007c59de5fb9a291ce Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Mon, 13 Jul 2020 10:51:30 +0100 Subject: [PATCH 09/15] Removed unused Origin.distribution property from original design --- packages/@aws-cdk/aws-cloudfront/lib/distribution.ts | 3 +-- packages/@aws-cdk/aws-cloudfront/lib/origin.ts | 12 ++---------- .../aws-cloudfront/lib/private/cache-behavior.ts | 2 +- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index c1bda91daa6a6..eb6a8ac7cd8eb 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -83,7 +83,6 @@ export interface DistributionProps { /** * How CloudFront should handle requests that are not successful (e.g., PageNotFound). * - * * @default - No custom error configuration. */ readonly errorConfigurations?: CfnDistribution.CustomErrorResponseProperty[]; @@ -299,4 +298,4 @@ export interface BehaviorOptions { * @default empty list */ readonly forwardQueryStringCacheKeys?: string[]; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts index 53a772bd80171..5a9468861c1c0 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts @@ -1,7 +1,7 @@ import { IBucket } from '@aws-cdk/aws-s3'; import { Construct } from '@aws-cdk/core'; import { CfnDistribution } from './cloudfront.generated'; -import { Distribution, OriginProtocolPolicy } from './distribution'; +import { OriginProtocolPolicy } from './distribution'; import { OriginAccessIdentity } from './origin_access_identity'; /** @@ -15,14 +15,6 @@ export interface OriginProps { * The domain name of the Amazon S3 bucket or HTTP server origin. */ readonly domainName: string; - - /** - * The Distribution this Origin will be associated with. - * [disable-awslint:ref-via-interface] - * - * @default - This will be set when the Origin is added to the Distribution. - */ - readonly distribution?: Distribution; } /** @@ -176,4 +168,4 @@ export class HttpOrigin extends Origin { originProtocolPolicy: this.protocolPolicy ?? OriginProtocolPolicy.HTTPS_ONLY, }; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts b/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts index 2ffcdf5b1f576..59f0226a92a31 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts @@ -52,4 +52,4 @@ export class CacheBehavior { }; } -} \ No newline at end of file +} From 9d770b505e6b2f8da7495cca91d1f8c0880806a2 Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Mon, 13 Jul 2020 11:07:35 +0100 Subject: [PATCH 10/15] Test token fix --- packages/@aws-cdk/aws-cloudfront/test/origin.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts index b5adcb4b18404..d621e829d0142 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts @@ -24,7 +24,7 @@ describe('fromBucket', () => { id: 'MyOrigin', domainName: bucket.bucketRegionalDomainName, s3OriginConfig: { - originAccessIdentity: 'origin-access-identity/cloudfront/${Token[TOKEN.26]}', + originAccessIdentity: 'origin-access-identity/cloudfront/${Token[TOKEN.69]}', }, }); }); From 73c0acdda1c597d8d89754ee2b19057e58231bbe Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Mon, 13 Jul 2020 13:32:15 +0100 Subject: [PATCH 11/15] Updates per feedback from @elabd and @iliapolo --- packages/@aws-cdk/aws-cloudfront/README.md | 56 ++++----- .../aws-cloudfront/lib/distribution.ts | 111 +++++++++++------- .../@aws-cdk/aws-cloudfront/lib/origin.ts | 85 +++++++++----- packages/@aws-cdk/aws-cloudfront/package.json | 11 +- .../aws-cloudfront/test/distribution.test.ts | 72 ++++++------ .../aws-cloudfront/test/origin.test.ts | 41 ++++--- .../test/private/cache-behavior.test.ts | 8 +- packages/@aws-cdk/aws-s3/lib/bucket.ts | 26 +++- packages/@aws-cdk/aws-s3/test/test.bucket.ts | 53 +++++++++ 9 files changed, 301 insertions(+), 162 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront/README.md b/packages/@aws-cdk/aws-cloudfront/README.md index 180cf2a5c7dfb..398db818c869b 100644 --- a/packages/@aws-cdk/aws-cloudfront/README.md +++ b/packages/@aws-cdk/aws-cloudfront/README.md @@ -3,13 +3,17 @@ --- -![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) +| Features | Stability | +| --- | --- | +| CFN Resources | ![Stable](https://img.shields.io/badge/stable-success.svg?style=for-the-badge) | +| Higher level constructs for CloudFrontWebDistribution | ![Stable](https://img.shields.io/badge/stable-success.svg?style=for-the-badge) | +| Higher level constructs for Distribution | ![Experimental](https://img.shields.io/badge/experimental-important.svg?style=for-the-badge) | -> All classes with the `Cfn` prefix in this module ([CFN Resources](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) are always stable and safe to use. +> **CFN Resources:** All classes with the `Cfn` prefix in this module ([CFN Resources](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) are always stable and safe to use. -![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) +> **Experimental:** Higher level constructs in this module that are marked as experimental are under active development. They are subject to non-backward compatible changes or removal in any future version. These are not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be announced in the release notes. This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. -> The APIs of higher level constructs in this module are experimental and under active development. They are subject to non-backward compatible changes or removal in any future version. These are not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be announced in the release notes. This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. +> **Stable:** Higher level constructs in this module that are marked stable will not undergo any breaking changes. They will strictly follow the [Semantic Versioning](https://semver.org/) model. --- @@ -137,36 +141,25 @@ documents. import * as cloudfront from '@aws-cdk/aws-cloudfront'; // Creates a distribution for a S3 bucket. -const myBucket = new s3.Bucket(...); +const myBucket = new s3.Bucket(this, 'myBucket'); new cloudfront.Distribution(this, 'myDist', { defaultBehavior: { origin: cloudfront.Origin.fromBucket(myBucket) }, }); - -// Equivalent to the above -const myBucket = new s3.Bucket(...); -cloudfront.Distribution.forBucket(this, 'myDist', myBucket); - -// Creates a distribution for a S3 bucket that has been configured for website hosting. -const myWebsiteBucket = new s3.Bucket(...); -new cloudfront.Distribution(this, 'myDist', { - defaultBehavior: { origin: cloudfront.Origin.fromWebsiteBucket(myBucket) }, -}); - -// Equivalent to the above -const myBucket = new s3.Bucket(...); -cloudfront.Distribution.forWebsiteBucket(this, 'myDist', myBucket); ``` -The `forBucket` options will automatically create an origin access identity and grant it access to the underlying bucket. This can be used in -conjunction with a bucket that is not public to require that your users access your content using CloudFront URLs and not S3 URLs directly. +The above will treat the bucket differently based on if `IBucket.isWebsite` is set or not. If the bucket is configured as a website, the bucket is +treated as an HTTP origin, and the built-in S3 redirects and error pages can be used. Otherwise, the bucket is handled as a bucket origin and +CloudFront's redirect and error handling will be used. In the latter case, the Origin wil create an origin access identity and grant it access to the +underlying bucket. This can be used in conjunction with a bucket that is not public to require that your users access your content using CloudFront +URLs and not S3 URLs directly. ### Domain Names and Certificates -When you create a distribution, CloudFront returns a domain name for the distribution, for example: `d111111abcdef8.cloudfront.net`. CloudFront -distributions use a default certificate (`*.cloudfront.net`) to support HTTPS by default. If you want to use your own domain name, such as -`www.example.com`, you must associate a certificate with your distribution that contains your domain name. The certificate must be present in the AWS -Certificate Manager (ACM) service in the US East (N. Virginia) region; the certificate may either be created by ACM, or created elsewhere and -imported into ACM. +When you create a distribution, CloudFront assigns a domain name for the distribution, for example: `d111111abcdef8.cloudfront.net`; this value can +be retrieved from `distribution.domainName`. CloudFront distributions use a default certificate (`*.cloudfront.net`) to support HTTPS by default. If +you want to use your own domain name, such as `www.example.com`, you must associate a certificate with your distribution that contains your domain +name. The certificate must be present in the AWS Certificate Manager (ACM) service in the US East (N. Virginia) region; the certificate may either be +created by ACM, or created elsewhere and imported into ACM. ```ts const myCertificate = new acm.DnsValidatedCertificate(this, 'mySiteCert', { @@ -198,13 +191,12 @@ const myWebDistribution = new cloudfront.Distribution(this, 'myDist', { }); ``` -Additional cache behaviors can be specified at creation, or added after the initial creation. Each additional behavior is associated with an origin, +Additional behaviors can be specified at creation, or added after the initial creation. Each additional behavior is associated with an origin, and enable customization for a specific set of resources based on a URL path pattern. For example, we can add a behavior to `myWebDistribution` to override the default time-to-live (TTL) for all of the images. ```ts -myWebDistribution.addBehavior('/images/*.jpg', { - origin: cloudfront.Origin.fromBucket(myOtherBucket), +myWebDistribution.addBehavior('/images/*.jpg', cloudfront.Origin.fromBucket(myOtherBucket), { viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, defaultTtl: cdk.Duration.days(7), }); @@ -220,12 +212,12 @@ new cloudfront.Distribution(this, 'myDist', { allowedMethods: AllowedMethods.ALLOW_ALL, viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, }, - additionalBehaviors: [{ - '/images/*.jpg', { + additionalBehaviors: { + '/images/*.jpg': { origin: bucketOrigin, viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, defaultTtl: cdk.Duration.days(7), }, - }] + }, }); ``` diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index eb6a8ac7cd8eb..5d1be4a7179e5 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -1,6 +1,5 @@ import * as acm from '@aws-cdk/aws-certificatemanager'; -import * as s3 from '@aws-cdk/aws-s3'; -import { Construct, IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; +import { Construct, IResource, Lazy, Resource, Stack, Token, Duration } from '@aws-cdk/core'; import { CfnDistribution } from './cloudfront.generated'; import { Origin } from './origin'; import { CacheBehavior } from './private/cache-behavior'; @@ -85,7 +84,7 @@ export interface DistributionProps { * * @default - No custom error configuration. */ - readonly errorConfigurations?: CfnDistribution.CustomErrorResponseProperty[]; + readonly errorConfigurations?: CustomErrorResponse[]; } /** @@ -111,35 +110,14 @@ export class Distribution extends Resource implements IDistribution { }(); } - /** - * Creates a Distribution for an S3 Bucket, where the bucket has not been configured for website hosting. - * - * This creates a single-origin distribution with a single default behavior. - */ - public static forBucket(scope: Construct, id: string, bucket: s3.IBucket): Distribution { - return new Distribution(scope, id, { - defaultBehavior: { origin: Origin.fromBucket(scope, 'SingleOriginBucket', bucket) }, - }); - } - - /** - * Creates a Distribution for an S3 bucket, where the bucket has been configured for website hosting. - * - * This creates a single-origin distribution with a single default behavior. - */ - public static forWebsiteBucket(scope: Construct, id: string, bucket: s3.IBucket): Distribution { - return new Distribution(scope, id, { - defaultBehavior: { origin: Origin.fromWebsiteBucket(scope, 'SingleOriginWebsiteBucket', bucket) }, - }); - } - public readonly domainName: string; public readonly distributionId: string; private readonly defaultBehavior: CacheBehavior; private readonly additionalBehaviors: CacheBehavior[] = []; + private readonly origins: Set = new Set(); - private readonly errorConfigurations: CfnDistribution.CustomErrorResponseProperty[]; + private readonly errorConfigurations: CustomErrorResponse[]; private readonly certificate?: acm.ICertificate; constructor(scope: Construct, id: string, props: DistributionProps) { @@ -153,9 +131,10 @@ export class Distribution extends Resource implements IDistribution { } this.defaultBehavior = new CacheBehavior({ pathPattern: '*', ...props.defaultBehavior }); + this.addOrigin(this.defaultBehavior.origin); if (props.additionalBehaviors) { Object.entries(props.additionalBehaviors).forEach(([pathPattern, behaviorOptions]) => { - this.addBehavior(pathPattern, behaviorOptions); + this.addBehavior(pathPattern, behaviorOptions.origin, behaviorOptions); }); } @@ -182,20 +161,24 @@ export class Distribution extends Resource implements IDistribution { * @param pathPattern the path pattern (e.g., 'images/*') that specifies which requests to apply the behavior to. * @param behaviorOptions the options for the behavior at this path. */ - public addBehavior(pathPattern: string, behaviorOptions: BehaviorOptions) { + public addBehavior(pathPattern: string, origin: Origin, behaviorOptions: AddBehaviorOptions = {}) { if (pathPattern === '*') { throw new Error('Only the default behavior can have a path pattern of \'*\''); } - this.additionalBehaviors.push(new CacheBehavior({ pathPattern, ...behaviorOptions })); + this.additionalBehaviors.push(new CacheBehavior({ pathPattern, origin, ...behaviorOptions })); + this.addOrigin(origin); } - private renderOrigins(): CfnDistribution.OriginProperty[] { - const origins = new Set(); - origins.add(this.defaultBehavior.origin); - this.additionalBehaviors.forEach(behavior => origins.add(behavior.origin)); + private addOrigin(origin: Origin) { + if (!this.origins.has(origin)) { + this.origins.add(origin); + origin.bind(this, { originIndex: this.origins.size }); + } + } + private renderOrigins(): CfnDistribution.OriginProperty[] { const renderedOrigins: CfnDistribution.OriginProperty[] = []; - origins.forEach(origin => renderedOrigins.push(origin._renderOrigin())); + this.origins.forEach(origin => renderedOrigins.push(origin._renderOrigin())); return renderedOrigins; } @@ -206,7 +189,7 @@ export class Distribution extends Resource implements IDistribution { private renderCustomErrorResponses(): CfnDistribution.CustomErrorResponseProperty[] | undefined { if (this.errorConfigurations.length === 0) { return undefined; } - function validateCustomErrorResponse(errorResponse: CfnDistribution.CustomErrorResponseProperty) { + function validateCustomErrorResponse(errorResponse: CustomErrorResponse) { if (errorResponse.responsePagePath && !errorResponse.responseCode) { throw new Error('\'responseCode\' must be provided if \'responsePagePath\' is defined'); } @@ -215,7 +198,15 @@ export class Distribution extends Resource implements IDistribution { } } this.errorConfigurations.forEach(e => validateCustomErrorResponse(e)); - return this.errorConfigurations; + + return this.errorConfigurations.map(errorConfig => { + return { + errorCachingMinTtl: errorConfig.errorCachingMinTtl?.toSeconds(), + errorCode: errorConfig.errorCode, + responseCode: errorConfig.responseCode, + responsePagePath: errorConfig.responsePagePath, + }; + }); } } @@ -265,16 +256,44 @@ export class AllowedMethods { } /** - * Options for creating a new behavior. + * Options for configuring custom error responses. * * @experimental */ -export interface BehaviorOptions { +export interface CustomErrorResponse { /** - * The origin that you want CloudFront to route requests to when they match this cache behavior. + * The minimum amount of time, in seconds, that you want CloudFront to cache the HTTP status code specified in ErrorCode. + * + * @default the default caching TTL behavior applies */ - readonly origin: Origin; + readonly errorCachingMinTtl?: Duration; + /** + * The HTTP status code for which you want to specify a custom error page and/or a caching duration. + */ + readonly errorCode: number; + /** + * The HTTP status code that you want CloudFront to return to the viewer along with the custom error page. + * + * If you specify a value for `responseCode`, you must also specify a value for `responsePagePath`. + * + * @default not set, the error code will be returned as the response code. + */ + readonly responseCode?: number; + /** + * The path to the custom error page that you want CloudFront to return to a viewer when your origin returns the + * `errorCode`, for example, /4xx-errors/403-forbidden.html + * + * @default the default CloudFront response is shown. + */ + readonly responsePagePath?: string; +} +/** + * Options for adding a new behavior to a Distribution. + * + * @experimental + */ +export interface AddBehaviorOptions { /** * HTTP methods to allow for this behavior. * @@ -299,3 +318,15 @@ export interface BehaviorOptions { */ readonly forwardQueryStringCacheKeys?: string[]; } + +/** + * Options for creating a new behavior. + * + * @experimental + */ +export interface BehaviorOptions extends AddBehaviorOptions { + /** + * The origin that you want CloudFront to route requests to when they match this behavior. + */ + readonly origin: Origin; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts index 5a9468861c1c0..04ab180e359ea 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts @@ -17,13 +17,23 @@ export interface OriginProps { readonly domainName: string; } +/** + * Options passed to Origin.bind(). + */ +export interface OriginBindOptions { + /** + * The positional index of this origin within the distribution. Used for ensuring unique IDs. + */ + readonly originIndex: number; +} + /** * Represents a distribution origin, that describes the Amazon S3 bucket, HTTP server (for example, a web server), * Amazon MediaStore, or other server from which CloudFront gets your files. * * @experimental */ -export abstract class Origin extends Construct { +export abstract class Origin { /** * Creates a pre-configured origin for a S3 bucket. @@ -33,38 +43,45 @@ export abstract class Origin extends Construct { * * @param bucket the bucket to act as an origin. */ - public static fromBucket(scope: Construct, id: string, bucket: IBucket): Origin { - return new S3Origin(scope, id, { - domainName: bucket.bucketRegionalDomainName, - bucket, - }); + public static fromBucket(bucket: IBucket): Origin { + if (bucket.isWebsite) { + return new HttpOrigin({ + domainName: bucket.bucketWebsiteDomainName, + protocolPolicy: OriginProtocolPolicy.HTTP_ONLY, // S3 only supports HTTP for website buckets + }); + } else { + return new S3Origin({ domainName: bucket.bucketRegionalDomainName, bucket }); + } } /** - * Creates a pre-configured origin for a S3 bucket, where the bucket has been configured for website hosting. - * - * @param bucket the bucket to act as an origin. + * The domain name of the origin. */ - public static fromWebsiteBucket(scope: Construct, id: string, bucket: IBucket): Origin { - return new HttpOrigin(scope, id, { - domainName: bucket.bucketWebsiteDomainName, - protocolPolicy: OriginProtocolPolicy.HTTP_ONLY, // S3 only supports HTTP for website buckets - }); + public readonly domainName: string; + + private originId!: string; + + constructor(props: OriginProps) { + this.domainName = props.domainName; } /** - * The domain name of the origin. + * Binds the origin to the associated Distribution. Can be used to grant permissions, create dependent resources, etc. + * + * @param scope the distribution to bind this Origin to. */ - public readonly domainName: string; + public bind(scope: Construct, options: OriginBindOptions): void { + this.originId = new Construct(scope, `Origin${options.originIndex}`).node.uniqueId; + } + /** - * The unique id of the origin. + * The unique id for this origin. + * + * Cannot be accesed until bind() is called. */ - public readonly id: string; - - constructor(scope: Construct, id: string, props: OriginProps) { - super(scope, id); - this.domainName = props.domainName; - this.id = this.node.id; + public get id(): string { + if (!this.originId) { throw new Error('Cannot access originId until `bind` is called.'); } + return this.originId; } /** @@ -120,13 +137,20 @@ export interface S3OriginProps extends OriginProps { * @experimental */ export class S3Origin extends Origin { - private readonly originAccessIdentity: OriginAccessIdentity; + private readonly bucket: IBucket; + private originAccessIdentity!: OriginAccessIdentity; - constructor(scope: Construct, id: string, props: S3OriginProps) { - super(scope, id, props); + constructor(props: S3OriginProps) { + super(props); + this.bucket = props.bucket; + } - this.originAccessIdentity = new OriginAccessIdentity(this, 'S3OriginIdentity'); - props.bucket.grantRead(this.originAccessIdentity); + public bind(scope: Construct, options: OriginBindOptions) { + super.bind(scope, options); + if (!this.originAccessIdentity) { + this.originAccessIdentity = new OriginAccessIdentity(scope, `S3Origin${options.originIndex}`); + this.bucket.grantRead(this.originAccessIdentity); + } } protected renderS3OriginConfig(): CfnDistribution.S3OriginConfigProperty | undefined { @@ -157,9 +181,8 @@ export class HttpOrigin extends Origin { private readonly protocolPolicy?: OriginProtocolPolicy; - constructor(scope: Construct, id: string, props: HttpOriginProps) { - super(scope, id, props); - + constructor(props: HttpOriginProps) { + super(props); this.protocolPolicy = props.protocolPolicy; } diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index fe83de66a6a14..73c7ff344e848 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -94,7 +94,16 @@ "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", - "maturity": "experimental", + "features": [ + { + "name": "Higher level constructs for CloudFrontWebDistribution", + "stability": "Stable" + }, + { + "name": "Higher level constructs for Distribution", + "stability": "Experimental" + } + ], "awslint": { "exclude": [ "props-physical-name:@aws-cdk/aws-cloudfront.Distribution", diff --git a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts index f9fb050da8a2c..4ff1fdc456650 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts @@ -1,7 +1,7 @@ import '@aws-cdk/assert/jest'; import * as acm from '@aws-cdk/aws-certificatemanager'; import * as s3 from '@aws-cdk/aws-s3'; -import { App, Stack } from '@aws-cdk/core'; +import { App, Duration, Stack } from '@aws-cdk/core'; import { Distribution, Origin, PriceClass } from '../lib'; let app: App; @@ -15,23 +15,23 @@ beforeEach(() => { }); test('minimal example renders correctly', () => { - const origin = Origin.fromBucket(stack, 'MyOrigin', new s3.Bucket(stack, 'Bucket')); + const origin = Origin.fromBucket(new s3.Bucket(stack, 'Bucket')); new Distribution(stack, 'MyDist', { defaultBehavior: { origin } }); expect(stack).toHaveResource('AWS::CloudFront::Distribution', { DistributionConfig: { DefaultCacheBehavior: { ForwardedValues: { QueryString: false }, - TargetOriginId: 'MyOrigin', + TargetOriginId: 'StackMyDistOrigin1D6D5E535', ViewerProtocolPolicy: 'allow-all', }, Enabled: true, Origins: [{ DomainName: { 'Fn::GetAtt': [ 'Bucket83908E77', 'RegionalDomainName' ] }, - Id: 'MyOrigin', + Id: 'StackMyDistOrigin1D6D5E535', S3OriginConfig: { OriginAccessIdentity: { 'Fn::Join': [ '', - [ 'origin-access-identity/cloudfront/', { Ref: 'MyOriginS3OriginIdentityBEF16CC0' } ], + [ 'origin-access-identity/cloudfront/', { Ref: 'MyDistS3Origin1ED86A27E' } ], ]}, }, }], @@ -42,7 +42,7 @@ test('minimal example renders correctly', () => { describe('multiple behaviors', () => { test('a second behavior can\'t be specified with the catch-all path pattern', () => { - const origin = Origin.fromBucket(stack, 'MyOrigin', new s3.Bucket(stack, 'Bucket')); + const origin = Origin.fromBucket(new s3.Bucket(stack, 'Bucket')); expect(() => { new Distribution(stack, 'MyDist', { @@ -55,7 +55,7 @@ describe('multiple behaviors', () => { }); test('a second behavior can be added to the original origin', () => { - const origin = Origin.fromBucket(stack, 'MyOrigin', new s3.Bucket(stack, 'Bucket')); + const origin = Origin.fromBucket(new s3.Bucket(stack, 'Bucket')); new Distribution(stack, 'MyDist', { defaultBehavior: { origin }, additionalBehaviors: { @@ -67,22 +67,22 @@ describe('multiple behaviors', () => { DistributionConfig: { DefaultCacheBehavior: { ForwardedValues: { QueryString: false }, - TargetOriginId: 'MyOrigin', + TargetOriginId: 'StackMyDistOrigin1D6D5E535', ViewerProtocolPolicy: 'allow-all', }, CacheBehaviors: [{ PathPattern: 'api/*', ForwardedValues: { QueryString: false }, - TargetOriginId: 'MyOrigin', + TargetOriginId: 'StackMyDistOrigin1D6D5E535', ViewerProtocolPolicy: 'allow-all', }], Enabled: true, Origins: [{ DomainName: { 'Fn::GetAtt': [ 'Bucket83908E77', 'RegionalDomainName' ] }, - Id: 'MyOrigin', + Id: 'StackMyDistOrigin1D6D5E535', S3OriginConfig: { OriginAccessIdentity: { 'Fn::Join': [ '', - [ 'origin-access-identity/cloudfront/', { Ref: 'MyOriginS3OriginIdentityBEF16CC0' } ], + [ 'origin-access-identity/cloudfront/', { Ref: 'MyDistS3Origin1ED86A27E' } ], ]}, }, }], @@ -91,8 +91,8 @@ describe('multiple behaviors', () => { }); test('a second behavior can be added to a secondary origin', () => { - const origin = Origin.fromBucket(stack, 'MyOrigin', new s3.Bucket(stack, 'Bucket')); - const origin2 = Origin.fromBucket(stack, 'MyOrigin2', new s3.Bucket(stack, 'Bucket2')); + const origin = Origin.fromBucket(new s3.Bucket(stack, 'Bucket')); + const origin2 = Origin.fromBucket(new s3.Bucket(stack, 'Bucket2')); new Distribution(stack, 'MyDist', { defaultBehavior: { origin }, additionalBehaviors: { @@ -104,31 +104,31 @@ describe('multiple behaviors', () => { DistributionConfig: { DefaultCacheBehavior: { ForwardedValues: { QueryString: false }, - TargetOriginId: 'MyOrigin', + TargetOriginId: 'StackMyDistOrigin1D6D5E535', ViewerProtocolPolicy: 'allow-all', }, CacheBehaviors: [{ PathPattern: 'api/*', ForwardedValues: { QueryString: false }, - TargetOriginId: 'MyOrigin2', + TargetOriginId: 'StackMyDistOrigin20B96F3AD', ViewerProtocolPolicy: 'allow-all', }], Enabled: true, Origins: [{ DomainName: { 'Fn::GetAtt': [ 'Bucket83908E77', 'RegionalDomainName' ] }, - Id: 'MyOrigin', + Id: 'StackMyDistOrigin1D6D5E535', S3OriginConfig: { OriginAccessIdentity: { 'Fn::Join': [ '', - [ 'origin-access-identity/cloudfront/', { Ref: 'MyOriginS3OriginIdentityBEF16CC0' } ], + [ 'origin-access-identity/cloudfront/', { Ref: 'MyDistS3Origin1ED86A27E' } ], ]}, }, }, { DomainName: { 'Fn::GetAtt': [ 'Bucket25524B414', 'RegionalDomainName' ] }, - Id: 'MyOrigin2', + Id: 'StackMyDistOrigin20B96F3AD', S3OriginConfig: { OriginAccessIdentity: { 'Fn::Join': [ '', - [ 'origin-access-identity/cloudfront/', { Ref: 'MyOrigin2S3OriginIdentityB67B10D6' } ], + [ 'origin-access-identity/cloudfront/', { Ref: 'MyDistS3Origin2E88F08BB' } ], ]}, }, }], @@ -137,51 +137,51 @@ describe('multiple behaviors', () => { }); test('behavior creation order is preserved', () => { - const origin = Origin.fromBucket(stack, 'MyOrigin', new s3.Bucket(stack, 'Bucket')); - const origin2 = Origin.fromBucket(stack, 'MyOrigin2', new s3.Bucket(stack, 'Bucket2')); + const origin = Origin.fromBucket(new s3.Bucket(stack, 'Bucket')); + const origin2 = Origin.fromBucket(new s3.Bucket(stack, 'Bucket2')); const dist = new Distribution(stack, 'MyDist', { defaultBehavior: { origin }, additionalBehaviors: { 'api/1*': { origin: origin2 }, }, }); - dist.addBehavior('api/2*', { origin }); + dist.addBehavior('api/2*', origin); expect(stack).toHaveResource('AWS::CloudFront::Distribution', { DistributionConfig: { DefaultCacheBehavior: { ForwardedValues: { QueryString: false }, - TargetOriginId: 'MyOrigin', + TargetOriginId: 'StackMyDistOrigin1D6D5E535', ViewerProtocolPolicy: 'allow-all', }, CacheBehaviors: [{ PathPattern: 'api/1*', ForwardedValues: { QueryString: false }, - TargetOriginId: 'MyOrigin2', + TargetOriginId: 'StackMyDistOrigin20B96F3AD', ViewerProtocolPolicy: 'allow-all', }, { PathPattern: 'api/2*', ForwardedValues: { QueryString: false }, - TargetOriginId: 'MyOrigin', + TargetOriginId: 'StackMyDistOrigin1D6D5E535', ViewerProtocolPolicy: 'allow-all', }], Enabled: true, Origins: [{ DomainName: { 'Fn::GetAtt': [ 'Bucket83908E77', 'RegionalDomainName' ] }, - Id: 'MyOrigin', + Id: 'StackMyDistOrigin1D6D5E535', S3OriginConfig: { OriginAccessIdentity: { 'Fn::Join': [ '', - [ 'origin-access-identity/cloudfront/', { Ref: 'MyOriginS3OriginIdentityBEF16CC0' } ], + [ 'origin-access-identity/cloudfront/', { Ref: 'MyDistS3Origin1ED86A27E' } ], ]}, }, }, { DomainName: { 'Fn::GetAtt': [ 'Bucket25524B414', 'RegionalDomainName' ] }, - Id: 'MyOrigin2', + Id: 'StackMyDistOrigin20B96F3AD', S3OriginConfig: { OriginAccessIdentity: { 'Fn::Join': [ '', - [ 'origin-access-identity/cloudfront/', { Ref: 'MyOrigin2S3OriginIdentityB67B10D6' } ], + [ 'origin-access-identity/cloudfront/', { Ref: 'MyDistS3Origin2E88F08BB' } ], ]}, }, }], @@ -194,7 +194,7 @@ describe('multiple behaviors', () => { describe('certificates', () => { test('should fail if using an imported certificate from outside of us-east-1', () => { - const origin = Origin.fromBucket(stack, 'Origin', new s3.Bucket(stack, 'Bucket')); + const origin = Origin.fromBucket(new s3.Bucket(stack, 'Bucket')); const certificate = acm.Certificate.fromCertificateArn(stack, 'Cert', 'arn:aws:acm:eu-west-1:123456789012:certificate/12345678-1234-1234-1234-123456789012'); expect(() => { @@ -209,7 +209,7 @@ describe('certificates', () => { const certificate = acm.Certificate.fromCertificateArn(stack, 'Cert', 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012'); new Distribution(stack, 'Dist', { - defaultBehavior: { origin: Origin.fromBucket(stack, 'Origin', new s3.Bucket(stack, 'Bucket')) }, + defaultBehavior: { origin: Origin.fromBucket(new s3.Bucket(stack, 'Bucket')) }, certificate, }); @@ -226,7 +226,7 @@ describe('certificates', () => { describe('custom error responses', () => { test('should fail if responsePagePath is defined but responseCode is not', () => { - const origin = Origin.fromBucket(stack, 'Origin', new s3.Bucket(stack, 'Bucket')); + const origin = Origin.fromBucket(new s3.Bucket(stack, 'Bucket')); expect(() => { new Distribution(stack, 'Dist', { @@ -240,7 +240,7 @@ describe('custom error responses', () => { }); test('should fail if only the error code is provided', () => { - const origin = Origin.fromBucket(stack, 'Origin', new s3.Bucket(stack, 'Bucket')); + const origin = Origin.fromBucket(new s3.Bucket(stack, 'Bucket')); expect(() => { new Distribution(stack, 'Dist', { @@ -251,7 +251,7 @@ describe('custom error responses', () => { }); test('should render the array of error configs if provided', () => { - const origin = Origin.fromBucket(stack, 'Origin', new s3.Bucket(stack, 'Bucket')); + const origin = Origin.fromBucket(new s3.Bucket(stack, 'Bucket')); new Distribution(stack, 'Dist', { defaultBehavior: { origin }, errorConfigurations: [{ @@ -261,7 +261,7 @@ describe('custom error responses', () => { }, { errorCode: 500, - errorCachingMinTtl: 2, + errorCachingMinTtl: Duration.seconds(2), }], }); @@ -285,7 +285,7 @@ describe('custom error responses', () => { }); test('price class is included if provided', () => { - const origin = Origin.fromBucket(stack, 'Origin', new s3.Bucket(stack, 'Bucket')); + const origin = Origin.fromBucket(new s3.Bucket(stack, 'Bucket')); new Distribution(stack, 'Dist', { defaultBehavior: { origin }, priceClass: PriceClass.PRICE_CLASS_200, diff --git a/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts index d621e829d0142..4a79fd52d0598 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts @@ -15,13 +15,14 @@ beforeEach(() => { describe('fromBucket', () => { - test('renders all properties, including S3Origin config', () => { + test('as bucket, renders all properties, including S3Origin config', () => { const bucket = new s3.Bucket(stack, 'Bucket'); - const origin = Origin.fromBucket(stack, 'MyOrigin', bucket); + const origin = Origin.fromBucket(bucket); + origin.bind(stack, { originIndex: 0 }); expect(origin._renderOrigin()).toEqual({ - id: 'MyOrigin', + id: 'StackOrigin029E19582', domainName: bucket.bucketRegionalDomainName, s3OriginConfig: { originAccessIdentity: 'origin-access-identity/cloudfront/${Token[TOKEN.69]}', @@ -29,10 +30,10 @@ describe('fromBucket', () => { }); }); - test('creates an OriginAccessIdentity and grants read permissions on the bucket', () => { + test('as bucket, creates an OriginAccessIdentity and grants read permissions on the bucket', () => { const bucket = new s3.Bucket(stack, 'Bucket'); - const origin = Origin.fromBucket(stack, 'Origin', bucket); + const origin = Origin.fromBucket(bucket); new Distribution(stack, 'Dist', { defaultBehavior: { origin } }); expect(stack).toHaveResourceLike('AWS::CloudFront::CloudFrontOriginAccessIdentity', { @@ -44,27 +45,29 @@ describe('fromBucket', () => { PolicyDocument: { Statement: [{ Principal: { - CanonicalUser: { 'Fn::GetAtt': [ 'OriginS3OriginIdentity1E4900C6', 'S3CanonicalUserId' ] }, + CanonicalUser: { 'Fn::GetAtt': [ 'DistS3Origin1C4519663', 'S3CanonicalUserId' ] }, }, }], }, }); }); -}); + test('as website buvcket, renders all properties, including custom origin config', () => { + const bucket = new s3.Bucket(stack, 'Bucket', { + websiteIndexDocument: 'index.html', + }); -test('fromWebsiteBucket renders all properties, including custom origin config', () => { - const bucket = new s3.Bucket(stack, 'Bucket', { - websiteIndexDocument: 'index.html', + const origin = Origin.fromBucket(bucket); + origin.bind(stack, { originIndex: 0 }); + + expect(origin._renderOrigin()).toEqual({ + id: 'StackOrigin029E19582', + domainName: bucket.bucketWebsiteDomainName, + customOriginConfig: { + originProtocolPolicy: 'http-only', + }, + }); }); - const origin = Origin.fromWebsiteBucket(stack, 'MyOrigin', bucket); +}); - expect(origin._renderOrigin()).toEqual({ - id: 'MyOrigin', - domainName: bucket.bucketWebsiteDomainName, - customOriginConfig: { - originProtocolPolicy: 'http-only', - }, - }); -}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts b/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts index d49f41b316b38..110d3f295a889 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts @@ -15,10 +15,12 @@ beforeEach(() => { }); test('renders the minimum template with an origin and path specified', () => { + const origin = Origin.fromBucket(new s3.Bucket(stack, 'MyBucket')); const behavior = new CacheBehavior({ - origin: Origin.fromBucket(stack, 'MyOrigin', new s3.Bucket(stack, 'MyBucket')), + origin, pathPattern: '*', }); + origin.bind(stack, { originIndex: 0 }); expect(behavior._renderBehavior()).toEqual({ targetOriginId: behavior.origin.id, @@ -29,13 +31,15 @@ test('renders the minimum template with an origin and path specified', () => { }); test('renders with all properties specified', () => { + const origin = Origin.fromBucket(new s3.Bucket(stack, 'MyBucket')); const behavior = new CacheBehavior({ - origin: Origin.fromBucket(stack, 'MyOrigin', new s3.Bucket(stack, 'MyBucket')), + origin, pathPattern: '*', allowedMethods: AllowedMethods.ALLOW_ALL, forwardQueryString: true, forwardQueryStringCacheKeys: ['user_id', 'auth'], }); + origin.bind(stack, { originIndex: 0 }); expect(behavior._renderBehavior()).toEqual({ targetOriginId: behavior.origin.id, diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 03bf119e35574..028716557b50c 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -54,6 +54,13 @@ export interface IBucket extends IResource { */ readonly bucketRegionalDomainName: string; + /** + * If this bucket has been configured for static website hosting. + * + * @default false + */ + readonly isWebsite?: boolean; + /** * Optional KMS encryption key associated with this bucket. */ @@ -279,6 +286,13 @@ export interface BucketAttributes { readonly bucketWebsiteNewUrlFormat?: boolean; readonly encryptionKey?: kms.IKey; + + /** + * If this bucket has been configured for static website hosting. + * + * @default false + */ + readonly isWebsite?: boolean; } /** @@ -312,6 +326,11 @@ abstract class BucketBase extends Resource implements IBucket { */ public abstract readonly encryptionKey?: kms.IKey; + /** + * If this bucket has been configured for static website hosting. + */ + public abstract readonly isWebsite?: boolean; + /** * The resource policy associated with this bucket. * @@ -1003,6 +1022,7 @@ export class Bucket extends BucketBase { public readonly bucketDualStackDomainName = attrs.bucketDualStackDomainName || `${bucketName}.s3.dualstack.${region}.${urlSuffix}`; public readonly bucketWebsiteNewUrlFormat = newUrlFormat; public readonly encryptionKey = attrs.encryptionKey; + public readonly isWebsite = attrs.isWebsite ?? false; public policy?: BucketPolicy = undefined; protected autoCreatePolicy = false; protected disallowPublicAccess = false; @@ -1027,6 +1047,7 @@ export class Bucket extends BucketBase { public readonly bucketRegionalDomainName: string; public readonly encryptionKey?: kms.IKey; + public readonly isWebsite?: boolean; public policy?: BucketPolicy; protected autoCreatePolicy = true; protected disallowPublicAccess?: boolean; @@ -1046,12 +1067,15 @@ export class Bucket extends BucketBase { this.validateBucketName(this.physicalName); + const websiteConfiguration = this.renderWebsiteConfiguration(props); + this.isWebsite = (websiteConfiguration !== undefined); + const resource = new CfnBucket(this, 'Resource', { bucketName: this.physicalName, bucketEncryption, versioningConfiguration: props.versioned ? { status: 'Enabled' } : undefined, lifecycleConfiguration: Lazy.anyValue({ produce: () => this.parseLifecycleConfiguration() }), - websiteConfiguration: this.renderWebsiteConfiguration(props), + websiteConfiguration, publicAccessBlockConfiguration: props.blockPublicAccess, metricsConfigurations: Lazy.anyValue({ produce: () => this.parseMetricConfiguration() }), corsConfiguration: Lazy.anyValue({ produce: () => this.parseCorsConfiguration() }), diff --git a/packages/@aws-cdk/aws-s3/test/test.bucket.ts b/packages/@aws-cdk/aws-s3/test/test.bucket.ts index 89bcd0cbdc426..3f177974fdbc1 100644 --- a/packages/@aws-cdk/aws-s3/test/test.bucket.ts +++ b/packages/@aws-cdk/aws-s3/test/test.bucket.ts @@ -1797,6 +1797,59 @@ export = { }, /The condition property cannot be an empty object/); test.done(); }, + 'isWebsite set properly with': { + 'only index doc'(test: Test) { + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'Website', { + websiteIndexDocument: 'index2.html', + }); + test.equal(bucket.isWebsite, true); + test.done(); + }, + 'error and index docs'(test: Test) { + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'Website', { + websiteIndexDocument: 'index2.html', + websiteErrorDocument: 'error.html', + }); + test.equal(bucket.isWebsite, true); + test.done(); + }, + 'redirects'(test: Test) { + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'Website', { + websiteRedirect: { + hostName: 'www.example.com', + protocol: s3.RedirectProtocol.HTTPS, + }, + }); + test.equal(bucket.isWebsite, true); + test.done(); + }, + 'no website properties set'(test: Test) { + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'Website'); + test.equal(bucket.isWebsite, false); + test.done(); + }, + 'imported website buckets'(test: Test) { + const stack = new cdk.Stack(); + const bucket = s3.Bucket.fromBucketAttributes(stack, 'Website', { + bucketArn: 'arn:aws:s3:::my-bucket', + isWebsite: true, + }); + test.equal(bucket.isWebsite, true); + test.done(); + }, + 'imported buckets'(test: Test) { + const stack = new cdk.Stack(); + const bucket = s3.Bucket.fromBucketAttributes(stack, 'NotWebsite', { + bucketArn: 'arn:aws:s3:::my-bucket', + }); + test.equal(bucket.isWebsite, false); + test.done(); + }, + }, }, 'Bucket.fromBucketArn'(test: Test) { From 5e36228cf8fa18d634dcfab0c08d6295ad061790 Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Wed, 15 Jul 2020 13:24:53 +0100 Subject: [PATCH 12/15] Flipped README order to put Distribution first --- packages/@aws-cdk/aws-cloudfront/README.md | 200 +++++++++--------- packages/@aws-cdk/aws-cloudfront/package.json | 8 +- 2 files changed, 105 insertions(+), 103 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront/README.md b/packages/@aws-cdk/aws-cloudfront/README.md index 398db818c869b..f008ec6679c48 100644 --- a/packages/@aws-cdk/aws-cloudfront/README.md +++ b/packages/@aws-cdk/aws-cloudfront/README.md @@ -6,8 +6,8 @@ | Features | Stability | | --- | --- | | CFN Resources | ![Stable](https://img.shields.io/badge/stable-success.svg?style=for-the-badge) | -| Higher level constructs for CloudFrontWebDistribution | ![Stable](https://img.shields.io/badge/stable-success.svg?style=for-the-badge) | | Higher level constructs for Distribution | ![Experimental](https://img.shields.io/badge/experimental-important.svg?style=for-the-badge) | +| Higher level constructs for CloudFrontWebDistribution | ![Stable](https://img.shields.io/badge/stable-success.svg?style=for-the-badge) | > **CFN Resources:** All classes with the `Cfn` prefix in this module ([CFN Resources](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) are always stable and safe to use. @@ -23,108 +23,14 @@ your users. CloudFront delivers your content through a worldwide network of data you're serving with CloudFront, the user is routed to the edge location that provides the lowest latency, so that content is delivered with the best possible performance. -## CloudFrontWebDistribution API - Stable - -![cdk-constructs: Stable](https://img.shields.io/badge/cdk--constructs-stable-success.svg?style=for-the-badge) - -A CloudFront construct - for setting up the AWS CDN with ease! - -Example usage: - -```ts -const sourceBucket = new Bucket(this, 'Bucket'); - -const distribution = new CloudFrontWebDistribution(this, 'MyDistribution', { - originConfigs: [ - { - s3OriginSource: { - s3BucketSource: sourceBucket - }, - behaviors : [ {isDefaultBehavior: true}] - } - ] - }); -``` - -### Viewer certificate - -By default, CloudFront Web Distributions will answer HTTPS requests with CloudFront's default certificate, only containing the distribution `domainName` (e.g. d111111abcdef8.cloudfront.net). -You can customize the viewer certificate property to provide a custom certificate and/or list of domain name aliases to fit your needs. - -See [Using Alternate Domain Names and HTTPS](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-https-alternate-domain-names.html) in the CloudFront User Guide. - -#### Default certificate - -You can customize the default certificate aliases. This is intended to be used in combination with CNAME records in your DNS zone. - -Example: - -[create a distrubution with an default certificiate example](test/example.default-cert-alias.lit.ts) - -#### ACM certificate - -You can change the default certificate by one stored AWS Certificate Manager, or ACM. -Those certificate can either be generated by AWS, or purchased by another CA imported into ACM. - -For more information, see [the aws-certificatemanager module documentation](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-certificatemanager-readme.html) or [Importing Certificates into AWS Certificate Manager](https://docs.aws.amazon.com/acm/latest/userguide/import-certificate.html) in the AWS Certificate Manager User Guide. - -Example: - -[create a distrubution with an acm certificate example](test/example.acm-cert-alias.lit.ts) - -#### IAM certificate - -You can also import a certificate into the IAM certificate store. - -See [Importing an SSL/TLS Certificate](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cnames-and-https-procedures.html#cnames-and-https-uploading-certificates) in the CloudFront User Guide. - -Example: - -[create a distrubution with an iam certificate example](test/example.iam-cert-alias.lit.ts) - -#### Restrictions - -CloudFront supports adding restrictions to your distribution. - -See [Restricting the Geographic Distribution of Your Content](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/georestrictions.html) in the CloudFront User Guide. - -Example: - -```ts -new cloudfront.CloudFrontWebDistribution(stack, 'MyDistribution', { - //... - geoRestriction: GeoRestriction.whitelist('US', 'UK') -}); -``` - -### Connection behaviors between CloudFront and your origin - -CloudFront provides you even more control over the connection behaviors between CloudFront and your origin. You can now configure the number of connection attempts CloudFront will make to your origin and the origin connection timeout for each attempt. - -See [Origin Connection Attempts](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#origin-connection-attempts) - -See [Origin Connection Timeout](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#origin-connection-timeout) - -Example usage: - -```ts -const distribution = new CloudFrontWebDistribution(this, 'MyDistribution', { - originConfigs: [ - { - ..., - connectionAttempts: 3, - connectionTimeout: cdk.Duration.seconds(10), - } - ] -}); -``` - ## Distribution API - Experimental ![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) -In addition to the APIs listed above, a new construct (`Distribution`) is under active development as a newer, friendlier way to define CloudFront -distributions. +The `Distribution` API is currently being built to replace the existing `CloudFrontWebDistribution` API. The `Distribution` API is optimized for the +most common use cases of CloudFront distributions (e.g., single origin and behavior, few customizations) while still providing the ability for more +advanced use cases. The API focuses on simplicity for the common use cases, and convenience methods for creating the behaviors and origins necessary +for more complex use cases. ### Creating a distribution @@ -221,3 +127,99 @@ new cloudfront.Distribution(this, 'myDist', { }, }); ``` + +## CloudFrontWebDistribution API - Stable + +![cdk-constructs: Stable](https://img.shields.io/badge/cdk--constructs-stable-success.svg?style=for-the-badge) + +A CloudFront construct - for setting up the AWS CDN with ease! + +Example usage: + +```ts +const sourceBucket = new Bucket(this, 'Bucket'); + +const distribution = new CloudFrontWebDistribution(this, 'MyDistribution', { + originConfigs: [ + { + s3OriginSource: { + s3BucketSource: sourceBucket + }, + behaviors : [ {isDefaultBehavior: true}] + } + ] + }); +``` + +### Viewer certificate + +By default, CloudFront Web Distributions will answer HTTPS requests with CloudFront's default certificate, only containing the distribution `domainName` (e.g. d111111abcdef8.cloudfront.net). +You can customize the viewer certificate property to provide a custom certificate and/or list of domain name aliases to fit your needs. + +See [Using Alternate Domain Names and HTTPS](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-https-alternate-domain-names.html) in the CloudFront User Guide. + +#### Default certificate + +You can customize the default certificate aliases. This is intended to be used in combination with CNAME records in your DNS zone. + +Example: + +[create a distrubution with an default certificiate example](test/example.default-cert-alias.lit.ts) + +#### ACM certificate + +You can change the default certificate by one stored AWS Certificate Manager, or ACM. +Those certificate can either be generated by AWS, or purchased by another CA imported into ACM. + +For more information, see [the aws-certificatemanager module documentation](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-certificatemanager-readme.html) or [Importing Certificates into AWS Certificate Manager](https://docs.aws.amazon.com/acm/latest/userguide/import-certificate.html) in the AWS Certificate Manager User Guide. + +Example: + +[create a distrubution with an acm certificate example](test/example.acm-cert-alias.lit.ts) + +#### IAM certificate + +You can also import a certificate into the IAM certificate store. + +See [Importing an SSL/TLS Certificate](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cnames-and-https-procedures.html#cnames-and-https-uploading-certificates) in the CloudFront User Guide. + +Example: + +[create a distrubution with an iam certificate example](test/example.iam-cert-alias.lit.ts) + +#### Restrictions + +CloudFront supports adding restrictions to your distribution. + +See [Restricting the Geographic Distribution of Your Content](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/georestrictions.html) in the CloudFront User Guide. + +Example: + +```ts +new cloudfront.CloudFrontWebDistribution(stack, 'MyDistribution', { + //... + geoRestriction: GeoRestriction.whitelist('US', 'UK') +}); +``` + +### Connection behaviors between CloudFront and your origin + +CloudFront provides you even more control over the connection behaviors between CloudFront and your origin. You can now configure the number of connection attempts CloudFront will make to your origin and the origin connection timeout for each attempt. + +See [Origin Connection Attempts](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#origin-connection-attempts) + +See [Origin Connection Timeout](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#origin-connection-timeout) + +Example usage: + +```ts +const distribution = new CloudFrontWebDistribution(this, 'MyDistribution', { + originConfigs: [ + { + ..., + connectionAttempts: 3, + connectionTimeout: cdk.Duration.seconds(10), + } + ] +}); +``` diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index 73c7ff344e848..2d8f201021ba5 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -95,13 +95,13 @@ }, "stability": "experimental", "features": [ - { - "name": "Higher level constructs for CloudFrontWebDistribution", - "stability": "Stable" - }, { "name": "Higher level constructs for Distribution", "stability": "Experimental" + }, + { + "name": "Higher level constructs for CloudFrontWebDistribution", + "stability": "Stable" } ], "awslint": { From 886af07cd5d538374f9a301bff3ec39c5dc3ac7f Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Wed, 15 Jul 2020 13:40:39 +0100 Subject: [PATCH 13/15] Apply suggestions from code review Suggestions from review. Co-authored-by: Elad Ben-Israel --- packages/@aws-cdk/aws-cloudfront/lib/distribution.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index 5d1be4a7179e5..1241886b0c93c 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -75,7 +75,7 @@ export interface DistributionProps { * If you specify a price class other than PriceClass_All, CloudFront serves your objects from the CloudFront edge location * that has the lowest latency among the edge locations in your price class. * - * @default PRICE_CLASS_ALL + * @default PriceClass.PRICE_CLASS_ALL */ readonly priceClass?: PriceClass; @@ -126,7 +126,7 @@ export class Distribution extends Resource implements IDistribution { if (props.certificate) { const certificateRegion = Stack.of(this).parseArn(props.certificate.certificateArn).region; if (!Token.isUnresolved(certificateRegion) && certificateRegion !== 'us-east-1') { - throw new Error('Distribution certificates must be in the us-east-1 region.'); + throw new Error('Distribution certificates must be in the us-east-1 region and the certificate you provided is in $Region.'); } } @@ -266,7 +266,7 @@ export interface CustomErrorResponse { * * @default the default caching TTL behavior applies */ - readonly errorCachingMinTtl?: Duration; + readonly ttl?: Duration; /** * The HTTP status code for which you want to specify a custom error page and/or a caching duration. */ @@ -276,7 +276,7 @@ export interface CustomErrorResponse { * * If you specify a value for `responseCode`, you must also specify a value for `responsePagePath`. * - * @default not set, the error code will be returned as the response code. + * @default - not set, the error code will be returned as the response code. */ readonly responseCode?: number; /** @@ -329,4 +329,4 @@ export interface BehaviorOptions extends AddBehaviorOptions { * The origin that you want CloudFront to route requests to when they match this behavior. */ readonly origin: Origin; -} \ No newline at end of file +} From 4466043e4e4620438d5d63bd6b5917de73aafccf Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Wed, 15 Jul 2020 14:01:51 +0100 Subject: [PATCH 14/15] More PR comments updates --- .../aws-cloudfront/lib/distribution.ts | 64 +++++++++++-------- .../@aws-cdk/aws-cloudfront/lib/origin.ts | 25 ++++---- packages/@aws-cdk/aws-cloudfront/package.json | 8 --- .../aws-cloudfront/test/distribution.test.ts | 16 ++--- .../aws-cloudfront/test/origin.test.ts | 4 +- .../test/private/cache-behavior.test.ts | 4 +- packages/@aws-cdk/aws-s3/lib/bucket.ts | 2 - 7 files changed, 62 insertions(+), 61 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index 1241886b0c93c..561a7288f936f 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -58,14 +58,14 @@ export interface DistributionProps { /** * Additional behaviors for the distribution, mapped by the pathPattern that specifies which requests to apply the behavior to. * - * @default no additional behaviors are added. + * @default - no additional behaviors are added. */ readonly additionalBehaviors?: Record; /** * A certificate to associate with the distribution. The certificate must be located in N. Virginia (us-east-1). * - * @default the CloudFront wildcard certificate (*.cloudfront.net) will be used. + * @default - the CloudFront wildcard certificate (*.cloudfront.net) will be used. */ readonly certificate?: acm.ICertificate; @@ -82,9 +82,9 @@ export interface DistributionProps { /** * How CloudFront should handle requests that are not successful (e.g., PageNotFound). * - * @default - No custom error configuration. + * @default - No custom error responses. */ - readonly errorConfigurations?: CustomErrorResponse[]; + readonly errorResponses?: ErrorResponse[]; } /** @@ -117,7 +117,7 @@ export class Distribution extends Resource implements IDistribution { private readonly additionalBehaviors: CacheBehavior[] = []; private readonly origins: Set = new Set(); - private readonly errorConfigurations: CustomErrorResponse[]; + private readonly errorResponses: ErrorResponse[]; private readonly certificate?: acm.ICertificate; constructor(scope: Construct, id: string, props: DistributionProps) { @@ -139,7 +139,7 @@ export class Distribution extends Resource implements IDistribution { } this.certificate = props.certificate; - this.errorConfigurations = props.errorConfigurations ?? []; + this.errorResponses = props.errorResponses ?? []; const distribution = new CfnDistribution(this, 'CFDistribution', { distributionConfig: { enabled: true, @@ -147,7 +147,7 @@ export class Distribution extends Resource implements IDistribution { defaultCacheBehavior: this.defaultBehavior._renderBehavior(), cacheBehaviors: Lazy.anyValue({ produce: () => this.renderCacheBehaviors() }), viewerCertificate: this.certificate ? { acmCertificateArn: this.certificate.certificateArn } : undefined, - customErrorResponses: this.renderCustomErrorResponses(), + customErrorResponses: this.renderErrorResponses(), priceClass: props.priceClass ?? undefined, } }); @@ -172,7 +172,7 @@ export class Distribution extends Resource implements IDistribution { private addOrigin(origin: Origin) { if (!this.origins.has(origin)) { this.origins.add(origin); - origin.bind(this, { originIndex: this.origins.size }); + origin._bind(this, { originIndex: this.origins.size }); } } @@ -187,23 +187,23 @@ export class Distribution extends Resource implements IDistribution { return this.additionalBehaviors.map(behavior => behavior._renderBehavior()); } - private renderCustomErrorResponses(): CfnDistribution.CustomErrorResponseProperty[] | undefined { - if (this.errorConfigurations.length === 0) { return undefined; } - function validateCustomErrorResponse(errorResponse: CustomErrorResponse) { - if (errorResponse.responsePagePath && !errorResponse.responseCode) { + private renderErrorResponses(): CfnDistribution.CustomErrorResponseProperty[] | undefined { + if (this.errorResponses.length === 0) { return undefined; } + function validateCustomErrorResponse(errorResponse: ErrorResponse) { + if (errorResponse.responsePagePath && !errorResponse.responseHttpStatus) { throw new Error('\'responseCode\' must be provided if \'responsePagePath\' is defined'); } - if (!errorResponse.responseCode && !errorResponse.errorCachingMinTtl) { + if (!errorResponse.responseHttpStatus && !errorResponse.ttl) { throw new Error('A custom error response without either a \'responseCode\' or \'errorCachingMinTtl\' is not valid.'); } } - this.errorConfigurations.forEach(e => validateCustomErrorResponse(e)); + this.errorResponses.forEach(e => validateCustomErrorResponse(e)); - return this.errorConfigurations.map(errorConfig => { + return this.errorResponses.map(errorConfig => { return { - errorCachingMinTtl: errorConfig.errorCachingMinTtl?.toSeconds(), - errorCode: errorConfig.errorCode, - responseCode: errorConfig.responseCode, + errorCachingMinTtl: errorConfig.ttl?.toSeconds(), + errorCode: errorConfig.httpStatus, + responseCode: errorConfig.responseHttpStatus, responsePagePath: errorConfig.responsePagePath, }; }); @@ -213,10 +213,14 @@ export class Distribution extends Resource implements IDistribution { /** * The price class determines how many edge locations CloudFront will use for your distribution. + * See https://aws.amazon.com/cloudfront/pricing/ for full list of supported regions. */ export enum PriceClass { + /** USA, Canada, Europe, & Israel */ PRICE_CLASS_100 = 'PriceClass_100', + /** PRICE_CLASS_100 + South Africa, Kenya, Middle East, Japan, Singapore, South Korea, Taiwan, Hong Kong, & Philippines */ PRICE_CLASS_200 = 'PriceClass_200', + /** All locations */ PRICE_CLASS_ALL = 'PriceClass_All' } @@ -224,8 +228,11 @@ export enum PriceClass { * How HTTPs should be handled with your distribution. */ export enum ViewerProtocolPolicy { + /** HTTPS only */ HTTPS_ONLY = 'https-only', + /** Will redirect HTTP requests to HTTPS */ REDIRECT_TO_HTTPS = 'redirect-to-https', + /** Both HTTP and HTTPS supported */ ALLOW_ALL = 'allow-all' } @@ -233,8 +240,11 @@ export enum ViewerProtocolPolicy { * Defines what protocols CloudFront will use to connect to an origin. */ export enum OriginProtocolPolicy { + /** Connect on HTTP only */ HTTP_ONLY = 'http-only', + /** Connect with the same protocol as the viewer */ MATCH_VIEWER = 'match-viewer', + /** Connect on HTTPS only */ HTTPS_ONLY = 'https-only', } @@ -260,30 +270,30 @@ export class AllowedMethods { * * @experimental */ -export interface CustomErrorResponse { +export interface ErrorResponse { /** * The minimum amount of time, in seconds, that you want CloudFront to cache the HTTP status code specified in ErrorCode. * - * @default the default caching TTL behavior applies + * @default - the default caching TTL behavior applies */ readonly ttl?: Duration; /** * The HTTP status code for which you want to specify a custom error page and/or a caching duration. */ - readonly errorCode: number; + readonly httpStatus: number; /** * The HTTP status code that you want CloudFront to return to the viewer along with the custom error page. * - * If you specify a value for `responseCode`, you must also specify a value for `responsePagePath`. + * If you specify a value for `responseHttpStatus`, you must also specify a value for `responsePagePath`. * * @default - not set, the error code will be returned as the response code. */ - readonly responseCode?: number; + readonly responseHttpStatus?: number; /** * The path to the custom error page that you want CloudFront to return to a viewer when your origin returns the - * `errorCode`, for example, /4xx-errors/403-forbidden.html + * `httpStatus`, for example, /4xx-errors/403-forbidden.html * - * @default the default CloudFront response is shown. + * @default - the default CloudFront response is shown. */ readonly responsePagePath?: string; } @@ -297,7 +307,7 @@ export interface AddBehaviorOptions { /** * HTTP methods to allow for this behavior. * - * @default GET and HEAD + * @default - GET and HEAD */ readonly allowedMethods?: AllowedMethods; @@ -314,7 +324,7 @@ export interface AddBehaviorOptions { /** * A set of query string parameter names to use for caching if `forwardQueryString` is set to true. * - * @default empty list + * @default [] */ readonly forwardQueryStringCacheKeys?: string[]; } diff --git a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts index 04ab180e359ea..f876c71bdf4b2 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts @@ -20,7 +20,7 @@ export interface OriginProps { /** * Options passed to Origin.bind(). */ -export interface OriginBindOptions { +interface OriginBindOptions { /** * The positional index of this origin within the distribution. Used for ensuring unique IDs. */ @@ -65,15 +65,6 @@ export abstract class Origin { this.domainName = props.domainName; } - /** - * Binds the origin to the associated Distribution. Can be used to grant permissions, create dependent resources, etc. - * - * @param scope the distribution to bind this Origin to. - */ - public bind(scope: Construct, options: OriginBindOptions): void { - this.originId = new Construct(scope, `Origin${options.originIndex}`).node.uniqueId; - } - /** * The unique id for this origin. * @@ -84,6 +75,15 @@ export abstract class Origin { return this.originId; } + /** + * Binds the origin to the associated Distribution. Can be used to grant permissions, create dependent resources, etc. + * + * @internal + */ + public _bind(scope: Construct, options: OriginBindOptions): void { + this.originId = new Construct(scope, `Origin${options.originIndex}`).node.uniqueId; + } + /** * Creates and returns the CloudFormation representation of this origin. * @@ -145,8 +145,9 @@ export class S3Origin extends Origin { this.bucket = props.bucket; } - public bind(scope: Construct, options: OriginBindOptions) { - super.bind(scope, options); + /** @internal */ + public _bind(scope: Construct, options: OriginBindOptions) { + super._bind(scope, options); if (!this.originAccessIdentity) { this.originAccessIdentity = new OriginAccessIdentity(scope, `S3Origin${options.originIndex}`); this.bucket.grantRead(this.originAccessIdentity); diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index 2d8f201021ba5..2bf99ded0c08c 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -111,7 +111,6 @@ "props-physical-name:@aws-cdk/aws-cloudfront.CloudFrontWebDistribution", "props-physical-name:@aws-cdk/aws-cloudfront.CloudFrontWebDistributionProps", "props-physical-name:@aws-cdk/aws-cloudfront.OriginAccessIdentityProps", - "docs-public-apis:@aws-cdk/aws-cloudfront.OriginProtocolPolicy", "docs-public-apis:@aws-cdk/aws-cloudfront.ViewerProtocolPolicy.ALLOW_ALL", "docs-public-apis:@aws-cdk/aws-cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS", "props-default-doc:@aws-cdk/aws-cloudfront.Behavior.isDefaultBehavior", @@ -131,18 +130,11 @@ "docs-public-apis:@aws-cdk/aws-cloudfront.HttpVersion.HTTP1_1", "docs-public-apis:@aws-cdk/aws-cloudfront.HttpVersion.HTTP2", "docs-public-apis:@aws-cdk/aws-cloudfront.LambdaEdgeEventType", - "docs-public-apis:@aws-cdk/aws-cloudfront.ViewerProtocolPolicy.HTTPS_ONLY", - "docs-public-apis:@aws-cdk/aws-cloudfront.OriginProtocolPolicy.HTTP_ONLY", - "docs-public-apis:@aws-cdk/aws-cloudfront.OriginProtocolPolicy.MATCH_VIEWER", - "docs-public-apis:@aws-cdk/aws-cloudfront.OriginProtocolPolicy.HTTPS_ONLY", "docs-public-apis:@aws-cdk/aws-cloudfront.OriginSslPolicy", "docs-public-apis:@aws-cdk/aws-cloudfront.OriginSslPolicy.SSL_V3", "docs-public-apis:@aws-cdk/aws-cloudfront.OriginSslPolicy.TLS_V1", "docs-public-apis:@aws-cdk/aws-cloudfront.OriginSslPolicy.TLS_V1_1", "docs-public-apis:@aws-cdk/aws-cloudfront.OriginSslPolicy.TLS_V1_2", - "docs-public-apis:@aws-cdk/aws-cloudfront.PriceClass.PRICE_CLASS_100", - "docs-public-apis:@aws-cdk/aws-cloudfront.PriceClass.PRICE_CLASS_200", - "docs-public-apis:@aws-cdk/aws-cloudfront.PriceClass.PRICE_CLASS_ALL", "docs-public-apis:@aws-cdk/aws-cloudfront.SSLMethod.SNI", "docs-public-apis:@aws-cdk/aws-cloudfront.SSLMethod.VIP", "docs-public-apis:@aws-cdk/aws-cloudfront.SecurityPolicyProtocol.SSL_V3", diff --git a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts index 4ff1fdc456650..86c91c2254cf8 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts @@ -231,8 +231,8 @@ describe('custom error responses', () => { expect(() => { new Distribution(stack, 'Dist', { defaultBehavior: { origin }, - errorConfigurations: [{ - errorCode: 404, + errorResponses: [{ + httpStatus: 404, responsePagePath: '/errors/404.html', }], }); @@ -245,7 +245,7 @@ describe('custom error responses', () => { expect(() => { new Distribution(stack, 'Dist', { defaultBehavior: { origin }, - errorConfigurations: [{ errorCode: 404 }], + errorResponses: [{ httpStatus: 404 }], }); }).toThrow(/A custom error response without either a \'responseCode\' or \'errorCachingMinTtl\' is not valid./); }); @@ -254,14 +254,14 @@ describe('custom error responses', () => { const origin = Origin.fromBucket(new s3.Bucket(stack, 'Bucket')); new Distribution(stack, 'Dist', { defaultBehavior: { origin }, - errorConfigurations: [{ - errorCode: 404, - responseCode: 404, + errorResponses: [{ + httpStatus: 404, + responseHttpStatus: 404, responsePagePath: '/errors/404.html', }, { - errorCode: 500, - errorCachingMinTtl: Duration.seconds(2), + httpStatus: 500, + ttl: Duration.seconds(2), }], }); diff --git a/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts index 4a79fd52d0598..b02a10e6300db 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts @@ -19,7 +19,7 @@ describe('fromBucket', () => { const bucket = new s3.Bucket(stack, 'Bucket'); const origin = Origin.fromBucket(bucket); - origin.bind(stack, { originIndex: 0 }); + origin._bind(stack, { originIndex: 0 }); expect(origin._renderOrigin()).toEqual({ id: 'StackOrigin029E19582', @@ -58,7 +58,7 @@ describe('fromBucket', () => { }); const origin = Origin.fromBucket(bucket); - origin.bind(stack, { originIndex: 0 }); + origin._bind(stack, { originIndex: 0 }); expect(origin._renderOrigin()).toEqual({ id: 'StackOrigin029E19582', diff --git a/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts b/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts index 110d3f295a889..b70d3ef02c590 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts @@ -20,7 +20,7 @@ test('renders the minimum template with an origin and path specified', () => { origin, pathPattern: '*', }); - origin.bind(stack, { originIndex: 0 }); + origin._bind(stack, { originIndex: 0 }); expect(behavior._renderBehavior()).toEqual({ targetOriginId: behavior.origin.id, @@ -39,7 +39,7 @@ test('renders with all properties specified', () => { forwardQueryString: true, forwardQueryStringCacheKeys: ['user_id', 'auth'], }); - origin.bind(stack, { originIndex: 0 }); + origin._bind(stack, { originIndex: 0 }); expect(behavior._renderBehavior()).toEqual({ targetOriginId: behavior.origin.id, diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 028716557b50c..9c49dde1be853 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -56,8 +56,6 @@ export interface IBucket extends IResource { /** * If this bucket has been configured for static website hosting. - * - * @default false */ readonly isWebsite?: boolean; From fbbda7297e005d945ce014e366d8a3807b739dc4 Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Wed, 15 Jul 2020 14:55:27 +0100 Subject: [PATCH 15/15] Soft-deprecating IDistribution.domainName --- packages/@aws-cdk/aws-cloudfront/README.md | 8 ++++---- packages/@aws-cdk/aws-cloudfront/lib/distribution.ts | 12 ++++++++++++ .../@aws-cdk/aws-cloudfront/lib/web_distribution.ts | 12 +++++++++++- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudfront/README.md b/packages/@aws-cdk/aws-cloudfront/README.md index f008ec6679c48..0ceb2f310733a 100644 --- a/packages/@aws-cdk/aws-cloudfront/README.md +++ b/packages/@aws-cdk/aws-cloudfront/README.md @@ -62,10 +62,10 @@ URLs and not S3 URLs directly. ### Domain Names and Certificates When you create a distribution, CloudFront assigns a domain name for the distribution, for example: `d111111abcdef8.cloudfront.net`; this value can -be retrieved from `distribution.domainName`. CloudFront distributions use a default certificate (`*.cloudfront.net`) to support HTTPS by default. If -you want to use your own domain name, such as `www.example.com`, you must associate a certificate with your distribution that contains your domain -name. The certificate must be present in the AWS Certificate Manager (ACM) service in the US East (N. Virginia) region; the certificate may either be -created by ACM, or created elsewhere and imported into ACM. +be retrieved from `distribution.distributionDomainName`. CloudFront distributions use a default certificate (`*.cloudfront.net`) to support HTTPS by +default. If you want to use your own domain name, such as `www.example.com`, you must associate a certificate with your distribution that contains +your domain name. The certificate must be present in the AWS Certificate Manager (ACM) service in the US East (N. Virginia) region; the certificate +may either be created by ACM, or created elsewhere and imported into ACM. ```ts const myCertificate = new acm.DnsValidatedCertificate(this, 'mySiteCert', { diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index 561a7288f936f..2290a8c366a2d 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -12,9 +12,17 @@ export interface IDistribution extends IResource { * The domain name of the Distribution, such as d111111abcdef8.cloudfront.net. * * @attribute + * @deprecated - Use `distributionDomainName` instead. */ readonly domainName: string; + /** + * The domain name of the Distribution, such as d111111abcdef8.cloudfront.net. + * + * @attribute + */ + readonly distributionDomainName: string; + /** * The distribution ID for this distribution. * @@ -100,17 +108,20 @@ export class Distribution extends Resource implements IDistribution { public static fromDistributionAttributes(scope: Construct, id: string, attrs: DistributionAttributes): IDistribution { return new class extends Resource implements IDistribution { public readonly domainName: string; + public readonly distributionDomainName: string; public readonly distributionId: string; constructor() { super(scope, id); this.domainName = attrs.domainName; + this.distributionDomainName = attrs.domainName; this.distributionId = attrs.distributionId; } }(); } public readonly domainName: string; + public readonly distributionDomainName: string; public readonly distributionId: string; private readonly defaultBehavior: CacheBehavior; @@ -152,6 +163,7 @@ export class Distribution extends Resource implements IDistribution { } }); this.domainName = distribution.attrDomainName; + this.distributionDomainName = distribution.attrDomainName; this.distributionId = distribution.ref; } diff --git a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts index 96f4207eb1401..e25bfce74409a 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts @@ -677,10 +677,19 @@ export class CloudFrontWebDistribution extends cdk.Resource implements IDistribu /** * The domain name created by CloudFront for this distribution. * If you are using aliases for your distribution, this is the domainName your DNS records should point to. - * (In Route53, you could create an ALIAS record to this value, for example. ) + * (In Route53, you could create an ALIAS record to this value, for example.) + * + * @deprecated - Use `distributionDomainName` instead. */ public readonly domainName: string; + /** + * The domain name created by CloudFront for this distribution. + * If you are using aliases for your distribution, this is the domainName your DNS records should point to. + * (In Route53, you could create an ALIAS record to this value, for example.) + */ + public readonly distributionDomainName: string; + /** * The distribution ID for this distribution. */ @@ -897,6 +906,7 @@ export class CloudFrontWebDistribution extends cdk.Resource implements IDistribu const distribution = new CfnDistribution(this, 'CFDistribution', { distributionConfig }); this.node.defaultChild = distribution; this.domainName = distribution.attrDomainName; + this.distributionDomainName = distribution.attrDomainName; this.distributionId = distribution.ref; }