@studion/infra-code-blocks
Version:
Studion common infra components
258 lines (257 loc) • 11 kB
JavaScript
;
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,
},
};
}