@serverless/domain
Advanced tools
Comparing version 2.0.0 to 2.0.1
{ | ||
"name": "@serverless/domain", | ||
"version": "2.0.0", | ||
"version": "2.0.1", | ||
"main": "./serverless.js", | ||
@@ -5,0 +5,0 @@ "publishConfig": { |
# Domain | ||
Easily provision custom domains for: | ||
- Static websites hosted on AWS CloudFront & AWS S3 via the Website Component. | ||
- APIs built via the Backend Component. | ||
### Inputs | ||
```yaml | ||
domain: | ||
component: '@serverless/domain' | ||
inputs: | ||
domain: mywebsite.com | ||
dns: | ||
$: ${websiteComponentInstance} | ||
api: ${backendComponentInstance} | ||
admin: ${anotherWebsiteComponentInstance} | ||
``` | ||
### Set-Up | ||
First, you must purchase your domain within Route53 manually. Then add it to the Component, as demonstrated above. | ||
### Using With The Website Component | ||
When used with the Website Component, the domain component will... | ||
- Create an AWS ACM Certificate, if one does not already exists for the domain. | ||
- Create an AWS Cloudfront Distribution that uses the AWS ACM Certificate. | ||
- Create Records in AWS Route 53 to point your custom domain to your AWS Cloudfront Distribution. |
const { Component } = require('@serverless/core') | ||
const { | ||
getClients, | ||
getDomains, | ||
prepareDomains, | ||
getDomainHostedZoneId, | ||
getCertificateArn, | ||
deployS3Domain, | ||
deployApiDomain | ||
describeCertificateByArn, | ||
getCertificateArnByDomain, | ||
createCertificate, | ||
validateCertificate, | ||
createCloudfrontDistribution, | ||
getCloudFrontDistributionByDomain, | ||
invalidateCloudfrontDistribution, | ||
deployApiDomain, | ||
removeApiDomain, | ||
removeApiDomainDnsRecords, | ||
configureDnsForCloudFrontDistribution, | ||
getApiDomainName, | ||
removeWebsiteDomainDnsRecords | ||
} = require('./utils') | ||
class Domain extends Component { | ||
/** | ||
* Remove | ||
*/ | ||
async default(inputs = {}) { | ||
this.context.status('Deploying') | ||
this.context.debug(`Starting Domain component deployment.`) | ||
this.context.debug(`Validating inputs.`) | ||
inputs.region = inputs.region || 'us-east-1' | ||
if (!inputs.domain) { | ||
throw Error(`"domain" is a required input.`) | ||
} | ||
const domains = getDomains(inputs) | ||
const clients = getClients(this.context.credentials.aws, inputs.region) | ||
// TODO: Check if domain has changed. | ||
// On domain change, call remove for all previous state. | ||
// Save domain | ||
this.state.domain = inputs.domain | ||
await this.save() | ||
// Get AWS SDK Clients | ||
const clients = getClients(this.context.credentials.aws) | ||
this.context.debug(`Formatting domains and identifying cloud services being used.`) | ||
const domains = prepareDomains(inputs) | ||
this.context.debug(`Getting the Hosted Zone ID for the domain ${inputs.domain}.`) | ||
const domainHostedZoneId = await getDomainHostedZoneId(clients.route53, inputs.domain) | ||
const certificateArn = await getCertificateArn( | ||
clients.acm, | ||
clients.route53, | ||
inputs.domain, | ||
domainHostedZoneId | ||
// // Get AWS ACM Certificate by ARN | ||
// this.context.debug(`Checking if an AWS ACM Certificate already exists.`) | ||
// let certificate | ||
// if (this.state.awsAcmCertificateArn) { | ||
// try { | ||
// certificate = await describeCertificateByArn(clients.acm, this.state.awsAcmCertificateArn) | ||
// this.context.debug(`AWS ACM Certificate already exists.`) | ||
// } catch (error) { | ||
// if (error.code === 'ResourceNotFoundException') { | ||
// this.context.debug( | ||
// `Couldn't find Certificate based on ARN: ${this.state.awsAcmCertificateArn}.` | ||
// ) | ||
// } else { | ||
// throw new Error(error) | ||
// } | ||
// } | ||
// } | ||
this.context.debug( | ||
`Searching for an AWS ACM Certificate based on the domain: ${inputs.domain}.` | ||
) | ||
let certificateArn = await getCertificateArnByDomain(clients.acm, inputs.domain) | ||
if (!certificateArn) { | ||
this.context.debug(`No existing AWS ACM Certificates found for the domain: ${inputs.domain}.`) | ||
this.context.debug(`Creating a new AWS ACM Certificate for the domain: ${inputs.domain}.`) | ||
certificateArn = await createCertificate(clients.acm, inputs.domain) | ||
} | ||
this.context.debug(`Checking the status of AWS ACM Certificate.`) | ||
const certificate = await describeCertificateByArn(clients.acm, certificateArn) | ||
if (certificate.Status === 'PENDING_VALIDATION') { | ||
this.context.debug(`AWS ACM Certificate Validation Status is "PENDING_VALIDATION".`) | ||
this.context.debug(`Validating AWS ACM Certificate via Route53 "DNS" method.`) | ||
await validateCertificate( | ||
clients.acm, | ||
clients.route53, | ||
certificate, | ||
inputs.domain, | ||
domainHostedZoneId | ||
) | ||
this.context.log( | ||
'Your AWS ACM Certificate has been created and is being validated via DNS. This could take up to 30 minutes since it depends on DNS propagation. Continuining deployment, but you may have to wait for DNS propagation.' | ||
) | ||
} | ||
if (certificate.Status !== 'ISSUED' && certificate.Status !== 'PENDING_VALIDATION') { | ||
// TODO: Should we auto-create a new one in this scenario? | ||
throw new Error( | ||
`Your AWS ACM Certificate for the domain "${inputs.domain}" has an unsupported status of: "${certificate.Status}". Please remove it manually and deploy again.` | ||
) | ||
} | ||
// Save dns info to state | ||
this.state.dns = this.state.dns && typeof this.state.dns === 'object' ? this.state.dns : {} | ||
// Setting up domains for different services | ||
for (const domainConfig of domains) { | ||
if (domainConfig.type === 's3') { | ||
await deployS3Domain( | ||
this.state.dns[domainConfig.domain] = this.state.dns[domainConfig.domain] || {} | ||
this.state.dns[domainConfig.domain].type = domainConfig.type | ||
if (domainConfig.type === 'awsS3Website') { | ||
this.state.dns[domainConfig.domain].domain = domainConfig.domain | ||
await this.save() | ||
this.context.debug(`Configuring domain "${domainConfig.domain}" for S3 Bucket Website`) | ||
this.context.debug(`Checking CloudFront distribution for domain "${domainConfig.domain}"`) | ||
let distribution = await getCloudFrontDistributionByDomain(clients.cf, domainConfig.domain) | ||
if (!distribution) { | ||
this.context.debug( | ||
`CloudFront distribution for domain "${domainConfig.domain}" not found. Creating...` | ||
) | ||
distribution = await createCloudfrontDistribution( | ||
clients.cf, | ||
domainConfig, | ||
certificate.CertificateArn | ||
) | ||
} | ||
this.context.debug(`Configuring DNS for distribution "${distribution.url}".`) | ||
await configureDnsForCloudFrontDistribution( | ||
clients.route53, | ||
clients.cf, | ||
domainConfig.domain, | ||
domainConfig, | ||
domainHostedZoneId, | ||
certificateArn | ||
distribution.url | ||
) | ||
} else if (domainConfig.type === 'api') { | ||
return deployApiDomain( | ||
this.context.debug(`Invalidating CloudFront distribution ${distribution.url}`) | ||
await invalidateCloudfrontDistribution(clients.cf, distribution.id) | ||
this.context.debug(`Using AWS Cloudfront Distribution with URL: "${domainConfig.domain}"`) | ||
} else if (domainConfig.type === 'awsApiGateway') { | ||
this.state.dns[domainConfig.domain].domain = domainConfig.domain | ||
await this.save() | ||
// Map APIG domain to API ID and recursively retry | ||
// if APIG domain need to be created first, or TooManyRequests error is thrown | ||
await deployApiDomain( | ||
clients.apig, | ||
clients.route53, | ||
domainConfig.domain, | ||
domainConfig.id, | ||
certificateArn, | ||
domainHostedZoneId | ||
domainConfig.apiId, | ||
certificate.CertificateArn, | ||
domainHostedZoneId, | ||
this // passing instance for helpful logs during the process | ||
) | ||
} | ||
// TODO: Remove unused domains that are kept in state | ||
} | ||
const outputs = {} | ||
let hasRoot = false | ||
outputs.domains = domains.map((domainConfig) => { | ||
if (domainConfig.root) { | ||
hasRoot = true | ||
} | ||
return `https://${domainConfig.domain}` | ||
}) | ||
if (hasRoot) { | ||
outputs.domains.unshift(`https://www.${this.state.domain}`) | ||
} | ||
return outputs | ||
} | ||
async remove() {} | ||
/** | ||
* Remove | ||
*/ | ||
async remove() { | ||
this.context.status('Deploying') | ||
this.context.debug(`Starting Domain component removal.`) | ||
// Get AWS SDK Clients | ||
const clients = getClients(this.context.credentials.aws) | ||
this.context.debug(`Getting the Hosted Zone ID for the domain ${this.state.domain}.`) | ||
const domainHostedZoneId = await getDomainHostedZoneId(clients.route53, this.state.domain) | ||
for (const subdomain of Object.keys(this.state.dns)) { | ||
const domainState = this.state.dns[subdomain] | ||
if (domainState.type === 'awsS3Website') { | ||
this.context.debug( | ||
`Fetching CloudFront distribution info for removal for domain ${domainState.domain}.` | ||
) | ||
const distribution = await getCloudFrontDistributionByDomain(clients.cf, domainState.domain) | ||
if (distribution) { | ||
this.context.debug(`Removing DNS records for website domain ${domainState.domain}.`) | ||
await removeWebsiteDomainDnsRecords( | ||
clients.route53, | ||
domainState.domain, | ||
domainHostedZoneId, | ||
distribution.url | ||
) | ||
await removeWebsiteDomainDnsRecords( | ||
clients.route53, | ||
`www.${domainState.domain}`, // it'll move on if it doesn't exist | ||
domainHostedZoneId, | ||
distribution.url | ||
) | ||
} | ||
} else if (domainState.type === 'awsApiGateway') { | ||
this.context.debug( | ||
`Fetching API Gateway domain ${domainState.domain} information for removal.` | ||
) | ||
const domainRes = await getApiDomainName(clients.apig, domainState.domain) | ||
if (domainRes) { | ||
this.context.debug(`Removing API Gateway domain ${domainState.domain}.`) | ||
await removeApiDomain(clients.apig, domainState.domain) | ||
this.context.debug(`Removing DNS records for API Gateway domain ${domainState.domain}.`) | ||
await removeApiDomainDnsRecords( | ||
clients.route53, | ||
domainState.domain, | ||
domainHostedZoneId, | ||
domainRes.distributionHostedZoneId, | ||
domainRes.distributionDomainName | ||
) | ||
} | ||
} | ||
} | ||
this.state = {} | ||
await this.save() | ||
return {} | ||
} | ||
} | ||
module.exports = Domain |
770
utils.js
const aws = require('aws-sdk') | ||
const { utils } = require('@serverless/core') | ||
const getClients = (credentials, region) => { | ||
const getOutdatedDomains = (inputs, state) => { | ||
if (inputs.domain !== state.domain) { | ||
return state | ||
} | ||
for (const domain of Object.keys(state.dns)) { | ||
if (!inputs.dns[domain]) | ||
} | ||
} | ||
/** | ||
* Get Clients | ||
* - Gets AWS SDK clients to use within this Component | ||
*/ | ||
const getClients = (credentials, region = 'us-east-1') => { | ||
const route53 = new aws.Route53({ | ||
@@ -32,21 +47,32 @@ credentials, | ||
} | ||
const getDomains = (config) => { | ||
/** | ||
* Prepare Domains | ||
* - Formats component domains & identifies cloud services they're using. | ||
*/ | ||
const prepareDomains = (config) => { | ||
const domains = [] | ||
for (const domain of config.dns || []) { | ||
for (const subdomain in config.dns || []) { | ||
const domainObj = {} | ||
if (domain.domain === 'root') { | ||
if (subdomain === '$') { | ||
domainObj.domain = config.domain | ||
domainObj.root = true | ||
} else { | ||
domainObj.domain = `${domain.domain}.${config.domain}` | ||
domainObj.domain = `${subdomain}.${config.domain}` | ||
} | ||
if (domain.target.url.includes('api')) { | ||
const id = domain.target.url.split('.')[0].split('//')[1] | ||
domainObj.id = id | ||
domainObj.type = 'api' | ||
if (config.dns[subdomain].url.includes('s3')) { | ||
domainObj.type = 'awsS3Website' | ||
// Get S3 static hosting endpoint of existing bucket to use w/ CloudFront. | ||
// The bucket name must be DNS compliant. | ||
domainObj.s3BucketName = config.dns[subdomain].url.replace('http://', '').split('.')[0] | ||
} | ||
if (domain.target.url.includes('s3')) { | ||
domainObj.type = 's3' | ||
// Check if referenced Component is using AWS API Gateway... | ||
if (config.dns[subdomain].url.includes('execute-api')) { | ||
domainObj.apiId = config.dns[subdomain].url.split('.')[0].split('//')[1] | ||
domainObj.type = 'awsApiGateway' | ||
} | ||
@@ -60,14 +86,15 @@ | ||
const getSecondLevelDomain = (domain) => { | ||
return domain | ||
.split('.') | ||
.slice(domain.split('.').length - 2) | ||
.join('.') | ||
} | ||
/** | ||
* Get Domain Hosted Zone ID | ||
* - Every Domain on Route53 always has a Hosted Zone w/ 2 Record Sets. | ||
* - These Record Sets are: "Name Servers (NS)" & "Start of Authority (SOA)" | ||
* - These don't need to be created and SHOULD NOT be modified. | ||
*/ | ||
const getDomainHostedZoneId = async (route53, secondLevelDomain) => { | ||
const getDomainHostedZoneId = async (route53, domain) => { | ||
const hostedZonesRes = await route53.listHostedZonesByName().promise() | ||
const hostedZone = hostedZonesRes.HostedZones.find( | ||
(zone) => zone.Name.includes(secondLevelDomain) // Name has a period at the end, which is why we're using includes rather than equals | ||
// Name has a period at the end, so we're using includes rather than equals | ||
(zone) => zone.Name.includes(domain) | ||
) | ||
@@ -77,23 +104,186 @@ | ||
throw Error( | ||
`Domain ${secondLevelDomain} was not found in your AWS account. Please purchase it from Route53 first then try again.` | ||
`Domain ${domain} was not found in your AWS account. Please purchase it from Route53 first then try again.` | ||
) | ||
} | ||
return hostedZone.Id.replace('/hostedzone/', '') | ||
return hostedZone.Id.replace('/hostedzone/', '') // hosted zone id is always prefixed with this :( | ||
} | ||
const createApigDomain = async (apig, route53, domain, certificateArn, domainHostedZoneId) => { | ||
/** | ||
* Describe Certificate By Arn | ||
* - Describe an AWS ACM Certificate by its ARN | ||
*/ | ||
const describeCertificateByArn = async (acm, certificateArn) => { | ||
const certificate = await acm.describeCertificate({ CertificateArn: certificateArn }).promise() | ||
return certificate && certificate.Certificate ? certificate.Certificate : null | ||
} | ||
/** | ||
* Get Certificate Arn By Domain | ||
* - Gets an AWS ACM Certificate by a specified domain or return null | ||
*/ | ||
const getCertificateArnByDomain = async (acm, domain) => { | ||
const listRes = await acm.listCertificates().promise() | ||
const certificate = listRes.CertificateSummaryList.find((cert) => cert.DomainName === domain) | ||
return certificate && certificate.CertificateArn ? certificate.CertificateArn : null | ||
} | ||
/** | ||
* Create Certificate | ||
* - Creates an AWS ACM Certificate for the specified domain | ||
*/ | ||
const createCertificate = async (acm, domain) => { | ||
const wildcardSubDomain = `*.${domain}` | ||
const params = { | ||
domainName: domain, | ||
certificateArn: certificateArn, | ||
securityPolicy: 'TLS_1_2', | ||
endpointConfiguration: { | ||
types: ['EDGE'] | ||
DomainName: domain, | ||
SubjectAlternativeNames: [domain, wildcardSubDomain], | ||
ValidationMethod: 'DNS' | ||
} | ||
return acm.requestCertificate(params).promise().CertificateArn | ||
} | ||
/** | ||
* Validate Certificate | ||
* - Validate an AWS ACM Certificate via the "DNS" method | ||
*/ | ||
const validateCertificate = async (acm, route53, certificate, domain, domainHostedZoneId) => { | ||
let readinessCheckCount = 10 | ||
let statusCheckCount = 10 | ||
let validationResourceRecord | ||
/** | ||
* Check Readiness | ||
* - Newly Created AWS ACM Certificates may not yet have the info needed to validate it | ||
* - Specifically, the "ResourceRecord" object in the Domain Validation Options | ||
* - Ensure this exists. | ||
*/ | ||
const checkReadiness = async function() { | ||
if (readinessCheckCount < 1) { | ||
throw new Error( | ||
'Your newly created AWS ACM Certificate is taking a while to initialize. Please try running this component again in a few minutes.' | ||
) | ||
} | ||
const cert = await describeCertificateByArn(acm, certificate.CertificateArn) | ||
// Find root domain validation option resource record | ||
cert.DomainValidationOptions.forEach((option) => { | ||
if (domain === option.DomainName) { | ||
validationResourceRecord = option.ResourceRecord | ||
} | ||
}) | ||
if (!validationResourceRecord) { | ||
readinessCheckCount-- | ||
await utils.sleep(5000) | ||
return await checkReadiness() | ||
} | ||
} | ||
const apigDomainName = await apig.createDomainName(params).promise() | ||
await checkReadiness() | ||
const checkRecordsParams = { | ||
HostedZoneId: domainHostedZoneId, | ||
MaxItems: '10', | ||
StartRecordName: validationResourceRecord.Name | ||
} | ||
// Check if the validation resource record sets already exist. | ||
// This might be the case if the user is trying to deploy multiple times while validation is occurring. | ||
const existingRecords = await route53.listResourceRecordSets(checkRecordsParams).promise() | ||
if (!existingRecords.ResourceRecordSets.length) { | ||
// Create CNAME record for DNS validation check | ||
// NOTE: It can take 30 minutes or longer for DNS propagation so validation can complete, just continue on and don't wait for this... | ||
const recordParams = { | ||
HostedZoneId: domainHostedZoneId, | ||
ChangeBatch: { | ||
Changes: [ | ||
{ | ||
Action: 'UPSERT', | ||
ResourceRecordSet: { | ||
Name: validationResourceRecord.Name, | ||
Type: validationResourceRecord.Type, | ||
TTL: 300, | ||
ResourceRecords: [ | ||
{ | ||
Value: validationResourceRecord.Value | ||
} | ||
] | ||
} | ||
} | ||
] | ||
} | ||
} | ||
await route53.changeResourceRecordSets(recordParams).promise() | ||
} | ||
/** | ||
* Check Validated Status | ||
* - Newly Validated AWS ACM Certificates may not yet show up as valid | ||
* - This gives them some time to update their status. | ||
*/ | ||
const checkStatus = async function() { | ||
if (statusCheckCount < 1) { | ||
throw new Error( | ||
'Your newly validated AWS ACM Certificate is taking a while to register as valid. Please try running this component again in a few minutes.' | ||
) | ||
} | ||
const cert = await describeCertificateByArn(acm, certificate.CertificateArn) | ||
if (cert.Status !== 'ISSUED') { | ||
statusCheckCount-- | ||
await utils.sleep(10000) | ||
return await checkStatus() | ||
} | ||
} | ||
await checkStatus() | ||
} | ||
/** | ||
* Create AWS API Gateway Domain | ||
*/ | ||
const createDomainInApig = async (apig, domain, certificateArn) => { | ||
try { | ||
const params = { | ||
domainName: domain, | ||
certificateArn: certificateArn, | ||
securityPolicy: 'TLS_1_2', | ||
endpointConfiguration: { | ||
types: ['EDGE'] | ||
} | ||
} | ||
const res = await apig.createDomainName(params).promise() | ||
return res | ||
} catch (e) { | ||
if (e.code === 'TooManyRequestsException') { | ||
await utils.sleep(2000) | ||
return createDomainInApig(apig, domain, certificateArn) | ||
} | ||
throw e | ||
} | ||
} | ||
/** | ||
* Configure DNS for the created API Gateway domain | ||
*/ | ||
const configureDnsForApigDomain = async ( | ||
route53, | ||
domain, | ||
hostedZoneId, | ||
distributionHostedZoneId, | ||
distributionDomainName | ||
) => { | ||
const dnsRecord = { | ||
HostedZoneId: domainHostedZoneId, | ||
HostedZoneId: hostedZoneId, | ||
ChangeBatch: { | ||
@@ -107,4 +297,4 @@ Changes: [ | ||
AliasTarget: { | ||
HostedZoneId: apigDomainName.distributionHostedZoneId, | ||
DNSName: apigDomainName.distributionDomainName, | ||
HostedZoneId: distributionHostedZoneId, | ||
DNSName: distributionDomainName, | ||
EvaluateTargetHealth: false | ||
@@ -121,191 +311,349 @@ } | ||
const deployS3Domain = async (route53, cf, domain, domainHostedZoneId, certificateArn) => { | ||
/** | ||
* Map API Gateway API to the created API Gateway Domain | ||
*/ | ||
const mapDomainToApi = async (apig, domain, apiId) => { | ||
try { | ||
const params = { | ||
DistributionConfig: { | ||
CallerReference: String(Date.now()), | ||
Aliases: { | ||
Quantity: 2, | ||
Items: [domain, `www.${domain}`] | ||
}, | ||
DefaultRootObject: 'index.html', | ||
Origins: { | ||
Quantity: 1, | ||
Items: [ | ||
{ | ||
Id: `S3-${domain}`, | ||
DomainName: `${domain}.s3.amazonaws.com`, | ||
OriginPath: '', | ||
CustomHeaders: { | ||
Quantity: 0, | ||
Items: [] | ||
}, | ||
S3OriginConfig: { | ||
OriginAccessIdentity: '' | ||
} | ||
domainName: domain, | ||
restApiId: apiId, | ||
basePath: '(none)', | ||
stage: 'production' | ||
} | ||
return apig.createBasePathMapping(params).promise() | ||
} catch (e) { | ||
if (e.code === 'TooManyRequestsException') { | ||
await utils.sleep(2000) | ||
return mapDomainToApi(apig, domain, apiId) | ||
} | ||
throw e | ||
} | ||
} | ||
const deployApiDomain = async ( | ||
apig, | ||
route53, | ||
domain, | ||
apiId, | ||
certificateArn, | ||
domainHostedZoneId, | ||
that | ||
) => { | ||
try { | ||
that.context.debug(`Mapping domain ${domain} to API ID ${apiId}`) | ||
await mapDomainToApi(apig, domain, apiId) | ||
} catch (e) { | ||
if (e.message === 'Invalid domain name identifier specified') { | ||
that.context.debug(`Domain ${domain} not found in API Gateway. Creating...`) | ||
const res = await createDomainInApig(apig, domain, certificateArn) | ||
that.state.dns[domain].distributionHostedZoneId = res.distributionHostedZoneId | ||
that.state.dns[domain].distributionDomainName = res.distributionDomainName | ||
await that.save() | ||
that.context.debug(`Configuring DNS for API Gateway domain ${domain}.`) | ||
await configureDnsForApigDomain( | ||
route53, | ||
domain, | ||
domainHostedZoneId, | ||
res.distributionHostedZoneId, | ||
res.distributionDomainName | ||
) | ||
// retry domain deployment now that domain is created | ||
return deployApiDomain(apig, route53, domain, apiId, certificateArn, domainHostedZoneId, that) | ||
} | ||
if (e.message === 'Base path already exists for this domain name') { | ||
that.context.debug(`Domain ${domain} is already mapped to API ID ${apiId}.`) | ||
return | ||
} | ||
throw new Error(e) | ||
} | ||
} | ||
/** | ||
* Get CloudFront Distribution using a domain name | ||
*/ | ||
const getCloudFrontDistributionByDomain = async (cf, domain) => { | ||
const listRes = await cf.listDistributions({}).promise() | ||
const distribution = listRes.DistributionList.Items.find((dist) => | ||
dist.Aliases.Items.includes(domain) | ||
) | ||
if (distribution) { | ||
return { | ||
arn: distribution.ARN, | ||
id: distribution.Id, | ||
url: distribution.DomainName | ||
} | ||
} | ||
return null | ||
} | ||
/** | ||
* Configure DNS records for a distribution domain | ||
*/ | ||
const configureDnsForCloudFrontDistribution = async ( | ||
route53, | ||
domainConfig, | ||
domainHostedZoneId, | ||
distributionUrl | ||
) => { | ||
const dnsRecordParams = { | ||
HostedZoneId: domainHostedZoneId, | ||
ChangeBatch: { | ||
Changes: [ | ||
{ | ||
Action: 'UPSERT', | ||
ResourceRecordSet: { | ||
Name: domainConfig.domain, | ||
Type: 'A', | ||
AliasTarget: { | ||
HostedZoneId: 'Z2FDTNDATAQYW2', // this is a constant that you can get from here https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region | ||
DNSName: distributionUrl, | ||
EvaluateTargetHealth: false | ||
} | ||
] | ||
}, | ||
OriginGroups: { | ||
Quantity: 0, | ||
Items: [] | ||
}, | ||
DefaultCacheBehavior: { | ||
TargetOriginId: `S3-${domain}`, | ||
ForwardedValues: { | ||
QueryString: false, | ||
Cookies: { | ||
Forward: 'none' | ||
}, | ||
Headers: { | ||
} | ||
} | ||
] | ||
} | ||
} | ||
if (domainConfig.root) { | ||
dnsRecordParams.ChangeBatch.Changes.push({ | ||
Action: 'UPSERT', | ||
ResourceRecordSet: { | ||
Name: `www.${domainConfig.domain}`, | ||
Type: 'A', | ||
AliasTarget: { | ||
HostedZoneId: 'Z2FDTNDATAQYW2', // this is a constant that you can get from here https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region | ||
DNSName: distributionUrl, | ||
EvaluateTargetHealth: false | ||
} | ||
} | ||
}) | ||
} | ||
return route53.changeResourceRecordSets(dnsRecordParams).promise() | ||
} | ||
/** | ||
* Create Cloudfront Distribution | ||
*/ | ||
const createCloudfrontDistribution = async (cf, domainConfig, certificateArn) => { | ||
const params = { | ||
DistributionConfig: { | ||
CallerReference: String(Date.now()), | ||
Aliases: { | ||
Quantity: 1, | ||
Items: [domainConfig.domain] | ||
}, | ||
DefaultRootObject: 'index.html', | ||
Origins: { | ||
Quantity: 1, | ||
Items: [ | ||
{ | ||
Id: `S3-${domainConfig.s3BucketName}`, | ||
DomainName: `${domainConfig.s3BucketName}.s3.amazonaws.com`, | ||
OriginPath: '', | ||
CustomHeaders: { | ||
Quantity: 0, | ||
Items: [] | ||
}, | ||
QueryStringCacheKeys: { | ||
Quantity: 0, | ||
Items: [] | ||
S3OriginConfig: { | ||
OriginAccessIdentity: '' | ||
} | ||
} | ||
] | ||
}, | ||
OriginGroups: { | ||
Quantity: 0, | ||
Items: [] | ||
}, | ||
DefaultCacheBehavior: { | ||
TargetOriginId: `S3-${domainConfig.s3BucketName}`, | ||
ForwardedValues: { | ||
QueryString: false, | ||
Cookies: { | ||
Forward: 'none' | ||
}, | ||
TrustedSigners: { | ||
Enabled: false, | ||
Headers: { | ||
Quantity: 0, | ||
Items: [] | ||
}, | ||
ViewerProtocolPolicy: 'redirect-to-https', | ||
MinTTL: 0, | ||
AllowedMethods: { | ||
Quantity: 2, | ||
Items: ['HEAD', 'GET'], | ||
CachedMethods: { | ||
Quantity: 2, | ||
Items: ['HEAD', 'GET'] | ||
} | ||
}, | ||
SmoothStreaming: false, | ||
DefaultTTL: 86400, | ||
MaxTTL: 31536000, | ||
Compress: false, | ||
LambdaFunctionAssociations: { | ||
QueryStringCacheKeys: { | ||
Quantity: 0, | ||
Items: [] | ||
}, | ||
FieldLevelEncryptionId: '' | ||
} | ||
}, | ||
CacheBehaviors: { | ||
TrustedSigners: { | ||
Enabled: false, | ||
Quantity: 0, | ||
Items: [] | ||
}, | ||
CustomErrorResponses: { | ||
ViewerProtocolPolicy: 'redirect-to-https', | ||
MinTTL: 0, | ||
AllowedMethods: { | ||
Quantity: 2, | ||
Items: ['HEAD', 'GET'], | ||
CachedMethods: { | ||
Quantity: 2, | ||
Items: ['HEAD', 'GET'] | ||
} | ||
}, | ||
SmoothStreaming: false, | ||
DefaultTTL: 86400, | ||
MaxTTL: 31536000, | ||
Compress: false, | ||
LambdaFunctionAssociations: { | ||
Quantity: 0, | ||
Items: [] | ||
}, | ||
Comment: '', | ||
Logging: { | ||
Enabled: false, | ||
IncludeCookies: false, | ||
Bucket: '', | ||
Prefix: '' | ||
}, | ||
PriceClass: 'PriceClass_All', | ||
Enabled: true, | ||
ViewerCertificate: { | ||
ACMCertificateArn: certificateArn, | ||
SSLSupportMethod: 'sni-only', | ||
MinimumProtocolVersion: 'TLSv1.1_2016', | ||
Certificate: certificateArn, | ||
CertificateSource: 'acm' | ||
}, | ||
Restrictions: { | ||
GeoRestriction: { | ||
RestrictionType: 'none', | ||
Quantity: 0, | ||
Items: [] | ||
} | ||
}, | ||
WebACLId: '', | ||
HttpVersion: 'http2', | ||
IsIPV6Enabled: true | ||
} | ||
FieldLevelEncryptionId: '' | ||
}, | ||
CacheBehaviors: { | ||
Quantity: 0, | ||
Items: [] | ||
}, | ||
CustomErrorResponses: { | ||
Quantity: 0, | ||
Items: [] | ||
}, | ||
Comment: '', | ||
Logging: { | ||
Enabled: false, | ||
IncludeCookies: false, | ||
Bucket: '', | ||
Prefix: '' | ||
}, | ||
PriceClass: 'PriceClass_All', | ||
Enabled: true, | ||
ViewerCertificate: { | ||
ACMCertificateArn: certificateArn, | ||
SSLSupportMethod: 'sni-only', | ||
MinimumProtocolVersion: 'TLSv1.1_2016', | ||
Certificate: certificateArn, | ||
CertificateSource: 'acm' | ||
}, | ||
Restrictions: { | ||
GeoRestriction: { | ||
RestrictionType: 'none', | ||
Quantity: 0, | ||
Items: [] | ||
} | ||
}, | ||
WebACLId: '', | ||
HttpVersion: 'http2', | ||
IsIPV6Enabled: true | ||
} | ||
} | ||
const res = await cf.createDistribution(params).promise() | ||
if (domainConfig.root) { | ||
params.DistributionConfig.Aliases.Quantity = 2 | ||
params.DistributionConfig.Aliases.Items.push(`www.${domainConfig.domain}`) | ||
} | ||
const distributionUrl = res.Distribution.DomainName | ||
const res = await cf.createDistribution(params).promise() | ||
const dnsRecordParams = { | ||
HostedZoneId: domainHostedZoneId, | ||
ChangeBatch: { | ||
Changes: [ | ||
{ | ||
Action: 'UPSERT', | ||
ResourceRecordSet: { | ||
Name: domain, | ||
Type: 'A', | ||
AliasTarget: { | ||
HostedZoneId: 'Z2FDTNDATAQYW2', // this is a constant that you can get from here https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region | ||
DNSName: distributionUrl, | ||
EvaluateTargetHealth: false | ||
} | ||
} | ||
} | ||
] | ||
return { | ||
id: res.Distribution.Id, | ||
arn: res.Distribution.ARN, | ||
url: res.Distribution.DomainName | ||
} | ||
// try { | ||
// } catch (e) { | ||
// if (e.code !== 'CNAMEAlreadyExists') { | ||
// throw e | ||
// } | ||
// } | ||
} | ||
/** | ||
* Invalidate Cloudfront Distribution | ||
*/ | ||
const invalidateCloudfrontDistribution = async (cf, distributionId) => { | ||
const params = { | ||
DistributionId: distributionId, | ||
InvalidationBatch: { | ||
CallerReference: String(Date.now()), | ||
Paths: { | ||
Quantity: 1, | ||
Items: ['/index.html'] | ||
} | ||
} | ||
await route53.changeResourceRecordSets(dnsRecordParams).promise() | ||
} catch (e) { | ||
if (e.code !== 'CNAMEAlreadyExists') { | ||
throw e | ||
} | ||
} | ||
await cf.createInvalidation(params).promise() | ||
} | ||
const deployApiDomain = async ( | ||
apig, | ||
/** | ||
* Remove AWS S3 Website DNS Records | ||
*/ | ||
const removeWebsiteDomainDnsRecords = async ( | ||
route53, | ||
domain, | ||
apiId, | ||
certificateArn, | ||
domainHostedZoneId | ||
domainHostedZoneId, | ||
distributionUrl | ||
) => { | ||
const params = { | ||
HostedZoneId: domainHostedZoneId, | ||
ChangeBatch: { | ||
Changes: [ | ||
{ | ||
Action: 'DELETE', | ||
ResourceRecordSet: { | ||
Name: domain, | ||
Type: 'A', | ||
AliasTarget: { | ||
HostedZoneId: 'Z2FDTNDATAQYW2', // this is a constant that you can get from here https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region | ||
DNSName: distributionUrl, | ||
EvaluateTargetHealth: false | ||
} | ||
} | ||
} | ||
] | ||
} | ||
} | ||
try { | ||
await apig | ||
.createBasePathMapping({ | ||
domainName: domain, | ||
restApiId: apiId, | ||
basePath: '(none)', | ||
stage: 'production' | ||
}) | ||
.promise() | ||
await route53.changeResourceRecordSets(params).promise() | ||
} catch (e) { | ||
if (e.message === 'Invalid domain name identifier specified') { | ||
await createApigDomain(apig, route53, domain, certificateArn, domainHostedZoneId) | ||
// avoiding "too many requests error" | ||
await utils.sleep(1000) | ||
return deployApiDomain(apig, route53, domain, apiId, certificateArn, domainHostedZoneId) | ||
if (e.code !== 'InvalidChangeBatch') { | ||
throw e | ||
} | ||
if (e.message === 'Base path already exists for this domain name') { | ||
return | ||
} | ||
throw new Error(e) | ||
} | ||
} | ||
const createCertificate = async (acm, route53, secondLevelDomain, domainHostedZoneId) => { | ||
/** | ||
* Remove API Gateway Domain | ||
*/ | ||
const removeApiDomain = async (apig, domain) => { | ||
const params = { | ||
DomainName: secondLevelDomain, | ||
SubjectAlternativeNames: `*.${secondLevelDomain}`, | ||
ValidationMethod: 'DNS' | ||
domainName: domain | ||
} | ||
const certificate = await acm.requestCertificate(params).promise() | ||
return apig.deleteDomainName(params).promise() | ||
} | ||
const certificateDnsRecord = (await acm | ||
.describeCertificate({ CertificateArn: certificate.CertificateArn }) | ||
.promise()).DomainValidationOptions.ResourceRecord | ||
/** | ||
* Remove API Gateway Domain DNS Records | ||
*/ | ||
const recordParams = { | ||
const removeApiDomainDnsRecords = async ( | ||
route53, | ||
domain, | ||
domainHostedZoneId, | ||
distributionHostedZoneId, | ||
distributionDomainName | ||
) => { | ||
const dnsRecord = { | ||
HostedZoneId: domainHostedZoneId, | ||
@@ -315,11 +663,11 @@ ChangeBatch: { | ||
{ | ||
Action: 'UPSERT', | ||
Action: 'DELETE', | ||
ResourceRecordSet: { | ||
Name: certificateDnsRecord.Name, | ||
Type: certificateDnsRecord.Type, | ||
ResourceRecords: [ | ||
{ | ||
Value: certificateDnsRecord.Value | ||
} | ||
] | ||
Name: domain, | ||
Type: 'A', | ||
AliasTarget: { | ||
HostedZoneId: distributionHostedZoneId, | ||
DNSName: distributionDomainName, | ||
EvaluateTargetHealth: false | ||
} | ||
} | ||
@@ -331,34 +679,42 @@ } | ||
await route53.changeResourceRecordSets(recordParams).promise() | ||
return certificate | ||
return route53.changeResourceRecordSets(dnsRecord).promise() | ||
} | ||
const getCertificateArn = async (acm, route53, secondLevelDomain, domainHostedZoneId) => { | ||
const listRes = await acm | ||
.listCertificates({ | ||
CertificateStatuses: ['ISSUED'] | ||
}) | ||
.promise() | ||
/** | ||
* Fetch API Gateway Domain Information | ||
*/ | ||
let certificate = listRes.CertificateSummaryList.find( | ||
(cert) => cert.DomainName === secondLevelDomain | ||
) | ||
if (!certificate) { | ||
certificate = await createCertificate(acm, route53, secondLevelDomain, domainHostedZoneId) | ||
const getApiDomainName = async (apig, domain) => { | ||
try { | ||
return apig.getDomainName({ domainName: domain }).promise() | ||
} catch (e) { | ||
if (e.code === 'NotFoundException:') { | ||
return null | ||
} | ||
} | ||
return certificate.CertificateArn | ||
} | ||
/** | ||
* Exports | ||
*/ | ||
module.exports = { | ||
getClients, | ||
getDomains, | ||
getSecondLevelDomain, | ||
prepareDomains, | ||
describeCertificateByArn, | ||
getCertificateArnByDomain, | ||
createCertificate, | ||
validateCertificate, | ||
getDomainHostedZoneId, | ||
deployS3Domain, | ||
createCloudfrontDistribution, | ||
invalidateCloudfrontDistribution, | ||
mapDomainToApi, | ||
createDomainInApig, | ||
configureDnsForApigDomain, | ||
deployApiDomain, | ||
createCertificate, | ||
getCertificateArn | ||
removeApiDomain, | ||
removeApiDomainDnsRecords, | ||
getCloudFrontDistributionByDomain, | ||
configureDnsForCloudFrontDistribution, | ||
getApiDomainName, | ||
removeWebsiteDomainDnsRecords | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
44317
919
34
1