Repository: serverless/serverless-azure-functions
Pull request #191 (2019-07-04 01:53:27)

feat: Rollback Plugin
## Updates
- Adds new `rollback` plugin
- Fixes `deploy list` to show actual timestamp from name, which is used for rollback
- Adds required functionality to `AzureBlobStorageService` and `ArmService`
- Adds previously missing tests of required functionality

## How to Use
1. Get a timestamp. Use
```
sls deploy list
```
to retrieve one.
2. Roll back function app to timestamp
```
sls rollback -t <timestamp>
```

## Options
In `deploy` section of `serverless.yml`, you can specify whether to run from a blob URL or deploy the package directly to the function app by setting:
```yaml
deploy:
  # Default is false
  runFromBlobUrl: true|false
```

Resolves [AB#317]


Patch:
diff --git a/package-lock.json b/package-lock.json
index b52ea54..f756d84 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1794,7 +1794,6 @@
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
       "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
-      "optional": true,
       "requires": {
         "kind-of": "^3.0.2",
         "longest": "^1.0.1",
@@ -1967,7 +1966,6 @@
       "version": "4.10.1",
       "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
       "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
-      "optional": true,
       "requires": {
         "bn.js": "^4.0.0",
         "inherits": "^2.0.1",
@@ -2437,8 +2435,7 @@
     "bn.js": {
       "version": "4.11.8",
       "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
-      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
-      "optional": true
+      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
     },
     "body-parser": {
       "version": "1.19.0",
@@ -2543,8 +2540,7 @@
     "brorand": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
-      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
-      "optional": true
+      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
     },
     "browser-process-hrtime": {
       "version": "0.1.3",
@@ -2573,7 +2569,6 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
       "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
-      "optional": true,
       "requires": {
         "buffer-xor": "^1.0.3",
         "cipher-base": "^1.0.0",
@@ -2610,7 +2605,6 @@
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
       "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
-      "optional": true,
       "requires": {
         "bn.js": "^4.1.0",
         "randombytes": "^2.0.1"
@@ -2707,8 +2701,7 @@
     "buffer-xor": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
-      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
-      "optional": true
+      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
     },
     "builtin-modules": {
       "version": "1.1.1",
@@ -2895,7 +2888,6 @@
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
       "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
-      "optional": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -3273,7 +3265,6 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
       "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
-      "optional": true,
       "requires": {
         "cipher-base": "^1.0.1",
         "inherits": "^2.0.1",
@@ -3286,7 +3277,6 @@
       "version": "1.1.7",
       "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
       "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
-      "optional": true,
       "requires": {
         "cipher-base": "^1.0.3",
         "create-hash": "^1.1.0",
@@ -3750,7 +3740,6 @@
       "version": "6.4.1",
       "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
       "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==",
-      "optional": true,
       "requires": {
         "bn.js": "^4.4.0",
         "brorand": "^1.0.1",
@@ -3812,7 +3801,6 @@
       "version": "0.1.7",
       "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
       "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
-      "optional": true,
       "requires": {
         "prr": "~1.0.1"
       }
@@ -4192,7 +4180,6 @@
       "version": "0.3.5",
       "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
       "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
-      "optional": true,
       "requires": {
         "d": "1",
         "es5-ext": "~0.10.14"
@@ -4207,7 +4194,6 @@
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
       "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
-      "optional": true,
       "requires": {
         "md5.js": "^1.3.4",
         "safe-buffer": "^5.1.1"
@@ -4737,8 +4723,7 @@
         },
         "ansi-regex": {
           "version": "2.1.1",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "aproba": {
           "version": "1.2.0",
@@ -4756,13 +4741,11 @@
         },
         "balanced-match": {
           "version": "1.0.0",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "bundled": true,
-          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -4775,18 +4758,15 @@
         },
         "code-point-at": {
           "version": "1.1.0",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "concat-map": {
           "version": "0.0.1",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "console-control-strings": {
           "version": "1.1.0",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -4889,8 +4869,7 @@
         },
         "inherits": {
           "version": "2.0.3",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "ini": {
           "version": "1.3.5",
@@ -4900,7 +4879,6 @@
         "is-fullwidth-code-point": {
           "version": "1.0.0",
           "bundled": true,
-          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -4913,20 +4891,17 @@
         "minimatch": {
           "version": "3.0.4",
           "bundled": true,
-          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
         },
         "minimist": {
           "version": "0.0.8",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "minipass": {
           "version": "2.3.5",
           "bundled": true,
-          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.2",
             "yallist": "^3.0.0"
@@ -4943,7 +4918,6 @@
         "mkdirp": {
           "version": "0.5.1",
           "bundled": true,
-          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
@@ -5016,8 +4990,7 @@
         },
         "number-is-nan": {
           "version": "1.0.1",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -5027,7 +5000,6 @@
         "once": {
           "version": "1.4.0",
           "bundled": true,
-          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -5103,8 +5075,7 @@
         },
         "safe-buffer": {
           "version": "5.1.2",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "safer-buffer": {
           "version": "2.1.2",
@@ -5134,7 +5105,6 @@
         "string-width": {
           "version": "1.0.2",
           "bundled": true,
-          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -5152,7 +5122,6 @@
         "strip-ansi": {
           "version": "3.0.1",
           "bundled": true,
-          "optional": true,
           "requires": {
             "ansi-regex": "^2.0.0"
           }
@@ -5191,13 +5160,11 @@
         },
         "wrappy": {
           "version": "1.0.2",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "yallist": {
           "version": "3.0.3",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         }
       }
     },
@@ -5505,7 +5472,6 @@
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
       "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
-      "optional": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -5515,7 +5481,6 @@
       "version": "1.1.7",
       "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
       "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
-      "optional": true,
       "requires": {
         "inherits": "^2.0.3",
         "minimalistic-assert": "^1.0.1"
@@ -5525,7 +5490,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
       "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
-      "optional": true,
       "requires": {
         "hash.js": "^1.0.3",
         "minimalistic-assert": "^1.0.0",
@@ -5844,8 +5808,7 @@
     "is-extglob": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
-      "optional": true
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
     },
     "is-fullwidth-code-point": {
       "version": "1.0.0",
@@ -7485,8 +7448,7 @@
     "longest": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
-      "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
-      "optional": true
+      "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc="
     },
     "loose-envify": {
       "version": "1.4.0",
@@ -7587,7 +7549,6 @@
       "version": "1.3.5",
       "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
       "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
-      "optional": true,
       "requires": {
         "hash-base": "^3.0.0",
         "inherits": "^2.0.1",
@@ -7613,7 +7574,6 @@
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
       "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=",
-      "optional": true,
       "requires": {
         "errno": "^0.1.3",
         "readable-stream": "^2.0.1"
@@ -7704,14 +7664,12 @@
     "minimalistic-assert": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
-      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
-      "optional": true
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
     },
     "minimalistic-crypto-utils": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
-      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
-      "optional": true
+      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
     },
     "minimatch": {
       "version": "3.0.4",
@@ -8309,7 +8267,6 @@
       "version": "5.1.4",
       "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz",
       "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==",
-      "optional": true,
       "requires": {
         "asn1.js": "^4.0.0",
         "browserify-aes": "^1.0.0",
@@ -8418,7 +8375,6 @@
       "version": "3.0.17",
       "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
       "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==",
-      "optional": true,
       "requires": {
         "create-hash": "^1.1.2",
         "create-hmac": "^1.1.4",
@@ -8621,8 +8577,7 @@
     "prr": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
-      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
-      "optional": true
+      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY="
     },
     "pseudomap": {
       "version": "1.0.2",
@@ -8689,7 +8644,6 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
       "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
-      "optional": true,
       "requires": {
         "safe-buffer": "^5.1.0"
       }
@@ -9102,7 +9056,6 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
       "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
-      "optional": true,
       "requires": {
         "hash-base": "^3.0.0",
         "inherits": "^2.0.1"
@@ -9492,7 +9445,6 @@
       "version": "2.4.11",
       "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
       "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
-      "optional": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -9688,8 +9640,7 @@
     "source-list-map": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
-      "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==",
-      "optional": true
+      "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw=="
     },
     "source-map": {
       "version": "0.7.3",
@@ -10246,8 +10197,7 @@
     "tapable": {
       "version": "0.2.9",
       "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.9.tgz",
-      "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==",
-      "optional": true
+      "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A=="
     },
     "tar-stream": {
       "version": "1.6.2",
@@ -11097,7 +11047,6 @@
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz",
       "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==",
-      "optional": true,
       "requires": {
         "source-list-map": "^2.0.0",
         "source-map": "~0.6.1"
@@ -11106,8 +11055,7 @@
         "source-map": {
           "version": "0.6.1",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-          "optional": true
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
         }
       }
     },
diff --git a/src/armTemplates/resources/functionApp.ts b/src/armTemplates/resources/functionApp.ts
index d2058a3..48c7f5c 100644
--- a/src/armTemplates/resources/functionApp.ts
+++ b/src/armTemplates/resources/functionApp.ts
@@ -16,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"
@@ -86,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/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/serverless.ts b/src/models/serverless.ts
index dc7f0b9..2f009ab 100644
--- a/src/models/serverless.ts
+++ b/src/models/serverless.ts
@@ -27,6 +27,7 @@ export interface FunctionAppConfig extends ResourceConfig {
 export interface DeploymentConfig {
   rollback?: boolean;
   container?: string;
+  runFromBlobUrl?: boolean;
 }
 
 export interface ServerlessAzureProvider {
diff --git a/src/plugins/deploy/azureDeployPlugin.ts b/src/plugins/deploy/azureDeployPlugin.ts
index 88e253c..a17f23c 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> };
@@ -45,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);
diff --git a/src/plugins/login/loginPlugin.ts b/src/plugins/login/loginPlugin.ts
index 083831f..1b36eb6 100644
--- a/src/plugins/login/loginPlugin.ts
+++ b/src/plugins/login/loginPlugin.ts
@@ -12,6 +12,7 @@ export class AzureLoginPlugin {
     this.hooks = {
       "before:package:initialize": this.login.bind(this),
       "before:deploy:list:list": this.login.bind(this),
+      "before:rollback:rollback": this.login.bind(this),
     };
   }
 
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 df1afe8..d021b36 100644
--- a/src/services/armService.ts
+++ b/src/services/armService.ts
@@ -1,13 +1,13 @@
-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, ServerlessAzureOptions } 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;
@@ -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 }
       }
     });
 
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 6b782d4..aee0061 100644
--- a/src/services/baseService.ts
+++ b/src/services/baseService.ts
@@ -48,10 +48,16 @@ export abstract class BaseService {
     }
   }
 
+  /**
+   * Name of Azure Region for deployment
+   */
   public getRegion(): string {
     return this.options.region || this.config.provider.region;
   }
 
+  /**
+   * Name of current deployment stage
+   */
   public getStage(): string {
     return this.options.stage || this.config.provider.stage;
   }
@@ -60,6 +66,9 @@ export abstract class BaseService {
     return this.config.provider.prefix;
   }
 
+  /**
+   * Name of current resource group
+   */
   public getResourceGroupName(): string {
     const regionName = Utils.createShortAzureRegionName(this.getRegion());
     const stageName = Utils.createShortStageName(this.getStage());
@@ -69,24 +78,43 @@ export abstract class BaseService {
       || `${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 config;
+    return {
+      ...configConstants.deploymentConfig,
+      ...providedConfig,
+    }
   }
 
+  /**
+   * Name of current ARM deployment
+   */
   public getDeploymentName(): string {
     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
    */
@@ -142,10 +170,17 @@ export abstract class BaseService {
     });
   }
 
+  /**
+   * 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"];
   }
diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts
index c29b8da..0072afe 100644
--- a/src/services/functionAppService.ts
+++ b/src/services/functionAppService.ts
@@ -14,14 +14,12 @@ 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> {
@@ -117,8 +115,10 @@ export class FunctionAppService extends BaseService {
 
     this.log("Deploying serverless functions...");
 
-    await this.uploadZippedArfifactToFunctionApp(functionApp);
-    await this.uploadZippedArtifactToBlobStorage();
+    const functionZipFile = this.getFunctionZipFile();
+    const uploadFunctionApp = this.uploadZippedArfifactToFunctionApp(functionApp, functionZipFile);
+    const uploadBlobStorage = await this.uploadZippedArtifactToBlobStorage(functionZipFile);
+    await Promise.all([uploadFunctionApp, uploadBlobStorage]);
   }
 
   /**
@@ -139,16 +139,16 @@ export class FunctionAppService extends BaseService {
     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 = {
@@ -162,7 +162,7 @@ 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();
@@ -185,11 +185,11 @@ export class FunctionAppService extends BaseService {
   /**
    * 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),
     );
diff --git a/src/services/resourceService.ts b/src/services/resourceService.ts
index 2a9baa2..4b7dca0 100644
--- a/src/services/resourceService.ts
+++ b/src/services/resourceService.ts
@@ -12,11 +12,22 @@ 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}`);
 
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 974d865..8bc5e70 100644
--- a/src/shared/utils.ts
+++ b/src/shared/utils.ts
@@ -203,4 +203,20 @@ export class Utils {
       })
       .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/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/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/azureBlobStorageService.test.ts b/src/services/azureBlobStorageService.test.ts
index 7d37cb7..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");
@@ -14,6 +14,9 @@ jest.mock("./loginService");
 import { AzureLoginService } from "./loginService"
 import { StorageAccountResource } from "../armTemplates/resources/storageAccount";
 
+jest.mock("fs")
+import fs from "fs";
+
 describe("Azure Blob Storage Service", () => {
   const filePath = "deployments/deployment.zip";
   const fileName = "deployment.zip";
@@ -24,7 +27,8 @@ describe("Azure Blob Storage Service", () => {
   const sls = MockFactory.createTestServerless();
   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/functionAppService.test.ts b/src/services/functionAppService.test.ts
index f923c6d..eebc61a 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];
diff --git a/src/services/resourceService.test.ts b/src/services/resourceService.test.ts
index 3b75754..bd22314 100644
--- a/src/services/resourceService.test.ts
+++ b/src/services/resourceService.test.ts
@@ -8,6 +8,7 @@ import { Utils } from "../shared/utils";
 
 describe("Resource Service", () => {
   const deployments = MockFactory.createTestDeployments();
+  const template = "myTemplate";
 
   beforeAll(() => {
     ResourceManagementClient.prototype.resourceGroups = {
@@ -18,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;
   });
 
@@ -88,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 6982004..3e6feda 100644
--- a/src/shared/utils.test.ts
+++ b/src/shared/utils.test.ts
@@ -162,4 +162,12 @@ describe("utils", () => {
     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 ba015c6..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,7 +12,7 @@ import Service from "serverless/classes/Service";
 import Utils from "serverless/classes/Utils";
 import PluginManager from "serverless/lib/classes/PluginManager";
 import { ApiCorsPolicy, ApiManagementConfig } from "../models/apiManagement";
-import { ArmResourceTemplate } from "../models/armTemplates";
+import { ArmDeployment, ArmResourceTemplate } from "../models/armTemplates";
 import { ServicePrincipalEnvVariables } from "../models/azureProvider";
 import { Logger } from "../models/generic";
 import { ServerlessAzureConfig, ServerlessAzureProvider } from "../models/serverless";
@@ -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
+      }))
     }
   }
 
@@ -489,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#",