You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by mo...@apache.org on 2016/05/26 20:54:45 UTC

incubator-zeppelin git commit: [ZEPPELIN-848] Add support for encrypted data stored in Amazon S3

Repository: incubator-zeppelin
Updated Branches:
  refs/heads/master 2ed0f644d -> db69e921b


[ZEPPELIN-848] Add support for encrypted data stored in Amazon S3

### What is this PR for?
Adds support for using the AWS KMS or a custom encryption materials
provider class to encrypt data stored in Amazon S3.  Also a minor
improvement to logic inside the S3 notebook repo when dealing with local files.

### What type of PR is it?
Improvement

### What is the Jira issue?
https://issues.apache.org/jira/browse/ZEPPELIN-848

### How should this be tested?
Running in EMR or another system in AWS is easiest.  Make appropriate changes to the config and use an AWS KMS key

### Questions:
* Does the licenses files need update? -- NO
* Is there breaking changes for older versions? -- NO
* Does this needs documentation? -- YES, changes in storage.md and zeppelin-site.xml.template

Author: Nate Sammons <Na...@nasdaq.com>
Author: Nate Sammons <na...@nasdaq.com>

Closes #886 from natesammons-nasdaq/master and squashes the following commits:

a6e074f [Nate Sammons] Merge remote-tracking branch 'origin/master'
cdd3107 [Nate Sammons] Merge remote-tracking branch 'apache/master'
48b89c0 [Nate Sammons] Update install.md
ff1540b [Nate Sammons] Merge remote-tracking branch 'apache/master'
84709c4 [Nate Sammons] Merge remote-tracking branch 'apache/master'
513361f [Nate Sammons] Update line length
b318c79 [Nate Sammons] Merge remote-tracking branch 'apache/master'
ceb5847 [Nate Sammons] Merge remote-tracking branch 'apache/master'
1475aa0 [Nate Sammons] Merge remote-tracking branch 'apache/master'
84ddd3b [Nate Sammons] Log exception when reloading notebooks
b55b98c [Nate Sammons] Updated exception handling
8628d95 [Nate Sammons] ZEPPELIN-848: Add support for encrypted data stored in Amazon S3


Project: http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/commit/db69e921
Tree: http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/tree/db69e921
Diff: http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/diff/db69e921

Branch: refs/heads/master
Commit: db69e921b0cb289f733ddfff5fe0563e16ec60e2
Parents: 2ed0f64
Author: Nate Sammons <Na...@nasdaq.com>
Authored: Thu May 26 07:55:03 2016 -0700
Committer: Lee moon soo <mo...@apache.org>
Committed: Thu May 26 13:55:47 2016 -0700

----------------------------------------------------------------------
 conf/zeppelin-site.xml.template                 |  27 ++-
 docs/install/install.md                         |  12 ++
 docs/storage/storage.md                         |  80 ++++++---
 .../apache/zeppelin/socket/NotebookServer.java  |   2 +-
 .../zeppelin/conf/ZeppelinConfiguration.java    |  10 ++
 .../zeppelin/notebook/repo/S3NotebookRepo.java  | 169 ++++++++++++-------
 6 files changed, 215 insertions(+), 85 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/db69e921/conf/zeppelin-site.xml.template
----------------------------------------------------------------------
diff --git a/conf/zeppelin-site.xml.template b/conf/zeppelin-site.xml.template
index caf0900..6e65fca 100755
--- a/conf/zeppelin-site.xml.template
+++ b/conf/zeppelin-site.xml.template
@@ -62,7 +62,8 @@
 </property>
 
 
-<!-- If used S3 to storage the notebooks, it is necessary the following folder structure bucketname/username/notebook/ -->
+<!-- Amazon S3 notebook storage -->
+<!-- Creates the following directory structure: s3://{bucket}/{username}/{notebook-id}/note.json -->
 <!--
 <property>
   <name>zeppelin.notebook.s3.user</name>
@@ -89,6 +90,30 @@
 </property>
 -->
 
+<!-- Additionally, encryption is supported for notebook data stored in S3 -->
+<!-- Use the AWS KMS to encrypt data -->
+<!-- If used, the EC2 role assigned to the EMR cluster must have rights to use the given key -->
+<!-- See https://aws.amazon.com/kms/ and http://docs.aws.amazon.com/kms/latest/developerguide/concepts.html -->
+<!--
+<property>
+  <name>zeppelin.notebook.s3.kmsKeyID</name>
+  <value>AWS-KMS-Key-UUID</value>
+  <description>AWS KMS key ID used to encrypt notebook data in S3</description>
+</property>
+-->
+
+<!-- Use a custom encryption materials provider to encrypt data -->
+<!-- No configuration is given to the provider, so you must use system properties or another means to configure -->
+<!-- See https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/model/EncryptionMaterialsProvider.html -->
+<!--
+<property>
+  <name>zeppelin.notebook.s3.encryptionMaterialsProvider</name>
+  <value>provider implementation class name</value>
+  <description>Custom encryption materials provider used to encrypt notebook data in S3</description>
+</property>
+-->
+
+
 <!-- If using Azure for storage use the following settings -->
 <!--
 <property>

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/db69e921/docs/install/install.md
----------------------------------------------------------------------
diff --git a/docs/install/install.md b/docs/install/install.md
index 353fd18..7bbc0d1 100644
--- a/docs/install/install.md
+++ b/docs/install/install.md
@@ -193,6 +193,18 @@ You can configure Zeppelin with both **environment variables** in `conf/zeppelin
     <td>Endpoint for the bucket</td>
   </tr>
   <tr>
+    <td>ZEPPELIN_NOTEBOOK_S3_KMS_KEY_ID</td>
+    <td>zeppelin.notebook.s3.kmsKeyID</td>
+    <td></td>
+    <td>AWS KMS Key ID to use for encrypting data in S3 (optional)</td>
+  </tr>
+  <tr>
+    <td>ZEPPELIN_NOTEBOOK_S3_EMP</td>
+    <td>zeppelin.notebook.s3.encryptionMaterialsProvider</td>
+    <td></td>
+    <td>Class name of a custom S3 encryption materials provider implementation to use for encrypting data in S3 (optional)</td>
+  </tr>
+  <tr>
     <td>ZEPPELIN_NOTEBOOK_AZURE_CONNECTION_STRING</td>
     <td>zeppelin.notebook.azure.connectionString</td>
     <td></td>

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/db69e921/docs/storage/storage.md
----------------------------------------------------------------------
diff --git a/docs/storage/storage.md b/docs/storage/storage.md
index 65dc0ef..92eb42f 100644
--- a/docs/storage/storage.md
+++ b/docs/storage/storage.md
@@ -20,15 +20,16 @@ limitations under the License.
 ### Notebook Storage
 
 Zeppelin has a pluggable notebook storage mechanism controlled by `zeppelin.notebook.storage` configuration option with multiple implementations.
-There are few Notebook storages available for a use out of the box:
+There are few Notebook storage systems available for a use out of the box:
  - (default) all notes are saved in the notebook folder in your local File System - `VFSNotebookRepo`
  - there is also an option to version it using local Git repository - `GitNotebookRepo`
- - another option is Amazon S3 service - `S3NotebookRepo`
+ - another option is Amazon's S3 service - `S3NotebookRepo`
 
-Multiple storages can be used at the same time by providing a comma-separated list of the class-names in the configuration.
+Multiple storage systems can be used at the same time by providing a comma-separated list of the class-names in the configuration.
 By default, only first two of them will be automatically kept in sync by Zeppelin.
 
 </br>
+
 #### Notebook Storage in local Git repository <a name="Git"></a>
 
 To enable versioning for all your local notebooks though a standard Git repository - uncomment the next property in `zeppelin-site.xml` in order to use GitNotebookRepo class:
@@ -42,44 +43,46 @@ To enable versioning for all your local notebooks though a standard Git reposito
 ```
 
 </br>
+
 #### Notebook Storage in S3  <a name="S3"></a>
 
-For notebook storage in S3 you need the AWS credentials, for this there are three options, the environment variable ```AWS_ACCESS_KEY_ID``` and ```AWS_ACCESS_SECRET_KEY```,  credentials file in the folder .aws in you home and IAM role for your instance. For complete the need steps is necessary:
+Notebooks may be stored in S3, and optionally encrypted.  The [``DefaultAWSCredentialsProviderChain``](https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/DefaultAWSCredentialsProviderChain.html) credentials provider is used for credentials and checks the following:
+
+- The ``AWS_ACCESS_KEY_ID`` and ``AWS_SECRET_ACCESS_KEY`` environment variables
+- The ``aws.accessKeyId`` and ``aws.secretKey`` Java System properties
+- Credential profiles file at the default location (````~/.aws/credentials````) used by the AWS CLI
+- Instance profile credentials delivered through the Amazon EC2 metadata service
 
 </br>
-you need the following folder structure on S3
+The following folder structure will be created in S3:
 
 ```
-bucket_name/
-  username/
-    notebook/
-
+s3://bucket_name/username/notebook-id/
 ```
 
-set the environment variable in the file **zeppelin-env.sh**:
+Configure by setting environment variables in the file **zeppelin-env.sh**:
 
 ```
 export ZEPPELIN_NOTEBOOK_S3_BUCKET = bucket_name
 export ZEPPELIN_NOTEBOOK_S3_USER = username
 ```
 
-in the file **zeppelin-site.xml** uncomment and complete the next property:
+Or using the file **zeppelin-site.xml** uncomment and complete the S3 settings:
 
 ```
-<!--If used S3 to storage, it is necessary the following folder structure bucket_name/username/notebook/-->
-<property>
-  <name>zeppelin.notebook.s3.user</name>
-  <value>username</value>
-  <description>user name for s3 folder structure</description>
-</property>
 <property>
   <name>zeppelin.notebook.s3.bucket</name>
   <value>bucket_name</value>
   <description>bucket name for notebook storage</description>
 </property>
+<property>
+  <name>zeppelin.notebook.s3.user</name>
+  <value>username</value>
+  <description>user name for s3 folder structure</description>
+</property>
 ```
 
-uncomment the next property for use S3NotebookRepo class:
+Uncomment the next property for use S3NotebookRepo class:
 
 ```
 <property>
@@ -89,7 +92,7 @@ uncomment the next property for use S3NotebookRepo class:
 </property>
 ```
 
-comment the next property:
+Comment out the next property to disable local notebook storage (the default):
 
 ```
 <property>
@@ -97,4 +100,41 @@ comment the next property:
   <value>org.apache.zeppelin.notebook.repo.VFSNotebookRepo</value>
   <description>notebook persistence layer implementation</description>
 </property>
-```   
+```
+
+#### Data Encryption in S3
+
+##### AWS KMS encryption keys
+
+To use an [AWS KMS](https://aws.amazon.com/kms/) encryption key to encrypt notebooks, set the following environment variable in the file **zeppelin-env.sh**:
+
+```
+export ZEPPELIN_NOTEBOOK_S3_KMS_KEY_ID = kms-key-id
+```
+
+Or using the following setting in **zeppelin-site.xml**:
+```
+<property>
+  <name>zeppelin.notebook.s3.kmsKeyID</name>
+  <value>AWS-KMS-Key-UUID</value>
+  <description>AWS KMS key ID used to encrypt notebook data in S3</description>
+</property>
+```
+
+##### Custom Encryption Materials Provider class
+
+You may use a custom [``EncryptionMaterialsProvider``](https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/model/EncryptionMaterialsProvider.html) class as long as it is available in the classpath and able to initialize itself from system properties or another mechanism.  To use this, set the following environment variable in the file **zeppelin-env.sh**:
+
+
+```
+export ZEPPELIN_NOTEBOOK_S3_EMP = class-name
+```
+
+Or using the following setting in **zeppelin-site.xml**:
+```
+<property>
+  <name>zeppelin.notebook.s3.encryptionMaterialsProvider</name>
+  <value>provider implementation class name</value>
+  <description>Custom encryption materials provider used to encrypt notebook data in S3</description>
+</property>
+```

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/db69e921/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
----------------------------------------------------------------------
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
index 3fb842d..05a0ae8 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
@@ -361,7 +361,7 @@ public class NotebookServer extends WebSocketServlet implements
       try {
         notebook.reloadAllNotes();
       } catch (IOException e) {
-        LOG.error("Fail to reload notes from repository");
+        LOG.error("Fail to reload notes from repository", e);
       }
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/db69e921/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java
index 9eceeed..9142976 100644
--- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java
+++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java
@@ -338,6 +338,14 @@ public class ZeppelinConfiguration extends XMLConfiguration {
     return getString(ConfVars.ZEPPELIN_NOTEBOOK_S3_ENDPOINT);
   }
 
+  public String getS3KMSKeyID() {
+    return getString(ConfVars.ZEPPELIN_NOTEBOOK_S3_KMS_KEY_ID);
+  }
+
+  public String getS3EncryptionMaterialsProviderClass() {
+    return getString(ConfVars.ZEPPELIN_NOTEBOOK_S3_EMP);
+  }
+
   public String getInterpreterDir() {
     return getRelativeDir(ConfVars.ZEPPELIN_INTERPRETER_DIR);
   }
@@ -497,6 +505,8 @@ public class ZeppelinConfiguration extends XMLConfiguration {
     ZEPPELIN_NOTEBOOK_S3_BUCKET("zeppelin.notebook.s3.bucket", "zeppelin"),
     ZEPPELIN_NOTEBOOK_S3_ENDPOINT("zeppelin.notebook.s3.endpoint", "s3.amazonaws.com"),
     ZEPPELIN_NOTEBOOK_S3_USER("zeppelin.notebook.s3.user", "user"),
+    ZEPPELIN_NOTEBOOK_S3_EMP("zeppelin.notebook.s3.encryptionMaterialsProvider", null),
+    ZEPPELIN_NOTEBOOK_S3_KMS_KEY_ID("zeppelin.notebook.s3.kmsKeyID", null),
     ZEPPELIN_NOTEBOOK_AZURE_CONNECTION_STRING("zeppelin.notebook.azure.connectionString", null),
     ZEPPELIN_NOTEBOOK_AZURE_SHARE("zeppelin.notebook.azure.share", "zeppelin"),
     ZEPPELIN_NOTEBOOK_AZURE_USER("zeppelin.notebook.azure.user", "user"),

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/db69e921/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/S3NotebookRepo.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/S3NotebookRepo.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/S3NotebookRepo.java
index 82d321e..451d483 100644
--- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/S3NotebookRepo.java
+++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/S3NotebookRepo.java
@@ -26,6 +26,11 @@ import java.io.Writer;
 import java.util.LinkedList;
 import java.util.List;
 
+import com.amazonaws.auth.AWSCredentialsProvider;
+import com.amazonaws.services.s3.AmazonS3EncryptionClient;
+import com.amazonaws.services.s3.model.EncryptionMaterialsProvider;
+import com.amazonaws.services.s3.model.KMSEncryptionMaterialsProvider;
+import org.apache.commons.io.FileUtils;
 import org.apache.commons.io.IOUtils;
 import org.apache.zeppelin.conf.ZeppelinConfiguration;
 import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars;
@@ -37,7 +42,6 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.amazonaws.AmazonClientException;
-import com.amazonaws.AmazonServiceException;
 import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
 import com.amazonaws.services.s3.AmazonS3;
 import com.amazonaws.services.s3.AmazonS3Client;
@@ -70,69 +74,90 @@ public class S3NotebookRepo implements NotebookRepo {
   //  3. Credential profiles file at the default location (~/.aws/credentials)
   //       shared by all AWS SDKs and the AWS CLI
   //  4. Instance profile credentials delivered through the Amazon EC2 metadata service
-  private AmazonS3 s3client = new AmazonS3Client(new DefaultAWSCredentialsProviderChain());
-  private static String bucketName = "";
-  private static String endpoint = "";
-  private String user = "";
-
-  private ZeppelinConfiguration conf;
+  private final AmazonS3 s3client;
+  private final String bucketName;
+  private final String user;
+  private final ZeppelinConfiguration conf;
 
   public S3NotebookRepo(ZeppelinConfiguration conf) throws IOException {
     this.conf = conf;
     bucketName = conf.getBucketName();
-    endpoint = conf.getEndpoint();
     user = conf.getUser();
-    
-    s3client.setEndpoint(endpoint);
+
+    // always use the default provider chain
+    AWSCredentialsProvider credentialsProvider = new DefaultAWSCredentialsProviderChain();
+
+    // see if we should be encrypting data in S3
+    String kmsKeyID = conf.getS3KMSKeyID();
+    if (kmsKeyID != null) {
+      // use the AWS KMS to encrypt data
+      KMSEncryptionMaterialsProvider emp = new KMSEncryptionMaterialsProvider(kmsKeyID);
+      this.s3client = new AmazonS3EncryptionClient(credentialsProvider, emp);
+    }
+    else if (conf.getS3EncryptionMaterialsProviderClass() != null) {
+      // use a custom encryption materials provider class
+      EncryptionMaterialsProvider emp = createCustomProvider(conf);
+      this.s3client = new AmazonS3EncryptionClient(credentialsProvider, emp);
+    }
+    else {
+      // regular S3
+      this.s3client = new AmazonS3Client(credentialsProvider);
+    }
+
+    // set S3 endpoint to use
+    s3client.setEndpoint(conf.getEndpoint());
+  }
+
+  /**
+   * Create an instance of a custom encryption materials provider class
+   * which supplies encryption keys to use when reading/writing data in S3.
+   */
+  private EncryptionMaterialsProvider createCustomProvider(ZeppelinConfiguration conf)
+      throws IOException {
+    // use a custom encryption materials provider class
+    String empClassname = conf.getS3EncryptionMaterialsProviderClass();
+    EncryptionMaterialsProvider emp;
+    try {
+      Object empInstance = Class.forName(empClassname).newInstance();
+      if (empInstance instanceof EncryptionMaterialsProvider) {
+        emp = (EncryptionMaterialsProvider) empInstance;
+      }
+      else {
+        throw new IOException("Class " + empClassname + " does not implement "
+                + EncryptionMaterialsProvider.class.getName());
+      }
+    }
+    catch (Exception e) {
+      throw new IOException("Unable to instantiate encryption materials provider class "
+              + empClassname + ": " + e, e);
+    }
+
+    return emp;
   }
 
   @Override
   public List<NoteInfo> list() throws IOException {
-    List<NoteInfo> infos = new LinkedList<NoteInfo>();
-    NoteInfo info = null;
+    List<NoteInfo> infos = new LinkedList<>();
+    NoteInfo info;
     try {
       ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
-          .withBucketName(bucketName)
-          .withPrefix(user + "/" + "notebook");
+              .withBucketName(bucketName)
+              .withPrefix(user + "/" + "notebook");
       ObjectListing objectListing;
       do {
         objectListing = s3client.listObjects(listObjectsRequest);
-
-        for (S3ObjectSummary objectSummary :
-          objectListing.getObjectSummaries()) {
-          if (objectSummary.getKey().contains("note.json")) {
-            try {
-              info = getNoteInfo(objectSummary.getKey());
-              if (info != null) {
-                infos.add(info);
-              }
-            } catch (AmazonServiceException ase) {
-              LOG.warn("Caught an AmazonServiceException for some reason.\n" +
-                  "Error Message: {}", ase.getMessage());
-            } catch (AmazonClientException ace) {
-              LOG.info("Caught an AmazonClientException, " +
-                  "which means the client encountered " +
-                  "an internal error while trying to communicate" +
-                  " with S3, " +
-                  "such as not being able to access the network.");
-              LOG.info("Error Message: " + ace.getMessage());
-            } catch (Exception e) {
-              LOG.error("Can't read note ", e);
+        for (S3ObjectSummary objectSummary : objectListing.getObjectSummaries()) {
+          if (objectSummary.getKey().endsWith("note.json")) {
+            info = getNoteInfo(objectSummary.getKey());
+            if (info != null) {
+              infos.add(info);
             }
           }
         }
         listObjectsRequest.setMarker(objectListing.getNextMarker());
       } while (objectListing.isTruncated());
-    } catch (AmazonServiceException ase) {
-      LOG.warn("Caught an AmazonServiceException for some reason.\n" +
-          "Error Message: {}", ase.getMessage());
     } catch (AmazonClientException ace) {
-      LOG.info("Caught an AmazonClientException, " +
-          "which means the client encountered " +
-          "an internal error while trying to communicate" +
-          " with S3, " +
-          "such as not being able to access the network.");
-      LOG.info("Error Message: " + ace.getMessage());
+      throw new IOException("Unable to list objects in S3: " + ace, ace);
     }
     return infos;
   }
@@ -142,19 +167,26 @@ public class S3NotebookRepo implements NotebookRepo {
     gsonBuilder.setPrettyPrinting();
     Gson gson = gsonBuilder.create();
 
-    S3Object s3object = s3client.getObject(new GetObjectRequest(
-        bucketName, key));
+    S3Object s3object;
+    try {
+      s3object = s3client.getObject(new GetObjectRequest(bucketName, key));
+    }
+    catch (AmazonClientException ace) {
+      throw new IOException("Unable to retrieve object from S3: " + ace, ace);
+    }
 
-    InputStream ins = s3object.getObjectContent();
-    String json = IOUtils.toString(ins, conf.getString(ConfVars.ZEPPELIN_ENCODING));
-    ins.close();
-    Note note = gson.fromJson(json, Note.class);
+    Note note;
+    try (InputStream ins = s3object.getObjectContent()) {
+      String json = IOUtils.toString(ins, conf.getString(ConfVars.ZEPPELIN_ENCODING));
+      note = gson.fromJson(json, Note.class);
+    }
 
     for (Paragraph p : note.getParagraphs()) {
       if (p.getStatus() == Status.PENDING || p.getStatus() == Status.RUNNING) {
         p.setStatus(Status.ABORT);
       }
     }
+
     return note;
   }
 
@@ -177,12 +209,18 @@ public class S3NotebookRepo implements NotebookRepo {
     String key = user + "/" + "notebook" + "/" + note.id() + "/" + "note.json";
 
     File file = File.createTempFile("note", "json");
-    file.deleteOnExit();
-    Writer writer = new OutputStreamWriter(new FileOutputStream(file));
-
-    writer.write(json);
-    writer.close();
-    s3client.putObject(new PutObjectRequest(bucketName, key, file));
+    try {
+      Writer writer = new OutputStreamWriter(new FileOutputStream(file));
+      writer.write(json);
+      writer.close();
+      s3client.putObject(new PutObjectRequest(bucketName, key, file));
+    }
+    catch (AmazonClientException ace) {
+      throw new IOException("Unable to store note in S3: " + ace, ace);
+    }
+    finally {
+      FileUtils.deleteQuietly(file);
+    }
   }
 
   @Override
@@ -191,13 +229,18 @@ public class S3NotebookRepo implements NotebookRepo {
     final ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
         .withBucketName(bucketName).withPrefix(key);
 
-    ObjectListing objects = s3client.listObjects(listObjectsRequest);
-    do {
-      for (S3ObjectSummary objectSummary : objects.getObjectSummaries()) {
-        s3client.deleteObject(bucketName, objectSummary.getKey());
-      }
-      objects = s3client.listNextBatchOfObjects(objects);
-    } while (objects.isTruncated());
+    try {
+      ObjectListing objects = s3client.listObjects(listObjectsRequest);
+      do {
+        for (S3ObjectSummary objectSummary : objects.getObjectSummaries()) {
+          s3client.deleteObject(bucketName, objectSummary.getKey());
+        }
+        objects = s3client.listNextBatchOfObjects(objects);
+      } while (objects.isTruncated());
+    }
+    catch (AmazonClientException ace) {
+      throw new IOException("Unable to remove note in S3: " + ace, ace);
+    }
   }
 
   @Override