Skip to content

Commit

Permalink
integrate route53 and acm:
Browse files Browse the repository at this point in the history
 - automatically create ALIAS record(s) for distribution
 - automatically request and certificate using route53
  • Loading branch information
pecirep committed Feb 14, 2022
1 parent c93035a commit 8926169
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 4 deletions.
62 changes: 58 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const bucketUtils = require('./lib/bucketUtils');
const uploadDirectory = require('./lib/upload');
const validateClient = require('./lib/validate');
const invalidateCloudfrontDistribution = require('./lib/cloudFront');
const {groupDomainsByHostedZone} = require('./lib/route53');

class ServerlessFullstackPlugin {
constructor(serverless, cliOptions) {
Expand Down Expand Up @@ -256,8 +257,9 @@ class ServerlessFullstackPlugin {
filename: filename
});

this.prepareResources(resources);
return _.merge(baseResources, resources);
return this.prepareResources(resources).then(() => {
return _.merge(baseResources, resources);
});
}

checkForApiGataway() {
Expand Down Expand Up @@ -335,9 +337,11 @@ class ServerlessFullstackPlugin {
this.serverless.cli.consoleLog(` ${apiDistributionDomain.OutputValue} (CNAME: ${cnameDomain})`);
}

prepareResources(resources) {
async prepareResources(resources) {
const distributionConfig = resources.Resources.ApiDistribution.Properties.DistributionConfig;

await this.prepareRoute53(resources.Resources);

this.prepareLogging(distributionConfig);
this.prepareDomain(distributionConfig);
this.preparePriceClass(distributionConfig);
Expand All @@ -353,6 +357,56 @@ class ServerlessFullstackPlugin {

}

async prepareRoute53(resources) {
if (this.options.domain) {
const certificate = this.getConfig("certificate", null);
const distributionCertificate = resources.ApiDistribution.Properties.DistributionConfig.ViewerCertificate;

if (this.getConfig("route53", false) === true) {
const filename = path.resolve(__dirname, 'lib/resources/templates.yml');
const content = fs.readFileSync(filename, 'utf-8');
const templates = yaml.safeLoad(content, {filename});

const domains = Array.isArray(this.options.domain) ? this.options.domain : [this.options.domain];
const domainsByHostedZones = await groupDomainsByHostedZone(this.serverless, domains);
const domainsWithoutHostedZone = domainsByHostedZones
.filter((hostedZone) => !hostedZone.Id)
.reduce((acc, hostedZone) => [...acc, ...hostedZone.domains], []);
const filteredDomainsByHostedZones = domainsByHostedZones
.filter(hostedZone => !!hostedZone.Id && hostedZone.domains.length);

if (domainsWithoutHostedZone?.length > 0)
this.serverless.cli.log(`No hosted zones found for ${domainsWithoutHostedZone}, records pointing to`
+` the cloudfront domain will have to be added manually.`, "Route53", {color: "orange", underline: true});

const aliasTemplate = templates.Route53AliasTemplate;
const recordSetTemplate = aliasTemplate.Properties.RecordSets.pop();
recordSetTemplate.AliasTarget.DNSName = {"Fn::GetAtt": ["ApiDistribution", "DomainName"]};

for (const hostedZone of filteredDomainsByHostedZones) {
const recordSets = hostedZone.domains.map(domain => ({...recordSetTemplate, Name: domain}));
const alias = {...aliasTemplate, Properties: {...aliasTemplate.Properties, RecordSets: recordSets, HostedZoneId: hostedZone.Id}};
resources["Route53AliasHZ" + hostedZone.Id] = alias;
}

// only create and override if not specified
if (certificate === null) {
const certTemplate = templates.CertTemplate;
certTemplate.Properties.DomainName = domains[0];
if (domains.length > 1) certTemplate.Properties.SubjectAlternativeNames = domains.slice(1);

const route53domainValidations = filteredDomainsByHostedZones.flatMap(hz => hz.domains.map(DomainName => ({DomainName, HostedZoneId: hz.Id})));
const manualValidations = domainsWithoutHostedZone.map(DomainName => ({DomainName, ValidationDomain: DomainName}));
certTemplate.Properties.DomainValidationOptions = [...route53domainValidations, ...manualValidations];

const certResourceName = "ApiDistributionCertificate";
resources[certResourceName] = certTemplate;
distributionCertificate.AcmCertificateArn = {Ref: certResourceName};
}
}
}
}

prepareLogging(distributionConfig) {
const loggingBucket = this.getConfig('logging.bucket', null);

Expand Down Expand Up @@ -428,7 +482,7 @@ class ServerlessFullstackPlugin {
if (certificate !== null) {
this.serverless.cli.log(`Configuring SSL certificate...`);
distributionConfig.ViewerCertificate.AcmCertificateArn = certificate;
} else {
} else if (!distributionConfig.ViewerCertificate.AcmCertificateArn) {
delete distributionConfig.ViewerCertificate;
}
}
Expand Down
25 changes: 25 additions & 0 deletions lib/resources/templates.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Route53AliasTemplate:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneId: hostedZoneId
RecordSets:
- Name: domain.tld
Type: A
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW2 # The static CloudFront Hosted Zone ID
DNSName: # !GetAtt [ApiDistribution, DomainName]
EvaluateTargetHealth: false
# ...

CertTemplate:
Type: AWS::CertificateManager::Certificate
Properties:
DomainName: domain.tld
DomainValidationOptions:
# - DomainName: domain.tld
# HostedZoneId: hostedZoneId
# - DomainName: example.com
# ValidationDomain: example.com
# ...
ValidationMethod: DNS
# SubjectAlternativeNames:
18 changes: 18 additions & 0 deletions lib/route53.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// get hosted zones, group domains by HZ.Id using .reduce and extract values
const groupDomainsByHostedZone = async (serverless, domains) => {
const r53response = await serverless.getProvider('aws').request('Route53', 'listHostedZones', {});
// we only want raw Ids
const hostedZones = r53response.HostedZones.map(hz => ({...hz, Id: hz.Id.split("/").reverse()[0]}));

const hostedZoneMap = domains.reduce((accumulator, domain) => {
const hostedZone = hostedZones.find(hostedZone => `${domain}.`.includes(hostedZone.Name));
if (accumulator[hostedZone?.Id]) accumulator[hostedZone?.Id].domains.push(domain);
else accumulator[hostedZone?.Id] = {...hostedZone, domains: [domain]};
return accumulator;
}, {});
return Object.values(hostedZoneMap);
}

module.exports = {
groupDomainsByHostedZone
};

0 comments on commit 8926169

Please sign in to comment.