Jenkins jobs often need exclusive access to certain resources, like a specific test environment, a physical device, or a database instance. Without proper coordination, multiple jobs could try to use the same resource simultaneously, leading to data corruption, test failures, and general chaos. The "Lockable Resources" plugin for Jenkins is the standard way to manage this.

Let’s see it in action. Imagine we have three Jenkins jobs: job-a, job-b, and job-c. We also have a single, shared testing machine named test-machine-01 that only one job can use at a time.

Here’s the configuration for job-a to ensure it exclusively uses test-machine-01:

pipeline {
    agent any
    stages {
        stage('Acquire Lock') {
            steps {
                lock(resource: 'test-machine-01', quantity: 1) {
                    // The code within this block will only execute
                    // when the 'test-machine-01' resource is available.
                    // If it's already in use, this job will wait here.
                    echo "Lock acquired for test-machine-01. Starting work..."
                    // Simulate work that requires the resource
                    sleep(time: 30, unit: 'SECONDS')
                    echo "Work finished. Releasing lock..."
                }
            }
        }
        stage('Post-Lock Work') {
            steps {
                echo "This stage runs after the lock is released."
            }
        }
    }
}

The lock step is the core. You specify the resource name (which must be defined in the Jenkins global configuration) and the quantity needed. If test-machine-01 is currently in use by another job, job-a will simply pause at the lock step, showing a "Waiting for lock" status in the build queue. Once the previous job releases the lock, job-a will acquire it and proceed.

The real magic happens when you configure the resources themselves. Navigate to "Manage Jenkins" -> "Configure System" and scroll down to the "Lockable Resources" section. Here, you can define your shared resources.

For our test-machine-01 example, you’d add it like this:

  • Resource Name: test-machine-01
  • Description: Dedicated machine for integration tests
  • Capabilities: (Optional, but useful for more complex scenarios)
  • Reserved by: (Automatically populated when a job holds the lock)

You can also define resources with a quantity greater than 1. For instance, if you had four identical load balancer instances, you could create a resource named load-balancer with a quantity of 4. A job requesting lock(resource: 'load-balancer', quantity: 2) would then acquire two of those instances, leaving two available for other jobs.

The internal mechanism involves a dedicated Jenkins subsystem that tracks resource availability. When a lock step is encountered, Jenkins checks the state of the requested resource. If the available quantity is sufficient, the resource is marked as "in use" for that specific build, and the build proceeds. If not, the build is queued, and Jenkins periodically re-checks resource availability. When a build finishes (or explicitly releases the lock using unlock), the resource is marked as available again. This prevents race conditions by enforcing sequential access to critical shared components.

A subtle but powerful feature is the ability to define capabilities for your resources. Instead of just locking a generic test-machine, you could define multiple machines (test-machine-01, test-machine-02) and assign them a capability like docker-enabled. Then, your pipeline could request lock(capabilities: 'docker-enabled', quantity: 1), and Jenkins would pick any available machine with that capability, offering more flexibility than locking a specific named resource.

When a job fails while holding a lock, the lock is not automatically released by default. This can leave resources permanently unavailable until manually unlocked.

Want structured learning?

Take the full Jenkins course →