Your application requires provisioning resources that Pulumi does not have a native provider for, such as an internal company API or a legacy system. How do you build a Pulumi dynamic provider?
Quick Answer
A dynamic provider implements create, read, update, and delete operations as code functions that Pulumi's engine calls during deployments. Define a class extending `pulumi.dynamic.ResourceProvider` with CRUD methods, then create a `pulumi.dynamic.Resource` subclass that uses it.
Detailed Answer
Dynamic Provider Concept
Pulumi's built-in providers (AWS, Azure, GCP, Kubernetes) cover major cloud platforms, but organizations often need to manage resources in custom systems: internal service registries, legacy CMDB databases, DNS appliances, or third-party SaaS APIs without Pulumi providers. Dynamic providers let you write CRUD logic in your Pulumi program's language (TypeScript/Python) without building a full provider binary.
Implementation
A dynamic provider is a class implementing the ResourceProvider interface with methods: create(inputs) returns the new resource's ID and output properties, read(id, props) fetches current state, update(id, olds, news) modifies the resource, delete(id, props) removes it, and optionally diff(id, olds, news) determines if an update is needed. Pulumi's engine calls these methods at the appropriate lifecycle points. Each method must be idempotent.
Limitations
Dynamic providers serialize the provider code into Pulumi state, so they cannot reference external dependencies or closures that aren't serializable. For complex providers, consider building a native Pulumi provider using the Pulumi Provider SDK (Go/Python), which generates a proper provider binary with schema, documentation, and multi-language support. Dynamic providers are best for internal tooling, prototypes, or simple integrations.
Production Usage
In production, dynamic providers are commonly used for: registering services in Consul/Eureka, creating DNS records in legacy BIND servers via API, provisioning accounts in internal identity systems, or managing configuration in proprietary management platforms. Always implement proper error handling and idempotency in CRUD methods.
Code Example
import * as pulumi from "@pulumi/pulumi";
import axios from "axios";
// Dynamic provider for an internal service registry
interface ServiceRegistryInputs {
serviceName: string;
serviceUrl: string;
healthCheckPath: string;
team: string;
environment: string;
}
const serviceRegistryProvider: pulumi.dynamic.ResourceProvider = {
async create(inputs: ServiceRegistryInputs): Promise<pulumi.dynamic.CreateResult> {
// Call internal API to register service
const response = await axios.post("https://registry.internal.acme.com/api/v1/services", {
name: inputs.serviceName,
url: inputs.serviceUrl,
health_check: inputs.healthCheckPath,
team: inputs.team,
environment: inputs.environment,
}, {
headers: { "Authorization": `Bearer ${process.env.REGISTRY_TOKEN}` },
});
return {
id: response.data.id,
outs: {
...inputs,
registryId: response.data.id,
registeredAt: response.data.created_at,
},
};
},
async read(id: string, props: any): Promise<pulumi.dynamic.ReadResult> {
const response = await axios.get(
`https://registry.internal.acme.com/api/v1/services/${id}`,
{ headers: { "Authorization": `Bearer ${process.env.REGISTRY_TOKEN}` } }
);
return {
id: id,
props: {
serviceName: response.data.name,
serviceUrl: response.data.url,
healthCheckPath: response.data.health_check,
team: response.data.team,
environment: response.data.environment,
registryId: id,
registeredAt: response.data.created_at,
},
};
},
async update(id: string, olds: any, news: ServiceRegistryInputs): Promise<pulumi.dynamic.UpdateResult> {
await axios.put(`https://registry.internal.acme.com/api/v1/services/${id}`, {
name: news.serviceName,
url: news.serviceUrl,
health_check: news.healthCheckPath,
team: news.team,
}, {
headers: { "Authorization": `Bearer ${process.env.REGISTRY_TOKEN}` },
});
return { outs: { ...news, registryId: id } };
},
async delete(id: string, props: any): Promise<void> {
await axios.delete(
`https://registry.internal.acme.com/api/v1/services/${id}`,
{ headers: { "Authorization": `Bearer ${process.env.REGISTRY_TOKEN}` } }
);
},
};
// Resource class using the dynamic provider
class ServiceRegistration extends pulumi.dynamic.Resource {
public readonly registryId!: pulumi.Output<string>;
public readonly registeredAt!: pulumi.Output<string>;
constructor(name: string, args: ServiceRegistryInputs, opts?: pulumi.CustomResourceOptions) {
super(serviceRegistryProvider, name, { ...args, registryId: undefined, registeredAt: undefined }, opts);
}
}
// Usage
const registration = new ServiceRegistration("order-service-reg", {
serviceName: "order-service",
serviceUrl: "https://orders.internal.acme.com",
healthCheckPath: "/health",
team: "commerce",
environment: pulumi.getStack(),
});Interview Tip
Dynamic providers show deep Pulumi knowledge. Explain the serialization limitation upfront and when to graduate to a native provider. Emphasize idempotency in CRUD methods - if create is called twice with the same inputs, it should be safe.