├── .gitignore ├── LICENSE ├── README.md ├── bin ├── build ├── clean ├── deploy ├── install └── package ├── dist └── stack.template ├── example ├── index.js └── stack.template └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | dist/function.zip 2 | example/index.zip 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jed Schmidt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cfn-api-gateway-custom-domain 2 | 3 | *[Ed note: in an amazing turn of events, this library was released mere hours before [AWS basically made it obsolete.](https://aws.amazon.com/about-aws/whats-new/2017/03/amazon-api-gateway-integrates-with-aws-certificate-manager-acm/)]* 4 | 5 | This is a [CloudFormation][] [custom resource][] for [API Gateway][] [custom domains][]. It runs [Certbot][] on [Lambda][] to create certificates, and automatically creates [Route53][] DNS records to respond to [Let's Encrypt][] domain ownership challenges. 6 | 7 | It's basically a [prollyfill][] for the conspicuously missing `AWS::ApiGateway::DomainName` resource type, which will likely land if/when [AWS Certificate Manager][] supports API Gateway. 8 | 9 | If you need to renew your certificates or would like to just use Route53 to create Let's Encrypt certificates, check out [certbot-route53.sh][]. 10 | 11 | Features 12 | -------- 13 | 14 | - **Fast**: Certificates are installed and enabled in minutes 15 | - **Free**: Certificates cost nothing (but you can [donate][]) 16 | - **Easy**: Certificates need only 14 lines in a CloudFormation template 17 | - **Safe**: Certificates never touch your email or machine 18 | 19 | Setup 20 | ----- 21 | 22 | Before you get started, you'll need to: 23 | 24 | 1. create a Route53 [public hosted zone][] for the domain, and 25 | 2. point the domain at [your zone's nameservers][]. 26 | 27 | Since Let's Encrypt needs to be able to contact Route53, your DNS settings must be in effect already. 28 | 29 | Usage 30 | ----- 31 | 32 | 1. First, make sure you have a `AWS::Route53::HostedZone` in the `Resources` section of your template: 33 | 34 | ```yaml 35 | MyHostedZone: 36 | Type: AWS::Route53::HostedZone 37 | Properties: 38 | Name: jedschmidt.com 39 | ``` 40 | 41 | 2. Then, add an API Gateway Custom Domain stack to your template: 42 | 43 | ```yaml 44 | ApiGatewayCustomDomain: 45 | Type: AWS::CloudFormation::Stack 46 | Properties: 47 | TemplateURL: https://s3.amazonaws.com/api-gateway-custom-domain/stack.template 48 | Parameters: 49 | LetsEncryptAccountEmail: me@jedschmidt.com 50 | LetsEncryptAgreeTOS: Yes 51 | LetsEncryptManualPublicIpLoggingOk: Yes 52 | ``` 53 | 54 | You'll need to specify three things: 55 | 56 | - `LetsEncryptAccountEmail`: The email address associated with your Let's Encrypt account 57 | - `LetsEncryptAgreeTOS`: That you agree to the [Let's Encrypt Terms of Service][]. This must be `Yes`. 58 | - `LetsEncryptManualPublicIpLoggingOk`: That you're okay with Let's Encrypt logging the IP address of the Lambda used to run `certbot`. This must be `Yes`. 59 | 60 | This stack has only one output: `ServiceToken`. This can be accessed using `!GetAtt {your-logical-stack-name}.Outputs.ServiceToken`. 61 | 62 | 3. Finally, add a custom domain to your template: 63 | 64 | ```yaml 65 | MyDomain: 66 | Type: Custom::ApiGatewayCustomDomain 67 | Properties: 68 | ServiceToken: !GetAtt ApiGatewayCustomDomain.Outputs.ServiceToken 69 | HostedZoneId: !Ref MyHostedZone 70 | Subdomain: www 71 | ``` 72 | 73 | You'll need to specify two things: 74 | 75 | - `Service Token`: The Service token output by your API Gateway Custom Domain stack 76 | - `HostedZoneId`: A reference to the existing `AWS::Route53::HostedZone` resource for which you're creating a certificate. 77 | 78 | You can also optionally specify: 79 | 80 | - `Subdomain`: The subdomain prefix for which you're creating a certificate, such as `www`. This is concatenated with the `Name` of the hosted zone, to create the full domain name. If this is omitted, the bare apex domain is used. 81 | 82 | This resource returns the results of the [createDomainName][] function. 83 | 84 | At this point, you've done all you need to create/update/deploy your stack and get your certificate installed into API Gateway, but to user the domain you'll need to add an alias DNS record that resolves your domain to the CloudFront distribution created with your custom domain name, and then map the domain to a stage of your rest API: 85 | 86 | ```yaml 87 | MyDNSRecord: 88 | Type: AWS::Route53::RecordSetGroup 89 | Properties: 90 | HostedZoneId: !Ref MyHostedZone 91 | RecordSets: 92 | - Type: A 93 | Name: !GetAtt MyDomain.domainName 94 | AliasTarget: 95 | HostedZoneId: Z2FDTNDATAQYW2 # (hardcoded for all CloudFormation templates) 96 | DNSName: !GetAtt MyDomain.distributionDomainName 97 | 98 | MyPathMapping: 99 | Type: AWS::ApiGateway::BasePathMapping 100 | Properties: 101 | DomainName: !GetAtt MyDomain.domainName 102 | RestApiId: !Ref MyRestAPI 103 | Stage: prod 104 | ``` 105 | 106 | Example 107 | ------- 108 | 109 | See the included [example][] for a simple website redirect app configured entirely with CloudFormation. 110 | 111 | How it works 112 | ------------ 113 | 114 | When a custom domain name is first created in your stack, CloudFormation calls a [node.js function][] in a [Lambda-backed custom resource][], which in turn launches [Certbot][] in a Python subprocess. Certbot then contacts Let's Encrypt to get a challenge string, which is placed in a TXT record on Route53. Once the record is live, Certbot tells Let's Encrypt to verify it, and once it's verified, Let's Encrypt sends the certificate back to Certbot and then to API Gateway, where it's used to create a custom domain. 115 | 116 | Todo 117 | ---- 118 | 119 | - [Automate certificate renewal](#1) 120 | - [Revoke certificates on deletion](#2) 121 | 122 | Thanks 123 | ------ 124 | 125 | - [Let's Encrypt][] for taking the tedium and cost out of making web sites more secure. 126 | - [Michael Hart][] overall really, but in particular for the amazing [docker-lambda][], without which this project would not be possible. 127 | - [Eric Hammond][], for his helpful [explorations of CloudFormation and Lambda][]. 128 | 129 | [API Gateway]: https://aws.amazon.com/api-gateway 130 | [Lambda]: https://aws.amazon.com/lambda 131 | [custom domains]: http://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-custom-domains.html 132 | [CloudFormation]: https://aws.amazon.com/cloudformation 133 | [custom resource]: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html 134 | [Route53]: https://aws.amazon.com/route53 135 | [Let's Encrypt]: https://letsencrypt.org 136 | [Certbot]: https://certbot.eff.org 137 | [certbot-route53.sh]: https://git.io/vylLx 138 | [createDomainName]: http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/APIGateway.html#createDomainName-property 139 | [public hosted zone]: http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/CreatingHostedZone.html 140 | [your zone's nameservers]: http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/GetInfoAboutHostedZone.html 141 | [example]: https://github.com/jed/cfn-api-gateway-custom-domain/blob/master/example/stack.template 142 | [Lambda-backed custom resource]: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources-lambda.html 143 | [node.js function]: https://github.com/jed/cfn-api-gateway-custom-domain/blob/master/index.js 144 | [explorations of CloudFormation and Lambda]: https://alestic.com 145 | [Eric Hammond]: https://alestic.com/about/ 146 | [docker-lambda]: https://github.com/lambci/docker-lambda 147 | [Michael Hart]: https://twitter.com/hichaelmart 148 | [Let's Encrypt Terms of Service]: https://gist.github.com/kennwhite/9541c8f37ec2994352c4554ca2afeece 149 | [prollyfill]: https://twitter.com/slexaxton/status/257543702124306432?lang=en 150 | [AWS Certificate Manager]: https://aws.amazon.com/certificate-manager/ 151 | [donate]: https://letsencrypt.org/donate/ 152 | -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | chmod a+x index.js bin/* 4 | 5 | docker run \ 6 | --interactive \ 7 | --rm \ 8 | --volume $PWD/bin/install:/tmp/install \ 9 | --volume $PWD/dist:/tmp/dist \ 10 | lambci/lambda:build \ 11 | /tmp/install 12 | -------------------------------------------------------------------------------- /bin/clean: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm -rf dist/function.zip 4 | -------------------------------------------------------------------------------- /bin/deploy: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DOMAIN='jedschmidt.com' 4 | TARGET='https://twitter.com/jedschmidt' 5 | 6 | (cd example; zip index.zip index.js) 7 | 8 | aws s3 sync example s3://cfn-api-gateway-custom-domain/example 9 | 10 | aws cloudformation deploy \ 11 | --template-file example/stack.template \ 12 | --stack-name jedschmidt-redirect \ 13 | --capabilities CAPABILITY_IAM \ 14 | --parameter-overrides DomainName=$DOMAIN TargetLocation=$TARGET 15 | 16 | aws cloudformation describe-stacks \ 17 | --stack-name jedschmidt-redirect \ 18 | --query 'Stacks[0]' 19 | -------------------------------------------------------------------------------- /bin/install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | easy_install virtualenv 4 | virtualenv /var/task 5 | source /var/task/bin/activate 6 | pip install certbot 7 | find . -name '*.py' -delete 8 | cd /var/task 9 | zip -qr9 /tmp/dist/function.zip * 10 | -------------------------------------------------------------------------------- /bin/package: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | zip dist/function.zip index.js 4 | 5 | aws s3 sync dist s3://cfn-api-gateway-custom-domain --acl public-read 6 | -------------------------------------------------------------------------------- /dist/stack.template: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | 3 | Description: API Gateway custom domains, backed by Let's Encrypt certificates 4 | 5 | Parameters: 6 | LetsEncryptAccountEmail: 7 | Type: String 8 | Description: Email address of Let's Encrypt account 9 | 10 | LetsEncryptAgreeTOS: 11 | Type: String 12 | Description: Do you agree to the ACME Subscriber Agreement? 13 | ConstraintDescription: You must agree to the terms. 14 | AllowedValues: 15 | - true 16 | 17 | LetsEncryptManualPublicIpLoggingOk: 18 | Type: String 19 | Description: Are you okay with Let's Encrypt logging your public IP address? 20 | ConstraintDescription: You must allow IP logging. 21 | AllowedValues: 22 | - true 23 | 24 | Resources: 25 | CertbotLambda: 26 | Type: AWS::Lambda::Function 27 | Properties: 28 | Code: 29 | S3Bucket: cfn-api-gateway-custom-domain 30 | S3Key: function.zip 31 | Environment: 32 | Variables: 33 | EMAIL_ADDRESS: !Ref LetsEncryptAccountEmail 34 | Handler: index.handler 35 | MemorySize: 256 36 | Role: !GetAtt CertbotLambdaExecution.Arn 37 | Runtime: nodejs4.3 38 | Timeout: 300 39 | 40 | CertbotLambdaExecution: 41 | Type: AWS::IAM::Role 42 | Properties: 43 | AssumeRolePolicyDocument: 44 | Statement: 45 | - Action: sts:AssumeRole 46 | Effect: Allow 47 | Principal: 48 | Service: lambda.amazonaws.com 49 | Version: '2012-10-17' 50 | ManagedPolicyArns: 51 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 52 | Policies: 53 | - PolicyName: UpdateDNSRecords 54 | PolicyDocument: 55 | Version: '2012-10-17' 56 | Statement: 57 | - Effect: Allow 58 | Action: 59 | - route53:ChangeResourceRecordSets 60 | - route53:GetChange 61 | - route53:GetHostedZone 62 | Resource: '*' 63 | - PolicyName: UpdateDomains 64 | PolicyDocument: 65 | Version: '2012-10-17' 66 | Statement: 67 | - Effect: Allow 68 | Action: 69 | - apigateway:POST 70 | - apigateway:DELETE 71 | Resource: '*' 72 | 73 | Outputs: 74 | ServiceToken: 75 | Description: ARN of Certbot Lambda 76 | Value: !GetAtt CertbotLambda.Arn 77 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | exports.handler = function(event, context) { 2 | context.succeed({ 3 | statusCode: 302, 4 | headers: {Location: process.env.TARGET_LOCATION} 5 | }) 6 | } 7 | -------------------------------------------------------------------------------- /example/stack.template: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | 4 | Parameters: 5 | DomainName: 6 | Type: String 7 | Description: Domain name of the redirected site 8 | 9 | TargetLocation: 10 | Type: String 11 | Description: URL of the target redirect 12 | 13 | Resources: 14 | MyEndpoint: 15 | Type: AWS::Serverless::Function 16 | Properties: 17 | Handler: index.handler 18 | Runtime: nodejs4.3 19 | CodeUri: 20 | Bucket: cfn-api-gateway-custom-domain 21 | Key: example/index.zip 22 | 23 | Environment: 24 | Variables: 25 | TARGET_LOCATION: !Ref TargetLocation 26 | Events: 27 | get: 28 | Type: Api 29 | Properties: 30 | Path: / 31 | Method: get 32 | RestApiId: !Ref MyAPI 33 | 34 | MyAPI: 35 | Type: AWS::Serverless::Api 36 | Properties: 37 | StageName: prod 38 | DefinitionBody: 39 | swagger: 2.0 40 | paths: 41 | /: 42 | get: 43 | x-amazon-apigateway-integration: 44 | httpMethod: POST 45 | type: aws_proxy 46 | uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyEndpoint.Arn}/invocations 47 | 48 | MyHostedZone: 49 | Type: AWS::Route53::HostedZone 50 | Properties: 51 | Name: !Ref DomainName 52 | 53 | ApiGatewayCustomDomain: 54 | Type: AWS::CloudFormation::Stack 55 | Properties: 56 | TemplateURL: https://s3.amazonaws.com/cfn-api-gateway-custom-domain/stack.template 57 | Parameters: 58 | LetsEncryptAccountEmail: me@jedschmidt.com 59 | LetsEncryptAgreeTOS: true 60 | LetsEncryptManualPublicIpLoggingOk: true 61 | 62 | MyDomain: 63 | Type: Custom::ApiGatewayCustomDomain 64 | Properties: 65 | ServiceToken: !GetAtt ApiGatewayCustomDomain.Outputs.ServiceToken 66 | HostedZoneId: !Ref MyHostedZone 67 | Subdomain: www 68 | 69 | MyDNSRecord: 70 | Type: AWS::Route53::RecordSetGroup 71 | Properties: 72 | HostedZoneId: !Ref MyHostedZone 73 | RecordSets: 74 | - Type: A 75 | Name: !GetAtt MyDomain.domainName 76 | AliasTarget: 77 | HostedZoneId: Z2FDTNDATAQYW2 # This is hardcoded for all CloudFormation templates 78 | DNSName: !GetAtt MyDomain.distributionDomainName 79 | 80 | MyMapping: 81 | Type: AWS::ApiGateway::BasePathMapping 82 | Properties: 83 | DomainName: !GetAtt MyDomain.domainName 84 | RestApiId: !Ref MyAPI 85 | Stage: prod 86 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/local/lib64/node-v4.3.x/bin/node 2 | 3 | 'use strict' 4 | 5 | var child_process = require('child_process') 6 | var https = require('https') 7 | var http = require('http') 8 | var url = require('url') 9 | var fs = require('fs') 10 | var aws = require('aws-sdk') 11 | 12 | var route53 = new aws.Route53() 13 | var apiGateway = new aws.APIGateway() 14 | 15 | if ('CERTBOT_DOMAIN' in process.env) runHook() 16 | 17 | else exports.handler = function(event, context) { 18 | console.log("RECEIVED EVENT: %s", event.RequestType) 19 | var handlers = {Create, Delete, Update} 20 | var handler = handlers[event.RequestType] || function(event, cb) { 21 | cb(new TypeError(`Invalid RequestType: ${event.RequestType}`)) 22 | } 23 | 24 | handler(event, (err, Data) => { 25 | var Status = err ? 'FAILED' : 'SUCCESS' 26 | var Reason = err ? err.message : `See the CloudWatch Log Stream: ${context.logStreamName}` 27 | var responseData = { 28 | Status, 29 | Reason, 30 | PhysicalResourceId: context.logStreamName, 31 | StackId: event.StackId, 32 | RequestId: event.RequestId, 33 | LogicalResourceId: event.LogicalResourceId, 34 | Data 35 | } 36 | 37 | console.log("RESPONSE: %j", responseData) 38 | 39 | if (!event.ResponseURL) return context.succeed(responseData) 40 | 41 | var responseBody = new Buffer(JSON.stringify(responseData)) 42 | var uri = url.parse(event.ResponseURL) 43 | var options = { 44 | hostname: uri.hostname, 45 | port: 443, 46 | path: uri.path, 47 | method: 'PUT', 48 | headers: { 49 | 'Content-Type': '', 50 | 'Content-Length': responseBody.length 51 | } 52 | } 53 | 54 | var req = https.request(options) 55 | .on('error', onerror) 56 | .on('response', onresponse) 57 | 58 | function onresponse(res) { 59 | req.removeListener('error', onerror) 60 | 61 | if (res.statusCode == 200) return context.done() 62 | 63 | console.log(http.STATUS_CODES[res.statusCode]) 64 | context.done() 65 | } 66 | 67 | function onerror(err) { 68 | req.removeListener('response', onresponse) 69 | 70 | console.log(err) 71 | context.done() 72 | } 73 | 74 | req.end(responseBody) 75 | }) 76 | } 77 | 78 | function runHook() { 79 | var HostedZoneId = process.argv[2] 80 | var Action = process.env.CERTBOT_AUTH_OUTPUT ? 'DELETE' : 'UPSERT' 81 | var Name = `_acme-challenge.${process.env.CERTBOT_DOMAIN}` 82 | var Value = `"${process.env.CERTBOT_VALIDATION}"` 83 | 84 | var ResourceRecordSet = {Name, ResourceRecords: [{Value}], Type: 'TXT', TTL: 30} 85 | var Change = {Action, ResourceRecordSet} 86 | var params = {HostedZoneId, ChangeBatch: {Changes: [Change]}} 87 | 88 | route53.changeResourceRecordSets(params, (err, data) => { 89 | if (err) throw err 90 | 91 | if (Action === 'DELETE') return process.exit() 92 | 93 | var params = {Id: data.ChangeInfo.Id} 94 | 95 | route53.waitFor('resourceRecordSetsChanged', params, (err, data) => { 96 | if (err) throw err 97 | 98 | process.exit() 99 | }) 100 | }) 101 | } 102 | 103 | function Create(event, cb) { 104 | console.log('CREATING DOMAIN: %j', event) 105 | 106 | var ResourceProperties = event.ResourceProperties || {} 107 | 108 | var HostedZoneId = ResourceProperties.HostedZoneId 109 | if (!HostedZoneId) return cb(new Error('"HostedZoneId" missing in ResourceProperties')) 110 | 111 | var EmailAddress = process.env.EMAIL_ADDRESS 112 | 113 | var Subdomain = ResourceProperties.Subdomain 114 | 115 | route53.getHostedZone({Id: HostedZoneId}, (err, data) => { 116 | if (err) return cb(err) 117 | 118 | var domainName = data.HostedZone.Name.slice(0, -1) 119 | if (Subdomain) domainName = `${Subdomain}.${domainName}` 120 | 121 | var args = [ 122 | 'certonly', 123 | '--non-interactive', 124 | '--manual', 125 | '--manual-auth-hook', `/var/task/index.js ${HostedZoneId}`, 126 | '--manual-cleanup-hook', `/var/task/index.js ${HostedZoneId}`, 127 | '--preferred-challenge', 'dns', 128 | '--config-dir', '/tmp', 129 | '--work-dir', '/tmp', 130 | '--logs-dir', '/tmp', 131 | '--agree-tos', 132 | '--manual-public-ip-logging-ok', 133 | '--email', EmailAddress, 134 | '--domains', domainName 135 | ] 136 | 137 | console.log('CALLING CERTBOT: %j', args) 138 | 139 | var child = child_process.spawn('bin/certbot', args, {stdio: 'inherit'}) 140 | 141 | child.on('error', onerror) 142 | child.on('exit', onexit) 143 | 144 | function onerror(err) { 145 | child.removeListener('exit', onexit) 146 | cb(err) 147 | } 148 | 149 | function onexit(code, signal) { 150 | child.removeListener('error', onerror) 151 | 152 | var date = new Date().toISOString().slice(0, 10) 153 | var certificateName = `lets-encrypt-certificate-for-${domainName}-${date}` 154 | 155 | var params = {domainName, certificateName} 156 | var paramKeys = ['Body', 'Chain', 'PrivateKey'] 157 | var paramNames = paramKeys.map(x => `certificate${x}`) 158 | 159 | var fileNames = ['cert', 'chain', 'privkey'] 160 | var filePaths = fileNames.map(x => `/tmp/live/${domainName}/${x}.pem`) 161 | var files = filePaths.map(path => { 162 | return new Promise((resolve, reject) => { 163 | fs.readFile(path, 'utf8', (err, data) => { 164 | err ? reject(err) : resolve(data) 165 | }) 166 | }) 167 | }) 168 | 169 | Promise.all(files).then(fileData => { 170 | paramNames.forEach((name, i) => params[name] = fileData[i]) 171 | 172 | apiGateway.createDomainName(params, cb) 173 | }, cb) 174 | } 175 | }) 176 | } 177 | 178 | function Delete(event, cb) { 179 | console.log('DELETING DOMAIN: %j', event) 180 | 181 | var domainName = event.ResourceProperties.domainName 182 | 183 | apiGateway.getDomainName({domainName}, (err, data) => { 184 | if (!err || err.code === 'NotFoundException') return cb() 185 | 186 | apiGateway.deleteDomainName({domainName}, cb) 187 | }) 188 | } 189 | 190 | function Update(event, cb) { 191 | var domainName = event.OldResourceProperties.domainName 192 | 193 | Delete({domainName}, (err, data) => err ? cb(err) : Create(event, cb)) 194 | } 195 | --------------------------------------------------------------------------------