UNPKG

@studion/infra-code-blocks

Version:
258 lines (257 loc) 11 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CloudFront = void 0; const aws = require("@pulumi/aws"); const pulumi = require("@pulumi/pulumi"); const common_tags_1 = require("../../shared/common-tags"); const acm_certificate_1 = require("../acm-certificate"); const s3_cache_strategy_1 = require("./s3-cache-strategy"); const lb_cache_strategy_1 = require("./lb-cache-strategy"); class CloudFront extends pulumi.ComponentResource { name; distribution; acmCertificate; constructor(name, args, opts = {}) { super('studion:cloudfront:CloudFront', name, {}, opts); this.name = name; const { behaviors, domain, certificate, hostedZoneId, tags } = args; const hasCustomDomain = !!domain || !!certificate; if (hasCustomDomain && !hostedZoneId) { throw new Error('Provide `hostedZoneId` alongside `domain` and/or `certificate`.'); } const defaultBehavior = behaviors.at(-1); const orderedBehaviors = behaviors.slice(0, -1); if (!defaultBehavior || !isDefaultBehavior(defaultBehavior)) { throw new Error('Default behavior must be placed last.'); } if (domain && hostedZoneId && !certificate) { this.acmCertificate = this.createCertificate({ domain, hostedZoneId }); } const defaultRootObject = isS3BehaviorType(defaultBehavior) ? 'index.html' : isCustomBehaviorType(defaultBehavior) ? defaultBehavior.defaultRootObject : undefined; this.distribution = this.createDistribution({ origins: this.createDistributionOrigins(behaviors), defaultCache: this.getCacheBehavior(defaultBehavior), orderedCaches: orderedBehaviors.length ? orderedBehaviors.map((it, idx) => this.getCacheBehavior(it, idx).apply(behavior => ({ pathPattern: it.pathPattern, ...behavior, }))) : undefined, domain, certificate: certificate || this.acmCertificate ? pulumi.output(certificate ?? this.acmCertificate.certificate) : undefined, certificateValidation: this.acmCertificate ? this.acmCertificate.certificateValidation : undefined, defaultRootObject, tags, }); if (hasCustomDomain && hostedZoneId) { this.createAliasRecord({ hostedZoneId }); } this.registerOutputs(); } createDistributionOrigins(behaviors) { return pulumi.output(behaviors).apply(entries => { const origins = entries.map(it => { if (isS3BehaviorType(it)) { return getOriginWithDefaults({ originId: it.bucket.arn, domainName: it.websiteConfig.websiteEndpoint, customOriginConfig: { originProtocolPolicy: 'http-only', }, }); } else if (isLbBehaviorType(it)) { return getOriginWithDefaults({ originId: it.loadBalancer.arn, domainName: it.dnsName ?? it.loadBalancer.dnsName, }); } else if (isCustomBehaviorType(it)) { return getOriginWithDefaults({ originId: it.originId, domainName: it.domainName, customOriginConfig: { ...(it.originProtocolPolicy ? { originProtocolPolicy: it.originProtocolPolicy } : undefined), }, }); } else { throw new Error('Unknown CloudFront behavior encountered during mapping to distribution origins.'); } }); // Remove duplicates, keeps the last occurrence of the origin return [...new Map(origins.map(it => [it.originId, it])).values()]; }); } getCacheBehavior(behavior, order) { const isDefault = isDefaultBehavior(behavior); const getStrategyName = (backend) => { const suffix = isDefault ? 'default' : `ordered-${order}`; return `${this.name}-${backend}-cache-strategy-${suffix}`; }; if (isS3BehaviorType(behavior)) { const strategy = new s3_cache_strategy_1.S3CacheStrategy(getStrategyName('s3'), { pathPattern: behavior.pathPattern, bucket: behavior.bucket, cacheTtl: behavior.cacheTtl, }, { parent: this }); return pulumi.output(strategy.config); } else if (isLbBehaviorType(behavior)) { const strategy = new lb_cache_strategy_1.LbCacheStrategy(getStrategyName('lb'), { pathPattern: behavior.pathPattern, loadBalancer: behavior.loadBalancer, }, { parent: this }); return pulumi.output(strategy.config); } else if (isCustomBehaviorType(behavior)) { return isS3Domain(behavior.domainName).apply(isS3 => ({ targetOriginId: behavior.originId, allowedMethods: behavior.allowedMethods ?? (isS3 ? ['GET', 'HEAD'] : ['GET', 'HEAD', 'OPTIONS', 'PUT', 'POST', 'PATCH', 'DELETE']), cachedMethods: behavior.cachedMethods ?? ['GET', 'HEAD'], ...(behavior.compress != null && { compress: behavior.compress }), viewerProtocolPolicy: 'redirect-to-https', cachePolicyId: behavior.cachePolicyId ?? aws.cloudfront .getCachePolicyOutput({ name: 'Managed-CachingDisabled' }) .apply(p => p.id), originRequestPolicyId: behavior.originRequestPolicyId ?? (isS3 ? undefined : aws.cloudfront .getOriginRequestPolicyOutput({ name: 'Managed-AllViewerExceptHostHeader', }) .apply(p => p.id)), responseHeadersPolicyId: behavior.responseHeadersPolicyId ?? aws.cloudfront .getResponseHeadersPolicyOutput({ name: 'Managed-SecurityHeadersPolicy', }) .apply(p => p.id), })); } else { throw new Error('Unknown CloudFront behavior encountered during mapping to distribution cache behaviors.'); } } createCertificate({ domain, hostedZoneId, }) { return new acm_certificate_1.AcmCertificate(`${domain}-acm-certificate`, { domain, hostedZoneId, region: 'us-east-1', // CF requires certificates to be in this region }, { parent: this }); } createDistribution({ origins, defaultCache, orderedCaches, domain, certificate, certificateValidation, defaultRootObject, tags, }) { return new aws.cloudfront.Distribution(`${this.name}-distribution`, { enabled: true, isIpv6Enabled: true, waitForDeployment: true, httpVersion: 'http2and3', ...(defaultRootObject && { defaultRootObject }), ...(certificate ? { aliases: domain ? [domain] : pulumi .all([ certificate.domainName, certificate.subjectAlternativeNames, ]) .apply(([dn, sans = []]) => [...new Set([dn, ...sans])]), viewerCertificate: { acmCertificateArn: certificate.arn, sslSupportMethod: 'sni-only', minimumProtocolVersion: 'TLSv1.2_2021', }, } : { viewerCertificate: { cloudfrontDefaultCertificate: true, }, }), origins, defaultCacheBehavior: defaultCache, ...(orderedCaches && { orderedCacheBehaviors: orderedCaches }), priceClass: 'PriceClass_100', restrictions: { geoRestriction: { restrictionType: 'none' }, }, tags: { ...common_tags_1.commonTags, ...tags }, }, { parent: this, aliases: [{ name: `${this.name}-cloudfront` }], ...(certificateValidation ? { dependsOn: [certificateValidation] } : undefined), }); } createAliasRecord({ hostedZoneId, }) { return this.distribution.aliases.apply(aliases => aliases?.map((alias, index) => new aws.route53.Record(`${this.name}-dns-a-record-${index}`, { type: 'A', name: alias, zoneId: hostedZoneId, aliases: [ { name: this.distribution.domainName, zoneId: this.distribution.hostedZoneId, evaluateTargetHealth: false, }, ], }, { parent: this, aliases: [{ name: `${this.name}-cdn-route53-record` }], }))); } } exports.CloudFront = CloudFront; (function (CloudFront) { let BehaviorType; (function (BehaviorType) { BehaviorType["S3"] = "s3"; BehaviorType["LB"] = "lb"; BehaviorType["CUSTOM"] = "custom"; })(BehaviorType = CloudFront.BehaviorType || (CloudFront.BehaviorType = {})); })(CloudFront || (exports.CloudFront = CloudFront = {})); const S3_DOMAIN_REGEX = /\.s3(?:[.\-][a-z0-9]+(?:-[a-z0-9]+)*)*\.amazonaws\.com$/; function isDefaultBehavior(value) { return value.pathPattern === '*' || value.pathPattern === '/*'; } function isS3BehaviorType(value) { return value.type === CloudFront.BehaviorType.S3; } function isLbBehaviorType(value) { return value.type === CloudFront.BehaviorType.LB; } function isCustomBehaviorType(value) { return value.type === CloudFront.BehaviorType.CUSTOM; } function isS3Domain(domainName) { return pulumi.output(domainName).apply(dn => S3_DOMAIN_REGEX.test(dn)); } function getOriginWithDefaults({ originId, domainName, customOriginConfig, }) { return { originId, domainName, customOriginConfig: { originProtocolPolicy: 'https-only', httpPort: 80, httpsPort: 443, originSslProtocols: ['TLSv1.2'], ...customOriginConfig, }, }; }