Repository: amplify-education/serverless-domain-manager (ts)

Issue:
Create R53 Endpoints with Latency or Weighted Base routing
The plugin currently allows only Simple routing. In cases where we want deploy API services to multiple region for DR purposes, the second custom domain has to be manually created as it overwrites the previous one. Also, how do we provide options for Routing(that than simple)

Pull request description:
Support latency and weighted routing
Fixes #96 

**Description of Issue Fixed**
Allow creating Route 53 records with latency or weighted routing policies.

**Changes proposed in this pull request**:
* Add an expandable `route53Params` config object
* Add `routingPolicy`, `weight`, `setIdentifier` and `healthCheckId` options to the `route53Params` object

**Motivation for the change**
We are working to improve latency for our global users by doing a number of regional serverless deployments with Route 53 latency records routing to the best endpoint. We would like to continue using _serverless-domain-manager_ and be able to allow it to create the route53 records as part of the serverless deployment workflow.

Patch:
diff --git a/README.md b/README.md
index 80405d0..913164c 100644
--- a/README.md
+++ b/README.md
@@ -120,6 +120,22 @@ custom:
         endpointType: 'regional'
 ```
 
+For multi-region deployments, a `route53Params` structure can be used to support latency or weighted routing policies
+
+```yaml
+custom:
+  customDomain:
+    domainName: serverless.foo.com
+    stage: ci
+    basePath: api
+    certificateName: '*.foo.com'
+    createRoute53Record: true
+    endpointType: 'regional'
+    securityPolicy: tls_1_2
+    route53Params:
+      routingPolicy: latency
+```
+
 | Parameter Name | Default Value | Description |
 | --- | --- | --- |
 | domainName _(Required)_ | | The domain name to be created in API Gateway and Route53 (if enabled) for this API. |
@@ -137,7 +153,11 @@ securityPolicy | tls_1_2 | The security policy to apply to the custom domain nam
 allowPathMatching | false | When updating an existing api mapping this will match on the basePath instead of the API ID to find existing mappings for an update. This should only be used when changing API types. For example, migrating a REST API to an HTTP API. See Changing API Types for more information.  |
 | autoDomain | `false` | Toggles whether or not the plugin will run `create_domain/delete_domain` as part of `sls deploy/remove` so that multiple commands are not required. |
 | autoDomainWaitFor | `120` | How long to wait for create_domain to finish before starting deployment if domain does not exist immediately. |
-
+| route53Params |  | A set of options to customize Route 53 record creation. If left empty, A and AAAA records with simple routing will be created. If `createRoute53Record` is `false`, anything passed here will be ignored.  |
+| route53Params:<br/>&nbsp; routingPolicy | simple | Defines the Route 53 routing policy, accepts `simple`, `latency` or `weighted`. |
+| route53Params:<br/>&nbsp; weight | `200` | Sets the weight for weighted routing. Ignored for `simple` and `latency` routing. |
+| route53Params:<br/>&nbsp; setIdentifier |  | A unique identifier for records in a set of Route 53 records with the same domain name. Only relevant for `latency` and `weighted` routing. Defaults to the regional endpoint if not provided. |
+| route53Params:<br/>&nbsp; healthCheckId |  | An optional id for a Route 53 health check. If it is failing, Route 53 will stop routing to it. Only relevant for `latency` and `weighted` routing. If it is not provided, no health check will be associated with the record. |
 
 
 ## Running
diff --git a/package-lock.json b/package-lock.json
index 2d0c530..a5b1c51 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "serverless-domain-manager",
-  "version": "5.1.2",
+  "version": "5.1.5",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -1701,7 +1701,7 @@
     },
     "base64-js": {
       "version": "1.5.1",
-      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+      "resolved": "https://funimation-904385204553.d.codeartifact.us-west-2.amazonaws.com:443/npm/funicom-artifacts-nodejs-prd/base64-js/-/base64-js-1.5.1.tgz",
       "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
     },
     "bcrypt-pbkdf": {
@@ -1869,7 +1869,7 @@
     },
     "buffer": {
       "version": "4.9.2",
-      "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
+      "resolved": "https://funimation-904385204553.d.codeartifact.us-west-2.amazonaws.com:443/npm/funicom-artifacts-nodejs-prd/buffer/-/buffer-4.9.2.tgz",
       "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
       "requires": {
         "base64-js": "^1.0.2",
@@ -3148,7 +3148,7 @@
     },
     "events": {
       "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
+      "resolved": "https://funimation-904385204553.d.codeartifact.us-west-2.amazonaws.com:443/npm/funicom-artifacts-nodejs-prd/events/-/events-1.1.1.tgz",
       "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ="
     },
     "exit-on-epipe": {
@@ -3932,7 +3932,7 @@
     },
     "ieee754": {
       "version": "1.1.13",
-      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
+      "resolved": "https://funimation-904385204553.d.codeartifact.us-west-2.amazonaws.com:443/npm/funicom-artifacts-nodejs-prd/ieee754/-/ieee754-1.1.13.tgz",
       "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
     },
     "ignore": {
@@ -4162,7 +4162,7 @@
     },
     "isarray": {
       "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "resolved": "https://funimation-904385204553.d.codeartifact.us-west-2.amazonaws.com:443/npm/funicom-artifacts-nodejs-prd/isarray/-/isarray-1.0.0.tgz",
       "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
     },
     "isexe": {
@@ -4292,7 +4292,7 @@
     },
     "jmespath": {
       "version": "0.15.0",
-      "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz",
+      "resolved": "https://funimation-904385204553.d.codeartifact.us-west-2.amazonaws.com:443/npm/funicom-artifacts-nodejs-prd/jmespath/-/jmespath-0.15.0.tgz",
       "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc="
     },
     "js-tokens": {
@@ -5796,7 +5796,7 @@
     },
     "punycode": {
       "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+      "resolved": "https://funimation-904385204553.d.codeartifact.us-west-2.amazonaws.com:443/npm/funicom-artifacts-nodejs-prd/punycode/-/punycode-1.3.2.tgz",
       "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
     },
     "qrcode-terminal": {
@@ -5813,7 +5813,7 @@
     },
     "querystring": {
       "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+      "resolved": "https://funimation-904385204553.d.codeartifact.us-west-2.amazonaws.com:443/npm/funicom-artifacts-nodejs-prd/querystring/-/querystring-0.2.0.tgz",
       "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
     },
     "queue-microtask": {
@@ -6147,7 +6147,7 @@
     },
     "sax": {
       "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
+      "resolved": "https://funimation-904385204553.d.codeartifact.us-west-2.amazonaws.com:443/npm/funicom-artifacts-nodejs-prd/sax/-/sax-1.2.1.tgz",
       "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o="
     },
     "seek-bzip": {
@@ -6866,7 +6866,7 @@
     },
     "supports-color": {
       "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "resolved": "https://funimation-904385204553.d.codeartifact.us-west-2.amazonaws.com:443/npm/funicom-artifacts-nodejs-prd/supports-color/-/supports-color-7.2.0.tgz",
       "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
       "requires": {
         "has-flag": "^4.0.0"
@@ -7515,7 +7515,7 @@
     },
     "url": {
       "version": "0.10.3",
-      "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
+      "resolved": "https://funimation-904385204553.d.codeartifact.us-west-2.amazonaws.com:443/npm/funicom-artifacts-nodejs-prd/url/-/url-0.10.3.tgz",
       "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=",
       "requires": {
         "punycode": "1.3.2",
@@ -7548,7 +7548,7 @@
     },
     "uuid": {
       "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+      "resolved": "https://funimation-904385204553.d.codeartifact.us-west-2.amazonaws.com:443/npm/funicom-artifacts-nodejs-prd/uuid/-/uuid-3.3.2.tgz",
       "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
     },
     "verror": {
@@ -7791,7 +7791,7 @@
     },
     "xml2js": {
       "version": "0.4.19",
-      "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
+      "resolved": "https://funimation-904385204553.d.codeartifact.us-west-2.amazonaws.com:443/npm/funicom-artifacts-nodejs-prd/xml2js/-/xml2js-0.4.19.tgz",
       "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
       "requires": {
         "sax": ">=0.6.0",
@@ -7800,7 +7800,7 @@
     },
     "xmlbuilder": {
       "version": "9.0.7",
-      "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
+      "resolved": "https://funimation-904385204553.d.codeartifact.us-west-2.amazonaws.com:443/npm/funicom-artifacts-nodejs-prd/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
       "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0="
     },
     "xmlhttprequest-ssl": {
diff --git a/src/domain-config.ts b/src/domain-config.ts
index 96b2345..425b44f 100644
--- a/src/domain-config.ts
+++ b/src/domain-config.ts
@@ -5,7 +5,8 @@
 import * as AWS from "aws-sdk"; // imported for Types
 import DomainInfo = require("./domain-info");
 import Globals from "./globals";
-import {CustomDomain} from "./types";
+import {CustomDomain, Route53Params} from "./types";
+
 
 class DomainConfig {
 
@@ -25,11 +26,13 @@ class DomainConfig {
     public securityPolicy: string | undefined;
     public autoDomain: boolean | undefined;
     public autoDomainWaitFor: string | undefined;
+    public route53Params: Route53Params;
 
     public domainInfo: DomainInfo | undefined;
     public apiId: string | undefined;
     public apiMapping: AWS.ApiGatewayV2.GetApiMappingResponse;
     public allowPathMatching: boolean | false;
+    public region: string;
 
     constructor(config: CustomDomain) {
 
@@ -78,12 +81,31 @@ class DomainConfig {
         }
         this.securityPolicy = tlsVersionToUse;
 
-        let region = Globals.defaultRegion;
+        this.region = Globals.defaultRegion;
         if (this.endpointType === Globals.endpointTypes.regional) {
-            region = Globals.serverless.providers.aws.getRegion();
+            this.region = Globals.serverless.providers.aws.getRegion();
         }
-        const acmCredentials = Object.assign({}, Globals.serverless.providers.aws.getCredentials(), {region});
+        const acmCredentials = Object.assign({}, Globals.serverless.providers.aws.getCredentials(), { region: this.region });
         this.acm = new Globals.serverless.providers.aws.sdk.ACM(acmCredentials);
+
+        const routingPolicy = config.route53Params?.routingPolicy?.toLowerCase() ?? 'simple';
+        const routingPolicyToUse = Globals.routingPolicies[routingPolicy];
+        if (!routingPolicyToUse) {
+            throw new Error(`${routingPolicy} is not a supported routing policy, use simple, latency, or weighted.`);
+        }
+
+        if (routingPolicyToUse !== Globals.routingPolicies.simple
+            && endpointTypeToUse === Globals.endpointTypes.edge)
+        {
+            throw new Error(`${routingPolicy} routing is not intended to be used with edge endpoints. Use a regional endpoint instead.`);
+        }
+
+        this.route53Params = {
+            routingPolicy: routingPolicyToUse,
+            setIdentifier: config.route53Params?.setIdentifier,
+            weight: config.route53Params?.weight ?? 200,
+            healthCheckId: config.route53Params?.healthCheckId
+        }
     }
 
     /**
diff --git a/src/globals.ts b/src/globals.ts
index 58b4b5c..39e527a 100644
--- a/src/globals.ts
+++ b/src/globals.ts
@@ -32,6 +32,12 @@ export default class Globals {
         tls_1_2: "TLS_1_2",
     };
 
+    public static routingPolicies = {
+        simple: "simple",
+        latency: "latency",
+        weighted: "weighted",
+    };
+
     public static cliLog(prefix: string, message: string): void {
         Globals.serverless.cli.log(`${prefix} ${message}`, Globals.pluginName);
     }
diff --git a/src/index.ts b/src/index.ts
index 7cabf64..cc6c9f0 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -430,6 +430,27 @@ class ServerlessCustomDomain {
         }
         // Set up parameters
         const route53HostedZoneId = await this.getRoute53HostedZoneId(domain);
+
+        const healthCheckConfig = domain.route53Params.healthCheckId
+            ? { HealthCheckId: domain.route53Params.healthCheckId }
+            : {};
+
+        const latencyRecordConfig = domain.route53Params.routingPolicy === "latency"
+            ? {
+                Region: domain.region,
+                SetIdentifier: domain.route53Params.setIdentifier ?? domain.domainInfo.domainName,
+                ...healthCheckConfig,
+            }
+            : {};
+
+        const weightedRecordConfig = domain.route53Params.routingPolicy === "weighted"
+            ? {
+                Weight: domain.route53Params.weight,
+                SetIdentifier: domain.route53Params.setIdentifier ?? domain.domainInfo.domainName,
+                ...healthCheckConfig,
+            }
+            : {};
+
         const Changes = ["A", "AAAA"].map((Type) => ({
             Action: action,
             ResourceRecordSet: {
@@ -440,8 +461,11 @@ class ServerlessCustomDomain {
                 },
                 Name: domain.givenDomainName,
                 Type,
+                ...latencyRecordConfig,
+                ...weightedRecordConfig,
             },
         }));
+
         const params = {
             ChangeBatch: {
                 Changes,
diff --git a/src/types.ts b/src/types.ts
index 0e344d9..bfd930a 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -14,6 +14,7 @@ export interface CustomDomain { // tslint:disable-line
     autoDomain: boolean | undefined;
     autoDomainWaitFor: string | undefined;
     allowPathMatching: boolean | undefined;
+    route53Params: Route53Params | undefined
 }
 
 export interface ServerlessInstance { // tslint:disable-line
@@ -60,3 +61,10 @@ export interface ServerlessInstance { // tslint:disable-line
 export interface ServerlessOptions { // tslint:disable-line
     stage: string;
 }
+
+export interface Route53Params {
+    routingPolicy: 'simple' | 'latency' | 'weighted' | undefined;
+    weight: number | undefined;
+    setIdentifier: string | undefined;
+    healthCheckId: string | undefined;
+};


Test patch:
diff --git a/test/integration-tests/deploy.test.ts b/test/integration-tests/deploy.test.ts
index 3e6e845..6501793 100644
--- a/test/integration-tests/deploy.test.ts
+++ b/test/integration-tests/deploy.test.ts
@@ -100,6 +100,22 @@ const testCases = [
         testFolder: `${CONFIGS_FOLDER}/basepath-nested-stack`,
         testStage: "test",
     },
+    {
+        testBasePath: "(none)",
+        testDescription: "Deploy with latency routing",
+        testDomain: `route-53-latency-routing-${RANDOM_STRING}.${TEST_DOMAIN}`,
+        testEndpoint: "REGIONAL",
+        testFolder: `${CONFIGS_FOLDER}/route-53-latency-routing`,
+        testStage: "test",
+    },
+    {
+        testBasePath: "(none)",
+        testDescription: "Deploy with weighted routing",
+        testDomain: `route-53-weighted-routing-${RANDOM_STRING}.${TEST_DOMAIN}`,
+        testEndpoint: "REGIONAL",
+        testFolder: `${CONFIGS_FOLDER}/route-53-weighted-routing`,
+        testStage: "test",
+    },
 ];
 
 describe("Integration Tests", function() {
diff --git a/test/integration-tests/deploy/route-53-latency-routing/handler.js b/test/integration-tests/deploy/route-53-latency-routing/handler.js
new file mode 100644
index 0000000..1bd222d
--- /dev/null
+++ b/test/integration-tests/deploy/route-53-latency-routing/handler.js
@@ -0,0 +1,16 @@
+"use strict";
+
+module.exports.helloWorld = (event, context, callback) => {
+  const response = {
+    statusCode: 200,
+    headers: {
+      "Access-Control-Allow-Origin": "*", // Required for CORS support to work
+    },
+    body: JSON.stringify({
+      message: "Go Serverless v1.0! Your function executed successfully!",
+      input: event,
+    }),
+  };
+
+  callback(null, response);
+};
diff --git a/test/integration-tests/deploy/route-53-latency-routing/serverless.yml b/test/integration-tests/deploy/route-53-latency-routing/serverless.yml
new file mode 100644
index 0000000..6877bd9
--- /dev/null
+++ b/test/integration-tests/deploy/route-53-latency-routing/serverless.yml
@@ -0,0 +1,27 @@
+service: route-53-latency-routing-${opt:RANDOM_STRING}
+provider:
+  name: aws
+  runtime: nodejs12.x
+  region: us-west-2
+  endpointType: regional
+  stage: test
+functions:
+  helloWorld:
+    handler: handler.helloWorld
+    events:
+      - http:
+          path: hello-world
+          method: get
+          cors: true
+plugins:
+  - serverless-domain-manager
+custom:
+  customDomain:
+    domainName: route-53-latency-routing-${opt:RANDOM_STRING}.${env:TEST_DOMAIN}
+    endpointType: regional
+    route53Params:
+      routingPolicy: latency
+
+package:
+  exclude:
+    - node_modules/**
diff --git a/test/integration-tests/deploy/route-53-weighted-routing/handler.js b/test/integration-tests/deploy/route-53-weighted-routing/handler.js
new file mode 100644
index 0000000..1bd222d
--- /dev/null
+++ b/test/integration-tests/deploy/route-53-weighted-routing/handler.js
@@ -0,0 +1,16 @@
+"use strict";
+
+module.exports.helloWorld = (event, context, callback) => {
+  const response = {
+    statusCode: 200,
+    headers: {
+      "Access-Control-Allow-Origin": "*", // Required for CORS support to work
+    },
+    body: JSON.stringify({
+      message: "Go Serverless v1.0! Your function executed successfully!",
+      input: event,
+    }),
+  };
+
+  callback(null, response);
+};
diff --git a/test/integration-tests/deploy/route-53-weighted-routing/serverless.yml b/test/integration-tests/deploy/route-53-weighted-routing/serverless.yml
new file mode 100644
index 0000000..0d07d0a
--- /dev/null
+++ b/test/integration-tests/deploy/route-53-weighted-routing/serverless.yml
@@ -0,0 +1,28 @@
+service: route-53-weighted-routing-${opt:RANDOM_STRING}
+provider:
+  name: aws
+  runtime: nodejs12.x
+  region: us-west-2
+  endpointType: regional
+  stage: test
+functions:
+  helloWorld:
+    handler: handler.helloWorld
+    events:
+      - http:
+          path: hello-world
+          method: get
+          cors: true
+plugins:
+  - serverless-domain-manager
+custom:
+  customDomain:
+    domainName: route-53-weighted-routing-${opt:RANDOM_STRING}.${env:TEST_DOMAIN}
+    endpointType: regional
+    route53Params:
+      routingPolicy: weighted
+      weight: 100
+
+package:
+  exclude:
+    - node_modules/**
diff --git a/test/unit-tests/index.test.ts b/test/unit-tests/index.test.ts
index 3a1f126..ac59b96 100644
--- a/test/unit-tests/index.test.ts
+++ b/test/unit-tests/index.test.ts
@@ -55,6 +55,7 @@ const constructPlugin = (customDomainOptions, multiple: boolean = false) => {
         hostedZonePrivate: customDomainOptions.hostedZonePrivate,
         securityPolicy: customDomainOptions.securityPolicy,
         stage: customDomainOptions.stage,
+        route53Params: customDomainOptions.route53Params
     };
 
     const serverless = {
@@ -1873,4 +1874,344 @@ describe("Custom Domain Plugin", () => {
             consoleOutput = [];
         });
     });
+
+    describe("Route53 Routing Policies", () => {
+        it("Should create a new Alias Record with latency routing", async () => {
+            AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                callback(null, {
+                    HostedZones: [{
+                        Config: {PrivateZone: false},
+                        Id: "test_host_id",
+                        Name: "test_domain",
+                    }],
+                });
+            });
+
+            AWS.mock("Route53", "changeResourceRecordSets", (params, callback) => {
+                callback(null, params);
+            });
+
+            const plugin = constructPlugin({
+                basePath: "test_basepath",
+                domainName: "test_domain",
+                endpointType: "regional",
+                route53Params: {
+                    routingPolicy: 'latency'
+                }
+            });
+            plugin.route53 = new aws.Route53();
+
+            const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain);
+
+            dc.domainInfo = new DomainInfo(
+                {
+                    regionalDomainName: "test_regional_name",
+                    regionalHostedZoneId: "test_id",
+                },
+            );
+
+            const spy = chai.spy.on(plugin.route53, "changeResourceRecordSets");
+
+            await plugin.changeResourceRecordSet("UPSERT", dc);
+
+            const expectedParams = {
+                ChangeBatch: {
+                    Changes: [
+                        {
+                            Action: "UPSERT",
+                            ResourceRecordSet: {
+                                AliasTarget: {
+                                    DNSName: "test_regional_name",
+                                    EvaluateTargetHealth: false,
+                                    HostedZoneId: "test_id",
+                                },
+                                Name: "test_domain",
+                                Type: "A",
+                                Region: "eu-west-1",
+                                SetIdentifier: "test_regional_name",
+                            },
+                        },
+                        {
+                            Action: "UPSERT",
+                            ResourceRecordSet: {
+                                AliasTarget: {
+                                    DNSName: "test_regional_name",
+                                    EvaluateTargetHealth: false,
+                                    HostedZoneId: "test_id",
+                                },
+                                Name: "test_domain",
+                                Type: "AAAA",
+                                Region: "eu-west-1",
+                                SetIdentifier: "test_regional_name",
+                            },
+                        },
+                    ],
+                    Comment: "Record created by serverless-domain-manager",
+                },
+                HostedZoneId: "est_host_id", // getRoute53HostedZoneId strips first character
+            };
+            expect(spy).to.have.been.called.with(expectedParams);
+        });
+
+        it("Should create a new Alias Record with weighted routing", async () => {
+            AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                callback(null, {
+                    HostedZones: [{
+                        Config: {PrivateZone: false},
+                        Id: "test_host_id",
+                        Name: "test_domain",
+                    }],
+                });
+            });
+
+            AWS.mock("Route53", "changeResourceRecordSets", (params, callback) => {
+                callback(null, params);
+            });
+
+            const plugin = constructPlugin({
+                basePath: "test_basepath",
+                domainName: "test_domain",
+                endpointType: "regional",
+                route53Params: {
+                    routingPolicy: 'weighted',
+                    weight: 100,
+                    healthCheckId: "test_healthcheck",
+                }
+            });
+            plugin.route53 = new aws.Route53();
+
+            const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain);
+
+            dc.domainInfo = new DomainInfo(
+                {
+                    regionalDomainName: "test_regional_name",
+                    regionalHostedZoneId: "test_id",
+                },
+            );
+
+            const spy = chai.spy.on(plugin.route53, "changeResourceRecordSets");
+
+            await plugin.changeResourceRecordSet("UPSERT", dc);
+
+            const expectedParams = {
+                ChangeBatch: {
+                    Changes: [
+                        {
+                            Action: "UPSERT",
+                            ResourceRecordSet: {
+                                AliasTarget: {
+                                    DNSName: "test_regional_name",
+                                    EvaluateTargetHealth: false,
+                                    HostedZoneId: "test_id",
+                                },
+                                Name: "test_domain",
+                                Type: "A",
+                                SetIdentifier: "test_regional_name",
+                                Weight: 100,
+                                HealthCheckId: "test_healthcheck",
+                            },
+                        },
+                        {
+                            Action: "UPSERT",
+                            ResourceRecordSet: {
+                                AliasTarget: {
+                                    DNSName: "test_regional_name",
+                                    EvaluateTargetHealth: false,
+                                    HostedZoneId: "test_id",
+                                },
+                                Name: "test_domain",
+                                Type: "AAAA",
+                                SetIdentifier: "test_regional_name",
+                                Weight: 100,
+                                HealthCheckId: "test_healthcheck",
+                            },
+                        },
+                    ],
+                    Comment: "Record created by serverless-domain-manager",
+                },
+                HostedZoneId: "est_host_id", // getRoute53HostedZoneId strips first character
+            };
+            expect(spy).to.have.been.called.with(expectedParams);
+        });
+
+        it("Should exclude weight input with latency routing", async () => {
+            AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                callback(null, {
+                    HostedZones: [{
+                        Config: {PrivateZone: false},
+                        Id: "test_host_id",
+                        Name: "test_domain",
+                    }],
+                });
+            });
+
+            AWS.mock("Route53", "changeResourceRecordSets", (params, callback) => {
+                callback(null, params);
+            });
+
+            const plugin = constructPlugin({
+                basePath: "test_basepath",
+                domainName: "test_domain",
+                endpointType: "regional",
+                route53Params: {
+                    routingPolicy: 'latency',
+                    weight: 100,
+                }
+            });
+            plugin.route53 = new aws.Route53();
+
+            const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain);
+
+            dc.domainInfo = new DomainInfo(
+                {
+                    regionalDomainName: "test_regional_name",
+                    regionalHostedZoneId: "test_id",
+                },
+            );
+
+            const spy = chai.spy.on(plugin.route53, "changeResourceRecordSets");
+
+            await plugin.changeResourceRecordSet("UPSERT", dc);
+
+            const expectedParams = {
+                ChangeBatch: {
+                    Changes: [
+                        {
+                            Action: "UPSERT",
+                            ResourceRecordSet: {
+                                AliasTarget: {
+                                    DNSName: "test_regional_name",
+                                    EvaluateTargetHealth: false,
+                                    HostedZoneId: "test_id",
+                                },
+                                Name: "test_domain",
+                                Type: "A",
+                                Region: "eu-west-1",
+                                SetIdentifier: "test_regional_name",
+                            },
+                        },
+                        {
+                            Action: "UPSERT",
+                            ResourceRecordSet: {
+                                AliasTarget: {
+                                    DNSName: "test_regional_name",
+                                    EvaluateTargetHealth: false,
+                                    HostedZoneId: "test_id",
+                                },
+                                Name: "test_domain",
+                                Type: "AAAA",
+                                Region: "eu-west-1",
+                                SetIdentifier: "test_regional_name",
+                            },
+                        },
+                    ],
+                    Comment: "Record created by serverless-domain-manager",
+                },
+                HostedZoneId: "est_host_id", // getRoute53HostedZoneId strips first character
+            };
+            expect(spy).to.have.been.called.with(expectedParams);
+        });
+
+        it("Should exclude weight, region, set identifier, and health input with simple routing", async () => {
+            AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                callback(null, {
+                    HostedZones: [{
+                        Config: {PrivateZone: false},
+                        Id: "test_host_id",
+                        Name: "test_domain",
+                    }],
+                });
+            });
+
+            AWS.mock("Route53", "changeResourceRecordSets", (params, callback) => {
+                callback(null, params);
+            });
+
+            const plugin = constructPlugin({
+                basePath: "test_basepath",
+                domainName: "test_domain",
+                endpointType: "regional",
+                route53Params: {
+                    setIdentifier: "test_identifier",
+                    weight: 100,
+                }
+            });
+            plugin.route53 = new aws.Route53();
+
+            const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain);
+
+            dc.domainInfo = new DomainInfo(
+                {
+                    regionalDomainName: "test_regional_name",
+                    regionalHostedZoneId: "test_id",
+                },
+            );
+
+            const spy = chai.spy.on(plugin.route53, "changeResourceRecordSets");
+
+            await plugin.changeResourceRecordSet("UPSERT", dc);
+
+            const expectedParams = {
+                ChangeBatch: {
+                    Changes: [
+                        {
+                            Action: "UPSERT",
+                            ResourceRecordSet: {
+                                AliasTarget: {
+                                    DNSName: "test_regional_name",
+                                    EvaluateTargetHealth: false,
+                                    HostedZoneId: "test_id",
+                                },
+                                Name: "test_domain",
+                                Type: "A",
+                            },
+                        },
+                        {
+                            Action: "UPSERT",
+                            ResourceRecordSet: {
+                                AliasTarget: {
+                                    DNSName: "test_regional_name",
+                                    EvaluateTargetHealth: false,
+                                    HostedZoneId: "test_id",
+                                },
+                                Name: "test_domain",
+                                Type: "AAAA",
+                            },
+                        },
+                    ],
+                    Comment: "Record created by serverless-domain-manager",
+                },
+                HostedZoneId: "est_host_id", // getRoute53HostedZoneId strips first character
+            };
+            expect(spy).to.have.been.called.with(expectedParams);
+        });
+
+        it("Should throw an Error when passing a routing policy that is not supported", async () => {
+            const plugin = constructPlugin({route53Params: { routingPolicy: 'test_policy'} });
+
+            let errored = false;
+            try {
+                await plugin.hookWrapper(null);
+            } catch (err) {
+                errored = true;
+                expect(err.message).to.equal("test_policy is not a supported routing policy, use simple, latency, or weighted.");
+            }
+            expect(errored).to.equal(true);
+        });
+
+        it("Should throw an Error when using latency routing with edge endpoints", async () => {
+            const plugin = constructPlugin({
+                route53Params: { routingPolicy: "latency"}
+            });
+
+            let errored = false;
+            try {
+                await plugin.hookWrapper(null);
+            } catch (err) {
+                errored = true;
+                expect(err.message).to.equal("latency routing is not intended to be used with edge endpoints. Use a regional endpoint instead.");
+            }
+            expect(errored).to.equal(true);
+        });
+    });
 });