diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -791,6 +791,7 @@
     'DoorkeeperSchemaSpec' => 'applications/doorkeeper/storage/DoorkeeperSchemaSpec.php',
     'DoorkeeperTagView' => 'applications/doorkeeper/view/DoorkeeperTagView.php',
     'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php',
+    'DrydockAllocationContext' => 'applications/drydock/util/DrydockAllocationContext.php',
     'DrydockAllocatorWorker' => 'applications/drydock/worker/DrydockAllocatorWorker.php',
     'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php',
     'DrydockBlueprint' => 'applications/drydock/storage/DrydockBlueprint.php',
@@ -842,6 +843,8 @@
     'DrydockManagementLeaseWorkflow' => 'applications/drydock/management/DrydockManagementLeaseWorkflow.php',
     'DrydockManagementReleaseWorkflow' => 'applications/drydock/management/DrydockManagementReleaseWorkflow.php',
     'DrydockManagementWorkflow' => 'applications/drydock/management/DrydockManagementWorkflow.php',
+    'DrydockMinMaxBlueprintImplementation' => 'applications/drydock/blueprint/DrydockMinMaxBlueprintImplementation.php',
+    'DrydockMinMaxExpiryBlueprintImplementation' => 'applications/drydock/blueprint/DrydockMinMaxExpiryBlueprintImplementation.php',
     'DrydockPreallocatedHostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php',
     'DrydockQuery' => 'applications/drydock/query/DrydockQuery.php',
     'DrydockResource' => 'applications/drydock/storage/DrydockResource.php',
@@ -4464,6 +4467,7 @@
     'DoorkeeperSchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'DoorkeeperTagView' => 'AphrontView',
     'DoorkeeperTagsController' => 'PhabricatorController',
+    'DrydockAllocationContext' => 'Phobject',
     'DrydockAllocatorWorker' => 'PhabricatorWorker',
     'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface',
     'DrydockBlueprint' => array(
@@ -4529,6 +4533,8 @@
     'DrydockManagementLeaseWorkflow' => 'DrydockManagementWorkflow',
     'DrydockManagementReleaseWorkflow' => 'DrydockManagementWorkflow',
     'DrydockManagementWorkflow' => 'PhabricatorManagementWorkflow',
+    'DrydockMinMaxBlueprintImplementation' => 'DrydockBlueprintImplementation',
+    'DrydockMinMaxExpiryBlueprintImplementation' => 'DrydockMinMaxBlueprintImplementation',
     'DrydockPreallocatedHostBlueprintImplementation' => 'DrydockBlueprintImplementation',
     'DrydockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'DrydockResource' => array(
diff --git a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
--- a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
+++ b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
@@ -19,6 +19,10 @@
 
   abstract public function isEnabled();
 
+  public function isTest() {
+    return false;
+  }
+
   abstract public function getBlueprintName();
   abstract public function getDescription();
 
@@ -42,7 +46,7 @@
     return $lease;
   }
 
-  protected function getInstance() {
+  public function getInstance() {
     if (!$this->instance) {
       throw new Exception(
         pht('Attach the blueprint instance to the implementation.'));
@@ -127,6 +131,11 @@
     $allocated = false;
     $allocation_exception = null;
 
+    $context = new DrydockAllocationContext(
+      $this->getInstance(),
+      $resource,
+      $lease);
+
     $resource->openTransaction();
       $resource->beginReadLocking();
         $resource->reload();
@@ -142,9 +151,9 @@
 
         try {
           $allocated = $this->shouldAllocateLease(
+            $context,
             $resource,
-            $ephemeral_lease,
-            $other_leases);
+            $ephemeral_lease);
         } catch (Exception $ex) {
           $allocation_exception = $ex;
         }
@@ -197,19 +206,15 @@
    * better implemented in @{method:canAllocateLease}, which serves as a
    * cheap filter before lock acquisition.
    *
+   * @param   DrydockAllocationContext Relevant contextual information.
    * @param   DrydockResource     Candidate resource to allocate the lease on.
    * @param   DrydockLease        Pending lease that wants to allocate here.
-   * @param   list<DrydockLease>  Other allocated and acquired leases on the
-   *                              resource. The implementation can inspect them
-   *                              to verify it can safely add the new lease.
-   * @return  bool                True to allocate the lease on the resource;
-   *                              false to reject it.
    * @task lease
    */
   abstract protected function shouldAllocateLease(
+    DrydockAllocationContext $context,
     DrydockResource $resource,
-    DrydockLease $lease,
-    array $other_leases);
+    DrydockLease $lease);
 
 
   /**
@@ -265,10 +270,25 @@
     DrydockLease $lease);
 
 
+  /**
+   * Release an allocated lease, performing any desired cleanup.
+   *
+   * After this method executes, the lease status is moved to `RELEASED`.
+   *
+   * If release fails, throw an exception.
+   *
+   * @param   DrydockResource   Resource to release the lease from.
+   * @param   DrydockLease      Lease to release.
+   * @return  void
+   */
+  abstract protected function executeReleaseLease(
+    DrydockResource $resource,
+    DrydockLease $lease);
 
   final public function releaseLease(
     DrydockResource $resource,
-    DrydockLease $lease) {
+    DrydockLease $lease,
+    $caused_by_closing_resource = false) {
     $scope = $this->pushActiveScope(null, $lease);
 
     $released = false;
@@ -286,10 +306,36 @@
       $lease->endReadLocking();
     $lease->saveTransaction();
 
+    // Execute clean up outside of the lock and don't perform clean up if the
+    // resource is closing anyway, because in that scenario, the closing
+    // resource will clean up all the leases anyway (e.g. an EC2 host being
+    // terminated that contains leases on it's instance storage).
+    if ($released && !$caused_by_closing_resource) {
+      $this->executeReleaseLease($resource, $lease);
+    }
+
     if (!$released) {
       throw new Exception(pht('Unable to release lease: lease not active!'));
     }
 
+    if (!$caused_by_closing_resource) {
+      // Check to see if the resource has no more leases, and if so, ask the
+      // blueprint as to whether this resource should be closed.
+      $context = new DrydockAllocationContext(
+        $this->getInstance(),
+        $resource,
+        $lease);
+
+      if ($context->getCurrentResourceLeaseCount() === 0) {
+        if ($this->shouldCloseUnleasedResource($context, $resource)) {
+          self::writeLog(
+            $resource,
+            null,
+            pht('Closing resource because it has no more active leases'));
+          $this->closeResource($resource);
+        }
+      }
+    }
   }
 
 
@@ -301,10 +347,106 @@
     return true;
   }
 
-  abstract protected function executeAllocateResource(DrydockLease $lease);
+  public function canAllocateResourceForLease(DrydockLease $lease) {
+    return true;
+  }
 
+  abstract protected function executeInitializePendingResource(
+    DrydockResource $resource,
+    DrydockLease $lease);
+
+  abstract protected function executeAllocateResource(
+    DrydockResource $resource,
+    DrydockLease $lease);
+
+  /**
+   * Closes a previously allocated resource, performing any desired
+   * cleanup.
+   *
+   * After this method executes, the release status is moved to `CLOSED`.
+   *
+   * If release fails, throw an exception.
+   *
+   * @param   DrydockResource   Resource to close.
+   * @return  void
+   */
+  abstract protected function executeCloseResource(
+    DrydockResource $resource);
+
+  /**
+   * Return whether or not a resource that now has no leases on it
+   * should be automatically closed.
+   *
+   * @param DrydockAllocationContext Relevant contextual information.
+   * @param DrydockResource       The resource that has no more leases on it.
+   * @return bool
+   */
+  abstract protected function shouldCloseUnleasedResource(
+    DrydockAllocationContext $context,
+    DrydockResource $resource);
+
+  final public function closeResource(DrydockResource $resource) {
+    $resource->openTransaction();
+      $resource->setStatus(DrydockResourceStatus::STATUS_CLOSING);
+      $resource->save();
+
+      $statuses = array(
+        DrydockLeaseStatus::STATUS_PENDING,
+        DrydockLeaseStatus::STATUS_ACTIVE,
+      );
+
+      $leases = id(new DrydockLeaseQuery())
+        ->setViewer(PhabricatorUser::getOmnipotentUser())
+        ->withResourceIDs(array($resource->getID()))
+        ->withStatuses($statuses)
+        ->execute();
+
+      foreach ($leases as $lease) {
+        switch ($lease->getStatus()) {
+          case DrydockLeaseStatus::STATUS_PENDING:
+            $message = pht('Breaking pending lease (resource closing).');
+            $lease->setStatus(DrydockLeaseStatus::STATUS_BROKEN);
+            break;
+          case DrydockLeaseStatus::STATUS_ACTIVE:
+            $message = pht('Releasing active lease (resource closing).');
+            $this->releaseLease($resource, $lease, true);
+            $lease->setStatus(DrydockLeaseStatus::STATUS_RELEASED);
+            break;
+        }
+        self::writeLog($resource, $lease, $message);
+        $lease->save();
+      }
+
+      $this->executeCloseResource($resource);
+
+      $resource->setStatus(DrydockResourceStatus::STATUS_CLOSED);
+      $resource->save();
+    $resource->saveTransaction();
+  }
+
+  final public function initializePendingResource(
+    DrydockResource $resource,
+    DrydockLease $lease) {
+
+    $scope = $this->pushActiveScope($resource, $lease);
+
+    $this->log(pht(
+      'Blueprint \'%s\': Initializing Resource for \'%s\'',
+      $this->getBlueprintClass(),
+      $lease->getLeaseName()));
+
+    try {
+      $this->executeInitializePendingResource($resource, $lease);
+    } catch (Exception $ex) {
+      $this->logException($ex);
+      throw $ex;
+    }
+  }
+
+  final public function allocateResource(
+    DrydockResource $resource,
+    DrydockLease $lease) {
 
-  final public function allocateResource(DrydockLease $lease) {
     $scope = $this->pushActiveScope(null, $lease);
 
     $this->log(
@@ -314,7 +456,7 @@
         $lease->getLeaseName()));
 
     try {
-      $resource = $this->executeAllocateResource($lease);
+      $this->executeAllocateResource($resource, $lease);
       $this->validateAllocatedResource($resource);
     } catch (Exception $ex) {
       $this->logException($ex);
@@ -389,25 +531,6 @@
     return idx(self::getAllBlueprintImplementations(), $class);
   }
 
-  protected function newResourceTemplate($name) {
-    $resource = id(new DrydockResource())
-      ->setBlueprintPHID($this->getInstance()->getPHID())
-      ->setBlueprintClass($this->getBlueprintClass())
-      ->setType($this->getType())
-      ->setStatus(DrydockResourceStatus::STATUS_PENDING)
-      ->setName($name)
-      ->save();
-
-    $this->activeResource = $resource;
-
-    $this->log(
-      pht(
-        "Blueprint '%s': Created New Template",
-        $this->getBlueprintClass()));
-
-    return $resource;
-  }
-
   /**
    * Sanity checks that the blueprint is implemented properly.
    */
@@ -441,7 +564,7 @@
     }
   }
 
-  private function pushActiveScope(
+  public function pushActiveScope(
     DrydockResource $resource = null,
     DrydockLease $lease = null) {
 
diff --git a/src/applications/drydock/blueprint/DrydockMinMaxBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockMinMaxBlueprintImplementation.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/blueprint/DrydockMinMaxBlueprintImplementation.php
@@ -0,0 +1,152 @@
+<?php
+
+abstract class DrydockMinMaxBlueprintImplementation
+  extends DrydockBlueprintImplementation {
+
+  public function canAllocateMoreResources(array $pool) {
+    $max_count = $this->getDetail('max-count');
+
+    if ($max_count === null) {
+      $this->log(pht(
+        'There is no maximum resource limit specified for this blueprint'));
+      return true;
+    }
+
+    $count_pending = 0;
+    $count_allocating = 0;
+    $count_open = 0;
+
+    foreach ($pool as $resource) {
+      switch ($resource->getStatus()) {
+        case DrydockResourceStatus::STATUS_PENDING:
+          $count_pending++;
+          break;
+        case DrydockResourceStatus::STATUS_ALLOCATING:
+          $count_allocating++;
+          break;
+        case DrydockResourceStatus::STATUS_OPEN:
+          $count_open++;
+          break;
+        default:
+          $this->log(pht(
+            'Resource %d was in the pool of open resources, '.
+            'but has non-open status of %d',
+            $resource->getID(),
+            $resource->getStatus()));
+          break;
+      }
+    }
+
+    $this->log(pht(
+      'There are currently %d pending resources, %d allocating resources '.
+      'and %d open resources in the pool.',
+      $count_pending,
+      $count_allocating,
+      $count_open));
+
+    if (count($pool) < $max_count) {
+      $this->log(pht(
+        'Will permit resource allocation because %d is less than the maximum '.
+        'of %d.',
+        count($pool),
+        $max_count));
+      return true;
+    } else {
+      $this->log(pht(
+        'Will deny resource allocation because %d is less than the maximum '.
+        'of %d.',
+        count($pool),
+        $max_count));
+      return false;
+    }
+  }
+
+  protected function shouldAllocateLease(
+    DrydockAllocationContext $context,
+    DrydockResource $resource,
+    DrydockLease $lease) {
+
+    // If the current resource can allocate a lease, allow it.
+    if ($context->getCurrentResourceLeaseCount() <
+          $this->getDetail('leases-per-resource')) {
+      return true;
+    }
+
+    // We don't have enough room under the `leases-per-instance` limit, but
+    // this limit can be bypassed if we've allocated all of the resources
+    // we allow.
+    $open_count = $context->getBlueprintOpenResourceCount();
+    if ($open_count < $this->getDetail('max-count')) {
+      if ($this->getDetail('max-count') !== null) {
+        return false;
+      }
+    }
+
+    // Find the resource that has the least leases.
+    $all_lease_counts_grouped = $context->getResourceLeaseCounts();
+    $minimum_lease_count = $all_lease_counts_grouped[$resource->getID()];
+    $minimum_lease_resource_id = $resource->getID();
+    foreach ($all_lease_counts_grouped as $resource_id => $lease_count) {
+      if ($minimum_lease_count > $lease_count) {
+        $minimum_lease_count = $lease_count;
+        $minimum_lease_resource_id = $resource_id;
+      }
+    }
+
+    // If we are that resource, then allow it, otherwise let the other
+    // less-leased resource run through this logic and allocate the lease.
+    return $minimum_lease_resource_id === $resource->getID();
+  }
+
+  protected function shouldCloseUnleasedResource(
+    DrydockAllocationContext $context,
+    DrydockResource $resource) {
+
+    return $context->getBlueprintOpenResourceCount() >
+      $this->getDetail('min-count');
+  }
+
+  public function getFieldSpecifications() {
+    return array(
+      'min-max-header' => array(
+        'name' => pht('Allocation Limits'),
+        'type' => 'header',
+      ),
+      'min-count' => array(
+        'name' => pht('Minimum Resources'),
+        'type' => 'int',
+        'required' => true,
+        'caption' => pht(
+          'The minimum number of resources to keep open in '.
+          'this pool at all times.'),
+      ),
+      'max-count' => array(
+        'name' => pht('Maximum Resources'),
+        'type' => 'int',
+        'caption' => pht(
+          'The maximum number of resources to allow open at any time.  '.
+          'If the number of resources currently open are equal to '.
+          '`max-count` and another lease is requested, Drydock will place '.
+          'leases on existing resources and thus exceeding '.
+          '`leases-per-resource`.  If this parameter is left blank, then '.
+          'this blueprint has no limit on the number of resources it '.
+          'can allocate.'),
+      ),
+      'leases-per-resource' => array(
+        'name' => pht('Maximum Leases Per Resource'),
+        'type' => 'int',
+        'required' => true,
+        'caption' => pht(
+          'The soft limit on the number of leases to allocate to an '.
+          'individual resource in the pool.  Drydock will choose the '.
+          'resource with the lowest number of leases when selecting a '.
+          'resource to lease on.  If all current resources have '.
+          '`leases-per-resource` leases on them, then Drydock will allocate '.
+          'another resource providing `max-count` would not be exceeded.'.
+          '  If `max-count` would be exceeded, Drydock will instead '.
+          'overallocate the lease to an existing resource and '.
+          'exceed the limit specified here.'),
+      ),
+    );
+  }
+}
diff --git a/src/applications/drydock/blueprint/DrydockMinMaxExpiryBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockMinMaxExpiryBlueprintImplementation.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/blueprint/DrydockMinMaxExpiryBlueprintImplementation.php
@@ -0,0 +1,100 @@
+<?php
+
+abstract class DrydockMinMaxExpiryBlueprintImplementation
+  extends DrydockMinMaxBlueprintImplementation {
+
+  public function canAllocateMoreResources(array $pool) {
+    $max_count = $this->getDetail('max-count');
+
+    if ($max_count === null) {
+      return parent::canAllocateMoreResources($pool);
+    }
+
+    $expiry = $this->getDetail('expiry');
+
+    if ($expiry === null) {
+      return count($pool) < $max_count;
+    }
+
+    // Only count resources that haven't yet expired, so we can overallocate
+    // if another expired resource is about to be closed (but is still waiting
+    // on it's current resources to be released).
+    $now = time();
+    $pool_copy = array();
+    foreach ($pool as $resource) {
+      $lifetime = $now - $resource->getDateCreated();
+      if ($lifetime <= $expiry) {
+        $pool_copy[] = $resource;
+      }
+    }
+
+    return parent::canAllocateMoreResources($pool_copy);
+  }
+
+  protected function shouldAllocateLease(
+    DrydockAllocationContext $context,
+    DrydockResource $resource,
+    DrydockLease $lease) {
+
+    // If we have no leases allocated to this resource, then we always allow
+    // the parent logic to evaluate.  The reason for this is that an expired
+    // resource can only be closed when a lease is released, so if the resource
+    // is open and has no leases, then we'll never reach the code that checks
+    // the expiry to close it.  So we allow this lease to occur, so that we'll
+    // hit `shouldCloseUnleasedResource` in the future and the resource will
+    // be closed.
+    if ($context->getCurrentResourceLeaseCount() === 0) {
+      return parent::shouldAllocateLease($context, $resource, $lease);
+    }
+
+    $expiry = $this->getDetail('expiry');
+
+    if ($expiry !== null) {
+      $lifetime = time() - $resource->getDateCreated();
+
+      if ($lifetime > $expiry) {
+        // Prevent allocation of leases to this resource, since it's over
+        // it's lifetime allowed.
+        return false;
+      }
+    }
+
+    return parent::shouldAllocateLease($context, $resource, $lease);
+  }
+
+  protected function shouldCloseUnleasedResource(
+    DrydockAllocationContext $context,
+    DrydockResource $resource) {
+
+    $expiry = $this->getDetail('expiry');
+
+    if ($expiry !== null) {
+      $lifetime = time() - $resource->getDateCreated();
+
+      if ($lifetime > $expiry) {
+        // Force closure of resources that have expired.
+        return true;
+      }
+    }
+
+    return parent::shouldCloseUnleasedResource($context, $resource);
+  }
+
+  public function getFieldSpecifications() {
+    return array(
+      'expiry-header' => array(
+        'name' => pht('Resource Expiration'),
+        'type' => 'header',
+      ),
+      'expiry' => array(
+        'name' => pht('Expiry Time'),
+        'type' => 'int',
+        'caption' => pht(
+          'After this time (in seconds) has elapsed since resource creation, '.
+          'Drydock will no longer lease against the resource, and it will be '.
+          'closed when there are no more leases (regardless of minimum '.
+          'resource limits).'),
+      ),
+    ) + parent::getFieldSpecifications();
+  }
+}
diff --git a/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php
--- a/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php
+++ b/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php
@@ -19,7 +19,14 @@
     return false;
   }
 
-  protected function executeAllocateResource(DrydockLease $lease) {
+  protected function executeInitializePendingResource(
+    DrydockResource $resource,
+    DrydockLease $lease) {}
+
+  protected function executeAllocateResource(
+    DrydockResource $resource,
+    DrydockLease $lease) {
+
     throw new Exception(
       pht("Preallocated hosts can't be dynamically allocated."));
   }
@@ -32,9 +39,9 @@
   }
 
   protected function shouldAllocateLease(
+    DrydockAllocationContext $context,
     DrydockResource $resource,
-    DrydockLease $lease,
-    array $other_leases) {
+    DrydockLease $lease) {
     return true;
   }
 
@@ -113,4 +120,20 @@
     throw new Exception(pht("No interface of type '%s'.", $type));
   }
 
+  protected function executeReleaseLease(
+    DrydockResource $resource,
+    DrydockLease $lease) {
+
+    // TODO: Remove leased directory
+  }
+
+  protected function shouldCloseUnleasedResource(
+    DrydockAllocationContext $context,
+    DrydockResource $resource) {
+
+    return false;
+  }
+
+  protected function executeCloseResource(DrydockResource $resource) {}
+
 }
diff --git a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php
--- a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php
+++ b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php
@@ -26,14 +26,21 @@
   }
 
   protected function shouldAllocateLease(
+    DrydockAllocationContext $context,
     DrydockResource $resource,
-    DrydockLease $lease,
-    array $other_leases) {
+    DrydockLease $lease) {
 
-    return !$other_leases;
+    return $context->getCurrentResourceLeaseCount() === 0;
   }
 
-  protected function executeAllocateResource(DrydockLease $lease) {
+  protected function executeInitializePendingResource(
+    DrydockResource $resource,
+    DrydockLease $lease) {}
+
+  protected function executeAllocateResource(
+    DrydockResource $resource,
+    DrydockLease $lease) {
+
     $repository_id = $lease->getAttribute('repositoryID');
     if (!$repository_id) {
       throw new Exception(
@@ -79,15 +86,15 @@
 
     $this->log(pht('Complete.'));
 
-    $resource = $this->newResourceTemplate(
-      pht(
+    $resource
+      ->setName(pht(
         'Working Copy (%s)',
-        $repository->getCallsign()));
-    $resource->setStatus(DrydockResourceStatus::STATUS_OPEN);
-    $resource->setAttribute('lease.host', $host_lease->getID());
-    $resource->setAttribute('path', $path);
-    $resource->setAttribute('repositoryID', $repository->getID());
-    $resource->save();
+        $repository->getCallsign()))
+      ->setStatus(DrydockResourceStatus::STATUS_OPEN)
+      ->setAttribute('lease.host', $host_lease->getID())
+      ->setAttribute('path', $path)
+      ->setAttribute('repositoryID', $repository->getID())
+      ->save();
 
     return $resource;
   }
@@ -117,4 +124,19 @@
     throw new Exception(pht("No interface of type '%s'.", $type));
   }
 
+  protected function executeReleaseLease(
+    DrydockResource $resource,
+    DrydockLease $lease) {}
+
+  protected function shouldCloseUnleasedResource(
+    DrydockAllocationContext $context,
+    DrydockResource $resource) {
+
+    return false;
+  }
+
+  protected function executeCloseResource(DrydockResource $resource) {
+    // TODO: Remove leased directory
+  }
+
 }
diff --git a/src/applications/drydock/constants/DrydockResourceStatus.php b/src/applications/drydock/constants/DrydockResourceStatus.php
--- a/src/applications/drydock/constants/DrydockResourceStatus.php
+++ b/src/applications/drydock/constants/DrydockResourceStatus.php
@@ -7,12 +7,16 @@
   const STATUS_CLOSED       = 2;
   const STATUS_BROKEN       = 3;
   const STATUS_DESTROYED    = 4;
+  const STATUS_CLOSING      = 5;
+  const STATUS_ALLOCATING   = 6;
 
   public static function getNameForStatus($status) {
     $map = array(
       self::STATUS_PENDING      => pht('Pending'),
+      self::STATUS_ALLOCATING   => pht('Allocating'),
       self::STATUS_OPEN         => pht('Open'),
       self::STATUS_CLOSED       => pht('Closed'),
+      self::STATUS_CLOSING      => pht('Closing'),
       self::STATUS_BROKEN       => pht('Broken'),
       self::STATUS_DESTROYED    => pht('Destroyed'),
     );
@@ -23,8 +27,10 @@
   public static function getAllStatuses() {
     return array(
      self::STATUS_PENDING,
+     self::STATUS_ALLOCATING,
      self::STATUS_OPEN,
      self::STATUS_CLOSED,
+     self::STATUS_CLOSING,
      self::STATUS_BROKEN,
      self::STATUS_DESTROYED,
     );
diff --git a/src/applications/drydock/controller/DrydockBlueprintCreateController.php b/src/applications/drydock/controller/DrydockBlueprintCreateController.php
--- a/src/applications/drydock/controller/DrydockBlueprintCreateController.php
+++ b/src/applications/drydock/controller/DrydockBlueprintCreateController.php
@@ -34,6 +34,11 @@
       ->setError($e_blueprint);
 
     foreach ($implementations as $implementation_name => $implementation) {
+      if ($implementation->isTest()) {
+        // Never show testing blueprints in the interface.
+        continue;
+      }
+
       $disabled = !$implementation->isEnabled();
 
       $control->addButton(
diff --git a/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php b/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php
--- a/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php
+++ b/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php
@@ -19,6 +19,10 @@
             'param'     => 'name=value,...',
             'help'      => pht('Resource specficiation.'),
           ),
+          array(
+            'name'      => 'in-process',
+            'help'      => pht('Acquire lease in-process.'),
+          ),
         ));
   }
 
@@ -40,7 +44,9 @@
       $attributes = $options->parse($attributes);
     }
 
-    PhabricatorWorker::setRunAllTasksInProcess(true);
+    if ($args->getArg('in-process')) {
+      PhabricatorWorker::setRunAllTasksInProcess(true);
+    }
 
     $lease = id(new DrydockLease())
       ->setResourceType($resource_type);
@@ -48,8 +54,18 @@
       $lease->setAttributes($attributes);
     }
     $lease
-      ->queueForActivation()
-      ->waitUntilActive();
+      ->queueForActivation();
+
+    while (true) {
+      try {
+        $lease->waitUntilActive();
+        break;
+      } catch (PhabricatorWorkerYieldException $ex) {
+        $console->writeOut(
+          "%s\n",
+          pht('Task yielded while acquiring %s...', $lease->getID()));
+      }
+    }
 
     $console->writeOut("%s\n", pht('Acquired Lease %s', $lease->getID()));
     return 0;
diff --git a/src/applications/drydock/storage/DrydockResource.php b/src/applications/drydock/storage/DrydockResource.php
--- a/src/applications/drydock/storage/DrydockResource.php
+++ b/src/applications/drydock/storage/DrydockResource.php
@@ -75,36 +75,8 @@
   }
 
   public function closeResource() {
-    $this->openTransaction();
-      $statuses = array(
-        DrydockLeaseStatus::STATUS_PENDING,
-        DrydockLeaseStatus::STATUS_ACTIVE,
-      );
-
-      $leases = id(new DrydockLeaseQuery())
-        ->setViewer(PhabricatorUser::getOmnipotentUser())
-        ->withResourceIDs(array($this->getID()))
-        ->withStatuses($statuses)
-        ->execute();
-
-      foreach ($leases as $lease) {
-        switch ($lease->getStatus()) {
-          case DrydockLeaseStatus::STATUS_PENDING:
-            $message = pht('Breaking pending lease (resource closing).');
-            $lease->setStatus(DrydockLeaseStatus::STATUS_BROKEN);
-            break;
-          case DrydockLeaseStatus::STATUS_ACTIVE:
-            $message = pht('Releasing active lease (resource closing).');
-            $lease->setStatus(DrydockLeaseStatus::STATUS_RELEASED);
-            break;
-        }
-        DrydockBlueprintImplementation::writeLog($this, $lease, $message);
-        $lease->save();
-      }
-
-      $this->setStatus(DrydockResourceStatus::STATUS_CLOSED);
-      $this->save();
-    $this->saveTransaction();
+    $blueprint = $this->getBlueprint();
+    $blueprint->closeResource($this);
   }
 
 
diff --git a/src/applications/drydock/util/DrydockAllocationContext.php b/src/applications/drydock/util/DrydockAllocationContext.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/util/DrydockAllocationContext.php
@@ -0,0 +1,98 @@
+<?php
+
+final class DrydockAllocationContext extends Phobject {
+
+  private $blueprintOpenResourceCount;
+  private $resourceLeaseCounts;
+  private $currentResourceLeaseCount;
+
+  public function __construct(
+    DrydockBlueprint $blueprint,
+    DrydockResource $resource = null,
+    DrydockLease $lease = null) {
+
+    $table_blueprint = $blueprint->getTableName();
+    $table_resource = id(new DrydockResource())->getTableName();
+    $table_lease = id(new DrydockLease())->getTableName();
+
+    $conn = $blueprint->establishConnection('r');
+
+    $result = queryfx_one(
+      $conn,
+      'SELECT COUNT(id) AS count '.
+      'FROM %T '.
+      'WHERE blueprintPHID = %s '.
+      'AND status IN (%Ld)',
+      $table_resource,
+      $blueprint->getPHID(),
+      array(
+        DrydockResourceStatus::STATUS_PENDING,
+        DrydockResourceStatus::STATUS_OPEN,
+        DrydockResourceStatus::STATUS_ALLOCATING,
+      ));
+    $this->setBlueprintOpenResourceCount($result['count']);
+
+    $results = queryfx_all(
+      $conn,
+      'SELECT '.
+      '  resource.id AS resourceID, '.
+      '  COUNT(lease.id) AS leaseCount '.
+      'FROM %T AS resource '.
+      'LEFT JOIN %T AS lease '.
+      '  ON lease.resourceID = resource.id '.
+      'WHERE resource.blueprintPHID = %s '.
+      'AND resource.status IN (%Ld) '.
+      'AND lease.status IN (%Ld) '.
+      'GROUP BY resource.id',
+      $table_resource,
+      $table_lease,
+      $blueprint->getPHID(),
+      array(
+        DrydockResourceStatus::STATUS_PENDING,
+        DrydockResourceStatus::STATUS_OPEN,
+        DrydockResourceStatus::STATUS_ALLOCATING,
+      ),
+      array(
+        DrydockLeaseStatus::STATUS_PENDING,
+        DrydockLeaseStatus::STATUS_ACQUIRING,
+        DrydockLeaseStatus::STATUS_ACTIVE,
+      ));
+    $results = ipull($results, 'leaseCount', 'resourceID');
+    $this->setResourceLeaseCounts($results);
+
+    if ($resource !== null) {
+      $this->setCurrentResourceLeaseCount(idx($results, $resource->getID(), 0));
+    }
+
+    // $lease is not yet used, but it's passed in so we can add additional
+    // contextual statistics later.
+  }
+
+  public function setBlueprintOpenResourceCount($blueprint_resource_count) {
+    $this->blueprintOpenResourceCount = $blueprint_resource_count;
+    return $this;
+  }
+
+  public function getBlueprintOpenResourceCount() {
+    return $this->blueprintOpenResourceCount;
+  }
+
+  public function setResourceLeaseCounts($resource_lease_counts) {
+    $this->resourceLeaseCounts = $resource_lease_counts;
+    return $this;
+  }
+
+  public function getResourceLeaseCounts() {
+    return $this->resourceLeaseCounts;
+  }
+
+  public function setCurrentResourceLeaseCount($resource_lease_counts) {
+    $this->currentResourceLeaseCount = $resource_lease_counts;
+    return $this;
+  }
+
+  public function getCurrentResourceLeaseCount() {
+    return $this->currentResourceLeaseCount;
+  }
+
+}
diff --git a/src/applications/drydock/view/DrydockResourceListView.php b/src/applications/drydock/view/DrydockResourceListView.php
--- a/src/applications/drydock/view/DrydockResourceListView.php
+++ b/src/applications/drydock/view/DrydockResourceListView.php
@@ -26,6 +26,7 @@
       $item->addAttribute($status);
 
       switch ($resource->getStatus()) {
+        case DrydockResourceStatus::STATUS_ALLOCATING:
         case DrydockResourceStatus::STATUS_PENDING:
           $item->setStatusIcon('fa-dot-circle-o yellow');
           break;
diff --git a/src/applications/drydock/worker/DrydockAllocatorWorker.php b/src/applications/drydock/worker/DrydockAllocatorWorker.php
--- a/src/applications/drydock/worker/DrydockAllocatorWorker.php
+++ b/src/applications/drydock/worker/DrydockAllocatorWorker.php
@@ -39,10 +39,21 @@
 
   protected function doWork() {
     $lease = $this->loadLease();
+
+    if ($lease->getStatus() != DrydockLeaseStatus::STATUS_PENDING) {
+      // We can't handle non-pending leases.
+      return;
+    }
+
     $this->logToDrydock(pht('Allocating Lease'));
 
     try {
       $this->allocateLease($lease);
+    } catch (PhabricatorWorkerYieldException $ex) {
+      $this->logToDrydock(pht(
+        'Another Drydock lease is being allocated right now; '.
+        'lease acquisition will continue soon'));
+      throw $ex;
     } catch (Exception $ex) {
 
       // TODO: We should really do this when archiving the task, if we've
@@ -50,7 +61,9 @@
       // and always fail after the first retry right now, so this is
       // functionally equivalent.
       $lease->reload();
-      if ($lease->getStatus() == DrydockLeaseStatus::STATUS_PENDING) {
+      if ($lease->getStatus() == DrydockLeaseStatus::STATUS_PENDING ||
+        $lease->getStatus() == DrydockLeaseStatus::STATUS_ACQUIRING) {
+
         $lease->setStatus(DrydockLeaseStatus::STATUS_BROKEN);
         $lease->save();
       }
@@ -76,6 +89,16 @@
 
     $blueprints = $this->loadAllBlueprints();
 
+    $lock = PhabricatorGlobalLock::newLock('drydockallocation');
+    try {
+      $lock->lock();
+    } catch (PhutilLockException $ex) {
+      // The lock is expected to be released reasonably quickly, so
+      // just push the work to the back of the queue and let it be
+      // reprocessed as soon as possible.
+      throw new PhabricatorWorkerYieldException(1);
+    }
+
     // TODO: Policy stuff.
     $pool = id(new DrydockResource())->loadAllWhere(
       'type = %s AND status = %s',
@@ -116,68 +139,226 @@
     }
 
     if (!$resource) {
-      $blueprints = DrydockBlueprintImplementation
-        ::getAllBlueprintImplementationsForResource($type);
+      // Attempt to use pending resources if we can.
+      $pool = id(new DrydockResource())->loadAllWhere(
+        'type = %s AND status = %s',
+        $lease->getResourceType(),
+        DrydockResourceStatus::STATUS_PENDING);
 
       $this->logToDrydock(
-        pht('Found %d Blueprints', count($blueprints)));
+        pht('Found %d Pending Resource(s)', count($pool)));
 
-      foreach ($blueprints as $key => $candidate_blueprint) {
-        if (!$candidate_blueprint->isEnabled()) {
-          unset($blueprints[$key]);
+      $candidates = array();
+      foreach ($pool as $key => $candidate) {
+        if (!isset($blueprints[$candidate->getBlueprintPHID()])) {
+          unset($pool[$key]);
           continue;
         }
+
+        $blueprint = $blueprints[$candidate->getBlueprintPHID()];
+        $implementation = $blueprint->getImplementation();
+
+        if ($implementation->filterResource($candidate, $lease)) {
+          $candidates[] = $candidate;
+        }
       }
 
       $this->logToDrydock(
-        pht('%d Blueprints Enabled', count($blueprints)));
+        pht('%d Pending Resource(s) Remain',
+        count($candidates)));
+
+      $resource = null;
+      if ($candidates) {
+        shuffle($candidates);
+        foreach ($candidates as $candidate_resource) {
+          $blueprint = $blueprints[$candidate_resource->getBlueprintPHID()]
+            ->getImplementation();
+          if ($blueprint->allocateLease($candidate_resource, $lease)) {
+            $resource = $candidate_resource;
+            break;
+          }
+        }
+      }
+    }
+
+    if ($resource) {
+      $lock->unlock();
+    } else {
+      $blueprints = id(new DrydockBlueprintQuery())
+        ->setViewer(PhabricatorUser::getOmnipotentUser())
+        ->execute();
+      $blueprints = mpull($blueprints, 'getImplementation', 'getPHID');
+
+      $this->logToDrydock(
+        pht('Found %d Blueprints', count($blueprints)));
 
       foreach ($blueprints as $key => $candidate_blueprint) {
-        if (!$candidate_blueprint->canAllocateMoreResources($pool)) {
+        if (!$candidate_blueprint->isEnabled()) {
+          unset($blueprints[$key]);
+          continue;
+        }
+
+        if ($candidate_blueprint->getType() !==
+          $lease->getResourceType()) {
           unset($blueprints[$key]);
           continue;
         }
       }
 
       $this->logToDrydock(
-        pht('%d Blueprints Can Allocate', count($blueprints)));
+        pht('%d Blueprints Enabled', count($blueprints)));
 
-      if (!$blueprints) {
-        $lease->setStatus(DrydockLeaseStatus::STATUS_BROKEN);
-        $lease->save();
+      $resources_per_blueprint = id(new DrydockResourceQuery())
+        ->setViewer(PhabricatorUser::getOmnipotentUser())
+        ->withStatuses(array(
+          DrydockResourceStatus::STATUS_PENDING,
+          DrydockResourceStatus::STATUS_OPEN,
+          DrydockResourceStatus::STATUS_ALLOCATING,
+        ))
+        ->execute();
+      $resources_per_blueprint = mgroup(
+        $resources_per_blueprint,
+        'getBlueprintPHID');
+
+      try {
+        foreach ($blueprints as $key => $candidate_blueprint) {
+          $scope = $candidate_blueprint->pushActiveScope(null, $lease);
+
+          $rpool = idx($resources_per_blueprint, $key, array());
+          if (!$candidate_blueprint->canAllocateMoreResources($rpool)) {
+            unset($blueprints[$key]);
+            continue;
+          }
+
+          if (!$candidate_blueprint->canAllocateResourceForLease($lease)) {
+            unset($blueprints[$key]);
+            continue;
+          }
+        }
 
         $this->logToDrydock(
-          pht(
-            "There are no resources of type '%s' available, and no ".
-            "blueprints which can allocate new ones.",
-            $type));
+          pht('%d Blueprints Can Allocate', count($blueprints)));
+
+        if (!$blueprints) {
+          $lease->setStatus(DrydockLeaseStatus::STATUS_BROKEN);
+          $lease->save();
+
+          $this->logToDrydock(
+            pht(
+              "There are no resources of type '%s' available, and no ".
+              "blueprints which can allocate new ones.",
+              $type));
+
+          $lock->unlock();
+          return;
+        }
 
-        return;
+        // TODO: Rank intelligently.
+        shuffle($blueprints);
+
+        $blueprint = head($blueprints);
+
+        // Create and save the resource preemptively with STATUS_ALLOCATING
+        // before we unlock, so that other workers will correctly count the
+        // new resource "to be allocated" when determining if they can allocate
+        // more resources to a blueprint.
+        $resource = id(new DrydockResource())
+          ->setBlueprintPHID($blueprint->getInstance()->getPHID())
+          ->setType($blueprint->getType())
+          ->setName(pht('Pending Allocation'))
+          ->setStatus(DrydockResourceStatus::STATUS_ALLOCATING)
+          ->save();
+
+        // Pre-emptively allocate the lease on the resource inside the lock,
+        // to ensure that other allocators don't cause this worker to lose
+        // an allocation race.  If we fail to allocate a lease here, then the
+        // blueprint is allocating resources it can't lease against.
+        //
+        // NOTE; shouldAllocateLease is specified to only check resource
+        // constraints, which means that it shouldn't be checking compatibility
+        // of details on resources or leases.  If there are any
+        // shouldAllocateLease that use details on the resources or leases to
+        // complete their work, then we might have to change this to:
+        //
+        //   $lease->setStatus(DrydockLeaseStatus::STATUS_ACQUIRING);
+        //   $lease->setResourceID($resource->getID());
+        //   $lease->attachResource($resource);
+        //   $lease->save();
+        //
+        // and bypass the resource quota logic entirely (and just assume that
+        // a resource allocated by a blueprint can have the lease allocated
+        // against it).
+        //
+        if (!$blueprint->allocateLease($resource, $lease)) {
+          throw new Exception(
+            'Blueprint allocated a resource, but can\'t lease against it.');
+        }
+
+        $this->logToDrydock(
+          pht('Pre-emptively allocated the lease against the new resource.'));
+
+        // We now have to set the resource into Pending status, now that the
+        // initial lease has been grabbed on the resource.  This ensures that
+        // as soon as we leave the lock, other allocators can start taking
+        // leases on it.  If we didn't do this, we can run into a scenario
+        // where all resources are in "ALLOCATING" status when an allocator
+        // runs, and instead of overleasing, the request would fail.
+        //
+        // TODO: I think this means we can remove the "ALLOCATING" status now,
+        // but I'm not entirely sure.  It's only ever used inside the lock, so
+        // I don't think any other allocators can race when attempting to
+        // use a still-allocating resource.
+        $resource
+          ->setStatus(DrydockResourceStatus::STATUS_PENDING)
+          ->save();
+
+        $this->logToDrydock(
+          pht('Moved the resource to the pending status.'));
+
+        // We must allow some initial set up of resource attributes within the
+        // lock such that when we exit, method calls to canAllocateLease will
+        // succeed even for pending resources.
+        $this->logToDrydock(
+          pht('Started initialization of pending resource.'));
+
+        $blueprint->initializePendingResource($resource, $lease);
+
+        $this->logToDrydock(
+          pht('Finished initialization of pending resource.'));
+
+        $lock->unlock();
+      } catch (Exception $ex) {
+        $lock->unlock();
+        throw $ex;
       }
 
-      // TODO: Rank intelligently.
-      shuffle($blueprints);
-
-      $blueprint = head($blueprints);
-      $resource = $blueprint->allocateResource($lease);
-
-      if (!$blueprint->allocateLease($resource, $lease)) {
-        // TODO: This "should" happen only if we lost a race with another lease,
-        // which happened to acquire this resource immediately after we
-        // allocated it. In this case, the right behavior is to retry
-        // immediately. However, other things like a blueprint allocating a
-        // resource it can't actually allocate the lease on might be happening
-        // too, in which case we'd just allocate infinite resources. Probably
-        // what we should do is test for an active or allocated lease and retry
-        // if we find one (although it might have already been released by now)
-        // and fail really hard ("your configuration is a huge broken mess")
-        // otherwise. But just throw for now since this stuff is all edge-casey.
-        // Alternatively we could bring resources up in a "BESPOKE" status
-        // and then switch them to "OPEN" only after the allocating lease gets
-        // its grubby mitts on the resource. This might make more sense but
-        // is a bit messy.
-        throw new Exception(pht('Lost an allocation race?'));
+      try {
+        $blueprint->allocateResource($resource, $lease);
+      } catch (Exception $ex) {
+        $resource->setStatus(DrydockResourceStatus::STATUS_BROKEN);
+        $resource->save();
+        throw $ex;
       }
+
+      // We do not need to call allocateLease here, because we have already
+      // performed this check inside the lock.  If the logic at the end of the
+      // lock changes to bypass allocateLease, then we probably need to do some
+      // logic like (where STATUS_RESERVED does not count towards allocation
+      // limits):
+      //
+      //   $lock->lock(10000);
+      //   $lease->setStatus(DrydockLeaseStatus::STATUS_RESERVED);
+      //   $lease->save();
+      //   try {
+      //     if (!$blueprint->allocateLease($resource, $lease)) {
+      //       throw new Exception('Lost an allocation race?');
+      //     }
+      //   } catch (Exception $ex) {
+      //     $lock->unlock();
+      //     throw $ex;
+      //   }
+      //   $lock->unlock();
+      //
     }
 
     $blueprint = $resource->getBlueprint();
diff --git a/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php
--- a/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php
@@ -21,29 +21,49 @@
 
     $settings = $this->getSettings();
 
-    // Create the lease.
-    $lease = id(new DrydockLease())
-      ->setResourceType('host')
-      ->setOwnerPHID($build_target->getPHID())
-      ->setAttributes(
+    // This build step is reentrant, because waitUntilActive may
+    // throw PhabricatorWorkerYieldException.  Check to see if there
+    // is already a lease on the build target, and if so, wait until
+    // that lease is active instead of creating a new one.
+    $artifacts = id(new HarbormasterBuildArtifactQuery())
+      ->setViewer(PhabricatorUser::getOmnipotentUser())
+      ->withBuildTargetPHIDs(array($build_target->getPHID()))
+      ->execute();
+    $artifact = count($artifacts) > 0 ? head($artifacts) : null;
+
+    if ($artifact === null) {
+      // Create the lease.
+      $lease = id(new DrydockLease())
+        ->setResourceType('host')
+        ->setOwnerPHID($build_target->getPHID())
+        ->setAttributes(
+          array(
+            'platform' => $settings['platform'],
+          ))
+        ->queueForActivation();
+
+      // Create the associated artifact.
+      $artifact = $build_target->createArtifact(
+        PhabricatorUser::getOmnipotentUser(),
+        $settings['name'],
+        HarbormasterHostArtifact::ARTIFACTCONST,
         array(
-          'platform' => $settings['platform'],
-        ))
-      ->queueForActivation();
+          'drydockLeasePHID' => $lease->getPHID(),
+        ));
+    } else {
+      // Load the lease.
+      $impl = $artifact->getArtifactImplementation();
+      $lease = $impl->loadArtifactLease(PhabricatorUser::getOmnipotentUser());
+    }
 
     // Wait until the lease is fulfilled.
-    // TODO: This will throw an exception if the lease can't be fulfilled;
-    // we should treat that as build failure not build error.
-    $lease->waitUntilActive();
-
-    // Create the associated artifact.
-    $artifact = $build_target->createArtifact(
-      PhabricatorUser::getOmnipotentUser(),
-      $settings['name'],
-      HarbormasterHostArtifact::ARTIFACTCONST,
-      array(
-        'drydockLeasePHID' => $lease->getPHID(),
-      ));
+    try {
+      $lease->waitUntilActive();
+    } catch (PhabricatorWorkerYieldException $ex) {
+      throw $ex;
+    } catch (Exception $ex) {
+      throw new HarbormasterBuildFailureException($ex->getMessage());
+    }
   }
 
   public function getArtifactOutputs() {