Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(custom-resource): extend alias to app-level and domain-level for NLB #3070

Merged
merged 7 commits into from
Nov 24, 2021
130 changes: 118 additions & 12 deletions cf-custom-resources/lib/nlb-cert-validator-updater.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const DELAY_RECORD_SETS_CHANGE_IN_S = 30;
const ATTEMPTS_CERTIFICATE_VALIDATED = 19;
const DELAY_CERTIFICATE_VALIDATED_IN_S = 30;

let acm, envRoute53, envHostedZoneID, appName, envName, serviceName, certificateDomain;
let acm, appRoute53, envRoute53, rootHostedZoneID, appHostedZoneID, envHostedZoneID;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you think of something like this? Does it make testing difficult?

const clients = {
  app: {
     route53: ( () => { 
       let client;
       return () => {
          if (client) { return client }
          client = new AWS.Route53({ ...});
          return client;
        }
    })()
  },
  
  acm: (() => { let client; ... })(),
}

This way throughout the code we can do client.app.route53() or client.acm() everything is guaranteed to be a singleton and it removes all these global clients

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea - updated!

let appName, envName, serviceName, certificateDomain, domainTypes, rootDNSRole, domainName;
let defaultSleep = function (ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
};
Expand Down Expand Up @@ -78,22 +79,25 @@ function report (
}

exports.handler = async function (event, context) {
// Destruct resource properties into local variables.
const props = event.ResourceProperties;

let {LoadBalancerDNS: loadBalancerDNS,
LoadBalancerHostedZoneID: loadBalancerHostedZoneID,
DomainName: domainName,
} = props;
const aliases = new Set(props.Aliases);

acm = new AWS.ACM();
envRoute53 = new AWS.Route53();
// Initialize global variables.
envHostedZoneID = props.EnvHostedZoneId;
envName = props.EnvName;
appName = props.AppName;
serviceName = props.ServiceName;
domainName = props.DomainName;
rootDNSRole = props.RootDNSRole;
certificateDomain = `${serviceName}-nlb.${envName}.${appName}.${domainName}`;

// Load resources that are needed by default.
loadResources();

// NOTE: If the aliases have changed, then we need to replace the certificate being used, as well as deleting/adding
// validation records and A records. In general, any change in aliases indicate a "replacement" of the resources
// managed by the custom resource lambda; on the contrary, the same set of aliases indicate that there is no need to
Expand Down Expand Up @@ -141,8 +145,9 @@ async function validateAliases(aliases, loadBalancerDNS) {
let promises = [];

for (let alias of aliases) {
const promise = envRoute53.listResourceRecordSets({
HostedZoneId: envHostedZoneID,
let r = await domainResources(alias);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let r = await domainResources(alias);
let {hostedZoneId, route53, domain} = await domainResources(alias);

const promise = r.route53Client.listResourceRecordSets({
HostedZoneId: r.hostedZoneID,
MaxItems: "1",
StartRecordName: alias,
}).promise().then((data) => {
Expand Down Expand Up @@ -243,7 +248,6 @@ async function activate(validationOptions, certificateARN, loadBalancerDNS, load
promises.push(activateOption(option, loadBalancerDNS, loadBalancerHostedZone));
}
await Promise.all(promises);

await acm.waitFor("certificateValidated", {
// Wait up to 9 minutes and 30 seconds
$waiter: {
Expand Down Expand Up @@ -291,15 +295,16 @@ async function activateOption(option, loadBalancerDNS, loadBalancerHostedZone) {
});
}

let { ChangeInfo } = await envRoute53.changeResourceRecordSets({
let r = await domainResources(option.DomainName);
let { ChangeInfo } = await r.route53Client.changeResourceRecordSets({
ChangeBatch: {
Comment: "Validate the certificate and create A record for the alias",
Changes: changes,
},
HostedZoneId: envHostedZoneID,
HostedZoneId: r.hostedZoneID,
}).promise();

await envRoute53.waitFor('resourceRecordSetsChanged', {
await r.route53Client.waitFor('resourceRecordSetsChanged', {
// Wait up to 5 minutes
$waiter: {
delay: DELAY_RECORD_SETS_CHANGE_IN_S,
Expand All @@ -319,6 +324,107 @@ exports.deadlineExpired = function () {
});
};

/**
* Load clients and variables that can be reused between calls.
*/
function loadResources() {
if (!acm) {
acm = new AWS.ACM();
}

if (!envRoute53) {
envRoute53 = new AWS.Route53();
}

domainTypes = {
EnvDomainZone: {
regex: new RegExp(`^([^\.]+\.)?${envName}.${appName}.${domainName}`),
domain: `${envName}.${appName}.${domainName}`,
},
AppDomainZone: {
regex: new RegExp(`^([^\.]+\.)?${appName}.${domainName}`),
domain: `${appName}.${domainName}`,
},
RootDomainZone: {
regex: new RegExp(`^([^\.]+\.)?${domainName}`),
domain: `${domainName}`,
},
OtherDomainZone: {},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just curious about this domainType. How is it used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! This is not used. I should just delete it!

};

}

/**
* Lazy load application-level clients and variables that can be reused between calls.
*/
async function lazyLoadAppResources() {
lazyLoadAppRoute53Client();
if (!appHostedZoneID) {
appHostedZoneID = await hostedZoneID(`${appName}.${domainName}`);
}
}

/**
* Lazy load application-level clients and variables that can be reused between calls.
*/
async function lazyLoadRootResources() {
lazyLoadAppRoute53Client();
if (!rootHostedZoneID) {
rootHostedZoneID = await hostedZoneID(domainName);
}
}

function lazyLoadAppRoute53Client() {
if (appRoute53) {
return;
}
appRoute53 = new AWS.Route53({
credentials: new AWS.ChainableTemporaryCredentials({
params: { RoleArn: rootDNSRole, },
masterCredentials: new AWS.EnvironmentCredentials("AWS"),
}),
});
}

async function hostedZoneID(domain) {
const { HostedZones } = await appRoute53
.listHostedZonesByName({
DNSName: domain,
MaxItems: "1",
}).promise();
if (!HostedZones || HostedZones.length === 0) {
throw new Error( `Couldn't find any Hosted Zone with DNS name ${domainName}.`);
}
return HostedZones[0].Id.split("/").pop();
}

async function domainResources (alias) {
if (domainTypes.EnvDomainZone.regex.test(alias)) {
return {
domain: domainTypes.EnvDomainZone.domain,
route53Client: envRoute53,
hostedZoneID: envHostedZoneID,
};
}
if (domainTypes.AppDomainZone.regex.test(alias)) {
await lazyLoadAppResources();
return {
domain: domainTypes.AppDomainZone.domain,
route53Client: appRoute53,
hostedZoneID: appHostedZoneID,
};
}
if (domainTypes.RootDomainZone.regex.test(alias)) {
await lazyLoadRootResources();
return {
domain: domainTypes.RootDomainZone.domain,
route53Client: appRoute53,
hostedZoneID: rootHostedZoneID,
};
}
throw new Error(`unrecognized domain type for ${alias}`);
}

exports.withSleep = function (s) {
sleep = s;
};
Expand All @@ -328,4 +434,4 @@ exports.reset = function () {
exports.withDeadlineExpired = function (d) {
exports.deadlineExpired = d;
};
exports.attemptsValidationOptionsReady = ATTEMPTS_VALIDATION_OPTIONS_READY;
exports.attemptsValidationOptionsReady = ATTEMPTS_VALIDATION_OPTIONS_READY;
Loading