Repository: serverless/serverless-azure-functions
Pull request #190 (2019-07-04 01:26:50)

fix: Update storage account naming convention
Ensures storage account names do not clash based on combination of prefix, region, stage & service name

- [x] Ensures generated name is <= 24 chars
- [x] Ensures region names don't conflict
- [x] Generates short region part from full value
- [x] Generates short stage part from full value
- [x] Includes partial hash of service name in full name

Resolves AB#420



Patch:
diff --git a/package-lock.json b/package-lock.json
index ddb886e..b52ea54 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2852,6 +2852,11 @@
       "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
       "dev": true
     },
+    "charenc": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
+      "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
+    },
     "chokidar": {
       "version": "2.1.6",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz",
@@ -3301,6 +3306,11 @@
         "which": "^1.2.9"
       }
     },
+    "crypt": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
+      "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
+    },
     "crypto-browserify": {
       "version": "3.12.0",
       "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
@@ -7563,6 +7573,16 @@
         "object-visit": "^1.0.0"
       }
     },
+    "md5": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
+      "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=",
+      "requires": {
+        "charenc": "~0.0.1",
+        "crypt": "~0.0.1",
+        "is-buffer": "~1.1.1"
+      }
+    },
     "md5.js": {
       "version": "1.3.5",
       "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
diff --git a/package.json b/package.json
index 2d65b3f..c0e9b79 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
     "js-yaml": "^3.13.1",
     "jsonpath": "^1.0.1",
     "lodash": "^4.16.6",
+    "md5": "^2.2.1",
     "open": "^6.3.0",
     "request": "^2.81.0",
     "rimraf": "^2.6.3",
diff --git a/src/armTemplates/compositeArmTemplate.ts b/src/armTemplates/compositeArmTemplate.ts
index a56b547..73d834b 100644
--- a/src/armTemplates/compositeArmTemplate.ts
+++ b/src/armTemplates/compositeArmTemplate.ts
@@ -1,6 +1,10 @@
-import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../models/armTemplates";
+import {
+  ArmResourceTemplateGenerator,
+  ArmResourceTemplate
+} from "../models/armTemplates";
 import { Guard } from "../shared/guard";
 import { ServerlessAzureConfig } from "../models/serverless";
+import { Utils } from "../shared/utils";
 
 export class CompositeArmTemplate implements ArmResourceTemplateGenerator {
   public constructor(private childTemplates: ArmResourceTemplateGenerator[]) {
@@ -9,22 +13,23 @@ export class CompositeArmTemplate implements ArmResourceTemplateGenerator {
 
   public getTemplate(): ArmResourceTemplate {
     const template: ArmResourceTemplate = {
-      "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
-      "contentVersion": "1.0.0.0",
-      "parameters": {},
-      "resources": [],
+      $schema:
+        "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
+      contentVersion: "1.0.0.0",
+      parameters: {},
+      resources: []
     };
 
-    this.childTemplates.forEach((resource) => {
+    this.childTemplates.forEach(resource => {
       const resourceTemplate = resource.getTemplate();
       template.parameters = {
         ...template.parameters,
-        ...resourceTemplate.parameters,
+        ...resourceTemplate.parameters
       };
 
       template.resources = [
         ...template.resources,
-        ...resourceTemplate.resources,
+        ...resourceTemplate.resources
       ];
     });
 
@@ -34,14 +39,14 @@ export class CompositeArmTemplate implements ArmResourceTemplateGenerator {
   public getParameters(config: ServerlessAzureConfig) {
     let parameters = {};
 
-    this.childTemplates.forEach((resource) => {
+    this.childTemplates.forEach(resource => {
       parameters = {
         ...parameters,
         ...resource.getParameters(config),
-        location: config.provider.region,
-      }
+        location: Utils.getNormalizedRegionName(config.provider.region)
+      };
     });
 
     return parameters;
   }
-}
\ No newline at end of file
+}
diff --git a/src/armTemplates/resources/apim.ts b/src/armTemplates/resources/apim.ts
index 46587db..eaca327 100644
--- a/src/armTemplates/resources/apim.ts
+++ b/src/armTemplates/resources/apim.ts
@@ -1,12 +1,13 @@
 import { ServerlessAzureConfig } from "../../models/serverless";
 import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
 import { ApiManagementConfig } from "../../models/apiManagement";
+import { Utils } from "../../shared/utils";
 
 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}-${config.provider.region}-${config.provider.stage}-apim`;
+      : `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-apim`;
   }
 
   public getTemplate(): ArmResourceTemplate {
diff --git a/src/armTemplates/resources/appInsights.ts b/src/armTemplates/resources/appInsights.ts
index 1223062..8667d92 100644
--- a/src/armTemplates/resources/appInsights.ts
+++ b/src/armTemplates/resources/appInsights.ts
@@ -1,11 +1,12 @@
 import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
 import { ServerlessAzureConfig } from "../../models/serverless";
+import { Utils } from "../../shared/utils";
 
 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}-${config.provider.region}-${config.provider.stage}-appinsights`;
+      : `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-appinsights`;
   }
 
   public getTemplate(): ArmResourceTemplate {
diff --git a/src/armTemplates/resources/appServicePlan.ts b/src/armTemplates/resources/appServicePlan.ts
index 8a6a378..9581996 100644
--- a/src/armTemplates/resources/appServicePlan.ts
+++ b/src/armTemplates/resources/appServicePlan.ts
@@ -1,11 +1,12 @@
 import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
 import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless";
+import { Utils } from "../../shared/utils";
 
 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}-${config.provider.region}-${config.provider.stage}-asp`;
+      : `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-asp`;
   }
 
   public getTemplate(): ArmResourceTemplate {
diff --git a/src/armTemplates/resources/functionApp.ts b/src/armTemplates/resources/functionApp.ts
index 50caba9..d2058a3 100644
--- a/src/armTemplates/resources/functionApp.ts
+++ b/src/armTemplates/resources/functionApp.ts
@@ -1,11 +1,14 @@
 import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
 import { ServerlessAzureConfig, FunctionAppConfig } from "../../models/serverless";
+import { Utils } from "../../shared/utils";
 
 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}-${config.provider.region}-${config.provider.stage}-${config.service}`;
+      : `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-${safeServiceName}`;
   }
 
   public getTemplate(): ArmResourceTemplate {
diff --git a/src/armTemplates/resources/hostingEnvironment.ts b/src/armTemplates/resources/hostingEnvironment.ts
index 0591002..233aff0 100644
--- a/src/armTemplates/resources/hostingEnvironment.ts
+++ b/src/armTemplates/resources/hostingEnvironment.ts
@@ -1,11 +1,12 @@
 import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
 import { ServerlessAzureConfig } from "../../models/serverless";
+import { Utils } from "../../shared/utils";
 
 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}-${config.provider.region}-${config.provider.stage}-ase`;
+      : `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-ase`;
   }
 
   public getTemplate(): ArmResourceTemplate {
diff --git a/src/armTemplates/resources/storageAccount.ts b/src/armTemplates/resources/storageAccount.ts
index 0c59f8b..0b6c9d8 100644
--- a/src/armTemplates/resources/storageAccount.ts
+++ b/src/armTemplates/resources/storageAccount.ts
@@ -1,6 +1,7 @@
 import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
 import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless";
 import { Utils } from "../../shared/utils";
+import md5 from "md5";
 
 export class StorageAccountResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
@@ -67,19 +68,30 @@ export class StorageAccountResource implements ArmResourceTemplateGenerator {
   /**
    * Gets a default storage account name.
    * Storage account names can have at most 24 characters and can have only alpha-numerics
-   * Default naming convention:
-   * 
-   * "(first 3 of prefix)(first 3 of region)(first 3 of stage)(first 12 of service)sa"
-   * (Maximum of 23 characters)
    * @param config Serverless Azure Config
    */
   private static getDefaultStorageAccountName(config: ServerlessAzureConfig): string {
-    const prefix = Utils.appendSubstrings(
-      3,
-      config.provider.prefix,
-      config.provider.region,
-      config.provider.stage,
-    );
-    return `${prefix}${config.service.substr(0, 12)}sa`.replace("-", "").toLocaleLowerCase();
+    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 9c1feff..a943893 100644
--- a/src/armTemplates/resources/virtualNetwork.ts
+++ b/src/armTemplates/resources/virtualNetwork.ts
@@ -1,11 +1,12 @@
 import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
 import { ServerlessAzureConfig } from "../../models/serverless";
+import { Utils } from "../../shared/utils";
 
 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}-${config.provider.region}-${config.provider.stage}-vnet`;
+      : `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-vnet`;
   }
 
   public getTemplate(): ArmResourceTemplate {
diff --git a/src/models/azureProvider.ts b/src/models/azureProvider.ts
index 9b293ae..08483f8 100644
--- a/src/models/azureProvider.ts
+++ b/src/models/azureProvider.ts
@@ -18,8 +18,3 @@ export interface FunctionMetadata {
   handler: string;
   events: FunctionEvent[];
 }
-
-export interface AzureServiceProvider {
-  resourceGroup: string;
-  deploymentName: string;
-}
diff --git a/src/models/serverless.ts b/src/models/serverless.ts
index bdf0dba..dc7f0b9 100644
--- a/src/models/serverless.ts
+++ b/src/models/serverless.ts
@@ -29,27 +29,30 @@ export interface DeploymentConfig {
   container?: string;
 }
 
+export interface ServerlessAzureProvider {
+  type?: string;
+  prefix?: string;
+  region: string;
+  stage: string;
+  name: string;
+  environment?: {
+    [key: string]: any;
+  };
+  deploymentName?: string;
+  resourceGroup?: string;
+  apim?: ApiManagementConfig;
+  functionApp?: FunctionAppConfig;
+  appInsights?: ResourceConfig;
+  appServicePlan?: ResourceConfig;
+  storageAccount?: ResourceConfig;
+  hostingEnvironment?: ResourceConfig;
+  virtualNetwork?: ResourceConfig;
+  armTemplate?: ArmTemplateConfig;
+}
+
 export interface ServerlessAzureConfig {
   service: string;
-  provider: {
-    type?: string;
-    prefix?: string;
-    region: string;
-    stage: string;
-    name: string;
-    environment?: {
-      [key: string]: any;
-    };
-    resourceGroup?: string;
-    apim?: ApiManagementConfig;
-    functionApp?: FunctionAppConfig;
-    appInsights?: ResourceConfig;
-    appServicePlan?: ResourceConfig;
-    storageAccount?: ResourceConfig;
-    hostingEnvironment?: ResourceConfig;
-    virtualNetwork?: ResourceConfig;
-    armTemplate?: ArmTemplateConfig;
-  };
+  provider: ServerlessAzureProvider;
   plugins: string[];
   functions: any;
 }
diff --git a/src/services/baseService.ts b/src/services/baseService.ts
index 1d53d74..6b782d4 100644
--- a/src/services/baseService.ts
+++ b/src/services/baseService.ts
@@ -8,22 +8,23 @@ import { configConstants } from "../config";
 import { DeploymentConfig, ServerlessAzureConfig } from "../models/serverless";
 import { Guard } from "../shared/guard";
 import { TokenCredentialsBase } from "@azure/ms-rest-nodeauth";
+import { Utils } from "../shared/utils";
 
 export abstract class BaseService {
   protected baseUrl: string;
   protected serviceName: string;
-  protected credentials: TokenCredentialsBase
+  protected credentials: TokenCredentialsBase;
   protected subscriptionId: string;
   protected resourceGroup: string;
   protected deploymentName: string;
-  protected deploymentConfig: DeploymentConfig
+  protected deploymentConfig: DeploymentConfig;
   protected storageAccountName: string;
   protected config: ServerlessAzureConfig;
 
   protected constructor(
     protected serverless: Serverless,
     protected options: ServerlessAzureOptions = { stage: null, region: null },
-    authenticate: boolean = true,
+    authenticate: boolean = true
   ) {
     Guard.null(serverless);
     this.setDefaultValues();
@@ -36,10 +37,14 @@ 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(
+      serverless.service as any
+    );
 
     if (!this.credentials && authenticate) {
-      throw new Error(`Azure Credentials has not been set in ${this.constructor.name}`);
+      throw new Error(
+        `Azure Credentials has not been set in ${this.constructor.name}`
+      );
     }
   }
 
@@ -56,24 +61,25 @@ export abstract class BaseService {
   }
 
   public getResourceGroupName(): string {
-    const name = this.options.resourceGroup
-      || this.config.provider["resourceGroup"]
-      || `${this.getPrefix()}-${this.getRegion()}-${this.getStage()}-${this.serviceName}-rg`;
+    const regionName = Utils.createShortAzureRegionName(this.getRegion());
+    const stageName = Utils.createShortStageName(this.getStage());
 
-    return name;
+    return this.options.resourceGroup
+      || this.config.provider.resourceGroup
+      || `${this.getPrefix()}-${regionName}-${stageName}-${this.serviceName}-rg`;
   }
 
   public getDeploymentConfig(): DeploymentConfig {
     const providedConfig = this.serverless["deploy"] as DeploymentConfig;
     const config = providedConfig || {
       rollback: configConstants.rollbackEnabled,
-      container: configConstants.deploymentArtifactContainer,
-    }
+      container: configConstants.deploymentArtifactContainer
+    };
     return config;
   }
 
   public getDeploymentName(): string {
-    const name = this.config.provider["deploymentName"] || `${this.resourceGroup}-deployment`
+    const name = this.config.provider.deploymentName || `${this.resourceGroup}-deployment`;
     return this.rollbackConfiguredName(name);
   }
 
@@ -94,20 +100,24 @@ export abstract class BaseService {
    * @param relativeUrl The relative url
    * @param options Additional HTTP options including headers, etc
    */
-  protected async sendApiRequest(method: string, relativeUrl: string, options: any = {}) {
+  protected async sendApiRequest(
+    method: string,
+    relativeUrl: string,
+    options: any = {}
+  ) {
     const defaultHeaders = {
-      Authorization: `Bearer ${this.getAccessToken()}`,
+      Authorization: `Bearer ${this.getAccessToken()}`
     };
 
     const allHeaders = {
       ...defaultHeaders,
-      ...options.headers,
+      ...options.headers
     };
 
     const requestOptions = {
       ...options,
       method,
-      headers: allHeaders,
+      headers: allHeaders
     };
 
     return await axios(relativeUrl, requestOptions);
@@ -120,14 +130,15 @@ export abstract class BaseService {
    */
   protected sendFile(requestOptions, filePath) {
     return new Promise((resolve, reject) => {
-      fs.createReadStream(filePath)
-        .pipe(request(requestOptions, (err, response) => {
+      fs.createReadStream(filePath).pipe(
+        request(requestOptions, (err, response) => {
           if (err) {
             this.log(JSON.stringify(err, null, 4));
             return reject(err);
           }
           resolve(response);
-        }));
+        })
+      );
     });
   }
 
@@ -140,16 +151,17 @@ export abstract class BaseService {
   }
 
   protected slsConfigFile(): string {
-    return ("config" in this.options) ? this.options["config"] : "serverless.yml";
+    return "config" in this.options ? this.options["config"] : "serverless.yml";
   }
 
   private setDefaultValues(): void {
     // TODO: Right now the serverless core will always default to AWS default region if the
     // region has not been set in the serverless.yml or CLI options
-    const awsDefault = "us-east-1"
+    const awsDefault = "us-east-1";
     const providerRegion = this.serverless.service.provider.region;
 
-    if (!providerRegion || providerRegion === awsDefault) { // no region specified in serverless.yml
+    if (!providerRegion || providerRegion === awsDefault) {
+      // no region specified in serverless.yml
       this.serverless.service.provider.region = "westus";
     }
 
@@ -167,7 +179,9 @@ export abstract class BaseService {
    * @param name Original name
    */
   private rollbackConfiguredName(name: string) {
-    return (this.deploymentConfig.rollback) ? `${name}-t${this.getTimestamp()}` : name;
+    return this.deploymentConfig.rollback
+      ? `${name}-t${this.getTimestamp()}`
+      : name;
   }
 
   /**
diff --git a/src/services/resourceService.ts b/src/services/resourceService.ts
index e36dd90..2a9baa2 100644
--- a/src/services/resourceService.ts
+++ b/src/services/resourceService.ts
@@ -1,6 +1,7 @@
 import Serverless from "serverless";
 import { ResourceManagementClient } from "@azure/arm-resources";
 import { BaseService } from "./baseService";
+import { Utils } from "../shared/utils";
 
 export class ResourceService extends BaseService {
   private resourceClient: ResourceManagementClient;
@@ -20,7 +21,7 @@ export class ResourceService extends BaseService {
     this.log(`Creating resource group: ${this.resourceGroup}`);
 
     return await this.resourceClient.resourceGroups.createOrUpdate(this.resourceGroup, {
-      location: this.getRegion(),
+      location: Utils.getNormalizedRegionName(this.getRegion()),
     });
   }
 
diff --git a/src/shared/utils.ts b/src/shared/utils.ts
index 8777c34..974d865 100644
--- a/src/shared/utils.ts
+++ b/src/shared/utils.ts
@@ -2,6 +2,7 @@ import { relative } from "path";
 import Serverless from "serverless";
 import { BindingUtils } from "./bindings";
 import { constants } from "./constants";
+import { Guard } from "./guard";
 
 export interface FunctionMetadata {
   entryPoint: any;
@@ -104,7 +105,7 @@ export class Utils {
       entryPoint = handlerSplit[handlerSplit.length - 1];
       handlerPath = `${handler.substring(0, handler.lastIndexOf("."))}.js`;
     }
-    
+
     const metaData = {
       entryPoint: entryPoint,
       handlerPath: handlerPath
@@ -136,4 +137,70 @@ 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("");
+  }
 }


Test patch:
diff --git a/src/armTemplates/resources/storageAccount.test.ts b/src/armTemplates/resources/storageAccount.test.ts
new file mode 100644
index 0000000..6507027
--- /dev/null
+++ b/src/armTemplates/resources/storageAccount.test.ts
@@ -0,0 +1,154 @@
+import md5 from "md5";
+import { StorageAccountResource } from "./storageAccount";
+import { ServerlessAzureConfig } from "../../models/serverless";
+import { Utils } from "../../shared/utils";
+
+describe("Storage Account Resource", () => {
+  const config: ServerlessAzureConfig = {
+    functions: [],
+    plugins: [],
+    provider: {
+      prefix: "sls",
+      name: "azure",
+      region: "westus",
+      stage: "dev",
+    },
+    service: "test-api"
+  }
+
+  it("Generates safe storage account name with short parts", () => {
+    const testConfig: ServerlessAzureConfig = {
+      ...config,
+      service: "test-api",
+    };
+
+    const result = StorageAccountResource.getResourceName(testConfig);
+    assertValidStorageAccountName(testConfig, result);
+    expect(result.startsWith("slswusdev")).toBe(true);
+  });
+
+  it("Generates safe storage account names with long parts", () => {
+    const testConfig: ServerlessAzureConfig = {
+      ...config,
+      provider: {
+        ...config.provider,
+        prefix: "my-long-test-prefix-name",
+        region: "Australia Southeast",
+        stage: "development"
+      },
+      service: "my-long-test-api",
+    };
+
+    const result = StorageAccountResource.getResourceName(testConfig);
+    assertValidStorageAccountName(testConfig, result);
+    expect(result.startsWith("mylaussedev")).toBe(true);
+  });
+
+  it("Generating a storage account name is idempotent", () => {
+    const result1 = StorageAccountResource.getResourceName(config);
+    const result2 = StorageAccountResource.getResourceName(config);
+
+    expect(result1).toEqual(result2);
+  });
+
+  it("Generates distinct account names based on region", () => {
+    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 regionConfigs = regions.map((region) => {
+      return {
+        ...config,
+        provider: {
+          ...config.provider,
+          region: region,
+        }
+      };
+    });
+
+    const results = {};
+    regionConfigs.forEach((config) => {
+      const result = StorageAccountResource.getResourceName(config);
+      assertValidStorageAccountName(config, result);
+      results[result] = config;
+    });
+
+    expect(Object.keys(results)).toHaveLength(regionConfigs.length);
+  });
+
+  it("Generates distinct account names based on stage", () => {
+    const stages = [
+      "dev",
+      "test",
+      "qa",
+      "uat",
+      "prod",
+      "preprod",
+    ];
+
+    const stageConfigs = stages.map((region) => {
+      return {
+        ...config,
+        provider: {
+          ...config.provider,
+          region: region,
+        }
+      };
+    });
+
+    const results = {};
+    stageConfigs.forEach((config) => {
+      const result = StorageAccountResource.getResourceName(config);
+      assertValidStorageAccountName(config, result);
+      results[result] = config;
+    });
+
+    expect(Object.keys(results)).toHaveLength(stageConfigs.length);
+  });
+
+  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(createSafeString(config.provider.prefix));
+    expect(value).toContain(createSafeString(config.provider.stage));
+    expect(value).toContain(md5(config.service).substr(0, 3));
+  }
+
+  function createSafeString(value: string) {
+    return value.replace(/\W+/g, "").toLocaleLowerCase().substr(0, 3);
+  };
+});
\ No newline at end of file
diff --git a/src/services/azureBlobStorageService.test.ts b/src/services/azureBlobStorageService.test.ts
index d4d1a10..7d37cb7 100644
--- a/src/services/azureBlobStorageService.test.ts
+++ b/src/services/azureBlobStorageService.test.ts
@@ -12,9 +12,9 @@ import { StorageAccounts, StorageManagementClientContext } from "@azure/arm-stor
 
 jest.mock("./loginService");
 import { AzureLoginService } from "./loginService"
+import { StorageAccountResource } from "../armTemplates/resources/storageAccount";
 
 describe("Azure Blob Storage Service", () => {
-
   const filePath = "deployments/deployment.zip";
   const fileName = "deployment.zip";
   const fileContents = "contents";
@@ -22,7 +22,7 @@ describe("Azure Blob Storage Service", () => {
 
   const containers = MockFactory.createTestAzureContainers();
   const sls = MockFactory.createTestServerless();
-  const accountName = "slswesdevservicenamesa";
+  const accountName = StorageAccountResource.getResourceName(sls.service as any);
   const options = MockFactory.createTestServerlessOptions();
   const blockBlobUrl = MockFactory.createTestBlockBlobUrl(containerName, filePath);
 
diff --git a/src/services/baseService.test.ts b/src/services/baseService.test.ts
index 06e2de2..1ecf667 100644
--- a/src/services/baseService.test.ts
+++ b/src/services/baseService.test.ts
@@ -8,6 +8,7 @@ import request from "request";
 import fs from "fs";
 import { BaseService } from "./baseService";
 import { ServerlessAzureOptions } from "../models/serverless";
+import { Utils } from "../shared/utils";
 
 class MockService extends BaseService {
   public constructor(serverless: Serverless, options?: ServerlessAzureOptions) {
@@ -118,10 +119,9 @@ describe("Base Service", () => {
     sls.service.provider["resourceGroup"] = null;
     const mockService = new MockService(sls);
     const actualResourceGroupName = mockService.getResourceGroupName();
-
-    const region = mockService.getRegion();
-    const stage = mockService.getStage();
-    const expectedResourceGroupName = `sls-${region}-${stage}-${sls.service["service"]}-rg`;
+    const expectedRegion = Utils.createShortAzureRegionName(mockService.getRegion());
+    const expectedStage = Utils.createShortStageName(mockService.getStage());
+    const expectedResourceGroupName = `sls-${expectedRegion}-${expectedStage}-${sls.service["service"]}-rg`;
 
     expect(actualResourceGroupName).toEqual(expectedResourceGroupName);
   });
diff --git a/src/services/resourceService.test.ts b/src/services/resourceService.test.ts
index f374306..3b75754 100644
--- a/src/services/resourceService.test.ts
+++ b/src/services/resourceService.test.ts
@@ -4,6 +4,7 @@ import { ResourceService } from "./resourceService";
 
 jest.mock("@azure/arm-resources")
 import { ResourceManagementClient } from "@azure/arm-resources";
+import { Utils } from "../shared/utils";
 
 describe("Resource Service", () => {
   const deployments = MockFactory.createTestDeployments();
@@ -37,14 +38,16 @@ describe("Resource Service", () => {
     const sls = MockFactory.createTestServerless();
     const resourceGroup = "myResourceGroup"
     const location = "West Us";
+    const expectedLocation = Utils.getNormalizedRegionName(location);
     sls.service.provider["resourceGroup"] = resourceGroup
     sls.service.provider.region = location;
     sls.variables["azureCredentials"] = "fake credentials"
     const options = MockFactory.createTestServerlessOptions();
     const service = new ResourceService(sls, options);
     service.deployResourceGroup();
+
     expect(ResourceManagementClient.prototype.resourceGroups.createOrUpdate)
-      .toBeCalledWith(resourceGroup, { location });
+      .toBeCalledWith(resourceGroup, { location: expectedLocation });
   });
 
   it("deletes a deployment", () => {
diff --git a/src/shared/utils.test.ts b/src/shared/utils.test.ts
index 8f31917..6982004 100644
--- a/src/shared/utils.test.ts
+++ b/src/shared/utils.test.ts
@@ -78,4 +78,88 @@ 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);
+  });
 });
diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts
index 6177bd7..ba015c6 100644
--- a/src/test/mockFactory.ts
+++ b/src/test/mockFactory.ts
@@ -13,9 +13,9 @@ import Utils from "serverless/classes/Utils";
 import PluginManager from "serverless/lib/classes/PluginManager";
 import { ApiCorsPolicy, ApiManagementConfig } from "../models/apiManagement";
 import { ArmResourceTemplate } from "../models/armTemplates";
-import { AzureServiceProvider, ServicePrincipalEnvVariables } from "../models/azureProvider";
+import { ServicePrincipalEnvVariables } from "../models/azureProvider";
 import { Logger } from "../models/generic";
-import { ServerlessAzureConfig } from "../models/serverless";
+import { ServerlessAzureConfig, ServerlessAzureProvider } from "../models/serverless";
 
 function getAttribute(object: any, prop: string, defaultValue: any): any {
   if (object && object[prop]) {
@@ -305,10 +305,14 @@ export class MockFactory {
     return result;
   }
 
-  public static createTestAzureServiceProvider(): AzureServiceProvider {
+  public static createTestAzureServiceProvider(): ServerlessAzureProvider {
     return {
+      name: "azure",
+      prefix: "sls",
       resourceGroup: "myResourceGroup",
       deploymentName: "myDeploymentName",
+      region: "eastus2",
+      stage: "dev",
     }
   }