Repository: serverless/serverless-azure-functions
Pull request #314 (2019-09-06 17:01:53)

fix: Fix regression of skipping identical ARM deployment
- [x] Added enum `ArmParamType` for commonly used parameter types within ARM templates
- [x] Ignoring `identity` property on resources for comparison, since that does not appear in ARM template from previous deployments
- [x] Added `deepEqual` to utility class with optional normalizer
- [x] Moved `AzureKeyVaultConfig` to `serverless.ts` near other config models
- [x] Added `ArmParameter` interface

Resolves issue of not being able to skip previous deployments due to `identity` as well as mismatch in casing of `type` within ARM parameters


Patch:
diff --git a/src/armTemplates/compositeArmTemplate.ts b/src/armTemplates/compositeArmTemplate.ts
index 299adbd..d79c084 100644
--- a/src/armTemplates/compositeArmTemplate.ts
+++ b/src/armTemplates/compositeArmTemplate.ts
@@ -1,4 +1,4 @@
-import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../models/armTemplates";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator, ArmParameters, ArmParamType } from "../models/armTemplates";
 import { ServerlessAzureConfig } from "../models/serverless";
 import { AzureNamingService } from "../services/namingService";
 import { Guard } from "../shared/guard";
@@ -33,14 +33,17 @@ export class CompositeArmTemplate implements ArmResourceTemplateGenerator {
     return template;
   }
 
-  public getParameters(config: ServerlessAzureConfig) {
-    let parameters = {};
+  public getParameters(config: ServerlessAzureConfig): ArmParameters {
+    let parameters: ArmParameters = {};
 
     this.childTemplates.forEach(resource => {
       parameters = {
         ...parameters,
         ...resource.getParameters(config),
-        location: AzureNamingService.getNormalizedRegionName(config.provider.region)
+        location: {
+          type: ArmParamType.String,
+          value: AzureNamingService.getNormalizedRegionName(config.provider.region)
+        }
       };
     });
 
diff --git a/src/armTemplates/resources/apim.ts b/src/armTemplates/resources/apim.ts
index dbc37cf..3544fba 100644
--- a/src/armTemplates/resources/apim.ts
+++ b/src/armTemplates/resources/apim.ts
@@ -1,5 +1,5 @@
 import { ApiManagementConfig } from "../../models/apiManagement";
-import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../../models/armTemplates";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator, ArmParamType, ArmParameters } from "../../models/armTemplates";
 import { ServerlessAzureConfig } from "../../models/serverless";
 import { AzureNamingService, AzureNamingServiceOptions } from "../../services/namingService";
 
@@ -20,27 +20,27 @@ export class ApimResource implements ArmResourceTemplateGenerator {
       "parameters": {
         "apiManagementName": {
           "defaultValue": "",
-          "type": "String"
+          "type": ArmParamType.String
         },
         "location": {
           "defaultValue": "",
-          "type": "String"
+          "type": ArmParamType.String
         },
         "apimSkuName": {
           "defaultValue": "Consumption",
-          "type": "String"
+          "type": ArmParamType.String
         },
         "apimCapacity": {
           "defaultValue": 0,
-          "type": "int"
+          "type": ArmParamType.Int
         },
         "apimPublisherEmail": {
           "defaultValue": "contact@contoso.com",
-          "type": "String"
+          "type": ArmParamType.String
         },
         "apimPublisherName": {
           "defaultValue": "Contoso",
-          "type": "String"
+          "type": ArmParamType.String
         }
       },
       "variables": {},
@@ -66,18 +66,33 @@ export class ApimResource implements ArmResourceTemplateGenerator {
     };
   }
 
-  public getParameters(config: ServerlessAzureConfig) {
+  public getParameters(config: ServerlessAzureConfig): ArmParameters {
     const apimConfig: ApiManagementConfig = {
       sku: {},
       ...config.provider.apim,
     };
 
     return {
-      apiManagementName: ApimResource.getResourceName(config),
-      apimSkuName: apimConfig.sku.name,
-      apimSkuCapacity: apimConfig.sku.capacity,
-      apimPublisherEmail: apimConfig.publisherEmail,
-      apimPublisherName: apimConfig.publisherName,
+      apiManagementName: {
+        type: ArmParamType.String,
+        value: ApimResource.getResourceName(config),
+      },
+      apimSkuName: {
+        type: ArmParamType.String,
+        value: apimConfig.sku.name,
+      },
+      apimSkuCapacity: {
+        type: ArmParamType.Int,
+        value: apimConfig.sku.capacity,
+      },
+      apimPublisherEmail: {
+        type: ArmParamType.String,
+        value: apimConfig.publisherEmail,
+      },
+      apimPublisherName: {
+        type: ArmParamType.String,
+        value: apimConfig.publisherName,
+      }
     };
   }
 }
diff --git a/src/armTemplates/resources/appInsights.ts b/src/armTemplates/resources/appInsights.ts
index cdb450f..bba0480 100644
--- a/src/armTemplates/resources/appInsights.ts
+++ b/src/armTemplates/resources/appInsights.ts
@@ -1,4 +1,4 @@
-import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../../models/armTemplates";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator, ArmParamType, ArmParameters } from "../../models/armTemplates";
 import { ServerlessAzureConfig } from "../../models/serverless";
 import { AzureNamingService, AzureNamingServiceOptions } from "../../services/namingService";
 
@@ -19,11 +19,11 @@ export class AppInsightsResource implements ArmResourceTemplateGenerator {
       "parameters": {
         "appInsightsName": {
           "defaultValue": "",
-          "type": "String"
+          "type": ArmParamType.String
         },
         "location": {
           "defaultValue": "",
-          "type": "String"
+          "type": ArmParamType.String
         }
       },
       "variables": {},
@@ -43,9 +43,12 @@ export class AppInsightsResource implements ArmResourceTemplateGenerator {
     }
   }
 
-  public getParameters(config: ServerlessAzureConfig): any {
+  public getParameters(config: ServerlessAzureConfig): ArmParameters {
     return {
-      appInsightsName: AppInsightsResource.getResourceName(config),
+      appInsightsName: {
+        type: ArmParamType.String,
+        value: AppInsightsResource.getResourceName(config),
+      }
     };
   }
 }
diff --git a/src/armTemplates/resources/appServicePlan.ts b/src/armTemplates/resources/appServicePlan.ts
index c452fb8..05406f4 100644
--- a/src/armTemplates/resources/appServicePlan.ts
+++ b/src/armTemplates/resources/appServicePlan.ts
@@ -1,4 +1,4 @@
-import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../../models/armTemplates";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator, ArmParamType, ArmParameters } from "../../models/armTemplates";
 import { ResourceConfig, ServerlessAzureConfig } from "../../models/serverless";
 import { AzureNamingService, AzureNamingServiceOptions } from "../../services/namingService";
 
@@ -19,19 +19,19 @@ export class AppServicePlanResource implements ArmResourceTemplateGenerator {
       "parameters": {
         "appServicePlanName": {
           "defaultValue": "",
-          "type": "String"
+          "type": ArmParamType.String
         },
         "location": {
           "defaultValue": "",
-          "type": "String"
+          "type": ArmParamType.String
         },
         "appServicePlanSkuName": {
           "defaultValue": "EP1",
-          "type": "String"
+          "type": ArmParamType.String
         },
         "appServicePlanSkuTier": {
           "defaultValue": "ElasticPremium",
-          "type": "String"
+          "type": ArmParamType.String
         }
       },
       "variables": {},
@@ -57,16 +57,25 @@ export class AppServicePlanResource implements ArmResourceTemplateGenerator {
     };
   }
 
-  public getParameters(config: ServerlessAzureConfig): any {
+  public getParameters(config: ServerlessAzureConfig): ArmParameters {
     const resourceConfig: ResourceConfig = {
       sku: {},
       ...config.provider.storageAccount,
     };
 
     return {
-      appServicePlanName: AppServicePlanResource.getResourceName(config),
-      appServicePlanSkuName: resourceConfig.sku.name,
-      appServicePlanSkuTier: resourceConfig.sku.tier,
+      appServicePlanName: {
+        type: ArmParamType.String,
+        value: AppServicePlanResource.getResourceName(config),
+      },
+      appServicePlanSkuName: {
+        type: ArmParamType.String,
+        value: resourceConfig.sku.name,
+      },
+      appServicePlanSkuTier: {
+        type: ArmParamType.String,
+        value: resourceConfig.sku.tier,
+      }
     }
   }
 }
diff --git a/src/armTemplates/resources/functionApp.ts b/src/armTemplates/resources/functionApp.ts
index a05b211..1ce2985 100644
--- a/src/armTemplates/resources/functionApp.ts
+++ b/src/armTemplates/resources/functionApp.ts
@@ -1,4 +1,4 @@
-import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../../models/armTemplates";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator, ArmParamType, ArmParameters } from "../../models/armTemplates";
 import { FunctionAppConfig, ServerlessAzureConfig } from "../../models/serverless";
 import { AzureNamingService, AzureNamingServiceOptions } from "../../services/namingService";
 
@@ -26,36 +26,36 @@ export class FunctionAppResource implements ArmResourceTemplateGenerator {
       "parameters": {
         "functionAppRunFromPackage": {
           "defaultValue": "1",
-          "type": "String"
+          "type": ArmParamType.String
         },
         "functionAppName": {
           "defaultValue": "",
-          "type": "String"
+          "type": ArmParamType.String
         },
         "functionAppNodeVersion": {
           "defaultValue": "",
-          "type": "String"
+          "type": ArmParamType.String
         },
         "functionAppWorkerRuntime": {
           "defaultValue": "node",
-          "type": "String"
+          "type": ArmParamType.String
         },
         "functionAppExtensionVersion": {
           "defaultValue": "~2",
-          "type": "String"
+          "type": ArmParamType.String
         },
         "storageAccountName": {
           "defaultValue": "",
-          "type": "String"
+          "type": ArmParamType.String
         },
         "appInsightsName": {
           "defaultValue": "",
-          "type": "String"
+          "type": ArmParamType.String
         },
         "location": {
           "defaultValue": "",
-          "type": "String"
-        }
+          "type": ArmParamType.String
+        },
       },
       "variables": {},
       "resources": [
@@ -65,7 +65,7 @@ export class FunctionAppResource implements ArmResourceTemplateGenerator {
           "name": "[parameters('functionAppName')]",
           "location": "[parameters('location')]",
           "identity": {
-            "type": "SystemAssigned"
+            "type": ArmParamType.SystemAssigned
           },
           "dependsOn": [
             "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]",
@@ -118,17 +118,29 @@ export class FunctionAppResource implements ArmResourceTemplateGenerator {
     };
   }
 
-  public getParameters(config: ServerlessAzureConfig): any {
+  public getParameters(config: ServerlessAzureConfig): ArmParameters {
     const resourceConfig: FunctionAppConfig = {
       ...config.provider.functionApp,
       nodeVersion: this.getRuntimeVersion(config.provider.runtime)
     };
 
     return {
-      functionAppName: FunctionAppResource.getResourceName(config),
-      functionAppNodeVersion: resourceConfig.nodeVersion,
-      functionAppWorkerRuntime: resourceConfig.workerRuntime,
-      functionAppExtensionVersion: resourceConfig.extensionVersion,
+      functionAppName: {
+        type: ArmParamType.String,
+        value: FunctionAppResource.getResourceName(config),
+      },
+      functionAppNodeVersion: {
+        type: ArmParamType.String,
+        value: resourceConfig.nodeVersion,
+      },
+      functionAppWorkerRuntime: {
+        type: ArmParamType.String,
+        value: resourceConfig.workerRuntime,
+      },
+      functionAppExtensionVersion: {
+        type: ArmParamType.String,
+        value: resourceConfig.extensionVersion,
+      }
     };
   }
 
diff --git a/src/armTemplates/resources/hostingEnvironment.ts b/src/armTemplates/resources/hostingEnvironment.ts
index 87caa16..f2b650f 100644
--- a/src/armTemplates/resources/hostingEnvironment.ts
+++ b/src/armTemplates/resources/hostingEnvironment.ts
@@ -1,4 +1,4 @@
-import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../../models/armTemplates";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator, ArmParamType, ArmParameters } from "../../models/armTemplates";
 import { ServerlessAzureConfig } from "../../models/serverless";
 import { AzureNamingService, AzureNamingServiceOptions } from "../../services/namingService";
 
@@ -19,15 +19,15 @@ export class HostingEnvironmentResource implements ArmResourceTemplateGenerator
       "parameters": {
         "hostingEnvironmentName": {
           "defaultValue": "",
-          "type": "String"
+          "type": ArmParamType.String
         },
         "virtualNetworkName": {
           "defaultValue": "",
-          "type": "String"
+          "type": ArmParamType.String
         },
         "location": {
           "defaultValue": "",
-          "type": "String"
+          "type": ArmParamType.String
         }
       },
       "variables": {},
@@ -65,9 +65,12 @@ export class HostingEnvironmentResource implements ArmResourceTemplateGenerator
     };
   }
 
-  public getParameters(config: ServerlessAzureConfig): any {
+  public getParameters(config: ServerlessAzureConfig): ArmParameters {
     return {
-      hostingEnvironmentName: HostingEnvironmentResource.getResourceName(config)
+      hostingEnvironmentName: {
+        type: ArmParamType.String,
+        value: HostingEnvironmentResource.getResourceName(config)
+      }
     }
   }
 }
diff --git a/src/armTemplates/resources/storageAccount.ts b/src/armTemplates/resources/storageAccount.ts
index dc1a768..734fd60 100644
--- a/src/armTemplates/resources/storageAccount.ts
+++ b/src/armTemplates/resources/storageAccount.ts
@@ -1,13 +1,7 @@
-import {
-  ArmResourceTemplate,
-  ArmResourceTemplateGenerator
-} from "../../models/armTemplates";
-import { ResourceConfig, ServerlessAzureConfig } from "../../models/serverless";
-import {
-  AzureNamingService,
-  AzureNamingServiceOptions
-} from "../../services/namingService";
 import configConstants from "../../config";
+import { ArmParameters, ArmParamType, ArmResourceTemplate, ArmResourceTemplateGenerator } from "../../models/armTemplates";
+import { ResourceConfig, ServerlessAzureConfig } from "../../models/serverless";
+import { AzureNamingService, AzureNamingServiceOptions } from "../../services/namingService";
 
 export class StorageAccountResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
@@ -24,25 +18,24 @@ export class StorageAccountResource implements ArmResourceTemplateGenerator {
 
   public getTemplate(): ArmResourceTemplate {
     return {
-      $schema:
-        "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
-      contentVersion: "1.0.0.0",
-      parameters: {
-        storageAccountName: {
-          defaultValue: "",
-          type: "String"
+      "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
+      "contentVersion": "1.0.0.0",
+      "parameters": {
+        "storageAccountName": {
+          "defaultValue": "",
+          "type": ArmParamType.String
         },
-        location: {
-          defaultValue: "",
-          type: "String"
+        "location": {
+          "defaultValue": "",
+          "type": ArmParamType.String
         },
-        storageAccountSkuName: {
-          defaultValue: "Standard_LRS",
-          type: "String"
+        "storageAccountSkuName": {
+          "defaultValue": "Standard_LRS",
+          "type": ArmParamType.String
         },
-        storageAccoutSkuTier: {
-          defaultValue: "Standard",
-          type: "String"
+        "storageAccoutSkuTier": {
+          "defaultValue": "Standard",
+          "type": ArmParamType.String
         }
       },
       variables: {},
@@ -65,16 +58,25 @@ export class StorageAccountResource implements ArmResourceTemplateGenerator {
     };
   }
 
-  public getParameters(config: ServerlessAzureConfig): any {
+  public getParameters(config: ServerlessAzureConfig): ArmParameters {
     const resourceConfig: ResourceConfig = {
       sku: {},
       ...config.provider.storageAccount
     };
 
     return {
-      storageAccountName: StorageAccountResource.getResourceName(config),
-      storageAccountSkuName: resourceConfig.sku.name,
-      storageAccoutSkuTier: resourceConfig.sku.tier
+      storageAccountName: {
+        type: ArmParamType.String,
+        value: StorageAccountResource.getResourceName(config),
+      },
+      storageAccountSkuName: {
+        type: ArmParamType.String,
+        value: resourceConfig.sku.name,
+      },
+      storageAccoutSkuTier: {
+        type: ArmParamType.String,
+        value: resourceConfig.sku.tier,
+      }
     };
   }
 }
diff --git a/src/armTemplates/resources/virtualNetwork.ts b/src/armTemplates/resources/virtualNetwork.ts
index dd3202b..9092eb8 100644
--- a/src/armTemplates/resources/virtualNetwork.ts
+++ b/src/armTemplates/resources/virtualNetwork.ts
@@ -1,4 +1,4 @@
-import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../../models/armTemplates";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator, ArmParamType, ArmParameters } from "../../models/armTemplates";
 import { ServerlessAzureConfig } from "../../models/serverless";
 import { AzureNamingService, AzureNamingServiceOptions } from "../../services/namingService";
 
@@ -19,15 +19,15 @@ export class VirtualNetworkResource implements ArmResourceTemplateGenerator {
       "parameters": {
         "hostingEnvironmentName": {
           "defaultValue": "",
-          "type": "String"
+          "type": ArmParamType.String
         },
         "virtualNetworkName": {
           "defaultValue": "",
-          "type": "String"
+          "type": ArmParamType.String
         },
         "location": {
           "defaultValue": "",
-          "type": "String"
+          "type": ArmParamType.String
         }
       },
       "variables": {},
@@ -83,9 +83,12 @@ export class VirtualNetworkResource implements ArmResourceTemplateGenerator {
     };
   }
 
-  public getParameters(config: ServerlessAzureConfig): any {
+  public getParameters(config: ServerlessAzureConfig): ArmParameters {
     return {
-      virtualNetworkName: VirtualNetworkResource.getResourceName(config),
+      virtualNetworkName: {
+        type: ArmParamType.String,
+        value: VirtualNetworkResource.getResourceName(config),
+      }
     }
   }
 }
diff --git a/src/models/armTemplates.ts b/src/models/armTemplates.ts
index 97ef914..778d666 100644
--- a/src/models/armTemplates.ts
+++ b/src/models/armTemplates.ts
@@ -5,7 +5,7 @@ import { ServerlessAzureConfig } from "./serverless";
  */
 export interface ArmResourceTemplateGenerator {
   getTemplate(): ArmResourceTemplate;
-  getParameters(config: ServerlessAzureConfig): any;
+  getParameters(config: ServerlessAzureConfig): ArmParameters;
 }
 
 export enum ArmTemplateProvisioningState {
@@ -22,6 +22,15 @@ export enum ArmTemplateType {
   AppServiceEnvironment = "ase",
 }
 
+/**
+ * Parameter types within an ARM template
+ */
+export enum ArmParamType {
+  String = "String",
+  Int = "Int",
+  SystemAssigned = "SystemAssigned",
+}
+
 /**
  * Represents an Azure ARM template
  */
@@ -35,10 +44,18 @@ export interface ArmResourceTemplate {
   variables?: any;
 }
 
+export interface ArmParameters {
+  [key: string]: {
+    type: ArmParamType;
+    value?: string | number;
+    defaultValue?: string | number;
+  };
+}
+
 /**
  * Represents an Azure ARM deployment
  */
 export interface ArmDeployment {
   template: ArmResourceTemplate;
-  parameters: { [key: string]: any };
+  parameters: ArmParameters;
 }
diff --git a/src/models/serverless.ts b/src/models/serverless.ts
index cfc161a..cf1aec6 100644
--- a/src/models/serverless.ts
+++ b/src/models/serverless.ts
@@ -1,12 +1,10 @@
 import { ApiManagementConfig } from "./apiManagement";
 import Serverless from "serverless";
+import { ArmParameters } from "./armTemplates";
 
 export interface ArmTemplateConfig {
   file: string;
-  parameters:
-  {
-    [key: string]: string;
-  };
+  parameters: ArmParameters;
 }
 
 export interface ResourceConfig {
@@ -51,9 +49,20 @@ export interface ServerlessAzureProvider {
   hostingEnvironment?: ResourceConfig;
   virtualNetwork?: ResourceConfig;
   armTemplate?: ArmTemplateConfig;
+  keyVaultConfig?: AzureKeyVaultConfig;
   runtime: string;
 }
 
+/**
+ * Defines the Azure Key Vault configuration
+ */
+export interface AzureKeyVaultConfig {
+  /** The name of the azure key vault */
+  name: string;
+  /** The name of the azure resource group with the key vault */
+  resourceGroup: string;
+}
+
 /**
  * Expanding type of incomplete `Serverless` type.
  */
diff --git a/src/services/armService.ts b/src/services/armService.ts
index 09d2b93..9a06782 100644
--- a/src/services/armService.ts
+++ b/src/services/armService.ts
@@ -4,13 +4,13 @@ import fs from "fs";
 import jsonpath from "jsonpath";
 import path from "path";
 import Serverless from "serverless";
-import { ArmDeployment, ArmResourceTemplateGenerator, ArmTemplateType } from "../models/armTemplates";
+import { ArmDeployment, ArmResourceTemplateGenerator, ArmTemplateType, ArmResourceTemplate, ArmParameters } from "../models/armTemplates";
 import { ArmTemplateConfig, ServerlessAzureConfig, ServerlessAzureOptions } from "../models/serverless";
 import { Guard } from "../shared/guard";
 import { BaseService } from "./baseService";
 import { ResourceService } from "./resourceService"
-import deepEqual from "deep-equal";
 import { DeploymentExtendedError } from "../models/azureProvider";
+import deepEqual from "deep-equal";
 
 export class ArmService extends BaseService {
   private resourceClient: ResourceManagementClient;
@@ -94,38 +94,22 @@ export class ArmService extends BaseService {
 
     this.applyEnvironmentVariables(deployment);
 
-    // Convert flat parameter list into ARM parameter format
-    const deploymentParameters = {};
-    Object.keys(deployment.parameters).forEach((key) => {
-      const parameterValue = deployment.parameters[key];
-      if (parameterValue) {
-        deploymentParameters[key] = { value: parameterValue }
-      }
-    });
+    const resourceService = new ResourceService(this.serverless, this.options);
+    const previous = await resourceService.getLastDeploymentTemplate();
+
+    if (this.areDeploymentsEqual(deployment, previous)) {
+      this.log("Generated template same as previous. Skipping ARM deployment");
+      return;
+    }
 
     // Construct deployment object
     const armDeployment: Deployment = {
       properties: {
+        ...deployment,
         mode: "Incremental",
-        template: deployment.template,
-        parameters: deploymentParameters,
       }
     };
 
-    const resourceService = new ResourceService(this.serverless, this.options);
-    const latest = await resourceService.getLastDeploymentTemplate();
-
-    if (latest) {
-      const templateEqual = deepEqual(latest.template, deployment.template);
-      const mergedDefaultParameters = this.mergeDefaultParams(deploymentParameters, deployment.template.parameters);
-      const paramatersEqual = deepEqual(latest.parameters, mergedDefaultParameters);
-
-      if (templateEqual && paramatersEqual) {
-        this.log("Generated template same as previous. Skipping ARM deployment");
-        return;
-      }
-    }
-
     // Deploy ARM template
     this.log("-> Deploying ARM template...");
     this.log(`---> Resource Group: ${this.resourceGroup}`)
@@ -148,6 +132,46 @@ export class ArmService extends BaseService {
     }
   }
 
+  private areDeploymentsEqual(current: ArmDeployment, previous: ArmDeployment): boolean {
+    if (!current || !previous) {
+      return false;
+    }
+    const mergedDefaultParameters = this.mergeDefaultParams(current.parameters, current.template.parameters);
+    const templateNormalizer = (template: ArmResourceTemplate): ArmResourceTemplate => {
+      return {
+        ...template,
+        resources: template.resources.map((item) => {
+          return {
+            ...item,
+            // Currently ignoring `identity` property given to function app arm template
+            identity: undefined
+          }
+        })
+      }
+    }
+
+    const paramsNormalizer = (params: ArmParameters): ArmParameters => {
+      const normalized = {};
+      const keys = Object.keys(params);
+      for (const key of keys) {
+        const original = params[key];
+        normalized[key] = {
+          ...original,
+          type: original.type.toLowerCase()
+        }
+      }
+      return normalized;
+    }
+
+    return deepEqual(
+      paramsNormalizer(mergedDefaultParameters),
+      paramsNormalizer(previous.parameters)
+    ) && deepEqual(
+      templateNormalizer(previous.template),
+      templateNormalizer(current.template),
+    );
+  }
+
   private deploymentErrorToString(deploymentError: DeploymentExtendedError) {
     if (!deploymentError.code || !deploymentError.message) {
       return JSON.stringify(deploymentError);
@@ -155,10 +179,12 @@ export class ArmService extends BaseService {
     let errorString = `${deploymentError.code} - ${deploymentError.message}`;
     if (deploymentError.details) {
 
-      errorString += `
-      ------------------------
-      DEPLOYMENT ERROR DETAILS
-      ------------------------\n`
+      errorString += [
+        "------------------------",
+        "DEPLOYMENT ERROR DETAILS",
+        "------------------------",
+      ].join("\n") + "\n"
+
       deploymentError.details.forEach((childError) => {
         errorString += `\n${this.deploymentErrorToString(childError)}`
       })
@@ -171,17 +197,14 @@ export class ArmService extends BaseService {
    * @param parameters Parameters with specified values
    * @param defaultParameters Parameters with `type` and `defaultValue`
    */
-  private mergeDefaultParams(parameters: any, defaultParameters: any) {
-    const mergedParams = {}
+  private mergeDefaultParams(parameters: ArmParameters, defaultParameters: ArmParameters): ArmParameters {
+    const mergedParams: ArmParameters = {}
     Object.keys(defaultParameters).forEach((key) => {
       const defaultParam = defaultParameters[key];
+      const param = parameters[key];
       mergedParams[key] = {
         type: defaultParam.type,
-        value: (key in parameters)
-          ?
-          parameters[key].value
-          :
-          defaultParameters[key].defaultValue
+        value: (param && param.value) ? param.value : defaultParam.value || defaultParam.defaultValue
       }
     });
     return mergedParams;
diff --git a/src/services/azureKeyVaultService.ts b/src/services/azureKeyVaultService.ts
index 8bb0c5a..bd476f0 100644
--- a/src/services/azureKeyVaultService.ts
+++ b/src/services/azureKeyVaultService.ts
@@ -3,16 +3,7 @@ import { BaseService } from "./baseService";
 import { FunctionAppService } from "./functionAppService";
 import { KeyVaultManagementClient } from "@azure/arm-keyvault";
 import { Vault, SecretPermissions } from "@azure/arm-keyvault/esm/models";
-
-/**
- * Defines the Azure Key Vault configuration
- */
-export interface AzureKeyVaultConfig {
-  /** The name of the azure key vault */
-  name: string;
-  /** The name of the azure resource group with the key vault */
-  resourceGroup: string;
-}
+import { AzureKeyVaultConfig } from "../models/serverless";
 
 /**
  * Services for the Key Vault Plugin
diff --git a/src/services/rollbackService.ts b/src/services/rollbackService.ts
index 540086d..9747196 100644
--- a/src/services/rollbackService.ts
+++ b/src/services/rollbackService.ts
@@ -7,7 +7,7 @@ import { AzureBlobStorageService } from "./azureBlobStorageService";
 import { BaseService } from "./baseService";
 import { FunctionAppService } from "./functionAppService";
 import { ResourceService } from "./resourceService";
-import { ArmDeployment } from "../models/armTemplates";
+import { ArmDeployment, ArmParamType } from "../models/armTemplates";
 import fs from "fs";
 
 /**
@@ -58,10 +58,13 @@ export class RollbackService extends BaseService {
     await this.blobService.initialize();
     if (this.deploymentConfig.runFromBlobUrl) {
       // Set functionRunFromPackage param to SAS URL of blob
-      armDeployment.parameters.functionAppRunFromPackage = await this.blobService.generateBlobSasTokenUrl(
-        this.deploymentConfig.container,
-        artifactName
-      )
+      armDeployment.parameters.functionAppRunFromPackage = {
+        type: ArmParamType.String,
+        value: await this.blobService.generateBlobSasTokenUrl(
+          this.deploymentConfig.container,
+          artifactName
+        )
+      }
     }
     await armService.deployTemplate(armDeployment);
     /**
@@ -82,9 +85,6 @@ export class RollbackService extends BaseService {
     const resourceService = new ResourceService(this.serverless, this.options);
     const { template } = await resourceService.getDeploymentTemplate(deployment.name);
     const { parameters } = deployment.properties;
-    for (const key of Object.keys(parameters)) {
-      parameters[key] = parameters[key].value;
-    }
     return {
       template,
       parameters,


Test patch:
diff --git a/src/services/armService.test.ts b/src/services/armService.test.ts
index a052906..361f17c 100644
--- a/src/services/armService.test.ts
+++ b/src/services/armService.test.ts
@@ -1,7 +1,7 @@
 import Serverless from "serverless";
 import { MockFactory } from "../test/mockFactory";
 import { ArmService } from "./armService";
-import { ArmResourceTemplate, ArmTemplateType, ArmDeployment, ArmTemplateProvisioningState } from "../models/armTemplates";
+import { ArmResourceTemplate, ArmTemplateType, ArmDeployment, ArmTemplateProvisioningState, ArmParamType } from "../models/armTemplates";
 import { ArmTemplateConfig, ServerlessAzureOptions } from "../models/serverless";
 import mockFs from "mock-fs";
 import jsonpath from "jsonpath";
@@ -47,10 +47,7 @@ describe("Arm Service", () => {
     it("Creates an ARM template from a specified file", async () => {
       const armTemplateConfig: ArmTemplateConfig = {
         file: "armTemplates/custom-template.json",
-        parameters: {
-          param1: "1",
-          param2: "2",
-        },
+        parameters: MockFactory.createTestParameters(),
       };
 
       const testTemplate: ArmResourceTemplate = MockFactory.createTestArmTemplate();
@@ -201,13 +198,34 @@ describe("Arm Service", () => {
 
     it("Does not deploy if previously deployed template is the same", async () => {
       const deployment: ArmDeployment = {
-        parameters: MockFactory.createTestParameters(false),
+        parameters: MockFactory.createTestParameters(),
         template: MockFactory.createTestArmTemplate()
       };
       await service.deployTemplate(deployment);
       expect(Deployments.prototype.createOrUpdate).not.toBeCalled()
     });
 
+    it("Does not deploy if identity is only difference between deployments", async () => {
+      const template = MockFactory.createTestArmTemplate();
+      const deployment: ArmDeployment = {
+        parameters: MockFactory.createTestParameters(),
+        template: {
+          ...template,
+          resources: template.resources.map((item) => {
+            return {
+              ...item,
+              identity: {
+                "type": ArmParamType.SystemAssigned
+              }
+            }
+          })
+        }
+      };
+
+      await service.deployTemplate(deployment);
+      expect(Deployments.prototype.createOrUpdate).not.toBeCalled()
+    });
+
     it("Calls deploy if previous template is the same but failed", async () => {
       const deployments = MockFactory.createTestDeployments();
       const failedDeployment: DeploymentExtended = {
@@ -221,7 +239,7 @@ describe("Arm Service", () => {
       ResourceService.prototype.getDeployments = jest.fn(() => Promise.resolve(deployments))
 
       const deployment: ArmDeployment = {
-        parameters: MockFactory.createTestParameters(false),
+        parameters: MockFactory.createTestParameters(),
         template: MockFactory.createTestArmTemplate()
       };
       await service.deployTemplate(deployment);
@@ -230,10 +248,10 @@ describe("Arm Service", () => {
 
     it("Calls deploy if parameters have changed from deployed template", async () => {
       const deployment: ArmDeployment = {
-        parameters: MockFactory.createTestParameters(false),
+        parameters: MockFactory.createTestParameters(),
         template: MockFactory.createTestArmTemplate()
       };
-      deployment.parameters.param1 = "3"
+      deployment.parameters.param1.value = "3";
       await service.deployTemplate(deployment);
       expect(Deployments.prototype.createOrUpdate).toBeCalled();
     });
@@ -241,11 +259,13 @@ describe("Arm Service", () => {
     it("Calls deploy if previously deployed template is different", async () => {
       ResourceService.prototype.getDeploymentTemplate = jest.fn(() => {
         return {
-          template: {}
+          template: {
+            resources: []
+          }
         }
       }) as any;
       const deployment: ArmDeployment = {
-        parameters: MockFactory.createTestParameters(false),
+        parameters: MockFactory.createTestParameters(),
         template: MockFactory.createTestArmTemplate()
       };
       await service.deployTemplate(deployment);
@@ -257,7 +277,7 @@ describe("Arm Service", () => {
         return []
       }) as any;
       const deployment: ArmDeployment = {
-        parameters: MockFactory.createTestParameters(false),
+        parameters: MockFactory.createTestParameters(),
         template: MockFactory.createTestArmTemplate()
       };
       await service.deployTemplate(deployment);
@@ -286,13 +306,6 @@ describe("Arm Service", () => {
     it("Deploys ARM template via resources REST API", async () => {
       sls.service.provider.runtime = "nodejs10.x";
       const deployment = await service.createDeploymentFromType(ArmTemplateType.Consumption);
-      const deploymentParameters = {};
-      Object.keys(deployment.parameters).forEach((key) => {
-        const parameterValue = deployment.parameters[key];
-        if (parameterValue) {
-          deploymentParameters[key] = { value: deployment.parameters[key] };
-        }
-      });
 
       await service.deployTemplate(deployment);
 
@@ -302,8 +315,7 @@ describe("Arm Service", () => {
       const expectedDeployment: Deployment = {
         properties: {
           mode: "Incremental",
-          template: deployment.template,
-          parameters: deploymentParameters,
+          ...deployment
         },
       };
 
@@ -335,10 +347,10 @@ describe("Arm Service", () => {
         }
       })) as any;
       const deployment: ArmDeployment = {
-        parameters: MockFactory.createTestParameters(false),
+        parameters: MockFactory.createTestParameters(),
         template: MockFactory.createTestArmTemplate()
       };
-      deployment.parameters.param1 = "3"
+      deployment.parameters.param1.value = "3"
       const { code, message, details } = lastDeploymentError;
       let errorPattern = [
         code,
diff --git a/src/services/azureKeyVaultService.test.ts b/src/services/azureKeyVaultService.test.ts
index a928c01..d06bba3 100644
--- a/src/services/azureKeyVaultService.test.ts
+++ b/src/services/azureKeyVaultService.test.ts
@@ -1,12 +1,10 @@
 import Serverless from "serverless";
 import { MockFactory } from "../test/mockFactory";
-import {
-  AzureKeyVaultService,
-  AzureKeyVaultConfig
-} from "./azureKeyVaultService";
+import { AzureKeyVaultService } from "./azureKeyVaultService";
 
 import { Vaults } from "@azure/arm-keyvault";
 import { FunctionAppService } from "./functionAppService";
+import { AzureKeyVaultConfig } from "../models/serverless";
 
 describe("Azure Key Vault Service", () => {
   const options: Serverless.Options = MockFactory.createTestServerlessOptions();
@@ -31,7 +29,6 @@ describe("Azure Key Vault Service", () => {
     }) as any;
 
     FunctionAppService.prototype.get = jest.fn(() => {
-      console.log("testing");
       return {
         identity: {
           tenantId: "tid",
diff --git a/src/services/rollbackService.test.ts b/src/services/rollbackService.test.ts
index f532b22..6a77eda 100644
--- a/src/services/rollbackService.test.ts
+++ b/src/services/rollbackService.test.ts
@@ -1,7 +1,7 @@
 import mockFs from "mock-fs";
 import path from "path";
 import Serverless from "serverless";
-import { ArmDeployment } from "../models/armTemplates";
+import { ArmDeployment, ArmParamType } from "../models/armTemplates";
 import { DeploymentConfig } from "../models/serverless";
 import { MockFactory } from "../test/mockFactory";
 import { RollbackService } from "./rollbackService";
@@ -24,9 +24,6 @@ describe("Rollback Service", () => {
 
   const template = MockFactory.createTestArmTemplate();
   const parameters = MockFactory.createTestParameters();
-  for (const p of Object.keys(parameters)) {
-    parameters[p] = parameters[p].value
-  }
   const appStub = "appStub";
   const sasURL = "sasURL";
   const containerName = "deployment-artifacts";
@@ -130,7 +127,10 @@ describe("Rollback Service", () => {
       ...armDeployment,
       parameters: {
         ...armDeployment.parameters,
-        functionAppRunFromPackage: sasURL,
+        functionAppRunFromPackage: {
+          type: ArmParamType.String,
+          value: sasURL
+        },
       }
     });
     expect(AzureBlobStorageService.prototype.downloadBinary).not.toBeCalled();
diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts
index da9771e..a4516df 100644
--- a/src/test/mockFactory.ts
+++ b/src/test/mockFactory.ts
@@ -12,7 +12,7 @@ import Service from "serverless/classes/Service";
 import Utils from "serverless/classes/Utils";
 import PluginManager from "serverless/lib/classes/PluginManager";
 import { ApiCorsPolicy, ApiManagementConfig } from "../models/apiManagement";
-import { ArmDeployment, ArmResourceTemplate, ArmTemplateProvisioningState } from "../models/armTemplates";
+import { ArmDeployment, ArmResourceTemplate, ArmTemplateProvisioningState, ArmParameters, ArmParamType } from "../models/armTemplates";
 import { ServicePrincipalEnvVariables } from "../models/azureProvider";
 import { Logger } from "../models/generic";
 import { ServerlessAzureConfig, ServerlessAzureProvider, ServerlessAzureFunctionConfig, ServerlessCliCommand } from "../models/serverless";
@@ -183,15 +183,11 @@ export class MockFactory {
     return result as DeploymentsListByResourceGroupResponse
   }
 
-  public static createTestParameters(wrap = true) {
-    return (wrap)
-      ? {
-        param1: { value: "1", type: "String" },
-        param2: { value: "2", type: "String" },
-      } : {
-        param1: "1",
-        param2: "2",
-      }
+  public static createTestParameters(): ArmParameters {
+    return {
+      param1: { value: "1", type: ArmParamType.String },
+      param2: { value: "2", type: ArmParamType.String },
+    }
   }
 
   public static createTestDeployment(name?: string, second: number = 0): DeploymentExtended {