Pulumi’s dynamic providers let you manage literally anything, not just cloud resources.

Here’s a simple Python dynamic provider that manages a "file" resource. This resource will create a file with specific content and ensure it’s deleted when the Pulumi stack is destroyed.

import pulumi
import os

class File(pulumi.ComponentResource):
    def __init__(self, name, content, opts=None):
        super().__init__('custom:file:File', name, {}, opts)
        self.content = content

        # Register the resource with its inputs
        self.register_outputs({})

    def create(self, props):
        # This method is called when the resource is created
        file_path = f"{self.urn.split('/')[-1]}.txt"
        with open(file_path, "w") as f:
            f.write(self.content)
        print(f"Created file: {file_path}")
        return {
            "filePath": file_path,
            "content": self.content
        }

    def update(self, id, olds, news):
        # This method is called when the resource is updated
        file_path = olds["filePath"]
        if olds["content"] != news["content"]:
            with open(file_path, "w") as f:
                f.write(news["content"])
            print(f"Updated file: {file_path}")
        return {
            "filePath": file_path,
            "content": news["content"]
        }

    def delete(self, id, props):
        # This method is called when the resource is deleted
        file_path = props["filePath"]
        if os.path.exists(file_path):
            os.remove(file_path)
            print(f"Deleted file: {file_path}")

# Example Usage in __main__.py
import pulumi_myprovider as myprovider

# Create a file resource
my_file = myprovider.File("my-test-file", content="Hello, Pulumi Dynamic Providers!")

This File component resource acts as our dynamic provider. When Pulumi encounters myprovider.File("my-test-file", ...), it doesn’t know about files. Instead, it consults the custom:file:File type. The create, update, and delete methods within our File class are invoked by the Pulumi engine to perform the actual operations on the filesystem.

The core idea is that Pulumi’s engine is agnostic to the type of resource being managed. It simply knows that for a resource of type custom:file:File, it needs to call a corresponding create, update, or delete function. These functions are implemented in your language of choice (Python, Go, TypeScript, .NET) and are responsible for interacting with any backend system – a custom API, a database, a legacy system, or in this case, the local filesystem.

The pulumi.ComponentResource is the base class for creating dynamic providers. You register your custom resource type with a unique URN (Uniform Resource Name) like custom:file:File. The create, update, and delete methods receive the resource’s properties and an identifier. The engine uses this identifier to track the resource’s state.

Let’s look at the create method:

def create(self, props):
    file_path = f"{self.urn.split('/')[-1]}.txt"
    with open(file_path, "w") as f:
        f.write(self.content)
    print(f"Created file: {file_path}")
    return {
        "filePath": file_path,
        "content": self.content
    }

When Pulumi needs to create this resource, it calls create. We construct a filename based on the resource’s URN (e.g., my-test-file.txt) and write the provided content to it. The dictionary returned by create is what Pulumi stores as the state of this resource. This state includes filePath and content, which are crucial for subsequent update and delete operations.

The update method handles changes. If the content input property changes, we rewrite the file. The delete method is straightforward: it removes the file if it exists.

The real power comes when you realize these methods can call any API. Imagine managing a Kubernetes custom resource, a database record, or even triggering a complex CI/CD pipeline. The Pulumi engine just orchestrates the calls to your provider’s methods.

The register_outputs({}) call in __init__ is important. It tells Pulumi that this component resource doesn’t have any outputs that should be directly exposed as stack outputs. The actual outputs are returned by the create, update, and delete methods.

The most surprising thing about dynamic providers is how little the Pulumi engine actually knows about your resources. It’s a generic orchestrator. Your provider code is where all the domain-specific logic lives. You could, in theory, write a dynamic provider that manages physical objects in your office by interfacing with a robotic arm.

The id parameter passed to create, update, and delete is an opaque identifier that the Pulumi engine assigns to the resource. Your provider should store and return this id if it’s managing external systems that require their own unique identifiers for updates and deletions. For our file example, we don’t explicitly use an id because the filesystem path serves as our implicit identifier for operations.

The next step in understanding dynamic providers is exploring how to package them into installable Pulumi packages, making them reusable across different projects and teams.

Want structured learning?

Take the full Pulumi course →