@0x4447/potato
Version:
🥔 Upload a static page to AWS S3 while automatically configuring CloudFront.
1,524 lines (1,230 loc) • 29.6 kB
JavaScript
let url = require('url');
let term = require('terminal-kit').terminal;
let upload = require('../helpers/upload');
//
// This promise is responsible for deploying a new static website by
// automatically configuring
//
// - Certificate Manager
// - Route 53
// - S3
// - CloudFront
//
module.exports = function(container) {
return new Promise(function(resolve, reject) {
ask_for_the_domain(container)
.then(function(container) {
return get_root_domain(container);
}).then(function(container) {
return list_all_certificates(container);
}).then(function(container) {
return look_for_domain_certificate(container);
}).then(function(container) {
return create_a_certificate(container);
}).then(function(container) {
return get_certificate_metadata(container);
}).then(function(container) {
return list_hosted_zones(container)
}).then(function(container) {
return look_for_domain(container)
}).then(function(container) {
return pre_certificate_check(container)
}).then(function(container) {
return update_route53_with_cert_validation(container);
}).then(function(container) {
return check_certificate_validity(container);
}).then(function(container) {
return check_if_bucket_exists(container)
}).then(function(container) {
return create_a_bucket(container)
}).then(function(container) {
return convert_bucket_to_site(container)
}).then(function(container) {
return change_bucket_policy(container)
}).then(function(container) {
return upload(container)
}).then(function(container) {
return create_a_distribution(container)
}).then(function(container) {
return get_all_domain_records(container);
}).then(function(container) {
return look_for_domain_entry(container);
}).then(function(container) {
return delete_domain_entry(container);
}).then(function(container) {
return create_a_route_53_record(container)
}).then(function(container) {
return print_domain_configuration(container)
}).then(function(container) {
return resolve(container)
}).catch(function(error) {
return reject(error);
});
});
}
// _____ _____ ____ __ __ _____ _____ ______ _____
// | __ \ | __ \ / __ \ | \/ | |_ _| / ____| | ____| / ____|
// | |__) | | |__) | | | | | | \ / | | | | (___ | |__ | (___
// | ___/ | _ / | | | | | |\/| | | | \___ \ | __| \___ \
// | | | | \ \ | |__| | | | | | _| |_ ____) | | |____ ____) |
// |_| |_| \_\ \____/ |_| |_| |_____| |_____/ |______| |_____/
//
//
// Ask the user the name of the domain they want to create
//
function ask_for_the_domain(container)
{
return new Promise(function(resolve, reject) {
//
// 1. Skip this view if the information was already passed in the
// CLI
//
if(container.bucket)
{
//
// -> Move to the next promise
//
return resolve(container);
}
term.clear();
term("\n");
term.yellow("\tType the domain name: ");
//
// 1. Listen for the user input
//
term.inputField({}, function(error, dns) {
//
// 1. Save the domain as is for any other sourpuss then changing
// the domain settings.
//
container.bucket = dns;
//
// -> Move to the next chain
//
return resolve(container);
});
});
}
//
// Get the root domain from the DNS address
//
function get_root_domain(container)
{
return new Promise(function(resolve, reject) {
//
// 2. Save the URL while getting the base domain, for example:
//
// subdomain.0x4447.com
//
// becomes
//
// 0x4447.com
//
// No matter how deep the subdomain goes.
//
container.domain = container.bucket.split('.').slice(-2).join('.');
//
// -> Move to the next chain
//
return resolve(container);
});
}
//
// List all the SSL certificates in the account
//
function list_all_certificates(container)
{
return new Promise(function(resolve, reject) {
term.clear();
term("\n");
term.yellow("\tGetting all Certificates...");
//
// 1. Ask AWS for all the certificates in the account
//
container.acm.listCertificates({}, function(error, data) {
//
// 1. Check if there was no error
//
if(error)
{
return reject(new Error(error.message));
}
//
// 2. Save an array of certs to the proceed
//
container.certificates = data.CertificateSummaryList
//
// -> Move to the next step once the animation finishes drawing
//
return resolve(container);
});
});
}
//
// Take the list of certificates that we got and look if we already have
// that certificate for that specific domain
//
function look_for_domain_certificate(container)
{
return new Promise(function(resolve, reject) {
term.clear();
term("\n");
term.yellow("\tLooking for Domain Certificate...");
//
// 1. Create a variable to store the ARN of the cert
//
let arn = null;
//
// 2. Loop over all the certs that we got
//
for(let key in container.certificates)
{
//
// 1. Look for a match
//
if(container.certificates[key].DomainName == container.bucket)
{
//
// 1. Save the ARN once it is found
//
arn = container.certificates[key].CertificateArn
//
// -> Stop the loop to preserve CPU
//
break;
}
}
//
// 3. Save the ARN to be used in the next chain
//
container.cert_arn = arn;
//
// -> Move to the next step once the animation finishes drawing
//
return resolve(container);
});
}
//
// If we didn't find the certificate then we create one
//
function create_a_certificate(container)
{
return new Promise(function(resolve, reject) {
//
// 1. Skip this step if the ARN is found
//
if(container.cert_arn)
{
//
// -> Move to the next chain
//
return resolve(container);
}
term.clear();
term("\n");
term.yellow("\tCreating the Certificate...");
//
// 2. Prepare the data to create a certificate
//
// Warning:
//
// IdempotencyToken - is used by AWS to understand if you
// by mistake made the same request multiple times. This
// way you won't get a ton of cert that are the same.
//
let params = {
DomainName: container.bucket,
IdempotencyToken: 'rnd_0x4447',
ValidationMethod: 'DNS'
};
//
// 3. Tell AWS that we want a new certificate
//
container.acm.requestCertificate(params, function(error, data) {
//
// 1. Check if there was no error
//
if(error)
{
return reject(new Error(error.message));
}
//
// 2. Save an array of certs to the proceed
//
container.cert_arn = data.CertificateArn;
//
// -> Move to the next step once the animation finishes drawing
//
return resolve(container);
});
});
}
//
// After creating the certificate we need to get some extra information
// so we can then use this information later in the code.
//
// This promise will also loop 30 times and wait since when you create a
// certificate all its meta-data won't be available right away.
//
function get_certificate_metadata(container)
{
return new Promise(function(resolve, reject) {
term.clear();
term("\n");
term.yellow("\tGetting Certificate Meta-Data...");
//
// 1. Make a variable that will keep all the information to create
// a certificate
//
let params = {
CertificateArn: container.cert_arn
};
//
// 2. Start the main loop and set the counter at 0
//
main(0)
//
// 3. The main function that will loop until it get the Resource
// record to then use to update the DNS setting of the domain
//
// We need to do it this way because when you create a Cert
// AWS will take a moment before the cert set in stone.
//
// This main will also timeout after 15 sec.
//
function main(count)
{
//
// 1. Get the full description of the cert
//
container.acm.describeCertificate(params, function(error, data) {
//
// 1. Check if there was no error
//
if(error)
{
return reject(new Error(error.message));
}
//
// 2. Save the information to validate the cert
//
let record = data.Certificate.DomainValidationOptions[0].ResourceRecord;
//
// 3. Check if we reached the limits of retries
//
if(count >= 30)
{
//
// 1. If we reached the limit we stop the app because
// there is no point in stressing out AWS
//
return reject(new Error("Unable to get a cert ARN"));
}
//
// 4. Check if we got the data that we need from AWS
//
if(record)
{
//
// 1. Save the data for the next chain
//
container.cert_validation = record
//
// -> Move to the next step once the animation finishes
// drawing
//
return resolve(container);
}
//
// 5. Set a timeout of 1 sec
//
setTimeout(function() {
//
// 1. Increases the counter so we can keep track of how
// many loops did we do.
//
count++;
//
// 2. Restart the main function to check if now we'll
// get what we need
//
main(count);
}, 1000);
});
}
});
}
//
// Query Route 53 to get all the domains that are available in the account
//
function list_hosted_zones(container)
{
return new Promise(function(resolve, reject) {
term.clear();
term("\n");
term.yellow("\tListing Hosted Zones...");
//
// 1. Query Route 53 for all the Zones (domains)
//
container.route53.listHostedZones({}, function(error, data) {
//
// 1. Check if there was an error
//
if(error)
{
return reject(error);
}
//
// 3. Save the result for the next chain
//
container.zones = data.HostedZones;
//
// -> Move to the next chain
//
return resolve(container);
});
});
}
//
// Go over the array of domains that we got and look for the one that we
// care about
//
function look_for_domain(container)
{
return new Promise(function(resolve, reject) {
term.clear();
term("\n");
term.yellow("\tLooking for Domain...");
//
// 1. Create a variable that will store the Zone ID
//
let zone_id = '';
//
// 2. Loop over all the Zones that we got to look for the
// domain and grab the Zone ID
//
for(let key in container.zones)
{
//
// 1. Compare the domains
//
if(container.zones[key].Name == container.domain + '.')
{
//
// 1. Save the Zone ID
//
zone_id = container.zones[key].Id.split("/")[2];
//
// -> Brake to preserve CPU cycles
//
break;
}
}
//
// 3. Save the zone ID for later
//
container.zone_id = zone_id;
//
// -> Move to the next chain
//
return resolve(container);
});
}
//
// First we do a quick check if the certificate is already verified, and based
// on this we might skip other future steps
//
function pre_certificate_check(container)
{
return new Promise(function(resolve, reject) {
term.clear();
term("\n");
term.yellow("\tPre check of the certificate...");
//
// 1. Make a variable that will keep all the information to create
// a certificate
//
let params = {
CertificateArn: container.cert_arn
};
//
// 1. Get the full description of the cert
//
container.acm.describeCertificate(params, function(error, data) {
//
// 1. Check if there was no error
//
if(error)
{
return reject(new Error(error.message));
}
//
// 2. Save the information to validate the cert
//
let status = data.Certificate.DomainValidationOptions[0].ValidationStatus;
//
// 4. Check if the cert is valid
//
if(status === 'SUCCESS')
{
//
// 1. Mark the cert to be valid so we can make future
// decision in the future.
//
container.cert_already_valid = true;
}
//
// -> Move to the next step once the animation finishes
// drawing
//
return resolve(container);
});
});
}
//
// Now that we have a new certificate, and we also found the Zone ID of the
// domain, we can add a new entry i the DNS so AWS can confirm the cert
// for us.
//
function update_route53_with_cert_validation(container)
{
return new Promise(function(resolve, reject) {
//
// 1. Check if:
//
// - No Domain found on Route 53
// - and Cert is invalid
//
// And if this matches, then we display instructions how to
// update the DNS to allow the cert verification.
//
if(!container.zone_id && !container.cert_already_valid)
{
term("\n");
term("\n");
term.yellow("\tYour domain is not managed by Route 53, please edit the DNS record and add the following:");
term("\n");
term("\n");
term.yellow("\t" + container.cert_validation.Name + " CNAME " + container.cert_validation.Value);
term("\n");
term("\n");
//
// -> After displaying the message stop the app.
//
return process.exit(11);
}
//
// 2. Skip this step if the cert is already valid
//
if(container.cert_already_valid)
{
//
// -> Move to the next chain
//
return resolve(container);
}
term.clear();
term("\n");
term.yellow("\tUpdate Route 53 with Certificate validation...");
//
// 1. Create all the options to create a new record that will
// be used to confirm the ownership of the cert
//
let params = {
ChangeBatch: {
Changes: [
{
Action: "CREATE",
ResourceRecordSet: {
Name: container.cert_validation.Name,
ResourceRecords: [{
Value: container.cert_validation.Value
}],
TTL: 60,
Type: container.cert_validation.Type
}
}
],
Comment: "Proof of ownership"
},
HostedZoneId: container.zone_id
};
//
// 2. Create a new DNS record
//
container.route53.changeResourceRecordSets(params, function(error, data) {
//
// 1. Check if there was no error
//
if(error)
{
return reject(new Error(error.message));
}
//
// -> Move to the next step once the animation finishes drawing
//
return resolve(container);
});
});
}
//
// After we added the entry to validate the cert we need to wait for
// AWS to validate it. To do this we are going to constantly check the
// state of the cert until it gets confirmed.
//
function check_certificate_validity(container)
{
return new Promise(function(resolve, reject) {
term.clear();
term("\n");
term.yellow("\tWaiting 60 sec for Certificate to validate...");
//
// 1. Make a variable that will keep all the information to create
// a certificate
//
let params = {
CertificateArn: container.cert_arn
};
//
// 2. Start the main loop and set the counter at 0
//
main(0);
//
// 3. The main function that will loop until it get the Resource
// record to then use to update the DNS setting of the domain
//
// We need to do it this way because when you create a Cert
// AWS will take a moment before the cert set in stone.
//
// This main will also timeout after 60 sec.
//
function main(count)
{
//
// 1. Get the full description of the cert
//
container.acm.describeCertificate(params, function(error, data) {
//
// 1. Check if there was no error
//
if(error)
{
return reject(new Error(error.message));
}
//
// 2. Save the information to validate the cert
//
let status = data.Certificate.DomainValidationOptions[0].ValidationStatus;
//
// 3. Check if we reached the limits of retries
//
if(status === 'FAILED')
{
//
// 1. If we reached the limit we stop the app because
// there is no point in stressing out AWS
//
return reject(new Error("Cert failed to confirm"));
}
//
// 4. Check if we got the data that we need from AWS
//
if(status === 'SUCCESS')
{
//
// -> Move to the next step once the animation finishes
// drawing
//
return resolve(container);
}
//
// 3. Check if we reached the limits of retries
//
if(count >= 60)
{
//
// 1. Explain the situation
//
term("\n");
term("\n");
term.yellow("\tWe did try for 30 sec but the cert is still waiting for validation.");
term("\n");
term.yellow("\tYou should visit the following AWS Console section to monitor the cert");
term("\n");
term("\n");
term.yellow("\thttps://console.aws.amazon.com/acm/home?region=" + container.region);
term("\n");
term("\n");
term.yellow("\tOnce the cert is validated re-run this CLI with the same domain that you used before.");
term("\n");
term("\n");
//
// -> Exit the app since there is nothing more to do.
//
return process.exit(11);
}
//
// 5. Set a timeout of 1 sec
//
setTimeout(function() {
//
// 1. Increases the counter so we can keep track of how
// many loops did we do.
//
count++;
//
// 2. Restart the main function to check if now we'll
// get what we need
//
main(count);
}, 1000);
});
}
});
}
//
// Before we do anything use the domain provided by the user and we check to
// see if it is exists already by trying to read the default index.html file
//
function check_if_bucket_exists(container)
{
return new Promise(function(resolve, reject) {
term.clear();
term("\n");
term.yellow("\tChecking if the S3 Bucket Exists...");
//
// 1. The options for S3
//
let params = {
Bucket: container.bucket,
Key: 'home'
};
//
// 2. Try to get a file from the provided bucket
//
container.s3.getObject(params, function(error, data) {
//
// 1. Check if there was an error
//
if(data)
{
return reject(new Error("The website already exists"));
}
//
// -> Move to the next chain
//
return resolve(container);
});
});
}
//
// Now that we know that the bucked doesn't exists we can create it
//
function create_a_bucket(container)
{
return new Promise(function(resolve, reject) {
term.clear();
term("\n");
term.yellow("\tCreating the S3 Bucket...");
//
// 1. The options for S3
//
let params = {
Bucket: container.bucket
};
//
// 2. Create the bucket
//
container.s3.createBucket(params, function(error, data) {
//
// 1. Check if there was an error
//
if(error)
{
return reject(error);
}
//
// 2. Make a precise Bucket URL so CloudFront will redirect all
// request to the main domain and not straight to the
// S3 Bucket prior to the domain propagation
//
container.bucket_url_path = container.bucket
+ '.s3-website-'
+ container.region
+ '.amazonaws.com';
//
// 3. Before we move to the next chain we need to give AWS
// a moment to finalize creating the S3 bucket. Sadly the
// callback is not being triggered when the Bucket is 100%
// done and if you have a fast connection you can go to
// the next step faster then AWS can make a bucket.
//
setTimeout(function() {
//
// -> Move to the next chain
//
return resolve(container);
}, 1500);
});
});
}
//
// After creating a bucket we need to tell S3 that this bucket will be
// hosting a website
//
function convert_bucket_to_site(container)
{
return new Promise(function(resolve, reject) {
term.clear();
term("\n");
term.yellow("\tConverting the S3 Bucket in to a website...");
//
// 1. The options for S3
//
let params = {
Bucket: container.bucket,
WebsiteConfiguration: {
ErrorDocument: {
Key: "error"
},
IndexDocument: {
Suffix: "home"
}
}
};
//
// 2. Convert the bucket in to a website
//
container.s3.putBucketWebsite(params, function(error, data) {
//
// 1. Check if there was an error
//
if(error)
{
return reject(error);
}
//
// -> Move to the next chain
//
return resolve(container);
});
});
}
//
// Update the Bucket policy to make sure it is accessible by the public.
// Otherwise CloudFront won't be able to publish the site.
//
function change_bucket_policy(container)
{
return new Promise(function(resolve, reject) {
term.clear();
term("\n");
term.yellow("\tChanging S3 Bucket Policy...");
//
// 1. Set the parameters to change the Bucket policy
//
let params = {
Bucket: container.bucket,
Policy: JSON.stringify({
Version: '2012-10-17',
Statement: [
{
Sid: 'PublicReadGetObject',
Effect: 'Allow',
Principal: '*',
Action: 's3:GetObject',
Resource: 'arn:aws:s3:::' + container.bucket + '/*'
}
]
})
};
//
// 2. Replace the Policy
//
container.s3.putBucketPolicy(params, function(error, data) {
//
// 1. Check if there was an error
//
if(error)
{
return reject(error);
}
//
// -> Move to the next chain
//
return resolve(container);
});
});
}
////////////////////////////////////////////////////////////////////////////////
//
// Upload the file to the S3 bucket so we can deliver something
//
// .upload();
//
////////////////////////////////////////////////////////////////////////////////
//
// Now that we have everything, we can finally use all this data and
// create a CloudFront Distribution
//
function create_a_distribution(container)
{
return new Promise(function(resolve, reject) {
term.clear();
term("\n");
term.yellow("\tCreating a CloudFront Distribution...");
//
// 1. All the setting necessary to create a CF Distribution
//
let params = {
DistributionConfig: {
CallerReference: new Date().toString(),
Comment: '-',
DefaultCacheBehavior: {
ForwardedValues: {
Cookies: {
Forward: 'none'
},
QueryString: false,
Headers: {
Quantity: 0
},
QueryStringCacheKeys: {
Quantity: 0
}
},
MinTTL: 0,
TargetOriginId: container.bucket_url_path,
TrustedSigners: {
Enabled: false,
Quantity: 0
},
ViewerProtocolPolicy: 'redirect-to-https',
AllowedMethods: {
Items: ['GET', 'HEAD'],
Quantity: 2,
CachedMethods: {
Items: ['GET', 'HEAD'],
Quantity: 2
}
},
Compress: true,
DefaultTTL: 86400,
LambdaFunctionAssociations: {
Quantity: 0,
},
MaxTTL: 31536000,
SmoothStreaming: false
},
Enabled: true,
Origins: {
Quantity: 1,
Items: [{
DomainName: container.bucket_url_path,
Id: container.bucket_url_path,
CustomOriginConfig: {
HTTPPort: 80,
HTTPSPort: 443,
OriginProtocolPolicy: 'http-only',
OriginSslProtocols: {
Quantity: 1,
Items: ['TLSv1.1']
}
}
}]
},
Aliases: {
Quantity: 1,
Items: [container.bucket]
},
CacheBehaviors: {
Quantity: 0
},
CustomErrorResponses: {
Quantity: 0
},
DefaultRootObject: 'home',
HttpVersion: 'http2',
IsIPV6Enabled: true,
PriceClass: 'PriceClass_100',
Restrictions: {
GeoRestriction: {
Quantity: 0,
RestrictionType: 'none'
}
},
ViewerCertificate: {
ACMCertificateArn: container.cert_arn,
CloudFrontDefaultCertificate: false,
MinimumProtocolVersion: 'TLSv1.1_2016',
SSLSupportMethod: 'sni-only'
}
}
};
//
// 2. Create the distribution
//
container.cloudfront.createDistribution(params, function(error, data) {
//
// 1. Check if there was an error
//
if(error)
{
return reject(error);
}
//
// 2. Save the unique domain name of CloudFront which will
// be used to create a DNS record so the domain will
// point in the right place
//
container.cloudfront_domain_name = data.Distribution.DomainName
//
// -> Move to the next chain
//
return resolve(container);
});
});
}
//
// Get all the DNS settings for a specific domain so we can see if what
// we want to set can be set.
//
function get_all_domain_records(container)
{
return new Promise(function(resolve, reject) {
term.clear();
term("\n");
term.yellow("\tGetting all the Domain Records...");
//
// 1. Specify the Domain that we want the date from
//
params = {
HostedZoneId: container.zone_id
};
//
// 2. Request all the DSN records
//
container.route53.listResourceRecordSets(params, function(error, data) {
//
// 1. Check if there was an error
//
if(error)
{
return reject(error);
}
//
// 2. Save the result for the next chain
//
container.entries = data.ResourceRecordSets
//
// -> Move to the next chain
//
return resolve(container);
});
});
}
//
// Loop over the DNS records and check if the record type that we want to
// already exists
//
function look_for_domain_entry(container)
{
return new Promise(function(resolve, reject) {
term.clear();
term("\n");
term.yellow("\tLooking for domain entry...");
//
// 1. Create a variable that will store the DNS entry
//
let dns_entry = null;
//
// 2. Loop over all the Zones that we got to look for the
// domain and grab the Zone ID
//
for(let key in container.entries)
{
//
// 1. Check if the domain name entry matches our own
//
if(container.entries[key].Name == container.bucket + '.')
{
//
// 1. Once the have the domain matching see if it has a
// record of type A
//
if(container.entries[key].Type == 'A')
{
//
// 1. Save the whole object since it is needed to
// delete the entry, and this whole object is
// used to match the delete action
//
dns_entry = container.entries[key]
//
// -> Brake to preserve CPU cycles
//
break;
}
}
}
//
// 3. Save the entry for the next entry
//
container.dns_entry = dns_entry;
//
// -> Move to the next chain
//
return resolve(container);
});
}
//
// Make sure that if there is already a A entry in the domain setting we
// remove it before adding the new one.
//
function delete_domain_entry(container)
{
return new Promise(function(resolve, reject) {
//
// 1. Check if a record was found
//
if(!container.dns_entry)
{
//
// -> Move to the next step
//
return resolve(container);
}
term.clear();
term("\n");
term.yellow("\tDeleting duplicate entry...");
//
// 2. Create the Delete action for Route 53
//
let params = {
ChangeBatch: {
Changes: [{
Action: "DELETE",
ResourceRecordSet: container.dns_entry
}]
},
HostedZoneId: container.zone_id
};
//
// 3. Perform the action on Route 53
//
container.route53.changeResourceRecordSets(params, function(error, data) {
//
// 1. Check if there was no error
//
if(error)
{
return reject(new Error(error.message));
}
//
// 2. Wait few sec since the delete action is not instant, but
// it is fast enough that constantly checking is overkill
//
setTimeout(function() {
//
// -> Move to the next step
//
return resolve(container);
}, 3000)
});
});
}
//
// Create a DNS entry that will point the domain to the CloudFront
// Distribution. If the domain is not found in Route 53 we just display
// what needs to be set in the DNS
//
function create_a_route_53_record(container)
{
return new Promise(function(resolve, reject) {
//
// 1. Update a record only if we have the domain in ROute 53
//
if(!container.zone_id)
{
//
// -> Move to the next chain
//
return resolve(container);
}
term.clear();
term("\n");
term.yellow("\tCreating a new Route 53 entry...");
//
// 2. All the options to add a new record
//
let options = {
ChangeBatch: {
Changes: [{
Action: "CREATE",
ResourceRecordSet: {
AliasTarget: {
DNSName: container.cloudfront_domain_name,
EvaluateTargetHealth: false,
HostedZoneId: 'Z2FDTNDATAQYW2' // Fixed ID CloudFront distribution
},
Name: container.bucket,
Type: "A"
}
}],
Comment: "S3 Hosted Site"
},
HostedZoneId: container.zone_id
};
//
// 3. Execute the change on Route 53
//
container.route53.changeResourceRecordSets(options, function(error, data) {
//
// 1. Check if there was an error
//
if(error)
{
return reject(error);
}
//
// -> Move to the next chain
//
return resolve(container);
});
});
}
//
// If we don't have the domain in Route 53 then we just print out
// what needs to be set in the domain to make sure all the traffic
// goes to CloudFront
//
function print_domain_configuration(container)
{
return new Promise(function(resolve, reject) {
//
// 1. Skip this step if we have the domain in Route 53 because
// it means that we were able to automatically update
// the record
//
if(container.zone_id)
{
//
// -> Move to the next chain
//
return resolve(container);
}
term.clear();
term("\n");
term.brightWhite("\tPlease update your DNS record with the following...");
term("\n");
term.brightWhite("\tPoint your domain name " + container.domain + " to the following A record");
term("\n");
term.brightWhite("\t" + container.cloudfront_domain_name);
term("\n");
term("\n");
//
// -> Move to the next chain
//
return resolve(container);
});
}