serverless-multi-regions
Version:
Deploy an API Gateway service in multiple regions with a global CloudFront distribution and health checks
346 lines (291 loc) • 11.7 kB
JavaScript
const path = require('path');
const _ = require('lodash');
const yaml = require('js-yaml');
const fs = require('fs');
class Plugin {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options;
this.hooks = {
'before:deploy:createDeploymentArtifacts': this.createDeploymentArtifacts.bind(this)
};
}
createDeploymentArtifacts() {
if (!this.serverless.service.custom.cdn) {
this.serverless.service.custom.cdn = {};
}
if (!this.serverless.service.custom.dns) {
this.serverless.service.custom.dns = {};
}
const disabled = this.serverless.service.custom.cdn.disabled;
if (disabled !== undefined && disabled) {
return;
}
this.fullDomainName = this.serverless.service.custom.dns.domainName;
if (!this.fullDomainName) {
this.serverless.cli.log('The domainName parameter is required');
return;
}
const hostSegments = this.fullDomainName.split('.');
if (hostSegments.length < 3) {
this.serverless.cli.log(`The domainName was not valid: ${this.fullDomainName}.`);
return;
}
this.hostName = `${hostSegments[hostSegments.length - 3]}.${hostSegments[hostSegments.length - 2]}.${hostSegments[hostSegments.length - 1]}`; // eslint-disable-line
this.regionalDomainName = this.buildRegionalDomainName(hostSegments);
const baseResources = this.serverless.service.provider.compiledCloudFormationTemplate;
const filename = path.resolve(__dirname, 'resources.yml'); // eslint-disable-line
const content = fs.readFileSync(filename, 'utf-8');
const resources = yaml.load(content, {
filename: filename
});
return this.prepareResources(resources).then(() => {
this.serverless.cli.log(
`The serverless-multi-region plugin completed resources: ${yaml.dump(resources)}`
);
_.merge(baseResources, resources);
});
}
prepareResources(resources) {
const credentials = this.serverless.providers.aws.getCredentials();
const acmCredentials = Object.assign({}, credentials, {region: this.options.region});
this.acm = new this.serverless.providers.aws.sdk.ACM(acmCredentials);
const distributionConfig = resources.Resources.ApiDistribution.Properties.DistributionConfig;
const cloudFrontRegion = this.serverless.service.custom.cdn.region;
const enabled = this.serverless.service.custom.cdn.enabled;
let createCdn = true;
if (
cloudFrontRegion !== this.options.region ||
(enabled && !enabled.includes(this.options.stage))
) {
createCdn = false;
delete resources.Resources.ApiGlobalEndpointRecord;
delete resources.Outputs.ApiDistribution;
delete resources.Outputs.GlobalEndpoint;
} else {
this.prepareCdnComment(distributionConfig);
this.prepareCdnOrigins(distributionConfig);
this.prepareCdnHeaders(distributionConfig);
this.prepareCdnPriceClass(distributionConfig);
this.prepareCdnAliases(distributionConfig);
this.prepareCdnLogging(distributionConfig);
this.prepareCdnWaf(distributionConfig);
this.prepareApiGlobalEndpointRecord(resources);
}
this.prepareApiRegionalBasePathMapping(resources);
this.prepareApiRegionalEndpointRecord(resources);
this.prepareApiRegionalHealthCheck(resources);
return this.prepareApiRegionalDomainSettings(resources).then(() => {
if (createCdn) {
return this.prepareCdnCertificate(distributionConfig);
} else {
delete resources.Resources.ApiDistribution;
}
});
}
buildRegionalDomainName(hostSegments) {
let regionalDomainName = this.serverless.service.custom.dns.regionalDomainName;
if (!regionalDomainName) {
const lastNonHostSegment = hostSegments[hostSegments.length - 3];
hostSegments[hostSegments.length - 3] = `${lastNonHostSegment}-${this.options.stage}`;
regionalDomainName = hostSegments.join('.');
}
return regionalDomainName;
}
prepareApiRegionalDomainSettings(resources) {
const properties = resources.Resources.ApiRegionalDomainName.Properties;
properties.DomainName = this.regionalDomainName;
const regionSettings = this.serverless.service.custom.dns[this.options.region];
if (regionSettings) {
const acmCertificateArn = regionSettings.acmCertificateArn;
if (acmCertificateArn) {
properties.RegionalCertificateArn = acmCertificateArn;
return Promise.resolve();
}
}
return this.getCertArnFromHostName().then(certArn => {
if (certArn) {
properties.RegionalCertificateArn = certArn;
} else {
delete properties.RegionalCertificateArn;
}
});
}
prepareApiRegionalBasePathMapping(resources) {
const apiGatewayStubDeployment = resources.Resources.ApiGatewayStubDeployment;
apiGatewayStubDeployment.DependsOn =
this.serverless.service.custom.gatewayMethodDependency || 'ApiGatewayMethodProxyVarAny';
apiGatewayStubDeployment.Properties.StageName = this.options.stage;
const properties = resources.Resources.ApiRegionalBasePathMapping.Properties;
properties.Stage = this.options.stage;
}
prepareApiRegionalEndpointRecord(resources) {
const properties = resources.Resources.ApiRegionalEndpointRecord.Properties;
const hostedZoneId = this.serverless.service.custom.dns.hostedZoneId;
if (hostedZoneId) {
delete properties.HostedZoneName;
properties.HostedZoneId = hostedZoneId;
} else {
delete properties.HostedZoneId;
properties.HostedZoneName = `${this.hostName}.`;
}
const regionSettings = this.serverless.service.custom.dns[this.options.region];
if (regionSettings && regionSettings.failover) {
delete properties.Region;
properties.Failover = regionSettings.failover;
} else {
delete properties.Failover;
properties.Region = this.options.region;
}
properties.SetIdentifier = this.options.region;
const elements = resources.Outputs.RegionalEndpoint.Value['Fn::Join'][1];
if (elements[2]) {
elements[2] = `/${this.options.stage}`;
}
}
prepareApiRegionalHealthCheck(resources) {
const dnsSettings = this.serverless.service.custom.dns;
const regionSettings = dnsSettings[this.options.region];
const properties = resources.Resources.ApiRegionalEndpointRecord.Properties;
if (regionSettings && regionSettings.healthCheckId) {
properties.HealthCheckId = regionSettings.healthCheckId;
delete resources.Resources.ApiRegionalHealthCheck;
} else {
const healthCheckProperties = resources.Resources.ApiRegionalHealthCheck.Properties;
if (dnsSettings.healthCheckResourcePath) {
healthCheckProperties.HealthCheckConfig.ResourcePath = dnsSettings.healthCheckResourcePath;
} else {
healthCheckProperties.HealthCheckConfig.ResourcePath = `/${this.options.stage}/healthcheck`;
}
}
}
prepareCdnComment(distributionConfig) {
const name = this.serverless.getProvider('aws').naming.getApiGatewayName();
distributionConfig.Comment = `API: ${name}`;
}
prepareCdnOrigins(distributionConfig) {
distributionConfig.Origins[0].DomainName = this.regionalDomainName;
}
prepareCdnHeaders(distributionConfig) {
const headers = this.serverless.service.custom.cdn.headers;
if (headers) {
distributionConfig.DefaultCacheBehavior.ForwardedValues.Headers = headers;
} else {
distributionConfig.DefaultCacheBehavior.ForwardedValues.Headers = ['Accept', 'Authorization'];
}
}
prepareCdnPriceClass(distributionConfig) {
const priceClass = this.serverless.service.custom.cdn.priceClass;
if (priceClass) {
distributionConfig.PriceClass = priceClass;
} else {
distributionConfig.PriceClass = 'PriceClass_100';
}
}
prepareCdnAliases(distributionConfig) {
let aliases = this.serverless.service.custom.cdn.aliases;
if (aliases) {
if (!aliases.length || aliases.length === 0) {
delete distributionConfig.Aliases;
}
distributionConfig.Aliases = aliases;
} else {
aliases = [this.fullDomainName];
distributionConfig.Aliases = aliases;
}
}
prepareCdnCertificate(distributionConfig) {
const acmCertificateArn = this.serverless.service.custom.cdn.acmCertificateArn;
if (acmCertificateArn) {
distributionConfig.ViewerCertificate.AcmCertificateArn = acmCertificateArn;
return Promise.resolve();
} else {
return this.getCertArnFromHostName().then(certArn => {
if (certArn) {
distributionConfig.ViewerCertificate.AcmCertificateArn = certArn;
} else {
delete distributionConfig.ViewerCertificate;
}
});
}
}
prepareCdnLogging(distributionConfig) {
const logging = this.serverless.service.custom.cdn.logging;
if (logging) {
distributionConfig.Logging.Bucket = `${logging.bucketName}.s3.amazonaws.com`;
distributionConfig.Logging.Prefix =
logging.prefix ||
`aws-cloudfront/api/${this.options.stage}/${this.serverless
.getProvider('aws')
.naming.getStackName()}`;
} else {
delete distributionConfig.Logging;
}
}
prepareCdnWaf(distributionConfig) {
const webACLId = this.serverless.service.custom.cdn.webACLId;
if (webACLId) {
distributionConfig.WebACLId = webACLId;
} else {
delete distributionConfig.WebACLId;
}
}
prepareApiGlobalEndpointRecord(resources) {
const properties = resources.Resources.ApiGlobalEndpointRecord.Properties;
const hostedZoneId = this.serverless.service.custom.dns.hostedZoneId;
if (hostedZoneId) {
delete properties.HostedZoneName;
properties.HostedZoneId = hostedZoneId;
} else {
delete properties.HostedZoneId;
properties.HostedZoneName = `${this.hostName}.`;
}
properties.Name = `${this.fullDomainName}.`;
const elements = resources.Outputs.GlobalEndpoint.Value['Fn::Join'][1];
if (elements[1]) {
elements[1] = this.fullDomainName;
}
}
/*
* Obtains the certification arn
*/
getCertArnFromHostName() {
const certRequest = this.acm
.listCertificates({CertificateStatuses: ['PENDING_VALIDATION', 'ISSUED', 'INACTIVE']})
.promise();
return certRequest
.then(data => {
// The more specific name will be the longest
let nameLength = 0;
let certArn;
const certificates = data.CertificateSummaryList;
// Derive certificate from domain name
certificates.forEach(certificate => {
let certificateListName = certificate.DomainName;
// Looks for wild card and takes it out when checking
if (certificateListName[0] === '*') {
certificateListName = certificateListName.substr(2);
}
// Looks to see if the name in the list is within the given domain
// Also checks if the name is more specific than previous ones
if (
this.hostName.includes(certificateListName) &&
certificateListName.length > nameLength
) {
nameLength = certificateListName.length;
certArn = certificate.CertificateArn;
}
});
if (certArn) {
this.serverless.cli.log(
`The host name ${this.hostName} resolved to the following certificateArn: ${certArn}`
);
}
return certArn;
})
.catch(err => {
throw Error(`Error: Could not list certificates in Certificate Manager.\n${err}`);
});
}
}
module.exports = Plugin;