Repository: serverless/serverless-azure-functions
Pull request #239 (2019-08-13 04:13:47)

perf: Short-circuit deployment if ARM template hasn't changed
This checks the previously deployed template (including parameters/values) to see if the generated template has changed since the last deployment. If it has not changed, it will not deploy the ARM template. Since many cases will be code changes, this will drastically increase the speed of the average deployment.

Resolves [AB#789]


Patch:
diff --git a/package-lock.json b/package-lock.json
index 63f220e..3969a80 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1794,7 +1794,6 @@
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
       "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
-      "optional": true,
       "requires": {
         "kind-of": "^3.0.2",
         "longest": "^1.0.1",
@@ -1967,7 +1966,6 @@
       "version": "4.10.1",
       "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
       "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
-      "optional": true,
       "requires": {
         "bn.js": "^4.0.0",
         "inherits": "^2.0.1",
@@ -2437,8 +2435,7 @@
     "bn.js": {
       "version": "4.11.8",
       "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
-      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
-      "optional": true
+      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
     },
     "body-parser": {
       "version": "1.19.0",
@@ -2543,8 +2540,7 @@
     "brorand": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
-      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
-      "optional": true
+      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
     },
     "browser-process-hrtime": {
       "version": "0.1.3",
@@ -2573,7 +2569,6 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
       "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
-      "optional": true,
       "requires": {
         "buffer-xor": "^1.0.3",
         "cipher-base": "^1.0.0",
@@ -2610,7 +2605,6 @@
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
       "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
-      "optional": true,
       "requires": {
         "bn.js": "^4.1.0",
         "randombytes": "^2.0.1"
@@ -2707,8 +2701,7 @@
     "buffer-xor": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
-      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
-      "optional": true
+      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
     },
     "builtin-modules": {
       "version": "1.1.1",
@@ -2890,7 +2883,6 @@
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
       "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
-      "optional": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -3268,7 +3260,6 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
       "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
-      "optional": true,
       "requires": {
         "cipher-base": "^1.0.1",
         "inherits": "^2.0.1",
@@ -3281,7 +3272,6 @@
       "version": "1.1.7",
       "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
       "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
-      "optional": true,
       "requires": {
         "cipher-base": "^1.0.3",
         "create-hash": "^1.1.0",
@@ -3521,8 +3511,7 @@
     "deep-equal": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
-      "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
-      "dev": true
+      "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU="
     },
     "deep-extend": {
       "version": "0.6.0",
@@ -3740,7 +3729,6 @@
       "version": "6.4.1",
       "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
       "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==",
-      "optional": true,
       "requires": {
         "bn.js": "^4.4.0",
         "brorand": "^1.0.1",
@@ -3802,7 +3790,6 @@
       "version": "0.1.7",
       "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
       "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
-      "optional": true,
       "requires": {
         "prr": "~1.0.1"
       }
@@ -4182,7 +4169,6 @@
       "version": "0.3.5",
       "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
       "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
-      "optional": true,
       "requires": {
         "d": "1",
         "es5-ext": "~0.10.14"
@@ -4197,7 +4183,6 @@
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
       "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
-      "optional": true,
       "requires": {
         "md5.js": "^1.3.4",
         "safe-buffer": "^5.1.1"
@@ -5476,7 +5461,6 @@
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
       "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
-      "optional": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -5486,7 +5470,6 @@
       "version": "1.1.7",
       "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
       "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
-      "optional": true,
       "requires": {
         "inherits": "^2.0.3",
         "minimalistic-assert": "^1.0.1"
@@ -5496,7 +5479,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
       "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
-      "optional": true,
       "requires": {
         "hash.js": "^1.0.3",
         "minimalistic-assert": "^1.0.0",
@@ -5815,8 +5797,7 @@
     "is-extglob": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
-      "optional": true
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
     },
     "is-fullwidth-code-point": {
       "version": "1.0.0",
@@ -7456,8 +7437,7 @@
     "longest": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
-      "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
-      "optional": true
+      "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc="
     },
     "loose-envify": {
       "version": "1.4.0",
@@ -7548,7 +7528,6 @@
       "version": "1.3.5",
       "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
       "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
-      "optional": true,
       "requires": {
         "hash-base": "^3.0.0",
         "inherits": "^2.0.1",
@@ -7574,7 +7553,6 @@
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
       "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=",
-      "optional": true,
       "requires": {
         "errno": "^0.1.3",
         "readable-stream": "^2.0.1"
@@ -7665,14 +7643,12 @@
     "minimalistic-assert": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
-      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
-      "optional": true
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
     },
     "minimalistic-crypto-utils": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
-      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
-      "optional": true
+      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
     },
     "minimatch": {
       "version": "3.0.4",
@@ -8279,7 +8255,6 @@
       "version": "5.1.4",
       "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz",
       "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==",
-      "optional": true,
       "requires": {
         "asn1.js": "^4.0.0",
         "browserify-aes": "^1.0.0",
@@ -8388,7 +8363,6 @@
       "version": "3.0.17",
       "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
       "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==",
-      "optional": true,
       "requires": {
         "create-hash": "^1.1.2",
         "create-hmac": "^1.1.4",
@@ -8591,8 +8565,7 @@
     "prr": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
-      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
-      "optional": true
+      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY="
     },
     "pseudomap": {
       "version": "1.0.2",
@@ -8659,7 +8632,6 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
       "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
-      "optional": true,
       "requires": {
         "safe-buffer": "^5.1.0"
       }
@@ -9072,7 +9044,6 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
       "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
-      "optional": true,
       "requires": {
         "hash-base": "^3.0.0",
         "inherits": "^2.0.1"
@@ -9462,7 +9433,6 @@
       "version": "2.4.11",
       "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
       "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
-      "optional": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -9658,8 +9628,7 @@
     "source-list-map": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
-      "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==",
-      "optional": true
+      "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw=="
     },
     "source-map": {
       "version": "0.7.3",
@@ -10216,8 +10185,7 @@
     "tapable": {
       "version": "0.2.9",
       "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.9.tgz",
-      "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==",
-      "optional": true
+      "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A=="
     },
     "tar-stream": {
       "version": "1.6.2",
@@ -11067,7 +11035,6 @@
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz",
       "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==",
-      "optional": true,
       "requires": {
         "source-list-map": "^2.0.0",
         "source-map": "~0.6.1"
@@ -11076,8 +11043,7 @@
         "source-map": {
           "version": "0.6.1",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-          "optional": true
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
         }
       }
     },
diff --git a/package.json b/package.json
index 1396e58..b7cb6f8 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
     "@azure/ms-rest-nodeauth": "^1.0.1",
     "@azure/storage-blob": "^10.3.0",
     "axios": "^0.18.0",
+    "deep-equal": "^1.0.1",
     "js-yaml": "^3.13.1",
     "jsonpath": "^1.0.1",
     "lodash": "^4.16.6",
diff --git a/pipelines/publish-prerelease.yml b/pipelines/publish-prerelease.yml
new file mode 100644
index 0000000..3634d30
--- /dev/null
+++ b/pipelines/publish-prerelease.yml
@@ -0,0 +1,43 @@
+trigger:
+  branches:
+    include:
+    - dev
+pr: none
+
+variables:
+- group: npm-release-credentials
+
+pool:
+  vmImage: 'ubuntu-latest'
+
+steps:
+- task: NodeTool@0
+  displayName: 'Use Node 10.x'
+  inputs:
+    versionSpec: 10.x
+
+- checkout: self
+  clean: true
+  persistCredentials: true
+  fetchDepth: 1
+
+- task: Bash@3
+  name: BumpNpmVersion
+  displayName: Bump NPM Prerelease Version
+  inputs:
+    targetType: filePath
+    filePath: ./scripts/version.sh
+    arguments: 'serverless/serverless-azure-functions'
+  env:
+    NPM_TOKEN: $(NPM_TOKEN)
+    GITHUB_ACCESS_TOKEN: $(GITHUB_ACCESS_TOKEN)
+    SOURCE_BRANCH: $(Build.SourceBranch)
+
+- task: Bash@3
+  displayName: 'Publish serverless-azure-functions to NPM'
+  inputs:
+    targetType: filePath
+    filePath: ./scripts/publish.sh
+    arguments: 'prerelease'
+  env:
+    NPM_TOKEN: $(NPM_TOKEN)
diff --git a/pipelines/publish-release.yml b/pipelines/publish-release.yml
new file mode 100644
index 0000000..b761823
--- /dev/null
+++ b/pipelines/publish-release.yml
@@ -0,0 +1,26 @@
+trigger: none
+
+pool:
+  vmImage: 'ubuntu-latest'
+
+variables:
+- group: npm-release-credentials
+
+steps:
+- task: NodeTool@0
+  displayName: 'Use Node 10.x'
+  inputs:
+    versionSpec: 10.x
+
+- checkout: self
+  clean: true
+  persistCredentials: true
+  fetchDepth: 1
+
+- task: Bash@3
+  displayName: 'Publish serverless-azure-functions to NPM'
+  inputs:
+    targetType: filePath
+    filePath: ./scripts/publish.sh
+  env:
+    NPM_TOKEN: $(NPM_TOKEN)
diff --git a/scripts/publish.sh b/scripts/publish.sh
new file mode 100644
index 0000000..7a4a807
--- /dev/null
+++ b/scripts/publish.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+set -eo pipefail
+
+$(pwd)/scripts/build.sh
+
+# NOTE: auth is taken care of via NPM_TOKEN env variable
+if [ -z "$1" ]; then
+  echo "Publishing 'latest' to NPM...";
+  npm publish
+else
+  echo "Publishing 'prerelease' to NPM...";
+  npm publish --tag=beta
+fi
diff --git a/scripts/version.sh b/scripts/version.sh
new file mode 100644
index 0000000..d6b54eb
--- /dev/null
+++ b/scripts/version.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+set -euo pipefail
+
+# Environment Variables Needed:
+# SOURCE_BRANCH
+# GITHUB_ACCESS_TOKEN
+
+PACKAGE_NAME=$1
+NPM_RELEASE_TYPE=${2-"prerelease"}
+
+# Get full branch name excluding refs/head from the env var SOURCE_BRANCH
+SOURCE_BRANCH_NAME=${SOURCE_BRANCH/refs\/heads\/}
+
+# Configure git to commit as SLS Azure Functions Service Account
+echo "Configuring git to use service account..."
+git config --local user.email "Serverless Azure Functions"
+git config --local user.name "sls-az@microsoft.com"
+
+git pull origin ${SOURCE_BRANCH_NAME}
+git checkout ${SOURCE_BRANCH_NAME}
+echo "Checked out branch: ${SOURCE_BRANCH_NAME}"
+
+NPM_VERSION=`npm version ${NPM_RELEASE_TYPE} -m "release: Update ${NPM_RELEASE_TYPE} version to %s ***NO_CI***"`
+echo "Set NPM version to: ${NPM_VERSION}"
+
+SHA=`git rev-parse HEAD`
+
+git remote add authOrigin https://${GITHUB_ACCESS_TOKEN}@github.com/serverless/serverless-azure-functions.git
+git push authOrigin ${SOURCE_BRANCH_NAME} --tags
+
+echo "Pushed new tag: ${PACKAGE_NAME}-${NPM_VERSION} @ SHA: ${SHA:0:8}"
diff --git a/src/services/armService.ts b/src/services/armService.ts
index d021b36..3abb531 100644
--- a/src/services/armService.ts
+++ b/src/services/armService.ts
@@ -8,6 +8,8 @@ import { ArmDeployment, ArmResourceTemplateGenerator, ArmTemplateType } from "..
 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";
 
 export class ArmService extends BaseService {
   private resourceClient: ResourceManagementClient;
@@ -110,17 +112,56 @@ export class ArmService extends BaseService {
       }
     };
 
+    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}`)
     this.log(`---> Deployment Name: ${this.deploymentName}`)
 
-    const result = await this.resourceClient.deployments.createOrUpdate(this.resourceGroup, this.deploymentName, armDeployment);
+    const result = await this.resourceClient.deployments.createOrUpdate(
+      this.resourceGroup,
+      this.deploymentName,
+      armDeployment
+    );
     this.log("-> ARM deployment complete");
 
     return result;
   }
 
+  /**
+   * Merge parameters and default parameters for comparison with previously deployed template
+   * @param parameters Parameters with specified values
+   * @param defaultParameters Parameters with `type` and `defaultValue`
+   */
+  private mergeDefaultParams(parameters: any, defaultParameters: any) {
+    const mergedParams = {}
+    Object.keys(defaultParameters).forEach((key) => {
+      const defaultParam = defaultParameters[key];
+      mergedParams[key] = {
+        type: defaultParam.type,
+        value: (key in parameters)
+          ?
+          parameters[key].value
+          :
+          defaultParameters[key].defaultValue
+      }
+    });
+    return mergedParams;
+  }
+
   /**
    * Applies sls yaml environment variables into the appSettings section of the function app configuration
    * @param deployment The ARM deployment
diff --git a/src/services/baseService.ts b/src/services/baseService.ts
index dc21b70..43c3808 100644
--- a/src/services/baseService.ts
+++ b/src/services/baseService.ts
@@ -191,10 +191,6 @@ export abstract class BaseService {
     (this.serverless.cli.log as any)(message, entity, options);
   }
 
-  protected prettyPrint(object: any) {
-    this.log(JSON.stringify(object, null, 2));
-  }
-
   /**
    * Get function objects
    */
@@ -210,6 +206,10 @@ export abstract class BaseService {
     return Utils.get(this.options, key, defaultValue);
   }
 
+  protected prettyPrint(object: any) {
+    this.log(JSON.stringify(object, null, 2));
+  }
+
   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
diff --git a/src/services/resourceService.ts b/src/services/resourceService.ts
index 1248e2d..51885e6 100644
--- a/src/services/resourceService.ts
+++ b/src/services/resourceService.ts
@@ -3,6 +3,8 @@ import { ResourceManagementClient } from "@azure/arm-resources";
 import { BaseService } from "./baseService";
 import { Utils } from "../shared/utils";
 import { AzureNamingService } from "./namingService";
+import { ArmDeployment } from "../models/armTemplates";
+import { DeploymentExtended } from "@azure/arm-resources/esm/models";
 
 export class ResourceService extends BaseService {
   private resourceClient: ResourceManagementClient;
@@ -14,11 +16,40 @@ export class ResourceService extends BaseService {
   }
 
   /**
-   * Get all deployments for resource group
+   * Get all deployments for resource group sorted by timestamp (most recent first)
    */
   public async getDeployments() {
     this.log(`Listing deployments for resource group '${this.resourceGroup}':`);
-    return await this.resourceClient.deployments.listByResourceGroup(this.resourceGroup);
+    const deployments = await this.resourceClient.deployments.listByResourceGroup(this.resourceGroup);
+    return deployments.sort((a: DeploymentExtended, b: DeploymentExtended) => {
+      return (a.properties.timestamp > b.properties.timestamp) ? 1 : -1
+    });
+  }
+
+  /**
+   * Get the most recent resource group deployment
+   */
+  public async getLastDeployment() {
+    const deployments = await this.getDeployments();
+    if (deployments && deployments.length) {
+      return deployments[0];
+    }
+  }
+
+  /**
+   * Get template from last resource group deployment
+   */
+  public async getLastDeploymentTemplate(): Promise<ArmDeployment> {
+    const deployment = await this.getLastDeployment();
+    if (!deployment) {
+      return;
+    }
+    const { parameters } = deployment.properties;
+    const { template } = await this.getDeploymentTemplate(deployment.name);
+    return {
+      template,
+      parameters
+    }
   }
 
   /**


Test patch:
diff --git a/src/services/armService.test.ts b/src/services/armService.test.ts
index 825599e..71dedfe 100644
--- a/src/services/armService.test.ts
+++ b/src/services/armService.test.ts
@@ -1,12 +1,13 @@
 import Serverless from "serverless";
 import { MockFactory } from "../test/mockFactory";
 import { ArmService } from "./armService";
-import { ArmResourceTemplate, ArmTemplateType } from "../models/armTemplates";
+import { ArmResourceTemplate, ArmTemplateType, ArmDeployment } from "../models/armTemplates";
 import { ArmTemplateConfig, ServerlessAzureOptions } from "../models/serverless";
 import mockFs from "mock-fs";
 import jsonpath from "jsonpath";
 import { Deployments } from "@azure/arm-resources";
 import { Deployment } from "@azure/arm-resources/esm/models";
+import { ResourceService } from "./resourceService";
 
 describe("Arm Service", () => {
   let sls: Serverless
@@ -29,6 +30,13 @@ describe("Arm Service", () => {
     };
 
     service = createService();
+    ResourceService.prototype.getDeployments = jest.fn(() => MockFactory.createTestDeployments()) as any;
+    ResourceService.prototype.getDeploymentTemplate = jest.fn(() => {
+      return {
+        template: MockFactory.createTestArmTemplate()
+      }
+    }) as any;
+
   })
 
   afterEach(() => {
@@ -151,6 +159,51 @@ describe("Arm Service", () => {
       Deployments.prototype.createOrUpdate = jest.fn(() => Promise.resolve(null));
     });
 
+    it("Does not deploy if previously deployed template is the same", async () => {
+      const deployment: ArmDeployment = {
+        parameters: MockFactory.createTestParameters(false),
+        template: MockFactory.createTestArmTemplate()
+      };
+      await service.deployTemplate(deployment);
+      expect(Deployments.prototype.createOrUpdate).not.toBeCalled()
+    });
+
+    it("Calls deploy if parameters have changed from deployed template", async () => {
+      const deployment: ArmDeployment = {
+        parameters: MockFactory.createTestParameters(false),
+        template: MockFactory.createTestArmTemplate()
+      };
+      deployment.parameters.param1 = "3"
+      await service.deployTemplate(deployment);
+      expect(Deployments.prototype.createOrUpdate).toBeCalled();
+    });
+
+    it("Calls deploy if previously deployed template is different", async () => {
+      ResourceService.prototype.getDeploymentTemplate = jest.fn(() => {
+        return {
+          template: {}
+        }
+      }) as any;
+      const deployment: ArmDeployment = {
+        parameters: MockFactory.createTestParameters(false),
+        template: MockFactory.createTestArmTemplate()
+      };
+      await service.deployTemplate(deployment);
+      expect(Deployments.prototype.createOrUpdate).toBeCalled()
+    });
+
+    it("Calls deploy if running first deployment", async () => {
+      ResourceService.prototype.getDeployments = jest.fn(() => {
+        return []
+      }) as any;
+      const deployment: ArmDeployment = {
+        parameters: MockFactory.createTestParameters(false),
+        template: MockFactory.createTestArmTemplate()
+      };
+      await service.deployTemplate(deployment);
+      expect(Deployments.prototype.createOrUpdate).toBeCalled()
+    });
+
     it("Appends environment variables into app settings of ARM template", async () => {
       const environmentConfig: any = {
         PARAM_1: "1",
diff --git a/src/services/loginService.test.ts b/src/services/loginService.test.ts
index 941b4fa..cfbc7bd 100644
--- a/src/services/loginService.test.ts
+++ b/src/services/loginService.test.ts
@@ -22,7 +22,7 @@ describe("Login Service", () => {
     const emptyObj = { subscriptions: [] };
     Object.defineProperty(nodeauth,
       "interactiveLoginWithAuthResponse",
-      { value: jest.fn(_obj => emptyObj) }
+      { value: jest.fn(() => emptyObj) }
     );
 
     await AzureLoginService.login();
diff --git a/src/services/resourceService.test.ts b/src/services/resourceService.test.ts
index fe46f31..dffaa4e 100644
--- a/src/services/resourceService.test.ts
+++ b/src/services/resourceService.test.ts
@@ -89,6 +89,7 @@ describe("Resource Service", () => {
     const options = MockFactory.createTestServerlessOptions();
     const service = new ResourceService(sls, options);
     const deps = await service.getDeployments();
+    // Make sure deps are in correct order
     expect(deps).toEqual(deployments);
   });
 
diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts
index b44a7e6..9789940 100644
--- a/src/test/mockFactory.ts
+++ b/src/test/mockFactory.ts
@@ -169,25 +169,29 @@ export class MockFactory {
     const result = [];
     const originalTimestamp = +MockFactory.createTestTimestamp();
     for (let i = 0; i < count; i++) {
+      const name = (includeTimestamp) ? `deployment${i + 1}-t${originalTimestamp + i}` : `deployment${i + 1}`;
       result.push(
-        MockFactory.createTestDeployment((includeTimestamp) ? `deployment${i + 1}-t${originalTimestamp + i}` : `deployment${i + 1}`)
+        MockFactory.createTestDeployment(name, i)
       )
     }
     return result as DeploymentsListByResourceGroupResponse
   }
 
-  public static createTestParameters() {
-    return {
+  public static createTestParameters(wrap = true) {
+    return (wrap) ? {
       param1: { value: "1", type: "String" },
       param2: { value: "2", type: "String" },
+    } : {
+      param1: "1",
+      param2: "2",
     }
   }
 
-  public static createTestDeployment(name?: string): DeploymentExtended {
+  public static createTestDeployment(name?: string, second: number = 0): DeploymentExtended {
     return {
       name: name || `deployment1-t${MockFactory.createTestTimestamp()}`,
       properties: {
-        timestamp: new Date(),
+        timestamp: new Date(2019, 1, 1, 0, 0, second),
         parameters: MockFactory.createTestParameters(),
       }
     }
@@ -430,7 +434,7 @@ export class MockFactory {
       handler: "handler.js",
     }
   }
-  
+
   public static createTestBinding() {
     // Only supporting HTTP for now, could support others
     return MockFactory.createTestHttpBinding();
@@ -461,7 +465,7 @@ export class MockFactory {
       name: "item",
       eventhubname: "hello",
       consumerGroup: "$Default",
-      connection: "EventHubsConnection"        
+      connection: "EventHubsConnection"
     }
   }
   public static createTestBindingsObject(name: string = "index.js") {