packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts (530 lines of code) (raw):

import { Construct } from 'constructs'; import { CfnDistribution } from './cloudfront.generated'; import { HttpVersion, IDistribution, LambdaEdgeEventType, OriginProtocolPolicy, PriceClass, ViewerProtocolPolicy, SSLMethod, SecurityPolicyProtocol } from './distribution'; import { FunctionAssociation } from './function'; import { GeoRestriction } from './geo-restriction'; import { IKeyGroup } from './key-group'; import { IOriginAccessIdentity } from './origin-access-identity'; import { formatDistributionArn } from './private/utils'; import * as certificatemanager from '../../aws-certificatemanager'; import * as iam from '../../aws-iam'; import * as lambda from '../../aws-lambda'; import * as s3 from '../../aws-s3'; import * as cdk from '../../core'; import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource'; /** * HTTP status code to failover to second origin */ export enum FailoverStatusCode { /** * Forbidden (403) */ FORBIDDEN = 403, /** * Not found (404) */ NOT_FOUND = 404, /** * Internal Server Error (500) */ INTERNAL_SERVER_ERROR = 500, /** * Bad Gateway (502) */ BAD_GATEWAY = 502, /** * Service Unavailable (503) */ SERVICE_UNAVAILABLE = 503, /** * Gateway Timeout (504) */ GATEWAY_TIMEOUT = 504, } /** * Configuration for custom domain names * * CloudFront can use a custom domain that you provide instead of a * "cloudfront.net" domain. To use this feature you must provide the list of * additional domains, and the ACM Certificate that CloudFront should use for * these additional domains. * @deprecated see `CloudFrontWebDistributionProps#viewerCertificate` with `ViewerCertificate#acmCertificate` */ export interface AliasConfiguration { /** * ARN of an AWS Certificate Manager (ACM) certificate. */ readonly acmCertRef: string; /** * Domain names on the certificate * * Both main domain name and Subject Alternative Names. */ readonly names: string[]; /** * How CloudFront should serve HTTPS requests. * * See the notes on SSLMethod if you wish to use other SSL termination types. * * @default SSLMethod.SNI * @see https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_ViewerCertificate.html */ readonly sslMethod?: SSLMethod; /** * The minimum version of the SSL protocol that you want CloudFront to use for HTTPS connections. * * CloudFront serves your objects only to browsers or devices that support at * least the SSL version that you specify. * * @default - SSLv3 if sslMethod VIP, TLSv1 if sslMethod SNI */ readonly securityPolicy?: SecurityPolicyProtocol; } /** * Logging configuration for incoming requests */ export interface LoggingConfiguration { /** * Bucket to log requests to * * @default - A logging bucket is automatically created. */ readonly bucket?: s3.IBucket; /** * Whether to include the cookies in the logs * * @default false */ readonly includeCookies?: boolean; /** * Where in the bucket to store logs * * @default - No prefix. */ readonly prefix?: string; } // Subset of SourceConfiguration for rendering properties internally interface SourceConfigurationRender { readonly connectionAttempts?: number; readonly connectionTimeout?: cdk.Duration; readonly s3OriginSource?: S3OriginConfig; readonly customOriginSource?: CustomOriginConfig; readonly originPath?: string; readonly originHeaders?: { [key: string]: string }; readonly originShieldRegion?: string; } /** * A source configuration is a wrapper for CloudFront origins and behaviors. * An origin is what CloudFront will "be in front of" - that is, CloudFront will pull its assets from an origin. * * If you're using s3 as a source - pass the `s3Origin` property, otherwise, pass the `customOriginSource` property. * * One or the other must be passed, and it is invalid to pass both in the same SourceConfiguration. */ export interface SourceConfiguration { /** * The number of times that CloudFront attempts to connect to the origin. * You can specify 1, 2, or 3 as the number of attempts. * * @default 3 */ readonly connectionAttempts?: number; /** * The number of seconds that CloudFront waits when trying to establish a connection to the origin. * You can specify a number of seconds between 1 and 10 (inclusive). * * @default cdk.Duration.seconds(10) */ readonly connectionTimeout?: cdk.Duration; /** * An s3 origin source - if you're using s3 for your assets */ readonly s3OriginSource?: S3OriginConfig; /** * A custom origin source - for all non-s3 sources. */ readonly customOriginSource?: CustomOriginConfig; /** * An s3 origin source for failover in case the s3OriginSource returns invalid status code * * @default - no failover configuration */ readonly failoverS3OriginSource?: S3OriginConfig; /** * A custom origin source for failover in case the s3OriginSource returns invalid status code * * @default - no failover configuration */ readonly failoverCustomOriginSource?: CustomOriginConfig; /** * HTTP status code to failover to second origin * * @default [500, 502, 503, 504] */ readonly failoverCriteriaStatusCodes?: FailoverStatusCode[]; /** * The behaviors associated with this source. * At least one (default) behavior must be included. */ readonly behaviors: Behavior[]; /** * The relative path to the origin root to use for sources. * * @default / * @deprecated Use originPath on s3OriginSource or customOriginSource */ readonly originPath?: string; /** * Any additional headers to pass to the origin * * @default - No additional headers are passed. * @deprecated Use originHeaders on s3OriginSource or customOriginSource */ readonly originHeaders?: { [key: string]: string }; /** * When you enable Origin Shield in the AWS Region that has the lowest latency to your origin, you can get better network performance * * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/origin-shield.html * * @default - origin shield not enabled */ readonly originShieldRegion?: string; } /** * A custom origin configuration */ export interface CustomOriginConfig { /** * The domain name of the custom origin. Should not include the path - that should be in the parent SourceConfiguration */ readonly domainName: string; /** * The origin HTTP port * * @default 80 */ readonly httpPort?: number; /** * The origin HTTPS port * * @default 443 */ readonly httpsPort?: number; /** * The keep alive timeout when making calls in seconds. * * @default Duration.seconds(5) */ readonly originKeepaliveTimeout?: cdk.Duration; /** * The protocol (http or https) policy to use when interacting with the origin. * * @default OriginProtocolPolicy.HttpsOnly */ readonly originProtocolPolicy?: OriginProtocolPolicy; /** * The read timeout when calling the origin in seconds * * @default Duration.seconds(30) */ readonly originReadTimeout?: cdk.Duration; /** * The SSL versions to use when interacting with the origin. * * @default OriginSslPolicy.TLS_V1_2 */ readonly allowedOriginSSLVersions?: OriginSslPolicy[]; /** * The relative path to the origin root to use for sources. * * @default / */ readonly originPath?: string; /** * Any additional headers to pass to the origin * * @default - No additional headers are passed. */ readonly originHeaders?: { [key: string]: string }; /** * When you enable Origin Shield in the AWS Region that has the lowest latency to your origin, you can get better network performance * * @default - origin shield not enabled */ readonly originShieldRegion?: string; } export enum OriginSslPolicy { SSL_V3 = 'SSLv3', TLS_V1 = 'TLSv1', TLS_V1_1 = 'TLSv1.1', TLS_V1_2 = 'TLSv1.2', } /** * S3 origin configuration for CloudFront */ export interface S3OriginConfig { /** * The source bucket to serve content from */ readonly s3BucketSource: s3.IBucket; /** * The optional Origin Access Identity of the origin identity cloudfront will use when calling your s3 bucket. * * @default No Origin Access Identity which requires the S3 bucket to be public accessible */ readonly originAccessIdentity?: IOriginAccessIdentity; /** * The relative path to the origin root to use for sources. * * @default / */ readonly originPath?: string; /** * Any additional headers to pass to the origin * * @default - No additional headers are passed. */ readonly originHeaders?: { [key: string]: string }; /** * When you enable Origin Shield in the AWS Region that has the lowest latency to your origin, you can get better network performance * * @default - origin shield not enabled */ readonly originShieldRegion?: string; } /** * An enum for the supported methods to a CloudFront distribution. */ export enum CloudFrontAllowedMethods { GET_HEAD = 'GH', GET_HEAD_OPTIONS = 'GHO', ALL = 'ALL', } /** * Enums for the methods CloudFront can cache. */ export enum CloudFrontAllowedCachedMethods { GET_HEAD = 'GH', GET_HEAD_OPTIONS = 'GHO', } /** * A CloudFront behavior wrapper. */ export interface Behavior { /** * If CloudFront should automatically compress some content types. * * @default true */ readonly compress?: boolean; /** * If this behavior is the default behavior for the distribution. * * You must specify exactly one default distribution per CloudFront distribution. * The default behavior is allowed to omit the "path" property. */ readonly isDefaultBehavior?: boolean; /** * Trusted signers is how CloudFront allows you to serve private content. * The signers are the account IDs that are allowed to sign cookies/presigned URLs for this distribution. * * If you pass a non empty value, all requests for this behavior must be signed (no public access will be allowed) * @deprecated - We recommend using trustedKeyGroups instead of trustedSigners. */ readonly trustedSigners?: string[]; /** * A list of Key Groups that CloudFront can use to validate signed URLs or signed cookies. * * @default - no KeyGroups are associated with cache behavior * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html */ readonly trustedKeyGroups?: IKeyGroup[]; /** * * The default amount of time CloudFront will cache an object. * * This value applies only when your custom origin does not add HTTP headers, * such as Cache-Control max-age, Cache-Control s-maxage, and Expires to objects. * @default 86400 (1 day) * */ readonly defaultTtl?: cdk.Duration; /** * The method this CloudFront distribution responds do. * * @default GET_HEAD */ readonly allowedMethods?: CloudFrontAllowedMethods; /** * The path this behavior responds to. * Required for all non-default behaviors. (The default behavior implicitly has "*" as the path pattern. ) * */ readonly pathPattern?: string; /** * Which methods are cached by CloudFront by default. * * @default GET_HEAD */ readonly cachedMethods?: CloudFrontAllowedCachedMethods; /** * The values CloudFront will forward to the origin when making a request. * * @default none (no cookies - no headers) * */ readonly forwardedValues?: CfnDistribution.ForwardedValuesProperty; /** * The minimum amount of time that you want objects to stay in the cache * before CloudFront queries your origin. */ readonly minTtl?: cdk.Duration; /** * The max amount of time you want objects to stay in the cache * before CloudFront queries your origin. * * @default Duration.seconds(31536000) (one year) */ readonly maxTtl?: cdk.Duration; /** * Declares associated lambda@edge functions for this distribution behaviour. * * @default No lambda function associated */ readonly lambdaFunctionAssociations?: LambdaFunctionAssociation[]; /** * The CloudFront functions to invoke before serving the contents. * * @default - no functions will be invoked */ readonly functionAssociations?: FunctionAssociation[]; /** * The viewer policy for this behavior. * * @default - the distribution wide viewer protocol policy will be used */ readonly viewerProtocolPolicy?: ViewerProtocolPolicy; } export interface LambdaFunctionAssociation { /** * The lambda event type defines at which event the lambda * is called during the request lifecycle */ readonly eventType: LambdaEdgeEventType; /** * A version of the lambda to associate */ readonly lambdaFunction: lambda.IVersion; /** * Allows a Lambda function to have read access to the body content. * Only valid for "request" event types (`ORIGIN_REQUEST` or `VIEWER_REQUEST`). * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-include-body-access.html * * @default false */ readonly includeBody?: boolean; } export interface ViewerCertificateOptions { /** * How CloudFront should serve HTTPS requests. * * See the notes on SSLMethod if you wish to use other SSL termination types. * * @default SSLMethod.SNI * @see https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_ViewerCertificate.html */ readonly sslMethod?: SSLMethod; /** * The minimum version of the SSL protocol that you want CloudFront to use for HTTPS connections. * * CloudFront serves your objects only to browsers or devices that support at * least the SSL version that you specify. * * @default - SSLv3 if sslMethod VIP, TLSv1 if sslMethod SNI */ readonly securityPolicy?: SecurityPolicyProtocol; /** * Domain names on the certificate (both main domain name and Subject Alternative names) */ readonly aliases?: string[]; } /** * Viewer certificate configuration class */ export class ViewerCertificate { /** * Generate an AWS Certificate Manager (ACM) viewer certificate configuration * * @param certificate AWS Certificate Manager (ACM) certificate. * Your certificate must be located in the us-east-1 (US East (N. Virginia)) region to be accessed by CloudFront * @param options certificate configuration options */ public static fromAcmCertificate(certificate: certificatemanager.ICertificate, options: ViewerCertificateOptions = {}) { const { sslMethod: sslSupportMethod = SSLMethod.SNI, securityPolicy: minimumProtocolVersion, aliases, } = options; return new ViewerCertificate({ acmCertificateArn: certificate.certificateArn, sslSupportMethod, minimumProtocolVersion, }, aliases); } /** * Generate an IAM viewer certificate configuration * * @param iamCertificateId Identifier of the IAM certificate * @param options certificate configuration options */ public static fromIamCertificate(iamCertificateId: string, options: ViewerCertificateOptions = {}) { const { sslMethod: sslSupportMethod = SSLMethod.SNI, securityPolicy: minimumProtocolVersion, aliases, } = options; return new ViewerCertificate({ iamCertificateId, sslSupportMethod, minimumProtocolVersion, }, aliases); } /** * Generate a viewer certificate configuration using * the CloudFront default certificate (e.g. d111111abcdef8.cloudfront.net) * and a `SecurityPolicyProtocol.TLS_V1` security policy. * * @param aliases Alternative CNAME aliases * You also must create a CNAME record with your DNS service to route queries */ public static fromCloudFrontDefaultCertificate(...aliases: string[]) { return new ViewerCertificate({ cloudFrontDefaultCertificate: true }, aliases); } private constructor( public readonly props: CfnDistribution.ViewerCertificateProperty, public readonly aliases: string[] = []) { } } export interface CloudFrontWebDistributionProps { /** * AliasConfiguration is used to configured CloudFront to respond to requests on custom domain names. * * @default - None. * @deprecated see `CloudFrontWebDistributionProps#viewerCertificate` with `ViewerCertificate#acmCertificate` */ readonly aliasConfiguration?: AliasConfiguration; /** * A comment for this distribution in the CloudFront console. * * @default - No comment is added to distribution. */ readonly comment?: string; /** * Enable or disable the distribution. * * @default true */ readonly enabled?: boolean; /** * The default object to serve. * * @default - "index.html" is served. */ readonly defaultRootObject?: string; /** * If your distribution should have IPv6 enabled. * * @default true */ readonly enableIpV6?: boolean; /** * The max supported HTTP Versions. * * @default HttpVersion.HTTP2 */ readonly httpVersion?: HttpVersion; /** * The price class for the distribution (this impacts how many locations CloudFront uses for your distribution, and billing) * * @default PriceClass.PRICE_CLASS_100 the cheapest option for CloudFront is picked by default. */ readonly priceClass?: PriceClass; /** * The default viewer policy for incoming clients. * * @default RedirectToHTTPs */ readonly viewerProtocolPolicy?: ViewerProtocolPolicy; /** * The origin configurations for this distribution. Behaviors are a part of the origin. */ readonly originConfigs: SourceConfiguration[]; /** * Optional - if we should enable logging. * You can pass an empty object ({}) to have us auto create a bucket for logging. * Omission of this property indicates no logging is to be enabled. * * @default - no logging is enabled by default. */ readonly loggingConfig?: LoggingConfiguration; /** * How CloudFront should handle requests that are not successful (eg PageNotFound) * * By default, CloudFront does not replace HTTP status codes in the 4xx and 5xx range * with custom error messages. CloudFront does not cache HTTP status codes. * * @default - No custom error configuration. */ readonly errorConfigurations?: CfnDistribution.CustomErrorResponseProperty[]; /** * Unique identifier that specifies the AWS WAF web ACL to associate with this CloudFront distribution. * * To specify a web ACL created using the latest version of AWS WAF, use the ACL ARN, for example * `arn:aws:wafv2:us-east-1:123456789012:global/webacl/ExampleWebACL/473e64fd-f30b-4765-81a0-62ad96dd167a`. * * To specify a web ACL created using AWS WAF Classic, use the ACL ID, for example `473e64fd-f30b-4765-81a0-62ad96dd167a`. * * @see https://docs.aws.amazon.com/waf/latest/developerguide/what-is-aws-waf.html * @see https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_CreateDistribution.html#API_CreateDistribution_RequestParameters. * * @default - No AWS Web Application Firewall web access control list (web ACL). */ readonly webACLId?: string; /** * Specifies whether you want viewers to use HTTP or HTTPS to request your objects, * whether you're using an alternate domain name with HTTPS, and if so, * if you're using AWS Certificate Manager (ACM) or a third-party certificate authority. * * @default ViewerCertificate.fromCloudFrontDefaultCertificate() * * @see https://aws.amazon.com/premiumsupport/knowledge-center/custom-ssl-certificate-cloudfront/ */ readonly viewerCertificate?: ViewerCertificate; /** * Controls the countries in which your content is distributed. * * @default No geo restriction */ readonly geoRestriction?: GeoRestriction; } /** * Internal only - just adds the originId string to the Behavior */ interface BehaviorWithOrigin extends Behavior { readonly targetOriginId: string; } /** * Attributes used to import a Distribution. */ export interface CloudFrontWebDistributionAttributes { /** * 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; } /** * Amazon CloudFront is a global content delivery network (CDN) service that securely delivers data, videos, * applications, and APIs to your viewers with low latency and high transfer speeds. * CloudFront fronts user provided content and caches it at edge locations across the world. * * Here's how you can use this construct: * * ```ts * const sourceBucket = new s3.Bucket(this, 'Bucket'); * * const distribution = new cloudfront.CloudFrontWebDistribution(this, 'MyDistribution', { * originConfigs: [ * { * s3OriginSource: { * s3BucketSource: sourceBucket, * }, * behaviors : [ {isDefaultBehavior: true}], * }, * ], * }); * ``` * * This will create a CloudFront distribution that uses your S3Bucket as its origin. * * You can customize the distribution using additional properties from the CloudFrontWebDistributionProps interface. * * @resource AWS::CloudFront::Distribution * @deprecated Use `Distribution` instead */ export class CloudFrontWebDistribution extends cdk.Resource implements IDistribution { /** * Creates a construct that represents an external (imported) distribution. */ public static fromDistributionAttributes(scope: Construct, id: string, attrs: CloudFrontWebDistributionAttributes): IDistribution { return new class extends cdk.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 get distributionArn(): string { return formatDistributionArn(this); } public grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant { return iam.Grant.addToPrincipal({ grantee, actions, resourceArns: [formatDistributionArn(this)] }); } public grantCreateInvalidation(identity: iam.IGrantable): iam.Grant { return this.grant(identity, 'cloudfront:CreateInvalidation'); } }(); } /** * The logging bucket for this CloudFront distribution. * If logging is not enabled for this distribution - this property will be undefined. */ public readonly loggingBucket?: s3.IBucket; /** * 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.) * * @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. */ public readonly distributionId: string; /** * Maps our methods to the string arrays they are */ private readonly METHOD_LOOKUP_MAP = { GH: ['GET', 'HEAD'], GHO: ['GET', 'HEAD', 'OPTIONS'], ALL: ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT'], }; /** * Maps for which SecurityPolicyProtocol are available to which SSLMethods */ private readonly VALID_SSL_PROTOCOLS: { [method in SSLMethod]: string[] } = { [SSLMethod.SNI]: [ SecurityPolicyProtocol.TLS_V1, SecurityPolicyProtocol.TLS_V1_1_2016, SecurityPolicyProtocol.TLS_V1_2016, SecurityPolicyProtocol.TLS_V1_2_2018, SecurityPolicyProtocol.TLS_V1_2_2019, SecurityPolicyProtocol.TLS_V1_2_2021, ], [SSLMethod.VIP]: [SecurityPolicyProtocol.SSL_V3, SecurityPolicyProtocol.TLS_V1], [SSLMethod.STATIC_IP]: [ SecurityPolicyProtocol.TLS_V1_2_2018, SecurityPolicyProtocol.TLS_V1_2_2019, SecurityPolicyProtocol.TLS_V1_2_2021, ], }; constructor(scope: Construct, id: string, props: CloudFrontWebDistributionProps) { super(scope, id); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); // Comments have an undocumented limit of 128 characters const trimmedComment = props.comment && props.comment.length > 128 ? `${props.comment.slice(0, 128 - 3)}...` : props.comment; const behaviors: BehaviorWithOrigin[] = []; const origins: CfnDistribution.OriginProperty[] = []; const originGroups: CfnDistribution.OriginGroupProperty[] = []; let originIndex = 1; for (const originConfig of props.originConfigs) { let originId = `origin${originIndex}`; const originProperty = this.toOriginProperty(originConfig, originId); if (originConfig.failoverCustomOriginSource || originConfig.failoverS3OriginSource) { const originSecondaryId = `originSecondary${originIndex}`; const originSecondaryProperty = this.toOriginProperty( { s3OriginSource: originConfig.failoverS3OriginSource, customOriginSource: originConfig.failoverCustomOriginSource, originPath: originConfig.originPath, originHeaders: originConfig.originHeaders, originShieldRegion: originConfig.originShieldRegion, }, originSecondaryId, ); const originGroupsId = `OriginGroup${originIndex}`; const failoverCodes = originConfig.failoverCriteriaStatusCodes ?? [500, 502, 503, 504]; originGroups.push({ id: originGroupsId, members: { items: [{ originId }, { originId: originSecondaryId }], quantity: 2, }, failoverCriteria: { statusCodes: { items: failoverCodes, quantity: failoverCodes.length, }, }, }); originId = originGroupsId; origins.push(originSecondaryProperty); } for (const behavior of originConfig.behaviors) { behaviors.push({ ...behavior, targetOriginId: originId }); } origins.push(originProperty); originIndex++; } origins.forEach(origin => { if (!origin.s3OriginConfig && !origin.customOriginConfig) { throw new cdk.ValidationError(`Origin ${origin.domainName} is missing either S3OriginConfig or CustomOriginConfig. At least 1 must be specified.`, this); } }); const originGroupsDistConfig = originGroups.length > 0 ? { items: originGroups, quantity: originGroups.length, } : undefined; const defaultBehaviors = behaviors.filter(behavior => behavior.isDefaultBehavior); if (defaultBehaviors.length !== 1) { throw new cdk.ValidationError('There can only be one default behavior across all sources. [ One default behavior per distribution ].', this); } const otherBehaviors: CfnDistribution.CacheBehaviorProperty[] = []; for (const behavior of behaviors.filter(b => !b.isDefaultBehavior)) { if (!behavior.pathPattern) { throw new cdk.ValidationError('pathPattern is required for all non-default behaviors', this); } otherBehaviors.push(this.toBehavior(behavior, props.viewerProtocolPolicy) as CfnDistribution.CacheBehaviorProperty); } let distributionConfig: CfnDistribution.DistributionConfigProperty = { comment: trimmedComment, enabled: props.enabled ?? true, defaultRootObject: props.defaultRootObject ?? 'index.html', httpVersion: props.httpVersion || HttpVersion.HTTP2, priceClass: props.priceClass || PriceClass.PRICE_CLASS_100, ipv6Enabled: props.enableIpV6 ?? true, // eslint-disable-next-line max-len customErrorResponses: props.errorConfigurations, // TODO: validation : https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-customerrorresponse.html#cfn-cloudfront-distribution-customerrorresponse-errorcachingminttl webAclId: props.webACLId, origins, originGroups: originGroupsDistConfig, defaultCacheBehavior: this.toBehavior(defaultBehaviors[0], props.viewerProtocolPolicy), cacheBehaviors: otherBehaviors.length > 0 ? otherBehaviors : undefined, }; if (props.aliasConfiguration && props.viewerCertificate) { throw new cdk.ValidationError([ 'You cannot set both aliasConfiguration and viewerCertificate properties.', 'Please only use viewerCertificate, as aliasConfiguration is deprecated.', ].join(' '), this); } let _viewerCertificate = props.viewerCertificate; if (props.aliasConfiguration) { const { acmCertRef, securityPolicy, sslMethod, names: aliases } = props.aliasConfiguration; _viewerCertificate = ViewerCertificate.fromAcmCertificate( certificatemanager.Certificate.fromCertificateArn(this, 'AliasConfigurationCert', acmCertRef), { securityPolicy, sslMethod, aliases }, ); } if (_viewerCertificate) { const { props: viewerCertificate, aliases } = _viewerCertificate; Object.assign(distributionConfig, { aliases, viewerCertificate }); const { minimumProtocolVersion, sslSupportMethod } = viewerCertificate; if (minimumProtocolVersion != null && sslSupportMethod != null) { const validProtocols = this.VALID_SSL_PROTOCOLS[sslSupportMethod as SSLMethod]; if (validProtocols.indexOf(minimumProtocolVersion.toString()) === -1) { throw new cdk.ValidationError(`${minimumProtocolVersion} is not compabtible with sslMethod ${sslSupportMethod}.\n\tValid Protocols are: ${validProtocols.join(', ')}`, this); } } } else { distributionConfig = { ...distributionConfig, viewerCertificate: { cloudFrontDefaultCertificate: true }, }; } if (props.loggingConfig) { this.loggingBucket = props.loggingConfig.bucket || new s3.Bucket(this, 'LoggingBucket', { encryption: s3.BucketEncryption.S3_MANAGED, }); distributionConfig = { ...distributionConfig, logging: { bucket: this.loggingBucket.bucketRegionalDomainName, includeCookies: props.loggingConfig.includeCookies || false, prefix: props.loggingConfig.prefix, }, }; } if (props.geoRestriction) { distributionConfig = { ...distributionConfig, restrictions: { geoRestriction: { restrictionType: props.geoRestriction.restrictionType, locations: props.geoRestriction.locations, }, }, }; } const distribution = new CfnDistribution(this, 'CFDistribution', { distributionConfig }); this.node.defaultChild = distribution; this.domainName = distribution.attrDomainName; this.distributionDomainName = distribution.attrDomainName; this.distributionId = distribution.ref; } public get distributionArn(): string { return formatDistributionArn(this); } /** * Adds an IAM policy statement associated with this distribution to an IAM * principal's policy. * * @param identity The principal * @param actions The set of actions to allow (i.e. "cloudfront:ListInvalidations") */ @MethodMetadata() public grant(identity: iam.IGrantable, ...actions: string[]): iam.Grant { return iam.Grant.addToPrincipal({ grantee: identity, actions, resourceArns: [formatDistributionArn(this)] }); } /** * Grant to create invalidations for this bucket to an IAM principal (Role/Group/User). * * @param identity The principal */ grantCreateInvalidation(identity: iam.IGrantable): iam.Grant { return this.grant(identity, 'cloudfront:CreateInvalidation'); } private toBehavior(input: BehaviorWithOrigin, protoPolicy?: ViewerProtocolPolicy) { let toReturn = { allowedMethods: this.METHOD_LOOKUP_MAP[input.allowedMethods || CloudFrontAllowedMethods.GET_HEAD], cachedMethods: this.METHOD_LOOKUP_MAP[input.cachedMethods || CloudFrontAllowedCachedMethods.GET_HEAD], compress: input.compress !== false, defaultTtl: input.defaultTtl && input.defaultTtl.toSeconds(), forwardedValues: input.forwardedValues || { queryString: false, cookies: { forward: 'none' } }, maxTtl: input.maxTtl && input.maxTtl.toSeconds(), minTtl: input.minTtl && input.minTtl.toSeconds(), trustedKeyGroups: input.trustedKeyGroups?.map(key => key.keyGroupId), trustedSigners: input.trustedSigners, targetOriginId: input.targetOriginId, viewerProtocolPolicy: input.viewerProtocolPolicy || protoPolicy || ViewerProtocolPolicy.REDIRECT_TO_HTTPS, }; if (!input.isDefaultBehavior) { toReturn = Object.assign(toReturn, { pathPattern: input.pathPattern }); } if (input.functionAssociations) { toReturn = Object.assign(toReturn, { functionAssociations: input.functionAssociations.map(association => ({ functionArn: association.function.functionArn, eventType: association.eventType.toString(), })), }); } if (input.lambdaFunctionAssociations) { const includeBodyEventTypes = [LambdaEdgeEventType.ORIGIN_REQUEST, LambdaEdgeEventType.VIEWER_REQUEST]; if (input.lambdaFunctionAssociations.some(fna => fna.includeBody && !includeBodyEventTypes.includes(fna.eventType))) { throw new cdk.ValidationError('\'includeBody\' can only be true for ORIGIN_REQUEST or VIEWER_REQUEST event types.', this); } toReturn = Object.assign(toReturn, { lambdaFunctionAssociations: input.lambdaFunctionAssociations .map(fna => ({ eventType: fna.eventType, lambdaFunctionArn: fna.lambdaFunction && fna.lambdaFunction.edgeArn, includeBody: fna.includeBody, })), }); // allow edgelambda.amazonaws.com to assume the functions' execution role. for (const a of input.lambdaFunctionAssociations) { if (a.lambdaFunction.role && iam.Role.isRole(a.lambdaFunction.role) && a.lambdaFunction.role.assumeRolePolicy) { a.lambdaFunction.role.assumeRolePolicy.addStatements(new iam.PolicyStatement({ actions: ['sts:AssumeRole'], principals: [new iam.ServicePrincipal('edgelambda.amazonaws.com')], })); } } } return toReturn; } private toOriginProperty(originConfig: SourceConfigurationRender, originId: string): CfnDistribution.OriginProperty { if ( !originConfig.s3OriginSource && !originConfig.customOriginSource ) { throw new cdk.ValidationError( 'There must be at least one origin source - either an s3OriginSource, a customOriginSource', this, ); } if (originConfig.customOriginSource && originConfig.s3OriginSource) { throw new cdk.ValidationError( 'There cannot be both an s3OriginSource and a customOriginSource in the same SourceConfiguration.', this, ); } if ([ originConfig.originHeaders, originConfig.s3OriginSource?.originHeaders, originConfig.customOriginSource?.originHeaders, ].filter(x => x).length > 1) { throw new cdk.ValidationError('Only one originHeaders field allowed across origin and failover origins', this); } if ([ originConfig.originPath, originConfig.s3OriginSource?.originPath, originConfig.customOriginSource?.originPath, ].filter(x => x).length > 1) { throw new cdk.ValidationError('Only one originPath field allowed across origin and failover origins', this); } if ([ originConfig.originShieldRegion, originConfig.s3OriginSource?.originShieldRegion, originConfig.customOriginSource?.originShieldRegion, ].filter(x => x).length > 1) { throw new cdk.ValidationError('Only one originShieldRegion field allowed across origin and failover origins', this); } const headers = originConfig.originHeaders ?? originConfig.s3OriginSource?.originHeaders ?? originConfig.customOriginSource?.originHeaders; const originHeaders: CfnDistribution.OriginCustomHeaderProperty[] = []; if (headers) { Object.keys(headers).forEach((key) => { const oHeader: CfnDistribution.OriginCustomHeaderProperty = { headerName: key, headerValue: headers[key], }; originHeaders.push(oHeader); }); } let s3OriginConfig: CfnDistribution.S3OriginConfigProperty | undefined; if (originConfig.s3OriginSource) { // first case for backwards compatibility if (originConfig.s3OriginSource.originAccessIdentity) { // grant CloudFront OriginAccessIdentity read access to S3 bucket // Used rather than `grantRead` because `grantRead` will grant overly-permissive policies. // Only GetObject is needed to retrieve objects for the distribution. // This also excludes KMS permissions; currently, OAI only supports SSE-S3 for buckets. // Source: https://aws.amazon.com/blogs/networking-and-content-delivery/serving-sse-kms-encrypted-content-from-s3-using-cloudfront/ originConfig.s3OriginSource.s3BucketSource.addToResourcePolicy(new iam.PolicyStatement({ resources: [originConfig.s3OriginSource.s3BucketSource.arnForObjects('*')], actions: ['s3:GetObject'], principals: [originConfig.s3OriginSource.originAccessIdentity.grantPrincipal], })); s3OriginConfig = { originAccessIdentity: `origin-access-identity/cloudfront/${originConfig.s3OriginSource.originAccessIdentity.originAccessIdentityId}`, }; } else { s3OriginConfig = {}; } } const connectionAttempts = originConfig.connectionAttempts ?? 3; if (connectionAttempts < 1 || 3 < connectionAttempts || !Number.isInteger(connectionAttempts)) { throw new cdk.ValidationError('connectionAttempts: You can specify 1, 2, or 3 as the number of attempts.', this); } const connectionTimeout = (originConfig.connectionTimeout || cdk.Duration.seconds(10)).toSeconds(); if (connectionTimeout < 1 || 10 < connectionTimeout || !Number.isInteger(connectionTimeout)) { throw new cdk.ValidationError('connectionTimeout: You can specify a number of seconds between 1 and 10 (inclusive).', this); } const originProperty: CfnDistribution.OriginProperty = { id: originId, domainName: originConfig.s3OriginSource ? originConfig.s3OriginSource.s3BucketSource.bucketRegionalDomainName : originConfig.customOriginSource!.domainName, originPath: originConfig.originPath ?? originConfig.customOriginSource?.originPath ?? originConfig.s3OriginSource?.originPath, originCustomHeaders: originHeaders.length > 0 ? originHeaders : undefined, s3OriginConfig, originShield: this.toOriginShieldProperty(originConfig), customOriginConfig: originConfig.customOriginSource ? { httpPort: originConfig.customOriginSource.httpPort || 80, httpsPort: originConfig.customOriginSource.httpsPort || 443, originKeepaliveTimeout: (originConfig.customOriginSource.originKeepaliveTimeout && originConfig.customOriginSource.originKeepaliveTimeout.toSeconds()) || 5, originReadTimeout: (originConfig.customOriginSource.originReadTimeout && originConfig.customOriginSource.originReadTimeout.toSeconds()) || 30, originProtocolPolicy: originConfig.customOriginSource.originProtocolPolicy || OriginProtocolPolicy.HTTPS_ONLY, originSslProtocols: originConfig.customOriginSource .allowedOriginSSLVersions || [OriginSslPolicy.TLS_V1_2], } : undefined, connectionAttempts, connectionTimeout, }; return originProperty; } /** * Takes origin shield region from props and converts to CfnDistribution.OriginShieldProperty */ private toOriginShieldProperty(originConfig:SourceConfigurationRender): CfnDistribution.OriginShieldProperty | undefined { const originShieldRegion = originConfig.originShieldRegion ?? originConfig.customOriginSource?.originShieldRegion ?? originConfig.s3OriginSource?.originShieldRegion; return originShieldRegion ? { enabled: true, originShieldRegion } : undefined; } }