Repository: amplify-education/serverless-domain-manager
Pull request #471 (2022-01-18 15:45:01)

AT-5519: Custom route53 profile
<!--- Information about referencing Github Issues: https://help.github.com/articles/basic-writing-and-formatting-syntax/#referencing-issues-and-pull-requests --->
Fixes #198

**Description of Issue Fixed**
Related PRs:
https://github.com/amplify-education/serverless-domain-manager/pull/198
https://github.com/amplify-education/serverless-domain-manager/pull/393


**Changes proposed in this pull request**:

- Add custom route53 profile options
- Refactoring

<!--- Please remember to allow edits from maintainers: https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork --->



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 069bfc2..2e4dcfe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
 and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
 
+## [5.4.0] - 2021-01-20
+- Added custom route53 profile options. Thank you @CodeVision ([393](https://github.com/amplify-education/serverless-domain-manager/pull/393))
+
 ## [5.3.2] - 2021-01-19
 - Added logs for Serverless Framework v3. Thank you @medikoo ([448](https://github.com/amplify-education/serverless-domain-manager/pull/448))
 
diff --git a/README.md b/README.md
index 2130449..950a819 100644
--- a/README.md
+++ b/README.md
@@ -144,6 +144,8 @@ custom:
 | certificateName | Closest match | The name of a specific certificate from Certificate Manager to use with this API. If not specified, the closest match will be used (i.e. for a given domain name `api.example.com`, a certificate for `api.example.com` will take precedence over a `*.example.com` certificate). <br><br> Note: Edge-optimized endpoints require that the certificate be located in `us-east-1` to be used with the CloudFront distribution. |
 | certificateArn | `(none)` | The arn of a specific certificate from Certificate Manager to use with this API. |
 | createRoute53Record | `true` | Toggles whether or not the plugin will create an A Alias and AAAA Alias records in Route53 mapping the `domainName` to the generated distribution domain name. If false, does not create a record. |
+| route53Profile | `(none)` | Profile to use for accessing Route53 resources when Route53 records are in a different account |
+| route53Region | `(none)` | Region to send Route53 services requests to (only applicable if also using route53Profile option) |
 | endpointType | edge | Defines the endpoint type, accepts `regional` or `edge`. |
 | apiType | rest | Defines the api type, accepts `rest`, `http` or `websocket`. |
 | hostedZoneId | | If hostedZoneId is set the route53 record set will be created in the matching zone, otherwise the hosted zone will be figured out from the domainName (hosted zone with matching domain). |
@@ -195,9 +197,11 @@ To run unit tests:
 npm test
 ```
 
-To run integration tests, set an environment variable `TEST_DOMAIN` to the domain you will be testing for (i.e. `example.com` if creating a domain for `api.example.com`). Then,
+To run integration tests, set an environment variable `TEST_DOMAIN` to the domain you will be testing for (i.e. `example.com` if creating a domain for `api.example.com`). 
+And `ROUTE53_PROFILE` for creating route53 record in one AWS account and deploy in another. Then,
 ```
 export TEST_DOMAIN=example.com
+export ROUTE53_PROFILE=default
 npm run build
 npm run integration-test
 ```
@@ -237,6 +241,7 @@ NOTE: Always test this process in a lower level staging or development environme
 * (1/17/2018) The `create_domain` command provided by this plugin does not currently update an existing Custom Domain's configuration. Instead, it only supports updating the Route 53 record pointing to the Custom Domain. For example, one must delete and recreate a Custom Domain to migrate it from regional to edge or vice versa, or to modify the certificate.
 * (8/22/2018) Creating a custom domain creates a CloudFront Distribution behind the scenes for fronting your API Gateway. This CloudFront Distribution is managed by AWS and cannot be viewed/managed by you. This is not a bug, but a quirk of how the Custom Domain feature works in API Gateway.
 * (2/12/2019) Users who upgraded from 2.x.x to version 3.0.4 (now unpublished) and then reverted back to 2.x.x will be unable to deploy because of a bug that will be fixed in 3.1.0. The workaround is to delete the basepath mapping manually, which will let them successfully revert back to 2.x.x.
+* (1/20/2022) Using `route53Profile` option requires having hosted zone for the domain in this profile and ACM certificate in the main profile (where functions are deployed).  
 
 # Responsible Disclosure
 If you have any security issue to report, contact project maintainers privately.
diff --git a/package-lock.json b/package-lock.json
index c386483..f674b6f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,21 +1,21 @@
 {
   "name": "serverless-domain-manager",
-  "version": "5.3.2",
+  "version": "5.4.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "serverless-domain-manager",
-      "version": "5.3.2",
+      "version": "5.4.0",
       "license": "MIT",
       "dependencies": {
-        "aws-sdk": "^2.1058.0",
+        "aws-sdk": "^2.1060.0",
         "chalk": "^4.1.2"
       },
       "devDependencies": {
-        "@types/mocha": "^9.0.0",
+        "@types/mocha": "^9.1.0",
         "@types/node": "^17.0.10",
-        "aws-sdk-mock": "^5.5.1",
+        "aws-sdk-mock": "^5.6.0",
         "chai": "^4.3.4",
         "chai-spies": "^1.0.0",
         "mocha": "^9.1.4",
@@ -1243,9 +1243,9 @@
       "dev": true
     },
     "node_modules/@types/mocha": {
-      "version": "9.0.0",
-      "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz",
-      "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==",
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.0.tgz",
+      "integrity": "sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg==",
       "dev": true
     },
     "node_modules/@types/node": {
@@ -1737,9 +1737,9 @@
       "dev": true
     },
     "node_modules/aws-sdk": {
-      "version": "2.1058.0",
-      "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1058.0.tgz",
-      "integrity": "sha512-q6bTq1DBBeBaU6GKKoTHmJj16WOQHhOoK7jwV93IT8pO0P1XH99gesFofhew3eT0h8Ev7quVKutk4B1kfnIXPQ==",
+      "version": "2.1060.0",
+      "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1060.0.tgz",
+      "integrity": "sha512-c734/CZiVSsuVnEkx/7dodI8ndgOnxiCTwwlEFlMxdAZfLLJplteFwi6c/J2GZQktvz2ysV/HVtNKcJkasYJzw==",
       "dependencies": {
         "buffer": "4.9.2",
         "events": "1.1.1",
@@ -1756,9 +1756,9 @@
       }
     },
     "node_modules/aws-sdk-mock": {
-      "version": "5.5.1",
-      "resolved": "https://registry.npmjs.org/aws-sdk-mock/-/aws-sdk-mock-5.5.1.tgz",
-      "integrity": "sha512-9wGrPozD7YciPt1qzo0ypviKEZKFJ6Jab8qtYmDRNG41WxZGJ//EEIv1FL6t4bEc8j4bPiwnoam5TP5eTv7UDA==",
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/aws-sdk-mock/-/aws-sdk-mock-5.6.0.tgz",
+      "integrity": "sha512-gUoEs/FaszVTLG5zBveg2NC2w/j/c9j6Ymgq4whrsdysClW6FWKT9w8eOUERZChliKZe/LwHHJFKjBHGrV9MJg==",
       "dev": true,
       "dependencies": {
         "aws-sdk": "^2.928.0",
@@ -10754,9 +10754,9 @@
       "dev": true
     },
     "@types/mocha": {
-      "version": "9.0.0",
-      "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz",
-      "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==",
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.0.tgz",
+      "integrity": "sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg==",
       "dev": true
     },
     "@types/node": {
@@ -11171,9 +11171,9 @@
       "dev": true
     },
     "aws-sdk": {
-      "version": "2.1058.0",
-      "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1058.0.tgz",
-      "integrity": "sha512-q6bTq1DBBeBaU6GKKoTHmJj16WOQHhOoK7jwV93IT8pO0P1XH99gesFofhew3eT0h8Ev7quVKutk4B1kfnIXPQ==",
+      "version": "2.1060.0",
+      "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1060.0.tgz",
+      "integrity": "sha512-c734/CZiVSsuVnEkx/7dodI8ndgOnxiCTwwlEFlMxdAZfLLJplteFwi6c/J2GZQktvz2ysV/HVtNKcJkasYJzw==",
       "requires": {
         "buffer": "4.9.2",
         "events": "1.1.1",
@@ -11187,9 +11187,9 @@
       }
     },
     "aws-sdk-mock": {
-      "version": "5.5.1",
-      "resolved": "https://registry.npmjs.org/aws-sdk-mock/-/aws-sdk-mock-5.5.1.tgz",
-      "integrity": "sha512-9wGrPozD7YciPt1qzo0ypviKEZKFJ6Jab8qtYmDRNG41WxZGJ//EEIv1FL6t4bEc8j4bPiwnoam5TP5eTv7UDA==",
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/aws-sdk-mock/-/aws-sdk-mock-5.6.0.tgz",
+      "integrity": "sha512-gUoEs/FaszVTLG5zBveg2NC2w/j/c9j6Ymgq4whrsdysClW6FWKT9w8eOUERZChliKZe/LwHHJFKjBHGrV9MJg==",
       "dev": true,
       "requires": {
         "aws-sdk": "^2.928.0",
diff --git a/package.json b/package.json
index 0e0ddd1..7a9cedc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "serverless-domain-manager",
-  "version": "5.3.2",
+  "version": "5.4.0",
   "engines": {
     "node": ">=12"
   },
@@ -46,9 +46,9 @@
     ]
   },
   "devDependencies": {
-    "@types/mocha": "^9.0.0",
+    "@types/mocha": "^9.1.0",
     "@types/node": "^17.0.10",
-    "aws-sdk-mock": "^5.5.1",
+    "aws-sdk-mock": "^5.6.0",
     "chai": "^4.3.4",
     "chai-spies": "^1.0.0",
     "mocha": "^9.1.4",
@@ -65,7 +65,7 @@
     "typescript": "^4.5.4"
   },
   "dependencies": {
-    "aws-sdk": "^2.1058.0",
+    "aws-sdk": "^2.1060.0",
     "chalk": "^4.1.2"
   },
   "peerDependencies": {
diff --git a/src/domain-config.ts b/src/domain-config.ts
index f72666e..454a234 100644
--- a/src/domain-config.ts
+++ b/src/domain-config.ts
@@ -17,6 +17,8 @@ class DomainConfig {
     public certificateName: string | undefined;
     public certificateArn: string | undefined;
     public createRoute53Record: boolean | undefined;
+    public route53Profile: string | undefined;
+    public route53Region: string | undefined;
     public endpointType: string | undefined;
     public apiType: string | undefined;
     public hostedZoneId: string | undefined;
@@ -41,6 +43,8 @@ class DomainConfig {
         this.certificateArn = config.certificateArn;
         this.certificateName = config.certificateName;
         this.createRoute53Record = config.createRoute53Record;
+        this.route53Profile = config.route53Profile;
+        this.route53Region = config.route53Region;
         this.hostedZoneId = config.hostedZoneId;
         this.hostedZonePrivate = config.hostedZonePrivate;
         this.allowPathMatching = config.allowPathMatching;
diff --git a/src/index.ts b/src/index.ts
index 09f86f4..cd53f50 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -13,7 +13,6 @@ class ServerlessCustomDomain {
 
     // AWS SDK resources
     public apiGatewayWrapper: APIGatewayWrapper;
-    public route53: any;
     public acm: any;
     public cloudFormationWrapper: CloudFormationWrapper;
 
@@ -169,10 +168,29 @@ class ServerlessCustomDomain {
         credentials.region = this.serverless.providers.aws.getRegion();
 
         this.apiGatewayWrapper = new APIGatewayWrapper(credentials);
-        this.route53 = new this.serverless.providers.aws.sdk.Route53(credentials);
         this.cloudFormationWrapper = new CloudFormationWrapper(credentials);
     }
 
+    /**
+     * Setup route53 resource
+     */
+    public createRoute53Resource(domain: DomainConfig): any {
+        let route53Credentials = this.serverless.providers.aws.getCredentials();
+        let route53Region = this.serverless.providers.aws.getRegion();
+
+        if (domain.route53Profile) {
+            route53Credentials = new this.serverless.providers.aws.sdk.SharedIniFileCredentials({
+                profile: domain.route53Profile,
+            });
+            route53Region = domain.route53Region || route53Region;
+        }
+
+        return new this.serverless.providers.aws.sdk.Route53({
+            credentials: route53Credentials,
+            region: route53Region,
+        });
+    }
+
     /**
      * Lifecycle function to create a domain
      * Wraps creating a domain and resource record set
@@ -482,7 +500,7 @@ class ServerlessCustomDomain {
         };
         // Make API call
         try {
-            await throttledCall(this.route53, "changeResourceRecordSets", params);
+            await throttledCall(this.createRoute53Resource(domain), "changeResourceRecordSets", params);
         } catch (err) {
             Globals.logError(err, domain.givenDomainName);
             throw new Error(`Failed to ${action} A Alias for ${domain.givenDomainName}\n`);
@@ -509,7 +527,7 @@ class ServerlessCustomDomain {
         const givenDomainNameReverse = domain.givenDomainName.split(".").reverse();
 
         try {
-            hostedZoneData = await throttledCall(this.route53, "listHostedZones", {});
+            hostedZoneData = await throttledCall(this.createRoute53Resource(domain), "listHostedZones", {});
             const targetHostedZone = hostedZoneData.HostedZones
                 .filter((hostedZone) => {
                     let hostedZoneName;
diff --git a/src/types.ts b/src/types.ts
index c53a782..fd60e40 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -5,6 +5,8 @@ export interface CustomDomain { // tslint:disable-line
     certificateName: string | undefined;
     certificateArn: string | undefined;
     createRoute53Record: boolean | undefined;
+    route53Profile: string | undefined;
+    route53Region: string | undefined;
     endpointType: string | undefined;
     apiType: string | undefined;
     hostedZoneId: string | undefined;
@@ -47,6 +49,7 @@ export interface ServerlessInstance { // tslint:disable-line
                 config: {
                     update(toUpdate: object): void,
                 },
+                SharedIniFileCredentials: any,
             }
             getCredentials(),
             getRegion(),


Test patch:
diff --git a/test/integration-tests/basic.test.ts b/test/integration-tests/basic.test.ts
index 4504c3f..7fe8fdf 100644
--- a/test/integration-tests/basic.test.ts
+++ b/test/integration-tests/basic.test.ts
@@ -98,6 +98,21 @@ describe("Integration Tests", function () {
         }
     });
 
+    it("Can use a specified profile for route53", async () => {
+        const testName = "route53-profile";
+        const configFolder = `${CONFIGS_FOLDER}/${testName}`;
+        const testURL = `${PLUGIN_IDENTIFIER}-${testName}-${RANDOM_STRING}.${TEST_DOMAIN}`;
+        try {
+            await utilities.createTempDir(TEMP_DIR, configFolder);
+            await utilities.slsCreateDomain(TEMP_DIR);
+            await utilities.slsCreateDomain(TEMP_DIR);
+            await utilities.slsCreateDomain(TEMP_DIR);
+            await utilities.slsDeploy(TEMP_DIR);
+        } finally {
+            await utilities.destroyResources(testURL);
+        }
+    });
+
     it("Creates a domain multiple times without failure", async () => {
         const testName = "create-domain-idempotent";
         const configFolder = `${CONFIGS_FOLDER}/${testName}`;
diff --git a/test/integration-tests/basic/route53-profile/handler.js b/test/integration-tests/basic/route53-profile/handler.js
new file mode 100644
index 0000000..8076060
--- /dev/null
+++ b/test/integration-tests/basic/route53-profile/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! Your function executed successfully!",
+      input: event,
+    }),
+  };
+
+  callback(null, response);
+};
diff --git a/test/integration-tests/basic/route53-profile/serverless.yml b/test/integration-tests/basic/route53-profile/serverless.yml
new file mode 100644
index 0000000..4324507
--- /dev/null
+++ b/test/integration-tests/basic/route53-profile/serverless.yml
@@ -0,0 +1,27 @@
+# Creating a domain should be idempotent
+service: ${env:PLUGIN_IDENTIFIER}-route53-profile-${env:RANDOM_STRING}
+provider:
+  lambdaHashingVersion: 20201221
+  name: aws
+  runtime: nodejs12.x
+  region: us-west-2
+  stage: test
+functions:
+  helloWorld:
+    handler: handler.helloWorld
+    events:
+      - http:
+          path: hello-world
+          method: get
+          cors: true
+plugins:
+  - serverless-domain-manager
+
+custom:
+  customDomain:
+    domainName: ${env:PLUGIN_IDENTIFIER}-route53-profile-${env:RANDOM_STRING}.${env:TEST_DOMAIN}
+    route53Profile: ${env:ROUTE53_PROFILE}
+
+package:
+  exclude:
+    - node_modules/**
diff --git a/test/unit-tests/index.test.ts b/test/unit-tests/index.test.ts
index 2442d09..1b47147 100644
--- a/test/unit-tests/index.test.ts
+++ b/test/unit-tests/index.test.ts
@@ -53,6 +53,8 @@ const constructPlugin = (customDomainOptions, multiple: boolean = false) => {
         endpointType: customDomainOptions.endpointType,
         hostedZoneId: customDomainOptions.hostedZoneId,
         hostedZonePrivate: customDomainOptions.hostedZonePrivate,
+        route53Profile: customDomainOptions.route53Profile,
+        route53Region: customDomainOptions.route53Region,
         securityPolicy: customDomainOptions.securityPolicy,
         stage: customDomainOptions.stage,
         route53Params: customDomainOptions.route53Params
@@ -77,6 +79,7 @@ const constructPlugin = (customDomainOptions, multiple: boolean = false) => {
                     ApiGatewayV2: aws.ApiGatewayV2,
                     CloudFormation: aws.CloudFormation,
                     Route53: aws.Route53,
+                    SharedIniFileCredentials: aws.SharedIniFileCredentials,
                     config: {
                         update: (toUpdate: object) => null,
                     },
@@ -123,6 +126,23 @@ describe("Custom Domain Plugin", () => {
         expect(returnedCreds.sessionToken).to.equal(testCreds.sessionToken);
     });
 
+    describe("custom route53 profile", () => {
+      it("uses the provided profile for route53", () => {
+        const route53ProfileConfig = {
+          route53Profile: "testroute53profile",
+          route53Region: "area-53-zone",
+        };
+        const plugin = constructPlugin(route53ProfileConfig);
+
+        plugin.initAWSResources();
+        const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain);
+        const route53 = plugin.createRoute53Resource(dc);
+
+        expect(route53.config.credentials.profile).to.equal(route53ProfileConfig.route53Profile);
+        expect(route53.config.region).to.equal(route53ProfileConfig.route53Region);
+      });
+    });
+
     describe("Domain Endpoint types", () => {
         it("Unsupported endpoint types throw exception", () => {
             const plugin = constructPlugin({endpointType: "notSupported"});
@@ -360,6 +380,7 @@ describe("Custom Domain Plugin", () => {
 
         it("Remove basepath mappings", async () => {
             AWS.mock("CloudFormation", "describeStackResource", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     StackResourceDetail: {
                         LogicalResourceId: "ApiGatewayRestApi",
@@ -368,6 +389,7 @@ describe("Custom Domain Plugin", () => {
                 });
             });
             AWS.mock("ApiGatewayV2", "getApiMappings", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     Items: [
                         {ApiId: "test_rest_api_id", MappingKey: "test", ApiMappingId: "test_mapping_id", Stage: "test"},
@@ -620,6 +642,7 @@ describe("Custom Domain Plugin", () => {
 
         it("Create a new A Alias Record", async () => {
             AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     HostedZones: [{
                         Config: {PrivateZone: false},
@@ -630,11 +653,13 @@ describe("Custom Domain Plugin", () => {
             });
 
             AWS.mock("Route53", "changeResourceRecordSets", (params, callback) => {
+                // @ts-ignore
                 callback(null, params);
             });
 
             const plugin = constructPlugin({basePath: "test_basepath", domainName: "test_domain"});
-            plugin.route53 = new aws.Route53();
+            const route53 = new aws.Route53();
+            plugin.createRoute53Resource = () => route53;
 
             const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain);
 
@@ -645,7 +670,7 @@ describe("Custom Domain Plugin", () => {
                 },
             );
 
-            const spy = chai.spy.on(plugin.route53, "changeResourceRecordSets");
+            const spy = chai.spy.on(route53, "changeResourceRecordSets");
 
             await plugin.changeResourceRecordSet("UPSERT", dc);
 
@@ -705,6 +730,7 @@ describe("Custom Domain Plugin", () => {
     describe("Gets existing basepath mappings correctly", () => {
         it("Returns undefined if no basepaths map to current api", async () => {
             AWS.mock("ApiGatewayV2", "getApiMappings", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     Items: [
                         {
@@ -770,6 +796,7 @@ describe("Custom Domain Plugin", () => {
     describe("Gets Rest API id correctly", () => {
         it("Fetches REST API id correctly when no ApiGateway specified", async () => {
             AWS.mock("CloudFormation", "describeStacks", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     Stacks: [
                         {
@@ -791,6 +818,7 @@ describe("Custom Domain Plugin", () => {
                 if (params.StackName === "custom-stage-name") {
                     throw new Error("error");
                 }
+                // @ts-ignore
                 callback(null, {
                     StackResourceDetail: {
                         LogicalResourceId: "ApiGatewayRestApi",
@@ -821,6 +849,7 @@ describe("Custom Domain Plugin", () => {
 
         it("Gets HTTP API id correctly when no ApiGateway specified", async () => {
             AWS.mock("CloudFormation", "describeStacks", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     Stacks: [
                         {
@@ -842,6 +871,7 @@ describe("Custom Domain Plugin", () => {
                 if (params.StackName === "custom-stage-name") {
                     throw new Error("error");
                 }
+                // @ts-ignore
                 callback(null, {
                     StackResourceDetail:
                         {
@@ -874,6 +904,7 @@ describe("Custom Domain Plugin", () => {
 
         it("Gets Websocket API id correctly when no ApiGateway specified", async () => {
             AWS.mock("CloudFormation", "describeStacks", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     Stacks: [
                         {
@@ -895,6 +926,7 @@ describe("Custom Domain Plugin", () => {
                 if (skipNames.indexOf(params.StackName) !== -1) {
                     throw new Error("error");
                 }
+                // @ts-ignore
                 callback(null, {
                     StackResourceDetail:
                         {
@@ -927,6 +959,7 @@ describe("Custom Domain Plugin", () => {
 
         it("serverless.yml defines explicitly the apiGateway", async () => {
             AWS.mock("CloudFormation", "describeStackResource", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     StackResourceDetail:
                         {
@@ -975,6 +1008,7 @@ describe("Custom Domain Plugin", () => {
 
         it("Delete A Alias Record", async () => {
             AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     HostedZones: [{
                         Config: {PrivateZone: false},
@@ -985,6 +1019,7 @@ describe("Custom Domain Plugin", () => {
             });
 
             AWS.mock("Route53", "changeResourceRecordSets", (params, callback) => {
+                // @ts-ignore
                 callback(null, params);
             });
 
@@ -992,11 +1027,12 @@ describe("Custom Domain Plugin", () => {
                 basePath: "test_basepath",
                 domainName: "test_domain",
             });
-            plugin.route53 = new aws.Route53();
+            const route53 = new aws.Route53();
+            plugin.createRoute53Resource = () => route53;
 
             const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain);
 
-            const spy = chai.spy.on(plugin.route53, "changeResourceRecordSets");
+            const spy = chai.spy.on(route53, "changeResourceRecordSets");
 
             dc.domainInfo = new DomainInfo({
                 distributionDomainName: "test_distribution_name",
@@ -1082,6 +1118,7 @@ describe("Custom Domain Plugin", () => {
                 callback(null, params);
             });
             AWS.mock("CloudFormation", "describeStacks", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     Stacks: [
                         {
@@ -1100,6 +1137,7 @@ describe("Custom Domain Plugin", () => {
                 });
             });
             AWS.mock("CloudFormation", "describeStackResource", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     StackResourceDetail:
                         {
@@ -1127,16 +1165,18 @@ describe("Custom Domain Plugin", () => {
                 callback(null, {});
             });
             AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                // @ts-ignore
                 callback(null, {HostedZones: [{Name: "test_domain", Id: "test_id", Config: {PrivateZone: false}}]});
             });
             AWS.mock("Route53", "changeResourceRecordSets", (params, callback) => {
+                // @ts-ignore
                 callback(null, params);
             });
 
             const plugin = constructPlugin({domainName: "test_domain"});
             plugin.initializeVariables();
             plugin.initAWSResources();
-            plugin.route53 = new aws.Route53();
+            plugin.createRoute53Resource = () => new aws.Route53();
 
             await plugin.deleteDomains();
             expect(consoleOutput[0]).to.equal(`Custom domain ${plugin.domains[0].givenDomainName} was deleted.`);
@@ -1145,23 +1185,26 @@ describe("Custom Domain Plugin", () => {
         it("createDomain if one does not exist before", async () => {
             AWS.mock("ACM", "listCertificates", certTestData);
             AWS.mock("ApiGatewayV2", "getDomainName", (params, callback) => {
+                // @ts-ignore
                 callback({code: "NotFoundException"}, {});
             });
             AWS.mock("APIGateway", "createDomainName", (params, callback) => {
                 callback(null, {distributionDomainName: "foo", regionalHostedZoneId: "test_id"});
             });
             AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     HostedZones: [{Name: "test_domain", Id: "test_id", Config: {PrivateZone: false}}],
                 });
             });
             AWS.mock("Route53", "changeResourceRecordSets", (params, callback) => {
+                // @ts-ignore
                 callback(null, params);
             });
 
             const plugin = constructPlugin({domainName: "test_domain"});
             plugin.initializeVariables();
-            plugin.route53 = new aws.Route53();
+            plugin.createRoute53Resource = () => new aws.Route53();
             plugin.domains.forEach((d) => {
                 d.acm = new aws.ACM();
             });
@@ -1185,16 +1228,18 @@ describe("Custom Domain Plugin", () => {
                 callback(null, {distributionDomainName: "foo", regionalHostedZoneId: "test_id"});
             });
             AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                // @ts-ignore
                 callback(null, {HostedZones: [{Name: "test_domain", Id: "test_id", Config: {PrivateZone: false}}]});
             });
             AWS.mock("Route53", "changeResourceRecordSets", (params, callback) => {
+                // @ts-ignore
                 callback(null, params);
             });
 
             const plugin = constructPlugin({domainName: "test_domain"});
             plugin.initializeVariables();
             plugin.initAWSResources();
-            plugin.route53 = new aws.Route53();
+            plugin.createRoute53Resource = () => new aws.Route53();
             plugin.initializeVariables();
             plugin.domains.forEach((d) => {
                 d.acm = new aws.ACM();
@@ -1213,6 +1258,7 @@ describe("Custom Domain Plugin", () => {
     describe("Select Hosted Zone", () => {
         it("Natural order", async () => {
             AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     HostedZones: [
                         {Name: "aaa.com.", Id: "/hostedzone/test_id_0", Config: {PrivateZone: false}},
@@ -1224,7 +1270,7 @@ describe("Custom Domain Plugin", () => {
             });
 
             const plugin = constructPlugin({domainName: "ccc.bbb.aaa.com"});
-            plugin.route53 = new aws.Route53();
+            plugin.createRoute53Resource = () => new aws.Route53();
             plugin.initializeVariables();
             const result = await plugin.getRoute53HostedZoneId(plugin.domains[0]);
             expect(result).to.equal("test_id_2");
@@ -1232,6 +1278,7 @@ describe("Custom Domain Plugin", () => {
 
         it("Reverse order", async () => {
             AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     HostedZones: [
                         {Name: "ddd.ccc.bbb.aaa.com.", Id: "/hostedzone/test_id_0", Config: {PrivateZone: false}},
@@ -1243,7 +1290,7 @@ describe("Custom Domain Plugin", () => {
             });
 
             const plugin = constructPlugin({domainName: "test.ccc.bbb.aaa.com"});
-            plugin.route53 = new aws.Route53();
+            plugin.createRoute53Resource = () => new aws.Route53();
             plugin.initializeVariables();
 
             const result = await plugin.getRoute53HostedZoneId(plugin.domains[0]);
@@ -1252,6 +1299,7 @@ describe("Custom Domain Plugin", () => {
 
         it("Random order", async () => {
             AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     HostedZones: [
                         {Name: "bbb.aaa.com.", Id: "/hostedzone/test_id_0", Config: {PrivateZone: false}},
@@ -1263,7 +1311,7 @@ describe("Custom Domain Plugin", () => {
             });
 
             const plugin = constructPlugin({domainName: "test.ccc.bbb.aaa.com"});
-            plugin.route53 = new aws.Route53();
+            plugin.createRoute53Resource = () => new aws.Route53();
             plugin.initializeVariables();
 
             const result = await plugin.getRoute53HostedZoneId(plugin.domains[0]);
@@ -1272,6 +1320,7 @@ describe("Custom Domain Plugin", () => {
 
         it("Sub domain name - only root hosted zones", async () => {
             AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     HostedZones: [
                         {Name: "aaa.com.", Id: "/hostedzone/test_id_0", Config: {PrivateZone: false}},
@@ -1282,7 +1331,7 @@ describe("Custom Domain Plugin", () => {
             });
 
             const plugin = constructPlugin({domainName: "bar.foo.bbb.fr"});
-            plugin.route53 = new aws.Route53();
+            plugin.createRoute53Resource = () => new aws.Route53();
             plugin.initializeVariables();
 
             const result = await plugin.getRoute53HostedZoneId(plugin.domains[0]);
@@ -1291,6 +1340,7 @@ describe("Custom Domain Plugin", () => {
 
         it("With matching root and sub hosted zone", async () => {
             AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     HostedZones: [
                         {Name: "a.aaa.com.", Id: "/hostedzone/test_id_0", Config: {PrivateZone: false}},
@@ -1300,7 +1350,7 @@ describe("Custom Domain Plugin", () => {
             });
 
             const plugin = constructPlugin({domainName: "test.a.aaa.com"});
-            plugin.route53 = new aws.Route53();
+            plugin.createRoute53Resource = () => new aws.Route53();
             plugin.initializeVariables();
 
             const result = await plugin.getRoute53HostedZoneId(plugin.domains[0]);
@@ -1309,6 +1359,7 @@ describe("Custom Domain Plugin", () => {
 
         it("Sub domain name - natural order", async () => {
             AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     HostedZones: [
                         {Name: "aaa.com.", Id: "/hostedzone/test_id_0", Config: {PrivateZone: false}},
@@ -1320,7 +1371,7 @@ describe("Custom Domain Plugin", () => {
             });
 
             const plugin = constructPlugin({domainName: "bar.foo.bbb.fr"});
-            plugin.route53 = new aws.Route53();
+            plugin.createRoute53Resource = () => new aws.Route53();
             plugin.initializeVariables();
 
             const result = await plugin.getRoute53HostedZoneId(plugin.domains[0]);
@@ -1329,6 +1380,7 @@ describe("Custom Domain Plugin", () => {
 
         it("Sub domain name - reverse order", async () => {
             AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     HostedZones: [
                         {Name: "foo.bbb.fr.", Id: "/hostedzone/test_id_3", Config: {PrivateZone: false}},
@@ -1340,7 +1392,7 @@ describe("Custom Domain Plugin", () => {
             });
 
             const plugin = constructPlugin({domainName: "bar.foo.bbb.fr"});
-            plugin.route53 = new aws.Route53();
+            plugin.createRoute53Resource = () => new aws.Route53();
             plugin.initializeVariables();
 
             const result = await plugin.getRoute53HostedZoneId(plugin.domains[0]);
@@ -1349,6 +1401,7 @@ describe("Custom Domain Plugin", () => {
 
         it("Sub domain name - random order", async () => {
             AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     HostedZones: [
                         {Name: "bbb.fr.", Id: "/hostedzone/test_id_1", Config: {PrivateZone: false}},
@@ -1359,7 +1412,7 @@ describe("Custom Domain Plugin", () => {
             });
 
             const plugin = constructPlugin({domainName: "bar.foo.bbb.fr"});
-            plugin.route53 = new aws.Route53();
+            plugin.createRoute53Resource = () => new aws.Route53();
             plugin.initializeVariables();
 
             const result = await plugin.getRoute53HostedZoneId(plugin.domains[0]);
@@ -1368,6 +1421,7 @@ describe("Custom Domain Plugin", () => {
 
         it("Private zone domain name", async () => {
             AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     HostedZones: [
                         {Name: "aaa.com.", Id: "/hostedzone/test_id_1", Config: {PrivateZone: false}},
@@ -1376,7 +1430,7 @@ describe("Custom Domain Plugin", () => {
             });
 
             const plugin = constructPlugin({domainName: "aaa.com", hostedZonePrivate: true});
-            plugin.route53 = new aws.Route53();
+            plugin.createRoute53Resource = () => new aws.Route53();
             plugin.initializeVariables();
 
             const result = await plugin.getRoute53HostedZoneId(plugin.domains[0]);
@@ -1385,6 +1439,7 @@ describe("Custom Domain Plugin", () => {
 
         it("Undefined hostedZonePrivate should still allow private domains", async () => {
             AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     HostedZones: [
                         {Name: "aaa.com.", Id: "/hostedzone/test_id_0", Config: {PrivateZone: true}},
@@ -1393,7 +1448,7 @@ describe("Custom Domain Plugin", () => {
             });
 
             const plugin = constructPlugin({domainName: "aaa.com"});
-            plugin.route53 = new aws.Route53();
+            plugin.createRoute53Resource = () => new aws.Route53();
             plugin.initializeVariables();
 
             const result = await plugin.getRoute53HostedZoneId(plugin.domains[0]);
@@ -1430,12 +1485,13 @@ describe("Custom Domain Plugin", () => {
 
         it("Fail getHostedZone", async () => {
             AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                // @ts-ignore
                 callback(null, {HostedZones: [{Name: "no_hosted_zone", Id: "test_id"}]});
             });
 
             const plugin = constructPlugin({domainName: "test_domain"});
             plugin.initializeVariables();
-            plugin.route53 = new aws.Route53();
+            plugin.createRoute53Resource = () => new aws.Route53();
             plugin.initializeVariables();
 
             return plugin.getRoute53HostedZoneId(plugin.domains[0]).then(() => {
@@ -1490,6 +1546,7 @@ describe("Custom Domain Plugin", () => {
     describe("Summary Printing", () => {
         it("Prints Summary", async () => {
             AWS.mock("ApiGatewayV2", "getDomainName", (params, callback) => {
+                // @ts-ignore
                 callback(null, {domainName: params, distributionDomainName: "test_distributed_domain_name"});
             });
             const plugin = constructPlugin({domainName: "test_domain"});
@@ -1704,6 +1761,7 @@ describe("Custom Domain Plugin", () => {
                     // this call should never happen since its after the last request that included a token
                 }];
             AWS.mock("ApiGatewayV2", "getApiMappings", (params, callback) => {
+                // @ts-ignore
                 callback(null, responses[callCount++]);
             });
 
@@ -1781,6 +1839,7 @@ describe("Custom Domain Plugin", () => {
 
         it("removeBasePathMapping should call deleteDomain when autoDomain is true", async () => {
             AWS.mock("CloudFormation", "describeStackResource", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     StackResourceDetail:
                         {
@@ -1790,6 +1849,7 @@ describe("Custom Domain Plugin", () => {
                 });
             });
             AWS.mock("ApiGatewayV2", "getApiMappings", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     Items: [
                         {ApiId: "test_rest_api_id", MappingKey: "test", ApiMappingId: "test_mapping_id", Stage: "test"},
@@ -1828,6 +1888,7 @@ describe("Custom Domain Plugin", () => {
 
         it("removeBasePathMapping should not call deleteDomain when autoDomain is not true", async () => {
             AWS.mock("CloudFormation", "describeStackResource", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     StackResourceDetail:
                         {
@@ -1837,6 +1898,7 @@ describe("Custom Domain Plugin", () => {
                 });
             });
             AWS.mock("ApiGatewayV2", "getApiMappings", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     Items: [
                         {ApiId: "test_rest_api_id", MappingKey: "test", ApiMappingId: "test_mapping_id", Stage: "test"},
@@ -1881,6 +1943,7 @@ describe("Custom Domain Plugin", () => {
     describe("Route53 Routing Policies", () => {
         it("Should create a new Alias Record with latency routing", async () => {
             AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     HostedZones: [{
                         Config: {PrivateZone: false},
@@ -1891,6 +1954,7 @@ describe("Custom Domain Plugin", () => {
             });
 
             AWS.mock("Route53", "changeResourceRecordSets", (params, callback) => {
+                // @ts-ignore
                 callback(null, params);
             });
 
@@ -1902,7 +1966,8 @@ describe("Custom Domain Plugin", () => {
                     routingPolicy: 'latency'
                 }
             });
-            plugin.route53 = new aws.Route53();
+            const route53 = new aws.Route53();
+            plugin.createRoute53Resource = () => route53;
 
             const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain);
 
@@ -1913,7 +1978,7 @@ describe("Custom Domain Plugin", () => {
                 },
             );
 
-            const spy = chai.spy.on(plugin.route53, "changeResourceRecordSets");
+            const spy = chai.spy.on(route53, "changeResourceRecordSets");
 
             await plugin.changeResourceRecordSet("UPSERT", dc);
 
@@ -1958,6 +2023,7 @@ describe("Custom Domain Plugin", () => {
 
         it("Should create a new Alias Record with weighted routing", async () => {
             AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     HostedZones: [{
                         Config: {PrivateZone: false},
@@ -1968,6 +2034,7 @@ describe("Custom Domain Plugin", () => {
             });
 
             AWS.mock("Route53", "changeResourceRecordSets", (params, callback) => {
+                // @ts-ignore
                 callback(null, params);
             });
 
@@ -1981,7 +2048,8 @@ describe("Custom Domain Plugin", () => {
                     healthCheckId: "test_healthcheck",
                 }
             });
-            plugin.route53 = new aws.Route53();
+            const route53 = new aws.Route53();
+            plugin.createRoute53Resource = () => route53;
 
             const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain);
 
@@ -1992,7 +2060,7 @@ describe("Custom Domain Plugin", () => {
                 },
             );
 
-            const spy = chai.spy.on(plugin.route53, "changeResourceRecordSets");
+            const spy = chai.spy.on(route53, "changeResourceRecordSets");
 
             await plugin.changeResourceRecordSet("UPSERT", dc);
 
@@ -2039,6 +2107,7 @@ describe("Custom Domain Plugin", () => {
 
         it("Should exclude weight input with latency routing", async () => {
             AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     HostedZones: [{
                         Config: {PrivateZone: false},
@@ -2049,6 +2118,7 @@ describe("Custom Domain Plugin", () => {
             });
 
             AWS.mock("Route53", "changeResourceRecordSets", (params, callback) => {
+                // @ts-ignore
                 callback(null, params);
             });
 
@@ -2061,7 +2131,8 @@ describe("Custom Domain Plugin", () => {
                     weight: 100,
                 }
             });
-            plugin.route53 = new aws.Route53();
+            const route53 = new aws.Route53();
+            plugin.createRoute53Resource = () => route53;
 
             const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain);
 
@@ -2072,7 +2143,7 @@ describe("Custom Domain Plugin", () => {
                 },
             );
 
-            const spy = chai.spy.on(plugin.route53, "changeResourceRecordSets");
+            const spy = chai.spy.on(route53, "changeResourceRecordSets");
 
             await plugin.changeResourceRecordSet("UPSERT", dc);
 
@@ -2117,6 +2188,7 @@ describe("Custom Domain Plugin", () => {
 
         it("Should exclude weight, region, set identifier, and health input with simple routing", async () => {
             AWS.mock("Route53", "listHostedZones", (params, callback) => {
+                // @ts-ignore
                 callback(null, {
                     HostedZones: [{
                         Config: {PrivateZone: false},
@@ -2127,6 +2199,7 @@ describe("Custom Domain Plugin", () => {
             });
 
             AWS.mock("Route53", "changeResourceRecordSets", (params, callback) => {
+                // @ts-ignore
                 callback(null, params);
             });
 
@@ -2139,7 +2212,8 @@ describe("Custom Domain Plugin", () => {
                     weight: 100,
                 }
             });
-            plugin.route53 = new aws.Route53();
+            const route53 = new aws.Route53();
+            plugin.createRoute53Resource = () => route53;
 
             const dc: DomainConfig = new DomainConfig(plugin.serverless.service.custom.customDomain);
 
@@ -2150,7 +2224,7 @@ describe("Custom Domain Plugin", () => {
                 },
             );
 
-            const spy = chai.spy.on(plugin.route53, "changeResourceRecordSets");
+            const spy = chai.spy.on(route53, "changeResourceRecordSets");
 
             await plugin.changeResourceRecordSet("UPSERT", dc);