Repository: serverless/serverless-azure-functions
Pull request #221 (2019-07-31 17:51:38)

feat: Added Azure Naming Service
- [x] Add `AzureNamingService`
- [x] Move naming utils to naming service
- [x] Added `getSafeResourceName` - used by `storageAccount` and `deploymentName`

Resolves [AB#597]

coveralls: 
[![Coverage Status](https://coveralls.io/builds/24908412/badge)](https://coveralls.io/builds/24908412)

Coverage increased (+0.09%) to 84.302% when pulling **e4250cf929048ed3bba680528725f88f47d8e0f7 on tabarlow/new-naming-service** into **1df392c52dc269f16561793e76663aa2771934a8 on dev**.

pjlittle: LGTM - thanks for centralizing this logic and updating across the board :+1: 

Patch:
diff --git a/src/armTemplates/compositeArmTemplate.ts b/src/armTemplates/compositeArmTemplate.ts
index 73d834b..299adbd 100644
--- a/src/armTemplates/compositeArmTemplate.ts
+++ b/src/armTemplates/compositeArmTemplate.ts
@@ -1,10 +1,7 @@
-import {
-  ArmResourceTemplateGenerator,
-  ArmResourceTemplate
-} from "../models/armTemplates";
-import { Guard } from "../shared/guard";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../models/armTemplates";
 import { ServerlessAzureConfig } from "../models/serverless";
-import { Utils } from "../shared/utils";
+import { AzureNamingService } from "../services/namingService";
+import { Guard } from "../shared/guard";
 
 export class CompositeArmTemplate implements ArmResourceTemplateGenerator {
   public constructor(private childTemplates: ArmResourceTemplateGenerator[]) {
@@ -43,7 +40,7 @@ export class CompositeArmTemplate implements ArmResourceTemplateGenerator {
       parameters = {
         ...parameters,
         ...resource.getParameters(config),
-        location: Utils.getNormalizedRegionName(config.provider.region)
+        location: AzureNamingService.getNormalizedRegionName(config.provider.region)
       };
     });
 
diff --git a/src/armTemplates/resources/apim.ts b/src/armTemplates/resources/apim.ts
index eaca327..5003361 100644
--- a/src/armTemplates/resources/apim.ts
+++ b/src/armTemplates/resources/apim.ts
@@ -1,13 +1,15 @@
-import { ServerlessAzureConfig } from "../../models/serverless";
-import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
 import { ApiManagementConfig } from "../../models/apiManagement";
-import { Utils } from "../../shared/utils";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../../models/armTemplates";
+import { ServerlessAzureConfig } from "../../models/serverless";
+import { AzureNamingService } from "../../services/namingService";
 
 export class ApimResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
-    return config.provider.apim && config.provider.apim.name
-      ? config.provider.apim.name
-      : `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-apim`;
+    return AzureNamingService.getResourceName(
+      config,
+      config.provider.apim,
+      "apim"
+    );
   }
 
   public getTemplate(): ArmResourceTemplate {
@@ -77,4 +79,4 @@ export class ApimResource implements ArmResourceTemplateGenerator {
       apimPublisherName: apimConfig.publisherName,
     };
   }
-}
\ No newline at end of file
+}
diff --git a/src/armTemplates/resources/appInsights.ts b/src/armTemplates/resources/appInsights.ts
index 8667d92..9426a0b 100644
--- a/src/armTemplates/resources/appInsights.ts
+++ b/src/armTemplates/resources/appInsights.ts
@@ -1,12 +1,14 @@
-import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../../models/armTemplates";
 import { ServerlessAzureConfig } from "../../models/serverless";
-import { Utils } from "../../shared/utils";
+import { AzureNamingService } from "../../services/namingService";
 
 export class AppInsightsResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
-    return config.provider.appInsights && config.provider.appInsights.name
-      ? config.provider.appInsights.name
-      : `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-appinsights`;
+    return AzureNamingService.getResourceName(
+      config,
+      config.provider.appInsights,
+      "appinsights"
+    );
   }
 
   public getTemplate(): ArmResourceTemplate {
@@ -45,4 +47,4 @@ export class AppInsightsResource implements ArmResourceTemplateGenerator {
       appInsightsName: AppInsightsResource.getResourceName(config),
     };
   }
-}
\ No newline at end of file
+}
diff --git a/src/armTemplates/resources/appServicePlan.ts b/src/armTemplates/resources/appServicePlan.ts
index 9581996..4485cb0 100644
--- a/src/armTemplates/resources/appServicePlan.ts
+++ b/src/armTemplates/resources/appServicePlan.ts
@@ -1,12 +1,14 @@
-import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
-import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless";
-import { Utils } from "../../shared/utils";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../../models/armTemplates";
+import { ResourceConfig, ServerlessAzureConfig } from "../../models/serverless";
+import { AzureNamingService } from "../../services/namingService";
 
 export class AppServicePlanResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
-    return config.provider.appServicePlan && config.provider.appServicePlan.name
-      ? config.provider.appServicePlan.name
-      : `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-asp`;
+    return AzureNamingService.getResourceName(
+      config,
+      config.provider.appInsights,
+      "asp"
+    );
   }
 
   public getTemplate(): ArmResourceTemplate {
@@ -66,4 +68,4 @@ export class AppServicePlanResource implements ArmResourceTemplateGenerator {
       appServicePlanSkuTier: resourceConfig.sku.tier,
     }
   }
-}
\ No newline at end of file
+}
diff --git a/src/armTemplates/resources/functionApp.ts b/src/armTemplates/resources/functionApp.ts
index 48c7f5c..1af37a3 100644
--- a/src/armTemplates/resources/functionApp.ts
+++ b/src/armTemplates/resources/functionApp.ts
@@ -1,14 +1,15 @@
-import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
-import { ServerlessAzureConfig, FunctionAppConfig } from "../../models/serverless";
-import { Utils } from "../../shared/utils";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../../models/armTemplates";
+import { FunctionAppConfig, ServerlessAzureConfig } from "../../models/serverless";
+import { AzureNamingService } from "../../services/namingService";
 
 export class FunctionAppResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
     const safeServiceName = config.service.replace(/\s/g, "-");
-
-    return config.provider.functionApp && config.provider.functionApp.name
-      ? config.provider.functionApp.name
-      : `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-${safeServiceName}`;
+    return AzureNamingService.getResourceName(
+      config,
+      config.provider.appInsights,
+      safeServiceName
+    );
   }
 
   public getTemplate(): ArmResourceTemplate {
diff --git a/src/armTemplates/resources/hostingEnvironment.ts b/src/armTemplates/resources/hostingEnvironment.ts
index 233aff0..7c8332b 100644
--- a/src/armTemplates/resources/hostingEnvironment.ts
+++ b/src/armTemplates/resources/hostingEnvironment.ts
@@ -1,12 +1,14 @@
-import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../../models/armTemplates";
 import { ServerlessAzureConfig } from "../../models/serverless";
-import { Utils } from "../../shared/utils";
+import { AzureNamingService } from "../../services/namingService";
 
 export class HostingEnvironmentResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
-    return config.provider.hostingEnvironment && config.provider.hostingEnvironment.name
-      ? config.provider.hostingEnvironment.name
-      : `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-ase`;
+    return AzureNamingService.getResourceName(
+      config,
+      config.provider.hostingEnvironment,
+      "ase"
+    );
   }
 
   public getTemplate(): ArmResourceTemplate {
diff --git a/src/armTemplates/resources/storageAccount.ts b/src/armTemplates/resources/storageAccount.ts
index 0b6c9d8..69dc7cd 100644
--- a/src/armTemplates/resources/storageAccount.ts
+++ b/src/armTemplates/resources/storageAccount.ts
@@ -1,13 +1,15 @@
-import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
-import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless";
-import { Utils } from "../../shared/utils";
-import md5 from "md5";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../../models/armTemplates";
+import { ResourceConfig, ServerlessAzureConfig } from "../../models/serverless";
+import { AzureNamingService } from "../../services/namingService";
+import configConstants from "../../config";
 
 export class StorageAccountResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
-    return config.provider.storageAccount && config.provider.storageAccount.name
-      ? config.provider.storageAccount.name
-      : StorageAccountResource.getDefaultStorageAccountName(config)
+    return AzureNamingService.getSafeResourceName(
+      config,
+      configConstants.naming.maxLength.storageAccount,
+      config.provider.storageAccount
+    );
   }
 
   public getTemplate(): ArmResourceTemplate {
@@ -64,34 +66,4 @@ export class StorageAccountResource implements ArmResourceTemplateGenerator {
       storageAccoutSkuTier: resourceConfig.sku.tier,
     };
   }
-
-  /**
-   * Gets a default storage account name.
-   * Storage account names can have at most 24 characters and can have only alpha-numerics
-   * @param config Serverless Azure Config
-   */
-  private static getDefaultStorageAccountName(config: ServerlessAzureConfig): string {
-    const maxAccountNameLength = 24;
-    const nameHash = md5(config.service);
-    const replacer = /\W+/g;
-
-    let safePrefix = config.provider.prefix.replace(replacer, "");
-    const safeRegion = Utils.createShortAzureRegionName(config.provider.region);
-    let safeStage = Utils.createShortStageName(config.provider.stage);
-    let safeNameHash = nameHash.substr(0, 6);
-
-    const remaining = maxAccountNameLength - (safePrefix.length + safeRegion.length + safeStage.length + safeNameHash.length);
-
-    // Dynamically adjust the substring based on space needed
-    if (remaining < 0) {
-      const partLength = Math.floor(Math.abs(remaining) / 3);
-      safePrefix = safePrefix.substr(0, partLength);
-      safeStage = safeStage.substr(0, partLength);
-      safeNameHash = safeNameHash.substr(0, partLength);
-    }
-
-    return [safePrefix, safeRegion, safeStage, safeNameHash]
-      .join("")
-      .toLocaleLowerCase();
-  }
-}
\ No newline at end of file
+}
diff --git a/src/armTemplates/resources/virtualNetwork.ts b/src/armTemplates/resources/virtualNetwork.ts
index a943893..454c500 100644
--- a/src/armTemplates/resources/virtualNetwork.ts
+++ b/src/armTemplates/resources/virtualNetwork.ts
@@ -1,12 +1,14 @@
-import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../../models/armTemplates";
 import { ServerlessAzureConfig } from "../../models/serverless";
-import { Utils } from "../../shared/utils";
+import { AzureNamingService } from "../../services/namingService";
 
 export class VirtualNetworkResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
-    return config.provider.virtualNetwork && config.provider.virtualNetwork.name
-      ? config.provider.virtualNetwork.name
-      : `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-vnet`;
+    return AzureNamingService.getResourceName(
+      config,
+      config.provider.hostingEnvironment,
+      "vnet"
+    );
   }
 
   public getTemplate(): ArmResourceTemplate {
diff --git a/src/config.ts b/src/config.ts
index 36414ee..ec67058 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -5,6 +5,16 @@ export const configConstants = {
     rollback: true,
     runFromBlobUrl: false,
   },
+  naming: {
+    maxLength: {
+      storageAccount: 24,
+      deploymentName: 64,
+    },
+    suffix: {
+      deployment: "DEPLOYMENT",
+      artifact: "ARTIFACT",
+    }
+  },
   functionAppApiPath: "/api/",
   functionAppDomain: ".azurewebsites.net",
   functionsAdminApiPath: "/admin/functions/",
diff --git a/src/services/baseService.ts b/src/services/baseService.ts
index c5715f4..cbf5acb 100644
--- a/src/services/baseService.ts
+++ b/src/services/baseService.ts
@@ -14,6 +14,7 @@ import {
 } from "../models/serverless";
 import { Guard } from "../shared/guard";
 import { Utils } from "../shared/utils";
+import { AzureNamingService } from "./namingService";
 
 export abstract class BaseService {
   protected baseUrl: string;
@@ -42,9 +43,7 @@ export abstract class BaseService {
     this.resourceGroup = this.getResourceGroupName();
     this.deploymentConfig = this.getDeploymentConfig();
     this.deploymentName = this.getDeploymentName();
-    this.storageAccountName = StorageAccountResource.getResourceName(
-      serverless.service as any
-    );
+    this.storageAccountName = StorageAccountResource.getResourceName(this.config);
 
     if (!this.credentials && authenticate) {
       throw new Error(
@@ -75,12 +74,9 @@ export abstract class BaseService {
    * Name of current resource group
    */
   public getResourceGroupName(): string {
-    const regionName = Utils.createShortAzureRegionName(this.getRegion());
-    const stageName = Utils.createShortStageName(this.getStage());
-
     return this.options.resourceGroup
       || this.config.provider.resourceGroup
-      || `${this.getPrefix()}-${regionName}-${stageName}-${this.serviceName}-rg`;
+      || AzureNamingService.getResourceName(this.config, null, `${this.serviceName}-rg`);
   }
 
   /**
@@ -96,11 +92,20 @@ export abstract class BaseService {
   }
 
   /**
-   * Name of current ARM deployment
+   * Name of current ARM deployment.
+   *
+   * Naming convention:
+   *
+   * {safeName (see naming service)}--{serviceName}(if rollback enabled: -t{timestamp})
+   *
+   * The string is guaranteed to be less than 64 characters, since that is the limit
+   * imposed by Azure deployment names. If a trim is needed, the service name will be trimmed
    */
   public getDeploymentName(): string {
-    const name = this.config.provider.deploymentName || `${this.resourceGroup}-deployment`;
-    return this.rollbackConfiguredName(name);
+    return AzureNamingService.getDeploymentName(
+      this.config,
+      (this.deploymentConfig.rollback) ? `t${this.getTimestamp()}` : null
+    )
   }
 
   /**
@@ -115,9 +120,10 @@ export abstract class BaseService {
    * Takes name of deployment and replaces `rg-deployment` or `deployment` with `artifact`
    */
   protected getArtifactName(deploymentName: string): string {
+    const { deployment, artifact } = configConstants.naming.suffix;
     return `${deploymentName
-      .replace("rg-deployment", "artifact")
-      .replace("deployment", "artifact")}.zip`;
+      .replace(`rg-${deployment}`, artifact)
+      .replace(deployment, artifact)}`
   }
 
   /**
@@ -218,16 +224,6 @@ export abstract class BaseService {
     }
   }
 
-  /**
-   * Add `-t{timestamp}` if rollback is enabled
-   * @param name Original name
-   */
-  private rollbackConfiguredName(name: string) {
-    return this.deploymentConfig.rollback
-      ? `${name}-t${this.getTimestamp()}`
-      : name;
-  }
-
   /**
    * Get timestamp from `packageTimestamp` serverless variable
    * If not set, create timestamp, set variable and return timestamp
diff --git a/src/services/namingService.ts b/src/services/namingService.ts
new file mode 100644
index 0000000..10c3755
--- /dev/null
+++ b/src/services/namingService.ts
@@ -0,0 +1,160 @@
+import { ServerlessAzureConfig, ResourceConfig } from "../models/serverless"
+import { Guard } from "../shared/guard"
+import md5 from "md5";
+import configConstants from "../config";
+
+export class AzureNamingService {
+
+  /**
+   * Get a resource name for an Azure Service. Naming convention:
+   *
+   * {prefix}-{shortRegionName}-{shortStageName}(optionally: -{suffix})
+   *
+   * @param config Serverless Azure Config for service (serverless.service)
+   * @param resourceConfig
+   * @param suffix
+   */
+  public static getResourceName(config: ServerlessAzureConfig, resourceConfig?: ResourceConfig, suffix?: string) {
+    if (resourceConfig && resourceConfig.name) {
+      return resourceConfig.name;
+    }
+    const { prefix, region, stage } = config.provider
+    let name = [
+      prefix,
+      this.createShortAzureRegionName(region),
+      this.createShortStageName(stage),
+    ].join("-");
+    if (suffix) {
+      name += `-${suffix}`;
+    }
+    return name.toLocaleLowerCase();
+  }
+
+  /**
+   * Get a name for an Azure resource that is shorter than a max length and has no forbidden characters
+   * Naming convention:
+   *
+   * {safePrefix}{safeRegion}{safeStage}{safeServiceNameHash}
+   *
+   * @param config Serverless Azure Config for service (serverless.service)
+   * @param maxLength Maximum length of name for resource
+   * @param resourceConfig Configuration for resource from serverless configuration
+   * @param forbidden Regex for characters to remove from name. Defaults to non-alpha-numerics
+   * @param replaceWith String to replace forbidden characters. Defaults to empty string
+   */
+  public static getSafeResourceName(config: ServerlessAzureConfig, maxLength: number, resourceConfig?: ResourceConfig, forbidden = /\W+/g, replaceWith = "") {
+    if (resourceConfig && resourceConfig.name) {
+      const { name } = resourceConfig;
+      if (name.length > maxLength) {
+        throw new Error(`Name '${name}' invalid. Should be shorter than ${maxLength} characters`);
+      }
+      return name.replace(forbidden, replaceWith);
+    }
+
+    const { prefix, region, stage } = config.provider;
+
+    const nameHash = md5(config.service);
+
+    let safePrefix = prefix.replace(forbidden, replaceWith);
+    const safeRegion = this.createShortAzureRegionName(region);
+    let safeStage = this.createShortStageName(stage);
+    let safeNameHash = nameHash.substr(0, 6);
+
+    const remaining = maxLength - (safePrefix.length + safeRegion.length + safeStage.length + safeNameHash.length);
+
+    // Dynamically adjust the substring based on space needed
+    if (remaining < 0) {
+      const partLength = Math.floor(Math.abs(remaining) / 3);
+      safePrefix = safePrefix.substr(0, partLength);
+      safeStage = safeStage.substr(0, partLength);
+      safeNameHash = safeNameHash.substr(0, partLength);
+    }
+
+    return [safePrefix, safeRegion, safeStage, safeNameHash]
+      .join("")
+      .toLocaleLowerCase();
+  }
+
+  public static getDeploymentName(config: ServerlessAzureConfig, timestamp?: string) {
+    let maxLength = configConstants.naming.maxLength.deploymentName;
+    const suffix = configConstants.naming.suffix.deployment;
+
+    const { deploymentName } = config.provider
+
+    if (timestamp) {
+      maxLength -= timestamp.length + suffix.length;;
+
+      const name = (deploymentName) ? deploymentName.substr(0, maxLength)
+        : [ AzureNamingService.getSafeResourceName(config, maxLength), suffix ].join("-");
+      return [ name, timestamp ].join("-");
+    }
+
+    return deploymentName.substr(0, maxLength);
+  }
+
+  /**
+   * Creates a short name to be used for state name abbreviation
+   * @param stageName The stage name
+   */
+  public static createShortStageName(stageName: string) {
+    Guard.empty(stageName);
+
+    const stageMap = {
+      "dogfood": "df",
+      "production": "prod",
+      "development": "dev",
+      "testing": "test"
+    };
+
+    return this.createShortName(stageName, stageMap);
+  }
+
+  /**
+   * Gets the normalized region name from long name (ex. West US 2 -> westus2)
+   * @param regionName The region name
+   */
+  public static getNormalizedRegionName(regionName: string) {
+    Guard.empty(regionName);
+    return regionName.replace(/\W/g, "").toLowerCase();
+  }
+
+  /**
+   * Creates a short name for an azure region
+   * @param regionName The azure region name
+   */
+  public static createShortAzureRegionName(regionName: string) {
+    Guard.empty(regionName);
+
+    const locationMap = {
+      "north": "n",
+      "south": "s",
+      "east": "e",
+      "west": "w",
+      "central": "c",
+    };
+
+    return this.createShortName(regionName, locationMap);
+  }
+
+  /**
+   * Creates a short name from a long name based on a well-known string map
+   * @param longName The long name to replace
+   * @param wellKnownMap A well known map of long terms to short abbreviations
+   */
+  private static createShortName(longName: string, wellKnownMap: { [key: string]: string }) {
+    Guard.empty(longName);
+    Guard.null(wellKnownMap);
+
+    const pattern = `(${Object.keys(wellKnownMap).join("|")})`;
+    const regex = new RegExp(pattern, "g");
+
+    return longName
+      .replace(/\W+/g, "")
+      .toLowerCase()
+      .split(regex)
+      .map((part) => {
+        return wellKnownMap[part] || part.substr(0, 3);
+      })
+      .join("");
+  }
+}
diff --git a/src/services/resourceService.ts b/src/services/resourceService.ts
index 2d75181..1248e2d 100644
--- a/src/services/resourceService.ts
+++ b/src/services/resourceService.ts
@@ -2,6 +2,7 @@ import Serverless from "serverless";
 import { ResourceManagementClient } from "@azure/arm-resources";
 import { BaseService } from "./baseService";
 import { Utils } from "../shared/utils";
+import { AzureNamingService } from "./namingService";
 
 export class ResourceService extends BaseService {
   private resourceClient: ResourceManagementClient;
@@ -57,7 +58,7 @@ export class ResourceService extends BaseService {
     this.log(`Creating resource group: ${this.resourceGroup}`);
 
     return await this.resourceClient.resourceGroups.createOrUpdate(this.resourceGroup, {
-      location: Utils.getNormalizedRegionName(this.getRegion()),
+      location: AzureNamingService.getNormalizedRegionName(this.getRegion()),
     });
   }
 
diff --git a/src/shared/utils.ts b/src/shared/utils.ts
index 7490d1c..bf3717f 100644
--- a/src/shared/utils.ts
+++ b/src/shared/utils.ts
@@ -1,9 +1,8 @@
 import { relative } from "path";
 import Serverless from "serverless";
+import { ServerlessAzureFunctionConfig } from "../models/serverless";
 import { BindingUtils } from "./bindings";
 import { constants } from "./constants";
-import { Guard } from "./guard";
-import { ServerlessAzureFunctionConfig } from "../models/serverless";
 
 export interface FunctionMetadata {
   entryPoint: any;
@@ -132,72 +131,6 @@ export class Utils {
     return result;
   }
 
-  /**
-   * Creates a short name to be used for state name abbreviation
-   * @param stageName The stage name
-   */
-  public static createShortStageName(stageName: string) {
-    Guard.empty(stageName);
-
-    const stageMap = {
-      "dogfood": "df",
-      "production": "prod",
-      "development": "dev",
-      "testing": "test"
-    };
-
-    return this.createShortName(stageName, stageMap);
-  }
-
-  /**
-   * Gets the normalized region name from long name (ex. West US 2 -> westus2)
-   * @param regionName The region name
-   */
-  public static getNormalizedRegionName(regionName: string) {
-    Guard.empty(regionName);
-    return regionName.replace(/\W/g, "").toLowerCase();
-  }
-
-  /**
-   * Creates a short name for an azure region
-   * @param regionName The azure region name
-   */
-  public static createShortAzureRegionName(regionName: string) {
-    Guard.empty(regionName);
-
-    const locationMap = {
-      "north": "n",
-      "south": "s",
-      "east": "e",
-      "west": "w",
-      "central": "c",
-    };
-
-    return this.createShortName(regionName, locationMap);
-  }
-
-  /**
-   * Creates a short name from a long name based on a well-known string map
-   * @param longName The long name to replace
-   * @param wellKnownMap A well known map of long terms to short abbreviations
-   */
-  private static createShortName(longName: string, wellKnownMap: { [key: string]: string }) {
-    Guard.empty(longName);
-    Guard.null(wellKnownMap);
-
-    const pattern = `(${Object.keys(wellKnownMap).join("|")})`;
-    const regex = new RegExp(pattern, "g");
-
-    return longName
-      .replace(/\W+/g, "")
-      .toLowerCase()
-      .split(regex)
-      .map((part) => {
-        return wellKnownMap[part] || part.substr(0, 3);
-      })
-      .join("");
-  }
-
   public static get(object: any, key: string, defaultValue?: any) {
     if (key in object) {
       return object[key];


Test patch:
diff --git a/src/armTemplates/resources/storageAccount.test.ts b/src/armTemplates/resources/storageAccount.test.ts
index 6507027..b398cda 100644
--- a/src/armTemplates/resources/storageAccount.test.ts
+++ b/src/armTemplates/resources/storageAccount.test.ts
@@ -1,7 +1,7 @@
 import md5 from "md5";
-import { StorageAccountResource } from "./storageAccount";
 import { ServerlessAzureConfig } from "../../models/serverless";
-import { Utils } from "../../shared/utils";
+import { AzureNamingService } from "../../services/namingService";
+import { StorageAccountResource } from "./storageAccount";
 
 describe("Storage Account Resource", () => {
   const config: ServerlessAzureConfig = {
@@ -142,7 +142,7 @@ describe("Storage Account Resource", () => {
   function assertValidStorageAccountName(config: ServerlessAzureConfig, value: string) {
     expect(value.length).toBeLessThanOrEqual(24);
     expect(value.match(/[a-z0-9]/g).length).toEqual(value.length);
-    expect(value).toContain(Utils.createShortAzureRegionName(config.provider.region));
+    expect(value).toContain(AzureNamingService.createShortAzureRegionName(config.provider.region));
     expect(value).toContain(createSafeString(config.provider.prefix));
     expect(value).toContain(createSafeString(config.provider.stage));
     expect(value).toContain(md5(config.service).substr(0, 3));
@@ -151,4 +151,4 @@ describe("Storage Account Resource", () => {
   function createSafeString(value: string) {
     return value.replace(/\W+/g, "").toLocaleLowerCase().substr(0, 3);
   };
-});
\ No newline at end of file
+});
diff --git a/src/services/apimService.test.ts b/src/services/apimService.test.ts
index 50131f2..4711370 100644
--- a/src/services/apimService.test.ts
+++ b/src/services/apimService.test.ts
@@ -19,7 +19,7 @@ import {
   OperationContract,
   ApiPolicyCreateOrUpdateResponse,
 } from "@azure/arm-apimanagement/esm/models";
-import { Utils } from "../shared/utils";
+import { AzureNamingService } from "./namingService";
 
 describe("APIM Service", () => {
   const apimConfig = MockFactory.createTestApimConfig();
@@ -62,7 +62,7 @@ describe("APIM Service", () => {
     (serverless.service.provider as any).apim = apimConfigName;
 
     const service = new ApimService(serverless);
-    const expectedRegionName = Utils.createShortAzureRegionName(service.getRegion());
+    const expectedRegionName = AzureNamingService.createShortAzureRegionName(service.getRegion());
     expect(apimConfigName.name.includes(expectedRegionName)).toBeTruthy();
   });
 
diff --git a/src/services/baseService.test.ts b/src/services/baseService.test.ts
index d001cb8..fb7fbc7 100644
--- a/src/services/baseService.test.ts
+++ b/src/services/baseService.test.ts
@@ -2,9 +2,9 @@ import fs from "fs";
 import mockFs from "mock-fs";
 import Serverless from "serverless";
 import { ServerlessAzureOptions } from "../models/serverless";
-import { Utils } from "../shared/utils";
 import { MockFactory } from "../test/mockFactory";
 import { BaseService } from "./baseService";
+import { AzureNamingService } from "./namingService";
 
 jest.mock("axios", () => jest.fn());
 import axios from "axios";
@@ -42,7 +42,7 @@ describe("Base Service", () => {
   let sls: Serverless;
 
   const slsConfig = {
-    service: "My custom service",
+    service: "my custom service",
     provider: {
       resourceGroup: "My-Resource-Group",
       deploymentName: "My-Deployment",
@@ -121,8 +121,8 @@ describe("Base Service", () => {
     sls.service.provider["resourceGroup"] = null;
     const mockService = new MockService(sls);
     const actualResourceGroupName = mockService.getResourceGroupName();
-    const expectedRegion = Utils.createShortAzureRegionName(mockService.getRegion());
-    const expectedStage = Utils.createShortStageName(mockService.getStage());
+    const expectedRegion = AzureNamingService.createShortAzureRegionName(mockService.getRegion());
+    const expectedStage = AzureNamingService.createShortStageName(mockService.getStage());
     const expectedResourceGroupName = `sls-${expectedRegion}-${expectedStage}-${sls.service["service"]}-rg`;
 
     expect(actualResourceGroupName).toEqual(expectedResourceGroupName);
diff --git a/src/services/namingService.test.ts b/src/services/namingService.test.ts
new file mode 100644
index 0000000..33749c4
--- /dev/null
+++ b/src/services/namingService.test.ts
@@ -0,0 +1,89 @@
+import { AzureNamingService } from "./namingService"
+
+describe("Naming Service", () => {
+
+  it("Creates a short name for an azure region", () => {
+    const expected = "ausse";
+    const actual = AzureNamingService.createShortAzureRegionName("australiasoutheast");
+
+    expect(actual).toEqual(expected);
+  });
+
+  it("Creates a short stage name from a well known name", () => {
+    const expected = "prod";
+    const actual = AzureNamingService.createShortStageName("production");
+
+    expect(actual).toEqual(expected);
+  });
+
+  it("Creates a short stage name from a unknown name", () => {
+    const value = "user acceptance";
+    const actual = AzureNamingService.createShortStageName(value);
+
+    expect(actual).toEqual(value.substr(0, 3));
+  });
+
+  it("Creates a short stage name from multiple values", () => {
+    const actual = AzureNamingService.createShortStageName("production dogfood");
+    expect(actual).toEqual("proddf");
+  });
+
+
+  it("Creates unique short names for all azure regions", () => {
+    const regions = [
+      "eastasia",
+      "southeastasia",
+      "centralus",
+      "eastus",
+      "eastus2",
+      "westus",
+      "northcentralus",
+      "southcentralus",
+      "northeurope",
+      "westeurope",
+      "japanwest",
+      "japaneast",
+      "brazilsouth",
+      "australiaeast",
+      "australiasoutheast",
+      "southindia",
+      "centralindia",
+      "westindia",
+      "canadacentral",
+      "canadaeast",
+      "uksouth",
+      "ukwest",
+      "westcentralus",
+      "westus2",
+      "koreacentral",
+      "koreasouth",
+      "francecentral",
+      "francesouth",
+      "australiacentral",
+      "australiacentral2",
+      "uaecentral",
+      "uaenorth",
+      "southafricanorth",
+      "southafricawest"
+    ];
+
+    const results = {};
+    regions.forEach((region) => {
+      const result = AzureNamingService.createShortAzureRegionName(region);
+      results[result] = region;
+    });
+
+    expect(Object.keys(results)).toHaveLength(regions.length);
+  });
+
+  it("gets a normalized region name from full region name", () => {
+    const result = AzureNamingService.getNormalizedRegionName("West US 2");
+    expect(result).toEqual("westus2");
+  });
+
+  it("Performs noop if region name is already normalized", () => {
+    const expected = "westus2";
+    const actual = AzureNamingService.getNormalizedRegionName(expected);
+    expect(actual).toEqual(expected);
+  });
+});
diff --git a/src/services/resourceService.test.ts b/src/services/resourceService.test.ts
index c78df52..fe46f31 100644
--- a/src/services/resourceService.test.ts
+++ b/src/services/resourceService.test.ts
@@ -1,10 +1,10 @@
+import { ResourceManagementClient } from "@azure/arm-resources";
 import { DeploymentsListByResourceGroupResponse } from "@azure/arm-resources/esm/models";
-import { Utils } from "../shared/utils";
 import { MockFactory } from "../test/mockFactory";
+import { AzureNamingService } from "./namingService";
 import { ResourceService } from "./resourceService";
 
 jest.mock("@azure/arm-resources")
-import { ResourceManagementClient } from "@azure/arm-resources";
 
 describe("Resource Service", () => {
   let deployments: DeploymentsListByResourceGroupResponse;
@@ -41,7 +41,7 @@ describe("Resource Service", () => {
     const sls = MockFactory.createTestServerless();
     const resourceGroup = "myResourceGroup"
     const location = "West Us";
-    const expectedLocation = Utils.getNormalizedRegionName(location);
+    const expectedLocation = AzureNamingService.getNormalizedRegionName(location);
     sls.service.provider["resourceGroup"] = resourceGroup
     sls.service.provider.region = location;
     sls.variables["azureCredentials"] = "fake credentials"
diff --git a/src/services/rollbackService.test.ts b/src/services/rollbackService.test.ts
index ffdf0e9..7d183ae 100644
--- a/src/services/rollbackService.test.ts
+++ b/src/services/rollbackService.test.ts
@@ -17,6 +17,7 @@ import { FunctionAppService } from "./functionAppService";
 
 jest.mock("./armService");
 import { ArmService } from "./armService";
+import configConstants from "../config";
 
 describe("Rollback Service", () => {
 
@@ -28,7 +29,8 @@ describe("Rollback Service", () => {
   const appStub = "appStub";
   const sasURL = "sasURL";
   const containerName = "deployment-artifacts";
-  const artifactName = MockFactory.createTestDeployment().name.replace("deployment", "artifact") + ".zip";
+  const artifactName = MockFactory.createTestDeployment().name.replace(
+    configConstants.naming.suffix.deployment, configConstants.naming.suffix.artifact);
   const artifactPath = `.serverless${path.sep}${artifactName}`
   const armDeployment: ArmDeployment = { template, parameters };
   const deploymentString = "deployments";
diff --git a/src/shared/utils.test.ts b/src/shared/utils.test.ts
index 4a121df..668d6e8 100644
--- a/src/shared/utils.test.ts
+++ b/src/shared/utils.test.ts
@@ -8,7 +8,7 @@ describe("utils", () => {
 
   beforeEach(() => {
     const slsConfig = {
-      service: "My test service",
+      service: "my test service",
       provider: "azure",
       functions: MockFactory.createTestSlsFunctionConfig(),
     };
@@ -79,90 +79,6 @@ describe("utils", () => {
     ).toEqual("abfgklpquvab");
   });
 
-  it("Creates a short name for an azure region", () => {
-    const expected = "ausse";
-    const actual = Utils.createShortAzureRegionName("australiasoutheast");
-
-    expect(actual).toEqual(expected);
-  });
-
-  it("Creates a short stage name from a well known name", () => {
-    const expected = "prod";
-    const actual = Utils.createShortStageName("production");
-
-    expect(actual).toEqual(expected);
-  });
-
-  it("Creates a short stage name from a unknown name", () => {
-    const value = "user acceptance";
-    const actual = Utils.createShortStageName(value);
-
-    expect(actual).toEqual(value.substr(0, 3));
-  });
-
-  it("Creates a short stage name from multiple values", () => {
-    const actual = Utils.createShortStageName("production dogfood");
-    expect(actual).toEqual("proddf");
-  });
-
-  it("Creates unique short names for all azure regions", () => {
-    const regions = [
-      "eastasia",
-      "southeastasia",
-      "centralus",
-      "eastus",
-      "eastus2",
-      "westus",
-      "northcentralus",
-      "southcentralus",
-      "northeurope",
-      "westeurope",
-      "japanwest",
-      "japaneast",
-      "brazilsouth",
-      "australiaeast",
-      "australiasoutheast",
-      "southindia",
-      "centralindia",
-      "westindia",
-      "canadacentral",
-      "canadaeast",
-      "uksouth",
-      "ukwest",
-      "westcentralus",
-      "westus2",
-      "koreacentral",
-      "koreasouth",
-      "francecentral",
-      "francesouth",
-      "australiacentral",
-      "australiacentral2",
-      "uaecentral",
-      "uaenorth",
-      "southafricanorth",
-      "southafricawest"
-    ];
-
-    const results = {};
-    regions.forEach((region) => {
-      const result = Utils.createShortAzureRegionName(region);
-      results[result] = region;
-    });
-
-    expect(Object.keys(results)).toHaveLength(regions.length);
-  });
-
-  it("gets a normalized region name from full region name", () => {
-    const result = Utils.getNormalizedRegionName("West US 2");
-    expect(result).toEqual("westus2");
-  });
-
-  it("Performs noop if region name is already normalized", () => {
-    const expected = "westus2";
-    const actual = Utils.getNormalizedRegionName(expected);
-    expect(actual).toEqual(expected);
-  });
-
   it("should get a timestamp from a name", () => {
     expect(Utils.getTimestampFromName("myDeployment-t12345")).toEqual("12345");
     expect(Utils.getTimestampFromName("myDeployment-t678987645")).toEqual("678987645");