Repository: serverless/serverless-azure-functions
Pull request #192 (2019-07-05 16:53:45)

fix: Exit process when error logging into Azure


Patch:
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..ab561f3
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,9 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
diff --git a/package-lock.json b/package-lock.json
index e4fb69a..b52ea54 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1794,6 +1794,7 @@
       "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",
@@ -1966,6 +1967,7 @@
       "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",
@@ -2435,7 +2437,8 @@
     "bn.js": {
       "version": "4.11.8",
       "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
-      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
+      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
+      "optional": true
     },
     "body-parser": {
       "version": "1.19.0",
@@ -2540,7 +2543,8 @@
     "brorand": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
-      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
+      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
+      "optional": true
     },
     "browser-process-hrtime": {
       "version": "0.1.3",
@@ -2569,6 +2573,7 @@
       "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",
@@ -2605,6 +2610,7 @@
       "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"
@@ -2701,7 +2707,8 @@
     "buffer-xor": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
-      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
+      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
+      "optional": true
     },
     "builtin-modules": {
       "version": "1.1.1",
@@ -2845,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",
@@ -2883,6 +2895,7 @@
       "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"
@@ -3260,6 +3273,7 @@
       "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",
@@ -3272,6 +3286,7 @@
       "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",
@@ -3291,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",
@@ -3730,6 +3750,7 @@
       "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",
@@ -3791,6 +3812,7 @@
       "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"
       }
@@ -4170,6 +4192,7 @@
       "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"
@@ -4184,6 +4207,7 @@
       "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"
@@ -4713,7 +4737,8 @@
         },
         "ansi-regex": {
           "version": "2.1.1",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "aproba": {
           "version": "1.2.0",
@@ -4731,11 +4756,13 @@
         },
         "balanced-match": {
           "version": "1.0.0",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "bundled": true,
+          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -4748,15 +4775,18 @@
         },
         "code-point-at": {
           "version": "1.1.0",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "concat-map": {
           "version": "0.0.1",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "console-control-strings": {
           "version": "1.1.0",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -4859,7 +4889,8 @@
         },
         "inherits": {
           "version": "2.0.3",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "ini": {
           "version": "1.3.5",
@@ -4869,6 +4900,7 @@
         "is-fullwidth-code-point": {
           "version": "1.0.0",
           "bundled": true,
+          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -4881,17 +4913,20 @@
         "minimatch": {
           "version": "3.0.4",
           "bundled": true,
+          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
         },
         "minimist": {
           "version": "0.0.8",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "minipass": {
           "version": "2.3.5",
           "bundled": true,
+          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.2",
             "yallist": "^3.0.0"
@@ -4908,6 +4943,7 @@
         "mkdirp": {
           "version": "0.5.1",
           "bundled": true,
+          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
@@ -4980,7 +5016,8 @@
         },
         "number-is-nan": {
           "version": "1.0.1",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -4990,6 +5027,7 @@
         "once": {
           "version": "1.4.0",
           "bundled": true,
+          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -5065,7 +5103,8 @@
         },
         "safe-buffer": {
           "version": "5.1.2",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "safer-buffer": {
           "version": "2.1.2",
@@ -5095,6 +5134,7 @@
         "string-width": {
           "version": "1.0.2",
           "bundled": true,
+          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -5112,6 +5152,7 @@
         "strip-ansi": {
           "version": "3.0.1",
           "bundled": true,
+          "optional": true,
           "requires": {
             "ansi-regex": "^2.0.0"
           }
@@ -5150,11 +5191,13 @@
         },
         "wrappy": {
           "version": "1.0.2",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "yallist": {
           "version": "3.0.3",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         }
       }
     },
@@ -5462,6 +5505,7 @@
       "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"
@@ -5471,6 +5515,7 @@
       "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"
@@ -5480,6 +5525,7 @@
       "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",
@@ -5798,7 +5844,8 @@
     "is-extglob": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+      "optional": true
     },
     "is-fullwidth-code-point": {
       "version": "1.0.0",
@@ -7438,7 +7485,8 @@
     "longest": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
-      "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc="
+      "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
+      "optional": true
     },
     "loose-envify": {
       "version": "1.4.0",
@@ -7525,10 +7573,21 @@
         "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",
       "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
+      "optional": true,
       "requires": {
         "hash-base": "^3.0.0",
         "inherits": "^2.0.1",
@@ -7554,6 +7613,7 @@
       "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"
@@ -7644,12 +7704,14 @@
     "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=="
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+      "optional": true
     },
     "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="
+      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
+      "optional": true
     },
     "minimatch": {
       "version": "3.0.4",
@@ -8247,6 +8309,7 @@
       "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",
@@ -8355,6 +8418,7 @@
       "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",
@@ -8557,7 +8621,8 @@
     "prr": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
-      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY="
+      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
+      "optional": true
     },
     "pseudomap": {
       "version": "1.0.2",
@@ -8624,6 +8689,7 @@
       "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"
       }
@@ -9036,6 +9102,7 @@
       "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"
@@ -9425,6 +9492,7 @@
       "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"
@@ -9620,7 +9688,8 @@
     "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=="
+      "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==",
+      "optional": true
     },
     "source-map": {
       "version": "0.7.3",
@@ -10177,7 +10246,8 @@
     "tapable": {
       "version": "0.2.9",
       "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.9.tgz",
-      "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A=="
+      "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==",
+      "optional": true
     },
     "tar-stream": {
       "version": "1.6.2",
@@ -11027,6 +11097,7 @@
       "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"
@@ -11035,7 +11106,8 @@
         "source-map": {
           "version": "0.6.1",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "optional": true
         }
       }
     },
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..48c7f5c 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 {
@@ -13,6 +16,10 @@ export class FunctionAppResource implements ArmResourceTemplateGenerator {
       "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
       "contentVersion": "1.0.0.0",
       "parameters": {
+        "functionAppRunFromPackage": {
+          "defaultValue": "1",
+          "type": "String"
+        },
         "functionAppName": {
           "defaultValue": "",
           "type": "String"
@@ -83,7 +90,7 @@ export class FunctionAppResource implements ArmResourceTemplateGenerator {
                 },
                 {
                   "name": "WEBSITE_RUN_FROM_PACKAGE",
-                  "value": "1"
+                  "value": "[parameters('functionAppRunFromPackage')]"
                 },
                 {
                   "name": "APPINSIGHTS_INSTRUMENTATIONKEY",
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/config.ts b/src/config.ts
index f24db2d..9cf1047 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,6 +1,10 @@
 export const configConstants = {
   bearer: "Bearer ",
-  deploymentArtifactContainer: "deployment-artifacts",
+  deploymentConfig: {
+    container: "deployment-artifacts",
+    rollback: true,
+    runFromBlobUrl: false,
+  },
   functionAppApiPath: "/api/",
   functionAppDomain: ".azurewebsites.net",
   functionsAdminApiPath: "/admin/functions/",
@@ -11,7 +15,6 @@ export const configConstants = {
   logStreamApiPath: "/api/logstream/application/functions/function/",
   masterKeyApiPath: "/api/functions/admin/masterkey",
   providerName: "azure",
-  rollbackEnabled: true,
   scmCommandApiPath: "/api/command",
   scmDomain: ".scm.azurewebsites.net",
   scmVfsPath: "/api/vfs/site/wwwroot/",
diff --git a/src/index.ts b/src/index.ts
index 42c35e9..4ad846a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -16,6 +16,7 @@ import { AzureApimServicePlugin } from "./plugins/apim/apimServicePlugin";
 import { AzureApimFunctionPlugin } from "./plugins/apim/apimFunctionPlugin";
 import { AzureFuncPlugin } from "./plugins/func/azureFunc";
 import { AzureOfflinePlugin } from "./plugins/offline/azureOfflinePlugin"
+import { AzureRollbackPlugin } from "./plugins/rollback/azureRollbackPlugin"
 
 
 export default class AzureIndex {
@@ -34,6 +35,7 @@ export default class AzureIndex {
     this.serverless.pluginManager.addPlugin(AzureApimFunctionPlugin);
     this.serverless.pluginManager.addPlugin(AzureFuncPlugin);
     this.serverless.pluginManager.addPlugin(AzureOfflinePlugin);
+    this.serverless.pluginManager.addPlugin(AzureRollbackPlugin);
   }
 }
 
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 f50d024..727d8ff 100644
--- a/src/models/serverless.ts
+++ b/src/models/serverless.ts
@@ -1,4 +1,5 @@
 import { ApiManagementConfig } from "./apiManagement";
+import Serverless from "serverless";
 
 export interface ArmTemplateConfig {
   file: string;
@@ -26,29 +27,52 @@ export interface FunctionAppConfig extends ResourceConfig {
 export interface DeploymentConfig {
   rollback?: boolean;
   container?: string;
+  runFromBlobUrl?: boolean;
+}
+
+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;
 }
+
+export interface ServerlessCommand {
+  usage: string;
+  lifecycleEvents: string[];
+  options?: {
+    [key: string]: {
+      usage: string;
+      shortcut?: string;
+    };
+  };
+  commands?: ServerlessCommandMap;
+}
+
+export interface ServerlessCommandMap {
+  [command: string]: ServerlessCommand;
+}
+export interface ServerlessAzureOptions extends Serverless.Options {
+  resourceGroup?: string;
+}
diff --git a/src/plugins/deploy/azureDeployPlugin.ts b/src/plugins/deploy/azureDeployPlugin.ts
index 40362ed..02ff649 100644
--- a/src/plugins/deploy/azureDeployPlugin.ts
+++ b/src/plugins/deploy/azureDeployPlugin.ts
@@ -1,6 +1,7 @@
 import Serverless from "serverless";
 import { FunctionAppService } from "../../services/functionAppService";
 import { ResourceService } from "../../services/resourceService";
+import { Utils } from "../../shared/utils";
 
 export class AzureDeployPlugin {
   public hooks: { [eventName: string]: Promise<any> };
@@ -21,6 +22,12 @@ export class AzureDeployPlugin {
               "list"
             ]
           }
+        },
+        options: {
+          "resourceGroup": {
+            usage: "Resource group for the service",
+            shortcut: "g",
+          }
         }
       }
     }
@@ -39,8 +46,14 @@ export class AzureDeployPlugin {
     for (const dep of deployments) {
       stringDeployments += "\n-----------\n"
       stringDeployments += `Name: ${dep.name}\n`
-      stringDeployments += `Timestamp: ${dep.properties.timestamp.getTime()}\n`;
-      stringDeployments += `Datetime: ${dep.properties.timestamp.toISOString()}\n`
+      const timestampFromName = Utils.getTimestampFromName(dep.name);
+      stringDeployments += `Timestamp: ${(timestampFromName) ? timestampFromName : "None"}\n`;
+      stringDeployments += `Datetime: ${(timestampFromName)
+        ?
+        new Date(+timestampFromName).toISOString()
+        :
+        "None"
+      }\n`
     }
     stringDeployments += "-----------\n"
     this.serverless.cli.log(stringDeployments);
@@ -52,8 +65,8 @@ export class AzureDeployPlugin {
     await resourceService.deployResourceGroup();
 
     const functionAppService = new FunctionAppService(this.serverless, this.options);
-
     const functionApp = await functionAppService.deploy();
+
     await functionAppService.uploadFunctions(functionApp);
   }
 }
diff --git a/src/plugins/invoke/azureInvoke.ts b/src/plugins/invoke/azureInvoke.ts
index f8da7e2..f5fa576 100644
--- a/src/plugins/invoke/azureInvoke.ts
+++ b/src/plugins/invoke/azureInvoke.ts
@@ -1,41 +1,72 @@
+import { isAbsolute, join } from "path";
 import Serverless from "serverless";
-import { join, isAbsolute } from "path";
-import AzureProvider from "../../provider/azureProvider";
+import { InvokeService } from "../../services/invokeService";
+import fs from "fs";
+import { ServerlessCommandMap } from "../../models/serverless";
 
 export class AzureInvoke {
   public hooks: { [eventName: string]: Promise<any> };
-  private provider: AzureProvider;
-
+  private commands: ServerlessCommandMap;
+  private invokeService: InvokeService;
   public constructor(private serverless: Serverless, private options: Serverless.Options) {
-    this.provider = (this.serverless.getProvider("azure") as any) as AzureProvider;
     const path = this.options["path"];
-
+    
     if (path) {
       const absolutePath = isAbsolute(path)
         ? path
         : join(this.serverless.config.servicePath, path);
+      this.serverless.cli.log(this.serverless.config.servicePath);
+      this.serverless.cli.log(path);
 
-      if (!this.serverless.utils.fileExistsSync(absolutePath)) {
+      if (!fs.existsSync(absolutePath)) {
         throw new Error("The file you provided does not exist.");
       }
-      this.options["data"] = this.serverless.utils.readFileSync(absolutePath);
+      this.options["data"] = fs.readFileSync(absolutePath).toString();
+    }
+
+    this.commands = {
+      invoke: {
+        usage: "Invoke command",
+        lifecycleEvents: ["invoke"],
+        options: {
+          function: {
+            usage: "Function to call",
+            shortcut: "f",
+          },
+          path: {
+            usage: "Path to file to put in body",
+            shortcut: "p"
+          },
+          data: {
+            usage: "Data string for body of request",
+            shortcut: "d"
+          },
+          method: {
+            usage: "HTTP method (Default is GET)",
+            shortcut: "m"
+          }
+        }
+      }
     }
 
     this.hooks = {
-      "before:invoke:invoke": this.provider.getAdminKey.bind(this),
       "invoke:invoke": this.invoke.bind(this)
     };
   }
-
+  
   private async invoke() {
-    const func = this.options.function;
-    const functionObject = this.serverless.service.getFunction(func);
-    const eventType = Object.keys(functionObject["events"][0])[0];
-
-    if (!this.options["data"]) {
-      this.options["data"] = {};
+    const functionName = this.options["function"];
+    const data = this.options["data"];
+    const method = this.options["method"] || "GET";
+    if (!functionName) {
+      this.serverless.cli.log("Need to provide a name of function to invoke");
+      return;
     }
 
-    return this.provider.invoke(func, eventType, this.options["data"]);
+    this.invokeService = new InvokeService(this.serverless, this.options);
+    const response =  await this.invokeService.invoke(method, functionName, data);
+    if(response){
+      this.serverless.cli.log(JSON.stringify(response.data));
+    }
   }
-}
+}
\ No newline at end of file
diff --git a/src/plugins/login/loginPlugin.ts b/src/plugins/login/loginPlugin.ts
index 083831f..6a1f488 100644
--- a/src/plugins/login/loginPlugin.ts
+++ b/src/plugins/login/loginPlugin.ts
@@ -12,6 +12,8 @@ export class AzureLoginPlugin {
     this.hooks = {
       "before:package:initialize": this.login.bind(this),
       "before:deploy:list:list": this.login.bind(this),
+      "before:invoke:invoke": this.login.bind(this),
+      "before:rollback:rollback": this.login.bind(this),
     };
   }
 
@@ -33,6 +35,7 @@ export class AzureLoginPlugin {
     catch (e) {
       this.serverless.cli.log("Error logging into azure");
       this.serverless.cli.log(`${e}`);
+      process.exit(0);
     }
   }
 }
\ No newline at end of file
diff --git a/src/plugins/offline/azureOfflinePlugin.ts b/src/plugins/offline/azureOfflinePlugin.ts
index 2a96789..0615edb 100644
--- a/src/plugins/offline/azureOfflinePlugin.ts
+++ b/src/plugins/offline/azureOfflinePlugin.ts
@@ -51,4 +51,4 @@ export class AzureOfflinePlugin {
   private async azureOfflineCleanup(){
     this.offlineService.cleanup();
   }
-}
\ No newline at end of file
+}
diff --git a/src/plugins/rollback/azureRollbackPlugin.ts b/src/plugins/rollback/azureRollbackPlugin.ts
new file mode 100644
index 0000000..a3c24e3
--- /dev/null
+++ b/src/plugins/rollback/azureRollbackPlugin.ts
@@ -0,0 +1,20 @@
+import Serverless from "serverless";
+import { RollbackService } from "../../services/rollbackService";
+
+/**
+ * Plugin for rolling back Function App Service to previous deployment
+ */
+export class AzureRollbackPlugin {
+  public hooks: { [eventName: string]: Promise<any> };
+
+  public constructor(private serverless: Serverless, private options: Serverless.Options) {
+    this.hooks = {
+      "rollback:rollback": this.rollback.bind(this)
+    };
+  }
+
+  private async rollback() {
+    const service = new RollbackService(this.serverless, this.options);
+    await service.rollback();
+  }
+}
diff --git a/src/services/armService.ts b/src/services/armService.ts
index 90feef5..d021b36 100644
--- a/src/services/armService.ts
+++ b/src/services/armService.ts
@@ -1,19 +1,19 @@
-import Serverless from "serverless";
-import { Deployment, DeploymentExtended } from "@azure/arm-resources/esm/models";
-import { BaseService } from "./baseService";
 import { ResourceManagementClient } from "@azure/arm-resources";
-import { Guard } from "../shared/guard";
-import { ServerlessAzureConfig, ArmTemplateConfig } from "../models/serverless";
-import { ArmDeployment, ArmResourceTemplateGenerator, ArmTemplateType } from "../models/armTemplates";
+import { Deployment, DeploymentExtended } from "@azure/arm-resources/esm/models";
 import fs from "fs";
-import path from "path";
 import jsonpath from "jsonpath";
+import path from "path";
+import Serverless from "serverless";
+import { ArmDeployment, ArmResourceTemplateGenerator, ArmTemplateType } from "../models/armTemplates";
+import { ArmTemplateConfig, ServerlessAzureConfig, ServerlessAzureOptions } from "../models/serverless";
+import { Guard } from "../shared/guard";
+import { BaseService } from "./baseService";
 
 export class ArmService extends BaseService {
   private resourceClient: ResourceManagementClient;
 
-  public constructor(serverless: Serverless) {
-    super(serverless);
+  public constructor(serverless: Serverless, options: ServerlessAzureOptions) {
+    super(serverless, options);
     this.resourceClient = new ResourceManagementClient(this.credentials, this.subscriptionId);
   }
 
@@ -97,7 +97,7 @@ export class ArmService extends BaseService {
     Object.keys(deployment.parameters).forEach((key) => {
       const parameterValue = deployment.parameters[key];
       if (parameterValue) {
-        deploymentParameters[key] = { value: deployment.parameters[key] };
+        deploymentParameters[key] = { value: parameterValue }
       }
     });
 
@@ -149,4 +149,4 @@ export class ArmService extends BaseService {
       });
     }
   }
-}
\ No newline at end of file
+}
diff --git a/src/services/azureBlobStorageService.ts b/src/services/azureBlobStorageService.ts
index 6764d2b..ba04917 100644
--- a/src/services/azureBlobStorageService.ts
+++ b/src/services/azureBlobStorageService.ts
@@ -1,11 +1,18 @@
 import { StorageAccounts, StorageManagementClientContext } from "@azure/arm-storage";
-import { Aborter, BlobSASPermissions, BlockBlobURL, ContainerURL, generateBlobSASQueryParameters,SASProtocol,
-  ServiceURL, SharedKeyCredential, StorageURL, TokenCredential, uploadFileToBlockBlob } from "@azure/storage-blob";
+import { Aborter, BlobSASPermissions, BlockBlobURL, ContainerURL,
+  generateBlobSASQueryParameters, SASProtocol, ServiceURL, SharedKeyCredential,
+  StorageURL, TokenCredential, uploadFileToBlockBlob, downloadBlobToBuffer } from "@azure/storage-blob";
+import fs from "fs";
 import Serverless from "serverless";
 import { Guard } from "../shared/guard";
 import { BaseService } from "./baseService";
 import { AzureLoginService } from "./loginService";
 
+/**
+ * Type of authentication with Azure Storage
+ * @member SharedKey - Retrieve and use a Shared Key for Azure Blob BStorage
+ * @member Token - Retrieve and use an Access Token to authenticate with Azure Blob Storage
+ */
 export enum AzureStorageAuthType {
   SharedKey,
   Token
@@ -35,6 +42,9 @@ export class AzureBlobStorageService extends BaseService {
    * to perform any operation with the service
    */
   public async initialize() {
+    if (this.storageCredential) {
+      return;
+    }
     this.storageCredential = (this.authType === AzureStorageAuthType.SharedKey) 
       ?
       new SharedKeyCredential(this.storageAccountName, await this.getKey())
@@ -59,6 +69,31 @@ export class AzureBlobStorageService extends BaseService {
     await uploadFileToBlockBlob(Aborter.none, path, this.getBlockBlobURL(containerName, name));
     this.log("Finished uploading blob");
   };
+
+  /**
+   * Download blob to file
+   * https://github.com/Azure/azure-storage-js/blob/master/blob/samples/highlevel.sample.js#L82-L97
+   * @param containerName Container containing blob to download
+   * @param blobName Blob to download
+   * @param targetPath Path to which blob will be downloaded
+   */
+  public async downloadBinary(containerName: string, blobName: string, targetPath: string) {
+    const blockBlobUrl = this.getBlockBlobURL(containerName, blobName);
+    const props = await blockBlobUrl.getProperties(Aborter.none);
+    const buffer = Buffer.alloc(props.contentLength);
+    await downloadBlobToBuffer(
+      Aborter.timeout(30 * 60 * 1000),
+      buffer,
+      blockBlobUrl,
+      0,
+      undefined,
+      {
+        blockSize: 4 * 1024 * 1024, // 4MB block size
+        parallelism: 20, // 20 concurrency
+      }
+    );    
+    fs.writeFileSync(targetPath, buffer, "binary");
+  }
   
   /**
    * Delete a blob from Azure Blob Storage
diff --git a/src/services/baseService.ts b/src/services/baseService.ts
index b99a5ce..f7f1e79 100644
--- a/src/services/baseService.ts
+++ b/src/services/baseService.ts
@@ -2,30 +2,31 @@ import axios from "axios";
 import fs from "fs";
 import request from "request";
 import Serverless from "serverless";
+import { ServerlessAzureOptions } from "../models/serverless";
 import { StorageAccountResource } from "../armTemplates/resources/storageAccount";
 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: Serverless.Options = { stage: null, region: null },
-    authenticate: boolean = true,
+    protected options: ServerlessAzureOptions = { stage: null, region: null },
+    authenticate: boolean = true
   ) {
     Guard.null(serverless);
-
     this.setDefaultValues();
 
     this.baseUrl = "https://management.azure.com";
@@ -36,72 +37,115 @@ 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}`
+      );
     }
   }
 
+  /**
+   * Name of Azure Region for deployment
+   */
   public getRegion(): string {
-    return this.options.region || this.serverless.service.provider.region;
+    return this.options.region || this.config.provider.region;
   }
-  
+
+  /**
+   * Name of current deployment stage
+   */
   public getStage(): string {
-    return this.options.stage || this.serverless.service.provider.stage;
+    return this.options.stage || this.config.provider.stage;
   }
 
+  public getPrefix(): string {
+    return this.config.provider.prefix;
+  }
+
+  /**
+   * Name of current resource group
+   */
   public getResourceGroupName(): string {
-    return this.options["resourceGroup"]
-      || this.serverless.service.provider["resourceGroup"]
-      || `${this.config.provider.prefix}-${this.getRegion()}-${this.getStage()}-${this.serviceName}-rg`;
+    const regionName = Utils.createShortAzureRegionName(this.getRegion());
+    const stageName = Utils.createShortStageName(this.getStage());
+
+    return this.options.resourceGroup
+      || this.config.provider.resourceGroup
+      || `${this.getPrefix()}-${regionName}-${stageName}-${this.serviceName}-rg`;
   }
 
+  /**
+   * Deployment config from `serverless.yml` or default.
+   * Defaults can be found in the `config.ts` file
+   */
   public getDeploymentConfig(): DeploymentConfig {
     const providedConfig = this.serverless["deploy"] as DeploymentConfig;
-    const config = providedConfig || {
-      rollback: configConstants.rollbackEnabled,
-      container: configConstants.deploymentArtifactContainer,
+    return {
+      ...configConstants.deploymentConfig,
+      ...providedConfig,
     }
-    return config;
   }
 
+  /**
+   * Name of current ARM deployment
+   */
   public getDeploymentName(): string {
-    const name = this.serverless.service.provider["deploymentName"] || `${this.resourceGroup}-deployment`
+    const name = this.config.provider.deploymentName || `${this.resourceGroup}-deployment`;
     return this.rollbackConfiguredName(name);
   }
 
+  /**
+   * Name of Function App Service
+   */
   public getServiceName(): string {
     return this.serverless.service["service"];
   }
 
+  /**
+   * Get rollback-configured artifact name. Contains `-t{timestamp}`
+   * Takes name of deployment and replaces `rg-deployment` or `deployment` with `artifact`
+   */
+  protected getArtifactName(deploymentName: string): string {
+    return `${deploymentName
+      .replace("rg-deployment", "artifact")
+      .replace("deployment", "artifact")}.zip`;
+  }
+
   /**
    * Get the access token from credentials token cache
    */
-  protected getAccessToken(): string{
+  protected getAccessToken(): string {
     return (this.credentials.tokenCache as any)._entries[0].accessToken;
   }
-
+  
   /**
    * Sends an API request using axios HTTP library
    * @param method The HTTP method
    * @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);
@@ -114,36 +158,45 @@ 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);
-        }));
+        })
+      );
     });
   }
 
+  /**
+   * Log message to Serverless CLI
+   * @param message Message to log
+   */
   protected log(message: string) {
     this.serverless.cli.log(message);
   }
 
+  /**
+   * Get function objects
+   */
   protected slsFunctions() {
     return this.serverless.service["functions"];
   }
 
   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";
     }
 
@@ -161,7 +214,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;
   }
 
   /**
@@ -176,4 +231,4 @@ export abstract class BaseService {
     }
     return timestamp;
   }
-}
+}
\ No newline at end of file
diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts
index ab5e69b..7f8f820 100644
--- a/src/services/functionAppService.ts
+++ b/src/services/functionAppService.ts
@@ -14,18 +14,20 @@ import { BaseService } from "./baseService";
 export class FunctionAppService extends BaseService {
   private webClient: WebSiteManagementClient;
   private blobService: AzureBlobStorageService;
-  private functionZipFile: string;
 
   public constructor(serverless: Serverless, options: Serverless.Options) {
     super(serverless, options);
+
     this.webClient = new WebSiteManagementClient(this.credentials, this.subscriptionId);
     this.blobService = new AzureBlobStorageService(serverless, options);
-    this.functionZipFile = this.getFunctionZipFile();
   }
 
   public async get(): Promise<Site> {
     const response: any = await this.webClient.webApps.get(this.resourceGroup, FunctionAppResource.getResourceName(this.config));
     if (response.error && (response.error.code === "ResourceNotFound" || response.error.code === "ResourceGroupNotFound")) {
+      this.serverless.cli.log(this.resourceGroup);
+      this.serverless.cli.log(FunctionAppResource.getResourceName(this.config));
+      this.serverless.cli.log(JSON.stringify(response));
       return null;
     }
 
@@ -114,10 +116,12 @@ export class FunctionAppService extends BaseService {
   public async uploadFunctions(functionApp: Site): Promise<any> {
     Guard.null(functionApp, "functionApp");
 
-    this.log("Deploying serverless functions...");    
+    this.log("Deploying serverless functions...");
 
-    await this.uploadZippedArfifactToFunctionApp(functionApp);
-    await this.uploadZippedArtifactToBlobStorage();
+    const functionZipFile = this.getFunctionZipFile();
+    const uploadFunctionApp = this.uploadZippedArfifactToFunctionApp(functionApp, functionZipFile);
+    const uploadBlobStorage = this.uploadZippedArtifactToBlobStorage(functionZipFile);
+    await Promise.all([uploadFunctionApp, uploadBlobStorage]);
   }
 
   /**
@@ -127,27 +131,27 @@ export class FunctionAppService extends BaseService {
   public async deploy() {
     this.log(`Creating function app: ${this.serviceName}`);
 
-    const armService = new ArmService(this.serverless);
+    const armService = new ArmService(this.serverless, this.options);
     let deployment: ArmDeployment = this.config.provider.armTemplate
       ? await armService.createDeploymentFromConfig(this.config.provider.armTemplate)
       : await armService.createDeploymentFromType(this.config.provider.type || "consumption");
 
     await armService.deployTemplate(deployment);
 
-    // Return function app 
+    // Return function app
     return await this.get();
   }
 
-  private async uploadZippedArfifactToFunctionApp(functionApp) {
+  public async uploadZippedArfifactToFunctionApp(functionApp: Site, functionZipFile: string) {
     const scmDomain = this.getScmDomain(functionApp);
 
     this.log(`Deploying zip file to function app: ${functionApp.name}`);
 
-    if (!(this.functionZipFile && fs.existsSync(this.functionZipFile))) {
+    if (!(functionZipFile && fs.existsSync(functionZipFile))) {
       throw new Error("No zip file found for function app");
     }
 
-    this.log(`-> Deploying service package @ ${this.functionZipFile}`);
+    this.log(`-> Deploying service package @ ${functionZipFile}`);
 
     // https://github.com/projectkudu/kudu/wiki/Deploying-from-a-zip-file-or-url
     const requestOptions = {
@@ -161,8 +165,8 @@ export class FunctionAppService extends BaseService {
       }
     };
 
-    await this.sendFile(requestOptions, this.functionZipFile);
-    
+    await this.sendFile(requestOptions, functionZipFile);
+
     this.log("-> Function package uploaded successfully");
     const serverlessFunctions = this.serverless.service.getAllFunctions();
     const deployedFunctions = await this.listFunctions(functionApp);
@@ -182,13 +186,13 @@ export class FunctionAppService extends BaseService {
   }
 
   /**
-   * Uploads artifact file to blob storage container 
+   * Uploads artifact file to blob storage container
    */
-  private async uploadZippedArtifactToBlobStorage() {
+  private async uploadZippedArtifactToBlobStorage(functionZipFile: string) {
     await this.blobService.initialize();
     await this.blobService.createContainerIfNotExists(this.deploymentConfig.container);
     await this.blobService.uploadFile(
-      this.functionZipFile,
+      functionZipFile,
       this.deploymentConfig.container,
       this.getArtifactName(this.deploymentName),
     );
@@ -213,7 +217,7 @@ export class FunctionAppService extends BaseService {
     return `${deploymentName.replace("rg-deployment", "artifact")}.zip`;
   }
 
-  private getFunctionHttpTriggerConfig(functionApp: Site, functionConfig: FunctionEnvelope): FunctionAppHttpTriggerConfig {
+  public getFunctionHttpTriggerConfig(functionApp: Site, functionConfig: FunctionEnvelope): FunctionAppHttpTriggerConfig {
     const httpTrigger = functionConfig.config.bindings.find((binding) => {
       return binding.type === "httpTrigger";
     });
diff --git a/src/services/invokeService.ts b/src/services/invokeService.ts
new file mode 100644
index 0000000..0b14fbc
--- /dev/null
+++ b/src/services/invokeService.ts
@@ -0,0 +1,87 @@
+import { BaseService } from "./baseService"
+import Serverless from "serverless";
+import axios from "axios";
+import { FunctionAppService } from "./functionAppService";
+
+export class InvokeService extends BaseService {
+  public functionAppService: FunctionAppService;
+
+  public constructor(serverless: Serverless, options: Serverless.Options) {
+    super(serverless, options);
+    this.functionAppService = new FunctionAppService(serverless, options);
+  }
+
+  /**
+   * Invoke an Azure Function
+   * @param method HTTP method
+   * @param functionName Name of function to invoke
+   * @param data Data to use as body or query params
+   */
+  public async invoke(method: string, functionName: string, data?: any){
+
+    const functionObject = this.slsFunctions()[functionName];
+    /* accesses the admin key */
+    if (!functionObject) {
+      this.serverless.cli.log(`Function ${functionName} does not exist`);
+      return;
+    }
+    
+    const eventType = Object.keys(functionObject["events"][0])[0];
+
+    if (eventType !== "http") {
+      this.log("Needs to be an http function");
+      return;
+    }
+    
+    const functionApp = await this.functionAppService.get();
+    const functionConfig = await this.functionAppService.getFunction(functionApp, functionName);
+    const httpConfig = this.functionAppService.getFunctionHttpTriggerConfig(functionApp, functionConfig);
+    let url  = "http://" + httpConfig.url;
+
+    if (method === "GET" && data) {
+      const queryString = this.getQueryString(data);
+      url += `?${queryString}`
+    }   
+
+    this.log(url);
+    const options = await this.getOptions(method, data);
+    this.log(`Invoking function ${functionName} with ${method} request`);
+    return await axios(url, options);
+  }
+
+  private getQueryString(eventData: any) {
+    if (typeof eventData === "string") {
+      try {
+        eventData = JSON.parse(eventData);
+      }
+      catch (error) {
+        return Promise.reject("The specified input data isn't a valid JSON string. " +
+        "Please correct it and try invoking the function again.");
+      }
+    }
+    return encodeURIComponent(Object.keys(eventData)
+      .map((key) => `${key}=${eventData[key]}`)
+      .join("&"));
+  }
+
+  /**
+ * Get options object  
+ * @param method The method used (POST or GET)
+ * @param data Data to use as body or query params
+ */
+  private async getOptions(method: string, data?: any) {
+  
+    const functionsAdminKey = await this.functionAppService.getMasterKey();
+    const functionApp = await this.functionAppService.get();
+    const options: any = {
+      host: functionApp.defaultHostName,
+      headers: {
+        "x-functions-key": functionsAdminKey
+      },
+      method,
+      data,
+    };
+    
+    return options;
+  }
+} 
\ No newline at end of file
diff --git a/src/services/resourceService.ts b/src/services/resourceService.ts
index e36dd90..4b7dca0 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;
@@ -11,16 +12,27 @@ export class ResourceService extends BaseService {
     this.resourceClient = new ResourceManagementClient(this.credentials, this.subscriptionId);
   }
 
+  /**
+   * Get all deployments for resource group
+   */
   public async getDeployments() {
     this.log(`Listing deployments for resource group '${this.resourceGroup}':`);
     return await this.resourceClient.deployments.listByResourceGroup(this.resourceGroup);
   }
 
+  /**
+   * Get ARM template for previous deployment
+   * @param deploymentName Name of deployment
+   */
+  public async getDeploymentTemplate(deploymentName: string) {
+    return await this.resourceClient.deployments.exportTemplate(this.resourceGroup, deploymentName);
+  }
+
   public async deployResourceGroup() {
     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/services/rollbackService.ts b/src/services/rollbackService.ts
new file mode 100644
index 0000000..0adb0bd
--- /dev/null
+++ b/src/services/rollbackService.ts
@@ -0,0 +1,150 @@
+import { DeploymentExtended } from "@azure/arm-resources/esm/models";
+import path from "path";
+import Serverless from "serverless";
+import { Utils } from "../shared/utils";
+import { ArmService } from "./armService";
+import { AzureBlobStorageService } from "./azureBlobStorageService";
+import { BaseService } from "./baseService";
+import { FunctionAppService } from "./functionAppService";
+import { ResourceService } from "./resourceService";
+import { ArmDeployment } from "../models/armTemplates";
+
+/**
+ * Services for the Rollback Plugin
+ */
+export class RollbackService extends BaseService {
+  private resourceService: ResourceService;
+  private blobService: AzureBlobStorageService;
+
+  /**
+   * Initialize rollback service, including authentication and initialization
+   * of a `ResourceService`
+   * @param serverless Serverless object
+   * @param options Serverless CLI options
+   */
+  public constructor(serverless: Serverless, options: Serverless.Options) {
+    super(serverless, options);
+    this.resourceService = new ResourceService(serverless, options);
+    this.blobService = new AzureBlobStorageService(serverless, options);
+  }
+
+  /**
+   * Rolls back the function app. If `timestamp` present in `options`,
+   * function app will be rolled back directly to that timestamp. Otherwise,
+   * function app will be rolled back to the deployment previous to the most recent
+   */
+  public async rollback() {
+    // Get deployment as specified by timestamp
+    const deployment = await this.getDeployment();
+    if (!deployment) {
+      return;
+    }
+    // Name of artifact in blob storage
+    const artifactName = this.getArtifactName(deployment.name);
+    // Redeploy resource group (includes SAS token URL if running from blob URL)
+    await this.redeployDeployment(deployment, artifactName);
+  }
+
+  /**
+   * Re-deploy a previous deployment of a resource group
+   * @param deployment Previous deployment retrieved from Azure SDK
+   * @param artifactName Name of zipped artifact in blob storage associated with deployment
+   */
+  private async redeployDeployment(deployment: DeploymentExtended, artifactName: string) {
+    const armService = new ArmService(this.serverless, this.options);
+    const armDeployment = await this.convertToArmDeployment(deployment);
+    // Initialize blob service for either creating SAS token or downloading artifact to uplod to function app
+    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
+      )
+    }
+    await armService.deployTemplate(armDeployment);
+    /**
+     * Cannot use an `else` statement just because deploying the artifact
+     * depends on `deployTemplate` already being called
+     */
+    if (!this.deploymentConfig.runFromBlobUrl) {
+      const artifactPath = await this.downloadArtifact(artifactName);
+      await this.redeployArtifact(artifactPath);
+    }
+  }
+
+  /**
+   * Convert previous deployment to ArmDeployment
+   * @param deployment Previous deployment retrieved from Azure SDK
+   */
+  private async convertToArmDeployment(deployment: DeploymentExtended): Promise<ArmDeployment> {
+    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,
+    }
+  }
+
+  /**
+   * Deploy zipped artifact to function app
+   * @param artifactPath Path to downloaded zipped artifact
+   */
+  private async redeployArtifact(artifactPath: string) {
+    const functionAppService = new FunctionAppService(this.serverless, this.options);
+    const functionApp = await functionAppService.get();
+    await functionAppService.uploadZippedArfifactToFunctionApp(functionApp, artifactPath);
+  }
+
+  /**
+   * Get deployment specified by timestamp in Serverless options
+   */
+  private async getDeployment(): Promise<DeploymentExtended> {
+    let timestamp = Utils.get(this.options, "timestamp");
+    if (!timestamp) {
+      this.log("Need to specify a timestamp for rollback. Run `sls deploy list` to see timestamps of deployments");
+      this.log("Example usage:\n\nsls rollback -t 1562014362");
+      return null;
+    }
+    const deployments = await this.getArmDeploymentsByTimestamp();
+    const deployment = deployments.get(timestamp);
+    if (!deployment) {
+      this.log(`Couldn't find deployment with timestamp: ${timestamp}`);
+      this.log(`Timestamps: ${Array.from(deployments.keys()).map((key) => `\n${key}`)}`)
+    }
+    return deployment;
+  }
+
+  /**
+   * Download zipped function app artifact from blob storage corresponding to the specified deployment
+   * @param artifactName Name of artifact to download
+   */
+  private async downloadArtifact(artifactName: string): Promise<string> {
+    const artifactPath = path.join(this.serverless.config.servicePath, ".serverless", artifactName)
+    await this.blobService.downloadBinary(
+      this.deploymentConfig.container,
+      artifactName,
+      artifactPath
+    );
+    return artifactPath;
+  }
+
+  /**
+   * Get all deployments of a resource group indexed by Unix timestamp string
+   */
+  private async getArmDeploymentsByTimestamp(): Promise<Map<string, DeploymentExtended>> {
+    const result = new Map<string, DeploymentExtended>();
+    const armDeployments = await this.resourceService.getDeployments();
+    for (const armDeployment of armDeployments) {
+      const timestamp = Utils.getTimestampFromName(armDeployment.name);
+      if (timestamp) {
+        result.set(timestamp, armDeployment);
+      }
+    }
+    return result;
+  }
+}
diff --git a/src/shared/utils.ts b/src/shared/utils.ts
index 8777c34..0fa71d8 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
@@ -117,13 +118,6 @@ export class Utils {
     return JSON.stringify(json, null, 2);
   }
 
-  public static interpolateFile(sls: Serverless, path: string, params: Map<string, string>) {
-    const template = sls.utils.readFileSync(path);
-    const names = params.keys();
-    const vals = params.values();
-    return new Function(...names, `return \`${template}\`;`)(...vals);
-  }
-
   /**
    * Take the first `substringSize` characters from each string and return as one string
    * @param substringSize Size of substring to take from beginning of each string
@@ -136,4 +130,86 @@ export class Utils {
     }
     return result;
   }
+
+  /**
+   * Creates a short name to be used for state name abbreviation
+   * @param stageName The stage name
+   */
+  public static createShortStageName(stageName: string) {
+    Guard.empty(stageName);
+
+    const stageMap = {
+      "dogfood": "df",
+      "production": "prod",
+      "development": "dev",
+      "testing": "test"
+    };
+
+    return this.createShortName(stageName, stageMap);
+  }
+
+  /**
+   * Gets the normalized region name from long name (ex. West US 2 -> westus2)
+   * @param regionName The region name
+   */
+  public static getNormalizedRegionName(regionName: string) {
+    Guard.empty(regionName);
+    return regionName.replace(/\W/g, "").toLowerCase();
+  }
+
+  /**
+   * Creates a short name for an azure region
+   * @param regionName The azure region name
+   */
+  public static createShortAzureRegionName(regionName: string) {
+    Guard.empty(regionName);
+
+    const locationMap = {
+      "north": "n",
+      "south": "s",
+      "east": "e",
+      "west": "w",
+      "central": "c",
+    };
+
+    return this.createShortName(regionName, locationMap);
+  }
+
+  /**
+   * Creates a short name from a long name based on a well-known string map
+   * @param longName The long name to replace
+   * @param wellKnownMap A well known map of long terms to short abbreviations
+   */
+  private static createShortName(longName: string, wellKnownMap: { [key: string]: string }) {
+    Guard.empty(longName);
+    Guard.null(wellKnownMap);
+
+    const pattern = `(${Object.keys(wellKnownMap).join("|")})`;
+    const regex = new RegExp(pattern, "g");
+
+    return longName
+      .replace(/\W+/g, "")
+      .toLowerCase()
+      .split(regex)
+      .map((part) => {
+        return wellKnownMap[part] || part.substr(0, 3);
+      })
+      .join("");
+  }
+
+  public static get(object: any, key: string, defaultValue?: any) {
+    if (key in object) {
+      return object[key];
+    }
+    return defaultValue
+  }
+
+  public static getTimestampFromName(name: string): string {
+    const regex = /.*-t([0-9]+)/;
+    const match = name.match(regex);
+    if (!match || match.length < 2) {
+      return null;
+    }
+    return match[1];
+  }
 }


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/plugins/deploy/azureDeployPlugin.test.ts b/src/plugins/deploy/azureDeployPlugin.test.ts
index f4ae441..3813675 100644
--- a/src/plugins/deploy/azureDeployPlugin.test.ts
+++ b/src/plugins/deploy/azureDeployPlugin.test.ts
@@ -36,7 +36,29 @@ describe("Deploy plugin", () => {
     expect(uploadFunctions).toBeCalledWith(functionAppStub);
   });
 
-  it("lists deployments", async () => {
+  it("lists deployments with timestamps", async () => {
+    const deployments = MockFactory.createTestDeployments(5, true);
+    ResourceService.prototype.getDeployments = jest.fn(() => Promise.resolve(deployments));
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    const plugin = new AzureDeployPlugin(sls, options);
+    await invokeHook(plugin, "deploy:list:list");
+    let expectedLogStatement = "\n\nDeployments";
+    const originalTimestamp = +MockFactory.createTestTimestamp();
+    let i = 0
+    for (const dep of deployments) {
+      const timestamp = originalTimestamp + i
+      expectedLogStatement += "\n-----------\n"
+      expectedLogStatement += `Name: ${dep.name}\n`
+      expectedLogStatement += `Timestamp: ${timestamp}\n`;
+      expectedLogStatement += `Datetime: ${new Date(timestamp).toISOString()}\n`
+      i++
+    }
+    expectedLogStatement += "-----------\n"
+    expect(sls.cli.log).lastCalledWith(expectedLogStatement);
+  });
+
+  it("lists deployments without timestamps", async () => {
     const deployments = MockFactory.createTestDeployments();
     ResourceService.prototype.getDeployments = jest.fn(() => Promise.resolve(deployments));
     const sls = MockFactory.createTestServerless();
@@ -47,8 +69,8 @@ describe("Deploy plugin", () => {
     for (const dep of deployments) {
       expectedLogStatement += "\n-----------\n"
       expectedLogStatement += `Name: ${dep.name}\n`
-      expectedLogStatement += `Timestamp: ${dep.properties.timestamp.getTime()}\n`;
-      expectedLogStatement += `Datetime: ${dep.properties.timestamp.toISOString()}\n`
+      expectedLogStatement += "Timestamp: None\n";
+      expectedLogStatement += "Datetime: None\n"
     }
     expectedLogStatement += "-----------\n"
     expect(sls.cli.log).lastCalledWith(expectedLogStatement);
diff --git a/src/plugins/invoke/azureInvoke.test.ts b/src/plugins/invoke/azureInvoke.test.ts
new file mode 100644
index 0000000..982f562
--- /dev/null
+++ b/src/plugins/invoke/azureInvoke.test.ts
@@ -0,0 +1,97 @@
+import { MockFactory } from "../../test/mockFactory";
+import { invokeHook } from "../../test/utils";
+import mockFs from "mock-fs";
+import { AzureInvoke } from "./azureInvoke";
+jest.mock("../../services/functionAppService");
+jest.mock("../../services/resourceService");
+jest.mock("../../services/invokeService");
+import { InvokeService } from "../../services/invokeService";
+
+describe("Azure Invoke Plugin", () => {
+  const fileContent =  JSON.stringify({
+    name: "Azure-Test",
+  });
+  afterEach(() => {
+    jest.resetAllMocks();
+  })
+
+  beforeAll(() => {
+    mockFs({
+      "testFile.json": fileContent,
+    }, { createCwd: true, createTmp: true });
+  });
+  afterAll(() => {
+    mockFs.restore();
+  });
+
+  it("calls invoke hook", async () => {
+    const expectedResult = { data: "test" };
+    const invoke = jest.fn(() => expectedResult);
+    InvokeService.prototype.invoke = invoke as any;
+
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    options["function"] = "testApp";
+    options["data"] = "{\"name\": \"AzureTest\"}";
+    options["method"] = "GET";
+
+    const plugin = new AzureInvoke(sls, options);
+    await invokeHook(plugin, "invoke:invoke");
+    expect(invoke).toBeCalledWith(options["method"], options["function"], options["data"]);
+    expect(sls.cli.log).toBeCalledWith(JSON.stringify(expectedResult.data));
+  });
+
+  it("calls the invoke hook with file path", async () => {
+    const expectedResult = { data: "test" };
+    const invoke = jest.fn(() => expectedResult);
+    InvokeService.prototype.invoke = invoke as any;
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    options["function"] = "testApp";
+    options["path"] = "testFile.json";
+    options["method"] = "GET";
+    const plugin = new AzureInvoke(sls, options);
+    await invokeHook(plugin, "invoke:invoke");
+    expect(invoke).toBeCalledWith(options["method"], options["function"], fileContent);
+    expect(sls.cli.log).toBeCalledWith(JSON.stringify(expectedResult.data));
+  
+  });
+
+  it("calls the invoke hook with file path", async () => {
+    const invoke = jest.fn();
+    InvokeService.prototype.invoke = invoke;
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    options["function"] = "testApp";
+    options["path"] = "notExist.json";
+    options["method"] = "GET";
+    expect(() => new AzureInvoke(sls, options)).toThrow();
+  });
+  
+  it("Function invoked with no data", async () => {
+    const expectedResult = { data: "test" };
+    const invoke = jest.fn(() => expectedResult);
+    InvokeService.prototype.invoke = invoke as any;
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    options["function"] = "testApp";
+    options["method"] = "GET";
+    const plugin = new AzureInvoke(sls, options);
+    await invokeHook(plugin, "invoke:invoke"); 
+    expect(invoke).toBeCalledWith(options["method"], options["function"], undefined);
+    expect(sls.cli.log).toBeCalledWith(JSON.stringify(expectedResult.data));
+  });
+
+  it("The invoke function fails when no function name is passed", async () => {
+    const invoke = jest.fn();
+    InvokeService.prototype.invoke = invoke;
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    options["function"] = null;
+    options["data"] = "{\"name\": \"AzureTest\"}";
+    options["method"] = "GET";
+    const plugin = new AzureInvoke(sls, options);
+    await invokeHook(plugin, "invoke:invoke"); 
+    expect(invoke).not.toBeCalled();
+  });
+});
diff --git a/src/plugins/login/loginPlugin.test.ts b/src/plugins/login/loginPlugin.test.ts
index fe1fa89..0620e6f 100644
--- a/src/plugins/login/loginPlugin.test.ts
+++ b/src/plugins/login/loginPlugin.test.ts
@@ -78,16 +78,18 @@ describe("Login Plugin", () => {
     expect(sls.variables["subscriptionId"]).toEqual("azureSubId");
   });
 
-  it("logs an error from authentication", async () => {
+  it("logs an error from authentication and exits process", async () => {
     unsetServicePrincipalEnvVariables();
+    process.exit = jest.fn() as any;
     const errorMessage = "This is my error message";
     AzureLoginService.interactiveLogin = jest.fn(() => {
       throw new Error(errorMessage);
-    });    
+    });
     const sls = MockFactory.createTestServerless();
     await invokeLoginHook(false, sls);
     expect(AzureLoginService.interactiveLogin).toBeCalled()
     expect(AzureLoginService.servicePrincipalLogin).not.toBeCalled();
-    expect(sls.cli.log).lastCalledWith(`Error: ${errorMessage}`)
+    expect(sls.cli.log).lastCalledWith(`Error: ${errorMessage}`);
+    expect(process.exit).toBeCalledWith(0);
   });
-})
+});
diff --git a/src/plugins/offline/azureOfflinePlugin.test.ts b/src/plugins/offline/azureOfflinePlugin.test.ts
index f786b78..d8a6cda 100644
--- a/src/plugins/offline/azureOfflinePlugin.test.ts
+++ b/src/plugins/offline/azureOfflinePlugin.test.ts
@@ -6,6 +6,9 @@ import { MockFactory } from "../../test/mockFactory";
 import { invokeHook } from "../../test/utils";
 import { AzureOfflinePlugin } from "./azureOfflinePlugin";
 
+jest.mock("../../services/offlineService")
+import { OfflineService } from "../../services/offlineService"
+
 describe("Azure Offline Plugin", () => {
 
   function createPlugin(sls?: Serverless, options?: Serverless.Options) {
@@ -15,68 +18,24 @@ describe("Azure Offline Plugin", () => {
     )
   }
 
-  beforeEach(() => {
-    mockFs({})
-  });
-
-  afterEach(() => {
-    mockFs.restore();
-  })
-
   it("invokes build hook", async () => {
     const sls = MockFactory.createTestServerless();
     const plugin = createPlugin(sls);
-    const writeFileSpy = jest.spyOn(fs, "writeFileSync");
     await invokeHook(plugin, "offline:build:build");
-    const calls = writeFileSpy.mock.calls;
-    const functionNames = sls.service.getAllFunctions();
-    expect(calls).toHaveLength(functionNames.length + 1);
-    for (let i = 0; i < functionNames.length; i++) {
-      const name = functionNames[i];
-      expect(calls[i][0]).toEqual(`${name}${path.sep}function.json`)
-      expect(
-        JSON.parse(calls[i][1])
-      ).toEqual(
-        MockFactory.createTestBindingsObject(`..${path.sep}${name}.js`)
-      );
-    }
-    expect(calls[calls.length - 1][0]).toEqual("local.settings.json");
-    writeFileSpy.mockRestore();
+    expect(OfflineService.prototype.build).toBeCalled();
   });
 
   it("invokes offline hook", async () => {
     const sls = MockFactory.createTestServerless();
     const plugin = createPlugin(sls);
     await invokeHook(plugin, "offline:offline");
-    // Trivial test for now. In the future, this process
-    // may spawn the start process itself rather than telling
-    // the user how to do it.
-    expect(sls.cli.log).toBeCalledTimes(3);
+    expect(OfflineService.prototype.start).toBeCalled();
   });
 
   it("invokes cleanup hook", async () => {
-    mockFs({
-      hello: {
-        "function.json": "contents"
-      },
-      goodbye: {
-        "function.json": "contents"
-      },
-      "local.settings.json": "contents",
-    });
-    const unlinkSpy = jest.spyOn(fs, "unlinkSync");
-    const rmdirSpy = jest.spyOn(fs, "rmdirSync")
     const sls = MockFactory.createTestServerless();
     const plugin = createPlugin(sls);
     await invokeHook(plugin, "offline:cleanup:cleanup");
-    const unlinkCalls = unlinkSpy.mock.calls;
-    expect(unlinkCalls[0][0]).toBe(`hello${path.sep}function.json`);
-    expect(unlinkCalls[1][0]).toBe(`goodbye${path.sep}function.json`);
-    expect(unlinkCalls[2][0]).toBe("local.settings.json");
-    const rmdirCalls = rmdirSpy.mock.calls;
-    expect(rmdirCalls[0][0]).toBe("hello");
-    expect(rmdirCalls[1][0]).toBe("goodbye");
-    unlinkSpy.mockRestore();
-    rmdirSpy.mockRestore();
+    expect(OfflineService.prototype.cleanup).toBeCalled();
   });
-});
\ No newline at end of file
+});
diff --git a/src/plugins/rollback/azureRollbackPlugin.test.ts b/src/plugins/rollback/azureRollbackPlugin.test.ts
new file mode 100644
index 0000000..ac63664
--- /dev/null
+++ b/src/plugins/rollback/azureRollbackPlugin.test.ts
@@ -0,0 +1,17 @@
+import { MockFactory } from "../../test/mockFactory";
+import { invokeHook } from "../../test/utils";
+import { AzureRollbackPlugin } from "./azureRollbackPlugin";
+
+jest.mock("../../services/rollbackService");
+import { RollbackService } from "../../services/rollbackService";
+
+describe("Rollback Plugin", () => {
+  it("should call rollback service", async () => {
+    const plugin = new AzureRollbackPlugin(
+      MockFactory.createTestServerless(),
+      MockFactory.createTestServerlessOptions(),
+    );
+    await invokeHook(plugin, "rollback:rollback");
+    expect(RollbackService.prototype.rollback).toBeCalled();
+  })
+});
diff --git a/src/services/armService.test.ts b/src/services/armService.test.ts
index 5ff5cac..825599e 100644
--- a/src/services/armService.test.ts
+++ b/src/services/armService.test.ts
@@ -2,7 +2,7 @@ import Serverless from "serverless";
 import { MockFactory } from "../test/mockFactory";
 import { ArmService } from "./armService";
 import { ArmResourceTemplate, ArmTemplateType } from "../models/armTemplates";
-import { ArmTemplateConfig } from "../models/serverless";
+import { ArmTemplateConfig, ServerlessAzureOptions } from "../models/serverless";
 import mockFs from "mock-fs";
 import jsonpath from "jsonpath";
 import { Deployments } from "@azure/arm-resources";
@@ -11,9 +11,10 @@ import { Deployment } from "@azure/arm-resources/esm/models";
 describe("Arm Service", () => {
   let sls: Serverless
   let service: ArmService;
+  let options: ServerlessAzureOptions;
 
   function createService() {
-    return new ArmService(sls);
+    return new ArmService(sls, options);
   }
 
   beforeEach(() => {
@@ -197,4 +198,4 @@ describe("Arm Service", () => {
       expect(call[2]).toEqual(expectedDeployment);
     });
   });
-});
\ No newline at end of file
+});
diff --git a/src/services/azureBlobStorageService.test.ts b/src/services/azureBlobStorageService.test.ts
index d4d1a10..6eb6e62 100644
--- a/src/services/azureBlobStorageService.test.ts
+++ b/src/services/azureBlobStorageService.test.ts
@@ -4,7 +4,7 @@ import { AzureBlobStorageService, AzureStorageAuthType } from "./azureBlobStorag
 
 jest.mock("@azure/storage-blob");
 jest.genMockFromModule("@azure/storage-blob")
-import { Aborter, BlockBlobURL, ContainerURL, ServiceURL, uploadFileToBlockBlob, TokenCredential, SharedKeyCredential } from "@azure/storage-blob";
+import { Aborter, BlockBlobURL, ContainerURL, ServiceURL, uploadFileToBlockBlob, TokenCredential, SharedKeyCredential, downloadBlobToBuffer, generateBlobSASQueryParameters, BlobSASPermissions } from "@azure/storage-blob";
 
 jest.mock("@azure/arm-storage")
 jest.genMockFromModule("@azure/arm-storage");
@@ -12,9 +12,12 @@ 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", () => {
+jest.mock("fs")
+import fs from "fs";
 
+describe("Azure Blob Storage Service", () => {
   const filePath = "deployments/deployment.zip";
   const fileName = "deployment.zip";
   const fileContents = "contents";
@@ -22,9 +25,10 @@ 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);
+  const blobContent = "testContent";
+  const blockBlobUrl = MockFactory.createTestBlockBlobUrl(containerName, filePath, blobContent);
 
   let service: AzureBlobStorageService;
   const token = "myToken";
@@ -130,6 +134,14 @@ describe("Azure Blob Storage Service", () => {
     expect(ContainerURL.fromServiceURL).toBeCalledWith(expect.anything(), newContainerName);
     expect(ContainerURL.prototype.create).toBeCalledWith(Aborter.none);
   });
+
+  it("should not create a container if it exists already", async () => {
+    ContainerURL.fromServiceURL = jest.fn(() => new ContainerURL(null, null));
+    ContainerURL.prototype.create = jest.fn(() => Promise.resolve({ statusCode: 201 })) as any;
+    await service.createContainerIfNotExists("container1");
+    expect(ContainerURL.fromServiceURL).not.toBeCalled();
+    expect(ContainerURL.prototype.create).not.toBeCalled
+  })
   
   it("should delete a container", async () => {
     const containerToDelete = "delete container";
@@ -139,4 +151,45 @@ describe("Azure Blob Storage Service", () => {
     expect(ContainerURL.fromServiceURL).toBeCalledWith(expect.anything(), containerToDelete);
     expect(ContainerURL.prototype.delete).toBeCalledWith(Aborter.none);
   });
+
+  it("should download a binary file", async () => {
+    const targetPath = "";
+    await service.downloadBinary(containerName, fileName, targetPath);
+    const buffer = Buffer.alloc(blobContent.length)
+    expect(downloadBlobToBuffer).toBeCalledWith(
+      Aborter.timeout(30 * 60 * 1000),
+      buffer,
+      blockBlobUrl,
+      0,
+      undefined,
+      {
+        blockSize: 4 * 1024 * 1024, // 4MB block size
+        parallelism: 20, // 20 concurrency
+      }
+    );
+    expect(fs.writeFileSync).toBeCalledWith(
+      targetPath,
+      buffer,
+      "binary"
+    )
+  });
+
+  it("should generate a SAS url", async () => {
+    const sasToken = "myToken"
+    BlobSASPermissions.parse = jest.fn(() => {
+      return {
+        toString: jest.fn()
+      }
+    }) as any;
+    (generateBlobSASQueryParameters as any).mockReturnValue(token);
+    const url = await service.generateBlobSasTokenUrl(containerName, fileName);
+    expect(generateBlobSASQueryParameters).toBeCalled();
+    expect(url).toEqual(`${blockBlobUrl.url}?${sasToken}`)
+  });
+
+  it("should throw an error when trying to get a SAS Token with Token Auth", async () => {
+    const newService = new AzureBlobStorageService(sls, options, AzureStorageAuthType.Token);
+    await newService.initialize();
+    await expect(newService.generateBlobSasTokenUrl(containerName, fileName)).rejects.not.toBeNull();
+  })
 });
diff --git a/src/services/baseService.test.ts b/src/services/baseService.test.ts
index 8f637e1..1ecf667 100644
--- a/src/services/baseService.test.ts
+++ b/src/services/baseService.test.ts
@@ -7,9 +7,11 @@ jest.mock("request", () => MockFactory.createTestMockRequestFactory());
 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?: Serverless.Options) {
+  public constructor(serverless: Serverless, options?: ServerlessAzureOptions) {
     super(serverless, options);
   }
 
@@ -84,7 +86,7 @@ describe("Base Service", () => {
   it("returns region and stage based on CLI options", () => {
     const cliOptions = {
       stage: "prod",
-      region: "eastus2"
+      region: "eastus2",
     };
     const mockService = new MockService(sls, cliOptions);
 
@@ -92,40 +94,51 @@ describe("Base Service", () => {
     expect(mockService.getStage()).toEqual(cliOptions.stage);
   });
 
-  it("Sets default region and stage values if not defined", () => {
-    const mockService = new MockService(sls);
-
-    expect(mockService).not.toBeNull();
-    expect(sls.service.provider.region).toEqual("westus");
-    expect(sls.service.provider.stage).toEqual("dev");
-  });
-
-  it("returns region and stage based on CLI options", () => {
+  it("use the resource group name specified in CLI", () => {
+    const resourceGroupName = "cliResourceGroupName"
     const cliOptions = {
       stage: "prod",
-      region: "eastus2"
+      region: "eastus2",
+      resourceGroup: resourceGroupName
     };
+
     const mockService = new MockService(sls, cliOptions);
+    const actualResourceGroupName = mockService.getResourceGroupName();
 
-    expect(mockService.getRegion()).toEqual(cliOptions.region);
-    expect(mockService.getStage()).toEqual(cliOptions.stage);
+    expect(actualResourceGroupName).toEqual(resourceGroupName);
   });
 
-  it("Generates resource group name from sls yaml config", () => {
+  it("use the resource group name from sls yaml config", () => {
     const mockService = new MockService(sls);
-    const resourceGroupName = mockService.getResourceGroupName();
+    const actualResourceGroupName = mockService.getResourceGroupName();
 
-    expect(resourceGroupName).toEqual(sls.service.provider["resourceGroup"]);
+    expect(actualResourceGroupName).toEqual(sls.service.provider["resourceGroup"]);
   });
 
   it("Generates resource group from convention when NOT defined in sls yaml", () => {
     sls.service.provider["resourceGroup"] = null;
     const mockService = new MockService(sls);
-    const resourceGroupName = mockService.getResourceGroupName();
-    const region = mockService.getRegion();
-    const stage = mockService.getStage();
+    const actualResourceGroupName = mockService.getResourceGroupName();
+    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);
+  });
+
+  it("set default prefix when one is not defined in yaml config", () => {
+    const mockService = new MockService(sls);
+    const actualPrefix = mockService.getPrefix();
+    expect(actualPrefix).toEqual("sls");
+  });
+
+  it("use the prefix defined in sls yaml config", () => {
+    const expectedPrefix = "testPrefix"
+    sls.service.provider["prefix"] = expectedPrefix;
+    const mockService = new MockService(sls);
+    const actualPrefix = mockService.getPrefix();
 
-    expect(resourceGroupName).toEqual(`sls-${region}-${stage}-${sls.service["service"]}-rg`);
+    expect(actualPrefix).toEqual(expectedPrefix);
   });
 
   it("Fails if credentials have not been set in serverless config", () => {
@@ -183,4 +196,4 @@ describe("Base Service", () => {
 
     readStreamSpy.mockRestore();
   });
-});
\ No newline at end of file
+});
diff --git a/src/services/funcService.test.ts b/src/services/funcService.test.ts
new file mode 100644
index 0000000..b42bb75
--- /dev/null
+++ b/src/services/funcService.test.ts
@@ -0,0 +1,152 @@
+import fs from "fs";
+import mockFs from "mock-fs";
+import rimraf from "rimraf";
+import { MockFactory } from "../test/mockFactory";
+import Serverless from "serverless";
+import { FuncService } from "./funcService";
+
+describe("Azure Func Service", () => {
+
+  function createService(sls?: Serverless, options?: Serverless.Options) {
+    return new FuncService(
+      sls || MockFactory.createTestServerless(),
+      options || MockFactory.createTestServerlessOptions(),
+    )
+  }
+
+  describe("Add command", () => {
+
+    beforeAll(() => {
+      mockFs({
+        "myExistingFunction": {
+          "index.js": "contents",
+          "function.json": "contents",
+        },
+        "serverless.yml": MockFactory.createTestServerlessYml(true) as any,
+      });
+    });
+
+    afterAll(() => {
+      mockFs.restore();
+    });
+
+    afterEach(() => {
+      jest.clearAllMocks();
+    })
+
+    it("returns with missing name", async () => {
+      const sls = MockFactory.createTestServerless();
+      const options = MockFactory.createTestServerlessOptions();
+
+      const service = createService(sls, options);
+      await service.add();
+
+      expect(sls.cli.log).toBeCalledWith("Need to provide a name of function to add")
+    });
+
+    it("returns with pre-existing function", async () => {
+      const sls = MockFactory.createTestServerless();
+      const options = MockFactory.createTestServerlessOptions();
+      options["name"] = "hello";
+
+      const service = createService(sls, options);
+      await service.add();
+
+      expect(sls.cli.log).toBeCalledWith("Function hello already exists");
+    });
+
+    it("creates function handler and updates serverless.yml", async () => {
+      const sls = MockFactory.createTestServerless();
+      const options = MockFactory.createTestServerlessOptions();
+      const functionName = "myFunction";
+      options["name"] = functionName;
+      const expectedFunctionsYml = MockFactory.createTestSlsFunctionConfig();
+      expectedFunctionsYml[functionName] = MockFactory.createTestFunctionMetadata(functionName);
+
+      const service = createService(sls, options);
+      await service.add();
+
+      const writeFileCalls = (sls.utils.writeFileSync as any).mock.calls;
+      expect(writeFileCalls[0][0]).toBe(`./${functionName}.js`);
+
+      expect(writeFileCalls[1][0]).toBe("serverless.yml");
+      expect(writeFileCalls[1][1]).toBe(MockFactory.createTestServerlessYml(true, expectedFunctionsYml));
+    });
+  });
+
+  describe("Remove command", () => {
+
+    beforeAll(() => {
+      mockFs({
+        "index.js": "contents",
+        "function1": {
+          "function.json": "contents",
+        },
+      });
+    });
+
+    afterAll(() => {
+      mockFs.restore();
+    });
+
+    it("returns with missing name", async () => {
+      const sls = MockFactory.createTestServerless();
+      const options = MockFactory.createTestServerlessOptions();
+      const service = createService(sls, options);
+      await service.remove();
+      expect(sls.cli.log).toBeCalledWith("Need to provide a name of function to remove")
+    });
+
+    it("returns with non-existing function", async () => {
+      const sls = MockFactory.createTestServerless();
+      const options = MockFactory.createTestServerlessOptions();
+      options["name"] = "myNonExistingFunction";
+      const service = createService(sls, options);
+      await service.remove();
+      expect(sls.cli.log).toBeCalledWith("Function myNonExistingFunction does not exist");
+    });
+
+    it("deletes directory and updates serverless.yml", async () => {
+      mockFs({
+        "hello.js": "contents",
+        hello: {
+          "function.json": "contents",
+        }
+      });
+      const sls = MockFactory.createTestServerless();
+      const options = MockFactory.createTestServerlessOptions();
+      const functionName = "hello";
+      options["name"] = functionName;
+      const unlinkSpy = jest.spyOn(fs, "unlinkSync");
+      const rimrafSpy = jest.spyOn(rimraf, "sync");
+      const service = createService(sls, options);
+      await service.remove();
+      expect(unlinkSpy).toBeCalledWith(`${functionName}.js`)
+      expect(rimrafSpy).toBeCalledWith(functionName);
+      unlinkSpy.mockRestore();
+      rimrafSpy.mockRestore();
+      const expectedFunctionsYml = MockFactory.createTestSlsFunctionConfig();
+      delete expectedFunctionsYml[functionName];
+      expect(sls.utils.writeFileSync).toBeCalledWith("serverless.yml", MockFactory.createTestServerlessYml(true, expectedFunctionsYml))
+    });
+
+    it("does not try to delete file or directory if they don't exist", async () => {
+      mockFs({})
+      const sls = MockFactory.createTestServerless();
+      const options = MockFactory.createTestServerlessOptions();
+      const functionName = "hello";
+      options["name"] = functionName;
+      const unlinkSpy = jest.spyOn(fs, "unlinkSync");
+      const rimrafSpy = jest.spyOn(rimraf, "sync");
+      const service = createService(sls, options);
+      await service.remove();
+      expect(unlinkSpy).not.toBeCalled();
+      expect(rimrafSpy).not.toBeCalled();
+      unlinkSpy.mockRestore();
+      rimrafSpy.mockRestore();
+      const expectedFunctionsYml = MockFactory.createTestSlsFunctionConfig();
+      delete expectedFunctionsYml[functionName];
+      expect(sls.utils.writeFileSync).toBeCalledWith("serverless.yml", MockFactory.createTestServerlessYml(true, expectedFunctionsYml))
+    });
+  });
+});
diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts
index f923c6d..e81cef8 100644
--- a/src/services/functionAppService.test.ts
+++ b/src/services/functionAppService.test.ts
@@ -215,7 +215,7 @@ describe("Function App Service", () => {
     const expectedArtifactName = service.getDeploymentName().replace("rg-deployment", "artifact");
     expect((AzureBlobStorageService.prototype as any).uploadFile).toBeCalledWith(
       slsService["artifact"],
-      configConstants.deploymentArtifactContainer,
+      configConstants.deploymentConfig.container,
       `${expectedArtifactName}.zip`,
     )
     const uploadCall = ((AzureBlobStorageService.prototype as any).uploadFile).mock.calls[0];
@@ -254,4 +254,4 @@ describe("Function App Service", () => {
     const service = createService(sls);
     await expect(service.uploadFunctions(app)).rejects.not.toBeNull()
   });
-});
\ No newline at end of file
+});
diff --git a/src/services/invokeService.test.ts b/src/services/invokeService.test.ts
new file mode 100644
index 0000000..0dd751a
--- /dev/null
+++ b/src/services/invokeService.test.ts
@@ -0,0 +1,73 @@
+import axios from "axios";
+import MockAdapter from "axios-mock-adapter";
+import { MockFactory } from "../test/mockFactory";
+import { InvokeService } from "./invokeService";
+jest.mock("@azure/arm-appservice")
+jest.mock("@azure/arm-resources")
+jest.mock("./functionAppService")
+import { FunctionAppService } from "./functionAppService";
+
+describe("Invoke Service ", () => {
+  const app = MockFactory.createTestSite();
+  const expectedSite = MockFactory.createTestSite();
+  const testData = "test-data";
+  const testResult = "test-data";
+  const authKey = "authKey";
+  const baseUrl = "https://management.azure.com"
+  const masterKeyUrl = `https://${app.defaultHostName}/admin/host/systemkeys/_master`;
+  const authKeyUrl = `${baseUrl}${app.id}/functions/admin/token?api-version=2016-08-01`;
+  let urlPOST = `http://${app.defaultHostName}/api/hello`;
+  let urlGET = `http://${app.defaultHostName}/api/hello?name%3D${testData}`;
+  let masterKey: string;
+
+  beforeAll(() => {
+    const axiosMock = new MockAdapter(axios);
+    // Master Key
+    axiosMock.onGet(masterKeyUrl).reply(200, { value: masterKey });
+    // Auth Key
+    axiosMock.onGet(authKeyUrl).reply(200, authKey);
+    //Mock url for GET
+    axiosMock.onGet(urlGET).reply(200, testResult);
+    //Mock url for POST
+    axiosMock.onPost(urlPOST).reply(200, testResult);
+  });
+  
+  beforeEach(() => {
+    FunctionAppService.prototype.getMasterKey = jest.fn();
+    FunctionAppService.prototype.get = jest.fn(() => Promise.resolve(expectedSite));
+  });
+
+  it("Invokes a function with GET request", async () => {
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    const expectedResult = {url: `${app.defaultHostName}/api/hello`};
+    const httpConfig = jest.fn(() => expectedResult);
+
+    FunctionAppService.prototype.getFunctionHttpTriggerConfig = httpConfig as any;
+
+    options["function"] = "hello";
+    options["data"] = `{"name": "${testData}"}`;
+    options["method"] = "GET";
+
+    const service = new InvokeService(sls, options);
+    const response = await service.invoke(options["method"], options["function"], options["data"]);
+    expect(JSON.stringify(response.data)).toEqual(JSON.stringify(testResult));
+  });
+
+  it("Invokes a function with POST request", async () => {
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    const expectedResult = {url: `${app.defaultHostName}/api/hello`};
+    const httpConfig = jest.fn(() => expectedResult);
+    FunctionAppService.prototype.getFunctionHttpTriggerConfig = httpConfig as any;
+
+    options["function"] = "hello";
+    options["data"] = `{"name": "${testData}"}`;
+    options["method"] = "POST";
+
+    const service = new InvokeService(sls, options);
+    const response = await service.invoke(options["method"], options["function"], options["data"]);
+    expect(JSON.stringify(response.data)).toEqual(JSON.stringify(testResult));
+  });
+  
+});
\ No newline at end of file
diff --git a/src/services/offlineService.test.ts b/src/services/offlineService.test.ts
index c416800..979dc0c 100644
--- a/src/services/offlineService.test.ts
+++ b/src/services/offlineService.test.ts
@@ -44,6 +44,19 @@ describe("Offline Service", () => {
     writeFileSpy.mockRestore();
   });
 
+  it("does not write files if they already exist", async () => {
+    const sls = MockFactory.createTestServerless();
+    const service = createService(sls);
+    const functionNames = sls.service.getAllFunctions();
+    const writeFileSpy = jest.spyOn(fs, "writeFileSync");
+    mockFs({
+      "local.settings.json": "contents",
+    });
+    await service.build();
+    const calls = writeFileSpy.mock.calls;
+    expect(calls).toHaveLength(functionNames.length);
+  });
+
   it("cleans up functions files", async () => {
     mockFs({
       hello: {
@@ -70,6 +83,31 @@ describe("Offline Service", () => {
     rmdirSpy.mockRestore();
   });
 
+  it("does not try to remove files if they don't exist", async () => {
+    mockFs({
+      hello: {
+        "function.json": "contents"
+      },
+      goodbye: {
+        "function.json": "contents"
+      },
+    })
+    const sls = MockFactory.createTestServerless();
+    const service = createService(sls);
+    const unlinkSpy = jest.spyOn(fs, "unlinkSync");
+    const rmdirSpy = jest.spyOn(fs, "rmdirSync")
+    await service.cleanup();
+    const unlinkCalls = unlinkSpy.mock.calls;
+    expect(unlinkCalls).toHaveLength(2);
+    expect(unlinkCalls[0][0]).toBe(`hello${path.sep}function.json`);
+    expect(unlinkCalls[1][0]).toBe(`goodbye${path.sep}function.json`);
+    const rmdirCalls = rmdirSpy.mock.calls;
+    expect(rmdirCalls[0][0]).toBe("hello");
+    expect(rmdirCalls[1][0]).toBe("goodbye");
+    unlinkSpy.mockRestore();
+    rmdirSpy.mockRestore();
+  });
+
   it("instructs users how to run locally", async () => {
     const sls = MockFactory.createTestServerless();
     const service = createService(sls);
@@ -79,4 +117,4 @@ describe("Offline Service", () => {
     // the user how to do it.
     expect(sls.cli.log).toBeCalledTimes(3);
   });
-});
\ No newline at end of file
+});
diff --git a/src/services/resourceService.test.ts b/src/services/resourceService.test.ts
index f374306..bd22314 100644
--- a/src/services/resourceService.test.ts
+++ b/src/services/resourceService.test.ts
@@ -4,9 +4,11 @@ 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();
+  const template = "myTemplate";
 
   beforeAll(() => {
     ResourceManagementClient.prototype.resourceGroups = {
@@ -17,6 +19,7 @@ describe("Resource Service", () => {
     ResourceManagementClient.prototype.deployments = {
       deleteMethod: jest.fn(),
       listByResourceGroup: jest.fn(() => Promise.resolve(deployments)),
+      exportTemplate: jest.fn(() => Promise.resolve(template)),
     } as any;
   });
 
@@ -37,14 +40,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", () => {
@@ -85,4 +90,21 @@ describe("Resource Service", () => {
     const deps = await service.getDeployments();
     expect(deps).toEqual(deployments);
   });
+
+  it("gets deployment template",async () => {
+    const sls = MockFactory.createTestServerless();
+    const resourceGroup = "myResourceGroup";
+    sls.service.provider["resourceGroup"] = resourceGroup
+    sls.variables["azureCredentials"] = "fake credentials"
+    const options = MockFactory.createTestServerlessOptions();
+    const service = new ResourceService(sls, options);
+    const deploymentName = "myDeployment";
+    const result = await service.getDeploymentTemplate(deploymentName);
+    expect(ResourceManagementClient.prototype.deployments.exportTemplate)
+      .toBeCalledWith(
+        resourceGroup,
+        deploymentName
+      );
+    expect(result).toEqual(template);
+  });
 });
\ No newline at end of file
diff --git a/src/services/rollbackService.test.ts b/src/services/rollbackService.test.ts
new file mode 100644
index 0000000..2b0e51d
--- /dev/null
+++ b/src/services/rollbackService.test.ts
@@ -0,0 +1,130 @@
+import mockFs from "mock-fs";
+import path from "path";
+import Serverless from "serverless";
+import { ArmDeployment } from "../models/armTemplates";
+import { DeploymentConfig } from "../models/serverless";
+import { MockFactory } from "../test/mockFactory";
+import { RollbackService } from "./rollbackService";
+
+jest.mock("./azureBlobStorageService");
+import { AzureBlobStorageService } from "./azureBlobStorageService";
+
+jest.mock("./resourceService");
+import { ResourceService } from "./resourceService";
+
+jest.mock("./functionAppService")
+import { FunctionAppService } from "./functionAppService";
+
+jest.mock("./armService");
+import { ArmService } from "./armService";
+
+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";
+  const artifactName = MockFactory.createTestDeployment().name.replace("deployment", "artifact") + ".zip";
+  const artifactPath = `.serverless${path.sep}${artifactName}`
+  const armDeployment: ArmDeployment = { template, parameters };
+
+  function createOptions(timestamp?: string): Serverless.Options {
+    return {
+      ...MockFactory.createTestServerlessOptions(),
+      timestamp: timestamp || MockFactory.createTestTimestamp(),
+    } as any
+  }
+
+  function createService(sls?: Serverless, options?: Serverless.Options): RollbackService {
+    return new RollbackService(
+      sls || MockFactory.createTestServerless(),
+      options || createOptions(),
+    )
+  }
+
+  beforeEach(() => {
+    // Mocking the file system so that files are not created in project directory
+    mockFs({})
+    ResourceService.prototype.getDeployments = jest.fn(() => Promise.resolve(
+      [
+        ...MockFactory.createTestDeployments(5, true),
+        MockFactory.createTestDeployment("noTimestamp")
+      ]
+    )) as any;
+    ResourceService.prototype.getDeploymentTemplate = jest.fn(
+      () => Promise.resolve({ template })
+    ) as any;
+    AzureBlobStorageService.prototype.generateBlobSasTokenUrl = jest.fn(() => sasURL) as any;
+    FunctionAppService.prototype.get = jest.fn(() => appStub) as any;
+  });
+
+  afterEach(() => {
+    mockFs.restore();
+    jest.resetAllMocks();
+  });
+
+  it("should return early with no timestamp", async () => {
+    const sls = MockFactory.createTestServerless();
+    const options = {} as any;
+    const service = createService(sls, options);
+    await service.rollback();
+    const calls = (sls.cli.log as any).mock.calls;
+    expect(calls[0][0]).toEqual("Need to specify a timestamp for rollback. Run `sls deploy list` to see timestamps of deployments");
+  });
+
+  it("should return early with invalid timestamp", async () => {
+    const sls = MockFactory.createTestServerless();
+    const timestamp = "garbage";
+    const options = createOptions(timestamp)
+    const service = createService(sls, options);
+    await service.rollback();
+    const calls = (sls.cli.log as any).mock.calls;
+    expect(calls[0][0]).toEqual(`Couldn't find deployment with timestamp: ${timestamp}`);
+  });
+
+  it("should deploy blob package directly to function app", async () => {
+    const service = createService();
+    await service.rollback();
+    expect(AzureBlobStorageService.prototype.initialize).toBeCalled();
+    expect(ArmService.prototype.deployTemplate).toBeCalledWith(armDeployment);
+    expect(AzureBlobStorageService.prototype.downloadBinary).toBeCalledWith(
+      containerName,
+      artifactName,
+      artifactPath,
+    );
+    expect(FunctionAppService.prototype.get).toBeCalled();
+    expect(FunctionAppService.prototype.uploadZippedArfifactToFunctionApp).toBeCalledWith(
+      appStub,
+      artifactPath
+    )
+  });
+
+  it("should deploy function app with SAS URL", async () => {
+    const sls = MockFactory.createTestServerless();
+    const deploymentConfig: DeploymentConfig = {
+      runFromBlobUrl: true
+    }
+    sls["deploy"] = deploymentConfig;
+    const service = createService(sls);
+    await service.rollback();
+    expect(AzureBlobStorageService.prototype.initialize).toBeCalled();
+    expect(ArmService.prototype.deployTemplate).toBeCalledWith({
+      ...armDeployment,
+      parameters: {
+        ...armDeployment.parameters,
+        functionAppRunFromPackage: sasURL,
+      }
+    });
+    expect(AzureBlobStorageService.prototype.downloadBinary).not.toBeCalled();
+    expect(AzureBlobStorageService.prototype.generateBlobSasTokenUrl).toBeCalledWith(
+      containerName,
+      artifactName
+    );
+    expect(FunctionAppService.prototype.get).not.toBeCalled();
+    expect(FunctionAppService.prototype.uploadZippedArfifactToFunctionApp).not.toBeCalled();
+  });
+});
diff --git a/src/shared/utils.test.ts b/src/shared/utils.test.ts
index 8f31917..3e6feda 100644
--- a/src/shared/utils.test.ts
+++ b/src/shared/utils.test.ts
@@ -78,4 +78,96 @@ describe("utils", () => {
       )
     ).toEqual("abfgklpquvab");
   });
+
+  it("Creates a short name for an azure region", () => {
+    const expected = "ausse";
+    const actual = Utils.createShortAzureRegionName("australiasoutheast");
+
+    expect(actual).toEqual(expected);
+  });
+
+  it("Creates a short stage name from a well known name", () => {
+    const expected = "prod";
+    const actual = Utils.createShortStageName("production");
+
+    expect(actual).toEqual(expected);
+  });
+
+  it("Creates a short stage name from a unknown name", () => {
+    const value = "user acceptance";
+    const actual = Utils.createShortStageName(value);
+
+    expect(actual).toEqual(value.substr(0, 3));
+  });
+
+  it("Creates a short stage name from multiple values", () => {
+    const actual = Utils.createShortStageName("production dogfood");
+    expect(actual).toEqual("proddf");
+  });
+
+  it("Creates unique short names for all azure regions", () => {
+    const regions = [
+      "eastasia",
+      "southeastasia",
+      "centralus",
+      "eastus",
+      "eastus2",
+      "westus",
+      "northcentralus",
+      "southcentralus",
+      "northeurope",
+      "westeurope",
+      "japanwest",
+      "japaneast",
+      "brazilsouth",
+      "australiaeast",
+      "australiasoutheast",
+      "southindia",
+      "centralindia",
+      "westindia",
+      "canadacentral",
+      "canadaeast",
+      "uksouth",
+      "ukwest",
+      "westcentralus",
+      "westus2",
+      "koreacentral",
+      "koreasouth",
+      "francecentral",
+      "francesouth",
+      "australiacentral",
+      "australiacentral2",
+      "uaecentral",
+      "uaenorth",
+      "southafricanorth",
+      "southafricawest"
+    ];
+
+    const results = {};
+    regions.forEach((region) => {
+      const result = Utils.createShortAzureRegionName(region);
+      results[result] = region;
+    });
+
+    expect(Object.keys(results)).toHaveLength(regions.length);
+  });
+
+  it("gets a normalized region name from full region name", () => {
+    const result = Utils.getNormalizedRegionName("West US 2");
+    expect(result).toEqual("westus2");
+  });
+
+  it("Performs noop if region name is already normalized", () => {
+    const expected = "westus2";
+    const actual = Utils.getNormalizedRegionName(expected);
+    expect(actual).toEqual(expected);
+  });
+  it("should get a timestamp from a name", () => {
+    expect(Utils.getTimestampFromName("myDeployment-t12345")).toEqual("12345");
+    expect(Utils.getTimestampFromName("myDeployment-t678987645")).toEqual("678987645");
+    expect(Utils.getTimestampFromName("-t12345")).toEqual("12345");
+
+    expect(Utils.getTimestampFromName("myDeployment-t")).toEqual(null);
+    expect(Utils.getTimestampFromName("")).toEqual(null);
+  })
 });
diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts
index 6177bd7..485b689 100644
--- a/src/test/mockFactory.ts
+++ b/src/test/mockFactory.ts
@@ -1,6 +1,6 @@
 import { ApiContract, ApiManagementServiceResource } from "@azure/arm-apimanagement/esm/models";
 import { FunctionEnvelope, Site } from "@azure/arm-appservice/esm/models";
-import { DeploymentsListByResourceGroupResponse } from "@azure/arm-resources/esm/models";
+import { DeploymentExtended, DeploymentsListByResourceGroupResponse } from "@azure/arm-resources/esm/models";
 import { HttpHeaders, HttpOperationResponse, HttpResponse, WebResource } from "@azure/ms-rest-js";
 import { AuthResponse, LinkedSubscription, TokenCredentialsBase } from "@azure/ms-rest-nodeauth";
 import { TokenClientCredentials, TokenResponse } from "@azure/ms-rest-nodeauth/dist/lib/credentials/tokenClientCredentials";
@@ -12,10 +12,10 @@ 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 { ArmResourceTemplate } from "../models/armTemplates";
-import { AzureServiceProvider, ServicePrincipalEnvVariables } from "../models/azureProvider";
+import { ArmDeployment, ArmResourceTemplate } from "../models/armTemplates";
+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]) {
@@ -143,19 +143,38 @@ export class MockFactory {
     return credentials;
   }
 
-  public static createTestDeployments(count: number = 5): DeploymentsListByResourceGroupResponse {
+  public static createTestTimestamp(): string {
+    return "1562184492";
+  }
+
+  public static createTestDeployments(count: number = 5, includeTimestamp = false): DeploymentsListByResourceGroupResponse {
     const result = [];
+    const originalTimestamp = +MockFactory.createTestTimestamp();
     for (let i = 0; i < count; i++) {
-      result.push({
-        name: `deployment${i + 1}`,
-        properties: {
-          timestamp: new Date(),
-        }
-      })
+      result.push(
+        MockFactory.createTestDeployment((includeTimestamp) ? `deployment${i + 1}-t${originalTimestamp + i}` : `deployment${i + 1}`)
+      )
     }
     return result as DeploymentsListByResourceGroupResponse
   }
 
+  public static createTestParameters() {
+    return {
+      param1: { value: "1", type: "String" },
+      param2: { value: "2", type: "String" },
+    }
+  }
+
+  public static createTestDeployment(name?: string): DeploymentExtended {
+    return {
+      name: name || `deployment1-t${MockFactory.createTestTimestamp()}`,
+      properties: {
+        timestamp: new Date(),
+        parameters: MockFactory.createTestParameters(),
+      }
+    }
+  }
+
   public static createTestAxiosResponse<T>(
     config: AxiosRequestConfig,
     responseJson: T,
@@ -195,11 +214,15 @@ export class MockFactory {
     return { containerItems: result } as ServiceListContainersSegmentResponse;
   }
 
-  public static createTestBlockBlobUrl(containerName: string, blobName: string) {
+  public static createTestBlockBlobUrl(containerName: string, blobName: string, contents: string = "test") {
     return {
       containerName,
       blobName,
+      url: `http://storage.azure.com/${containerName}/${blobName}`,
       delete: jest.fn(),
+      getProperties: jest.fn(() => Promise.resolve({
+        contentLength: contents.length
+      }))
     }
   }
 
@@ -305,10 +328,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",
     }
   }
 
@@ -485,6 +512,13 @@ export class MockFactory {
     };
   }
 
+  public static createTestArmDeployment(): ArmDeployment {
+    return {
+      template: MockFactory.createTestArmTemplate(),
+      parameters: MockFactory.createTestParameters(),
+    }
+  }
+
   public static createTestArmTemplate(): ArmResourceTemplate {
     return {
       "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",