diff --git a/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php b/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php
index 7890b27d28..fa9f83ad1e 100644
--- a/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php
+++ b/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php
@@ -1,221 +1,263 @@
 <?php
 
 final class DrydockManagementLeaseWorkflow
   extends DrydockManagementWorkflow {
 
   protected function didConstruct() {
     $this
       ->setName('lease')
       ->setSynopsis(pht('Lease a resource.'))
       ->setArguments(
         array(
           array(
             'name'      => 'type',
             'param'     => 'resource_type',
             'help'      => pht('Resource type.'),
           ),
           array(
             'name' => 'until',
             'param' => 'time',
             'help' => pht('Set lease expiration time.'),
           ),
           array(
             'name' => 'attributes',
             'param' => 'file',
             'help' => pht(
               'JSON file with lease attributes. Use "-" to read attributes '.
               'from stdin.'),
           ),
+          array(
+            'name' => 'count',
+            'param' => 'N',
+            'default' => 1,
+            'help' => pht('Lease a given number of identical resources.'),
+          ),
         ));
   }
 
   public function execute(PhutilArgumentParser $args) {
     $viewer = $this->getViewer();
 
     $resource_type = $args->getArg('type');
-    if (!$resource_type) {
+    if (!phutil_nonempty_string($resource_type)) {
       throw new PhutilArgumentUsageException(
         pht(
-          'Specify a resource type with `%s`.',
-          '--type'));
+          'Specify a resource type with "--type".'));
     }
 
     $until = $args->getArg('until');
     if (phutil_nonempty_string($until)) {
       $until = strtotime($until);
       if ($until <= 0) {
         throw new PhutilArgumentUsageException(
           pht(
-            'Unable to parse argument to "%s".',
-            '--until'));
+            'Unable to parse argument to "--until".'));
       }
     }
 
+    $count = $args->getArgAsInteger('count');
+    if ($count < 1) {
+      throw new PhutilArgumentUsageException(
+        pht(
+          'Value provided to "--count" must be a nonzero, positive '.
+          'number.'));
+    }
+
     $attributes_file = $args->getArg('attributes');
     if (phutil_nonempty_string($attributes_file)) {
       if ($attributes_file == '-') {
         echo tsprintf(
           "%s\n",
-          'Reading JSON attributes from stdin...');
+          pht('Reading JSON attributes from stdin...'));
         $data = file_get_contents('php://stdin');
       } else {
         $data = Filesystem::readFile($attributes_file);
       }
 
       $attributes = phutil_json_decode($data);
     } else {
       $attributes = array();
     }
 
-    $lease = id(new DrydockLease())
-      ->setResourceType($resource_type);
+    $leases = array();
+    for ($idx = 0; $idx < $count; $idx++) {
+      $lease = id(new DrydockLease())
+        ->setResourceType($resource_type);
 
-    $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID();
-    $lease->setAuthorizingPHID($drydock_phid);
+      $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID();
+      $lease->setAuthorizingPHID($drydock_phid);
 
-    if ($attributes) {
-      $lease->setAttributes($attributes);
-    }
+      if ($attributes) {
+        $lease->setAttributes($attributes);
+      }
 
-    // TODO: This is not hugely scalable, although this is a debugging workflow
-    // so maybe it's fine. Do we even need `bin/drydock lease` in the long run?
-    $all_blueprints = id(new DrydockBlueprintQuery())
-      ->setViewer($viewer)
-      ->execute();
-    $allowed_phids = mpull($all_blueprints, 'getPHID');
-    if (!$allowed_phids) {
-      throw new Exception(
-        pht(
-          'No blueprints exist which can plausibly allocate resources to '.
-          'satisfy the requested lease.'));
-    }
-    $lease->setAllowedBlueprintPHIDs($allowed_phids);
+      // TODO: This is not hugely scalable, although this is a debugging
+      // workflow so maybe it's fine. Do we even need `bin/drydock lease` in
+      // the long run?
+      $all_blueprints = id(new DrydockBlueprintQuery())
+        ->setViewer($viewer)
+        ->execute();
+      $allowed_phids = mpull($all_blueprints, 'getPHID');
+      if (!$allowed_phids) {
+        throw new Exception(
+          pht(
+            'No blueprints exist which can plausibly allocate resources to '.
+            'satisfy the requested lease.'));
+      }
+      $lease->setAllowedBlueprintPHIDs($allowed_phids);
 
-    if ($until) {
-      $lease->setUntil($until);
-    }
+      if ($until) {
+        $lease->setUntil($until);
+      }
 
-    // If something fatals or the user interrupts the process (for example,
-    // with "^C"), release the lease. We'll cancel this below, if the lease
-    // actually activates.
-    $lease->setReleaseOnDestruction(true);
+      // If something fatals or the user interrupts the process (for example,
+      // with "^C"), release the lease. We'll cancel this below, if the lease
+      // actually activates.
+      $lease->setReleaseOnDestruction(true);
+
+      $leases[] = $lease;
+    }
 
     // TODO: This would probably be better handled with PhutilSignalRouter,
     // but it currently doesn't route SIGINT. We're initializing it to setup
     // SIGTERM handling and make eventual migration easier.
     $router = PhutilSignalRouter::getRouter();
     pcntl_signal(SIGINT, array($this, 'didReceiveInterrupt'));
 
     $t_start = microtime(true);
-    $lease->queueForActivation();
+
 
     echo tsprintf(
-      "%s\n\n        __%s__\n\n%s\n",
-      pht('Queued lease for activation:'),
-      PhabricatorEnv::getProductionURI($lease->getURI()),
-      pht('Waiting for daemons to activate lease...'));
+      "%s\n\n",
+      pht('Leases queued for activation:'));
+
+    foreach ($leases as $lease) {
+      $lease->queueForActivation();
 
-    $this->waitUntilActive($lease);
+      echo tsprintf(
+        "        __%s__\n",
+        PhabricatorEnv::getProductionURI($lease->getURI()));
+    }
+
+    echo tsprintf(
+      "\n%s\n\n",
+      pht('Waiting for daemons to activate leases...'));
+
+    foreach ($leases as $lease) {
+      $this->waitUntilActive($lease);
+    }
 
     // Now that we've survived activation and the lease is good, make it
     // durable.
-    $lease->setReleaseOnDestruction(false);
+    foreach ($leases as $lease) {
+      $lease->setReleaseOnDestruction(false);
+    }
+
     $t_end = microtime(true);
 
     echo tsprintf(
-      "%s\n\n        %s\n\n%s\n",
+      "\n%s\n\n",
       pht(
-        'Activation complete. This lease is permanent until manually '.
-        'released with:'),
-      pht('$ ./bin/drydock release-lease --id %d', $lease->getID()),
+        'Activation complete. Leases are permanent until manually '.
+        'released with:'));
+
+    foreach ($leases as $lease) {
+      echo tsprintf(
+        "    %s\n",
+        pht('$ ./bin/drydock release-lease --id %d', $lease->getID()));
+    }
+
+    echo tsprintf(
+      "\n%s\n",
       pht(
-        'Lease activated in %sms.',
+        'Leases activated in %sms.',
         new PhutilNumber((int)(($t_end - $t_start) * 1000))));
 
     return 0;
   }
 
   public function didReceiveInterrupt($signo) {
     // Doing this makes us run destructors, particularly the "release on
     // destruction" trigger on the lease.
     exit(128 + $signo);
   }
 
   private function waitUntilActive(DrydockLease $lease) {
     $viewer = $this->getViewer();
 
     $log_cursor = 0;
     $log_types = DrydockLogType::getAllLogTypes();
 
     $is_active = false;
     while (!$is_active) {
       $lease->reload();
 
       $pager = id(new AphrontCursorPagerView())
         ->setBeforeID($log_cursor);
 
       // While we're waiting, show the user any logs which the daemons have
       // generated to give them some clue about what's going on.
       $logs = id(new DrydockLogQuery())
         ->setViewer($viewer)
         ->withLeasePHIDs(array($lease->getPHID()))
         ->executeWithCursorPager($pager);
       if ($logs) {
         $logs = mpull($logs, null, 'getID');
         ksort($logs);
         $log_cursor = last_key($logs);
       }
 
       foreach ($logs as $log) {
         $type_key = $log->getType();
         if (isset($log_types[$type_key])) {
           $type_object = id(clone $log_types[$type_key])
             ->setLog($log)
             ->setViewer($viewer);
 
           $log_data = $log->getData();
 
           $type = $type_object->getLogTypeName();
           $data = $type_object->renderLogForText($log_data);
         } else {
           $type = pht('Unknown ("%s")', $type_key);
           $data = null;
         }
 
         echo tsprintf(
-          "<%s> %B\n",
+          "(Lease #%d) <%s> %B\n",
+          $lease->getID(),
           $type,
           $data);
       }
 
       $status = $lease->getStatus();
 
       switch ($status) {
         case DrydockLeaseStatus::STATUS_ACTIVE:
           $is_active = true;
           break;
         case DrydockLeaseStatus::STATUS_RELEASED:
           throw new Exception(pht('Lease has already been released!'));
         case DrydockLeaseStatus::STATUS_DESTROYED:
           throw new Exception(pht('Lease has already been destroyed!'));
         case DrydockLeaseStatus::STATUS_BROKEN:
           throw new Exception(pht('Lease has been broken!'));
         case DrydockLeaseStatus::STATUS_PENDING:
         case DrydockLeaseStatus::STATUS_ACQUIRED:
           break;
         default:
           throw new Exception(
             pht(
               'Lease has unknown status "%s".',
               $status));
       }
 
       if ($is_active) {
         break;
       } else {
         sleep(1);
       }
     }
   }
 
 }