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

import { Construct } from 'constructs'; import { ICachePolicy } from './cache-policy'; import { CfnDistribution, CfnMonitoringSubscription } from './cloudfront.generated'; import { FunctionAssociation } from './function'; import { GeoRestriction } from './geo-restriction'; import { IKeyGroup } from './key-group'; import { IOrigin, OriginBindConfig, OriginBindOptions, OriginSelectionCriteria } from './origin'; import { IOriginRequestPolicy } from './origin-request-policy'; import { CacheBehavior } from './private/cache-behavior'; import { formatDistributionArn } from './private/utils'; import { IRealtimeLogConfig } from './realtime-log-config'; import { IResponseHeadersPolicy } from './response-headers-policy'; import * as acm from '../../aws-certificatemanager'; import * as cloudwatch from '../../aws-cloudwatch'; import * as iam from '../../aws-iam'; import * as lambda from '../../aws-lambda'; import * as s3 from '../../aws-s3'; import { ArnFormat, IResource, Lazy, Resource, Stack, Token, Duration, Names, FeatureFlags, Annotations, ValidationError } from '../../core'; import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource'; import { CLOUDFRONT_DEFAULT_SECURITY_POLICY_TLS_V1_2_2021 } from '../../cx-api'; /** * Interface for CloudFront distributions */ 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. * * @attribute */ readonly distributionId: string; /** * The distribution ARN for this distribution. * * @attribute */ readonly distributionArn: string; /** * 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") */ grant(identity: iam.IGrantable, ...actions: string[]): iam.Grant; /** * Grant to create invalidations for this bucket to an IAM principal (Role/Group/User). * * @param identity The principal */ grantCreateInvalidation(identity: iam.IGrantable): iam.Grant; } /** * 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; } interface BoundOrigin extends OriginBindOptions, OriginBindConfig { readonly origin: IOrigin; readonly originGroupId?: string; } /** * Properties for a Distribution */ export interface DistributionProps { /** * The default behavior for the distribution. */ 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<string, BehaviorOptions>; /** * 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; /** * Any comments you want to include about the distribution. * * @default - no comment */ readonly comment?: string; /** * The object that you want CloudFront to request from your origin (for example, index.html) * when a viewer requests the root URL for your distribution. If no default object is set, the * request goes to the origin's root (e.g., example.com/). * * @default - no default root object */ readonly defaultRootObject?: string; /** * Alternative domain names for this distribution. * * If you want to use your own domain name, such as www.example.com, instead of the cloudfront.net domain name, * you can add an alternate domain name to your distribution. If you attach a certificate to the distribution, * you should add (at least one of) the domain names of the certificate to this list. * * When you want to move a domain name between distributions, you can associate a certificate without specifying any domain names. * For more information, see the _Moving an alternate domain name to a different distribution_ section in the README. * * @default - The distribution will only support the default generated name (e.g., d111111abcdef8.cloudfront.net) */ readonly domainNames?: string[]; /** * Enable or disable the distribution. * * @default true */ readonly enabled?: boolean; /** * Whether CloudFront will respond to IPv6 DNS requests with an IPv6 address. * * If you specify false, CloudFront responds to IPv6 DNS requests with the DNS response code NOERROR and with no IP addresses. * This allows viewers to submit a second request, for an IPv4 address for your distribution. * * @default true */ readonly enableIpv6?: boolean; /** * Enable access logging for the distribution. * * @default - false, unless `logBucket` is specified. */ readonly enableLogging?: boolean; /** * Controls the countries in which your content is distributed. * * @default - No geographic restrictions */ readonly geoRestriction?: GeoRestriction; /** * Specify the maximum HTTP version that you want viewers to use to communicate with CloudFront. * * For viewers and CloudFront to use HTTP/2, viewers must support TLS 1.2 or later, and must support server name identification (SNI). * * @default HttpVersion.HTTP2 */ readonly httpVersion?: HttpVersion; /** * The Amazon S3 bucket to store the access logs in. * Make sure to set `objectOwnership` to `s3.ObjectOwnership.OBJECT_WRITER` in your custom bucket. * * @default - A bucket is created if `enableLogging` is true */ readonly logBucket?: s3.IBucket; /** * Specifies whether you want CloudFront to include cookies in access logs * * @default false */ readonly logIncludesCookies?: boolean; /** * An optional string that you want CloudFront to prefix to the access log filenames for this distribution. * * @default - no prefix */ readonly logFilePrefix?: string; /** * 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. * * @default PriceClass.PRICE_CLASS_ALL */ readonly priceClass?: PriceClass; /** * 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; /** * How CloudFront should handle requests that are not successful (e.g., PageNotFound). * * @default - No custom error responses. */ readonly errorResponses?: ErrorResponse[]; /** * 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 - SecurityPolicyProtocol.TLS_V1_2_2021 if the '@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021' feature flag is set; otherwise, SecurityPolicyProtocol.TLS_V1_2_2019. */ readonly minimumProtocolVersion?: SecurityPolicyProtocol; /** * The SSL method CloudFront will use for your distribution. * * Server Name Indication (SNI) - is an extension to the TLS computer networking protocol by which a client indicates * which hostname it is attempting to connect to at the start of the handshaking process. This allows a server to present * multiple certificates on the same IP address and TCP port number and hence allows multiple secure (HTTPS) websites * (or any other service over TLS) to be served by the same IP address without requiring all those sites to use the same certificate. * * CloudFront can use SNI to host multiple distributions on the same IP - which a large majority of clients will support. * * If your clients cannot support SNI however - CloudFront can use dedicated IPs for your distribution - but there is a prorated monthly charge for * using this feature. By default, we use SNI - but you can optionally enable dedicated IPs (VIP). * * See the CloudFront SSL for more details about pricing : https://aws.amazon.com/cloudfront/custom-ssl-domains/ * * @default SSLMethod.SNI */ readonly sslSupportMethod?: SSLMethod; /** * Whether to enable additional CloudWatch metrics. * * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/viewing-cloudfront-metrics.html * * @default false */ readonly publishAdditionalMetrics?: boolean; } /** * A CloudFront distribution with associated origin(s) and caching behavior(s). */ 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 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(grantee: iam.IGrantable): iam.Grant { return this.grant(grantee, 'cloudfront:CreateInvalidation'); } }(); } public readonly domainName: string; public readonly distributionDomainName: string; public readonly distributionId: string; private readonly httpVersion: HttpVersion; private readonly defaultBehavior: CacheBehavior; private readonly additionalBehaviors: CacheBehavior[] = []; private readonly boundOrigins: BoundOrigin[] = []; private readonly originGroups: CfnDistribution.OriginGroupProperty[] = []; private readonly errorResponses: ErrorResponse[]; private readonly certificate?: acm.ICertificate; private readonly publishAdditionalMetrics?: boolean; private webAclId?: string; constructor(scope: Construct, id: string, props: DistributionProps) { super(scope, id); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); if (props.certificate) { const certificateRegion = Stack.of(this).splitArn(props.certificate.certificateArn, ArnFormat.SLASH_RESOURCE_NAME).region; if (!Token.isUnresolved(certificateRegion) && certificateRegion !== 'us-east-1') { throw new ValidationError(`Distribution certificates must be in the us-east-1 region and the certificate you provided is in ${certificateRegion}.`, this); } if ((props.domainNames ?? []).length === 0) { Annotations.of(this).addWarningV2('@aws-cdk/aws-cloudfront:emptyDomainNames', 'No domain names are specified. You will need to specify it after running associate-alias CLI command manually. See the "Moving an alternate domain name to a different distribution" section of module\'s README for more info.'); } } this.httpVersion = props.httpVersion ?? HttpVersion.HTTP2; this.validateGrpc(props.defaultBehavior); const originId = this.addOrigin(props.defaultBehavior.origin); this.defaultBehavior = new CacheBehavior(originId, { pathPattern: '*', ...props.defaultBehavior }); if (props.additionalBehaviors) { Object.entries(props.additionalBehaviors).forEach(([pathPattern, behaviorOptions]) => { this.addBehavior(pathPattern, behaviorOptions.origin, behaviorOptions); }); } if (props.webAclId) { this.validateWebAclId(props.webAclId); this.webAclId = props.webAclId; } this.certificate = props.certificate; this.errorResponses = props.errorResponses ?? []; this.publishAdditionalMetrics = props.publishAdditionalMetrics; // 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 distribution = new CfnDistribution(this, 'Resource', { distributionConfig: { enabled: props.enabled ?? true, origins: Lazy.any({ produce: () => this.renderOrigins() }), originGroups: Lazy.any({ produce: () => this.renderOriginGroups() }), defaultCacheBehavior: this.defaultBehavior._renderBehavior(), aliases: props.domainNames, cacheBehaviors: Lazy.any({ produce: () => this.renderCacheBehaviors() }), comment: trimmedComment, customErrorResponses: this.renderErrorResponses(), defaultRootObject: props.defaultRootObject, httpVersion: this.httpVersion, ipv6Enabled: props.enableIpv6 ?? true, logging: this.renderLogging(props), priceClass: props.priceClass ?? undefined, restrictions: this.renderRestrictions(props.geoRestriction), viewerCertificate: this.certificate ? this.renderViewerCertificate(this.certificate, props.minimumProtocolVersion, props.sslSupportMethod) : undefined, webAclId: Lazy.string({ produce: () => this.webAclId }), }, }); this.domainName = distribution.attrDomainName; this.distributionDomainName = distribution.attrDomainName; this.distributionId = distribution.ref; if (props.publishAdditionalMetrics) { new CfnMonitoringSubscription(this, 'MonitoringSubscription', { distributionId: this.distributionId, monitoringSubscription: { realtimeMetricsSubscriptionConfig: { realtimeMetricsSubscriptionStatus: 'Enabled', }, }, }); } } public get distributionArn(): string { return formatDistributionArn(this); } /** * Return the given named metric for this Distribution */ @MethodMetadata() public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { return new cloudwatch.Metric({ namespace: 'AWS/CloudFront', metricName, dimensionsMap: { DistributionId: this.distributionId }, ...props, }); } /** * Metric for the total number of viewer requests received by CloudFront, for all HTTP methods and for both HTTP and HTTPS requests. * * @default - sum over 5 minutes */ @MethodMetadata() public metricRequests(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric('Requests', { statistic: 'sum', ...props }); } /** * Metric for the total number of bytes that viewers uploaded to your origin with CloudFront, using POST and PUT requests. * * @default - sum over 5 minutes */ @MethodMetadata() public metricBytesUploaded(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric('BytesUploaded', { statistic: 'sum', ...props }); } /** * Metric for the total number of bytes downloaded by viewers for GET, HEAD, and OPTIONS requests. * * @default - sum over 5 minutes */ @MethodMetadata() public metricBytesDownloaded(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric('BytesDownloaded', { statistic: 'sum', ...props }); } /** * Metric for the percentage of all viewer requests for which the response's HTTP status code is 4xx or 5xx. * * @default - average over 5 minutes */ @MethodMetadata() public metricTotalErrorRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric('TotalErrorRate', props); } /** * Metric for the percentage of all viewer requests for which the response's HTTP status code is 4xx. * * @default - average over 5 minutes */ @MethodMetadata() public metric4xxErrorRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric('4xxErrorRate', props); } /** * Metric for the percentage of all viewer requests for which the response's HTTP status code is 5xx. * * @default - average over 5 minutes */ @MethodMetadata() public metric5xxErrorRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric('5xxErrorRate', props); } /** * Metric for the total time spent from when CloudFront receives a request to when it starts providing a response to the network (not the viewer), * for requests that are served from the origin, not the CloudFront cache. * * This is also known as first byte latency, or time-to-first-byte. * * To obtain this metric, you need to set `publishAdditionalMetrics` to `true`. * * @default - average over 5 minutes */ @MethodMetadata() public metricOriginLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { if (this.publishAdditionalMetrics !== true) { throw new ValidationError('Origin latency metric is only available if \'publishAdditionalMetrics\' is set \'true\'', this); } return this.metric('OriginLatency', props); } /** * Metric for the percentage of all cacheable requests for which CloudFront served the content from its cache. * * HTTP POST and PUT requests, and errors, are not considered cacheable requests. * * To obtain this metric, you need to set `publishAdditionalMetrics` to `true`. * * @default - average over 5 minutes */ @MethodMetadata() public metricCacheHitRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric { if (this.publishAdditionalMetrics !== true) { throw new ValidationError('Cache hit rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'', this); } return this.metric('CacheHitRate', props); } /** * Metric for the percentage of all viewer requests for which the response's HTTP status code is 401. * * To obtain this metric, you need to set `publishAdditionalMetrics` to `true`. * * @default - average over 5 minutes */ @MethodMetadata() public metric401ErrorRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric { if (this.publishAdditionalMetrics !== true) { throw new ValidationError('401 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'', this); } return this.metric('401ErrorRate', props); } /** * Metric for the percentage of all viewer requests for which the response's HTTP status code is 403. * * To obtain this metric, you need to set `publishAdditionalMetrics` to `true`. * * @default - average over 5 minutes */ @MethodMetadata() public metric403ErrorRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric { if (this.publishAdditionalMetrics !== true) { throw new ValidationError('403 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'', this); } return this.metric('403ErrorRate', props); } /** * Metric for the percentage of all viewer requests for which the response's HTTP status code is 404. * * To obtain this metric, you need to set `publishAdditionalMetrics` to `true`. * * @default - average over 5 minutes */ @MethodMetadata() public metric404ErrorRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric { if (this.publishAdditionalMetrics !== true) { throw new ValidationError('404 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'', this); } return this.metric('404ErrorRate', props); } /** * Metric for the percentage of all viewer requests for which the response's HTTP status code is 502. * * To obtain this metric, you need to set `publishAdditionalMetrics` to `true`. * * @default - average over 5 minutes */ @MethodMetadata() public metric502ErrorRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric { if (this.publishAdditionalMetrics !== true) { throw new ValidationError('502 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'', this); } return this.metric('502ErrorRate', props); } /** * Metric for the percentage of all viewer requests for which the response's HTTP status code is 503. * * To obtain this metric, you need to set `publishAdditionalMetrics` to `true`. * * @default - average over 5 minutes */ @MethodMetadata() public metric503ErrorRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric { if (this.publishAdditionalMetrics !== true) { throw new ValidationError('503 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'', this); } return this.metric('503ErrorRate', props); } /** * Metric for the percentage of all viewer requests for which the response's HTTP status code is 504. * * To obtain this metric, you need to set `publishAdditionalMetrics` to `true`. * * @default - average over 5 minutes */ @MethodMetadata() public metric504ErrorRate(props?: cloudwatch.MetricOptions): cloudwatch.Metric { if (this.publishAdditionalMetrics !== true) { throw new ValidationError('504 error rate metric is only available if \'publishAdditionalMetrics\' is set \'true\'', this); } return this.metric('504ErrorRate', props); } /** * Adds a new behavior to this distribution for the given pathPattern. * * @param pathPattern the path pattern (e.g., 'images/*') that specifies which requests to apply the behavior to. * @param origin the origin to use for this behavior * @param behaviorOptions the options for the behavior at this path. */ @MethodMetadata() public addBehavior(pathPattern: string, origin: IOrigin, behaviorOptions: AddBehaviorOptions = {}) { if (pathPattern === '*') { throw new ValidationError('Only the default behavior can have a path pattern of \'*\'', this); } this.validateGrpc(behaviorOptions); const originId = this.addOrigin(origin); this.additionalBehaviors.push(new CacheBehavior(originId, { pathPattern, ...behaviorOptions })); } /** * 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 */ @MethodMetadata() public grantCreateInvalidation(identity: iam.IGrantable): iam.Grant { return this.grant(identity, 'cloudfront:CreateInvalidation'); } /** * Attach WAF WebACL to this CloudFront distribution * * WebACL must be in the us-east-1 region * * @param webAclId The WAF WebACL to associate with this distribution */ @MethodMetadata() public attachWebAclId(webAclId: string) { if (this.webAclId) { throw new ValidationError('A WebACL has already been attached to this distribution', this); } this.validateWebAclId(webAclId); this.webAclId = webAclId; } private validateWebAclId(webAclId: string) { if (Token.isUnresolved(webAclId)) { // Cannot validate unresolved tokens or non-string values at synth-time. return; } if (webAclId.startsWith('arn:')) { const webAclRegion = Stack.of(this).splitArn(webAclId, ArnFormat.SLASH_RESOURCE_NAME).region; if (!Token.isUnresolved(webAclRegion) && webAclRegion !== 'us-east-1') { throw new ValidationError(`WebACL for CloudFront distributions must be created in the us-east-1 region; received ${webAclRegion}`, this); } } } private addOrigin(origin: IOrigin, isFailoverOrigin: boolean = false): string { const ORIGIN_ID_MAX_LENGTH = 128; const existingOrigin = this.boundOrigins.find(boundOrigin => boundOrigin.origin === origin); if (existingOrigin) { return existingOrigin.originGroupId ?? existingOrigin.originId; } else { const originIndex = this.boundOrigins.length + 1; const scope = new Construct(this, `Origin${originIndex}`); const generatedId = Names.uniqueId(scope).slice(-ORIGIN_ID_MAX_LENGTH); const distributionId = this.distributionId; const originBindConfig = origin.bind(scope, { originId: generatedId, distributionId: Lazy.string({ produce: () => this.distributionId }) }); const originId = originBindConfig.originProperty?.id ?? generatedId; const duplicateId = this.boundOrigins.find(boundOrigin => boundOrigin.originProperty?.id === originBindConfig.originProperty?.id); if (duplicateId) { throw new ValidationError(`Origin with id ${duplicateId.originProperty?.id} already exists. OriginIds must be unique within a distribution`, this); } if (!originBindConfig.failoverConfig) { this.boundOrigins.push({ origin, originId, distributionId, ...originBindConfig }); } else { if (isFailoverOrigin) { throw new ValidationError('An Origin cannot use an Origin with its own failover configuration as its fallback origin!', this); } const groupIndex = this.originGroups.length + 1; const originGroupId = Names.uniqueId(new Construct(this, `OriginGroup${groupIndex}`)).slice(-ORIGIN_ID_MAX_LENGTH); this.boundOrigins.push({ origin, originId, distributionId, originGroupId, ...originBindConfig }); const failoverOriginId = this.addOrigin(originBindConfig.failoverConfig.failoverOrigin, true); this.addOriginGroup( originGroupId, originBindConfig.failoverConfig.statusCodes, originId, failoverOriginId, originBindConfig.selectionCriteria, ); return originGroupId; } return originBindConfig.originProperty?.id ?? originId; } } private addOriginGroup( originGroupId: string, statusCodes: number[] | undefined, originId: string, failoverOriginId: string, selectionCriteria: OriginSelectionCriteria | undefined, ): void { statusCodes = statusCodes ?? [500, 502, 503, 504]; if (statusCodes.length === 0) { throw new ValidationError('fallbackStatusCodes cannot be empty', this); } this.originGroups.push({ failoverCriteria: { statusCodes: { items: statusCodes, quantity: statusCodes.length, }, }, id: originGroupId, members: { items: [ { originId }, { originId: failoverOriginId }, ], quantity: 2, }, selectionCriteria, }); } private renderOrigins(): CfnDistribution.OriginProperty[] { const renderedOrigins: CfnDistribution.OriginProperty[] = []; this.boundOrigins.forEach(boundOrigin => { if (boundOrigin.originProperty) { renderedOrigins.push(boundOrigin.originProperty); } }); return renderedOrigins; } private renderOriginGroups(): CfnDistribution.OriginGroupsProperty | undefined { return this.originGroups.length === 0 ? undefined : { items: this.originGroups, quantity: this.originGroups.length, }; } private renderCacheBehaviors(): CfnDistribution.CacheBehaviorProperty[] | undefined { if (this.additionalBehaviors.length === 0) { return undefined; } return this.additionalBehaviors.map(behavior => behavior._renderBehavior()); } private renderErrorResponses(): CfnDistribution.CustomErrorResponseProperty[] | undefined { if (this.errorResponses.length === 0) { return undefined; } return this.errorResponses.map(errorConfig => { if (!errorConfig.responseHttpStatus && !errorConfig.ttl && !errorConfig.responsePagePath) { throw new ValidationError('A custom error response without either a \'responseHttpStatus\', \'ttl\' or \'responsePagePath\' is not valid.', this); } return { errorCachingMinTtl: errorConfig.ttl?.toSeconds(), errorCode: errorConfig.httpStatus, responseCode: errorConfig.responsePagePath ? errorConfig.responseHttpStatus ?? errorConfig.httpStatus : errorConfig.responseHttpStatus, responsePagePath: errorConfig.responsePagePath, }; }); } private renderLogging(props: DistributionProps): CfnDistribution.LoggingProperty | undefined { if (!props.enableLogging && !props.logBucket) { return undefined; } if (props.enableLogging === false && props.logBucket) { throw new ValidationError('Explicitly disabled logging but provided a logging bucket.', this); } const bucket = props.logBucket ?? new s3.Bucket(this, 'LoggingBucket', { encryption: s3.BucketEncryption.S3_MANAGED, // We need set objectOwnership to OBJECT_WRITER to enable ACL, which is disabled by default. objectOwnership: s3.ObjectOwnership.OBJECT_WRITER, }); return { bucket: bucket.bucketRegionalDomainName, includeCookies: props.logIncludesCookies, prefix: props.logFilePrefix, }; } private renderRestrictions(geoRestriction?: GeoRestriction) { return geoRestriction ? { geoRestriction: { restrictionType: geoRestriction.restrictionType, locations: geoRestriction.locations, }, } : undefined; } private renderViewerCertificate(certificate: acm.ICertificate, minimumProtocolVersionProp?: SecurityPolicyProtocol, sslSupportMethodProp?: SSLMethod): CfnDistribution.ViewerCertificateProperty { const defaultVersion = FeatureFlags.of(this).isEnabled(CLOUDFRONT_DEFAULT_SECURITY_POLICY_TLS_V1_2_2021) ? SecurityPolicyProtocol.TLS_V1_2_2021 : SecurityPolicyProtocol.TLS_V1_2_2019; const minimumProtocolVersion = minimumProtocolVersionProp ?? defaultVersion; const sslSupportMethod = sslSupportMethodProp ?? SSLMethod.SNI; return { acmCertificateArn: certificate.certificateArn, minimumProtocolVersion: minimumProtocolVersion, sslSupportMethod: sslSupportMethod, }; } private validateGrpc(behaviorOptions: AddBehaviorOptions) { if (!behaviorOptions.enableGrpc) { return; } const validHttpVersions = [HttpVersion.HTTP2, HttpVersion.HTTP2_AND_3]; if (!validHttpVersions.includes(this.httpVersion)) { throw new ValidationError(`'httpVersion' must be ${validHttpVersions.join(' or ')} if 'enableGrpc' in 'defaultBehavior' or 'additionalBehaviors' is true, got ${this.httpVersion}`, this); } } } /** Maximum HTTP version to support */ export enum HttpVersion { /** HTTP 1.1 */ HTTP1_1 = 'http1.1', /** HTTP 2 */ HTTP2 = 'http2', /** HTTP 2 and HTTP 3 */ HTTP2_AND_3 = 'http2and3', /** HTTP 3 */ HTTP3 = 'http3', } /** * 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', } /** * 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', } /** * 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', } /** * The SSL method CloudFront will use for your distribution. * * Server Name Indication (SNI) - is an extension to the TLS computer networking protocol by which a client indicates * which hostname it is attempting to connect to at the start of the handshaking process. This allows a server to present * multiple certificates on the same IP address and TCP port number and hence allows multiple secure (HTTPS) websites * (or any other service over TLS) to be served by the same IP address without requiring all those sites to use the same certificate. * * CloudFront can use SNI to host multiple distributions on the same IP - which a large majority of clients will support. * * If your clients cannot support SNI however - CloudFront can use dedicated IPs for your distribution - but there is a prorated monthly charge for * using this feature. By default, we use SNI - but you can optionally enable dedicated IPs (VIP). * * See the CloudFront SSL for more details about pricing : https://aws.amazon.com/cloudfront/custom-ssl-domains/ * */ export enum SSLMethod { SNI = 'sni-only', VIP = 'vip', STATIC_IP = 'static-ip', } /** * 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. */ export enum SecurityPolicyProtocol { SSL_V3 = 'SSLv3', TLS_V1 = 'TLSv1', TLS_V1_2016 = 'TLSv1_2016', TLS_V1_1_2016 = 'TLSv1.1_2016', TLS_V1_2_2018 = 'TLSv1.2_2018', TLS_V1_2_2019 = 'TLSv1.2_2019', TLS_V1_2_2021 = 'TLSv1.2_2021', } /** * 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; } } /** * The HTTP methods that the Behavior will cache requests on. */ export class CachedMethods { /** HEAD and GET */ public static readonly CACHE_GET_HEAD = new CachedMethods(['GET', 'HEAD']); /** HEAD, GET, and OPTIONS */ public static readonly CACHE_GET_HEAD_OPTIONS = new CachedMethods(['GET', 'HEAD', 'OPTIONS']); /** HTTP methods supported */ public readonly methods: string[]; private constructor(methods: string[]) { this.methods = methods; } } /** * Options for configuring custom error responses. */ 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 */ readonly ttl?: Duration; /** * The HTTP status code for which you want to specify a custom error page and/or a caching duration. */ 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 `responseHttpStatus`, you must also specify a value for `responsePagePath`. * * @default - the error code will be returned as the response code. */ readonly responseHttpStatus?: number; /** * The path to the custom error page that you want CloudFront to return to a viewer when your origin returns the * `httpStatus`, for example, /4xx-errors/403-forbidden.html * * @default - the default CloudFront response is shown. */ readonly responsePagePath?: string; } /** * The type of events that a Lambda@Edge function can be invoked in response to. */ export enum LambdaEdgeEventType { /** * The origin-request specifies the request to the * origin location (e.g. S3) */ ORIGIN_REQUEST = 'origin-request', /** * The origin-response specifies the response from the * origin location (e.g. S3) */ ORIGIN_RESPONSE = 'origin-response', /** * The viewer-request specifies the incoming request */ VIEWER_REQUEST = 'viewer-request', /** * The viewer-response specifies the outgoing response */ VIEWER_RESPONSE = 'viewer-response', } /** * Represents a Lambda function version and event type when using Lambda@Edge. * The type of the `AddBehaviorOptions.edgeLambdas` property. */ export interface EdgeLambda { /** * The version of the Lambda function that will be invoked. * * **Note**: it's not possible to use the '$LATEST' function version for Lambda@Edge! */ readonly functionVersion: lambda.IVersion; /** The type of event in response to which should the function be invoked. */ readonly eventType: LambdaEdgeEventType; /** * 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; } /** * Options for adding a new behavior to a Distribution. */ export interface AddBehaviorOptions { /** * HTTP methods to allow for this behavior. * * @default AllowedMethods.ALLOW_GET_HEAD */ readonly allowedMethods?: AllowedMethods; /** * HTTP methods to cache for this behavior. * * @default CachedMethods.CACHE_GET_HEAD */ readonly cachedMethods?: CachedMethods; /** * The cache policy for this behavior. The cache policy determines what values are included in the cache key, * and the time-to-live (TTL) values for the cache. * * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/controlling-the-cache-key.html. * @default CachePolicy.CACHING_OPTIMIZED */ readonly cachePolicy?: ICachePolicy; /** * Whether you want CloudFront to automatically compress certain files for this cache behavior. * See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/ServingCompressedFiles.html#compressed-content-cloudfront-file-types * for file types CloudFront will compress. * * @default true */ readonly compress?: boolean; /** * The origin request policy for this behavior. The origin request policy determines which values (e.g., headers, cookies) * are included in requests that CloudFront sends to the origin. * * @default - none */ readonly originRequestPolicy?: IOriginRequestPolicy; /** * The real-time log configuration to be attached to this cache behavior. * * @default - none */ readonly realtimeLogConfig?: IRealtimeLogConfig; /** * The response headers policy for this behavior. The response headers policy determines which headers are included in responses * * @default - none */ readonly responseHeadersPolicy?: IResponseHeadersPolicy; /** * Set this to true to indicate you want to distribute media files in the Microsoft Smooth Streaming format using this behavior. * * @default false */ readonly smoothStreaming?: boolean; /** * The protocol that viewers can use to access the files controlled by this behavior. * * @default ViewerProtocolPolicy.ALLOW_ALL */ readonly viewerProtocolPolicy?: ViewerProtocolPolicy; /** * The CloudFront functions to invoke before serving the contents. * * @default - no functions will be invoked */ readonly functionAssociations?: FunctionAssociation[]; /** * The Lambda@Edge functions to invoke before serving the contents. * * @default - no Lambda functions will be invoked * @see https://aws.amazon.com/lambda/edge */ readonly edgeLambdas?: EdgeLambda[]; /** * 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[]; /** * Enables your CloudFront distribution to receive gRPC requests and to proxy them directly to your origins. * * If the `enableGrpc` is set to true, the following restrictions apply: * - The `allowedMethods` property must be `AllowedMethods.ALLOW_ALL` to include POST method because gRPC only supports POST method. * - The `httpVersion` property must be `HttpVersion.HTTP2` or `HttpVersion.HTTP2_AND_3` because gRPC only supports versions including HTTP/2. * - The `edgeLambdas` property can't be specified because gRPC is not supported with Lambda@Edge. * * @default false * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-using-grpc.html */ readonly enableGrpc?: boolean; } /** * Options for creating a new behavior. */ export interface BehaviorOptions extends AddBehaviorOptions { /** * The origin that you want CloudFront to route requests to when they match this behavior. */ readonly origin: IOrigin; }