diff --git a/.gitignore b/.gitignore
index 901c2922699cb..6dda408d30db0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,24 +1,28 @@
-*.tsbuildinfo
-.cdk.staging
-
-.vscode
 # VSCode extension
+.vscode/
 /.favorites.json
+
+# TypeScript incremental build states
+*.tsbuildinfo
+
+# Local state files & OS specifics
 .DS_Store
-node_modules
+node_modules/
 lerna-debug.log
-dist
-pack
+dist/
+pack/
 .BUILD_COMPLETED
-.local-npm
-.tools
-coverage
+.local-npm/
+.tools/
+coverage/
 .nyc_output
 .LAST_BUILD
 *.sw[a-z]
 *~
 
-# we don't want tsconfig at the root
+# We don't want tsconfig at the root
 /tsconfig.json
+
+# CDK Context & Staging files
 cdk.context.json
-tsconfig.tsbuildinfo
+.cdk.staging/
diff --git a/packages/@aws-cdk/assets-docker/lib/adopt-repository/handler.js b/packages/@aws-cdk/assets-docker/lib/adopt-repository/handler.js
index abc883c0c108d..3d0cb009c8773 100644
--- a/packages/@aws-cdk/assets-docker/lib/adopt-repository/handler.js
+++ b/packages/@aws-cdk/assets-docker/lib/adopt-repository/handler.js
@@ -26,10 +26,14 @@ exports.handler = async function(event, context, _callback, respond) {
       }
     }
 
-    const repo = event.ResourceProperties.RepositoryName;
+    let repo = event.ResourceProperties.RepositoryName;
     if (!repo) {
       throw new Error('Missing required property "RepositoryName"');
     }
+    const isRepoUri = repo.match(/^(\d+\.dkr\.ecr\.[^.]+\.[^/]+\/)(.+)$/i);
+    if (isRepoUri) {
+      repo = isRepoUri[2];
+    }
 
     const adopter = await getAdopter(repo);
     if (event.RequestType === 'Delete') {
diff --git a/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts b/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts
index 6990201905ae7..397afb6828b0e 100644
--- a/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts
+++ b/packages/@aws-cdk/assets-docker/lib/adopted-repository.ts
@@ -59,6 +59,10 @@ export class AdoptedRepository extends ecr.RepositoryBase {
         PolicyDocument: this.policyDocument
       }
     });
+    if (fn.role) {
+      // Need to explicitly depend on the role's policies, so they are applied before we try to use them
+      adopter.node.addDependency(fn.role);
+    }
 
     // we use the Fn::GetAtt with the RepositoryName returned by the custom
     // resource in order to implicitly create a dependency between consumers
diff --git a/packages/@aws-cdk/assets-docker/lib/image-asset.ts b/packages/@aws-cdk/assets-docker/lib/image-asset.ts
index 0644af975dfde..3eccf231d8199 100644
--- a/packages/@aws-cdk/assets-docker/lib/image-asset.ts
+++ b/packages/@aws-cdk/assets-docker/lib/image-asset.ts
@@ -6,7 +6,7 @@ import fs = require('fs');
 import path = require('path');
 import { AdoptedRepository } from './adopted-repository';
 
-export interface DockerImageAssetProps {
+export interface DockerImageAssetProps extends assets.CopyOptions {
   /**
    * The directory where the Dockerfile is stored
    */
@@ -36,7 +36,7 @@ export interface DockerImageAssetProps {
  *
  * The image will be created in build time and uploaded to an ECR repository.
  */
-export class DockerImageAsset extends cdk.Construct {
+export class DockerImageAsset extends cdk.Construct implements assets.IAsset {
   /**
    * The full URI of the image (including a tag). Use this reference to pull
    * the asset.
@@ -48,6 +48,9 @@ export class DockerImageAsset extends cdk.Construct {
    */
   public repository: ecr.IRepository;
 
+  public readonly sourceHash: string;
+  public readonly artifactHash: string;
+
   /**
    * Directory where the source files are stored
    */
@@ -66,10 +69,12 @@ export class DockerImageAsset extends cdk.Construct {
     }
 
     const staging = new assets.Staging(this, 'Staging', {
+      ...props,
       sourcePath: dir
     });
 
     this.directory = staging.stagedPath;
+    this.sourceHash = staging.sourceHash;
 
     const imageNameParameter = new cdk.CfnParameter(this, 'ImageName', {
       type: 'String',
@@ -77,9 +82,10 @@ export class DockerImageAsset extends cdk.Construct {
     });
 
     const asset: cxapi.ContainerImageAssetMetadataEntry = {
+      id: this.node.uniqueId,
       packaging: 'container-image',
       path: this.directory,
-      id: this.node.uniqueId,
+      sourceHash: this.sourceHash,
       imageNameParameter: imageNameParameter.logicalId,
       repositoryName: props.repositoryName,
       buildArgs: props.buildArgs
@@ -87,10 +93,11 @@ export class DockerImageAsset extends cdk.Construct {
 
     this.node.addMetadata(cxapi.ASSET_METADATA, asset);
 
-    // parse repository name and tag from the parameter (<REPO_NAME>:<TAG>)
-    const components = cdk.Fn.split(':', imageNameParameter.stringValue);
+    // Parse repository name and tag from the parameter (<REPO_NAME>@sha256:<TAG>)
+    // Example: cdk/cdkexampleimageb2d7f504@sha256:72c4f956379a43b5623d529ddd969f6826dde944d6221f445ff3e7add9875500
+    const components = cdk.Fn.split('@sha256:', imageNameParameter.stringValue);
     const repositoryName = cdk.Fn.select(0, components).toString();
-    const imageTag = cdk.Fn.select(1, components).toString();
+    const imageSha = cdk.Fn.select(1, components).toString();
 
     // Require that repository adoption happens first, so we route the
     // input ARN into the Custom Resource and then get the URI which we use to
@@ -99,6 +106,7 @@ export class DockerImageAsset extends cdk.Construct {
     // If adoption fails (because the repository might be twice-adopted), we
     // haven't already started using the image.
     this.repository = new AdoptedRepository(this, 'AdoptRepository', { repositoryName });
-    this.imageUri = this.repository.repositoryUriForTag(imageTag);
+    this.imageUri = `${this.repository.repositoryUri}@sha256:${imageSha}`;
+    this.artifactHash = imageSha;
   }
 }
diff --git a/packages/@aws-cdk/assets-docker/test/integ.assets-docker.expected.json b/packages/@aws-cdk/assets-docker/test/integ.assets-docker.expected.json
new file mode 100644
index 0000000000000..54d3f8bb9f8c4
--- /dev/null
+++ b/packages/@aws-cdk/assets-docker/test/integ.assets-docker.expected.json
@@ -0,0 +1,326 @@
+{
+  "Parameters": {
+    "DockerImageImageName266E5998": {
+      "Type": "String",
+      "Description": "ECR repository name and tag asset \"integ-assets-docker/DockerImage\""
+    },
+    "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3Bucket92AB06B6": {
+      "Type": "String",
+      "Description": "S3 bucket for asset \"integ-assets-docker/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\""
+    },
+    "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276": {
+      "Type": "String",
+      "Description": "S3 key for asset version \"integ-assets-docker/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\""
+    },
+    "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeArtifactHash8BCBAA49": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"integ-assets-docker/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\""
+    }
+  },
+  "Resources": {
+    "DockerImageAdoptRepositoryA86481BC": {
+      "Type": "Custom::ECRAdoptedRepository",
+      "Properties": {
+        "ServiceToken": {
+          "Fn::GetAtt": [
+            "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c52BE89E9",
+            "Arn"
+          ]
+        },
+        "RepositoryName": {
+          "Fn::Select": [
+            0,
+            {
+              "Fn::Split": [
+                "@sha256:",
+                {
+                  "Ref": "DockerImageImageName266E5998"
+                }
+              ]
+            }
+          ]
+        }
+      },
+      "DependsOn": [
+        "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C",
+        "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17"
+      ]
+    },
+    "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17": {
+      "Type": "AWS::IAM::Role",
+      "Properties": {
+        "AssumeRolePolicyDocument": {
+          "Statement": [
+            {
+              "Action": "sts:AssumeRole",
+              "Effect": "Allow",
+              "Principal": {
+                "Service": {
+                  "Fn::Join": [
+                    "",
+                    [
+                      "lambda.",
+                      {
+                        "Ref": "AWS::URLSuffix"
+                      }
+                    ]
+                  ]
+                }
+              }
+            }
+          ],
+          "Version": "2012-10-17"
+        },
+        "ManagedPolicyArns": [
+          {
+            "Fn::Join": [
+              "",
+              [
+                "arn:",
+                {
+                  "Ref": "AWS::Partition"
+                },
+                ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
+              ]
+            ]
+          }
+        ]
+      }
+    },
+    "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C": {
+      "Type": "AWS::IAM::Policy",
+      "Properties": {
+        "PolicyDocument": {
+          "Statement": [
+            {
+              "Action": [
+                "ecr:GetRepositoryPolicy",
+                "ecr:SetRepositoryPolicy",
+                "ecr:DeleteRepository",
+                "ecr:ListImages",
+                "ecr:BatchDeleteImage"
+              ],
+              "Effect": "Allow",
+              "Resource": {
+                "Fn::Join": [
+                  "",
+                  [
+                    "arn:",
+                    {
+                      "Ref": "AWS::Partition"
+                    },
+                    ":ecr:",
+                    {
+                      "Ref": "AWS::Region"
+                    },
+                    ":",
+                    {
+                      "Ref": "AWS::AccountId"
+                    },
+                    ":repository/",
+                    {
+                      "Fn::Select": [
+                        0,
+                        {
+                          "Fn::Split": [
+                            "@sha256:",
+                            {
+                              "Ref": "DockerImageImageName266E5998"
+                            }
+                          ]
+                        }
+                      ]
+                    }
+                  ]
+                ]
+              }
+            }
+          ],
+          "Version": "2012-10-17"
+        },
+        "PolicyName": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C",
+        "Roles": [
+          {
+            "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17"
+          }
+        ]
+      }
+    },
+    "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c52BE89E9": {
+      "Type": "AWS::Lambda::Function",
+      "Properties": {
+        "Code": {
+          "S3Bucket": {
+            "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3Bucket92AB06B6"
+          },
+          "S3Key": {
+            "Fn::Join": [
+              "",
+              [
+                {
+                  "Fn::Select": [
+                    0,
+                    {
+                      "Fn::Split": [
+                        "||",
+                        {
+                          "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276"
+                        }
+                      ]
+                    }
+                  ]
+                },
+                {
+                  "Fn::Select": [
+                    1,
+                    {
+                      "Fn::Split": [
+                        "||",
+                        {
+                          "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276"
+                        }
+                      ]
+                    }
+                  ]
+                }
+              ]
+            ]
+          }
+        },
+        "Handler": "handler.handler",
+        "Role": {
+          "Fn::GetAtt": [
+            "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17",
+            "Arn"
+          ]
+        },
+        "Runtime": "nodejs8.10",
+        "Timeout": 300
+      },
+      "DependsOn": [
+        "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C",
+        "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17"
+      ]
+    }
+  },
+  "Outputs": {
+    "ArtifactHash": {
+      "Value": {
+        "Fn::Select": [
+          1,
+          {
+            "Fn::Split": [
+              "@sha256:",
+              {
+                "Ref": "DockerImageImageName266E5998"
+              }
+            ]
+          }
+        ]
+      }
+    },
+    "ImageUri": {
+      "Value": {
+        "Fn::Join": [
+          "",
+          [
+            {
+              "Fn::Select": [
+                4,
+                {
+                  "Fn::Split": [
+                    ":",
+                    {
+                      "Fn::Join": [
+                        "",
+                        [
+                          "arn:",
+                          {
+                            "Ref": "AWS::Partition"
+                          },
+                          ":ecr:",
+                          {
+                            "Ref": "AWS::Region"
+                          },
+                          ":",
+                          {
+                            "Ref": "AWS::AccountId"
+                          },
+                          ":repository/",
+                          {
+                            "Fn::GetAtt": [
+                              "DockerImageAdoptRepositoryA86481BC",
+                              "RepositoryName"
+                            ]
+                          }
+                        ]
+                      ]
+                    }
+                  ]
+                }
+              ]
+            },
+            ".dkr.ecr.",
+            {
+              "Fn::Select": [
+                3,
+                {
+                  "Fn::Split": [
+                    ":",
+                    {
+                      "Fn::Join": [
+                        "",
+                        [
+                          "arn:",
+                          {
+                            "Ref": "AWS::Partition"
+                          },
+                          ":ecr:",
+                          {
+                            "Ref": "AWS::Region"
+                          },
+                          ":",
+                          {
+                            "Ref": "AWS::AccountId"
+                          },
+                          ":repository/",
+                          {
+                            "Fn::GetAtt": [
+                              "DockerImageAdoptRepositoryA86481BC",
+                              "RepositoryName"
+                            ]
+                          }
+                        ]
+                      ]
+                    }
+                  ]
+                }
+              ]
+            },
+            ".amazonaws.com/",
+            {
+              "Fn::GetAtt": [
+                "DockerImageAdoptRepositoryA86481BC",
+                "RepositoryName"
+              ]
+            },
+            "@sha256:",
+            {
+              "Fn::Select": [
+                1,
+                {
+                  "Fn::Split": [
+                    "@sha256:",
+                    {
+                      "Ref": "DockerImageImageName266E5998"
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        ]
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/packages/@aws-cdk/assets-docker/test/integ.assets-docker.ts b/packages/@aws-cdk/assets-docker/test/integ.assets-docker.ts
new file mode 100644
index 0000000000000..91fe6afbd384c
--- /dev/null
+++ b/packages/@aws-cdk/assets-docker/test/integ.assets-docker.ts
@@ -0,0 +1,15 @@
+import cdk = require('@aws-cdk/cdk');
+import path = require('path');
+import assets = require('../lib');
+
+const app = new cdk.App();
+const stack = new cdk.Stack(app, 'integ-assets-docker');
+
+const asset = new assets.DockerImageAsset(stack, 'DockerImage', {
+  directory: path.join(__dirname, 'demo-image'),
+});
+
+new cdk.CfnOutput(stack, 'ArtifactHash', { value: asset.artifactHash });
+new cdk.CfnOutput(stack, 'ImageUri', { value: asset.imageUri });
+
+app.run();
diff --git a/packages/@aws-cdk/assets-docker/test/test.image-asset.ts b/packages/@aws-cdk/assets-docker/test/test.image-asset.ts
index 6374e0381ee15..869b596bf1421 100644
--- a/packages/@aws-cdk/assets-docker/test/test.image-asset.ts
+++ b/packages/@aws-cdk/assets-docker/test/test.image-asset.ts
@@ -121,7 +121,7 @@ export = {
     // THEN
     expect(stack).to(haveResource('Custom::ECRAdoptedRepository', {
       "RepositoryName": {
-        "Fn::Select": [ 0, { "Fn::Split": [ ":", { "Ref": "ImageImageName5E684353" } ] } ]
+        "Fn::Select": [ 0, { "Fn::Split": [ "@sha256:", { "Ref": "ImageImageName5E684353" } ] } ]
       },
       "PolicyDocument": {
         "Statement": [
@@ -182,8 +182,8 @@ export = {
 
     app.run();
 
-    test.ok(fs.existsSync('.stage-me/96e3ffe92a19cbaa6c558942f7a60246/Dockerfile'));
-    test.ok(fs.existsSync('.stage-me/96e3ffe92a19cbaa6c558942f7a60246/index.py'));
+    test.ok(fs.existsSync('.stage-me/1a17a141505ac69144931fe263d130f4612251caa4bbbdaf68a44ed0f405439c/Dockerfile'));
+    test.ok(fs.existsSync('.stage-me/1a17a141505ac69144931fe263d130f4612251caa4bbbdaf68a44ed0f405439c/index.py'));
     test.done();
   }
 };
diff --git a/packages/@aws-cdk/assets/lib/asset.ts b/packages/@aws-cdk/assets/lib/asset.ts
index 26aa2ae915c59..8653bbe04de39 100644
--- a/packages/@aws-cdk/assets/lib/asset.ts
+++ b/packages/@aws-cdk/assets/lib/asset.ts
@@ -4,6 +4,7 @@ import cdk = require('@aws-cdk/cdk');
 import cxapi = require('@aws-cdk/cx-api');
 import fs = require('fs');
 import path = require('path');
+import { CopyOptions } from './fs/copy-options';
 import { Staging } from './staging';
 
 /**
@@ -22,7 +23,7 @@ export enum AssetPackaging {
   File = 'file',
 }
 
-export interface AssetProps {
+export interface AssetProps extends CopyOptions {
   /**
    * The disk location of the asset.
    */
@@ -42,11 +43,29 @@ export interface AssetProps {
   readonly readers?: iam.IGrantable[];
 }
 
+export interface IAsset extends cdk.IConstruct {
+  /**
+   * A hash of the source of this asset, which is available at construction time. As this is a plain
+   * string, it can be used in construct IDs in order to enforce creation of a new resource when
+   * the content hash has changed.
+   */
+  readonly sourceHash: string;
+
+  /**
+   * A hash of the bundle for of this asset, which is only available at deployment time. As this is
+   * a late-bound token, it may not be used in construct IDs, but can be passed as a resource
+   * property in order to force a change on a resource when an asset is effectively updated. This is
+   * more reliable than `sourceHash` in particular for assets which bundling phase involve external
+   * resources that can change over time (such as Docker image builds).
+   */
+  readonly artifactHash: string;
+}
+
 /**
  * An asset represents a local file or directory, which is automatically uploaded to S3
  * and then can be referenced within a CDK application.
  */
-export class Asset extends cdk.Construct {
+export class Asset extends cdk.Construct implements IAsset {
   /**
    * Attribute that represents the name of the bucket this asset exists in.
    */
@@ -82,6 +101,9 @@ export class Asset extends cdk.Construct {
    */
   public readonly isZipArchive: boolean;
 
+  public readonly sourceHash: string;
+  public readonly artifactHash: string;
+
   /**
    * The S3 prefix where all different versions of this asset are stored
    */
@@ -92,8 +114,10 @@ export class Asset extends cdk.Construct {
 
     // stage the asset source (conditionally).
     const staging = new Staging(this, 'Stage', {
-      sourcePath: path.resolve(props.path)
+      ...props,
+      sourcePath: path.resolve(props.path),
     });
+    this.sourceHash = staging.sourceHash;
 
     this.assetPath = staging.stagedPath;
 
@@ -119,10 +143,16 @@ export class Asset extends cdk.Construct {
       description: `S3 key for asset version "${this.node.path}"`
     });
 
+    const hashParam = new cdk.CfnParameter(this, 'ArtifactHash', {
+      description: `Artifact hash for asset "${this.node.path}"`,
+      type: 'String',
+    });
+
     this.s3BucketName = bucketParam.stringValue;
     this.s3Prefix = cdk.Fn.select(0, cdk.Fn.split(cxapi.ASSET_PREFIX_SEPARATOR, keyParam.stringValue)).toString();
     const s3Filename = cdk.Fn.select(1, cdk.Fn.split(cxapi.ASSET_PREFIX_SEPARATOR, keyParam.stringValue)).toString();
     this.s3ObjectKey = `${this.s3Prefix}${s3Filename}`;
+    this.artifactHash = hashParam.stringValue;
 
     this.bucket = s3.Bucket.fromBucketName(this, 'AssetBucket', this.s3BucketName);
 
@@ -137,8 +167,11 @@ export class Asset extends cdk.Construct {
       path: this.assetPath,
       id: this.node.uniqueId,
       packaging: props.packaging,
+      sourceHash: this.sourceHash,
+
       s3BucketParameter: bucketParam.logicalId,
       s3KeyParameter: keyParam.logicalId,
+      artifactHashParameter: hashParam.logicalId,
     };
 
     this.node.addMetadata(cxapi.ASSET_METADATA, asset);
diff --git a/packages/@aws-cdk/assets/lib/fs/copy-options.ts b/packages/@aws-cdk/assets/lib/fs/copy-options.ts
new file mode 100644
index 0000000000000..ac8d8b5686f0d
--- /dev/null
+++ b/packages/@aws-cdk/assets/lib/fs/copy-options.ts
@@ -0,0 +1,20 @@
+import { FollowMode } from './follow-mode';
+
+/**
+ * Obtains applied when copying directories into the staging location.
+ */
+export interface CopyOptions {
+  /**
+   * A strategy for how to handle symlinks.
+   *
+   * @default Never
+   */
+  readonly follow?: FollowMode;
+
+  /**
+   * Glob patterns to exclude from the copy.
+   *
+   * @default nothing is excluded
+   */
+  readonly exclude?: string[];
+}
diff --git a/packages/@aws-cdk/assets/lib/fs/copy.ts b/packages/@aws-cdk/assets/lib/fs/copy.ts
index 6ea1f2a6e5f8c..ea011bb3a22a6 100644
--- a/packages/@aws-cdk/assets/lib/fs/copy.ts
+++ b/packages/@aws-cdk/assets/lib/fs/copy.ts
@@ -1,19 +1,8 @@
 import fs = require('fs');
-import minimatch = require('minimatch');
 import path = require('path');
+import { CopyOptions } from './copy-options';
 import { FollowMode } from './follow-mode';
-
-export interface CopyOptions {
-  /**
-   * @default External only follows symlinks that are external to the source directory
-   */
-  follow?: FollowMode;
-
-  /**
-   * glob patterns to exclude from the copy.
-   */
-  exclude?: string[];
-}
+import { shouldExclude, shouldFollow } from './utils';
 
 export function copyDirectory(srcDir: string, destDir: string, options: CopyOptions = { }, rootDir?: string) {
   const follow = options.follow !== undefined ? options.follow : FollowMode.External;
@@ -29,7 +18,7 @@ export function copyDirectory(srcDir: string, destDir: string, options: CopyOpti
   for (const file of files) {
     const sourceFilePath = path.join(srcDir, file);
 
-    if (shouldExclude(path.relative(rootDir, sourceFilePath))) {
+    if (shouldExclude(exclude, path.relative(rootDir, sourceFilePath))) {
       continue;
     }
 
@@ -45,10 +34,8 @@ export function copyDirectory(srcDir: string, destDir: string, options: CopyOpti
       // determine if this is an external link (i.e. the target's absolute path
       // is outside of the root directory).
       const targetPath = path.normalize(path.resolve(srcDir, target));
-      const rootPath = path.normalize(rootDir);
-      const external = !targetPath.startsWith(rootPath);
 
-      if (follow === FollowMode.External && external) {
+      if (shouldFollow(follow, rootDir, targetPath)) {
         stat = fs.statSync(sourceFilePath);
       } else {
         fs.symlinkSync(target, destFilePath);
@@ -67,23 +54,4 @@ export function copyDirectory(srcDir: string, destDir: string, options: CopyOpti
       stat = undefined;
     }
   }
-
-  function shouldExclude(filePath: string): boolean {
-    let excludeOutput = false;
-
-    for (const pattern of exclude) {
-      const negate = pattern.startsWith('!');
-      const match = minimatch(filePath, pattern, { matchBase: true, flipNegate: true });
-
-      if (!negate && match) {
-        excludeOutput = true;
-      }
-
-      if (negate && match) {
-        excludeOutput = false;
-      }
-    }
-
-    return excludeOutput;
-  }
-}
\ No newline at end of file
+}
diff --git a/packages/@aws-cdk/assets/lib/fs/fingerprint.ts b/packages/@aws-cdk/assets/lib/fs/fingerprint.ts
index 06cdb6a0ed2aa..7ff15fe38383c 100644
--- a/packages/@aws-cdk/assets/lib/fs/fingerprint.ts
+++ b/packages/@aws-cdk/assets/lib/fs/fingerprint.ts
@@ -1,29 +1,21 @@
 import crypto = require('crypto');
 import fs = require('fs');
 import path = require('path');
+import { CopyOptions } from './copy-options';
 import { FollowMode } from './follow-mode';
+import { shouldExclude, shouldFollow } from './utils';
 
 const BUFFER_SIZE = 8 * 1024;
+const CTRL_SOH = '\x01';
+const CTRL_SOT = '\x02';
+const CTRL_ETX = '\x03';
 
-export interface FingerprintOptions {
+export interface FingerprintOptions extends CopyOptions {
   /**
    * Extra information to encode into the fingerprint (e.g. build instructions
    * and other inputs)
    */
   extra?: string;
-
-  /**
-   * List of exclude patterns (see `CopyOptions`)
-   * @default include all files
-   */
-  exclude?: string[];
-
-  /**
-   * What to do when we encounter symlinks.
-   * @default External only follows symlinks that are external to the source
-   * directory
-   */
-  follow?: FollowMode;
 }
 
 /**
@@ -38,49 +30,64 @@ export interface FingerprintOptions {
  * @param options Fingerprinting options
  */
 export function fingerprint(fileOrDirectory: string, options: FingerprintOptions = { }) {
-  const follow = options.follow !== undefined ? options.follow : FollowMode.External;
-  const hash = crypto.createHash('md5');
-  addToHash(fileOrDirectory);
-
-  hash.update(`==follow==${follow}==\n\n`);
+  const hash = crypto.createHash('sha256');
+  _hashField(hash, 'options.extra', options.extra || '');
+  const follow = options.follow || FollowMode.External;
+  _hashField(hash, 'options.follow', follow);
 
-  if (options.extra) {
-    hash.update(`==extra==${options.extra}==\n\n`);
-  }
-
-  for (const ex of options.exclude || []) {
-    hash.update(`==exclude==${ex}==\n\n`);
-  }
+  const rootDirectory = fs.statSync(fileOrDirectory).isDirectory()
+    ? fileOrDirectory
+    : path.dirname(fileOrDirectory);
+  const exclude = options.exclude || [];
+  _processFileOrDirectory(fileOrDirectory);
 
   return hash.digest('hex');
 
-  function addToHash(pathToAdd: string) {
-    hash.update('==\n');
-    const relativePath = path.relative(fileOrDirectory, pathToAdd);
-    hash.update(relativePath + '\n');
-    hash.update('~~~~~~~~~~~~~~~~~~\n');
-    const stat = fs.statSync(pathToAdd);
+  function _processFileOrDirectory(symbolicPath: string, realPath = symbolicPath) {
+    if (shouldExclude(exclude, symbolicPath)) {
+      return;
+    }
+
+    const stat = fs.lstatSync(realPath);
+    const relativePath = path.relative(fileOrDirectory, symbolicPath);
 
     if (stat.isSymbolicLink()) {
-      const target = fs.readlinkSync(pathToAdd);
-      hash.update(target);
+      const linkTarget = fs.readlinkSync(realPath);
+      const resolvedLinkTarget = path.resolve(path.dirname(realPath), linkTarget);
+      if (shouldFollow(follow, rootDirectory, resolvedLinkTarget)) {
+        _processFileOrDirectory(symbolicPath, resolvedLinkTarget);
+      } else {
+        _hashField(hash, `link:${relativePath}`, linkTarget);
+      }
+    } else if (stat.isFile()) {
+      _hashField(hash, `file:${relativePath}`, _contentFingerprint(realPath, stat));
     } else if (stat.isDirectory()) {
-      for (const file of fs.readdirSync(pathToAdd)) {
-        addToHash(path.join(pathToAdd, file));
+      for (const item of fs.readdirSync(realPath).sort()) {
+        _processFileOrDirectory(path.join(symbolicPath, item), path.join(realPath, item));
       }
     } else {
-      const file = fs.openSync(pathToAdd, 'r');
-      const buffer = Buffer.alloc(BUFFER_SIZE);
+      throw new Error(`Unable to hash ${symbolicPath}: it is neither a file nor a directory`);
+    }
+  }
+}
 
-      try {
-        let bytesRead;
-        do {
-          bytesRead = fs.readSync(file, buffer, 0, BUFFER_SIZE, null);
-          hash.update(buffer.slice(0, bytesRead));
-        } while (bytesRead === BUFFER_SIZE);
-      } finally {
-        fs.closeSync(file);
-      }
+function _contentFingerprint(file: string, stat: fs.Stats): string {
+  const hash = crypto.createHash('sha256');
+  const buffer = Buffer.alloc(BUFFER_SIZE);
+  // tslint:disable-next-line: no-bitwise
+  const fd = fs.openSync(file, fs.constants.O_DSYNC | fs.constants.O_RDONLY | fs.constants.O_SYNC);
+  try {
+    let read = 0;
+    // tslint:disable-next-line: no-conditional-assignment
+    while ((read = fs.readSync(fd, buffer, 0, BUFFER_SIZE, null)) !== 0) {
+      hash.update(buffer.slice(0, read));
     }
+  } finally {
+    fs.closeSync(fd);
   }
-}
\ No newline at end of file
+  return `${stat.size}:${hash.digest('hex')}`;
+}
+
+function _hashField(hash: crypto.Hash, header: string, value: string | Buffer | DataView) {
+  hash.update(CTRL_SOH).update(header).update(CTRL_SOT).update(value).update(CTRL_ETX);
+}
diff --git a/packages/@aws-cdk/assets/lib/fs/follow-mode.ts b/packages/@aws-cdk/assets/lib/fs/follow-mode.ts
index 02ecebfaaa0a7..9334328982236 100644
--- a/packages/@aws-cdk/assets/lib/fs/follow-mode.ts
+++ b/packages/@aws-cdk/assets/lib/fs/follow-mode.ts
@@ -26,4 +26,4 @@ export enum FollowMode {
    * If the copy operation runs into an external symlink, it will fail.
    */
   BlockExternal = 'internal-only',
-}
\ No newline at end of file
+}
diff --git a/packages/@aws-cdk/assets/lib/fs/index.ts b/packages/@aws-cdk/assets/lib/fs/index.ts
index 31b1f468bbdfc..a66267535075d 100644
--- a/packages/@aws-cdk/assets/lib/fs/index.ts
+++ b/packages/@aws-cdk/assets/lib/fs/index.ts
@@ -1,3 +1,4 @@
+export * from './copy';
+export * from './copy-options';
 export * from './fingerprint';
 export * from './follow-mode';
-export * from './copy';
\ No newline at end of file
diff --git a/packages/@aws-cdk/assets/lib/fs/utils.ts b/packages/@aws-cdk/assets/lib/fs/utils.ts
new file mode 100644
index 0000000000000..7f5ec315538c7
--- /dev/null
+++ b/packages/@aws-cdk/assets/lib/fs/utils.ts
@@ -0,0 +1,60 @@
+import fs = require('fs');
+import minimatch = require('minimatch');
+import path = require('path');
+import { FollowMode } from './follow-mode';
+
+/**
+ * Determines whether a given file should be excluded or not based on given
+ * exclusion glob patterns.
+ *
+ * @param exclude  exclusion patterns
+ * @param filePath file apth to be assessed against the pattern
+ *
+ * @returns `true` if the file should be excluded
+ */
+export function shouldExclude(exclude: string[], filePath: string): boolean {
+  let excludeOutput = false;
+
+  for (const pattern of exclude) {
+    const negate = pattern.startsWith('!');
+    const match = minimatch(filePath, pattern, { matchBase: true, flipNegate: true });
+
+    if (!negate && match) {
+      excludeOutput = true;
+    }
+
+    if (negate && match) {
+      excludeOutput = false;
+    }
+  }
+
+  return excludeOutput;
+}
+
+/**
+ * Determines whether a symlink should be followed or not, based on a FollowMode.
+ *
+ * @param mode       the FollowMode.
+ * @param sourceRoot the root of the source tree.
+ * @param realPath   the real path of the target of the symlink.
+ *
+ * @returns true if the link should be followed.
+ */
+export function shouldFollow(mode: FollowMode, sourceRoot: string, realPath: string): boolean {
+  switch (mode) {
+    case FollowMode.Always:
+      return fs.existsSync(realPath);
+    case FollowMode.External:
+      return !_isInternal() && fs.existsSync(realPath);
+    case FollowMode.BlockExternal:
+      return _isInternal() && fs.existsSync(realPath);
+    case FollowMode.Never:
+      return false;
+    default:
+      throw new Error(`Unsupported FollowMode: ${mode}`);
+  }
+
+  function _isInternal(): boolean {
+    return path.resolve(realPath).startsWith(path.resolve(sourceRoot));
+  }
+}
diff --git a/packages/@aws-cdk/assets/lib/index.ts b/packages/@aws-cdk/assets/lib/index.ts
index 24ddffa892f0e..e57823463b2aa 100644
--- a/packages/@aws-cdk/assets/lib/index.ts
+++ b/packages/@aws-cdk/assets/lib/index.ts
@@ -1,2 +1,4 @@
 export * from './asset';
+export * from './fs/copy-options';
+export * from './fs/follow-mode';
 export * from './staging';
diff --git a/packages/@aws-cdk/assets/lib/staging.ts b/packages/@aws-cdk/assets/lib/staging.ts
index 9d55e957c95aa..05e465acb4227 100644
--- a/packages/@aws-cdk/assets/lib/staging.ts
+++ b/packages/@aws-cdk/assets/lib/staging.ts
@@ -2,9 +2,9 @@ import { Construct, Token } from '@aws-cdk/cdk';
 import cxapi = require('@aws-cdk/cx-api');
 import fs = require('fs');
 import path = require('path');
-import { copyDirectory, fingerprint } from './fs';
+import { copyDirectory, CopyOptions, fingerprint } from './fs';
 
-export interface StagingProps {
+export interface StagingProps extends CopyOptions {
   readonly sourcePath: string;
 }
 
@@ -41,6 +41,13 @@ export class Staging extends Construct {
    */
   public readonly sourcePath: string;
 
+  /**
+   * A cryptographic hash of the source document(s).
+   */
+  public readonly sourceHash: string;
+
+  private readonly copyOptions: CopyOptions;
+
   /**
    * The asset path after "prepare" is called.
    *
@@ -53,6 +60,8 @@ export class Staging extends Construct {
     super(scope, id);
 
     this.sourcePath = props.sourcePath;
+    this.copyOptions = props;
+    this.sourceHash = fingerprint(this.sourcePath, props);
     this.stagedPath = new Token(() => this._preparedAssetPath).toString();
   }
 
@@ -67,8 +76,7 @@ export class Staging extends Construct {
       fs.mkdirSync(stagingDir);
     }
 
-    const hash = fingerprint(this.sourcePath);
-    const targetPath = path.join(stagingDir, hash + path.extname(this.sourcePath));
+    const targetPath = path.join(stagingDir, this.sourceHash + path.extname(this.sourcePath));
 
     this._preparedAssetPath = targetPath;
 
@@ -83,9 +91,9 @@ export class Staging extends Construct {
       fs.copyFileSync(this.sourcePath, targetPath);
     } else if (stat.isDirectory()) {
       fs.mkdirSync(targetPath);
-      copyDirectory(this.sourcePath, targetPath);
+      copyDirectory(this.sourcePath, targetPath, this.copyOptions);
     } else {
       throw new Error(`Unknown file type: ${this.sourcePath}`);
     }
   }
-}
\ No newline at end of file
+}
diff --git a/packages/@aws-cdk/assets/package-lock.json b/packages/@aws-cdk/assets/package-lock.json
index c43201dfaeada..82d07dd1e76e1 100644
--- a/packages/@aws-cdk/assets/package-lock.json
+++ b/packages/@aws-cdk/assets/package-lock.json
@@ -4,12 +4,60 @@
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
+    "@sinonjs/commons": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz",
+      "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==",
+      "dev": true,
+      "requires": {
+        "type-detect": "4.0.8"
+      }
+    },
+    "@sinonjs/formatio": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz",
+      "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==",
+      "dev": true,
+      "requires": {
+        "@sinonjs/commons": "^1",
+        "@sinonjs/samsam": "^3.1.0"
+      }
+    },
+    "@sinonjs/samsam": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.1.tgz",
+      "integrity": "sha512-wRSfmyd81swH0hA1bxJZJ57xr22kC07a1N4zuIL47yTS04bDk6AoCkczcqHEjcRPmJ+FruGJ9WBQiJwMtIElFw==",
+      "dev": true,
+      "requires": {
+        "@sinonjs/commons": "^1.0.2",
+        "array-from": "^2.1.1",
+        "lodash": "^4.17.11"
+      }
+    },
+    "@sinonjs/text-encoding": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz",
+      "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
+      "dev": true
+    },
     "@types/minimatch": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
       "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
       "dev": true
     },
+    "@types/sinon": {
+      "version": "7.0.11",
+      "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.0.11.tgz",
+      "integrity": "sha512-6ee09Ugx6GyEr0opUIakmxIWFNmqYPjkqa3/BuxCBokA0klsOLPgMD5K4q40lH7/yZVuJVzOfQpd7pipwjngkQ==",
+      "dev": true
+    },
+    "array-from": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz",
+      "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=",
+      "dev": true
+    },
     "balanced-match": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@@ -29,6 +77,42 @@
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
       "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
     },
+    "diff": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+      "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+      "dev": true
+    },
+    "has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+      "dev": true
+    },
+    "isarray": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+      "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+      "dev": true
+    },
+    "just-extend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz",
+      "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==",
+      "dev": true
+    },
+    "lodash": {
+      "version": "4.17.11",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+      "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
+      "dev": true
+    },
+    "lolex": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.0.1.tgz",
+      "integrity": "sha512-UHuOBZ5jjsKuzbB/gRNNW8Vg8f00Emgskdq2kvZxgBJCS0aqquAuXai/SkWORlKeZEiNQWZjFZOqIUcH9LqKCw==",
+      "dev": true
+    },
     "minimatch": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@@ -36,6 +120,72 @@
       "requires": {
         "brace-expansion": "^1.1.7"
       }
+    },
+    "nise": {
+      "version": "1.4.10",
+      "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.10.tgz",
+      "integrity": "sha512-sa0RRbj53dovjc7wombHmVli9ZihXbXCQ2uH3TNm03DyvOSIQbxg+pbqDKrk2oxMK1rtLGVlKxcB9rrc6X5YjA==",
+      "dev": true,
+      "requires": {
+        "@sinonjs/formatio": "^3.1.0",
+        "@sinonjs/text-encoding": "^0.7.1",
+        "just-extend": "^4.0.2",
+        "lolex": "^2.3.2",
+        "path-to-regexp": "^1.7.0"
+      },
+      "dependencies": {
+        "lolex": {
+          "version": "2.7.5",
+          "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz",
+          "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==",
+          "dev": true
+        }
+      }
+    },
+    "path-to-regexp": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
+      "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
+      "dev": true,
+      "requires": {
+        "isarray": "0.0.1"
+      }
+    },
+    "sinon": {
+      "version": "7.3.2",
+      "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.3.2.tgz",
+      "integrity": "sha512-thErC1z64BeyGiPvF8aoSg0LEnptSaWE7YhdWWbWXgelOyThent7uKOnnEh9zBxDbKixtr5dEko+ws1sZMuFMA==",
+      "dev": true,
+      "requires": {
+        "@sinonjs/commons": "^1.4.0",
+        "@sinonjs/formatio": "^3.2.1",
+        "@sinonjs/samsam": "^3.3.1",
+        "diff": "^3.5.0",
+        "lolex": "^4.0.1",
+        "nise": "^1.4.10",
+        "supports-color": "^5.5.0"
+      }
+    },
+    "supports-color": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "dev": true,
+      "requires": {
+        "has-flag": "^3.0.0"
+      }
+    },
+    "ts-mock-imports": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/ts-mock-imports/-/ts-mock-imports-1.2.3.tgz",
+      "integrity": "sha512-pKeHFhlM4s4LvAPiixTsBTzJ65SY0pcXYFQ6nAmDOHl3lYZk4zi2zZFC3et6xX6tKhCCkt2NaYAY+vciPJlo8Q==",
+      "dev": true
+    },
+    "type-detect": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+      "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+      "dev": true
     }
   }
 }
diff --git a/packages/@aws-cdk/assets/package.json b/packages/@aws-cdk/assets/package.json
index 3f2cd8b1c9427..a2ac5394ea5c9 100644
--- a/packages/@aws-cdk/assets/package.json
+++ b/packages/@aws-cdk/assets/package.json
@@ -63,10 +63,13 @@
   "devDependencies": {
     "@aws-cdk/assert": "^0.31.0",
     "@types/minimatch": "^3.0.3",
+    "@types/sinon": "^7.0.11",
     "aws-cdk": "^0.31.0",
     "cdk-build-tools": "^0.31.0",
     "cdk-integ-tools": "^0.31.0",
-    "pkglint": "^0.31.0"
+    "pkglint": "^0.31.0",
+    "sinon": "^7.3.2",
+    "ts-mock-imports": "^1.2.3"
   },
   "dependencies": {
     "@aws-cdk/aws-iam": "^0.31.0",
diff --git a/packages/@aws-cdk/assets/test/fs/test.fs-fingerprint.ts b/packages/@aws-cdk/assets/test/fs/test.fs-fingerprint.ts
index 87cf001562055..8d4f76ce617d4 100644
--- a/packages/@aws-cdk/assets/test/fs/test.fs-fingerprint.ts
+++ b/packages/@aws-cdk/assets/test/fs/test.fs-fingerprint.ts
@@ -2,107 +2,157 @@ import fs = require('fs');
 import { Test } from 'nodeunit';
 import os = require('os');
 import path = require('path');
-import { copyDirectory } from '../../lib/fs/copy';
-import { fingerprint } from '../../lib/fs/fingerprint';
+import libfs = require('../../lib/fs');
 
 export = {
-  'single file'(test: Test) {
-    // GIVEN
-    const workdir = fs.mkdtempSync(path.join(os.tmpdir(), 'hash-tests'));
-    const content = 'Hello, world!';
-    const input1 = path.join(workdir, 'input1.txt');
-    const input2 = path.join(workdir, 'input2.txt');
-    const input3 = path.join(workdir, 'input3.txt');
-    fs.writeFileSync(input1, content);
-    fs.writeFileSync(input2, content);
-    fs.writeFileSync(input3, content + '.'); // add one character, hash should be different
-
-    // WHEN
-    const hash1 = fingerprint(input1);
-    const hash2 = fingerprint(input2);
-    const hash3 = fingerprint(input3);
-
-    // THEN
-    test.deepEqual(hash1, hash2);
-    test.notDeepEqual(hash3, hash1);
-    test.done();
+  files: {
+    'does not change with the file name'(test: Test) {
+      // GIVEN
+      const workdir = fs.mkdtempSync(path.join(os.tmpdir(), 'hash-tests'));
+      const content = 'Hello, world!';
+      const input1 = path.join(workdir, 'input1.txt');
+      const input2 = path.join(workdir, 'input2.txt');
+      const input3 = path.join(workdir, 'input3.txt');
+      fs.writeFileSync(input1, content);
+      fs.writeFileSync(input2, content);
+      fs.writeFileSync(input3, content + '.'); // add one character, hash should be different
+
+      // WHEN
+      const hash1 = libfs.fingerprint(input1);
+      const hash2 = libfs.fingerprint(input2);
+      const hash3 = libfs.fingerprint(input3);
+
+      // THEN
+      test.deepEqual(hash1, hash2);
+      test.notDeepEqual(hash3, hash1);
+      test.done();
+    },
+
+    'works on empty files'(test: Test) {
+      // GIVEN
+      const workdir = fs.mkdtempSync(path.join(os.tmpdir(), 'hash-tests'));
+      const input1 = path.join(workdir, 'empty');
+      const input2 = path.join(workdir, 'empty');
+      fs.writeFileSync(input1, '');
+      fs.writeFileSync(input2, '');
+
+      // WHEN
+      const hash1 = libfs.fingerprint(input1);
+      const hash2 = libfs.fingerprint(input2);
+
+      // THEN
+      test.deepEqual(hash1, hash2);
+      test.done();
+    },
   },
 
-  'empty file'(test: Test) {
-    // GIVEN
-    const workdir = fs.mkdtempSync(path.join(os.tmpdir(), 'hash-tests'));
-    const input1 = path.join(workdir, 'empty');
-    const input2 = path.join(workdir, 'empty');
-    fs.writeFileSync(input1, '');
-    fs.writeFileSync(input2, '');
-
-    // WHEN
-    const hash1 = fingerprint(input1);
-    const hash2 = fingerprint(input2);
-
-    // THEN
-    test.deepEqual(hash1, hash2);
-    test.done();
+  directories: {
+    'works on directories'(test: Test) {
+      // GIVEN
+      const srcdir = path.join(__dirname, 'fixtures', 'symlinks');
+      const outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'copy-tests'));
+      libfs.copyDirectory(srcdir, outdir);
+
+      // WHEN
+      const hashSrc = libfs.fingerprint(srcdir);
+      const hashCopy = libfs.fingerprint(outdir);
+
+      // THEN
+      test.deepEqual(hashSrc, hashCopy);
+      test.done();
+    },
+
+    'ignores requested files'(test: Test) {
+      // GIVEN
+      const srcdir = path.join(__dirname, 'fixtures', 'symlinks');
+      const outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'copy-tests'));
+      libfs.copyDirectory(srcdir, outdir);
+
+      // WHEN
+      const hashSrc = libfs.fingerprint(srcdir);
+
+      fs.writeFileSync(path.join(outdir, `${hashSrc}.ignoreme`), 'Ignore me!');
+      const hashCopy = libfs.fingerprint(outdir, { exclude: ['*.ignoreme'] });
+
+      // THEN
+      test.deepEqual(hashSrc, hashCopy);
+      test.done();
+    },
+
+    'changes with file names'(test: Test) {
+      // GIVEN
+      const srcdir = path.join(__dirname, 'fixtures', 'symlinks');
+      const cpydir = fs.mkdtempSync(path.join(os.tmpdir(), 'fingerprint-tests'));
+      libfs.copyDirectory(srcdir, cpydir);
+
+      // be careful not to break a symlink
+      fs.renameSync(path.join(cpydir, 'normal-dir', 'file-in-subdir.txt'), path.join(cpydir, 'move-me.txt'));
+
+      // WHEN
+      const hashSrc = libfs.fingerprint(srcdir);
+      const hashCopy = libfs.fingerprint(cpydir);
+
+      // THEN
+      test.notDeepEqual(hashSrc, hashCopy);
+      test.done();
+    },
   },
 
-  'directory'(test: Test) {
-    // GIVEN
-    const srcdir = path.join(__dirname, 'fixtures', 'symlinks');
-    const outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'copy-tests'));
-    copyDirectory(srcdir, outdir);
-
-    // WHEN
-    const hashSrc = fingerprint(srcdir);
-    const hashCopy = fingerprint(outdir);
-
-    // THEN
-    test.deepEqual(hashSrc, hashCopy);
-    test.done();
-  },
-
-  'directory, rename files (fingerprint should change)'(test: Test) {
-    // GIVEN
-    const srcdir = path.join(__dirname, 'fixtures', 'symlinks');
-    const cpydir = fs.mkdtempSync(path.join(os.tmpdir(), 'fingerprint-tests'));
-    copyDirectory(srcdir, cpydir);
-
-    // be careful not to break a symlink
-    fs.renameSync(path.join(cpydir, 'normal-dir', 'file-in-subdir.txt'), path.join(cpydir, 'move-me.txt'));
-
-    // WHEN
-    const hashSrc = fingerprint(srcdir);
-    const hashCopy = fingerprint(cpydir);
-
-    // THEN
-    test.notDeepEqual(hashSrc, hashCopy);
-    test.done();
-  },
-
-  'external symlink content changes (fingerprint should change)'(test: Test) {
-    // GIVEN
-    const dir1 = fs.mkdtempSync(path.join(os.tmpdir(), 'fingerprint-tests'));
-    const dir2 = fs.mkdtempSync(path.join(os.tmpdir(), 'fingerprint-tests'));
-    const target = path.join(dir1, 'boom.txt');
-    const content = 'boom';
-    fs.writeFileSync(target, content);
-    fs.symlinkSync(target, path.join(dir2, 'link-to-boom.txt'));
-
-    // now dir2 contains a symlink to a file in dir1
-
-    // WHEN
-    const original = fingerprint(dir2);
-
-    // now change the contents of the target
-    fs.writeFileSync(target, 'changning you!');
-    const afterChange = fingerprint(dir2);
-
-    // revert the content to original and expect hash to be reverted
-    fs.writeFileSync(target, content);
-    const afterRevert = fingerprint(dir2);
-
-    // THEN
-    test.notDeepEqual(original, afterChange);
-    test.deepEqual(afterRevert, original);
-    test.done();
+  symlinks: {
+    'changes with the contents of followed symlink referent'(test: Test) {
+      // GIVEN
+      const dir1 = fs.mkdtempSync(path.join(os.tmpdir(), 'fingerprint-tests'));
+      const dir2 = fs.mkdtempSync(path.join(os.tmpdir(), 'fingerprint-tests'));
+      const target = path.join(dir1, 'boom.txt');
+      const content = 'boom';
+      fs.writeFileSync(target, content);
+      fs.symlinkSync(target, path.join(dir2, 'link-to-boom.txt'));
+
+      // now dir2 contains a symlink to a file in dir1
+
+      // WHEN
+      const original = libfs.fingerprint(dir2);
+
+      // now change the contents of the target
+      fs.writeFileSync(target, 'changning you!');
+      const afterChange = libfs.fingerprint(dir2);
+
+      // revert the content to original and expect hash to be reverted
+      fs.writeFileSync(target, content);
+      const afterRevert = libfs.fingerprint(dir2);
+
+      // THEN
+      test.notDeepEqual(original, afterChange);
+      test.deepEqual(afterRevert, original);
+      test.done();
+    },
+
+    'does not change with the contents of un-followed symlink referent'(test: Test) {
+      // GIVEN
+      const dir1 = fs.mkdtempSync(path.join(os.tmpdir(), 'fingerprint-tests'));
+      const dir2 = fs.mkdtempSync(path.join(os.tmpdir(), 'fingerprint-tests'));
+      const target = path.join(dir1, 'boom.txt');
+      const content = 'boom';
+      fs.writeFileSync(target, content);
+      fs.symlinkSync(target, path.join(dir2, 'link-to-boom.txt'));
+
+      // now dir2 contains a symlink to a file in dir1
+
+      // WHEN
+      const original = libfs.fingerprint(dir2, { follow: libfs.FollowMode.Never });
+
+      // now change the contents of the target
+      fs.writeFileSync(target, 'changning you!');
+      const afterChange = libfs.fingerprint(dir2, { follow: libfs.FollowMode.Never });
+
+      // revert the content to original and expect hash to be reverted
+      fs.writeFileSync(target, content);
+      const afterRevert = libfs.fingerprint(dir2, { follow: libfs.FollowMode.Never });
+
+      // THEN
+      test.deepEqual(original, afterChange);
+      test.deepEqual(afterRevert, original);
+      test.done();
+    }
   }
 };
diff --git a/packages/@aws-cdk/assets/test/fs/test.utils.ts b/packages/@aws-cdk/assets/test/fs/test.utils.ts
new file mode 100644
index 0000000000000..c0c4107be43e3
--- /dev/null
+++ b/packages/@aws-cdk/assets/test/fs/test.utils.ts
@@ -0,0 +1,195 @@
+import fs = require('fs');
+import { Test } from 'nodeunit';
+import path = require('path');
+import { ImportMock } from 'ts-mock-imports';
+import { FollowMode } from '../../lib/fs';
+import util = require('../../lib/fs/utils');
+
+export = {
+  shouldExclude: {
+    'excludes nothing by default'(test: Test) {
+      test.ok(!util.shouldExclude([], path.join('some', 'file', 'path')));
+      test.done();
+    },
+
+    'excludes requested files'(test: Test) {
+      const exclusions = ['*.ignored'];
+      test.ok(util.shouldExclude(exclusions, path.join('some', 'file.ignored')));
+      test.ok(!util.shouldExclude(exclusions, path.join('some', 'important', 'file')));
+      test.done();
+    },
+
+    'does not exclude whitelisted files'(test: Test) {
+      const exclusions = ['*.ignored', '!important.*'];
+      test.ok(!util.shouldExclude(exclusions, path.join('some', 'important.ignored')));
+      test.done();
+    },
+  },
+
+  shouldFollow: {
+    always: {
+      'follows internal'(test: Test) {
+        const sourceRoot = path.join('source', 'root');
+        const linkTarget = path.join(sourceRoot, 'referent');
+
+        const mockFsExists = ImportMock.mockFunction(fs, 'existsSync', true);
+        try {
+          test.ok(util.shouldFollow(FollowMode.Always, sourceRoot, linkTarget));
+          test.ok(mockFsExists.calledOnceWith(linkTarget));
+          test.done();
+        } finally {
+          mockFsExists.restore();
+        }
+      },
+
+      'follows external'(test: Test) {
+        const sourceRoot = path.join('source', 'root');
+        const linkTarget = path.join('alternate', 'referent');
+        const mockFsExists = ImportMock.mockFunction(fs, 'existsSync', true);
+        try {
+          test.ok(util.shouldFollow(FollowMode.Always, sourceRoot, linkTarget));
+          test.ok(mockFsExists.calledOnceWith(linkTarget));
+          test.done();
+        } finally {
+          mockFsExists.restore();
+        }
+      },
+
+      'does not follow internal when the referent does not exist'(test: Test) {
+        const sourceRoot = path.join('source', 'root');
+        const linkTarget = path.join(sourceRoot, 'referent');
+        const mockFsExists = ImportMock.mockFunction(fs, 'existsSync', false);
+        try {
+          test.ok(!util.shouldFollow(FollowMode.Always, sourceRoot, linkTarget));
+          test.ok(mockFsExists.calledOnceWith(linkTarget));
+          test.done();
+        } finally {
+          mockFsExists.restore();
+        }
+      },
+
+      'does not follow external when the referent does not exist'(test: Test) {
+        const sourceRoot = path.join('source', 'root');
+        const linkTarget = path.join('alternate', 'referent');
+        const mockFsExists = ImportMock.mockFunction(fs, 'existsSync', false);
+        try {
+          test.ok(!util.shouldFollow(FollowMode.Always, sourceRoot, linkTarget));
+          test.ok(mockFsExists.calledOnceWith(linkTarget));
+          test.done();
+        } finally {
+          mockFsExists.restore();
+        }
+      },
+    },
+
+    external: {
+      'does not follow internal'(test: Test) {
+        const sourceRoot = path.join('source', 'root');
+        const linkTarget = path.join(sourceRoot, 'referent');
+        const mockFsExists = ImportMock.mockFunction(fs, 'existsSync');
+        try {
+          test.ok(!util.shouldFollow(FollowMode.External, sourceRoot, linkTarget));
+          test.ok(mockFsExists.notCalled);
+          test.done();
+        } finally {
+          mockFsExists.restore();
+        }
+      },
+
+      'follows external'(test: Test) {
+        const sourceRoot = path.join('source', 'root');
+        const linkTarget = path.join('alternate', 'referent');
+        const mockFsExists = ImportMock.mockFunction(fs, 'existsSync', true);
+        try {
+          test.ok(util.shouldFollow(FollowMode.External, sourceRoot, linkTarget));
+          test.ok(mockFsExists.calledOnceWith(linkTarget));
+          test.done();
+        } finally {
+          mockFsExists.restore();
+        }
+      },
+
+      'does not follow external when referent does not exist'(test: Test) {
+        const sourceRoot = path.join('source', 'root');
+        const linkTarget = path.join('alternate', 'referent');
+        const mockFsExists = ImportMock.mockFunction(fs, 'existsSync', false);
+        try {
+          test.ok(!util.shouldFollow(FollowMode.External, sourceRoot, linkTarget));
+          test.ok(mockFsExists.calledOnceWith(linkTarget));
+          test.done();
+        } finally {
+          mockFsExists.restore();
+        }
+      },
+    },
+
+    blockExternal: {
+      'follows internal'(test: Test) {
+        const sourceRoot = path.join('source', 'root');
+        const linkTarget = path.join(sourceRoot, 'referent');
+        const mockFsExists = ImportMock.mockFunction(fs, 'existsSync', true);
+        try {
+          test.ok(util.shouldFollow(FollowMode.BlockExternal, sourceRoot, linkTarget));
+          test.ok(mockFsExists.calledOnceWith(linkTarget));
+          test.done();
+        } finally {
+          mockFsExists.restore();
+        }
+      },
+
+      'does not follow internal when referent does not exist'(test: Test) {
+        const sourceRoot = path.join('source', 'root');
+        const linkTarget = path.join(sourceRoot, 'referent');
+        const mockFsExists = ImportMock.mockFunction(fs, 'existsSync', false);
+        try {
+          test.ok(!util.shouldFollow(FollowMode.BlockExternal, sourceRoot, linkTarget));
+          test.ok(mockFsExists.calledOnceWith(linkTarget));
+          test.done();
+        } finally {
+          mockFsExists.restore();
+        }
+      },
+
+      'does not follow external'(test: Test) {
+        const sourceRoot = path.join('source', 'root');
+        const linkTarget = path.join('alternate', 'referent');
+        const mockFsExists = ImportMock.mockFunction(fs, 'existsSync');
+        try {
+          test.ok(!util.shouldFollow(FollowMode.BlockExternal, sourceRoot, linkTarget));
+          test.ok(mockFsExists.notCalled);
+          test.done();
+        } finally {
+          mockFsExists.restore();
+        }
+      },
+    },
+
+    never: {
+      'does not follow internal'(test: Test) {
+        const sourceRoot = path.join('source', 'root');
+        const linkTarget = path.join(sourceRoot, 'referent');
+        const mockFsExists = ImportMock.mockFunction(fs, 'existsSync');
+        try {
+          test.ok(!util.shouldFollow(FollowMode.Never, sourceRoot, linkTarget));
+          test.ok(mockFsExists.notCalled);
+          test.done();
+        } finally {
+          mockFsExists.restore();
+        }
+      },
+
+      'does not follow external'(test: Test) {
+        const sourceRoot = path.join('source', 'root');
+        const linkTarget = path.join('alternate', 'referent');
+        const mockFsExists = ImportMock.mockFunction(fs, 'existsSync');
+        try {
+          test.ok(!util.shouldFollow(FollowMode.Never, sourceRoot, linkTarget));
+          test.ok(mockFsExists.notCalled);
+          test.done();
+        } finally {
+          mockFsExists.restore();
+        }
+      },
+    }
+  },
+};
diff --git a/packages/@aws-cdk/assets/test/integ.assets.directory.lit.expected.json b/packages/@aws-cdk/assets/test/integ.assets.directory.lit.expected.json
index 21728628fc03a..b399c6fa041c6 100644
--- a/packages/@aws-cdk/assets/test/integ.assets.directory.lit.expected.json
+++ b/packages/@aws-cdk/assets/test/integ.assets.directory.lit.expected.json
@@ -7,6 +7,10 @@
     "SampleAssetS3VersionKey3E106D34": {
       "Type": "String",
       "Description": "S3 key for asset version \"aws-cdk-asset-test/SampleAsset\""
+    },
+    "SampleAssetArtifactHashE80944C9": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"aws-cdk-asset-test/SampleAsset\""
     }
   },
   "Resources": {
diff --git a/packages/@aws-cdk/assets/test/integ.assets.file.lit.expected.json b/packages/@aws-cdk/assets/test/integ.assets.file.lit.expected.json
index 33c26403bc539..759ea473c59b4 100644
--- a/packages/@aws-cdk/assets/test/integ.assets.file.lit.expected.json
+++ b/packages/@aws-cdk/assets/test/integ.assets.file.lit.expected.json
@@ -7,6 +7,10 @@
     "SampleAssetS3VersionKey3E106D34": {
       "Type": "String",
       "Description": "S3 key for asset version \"aws-cdk-asset-file-test/SampleAsset\""
+    },
+    "SampleAssetArtifactHashE80944C9": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"aws-cdk-asset-file-test/SampleAsset\""
     }
   },
   "Resources": {
diff --git a/packages/@aws-cdk/assets/test/integ.assets.permissions.lit.expected.json b/packages/@aws-cdk/assets/test/integ.assets.permissions.lit.expected.json
index 6eebb424ee837..0f1462339cfc6 100644
--- a/packages/@aws-cdk/assets/test/integ.assets.permissions.lit.expected.json
+++ b/packages/@aws-cdk/assets/test/integ.assets.permissions.lit.expected.json
@@ -7,6 +7,10 @@
     "MyFileS3VersionKey568C3C9F": {
       "Type": "String",
       "Description": "S3 key for asset version \"aws-cdk-asset-refs/MyFile\""
+    },
+    "MyFileArtifactHashAB5F44E1": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"aws-cdk-asset-refs/MyFile\""
     }
   },
   "Resources": {
diff --git a/packages/@aws-cdk/assets/test/integ.assets.refs.lit.expected.json b/packages/@aws-cdk/assets/test/integ.assets.refs.lit.expected.json
index e7057a36b64a4..e80d3171ab4c7 100644
--- a/packages/@aws-cdk/assets/test/integ.assets.refs.lit.expected.json
+++ b/packages/@aws-cdk/assets/test/integ.assets.refs.lit.expected.json
@@ -7,6 +7,10 @@
     "SampleAssetS3VersionKey3E106D34": {
       "Type": "String",
       "Description": "S3 key for asset version \"aws-cdk-asset-refs/SampleAsset\""
+    },
+    "SampleAssetArtifactHashE80944C9": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"aws-cdk-asset-refs/SampleAsset\""
     }
   },
   "Outputs": {
@@ -175,4 +179,4 @@
       }
     }
   }
-}
+}
\ No newline at end of file
diff --git a/packages/@aws-cdk/assets/test/integ.multi-assets.expected.json b/packages/@aws-cdk/assets/test/integ.multi-assets.expected.json
index a71296157a8a3..e4cdd9326ecc4 100644
--- a/packages/@aws-cdk/assets/test/integ.multi-assets.expected.json
+++ b/packages/@aws-cdk/assets/test/integ.multi-assets.expected.json
@@ -1,4 +1,9 @@
 {
+  "Resources": {
+    "DummyResourceF3AB250A": {
+      "Type": "AWS::IAM::User"
+    }
+  },
   "Parameters": {
     "SampleAsset1S3Bucket469E18FF": {
       "Type": "String",
@@ -8,6 +13,10 @@
       "Type": "String",
       "Description": "S3 key for asset version \"aws-cdk-multi-assets/SampleAsset1\""
     },
+    "SampleAsset1ArtifactHash9E24B5F0": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"aws-cdk-multi-assets/SampleAsset1\""
+    },
     "SampleAsset2S3BucketC94C651A": {
       "Type": "String",
       "Description": "S3 bucket for asset \"aws-cdk-multi-assets/SampleAsset2\""
@@ -15,6 +24,10 @@
     "SampleAsset2S3VersionKey3A7E2CC4": {
       "Type": "String",
       "Description": "S3 key for asset version \"aws-cdk-multi-assets/SampleAsset2\""
+    },
+    "SampleAsset2ArtifactHash62F55C83": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"aws-cdk-multi-assets/SampleAsset2\""
     }
   }
 }
\ No newline at end of file
diff --git a/packages/@aws-cdk/assets/test/integ.multi-assets.ts b/packages/@aws-cdk/assets/test/integ.multi-assets.ts
index 4cf6220d8ca61..e18a0cd48e660 100644
--- a/packages/@aws-cdk/assets/test/integ.multi-assets.ts
+++ b/packages/@aws-cdk/assets/test/integ.multi-assets.ts
@@ -1,3 +1,4 @@
+import iam = require('@aws-cdk/aws-iam');
 import cdk = require('@aws-cdk/cdk');
 import path = require('path');
 import assets = require('../lib');
@@ -6,6 +7,9 @@ class TestStack extends cdk.Stack {
   constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
     super(scope, id, props);
 
+    // The template must contain at least one resource, so there is this...
+    new iam.User(this, 'DummyResource');
+
     // Check that the same asset added multiple times is
     // uploaded and copied.
     new assets.FileAsset(this, 'SampleAsset1', {
@@ -20,4 +24,4 @@ class TestStack extends cdk.Stack {
 
 const app = new cdk.App();
 new TestStack(app, 'aws-cdk-multi-assets');
-app.run();
\ No newline at end of file
+app.run();
diff --git a/packages/@aws-cdk/assets/test/test.asset.ts b/packages/@aws-cdk/assets/test/test.asset.ts
index 1d8ad7d56777d..bb92463bd7454 100644
--- a/packages/@aws-cdk/assets/test/test.asset.ts
+++ b/packages/@aws-cdk/assets/test/test.asset.ts
@@ -30,8 +30,10 @@ export = {
       path: SAMPLE_ASSET_DIR,
       id: 'MyAsset',
       packaging: 'zip',
+      sourceHash: '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2',
       s3BucketParameter: 'MyAssetS3Bucket68C9B344',
       s3KeyParameter: 'MyAssetS3VersionKey68E1A45D',
+      artifactHashParameter: 'MyAssetArtifactHashF518BDDE',
     });
 
     test.equal(template.Parameters.MyAssetS3Bucket68C9B344.Type, 'String');
@@ -55,8 +57,10 @@ export = {
       path: dirPath,
       id: "mystackMyAssetD6B1B593",
       packaging: "zip",
+      sourceHash: '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2',
       s3BucketParameter: "MyAssetS3Bucket68C9B344",
-      s3KeyParameter: "MyAssetS3VersionKey68E1A45D"
+      s3KeyParameter: "MyAssetS3VersionKey68E1A45D",
+      artifactHashParameter: 'MyAssetArtifactHashF518BDDE',
     });
 
     test.done();
@@ -76,8 +80,10 @@ export = {
       path: filePath,
       packaging: 'file',
       id: 'MyAsset',
+      sourceHash: '78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197',
       s3BucketParameter: 'MyAssetS3Bucket68C9B344',
       s3KeyParameter: 'MyAssetS3VersionKey68E1A45D',
+      artifactHashParameter: 'MyAssetArtifactHashF518BDDE',
     });
 
     // verify that now the template contains parameters for this asset
@@ -137,8 +143,8 @@ export = {
     const stack = new cdk.Stack();
 
     // WHEN
-    new ZipDirectoryAsset(stack, 'MyDirectory1', { path: '.' });
-    new ZipDirectoryAsset(stack, 'MyDirectory2', { path: '.' });
+    new ZipDirectoryAsset(stack, 'MyDirectory1', { path: path.join(__dirname, 'sample-asset-directory') });
+    new ZipDirectoryAsset(stack, 'MyDirectory2', { path: path.join(__dirname, 'sample-asset-directory') });
 
     // THEN: no error
 
@@ -219,7 +225,7 @@ export = {
 
   'staging': {
 
-    'copy file assets under .assets/fingerprint.ext'(test: Test) {
+    'copy file assets under .assets/${fingerprint}.ext'(test: Test) {
       const tempdir = mkdtempSync();
       process.chdir(tempdir); // change current directory to somewhere in /tmp
 
@@ -241,7 +247,7 @@ export = {
       // THEN
       app.run();
       test.ok(fs.existsSync(path.join(tempdir, '.assets')));
-      test.ok(fs.existsSync(path.join(tempdir, '.assets', 'fdb4701ff6c99e676018ee2c24a3119b.zip')));
+      test.ok(fs.existsSync(path.join(tempdir, '.assets', 'a7a79cdf84b802ea8b198059ff899cffc095a1b9606e919f98e05bf80779756b.zip')));
       fs.readdirSync(path.join(tempdir, '.assets'));
       test.done();
     },
@@ -264,8 +270,9 @@ export = {
       // THEN
       app.run();
       test.ok(fs.existsSync(path.join(tempdir, '.assets')));
-      test.ok(fs.existsSync(path.join(tempdir, '.assets', 'b550524e103eb4cf257c594fba5b9fe8', 'sample-asset-file.txt')));
-      test.ok(fs.existsSync(path.join(tempdir, '.assets', 'b550524e103eb4cf257c594fba5b9fe8', 'sample-jar-asset.jar')));
+      const hash = '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2';
+      test.ok(fs.existsSync(path.join(tempdir, '.assets', hash, 'sample-asset-file.txt')));
+      test.ok(fs.existsSync(path.join(tempdir, '.assets', hash, 'sample-jar-asset.jar')));
       fs.readdirSync(path.join(tempdir, '.assets'));
       test.done();
     },
@@ -295,7 +302,7 @@ export = {
       const template = SynthUtils.templateForStackName(session, stack.name);
 
       test.deepEqual(template.Resources.MyResource.Metadata, {
-        "aws:asset:path": `.my-awesome-staging-directory/b550524e103eb4cf257c594fba5b9fe8`,
+        "aws:asset:path": `.my-awesome-staging-directory/6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2`,
         "aws:asset:property": "PropName"
       });
       test.done();
@@ -323,7 +330,7 @@ export = {
       const template = SynthUtils.templateForStackName(session, stack.name);
 
       test.deepEqual(template.Resources.MyResource.Metadata, {
-        "aws:asset:path": `${staging}/b550524e103eb4cf257c594fba5b9fe8`,
+        "aws:asset:path": `${staging}/6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2`,
         "aws:asset:property": "PropName"
       });
       test.done();
@@ -351,7 +358,7 @@ export = {
       const artifact = session.getArtifact(stack.name);
 
       const md = Object.values(artifact.metadata || {})[0][0].data;
-      test.deepEqual(md.path, '.stageme/b550524e103eb4cf257c594fba5b9fe8');
+      test.deepEqual(md.path, '.stageme/6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2');
       test.done();
     }
 
diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.expected.json
index 0fbeaf9ea4e02..04b128f3787bb 100644
--- a/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.expected.json
+++ b/packages/@aws-cdk/aws-codebuild/test/integ.docker-asset.lit.expected.json
@@ -11,6 +11,10 @@
     "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276": {
       "Type": "String",
       "Description": "S3 key for asset version \"test-codebuild-docker-asset/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\""
+    },
+    "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeArtifactHash8BCBAA49": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"test-codebuild-docker-asset/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\""
     }
   },
   "Resources": {
@@ -28,7 +32,7 @@
             0,
             {
               "Fn::Split": [
-                ":",
+                "@sha256:",
                 {
                   "Ref": "MyImageImageName953AD232"
                 }
@@ -63,7 +67,11 @@
           ],
           "Version": "2012-10-17"
         }
-      }
+      },
+      "DependsOn": [
+        "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C",
+        "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17"
+      ]
     },
     "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17": {
       "Type": "AWS::IAM::Role",
@@ -142,7 +150,7 @@
                         0,
                         {
                           "Fn::Split": [
-                            ":",
+                            "@sha256:",
                             {
                               "Ref": "MyImageImageName953AD232"
                             }
@@ -413,13 +421,13 @@
                     "RepositoryName"
                   ]
                 },
-                ":",
+                "@sha256:",
                 {
                   "Fn::Select": [
                     1,
                     {
                       "Fn::Split": [
-                        ":",
+                        "@sha256:",
                         {
                           "Ref": "MyImageImageName953AD232"
                         }
diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-shell.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.project-shell.expected.json
index 976e7c9937dca..23577c4af4fa8 100644
--- a/packages/@aws-cdk/aws-codebuild/test/integ.project-shell.expected.json
+++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-shell.expected.json
@@ -7,6 +7,10 @@
     "BundleS3VersionKey720F2199": {
       "Type": "String",
       "Description": "S3 key for asset version \"aws-cdk-codebuild-project-shell/Bundle\""
+    },
+    "BundleArtifactHashEA214C27": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"aws-cdk-codebuild-project-shell/Bundle\""
     }
   },
   "Resources": {
diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-vpc.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.project-vpc.expected.json
index 0a42b9adfb276..35f355bfeb053 100644
--- a/packages/@aws-cdk/aws-codebuild/test/integ.project-vpc.expected.json
+++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-vpc.expected.json
@@ -535,6 +535,10 @@
     "BundleS3VersionKey720F2199": {
       "Type": "String",
       "Description": "S3 key for asset version \"aws-cdk-codebuild-project-vpc/Bundle\""
+    },
+    "BundleArtifactHashEA214C27": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"aws-cdk-codebuild-project-vpc/Bundle\""
     }
   }
 }
\ No newline at end of file
diff --git a/packages/@aws-cdk/aws-codebuild/test/test.project.ts b/packages/@aws-cdk/aws-codebuild/test/test.project.ts
index 27715110cce8f..174ce65be4eae 100644
--- a/packages/@aws-cdk/aws-codebuild/test/test.project.ts
+++ b/packages/@aws-cdk/aws-codebuild/test/test.project.ts
@@ -3,6 +3,7 @@ import assets = require('@aws-cdk/assets');
 import { Bucket } from '@aws-cdk/aws-s3';
 import cdk = require('@aws-cdk/cdk');
 import { Test } from 'nodeunit';
+import path = require('path');
 import codebuild = require('../lib');
 import { Cache, LocalCacheMode } from '../lib/cache';
 
@@ -129,8 +130,8 @@ export = {
 
     // WHEN
     new codebuild.Project(stack, 'Project', {
-      buildScriptAsset: new assets.ZipDirectoryAsset(stack, 'Asset', { path: '.' }),
-      buildScriptAssetEntrypoint: 'hello.sh',
+      buildScriptAsset: new assets.ZipDirectoryAsset(stack, 'Asset', { path: path.join(__dirname, 'script_bundle') }),
+      buildScriptAssetEntrypoint: 'build.sh',
     });
 
     // THEN
diff --git a/packages/@aws-cdk/aws-codedeploy/test/lambda/integ.deployment-group.expected.json b/packages/@aws-cdk/aws-codedeploy/test/lambda/integ.deployment-group.expected.json
index 27491e271ad0b..d1b1f415cd098 100644
--- a/packages/@aws-cdk/aws-codedeploy/test/lambda/integ.deployment-group.expected.json
+++ b/packages/@aws-cdk/aws-codedeploy/test/lambda/integ.deployment-group.expected.json
@@ -579,6 +579,10 @@
       "Type": "String",
       "Description": "S3 key for asset version \"aws-cdk-codedeploy-lambda/Handler/Code\""
     },
+    "HandlerCodeArtifactHashD7814EF8": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"aws-cdk-codedeploy-lambda/Handler/Code\""
+    },
     "PreHookCodeS3BucketE2616D65": {
       "Type": "String",
       "Description": "S3 bucket for asset \"aws-cdk-codedeploy-lambda/PreHook/Code\""
@@ -587,6 +591,10 @@
       "Type": "String",
       "Description": "S3 key for asset version \"aws-cdk-codedeploy-lambda/PreHook/Code\""
     },
+    "PreHookCodeArtifactHash540B37CB": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"aws-cdk-codedeploy-lambda/PreHook/Code\""
+    },
     "PostHookCodeS3BucketECF09EB8": {
       "Type": "String",
       "Description": "S3 bucket for asset \"aws-cdk-codedeploy-lambda/PostHook/Code\""
@@ -594,6 +602,10 @@
     "PostHookCodeS3VersionKey53451C7E": {
       "Type": "String",
       "Description": "S3 key for asset version \"aws-cdk-codedeploy-lambda/PostHook/Code\""
+    },
+    "PostHookCodeArtifactHash73D72B37": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"aws-cdk-codedeploy-lambda/PostHook/Code\""
     }
   }
-}
+}
\ No newline at end of file
diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-cross-region.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-cross-region.expected.json
index b3f3b9dbd5e79..27580b9b03a0b 100644
--- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-cross-region.expected.json
+++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-cross-region.expected.json
@@ -268,4 +268,4 @@
       }
     }
   }
-}
+}
\ No newline at end of file
diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-wtih-action-role.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-wtih-action-role.expected.json
index 04cf136975672..da68e1737ee0e 100644
--- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-wtih-action-role.expected.json
+++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn-wtih-action-role.expected.json
@@ -333,4 +333,4 @@
       }
     }
   }
-}
+}
\ No newline at end of file
diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn.expected.json
index 5f21f509449bf..21978f4abc275 100644
--- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn.expected.json
+++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-cfn.expected.json
@@ -407,4 +407,4 @@
       }
     }
   }
-}
+}
\ No newline at end of file
diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit-build.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit-build.expected.json
index 488d9b70a2369..7472a51f700cc 100644
--- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit-build.expected.json
+++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit-build.expected.json
@@ -582,4 +582,4 @@
       ]
     }
   }
-}
+}
\ No newline at end of file
diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json
index 242911394e9da..0001b338b25da 100644
--- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json
+++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json
@@ -815,4 +815,4 @@
       ]
     }
   }
-}
+}
\ No newline at end of file
diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-jenkins.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-jenkins.expected.json
index 93e0bd3c13d98..09dde45fc5791 100644
--- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-jenkins.expected.json
+++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-jenkins.expected.json
@@ -290,4 +290,4 @@
       }
     }
   }
-}
+}
\ No newline at end of file
diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-manual-approval.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-manual-approval.expected.json
index 81dd6190c7421..6df92db3bd42a 100644
--- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-manual-approval.expected.json
+++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-manual-approval.expected.json
@@ -203,4 +203,4 @@
       }
     }
   }
-}
+}
\ No newline at end of file
diff --git a/packages/@aws-cdk/aws-dynamodb-global/test/integ.dynamodb.global.expected.json b/packages/@aws-cdk/aws-dynamodb-global/test/integ.dynamodb.global.expected.json
index 68f0d210b3b7e..2c49fa3273040 100644
--- a/packages/@aws-cdk/aws-dynamodb-global/test/integ.dynamodb.global.expected.json
+++ b/packages/@aws-cdk/aws-dynamodb-global/test/integ.dynamodb.global.expected.json
@@ -243,6 +243,10 @@
       "SingletonLambdaD38B65A66B544FB69BAD9CD40A6DAC12CodeS3VersionKey59DB89A0": {
         "Type": "String",
         "Description": "S3 key for asset version \"globdynamodbinteg-CustomResource/SingletonLambdaD38B65A66B544FB69BAD9CD40A6DAC12/Code\""
+      },
+      "SingletonLambdaD38B65A66B544FB69BAD9CD40A6DAC12CodeArtifactHashCE92982B": {
+        "Type": "String",
+        "Description": "Artifact hash for asset \"globdynamodbinteg-CustomResource/SingletonLambdaD38B65A66B544FB69BAD9CD40A6DAC12/Code\""
       }
     }
   }
diff --git a/packages/@aws-cdk/aws-ecs-patterns/package-lock.json b/packages/@aws-cdk/aws-ecs-patterns/package-lock.json
index 72aae89489c98..2efb2f5ef2f45 100644
--- a/packages/@aws-cdk/aws-ecs-patterns/package-lock.json
+++ b/packages/@aws-cdk/aws-ecs-patterns/package-lock.json
@@ -1,5 +1,5 @@
 {
-	"name": "aws-ecs-patterns",
+	"name": "@aws-cdk/aws-ecs-patterns",
 	"version": "0.31.0",
 	"lockfileVersion": 1,
 	"requires": true,
diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json
index e6cc62dfd2fac..3fb9be342cd98 100644
--- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json
+++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json
@@ -853,23 +853,6 @@
         ]
       }
     },
-    "FrontendServiceSecurityGroup85470DEC": {
-      "Type": "AWS::EC2::SecurityGroup",
-      "Properties": {
-        "GroupDescription": "aws-ecs-integ-ecs/FrontendService/SecurityGroup",
-        "SecurityGroupEgress": [
-          {
-            "CidrIp": "0.0.0.0/0",
-            "Description": "Allow all outbound traffic by default",
-            "IpProtocol": "-1"
-          }
-        ],
-        "SecurityGroupIngress": [],
-        "VpcId": {
-          "Ref": "Vpc8378EB38"
-        }
-      }
-    },
     "FrontendServiceCloudmapService6FE76C06": {
       "Type": "AWS::ServiceDiscovery::Service",
       "Properties": {
@@ -899,6 +882,23 @@
           ]
         }
       }
+    },
+    "FrontendServiceSecurityGroup85470DEC": {
+      "Type": "AWS::EC2::SecurityGroup",
+      "Properties": {
+        "GroupDescription": "aws-ecs-integ-ecs/FrontendService/SecurityGroup",
+        "SecurityGroupEgress": [
+          {
+            "CidrIp": "0.0.0.0/0",
+            "Description": "Allow all outbound traffic by default",
+            "IpProtocol": "-1"
+          }
+        ],
+        "SecurityGroupIngress": [],
+        "VpcId": {
+          "Ref": "Vpc8378EB38"
+        }
+      }
     }
   }
-}
+}
\ No newline at end of file
diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json
index 6d9188792f1f3..938d4ab9bb5af 100644
--- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json
+++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json
@@ -865,4 +865,4 @@
       }
     }
   }
-}
+}
\ No newline at end of file
diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json b/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json
index 3b1452b8e2c4e..3aee844753d35 100644
--- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json
+++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.asset-image.expected.json
@@ -360,7 +360,7 @@
             0,
             {
               "Fn::Split": [
-                ":",
+                "@sha256:",
                 {
                   "Ref": "ImageImageName5E684353"
                 }
@@ -368,7 +368,11 @@
             }
           ]
         }
-      }
+      },
+      "DependsOn": [
+        "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C",
+        "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17"
+      ]
     },
     "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17": {
       "Type": "AWS::IAM::Role",
@@ -447,7 +451,7 @@
                         0,
                         {
                           "Fn::Split": [
-                            ":",
+                            "@sha256:",
                             {
                               "Ref": "ImageImageName5E684353"
                             }
@@ -743,13 +747,13 @@
                       "RepositoryName"
                     ]
                   },
-                  ":",
+                  "@sha256:",
                   {
                     "Fn::Select": [
                       1,
                       {
                         "Fn::Split": [
-                          ":",
+                          "@sha256:",
                           {
                             "Ref": "ImageImageName5E684353"
                           }
@@ -1009,6 +1013,10 @@
     "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276": {
       "Type": "String",
       "Description": "S3 key for asset version \"aws-ecs-integ/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\""
+    },
+    "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeArtifactHash8BCBAA49": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"aws-ecs-integ/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\""
     }
   },
   "Outputs": {
diff --git a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json
index ed1654619ee50..cf93a767e37b5 100644
--- a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json
+++ b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-task.lit.expected.json
@@ -692,13 +692,13 @@
                       "RepositoryName"
                     ]
                   },
-                  ":",
+                  "@sha256:",
                   {
                     "Fn::Select": [
                       1,
                       {
                         "Fn::Split": [
-                          ":",
+                          "@sha256:",
                           {
                             "Ref": "EventImageImageNameE972A8B1"
                           }
@@ -859,7 +859,7 @@
             0,
             {
               "Fn::Split": [
-                ":",
+                "@sha256:",
                 {
                   "Ref": "EventImageImageNameE972A8B1"
                 }
@@ -867,7 +867,11 @@
             }
           ]
         }
-      }
+      },
+      "DependsOn": [
+        "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C",
+        "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17"
+      ]
     },
     "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17": {
       "Type": "AWS::IAM::Role",
@@ -946,7 +950,7 @@
                         0,
                         {
                           "Fn::Split": [
-                            ":",
+                            "@sha256:",
                             {
                               "Ref": "EventImageImageNameE972A8B1"
                             }
@@ -1143,6 +1147,10 @@
     "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276": {
       "Type": "String",
       "Description": "S3 key for asset version \"aws-ecs-integ-ecs/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\""
+    },
+    "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeArtifactHash8BCBAA49": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"aws-ecs-integ-ecs/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\""
     }
   }
 }
diff --git a/packages/@aws-cdk/aws-lambda/test/integ.assets.file.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.assets.file.expected.json
index de0f8351c070a..8feb8949c3bd0 100644
--- a/packages/@aws-cdk/aws-lambda/test/integ.assets.file.expected.json
+++ b/packages/@aws-cdk/aws-lambda/test/integ.assets.file.expected.json
@@ -104,6 +104,10 @@
     "MyLambdaCodeS3VersionKey47762537": {
       "Type": "String",
       "Description": "S3 key for asset version \"lambda-test-assets-file/MyLambda/Code\""
+    },
+    "MyLambdaCodeArtifactHashF5E94E30": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"lambda-test-assets-file/MyLambda/Code\""
     }
   }
 }
\ No newline at end of file
diff --git a/packages/@aws-cdk/aws-lambda/test/integ.assets.lit.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.assets.lit.expected.json
index fa1bc3f5e8cea..f3801a2eabf33 100644
--- a/packages/@aws-cdk/aws-lambda/test/integ.assets.lit.expected.json
+++ b/packages/@aws-cdk/aws-lambda/test/integ.assets.lit.expected.json
@@ -104,6 +104,10 @@
     "MyLambdaCodeS3VersionKey47762537": {
       "Type": "String",
       "Description": "S3 key for asset version \"lambda-test-assets/MyLambda/Code\""
+    },
+    "MyLambdaCodeArtifactHashF5E94E30": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"lambda-test-assets/MyLambda/Code\""
     }
   }
 }
\ No newline at end of file
diff --git a/packages/@aws-cdk/aws-lambda/test/integ.layer-version.lit.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.layer-version.lit.expected.json
index af2da01f41b56..6021ea98d0d22 100644
--- a/packages/@aws-cdk/aws-lambda/test/integ.layer-version.lit.expected.json
+++ b/packages/@aws-cdk/aws-lambda/test/integ.layer-version.lit.expected.json
@@ -7,6 +7,10 @@
     "MyLayerCodeS3VersionKeyA45254EC": {
       "Type": "String",
       "Description": "S3 key for asset version \"aws-cdk-layer-version-1/MyLayer/Code\""
+    },
+    "MyLayerCodeArtifactHashCCFB62E9": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"aws-cdk-layer-version-1/MyLayer/Code\""
     }
   },
   "Resources": {
diff --git a/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json
index 9ef732e1d267b..0cdb053275989 100644
--- a/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json
+++ b/packages/@aws-cdk/aws-lambda/test/integ.log-retention.expected.json
@@ -378,6 +378,10 @@
     "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aCodeS3VersionKey10C1B354": {
       "Type": "String",
       "Description": "S3 key for asset version \"aws-cdk-lambda-log-retention/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/Code\""
+    },
+    "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aCodeArtifactHash327647CC": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"aws-cdk-lambda-log-retention/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/Code\""
     }
   }
 }
\ No newline at end of file
diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json
index f3d6889b8f319..69cf9b444c5e8 100644
--- a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json
+++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json
@@ -418,6 +418,10 @@
       "Type": "String",
       "Description": "S3 key for asset version \"test-bucket-deployments-1/DeployMe/Asset\""
     },
+    "DeployMeAssetArtifactHash31436FAA": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"test-bucket-deployments-1/DeployMe/Asset\""
+    },
     "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CCodeS3Bucket6E5FB2B7": {
       "Type": "String",
       "Description": "S3 bucket for asset \"test-bucket-deployments-1/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Code\""
@@ -426,6 +430,10 @@
       "Type": "String",
       "Description": "S3 key for asset version \"test-bucket-deployments-1/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Code\""
     },
+    "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CCodeArtifactHashEF37AD24": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"test-bucket-deployments-1/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Code\""
+    },
     "DeployWithPrefixAssetS3Bucket8B33F071": {
       "Type": "String",
       "Description": "S3 bucket for asset \"test-bucket-deployments-1/DeployWithPrefix/Asset\""
@@ -433,6 +441,10 @@
     "DeployWithPrefixAssetS3VersionKey45049418": {
       "Type": "String",
       "Description": "S3 key for asset version \"test-bucket-deployments-1/DeployWithPrefix/Asset\""
+    },
+    "DeployWithPrefixAssetArtifactHash9495ADE8": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"test-bucket-deployments-1/DeployWithPrefix/Asset\""
     }
   }
 }
\ No newline at end of file
diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.expected.json
index 39f4bde65a48a..4d900089ebbca 100644
--- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.expected.json
+++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.ec2-task.expected.json
@@ -500,13 +500,13 @@
                       "RepositoryName"
                     ]
                   },
-                  ":",
+                  "@sha256:",
                   {
                     "Fn::Select": [
                       1,
                       {
                         "Fn::Split": [
-                          ":",
+                          "@sha256:",
                           {
                             "Ref": "EventImageImageNameE972A8B1"
                           }
@@ -667,7 +667,7 @@
             0,
             {
               "Fn::Split": [
-                ":",
+                "@sha256:",
                 {
                   "Ref": "EventImageImageNameE972A8B1"
                 }
@@ -675,7 +675,11 @@
             }
           ]
         }
-      }
+      },
+      "DependsOn": [
+        "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C",
+        "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17"
+      ]
     },
     "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17": {
       "Type": "AWS::IAM::Role",
@@ -754,7 +758,7 @@
                         0,
                         {
                           "Fn::Split": [
-                            ":",
+                            "@sha256:",
                             {
                               "Ref": "EventImageImageNameE972A8B1"
                             }
@@ -985,6 +989,10 @@
     "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276": {
       "Type": "String",
       "Description": "S3 key for asset version \"aws-ecs-integ2/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\""
+    },
+    "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeArtifactHash8BCBAA49": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"aws-ecs-integ2/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\""
     }
   }
-}
+}
\ No newline at end of file
diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.expected.json
index 56f65b123f8fd..03a26b042def5 100644
--- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.expected.json
+++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.fargate-task.expected.json
@@ -120,13 +120,13 @@
                       "RepositoryName"
                     ]
                   },
-                  ":",
+                  "@sha256:",
                   {
                     "Fn::Select": [
                       1,
                       {
                         "Fn::Split": [
-                          ":",
+                          "@sha256:",
                           {
                             "Ref": "EventImageImageNameE972A8B1"
                           }
@@ -289,7 +289,7 @@
             0,
             {
               "Fn::Split": [
-                ":",
+                "@sha256:",
                 {
                   "Ref": "EventImageImageNameE972A8B1"
                 }
@@ -297,7 +297,11 @@
             }
           ]
         }
-      }
+      },
+      "DependsOn": [
+        "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C",
+        "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17"
+      ]
     },
     "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17": {
       "Type": "AWS::IAM::Role",
@@ -376,7 +380,7 @@
                         0,
                         {
                           "Fn::Split": [
-                            ":",
+                            "@sha256:",
                             {
                               "Ref": "EventImageImageNameE972A8B1"
                             }
@@ -629,6 +633,10 @@
     "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276": {
       "Type": "String",
       "Description": "S3 key for asset version \"aws-ecs-integ2/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\""
+    },
+    "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeArtifactHash8BCBAA49": {
+      "Type": "String",
+      "Description": "Artifact hash for asset \"aws-ecs-integ2/AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c/Code\""
     }
   }
-}
+}
\ No newline at end of file
diff --git a/packages/@aws-cdk/cx-api/lib/metadata/assets.ts b/packages/@aws-cdk/cx-api/lib/metadata/assets.ts
index 98086a44616b1..7103e2cc8aab6 100644
--- a/packages/@aws-cdk/cx-api/lib/metadata/assets.ts
+++ b/packages/@aws-cdk/cx-api/lib/metadata/assets.ts
@@ -26,21 +26,34 @@ export const ASSET_RESOURCE_METADATA_PROPERTY_KEY = 'aws:asset:property';
  */
 export const ASSET_PREFIX_SEPARATOR = '||';
 
-export interface FileAssetMetadataEntry {
+interface BaseAssetMetadataEntry {
   /**
    * Requested packaging style
    */
-  readonly packaging: 'zip' | 'file';
+  readonly packaging: string;
+
+  /**
+   * Logical identifier for the asset
+   */
+  readonly id: string;
+
+  /**
+   * The hash of the source directory used to build the asset.
+   */
+  readonly sourceHash: string;
 
   /**
    * Path on disk to the asset
    */
   readonly path: string;
 
+}
+
+export interface FileAssetMetadataEntry extends BaseAssetMetadataEntry {
   /**
-   * Logical identifier for the asset
+   * Requested packaging style
    */
-  readonly id: string;
+  readonly packaging: 'zip' | 'file';
 
   /**
    * Name of parameter where S3 bucket should be passed in
@@ -51,26 +64,21 @@ export interface FileAssetMetadataEntry {
    * Name of parameter where S3 key should be passed in
    */
   readonly s3KeyParameter: string;
-}
-
-export interface ContainerImageAssetMetadataEntry {
-  /**
-   * Type of asset
-   */
-  readonly packaging: 'container-image';
 
   /**
-   * Path on disk to the asset
+   * The name of the parameter where the hash of the bundled asset should be passed in.
    */
-  readonly path: string;
+  readonly artifactHashParameter: string;
+}
 
+export interface ContainerImageAssetMetadataEntry extends BaseAssetMetadataEntry {
   /**
-   * Logical identifier for the asset
+   * Type of asset
    */
-  readonly id: string;
+  readonly packaging: 'container-image';
 
   /**
-   * ECR Repository name and tag (separated by ":") where this asset is stored.
+   * ECR Repository name and repo digest (separated by "@sha256:") where this image is stored.
    */
   readonly imageNameParameter: string;
 
diff --git a/packages/aws-cdk/lib/api/toolkit-info.ts b/packages/aws-cdk/lib/api/toolkit-info.ts
index 9ddb0994d8345..988fdf29a1756 100644
--- a/packages/aws-cdk/lib/api/toolkit-info.ts
+++ b/packages/aws-cdk/lib/api/toolkit-info.ts
@@ -1,7 +1,7 @@
 import cxapi = require('@aws-cdk/cx-api');
 import aws = require('aws-sdk');
 import colors = require('colors/safe');
-import { md5hash } from '../archive';
+import { contentHash } from '../archive';
 import { debug } from '../logging';
 import { Mode } from './aws-auth/credentials';
 import { BUCKET_DOMAIN_NAME_OUTPUT, BUCKET_NAME_OUTPUT  } from './bootstrap-environment';
@@ -17,6 +17,7 @@ export interface UploadProps {
 export interface Uploaded {
   filename: string;
   key: string;
+  hash: string;
   changed: boolean;
 }
 
@@ -47,10 +48,10 @@ export class ToolkitInfo {
 
   /**
    * Uploads a data blob to S3 under the specified key prefix.
-   * Uses md5 hash to render the full key and skips upload if an object
+   * Uses a hash to render the full key and skips upload if an object
    * already exists by this key.
    */
-  public async uploadIfChanged(data: any, props: UploadProps): Promise<Uploaded> {
+  public async uploadIfChanged(data: string | Buffer | DataView, props: UploadProps): Promise<Uploaded> {
     const s3 = await this.props.sdk.s3(this.props.environment, Mode.ForWriting);
 
     const s3KeyPrefix = props.s3KeyPrefix || '';
@@ -58,7 +59,7 @@ export class ToolkitInfo {
 
     const bucket = this.props.bucketName;
 
-    const hash = md5hash(data);
+    const hash = contentHash(data);
     const filename = `${hash}${s3KeySuffix}`;
     const key = `${s3KeyPrefix}${filename}`;
     const url = `s3://${bucket}/${key}`;
@@ -66,10 +67,10 @@ export class ToolkitInfo {
     debug(`${url}: checking if already exists`);
     if (await objectExists(s3, bucket, key)) {
       debug(`${url}: found (skipping upload)`);
-      return { filename, key, changed: false };
+      return { filename, key, hash, changed: false };
     }
 
-    const uploaded = { filename, key, changed: true };
+    const uploaded = { filename, key, hash, changed: true };
 
     // Upload if it's new or server-side copy if it was already uploaded previously
     const previous = this.previousUploads[hash];
diff --git a/packages/aws-cdk/lib/archive.ts b/packages/aws-cdk/lib/archive.ts
index b7c3e647625ba..c924035bca369 100644
--- a/packages/aws-cdk/lib/archive.ts
+++ b/packages/aws-cdk/lib/archive.ts
@@ -25,6 +25,6 @@ export function zipDirectory(directory: string, outputFile: string): Promise<voi
   });
 }
 
-export function md5hash(data: any) {
+export function contentHash(data: string | Buffer | DataView) {
   return crypto.createHash('sha256').update(data).digest('hex');
 }
diff --git a/packages/aws-cdk/lib/assets.ts b/packages/aws-cdk/lib/assets.ts
index 5ea3de78d726e..9c88481909979 100644
--- a/packages/aws-cdk/lib/assets.ts
+++ b/packages/aws-cdk/lib/assets.ts
@@ -91,6 +91,7 @@ async function prepareFileAsset(
     return [
       { ParameterKey: asset.s3BucketParameter, UsePreviousValue: true },
       { ParameterKey: asset.s3KeyParameter, UsePreviousValue: true },
+      { ParameterKey: asset.artifactHashParameter, UsePreviousValue: true },
     ];
   }
 
@@ -101,7 +102,7 @@ async function prepareFileAsset(
 
   const s3KeyPrefix = `assets/${asset.id}/`;
 
-  const { filename, key, changed } = await toolkitInfo.uploadIfChanged(data, {
+  const { filename, key, changed, hash } = await toolkitInfo.uploadIfChanged(data, {
     s3KeyPrefix,
     s3KeySuffix: path.extname(filePath),
     contentType
@@ -120,6 +121,7 @@ async function prepareFileAsset(
   return [
     { ParameterKey: asset.s3BucketParameter, ParameterValue: toolkitInfo.bucketName },
     { ParameterKey: asset.s3KeyParameter, ParameterValue: `${s3KeyPrefix}${ASSET_PREFIX_SEPARATOR}${filename}` },
+    { ParameterKey: asset.artifactHashParameter, ParameterValue: hash },
   ];
 }
 
diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts
index 500c7d2bb9fc8..3087ded295671 100644
--- a/packages/aws-cdk/lib/cdk-toolkit.ts
+++ b/packages/aws-cdk/lib/cdk-toolkit.ts
@@ -80,7 +80,7 @@ export class CdkToolkit {
       options.exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Upstream);
 
     for (const stack of stacks) {
-      if (stacks.length !== 1) { highlight(stack.name); }
+      if (stacks.length !== 1) { highlight(stack.name); }
       if (!stack.environment) {
         // tslint:disable-next-line:max-line-length
         throw new Error(`Stack ${stack.name} does not define an environment, and AWS credentials could not be obtained from standard locations or no region was configured.`);
diff --git a/packages/aws-cdk/lib/docker.ts b/packages/aws-cdk/lib/docker.ts
index f7593ac21828d..f554aa20422c7 100644
--- a/packages/aws-cdk/lib/docker.ts
+++ b/packages/aws-cdk/lib/docker.ts
@@ -1,10 +1,8 @@
 import { ContainerImageAssetMetadataEntry } from '@aws-cdk/cx-api';
 import { CloudFormation } from 'aws-sdk';
-import crypto = require('crypto');
 import { ToolkitInfo } from './api/toolkit-info';
 import { debug, print } from './logging';
 import { shell } from './os';
-import { PleaseHold } from './util/please-hold';
 
 /**
  * Build and upload a Docker image
@@ -31,7 +29,7 @@ import { PleaseHold } from './util/please-hold';
 export async function prepareContainerAsset(asset: ContainerImageAssetMetadataEntry,
                                             toolkitInfo: ToolkitInfo,
                                             reuse: boolean,
-                                            ci?: boolean): Promise<CloudFormation.Parameter[]> {
+                                            ci?: boolean): Promise<[CloudFormation.Parameter]> {
 
   if (reuse) {
     return [
@@ -41,7 +39,6 @@ export async function prepareContainerAsset(asset: ContainerImageAssetMetadataEn
 
   debug(' 👑  Preparing Docker image asset:', asset.path);
 
-  const buildHold = new PleaseHold(` ⌛ Building Asset Docker image ${asset.id} from ${asset.path}; this may take a while.`);
   try {
     const ecr = await toolkitInfo.prepareEcrRepository(asset);
     const latest = `${ecr.repositoryUri}:latest`;
@@ -60,57 +57,39 @@ export async function prepareContainerAsset(asset: ContainerImageAssetMetadataEn
       }
     }
 
-    buildHold.start();
-
-    const baseCommand = ['docker',
-      'build',
-      ...Object.entries(asset.buildArgs || {}).map(([k, v]) => `--build-arg ${k}=${v}`), // Pass build args if any
-      '--quiet',
-      asset.path];
+    const baseCommand = [
+      'docker', 'build',
+      ...Object.entries(asset.buildArgs || {}).map(([k, v]) => `--build-arg ${k}=${v}`),
+      '--tag', latest,
+      asset.path
+    ];
 
     const command = ci
       ? [...baseCommand, '--cache-from', latest] // This does not fail if latest is not available
       : baseCommand;
-    const imageId = (await shell(command, { quiet: true })).trim();
-
-    buildHold.stop();
-
-    const tag = await calculateImageFingerprint(imageId);
-
-    debug(` ⌛  Image has tag ${tag}, checking ECR repository`);
-    const imageExists = await toolkitInfo.checkEcrImage(ecr.repositoryName, tag);
-
-    if (imageExists) {
-      debug(' 👑  Image already uploaded.');
-    } else {
-      // Login and push
-      debug(` ⌛  Image needs to be uploaded first.`);
-
-      if (!loggedIn) { // We could be already logged in if in CI
-        await dockerLogin(toolkitInfo);
-        loggedIn = true;
-      }
+    await shell(command);
 
-      const qualifiedImageName = `${ecr.repositoryUri}:${tag}`;
-
-      await shell(['docker', 'tag', imageId, qualifiedImageName]);
-
-      // There's no way to make this quiet, so we can't use a PleaseHold. Print a header message.
-      print(` ⌛ Pushing Docker image for ${asset.path}; this may take a while.`);
-      await shell(['docker', 'push', qualifiedImageName]);
-      debug(` 👑  Docker image for ${asset.path} pushed.`);
-    }
-
-    if (!loggedIn) { // We could be already logged in if in CI or if image did not exist
+    // Login and push
+    if (!loggedIn) { // We could be already logged in if in CI
       await dockerLogin(toolkitInfo);
+      loggedIn = true;
     }
 
-    // Always tag and push latest
-    await shell(['docker', 'tag', imageId, latest]);
+    // There's no way to make this quiet, so we can't use a PleaseHold. Print a header message.
+    print(` ⌛ Pushing Docker image for ${asset.path}; this may take a while.`);
     await shell(['docker', 'push', latest]);
+    debug(` 👑  Docker image for ${asset.path} pushed.`);
+
+    // Get the (single) repo-digest for latest, which'll be <ecr.repositoryUrl>@sha256:<repoImageSha256>
+    const repoDigests = (await shell(['docker', 'image', 'inspect', latest, '--format', '{{range .RepoDigests}}{{.}}|{{end}}'])).trim();
+    const requiredPrefix = `${ecr.repositoryUri}@sha256:`;
+    const repoDigest = repoDigests.split('|').find(digest => digest.startsWith(requiredPrefix));
+    if (!repoDigest) {
+      throw new Error(`Unable to identify repository digest (none starts with ${requiredPrefix}) in:\n${repoDigests}`);
+    }
 
     return [
-      { ParameterKey: asset.imageNameParameter, ParameterValue: `${ecr.repositoryName}:${tag}` },
+      { ParameterKey: asset.imageNameParameter, ParameterValue: repoDigest.replace(ecr.repositoryUri, ecr.repositoryName) },
     ];
   } catch (e) {
     if (e.code === 'ENOENT') {
@@ -118,8 +97,6 @@ export async function prepareContainerAsset(asset: ContainerImageAssetMetadataEn
       throw new Error('Error building Docker image asset; you need to have Docker installed in order to be able to build image assets. Please install Docker and try again.');
     }
     throw e;
-  } finally {
-    buildHold.stop();
   }
 }
 
@@ -133,156 +110,3 @@ async function dockerLogin(toolkitInfo: ToolkitInfo) {
   '--password', credentials.password,
   credentials.endpoint]);
 }
-
-/**
- * Calculate image fingerprint.
- *
- * The fingerprint has a high likelihood to be the same across repositories.
- * (As opposed to Docker's built-in image digest, which changes as soon
- * as the image is uploaded since it includes the tags that an image has).
- *
- * The fingerprint will be used as a tag to identify a particular image.
- */
-async function calculateImageFingerprint(imageId: string) {
-  const manifestString = await shell(['docker', 'inspect', imageId], { quiet: true });
-  const manifest = JSON.parse(manifestString)[0];
-
-  // Id can change
-  delete manifest.Id;
-
-  // Repository-based identifiers are out
-  delete manifest.RepoTags;
-  delete manifest.RepoDigests;
-
-  // Metadata that has no bearing on the image contents
-  delete manifest.Created;
-
-  // We're interested in the image itself, not any running instaces of it
-  delete manifest.Container;
-  delete manifest.ContainerConfig;
-
-  // We're not interested in the Docker version used to create this image
-  delete manifest.DockerVersion;
-
-  // On some Docker versions Metadata contains a LastTagTime which updates
-  // on every push, causing us to miss all cache hits.
-  delete manifest.Metadata;
-
-  // GraphDriver is about running the image, not about the image itself.
-  delete manifest.GraphDriver;
-
-  return crypto.createHash('sha256').update(JSON.stringify(manifest)).digest('hex');
-}
-
-/**
- * Example of a Docker manifest
- *
- * [
- *     {
- *         "Id": "sha256:3a90542991d03007fd1d8f3b3a6ab04ebb02386785430fe48a867768a048d828",
- *         "RepoTags": [
- *             "993655754359.dkr.ecr.us-east-1.amazonaws.com/cdk/awsecsintegimage7c15b8c6:latest"
- *         ],
- *         "RepoDigests": [
- *             "993655754359.dkr.ecr.us-east-1.amazo....5e50c0cfc3f2355191934b05df68cd3339a044959111ffec2e14765"
- *         ],
- *         "Parent": "sha256:465720f8f43c9c0aff5dcc731d4e368a3927cae4e885442d4ba0bf8a867b7561",
- *         "Comment": "",
- *         "Created": "2018-10-17T10:16:40.775888476Z",
- *         "Container": "20f145d2e7fbf126ca9f4422497b932bc96b5faa038dc032de1e246f64e03a66",
- *         "ContainerConfig": {
- *             "Hostname": "9b48b580a312",
- *             "Domainname": "",
- *             "User": "",
- *             "AttachStdin": false,
- *             "AttachStdout": false,
- *             "AttachStderr": false,
- *             "ExposedPorts": {
- *                 "8000/tcp": {}
- *             },
- *             "Tty": false,
- *             "OpenStdin": false,
- *             "StdinOnce": false,
- *             "Env": [
- *                 "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
- *                 "LANG=C.UTF-8",
- *                 "GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D",
- *                 "PYTHON_VERSION=3.6.6",
- *                 "PYTHON_PIP_VERSION=18.1"
- *             ],
- *             "Cmd": [
- *                 "/bin/sh",
- *                 "-c",
- *                 "#(nop) ",
- *                 "CMD [\"/bin/sh\" \"-c\" \"python3 index.py\"]"
- *             ],
- *             "ArgsEscaped": true,
- *             "Image": "sha256:465720f8f43c9c0aff5dcc731d4e368a3927cae4e885442d4ba0bf8a867b7561",
- *             "Volumes": null,
- *             "WorkingDir": "/code",
- *             "Entrypoint": null,
- *             "OnBuild": [],
- *             "Labels": {}
- *         },
- *         "DockerVersion": "17.03.2-ce",
- *         "Author": "",
- *         "Config": {
- *             "Hostname": "9b48b580a312",
- *             "Domainname": "",
- *             "User": "",
- *             "AttachStdin": false,
- *             "AttachStdout": false,
- *             "AttachStderr": false,
- *             "ExposedPorts": {
- *                 "8000/tcp": {}
- *             },
- *             "Tty": false,
- *             "OpenStdin": false,
- *             "StdinOnce": false,
- *             "Env": [
- *                 "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
- *                 "LANG=C.UTF-8",
- *                 "GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D",
- *                 "PYTHON_VERSION=3.6.6",
- *                 "PYTHON_PIP_VERSION=18.1"
- *             ],
- *             "Cmd": [
- *                 "/bin/sh",
- *                 "-c",
- *                 "python3 index.py"
- *             ],
- *             "ArgsEscaped": true,
- *             "Image": "sha256:465720f8f43c9c0aff5dcc731d4e368a3927cae4e885442d4ba0bf8a867b7561",
- *             "Volumes": null,
- *             "WorkingDir": "/code",
- *             "Entrypoint": null,
- *             "OnBuild": [],
- *             "Labels": {}
- *         },
- *         "Architecture": "amd64",
- *         "Os": "linux",
- *         "Size": 917730468,
- *         "VirtualSize": 917730468,
- *         "GraphDriver": {
- *             "Name": "aufs",
- *             "Data": null
- *         },
- *         "RootFS": {
- *             "Type": "layers",
- *             "Layers": [
- *                 "sha256:f715ed19c28b66943ac8bc12dbfb828e8394de2530bbaf1ecce906e748e4fdff",
- *                 "sha256:8bb25f9cdc41e7d085033af15a522973b44086d6eedd24c11cc61c9232324f77",
- *                 "sha256:08a01612ffca33483a1847c909836610610ce523fb7e1aca880140ee84df23e9",
- *                 "sha256:1191b3f5862aa9231858809b7ac8b91c0b727ce85c9b3279932f0baacc92967d",
- *                 "sha256:9978d084fd771e0b3d1acd7f3525d1b25288ababe9ad8ed259b36101e4e3addd",
- *                 "sha256:2f4f74d3821ecbdd60b5d932452ea9e30cecf902334165c4a19837f6ee636377",
- *                 "sha256:003bb6178bc3218242d73e51d5e9ab2f991dc607780194719c6bd4c8c412fe8c",
- *                 "sha256:15b32d849da2239b1af583f9381c7a75d7aceba12f5ddfffa7a059116cf05ab9",
- *                 "sha256:6e5c5f6bf043bc634378b1e4b61af09be74741f2ac80204d7a373713b1fd5a40",
- *                 "sha256:3260e00e353bfb765b25597d13868c2ef64cb3d509875abcfb58c4e9bf7f4ee2",
- *                 "sha256:f3274b75856311e92e14a1270c78737c86456d6353fe4a83bd2e81bcd2a996ea"
- *             ]
- *         }
- *     }
- * ]
- */
diff --git a/packages/aws-cdk/lib/util/please-hold.ts b/packages/aws-cdk/lib/util/please-hold.ts
deleted file mode 100644
index cb6eff963296b..0000000000000
--- a/packages/aws-cdk/lib/util/please-hold.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import colors = require('colors/safe');
-import { print } from "../logging";
-
-/**
- * Print a message to the logger in case the operation takes a long time
- */
-export class PleaseHold {
-  private handle?: NodeJS.Timer;
-
-  constructor(private readonly message: string, private readonly timeoutSec = 10) {
-  }
-
-  public start() {
-    this.handle = setTimeout(this.printMessage.bind(this), this.timeoutSec * 1000);
-  }
-
-  public stop() {
-    if (this.handle) {
-      clearTimeout(this.handle);
-    }
-  }
-
-  private printMessage() {
-    print(colors.yellow(this.message));
-  }
-}
diff --git a/packages/aws-cdk/test/test.archive.ts b/packages/aws-cdk/test/test.archive.ts
index 86e820204eee5..f26b662e79ca2 100644
--- a/packages/aws-cdk/test/test.archive.ts
+++ b/packages/aws-cdk/test/test.archive.ts
@@ -4,7 +4,7 @@ import { Test } from 'nodeunit';
 import os = require('os');
 import path = require('path');
 import { promisify } from 'util';
-import { md5hash, zipDirectory } from '../lib/archive';
+import { contentHash, zipDirectory } from '../lib/archive';
 const exec = promisify(_exec);
 
 export = {
@@ -38,8 +38,8 @@ export = {
     await new Promise(ok => setTimeout(ok, 2000)); // wait 2s
     await zipDirectory(originalDir, zipFile2);
 
-    const hash1 = md5hash(await fs.readFile(zipFile1));
-    const hash2 = md5hash(await fs.readFile(zipFile2));
+    const hash1 = contentHash(await fs.readFile(zipFile1));
+    const hash2 = contentHash(await fs.readFile(zipFile2));
 
     test.deepEqual(hash1, hash2, 'md5 hash of two zips of the same content are not the same');
     test.done();
diff --git a/packages/aws-cdk/test/test.assets.ts b/packages/aws-cdk/test/test.assets.ts
index ff1ec67a82bb5..5d4a93fae0d2a 100644
--- a/packages/aws-cdk/test/test.assets.ts
+++ b/packages/aws-cdk/test/test.assets.ts
@@ -21,7 +21,8 @@ export = {
             id: 'SomeStackSomeResource4567',
             packaging: 'file',
             s3BucketParameter: 'BucketParameter',
-            s3KeyParameter: 'KeyParameter'
+            s3KeyParameter: 'KeyParameter',
+            artifactHashParameter: 'ArtifactHashParameter',
           } as AssetMetadataEntry,
           trace: []
         }]
@@ -43,6 +44,7 @@ export = {
     test.deepEqual(params, [
       { ParameterKey: 'BucketParameter', ParameterValue: 'bucket' },
       { ParameterKey: 'KeyParameter', ParameterValue: 'assets/SomeStackSomeResource4567/||12345.js' },
+      { ParameterKey: 'ArtifactHashParameter', ParameterValue: '12345' },
     ]);
 
     test.done();
@@ -65,7 +67,8 @@ export = {
             id: 'SomeStackSomeResource4567',
             packaging: 'file',
             s3BucketParameter: 'BucketParameter',
-            s3KeyParameter: 'KeyParameter'
+            s3KeyParameter: 'KeyParameter',
+            artifactHashParameter: 'ArtifactHashParameter',
           } as AssetMetadataEntry,
           trace: []
         }]
@@ -87,6 +90,7 @@ export = {
     test.deepEqual(params, [
       { ParameterKey: 'BucketParameter', UsePreviousValue: true },
       { ParameterKey: 'KeyParameter', UsePreviousValue: true },
+      { ParameterKey: 'ArtifactHashParameter', UsePreviousValue: true },
     ]);
 
     test.done();
@@ -139,8 +143,9 @@ class FakeToolkit {
     const filename = `12345${props.s3KeySuffix}`;
     return {
       filename,
+      key: `${props.s3KeyPrefix}${filename}`,
+      hash: '12345',
       changed: true,
-      key: `${props.s3KeyPrefix}${filename}`
     };
   }
 }
diff --git a/packages/aws-cdk/test/test.docker.ts b/packages/aws-cdk/test/test.docker.ts
index b46d81b8042b8..4b2c4c875373a 100644
--- a/packages/aws-cdk/test/test.docker.ts
+++ b/packages/aws-cdk/test/test.docker.ts
@@ -40,6 +40,7 @@ export = {
       packaging: 'container-image',
       path: '/foo',
       repositoryName: 'some-name',
+      sourceHash: '0123456789abcdef',
     };
 
     try {
@@ -76,6 +77,7 @@ export = {
       imageNameParameter: 'MyParameter',
       packaging: 'container-image',
       path: '/foo',
+      sourceHash: '1234567890abcdef',
       repositoryName: 'some-name',
       buildArgs: {
         a: 'b',
@@ -90,7 +92,7 @@ export = {
     }
 
     // THEN
-    const command = ['docker', 'build', '--build-arg a=b', '--build-arg c=d', '--quiet', '/foo'];
+    const command = ['docker', 'build', '--build-arg a=b', '--build-arg c=d', '--tag', `uri:latest`, '/foo'];
     test.ok(shellStub.calledWith(command));
 
     prepareEcrRepositoryStub.restore();
diff --git a/packages/decdk/lib/declarative-stack.ts b/packages/decdk/lib/declarative-stack.ts
index c508362714123..385edd2640f9f 100644
--- a/packages/decdk/lib/declarative-stack.ts
+++ b/packages/decdk/lib/declarative-stack.ts
@@ -7,6 +7,7 @@ import { isConstruct, isDataType, isEnumLikeClass, isSerializableInterface, Sche
 export interface DeclarativeStackProps extends cdk.StackProps {
   typeSystem: reflect.TypeSystem;
   template: any;
+  workingDirectory?: string;
 }
 
 export class DeclarativeStack extends cdk.Stack {
@@ -37,7 +38,12 @@ export class DeclarativeStack extends cdk.Stack {
       const typeInfo = typeSystem.findFqn(rprops.Type + 'Props');
       const typeRef = new reflect.TypeReference(typeSystem, typeInfo);
       const Ctor = resolveType(rprops.Type);
-      new Ctor(this, logicalId, deserializeValue(this, typeRef, true, 'Properties', rprops.Properties));
+
+      // Changing working directory if needed, such that relative paths in the template are resolved relative to the
+      // template's location, and not to the current process' CWD.
+      _cwd(props.workingDirectory, () =>
+        new Ctor(this, logicalId, deserializeValue(this, typeRef, true, 'Properties', rprops.Properties)));
+
       delete template.Resources[logicalId];
     }
 
@@ -437,4 +443,15 @@ function isCfnResourceType(resourceType: string) {
   return resourceType.includes('::');
 }
 
-class ValidationError extends Error { }
\ No newline at end of file
+class ValidationError extends Error { }
+
+function _cwd<T>(workDir: string | undefined, cb: () => T): T {
+  if (!workDir) { return cb(); }
+  const prevWd = process.cwd();
+  try {
+    process.chdir(workDir);
+    return cb();
+  } finally {
+    process.chdir(prevWd);
+  }
+}
diff --git a/packages/decdk/test/__snapshots__/synth.test.js.snap b/packages/decdk/test/__snapshots__/synth.test.js.snap
index 155f265f0271e..ffc700b178f15 100644
--- a/packages/decdk/test/__snapshots__/synth.test.js.snap
+++ b/packages/decdk/test/__snapshots__/synth.test.js.snap
@@ -31,6 +31,10 @@ Object {
     },
   },
   "Parameters": Object {
+    "HelloLambdaCodeArtifactHashBB927E34": Object {
+      "Description": "Artifact hash for asset \\"apigw/HelloLambda/Code\\"",
+      "Type": "String",
+    },
     "HelloLambdaCodeS3BucketB83F7900": Object {
       "Description": "S3 bucket for asset \\"apigw/HelloLambda/Code\\"",
       "Type": "String",
@@ -947,6 +951,10 @@ Object {
     },
   },
   "Parameters": Object {
+    "HelloWorldFunctionCodeArtifactHashEF4E01C5": Object {
+      "Description": "Artifact hash for asset \\"lambda-events/HelloWorldFunction/Code\\"",
+      "Type": "String",
+    },
     "HelloWorldFunctionCodeS3BucketF87BE172": Object {
       "Description": "S3 bucket for asset \\"lambda-events/HelloWorldFunction/Code\\"",
       "Type": "String",
@@ -1512,6 +1520,10 @@ Object {
 exports[`lambda-topic.json: lambda-topic 1`] = `
 Object {
   "Parameters": Object {
+    "LambdaCodeArtifactHash305E64BB": Object {
+      "Description": "Artifact hash for asset \\"lambda-topic/Lambda/Code\\"",
+      "Type": "String",
+    },
     "LambdaCodeS3Bucket65766E44": Object {
       "Description": "S3 bucket for asset \\"lambda-topic/Lambda/Code\\"",
       "Type": "String",
diff --git a/packages/decdk/test/synth.test.ts b/packages/decdk/test/synth.test.ts
index 28324dd00d9d5..6725a3ba9927c 100644
--- a/packages/decdk/test/synth.test.ts
+++ b/packages/decdk/test/synth.test.ts
@@ -27,6 +27,7 @@ async function obtainTypeSystem() {
 
 for (const templateFile of fs.readdirSync(dir)) {
   test(templateFile, async () => {
+    const workingDirectory = dir;
     const template = await readTemplate(path.resolve(dir, templateFile));
     const typeSystem = await obtainTypeSystem();
 
@@ -34,10 +35,11 @@ for (const templateFile of fs.readdirSync(dir)) {
     const stackName = stackNameFromFileName(templateFile);
 
     new DeclarativeStack(app, stackName, {
+      workingDirectory,
       template,
       typeSystem
     });
 
     expect(app.synthesizeStack(stackName).template).toMatchSnapshot(stackName);
   });
-}
\ No newline at end of file
+}