0
# Provider Development
1
2
Pulumi's provider development framework enables building custom resource providers for new cloud services and infrastructure platforms, extending Pulumi's ecosystem beyond the built-in providers.
3
4
## Core Provider Interface
5
6
```typescript { .api }
7
interface Provider {
8
check?(urn: string, olds: any, news: any, randomSeed?: Buffer): Promise<CheckResult>;
9
create?(urn: string, inputs: any, timeout?: number, preview?: boolean): Promise<CreateResult>;
10
read?(id: string, urn: string, props?: any): Promise<ReadResult>;
11
update?(id: string, urn: string, olds: any, news: any, timeout?: number, ignoreChanges?: string[], preview?: boolean): Promise<UpdateResult>;
12
delete?(id: string, urn: string, props: any, timeout?: number): Promise<void>;
13
construct?(name: string, type: string, inputs: any, options: any): Promise<ConstructResult>;
14
invoke?(token: string, inputs: any, options?: any): Promise<InvokeResult>;
15
parameterize?(parameters: any): Promise<ParameterizeResult>;
16
configure?(inputs: any): Promise<void>;
17
}
18
19
function main(provider: Provider, args?: string[]): Promise<void>;
20
```
21
22
## Provider Result Types
23
24
```typescript { .api }
25
interface CheckResult {
26
inputs?: any;
27
failures?: CheckFailure[];
28
}
29
30
interface CreateResult {
31
id: string;
32
properties?: any;
33
}
34
35
interface ReadResult {
36
id?: string;
37
properties?: any;
38
inputs?: any;
39
}
40
41
interface UpdateResult {
42
properties?: any;
43
}
44
45
interface ConstructResult {
46
urn: string;
47
state?: any;
48
}
49
50
interface InvokeResult {
51
properties?: any;
52
failures?: CheckFailure[];
53
}
54
55
interface ParameterizeResult {
56
name: string;
57
version: string;
58
}
59
60
interface CheckFailure {
61
property: string;
62
reason: string;
63
}
64
```
65
66
## Utility Functions
67
68
```typescript { .api }
69
function deserializeInputs(inputsStruct: any, inputDependencies: any): any;
70
function containsOutputs(input: any): boolean;
71
function serializeProperties(props: any, keepOutputValues?: boolean): any;
72
function deserializeProperties(props: any): any;
73
```
74
75
## Usage Examples
76
77
### Basic Provider Implementation
78
79
```typescript
80
import * as pulumi from "@pulumi/pulumi";
81
import * as provider from "@pulumi/pulumi/provider";
82
83
interface MyServiceConfig {
84
apiEndpoint?: string;
85
apiToken?: string;
86
}
87
88
class MyProvider implements provider.Provider {
89
private config: MyServiceConfig = {};
90
91
async configure(inputs: any): Promise<void> {
92
this.config = inputs;
93
}
94
95
async check(urn: string, olds: any, news: any): Promise<provider.CheckResult> {
96
const failures: provider.CheckFailure[] = [];
97
98
// Validate required properties
99
if (!news.name) {
100
failures.push({ property: "name", reason: "name is required" });
101
}
102
103
return { inputs: news, failures };
104
}
105
106
async create(urn: string, inputs: any): Promise<provider.CreateResult> {
107
// Create resource via external API
108
const response = await this.callApi('POST', '/resources', inputs);
109
110
return {
111
id: response.id,
112
properties: {
113
...inputs,
114
status: response.status,
115
createdAt: response.createdAt,
116
},
117
};
118
}
119
120
async read(id: string, urn: string, props?: any): Promise<provider.ReadResult> {
121
try {
122
const response = await this.callApi('GET', `/resources/${id}`);
123
124
return {
125
id: response.id,
126
properties: {
127
name: response.name,
128
status: response.status,
129
createdAt: response.createdAt,
130
},
131
};
132
} catch (error) {
133
if (error.statusCode === 404) {
134
return { id: undefined, properties: undefined };
135
}
136
throw error;
137
}
138
}
139
140
async update(id: string, urn: string, olds: any, news: any): Promise<provider.UpdateResult> {
141
const response = await this.callApi('PUT', `/resources/${id}`, news);
142
143
return {
144
properties: {
145
...news,
146
status: response.status,
147
updatedAt: response.updatedAt,
148
},
149
};
150
}
151
152
async delete(id: string, urn: string, props: any): Promise<void> {
153
await this.callApi('DELETE', `/resources/${id}`);
154
}
155
156
async invoke(token: string, inputs: any): Promise<provider.InvokeResult> {
157
switch (token) {
158
case "myservice:index:getResource":
159
const response = await this.callApi('GET', `/resources/${inputs.id}`);
160
return { properties: response };
161
162
default:
163
throw new Error(`Unknown invoke token: ${token}`);
164
}
165
}
166
167
private async callApi(method: string, path: string, body?: any): Promise<any> {
168
const response = await fetch(`${this.config.apiEndpoint}${path}`, {
169
method,
170
headers: {
171
'Authorization': `Bearer ${this.config.apiToken}`,
172
'Content-Type': 'application/json',
173
},
174
body: body ? JSON.stringify(body) : undefined,
175
});
176
177
if (!response.ok) {
178
throw new Error(`API call failed: ${response.status} ${response.statusText}`);
179
}
180
181
return response.json();
182
}
183
}
184
185
// Start the provider
186
provider.main(new MyProvider());
187
```
188
189
### Component Resource Provider
190
191
```typescript
192
import * as pulumi from "@pulumi/pulumi";
193
import * as provider from "@pulumi/pulumi/provider";
194
195
class ComponentProvider implements provider.Provider {
196
async construct(name: string, type: string, inputs: any, options: any): Promise<provider.ConstructResult> {
197
switch (type) {
198
case "myservice:index:WebApp":
199
return this.constructWebApp(name, inputs, options);
200
201
default:
202
throw new Error(`Unknown component type: ${type}`);
203
}
204
}
205
206
private async constructWebApp(name: string, inputs: any, options: any): Promise<provider.ConstructResult> {
207
// Create child resources programmatically
208
const bucket = new pulumi.aws.s3.Bucket(`${name}-bucket`, {
209
website: {
210
indexDocument: "index.html",
211
},
212
});
213
214
const distribution = new pulumi.aws.cloudfront.Distribution(`${name}-cdn`, {
215
origins: [{
216
domainName: bucket.websiteEndpoint,
217
originId: "S3-Website",
218
customOriginConfig: {
219
httpPort: 80,
220
httpsPort: 443,
221
originProtocolPolicy: "http-only",
222
},
223
}],
224
defaultCacheBehavior: {
225
targetOriginId: "S3-Website",
226
viewerProtocolPolicy: "redirect-to-https",
227
compress: true,
228
allowedMethods: ["GET", "HEAD"],
229
},
230
enabled: true,
231
});
232
233
// Return component state
234
return {
235
urn: `urn:pulumi:${pulumi.getStack()}::${pulumi.getProject()}::${type}::${name}`,
236
state: {
237
bucketName: bucket.id,
238
distributionId: distribution.id,
239
websiteUrl: pulumi.interpolate`https://${distribution.domainName}`,
240
},
241
};
242
}
243
}
244
245
provider.main(new ComponentProvider());
246
```
247
248
### Provider with Schema
249
250
```typescript
251
import * as pulumi from "@pulumi/pulumi";
252
import * as provider from "@pulumi/pulumi/provider";
253
254
interface DatabaseResourceInputs {
255
name: string;
256
size?: "small" | "medium" | "large";
257
backupRetention?: number;
258
multiAZ?: boolean;
259
}
260
261
class DatabaseProvider implements provider.Provider {
262
async check(urn: string, olds: any, news: DatabaseResourceInputs): Promise<provider.CheckResult> {
263
const failures: provider.CheckFailure[] = [];
264
265
// Validate name
266
if (!news.name || news.name.length < 3) {
267
failures.push({
268
property: "name",
269
reason: "name must be at least 3 characters long"
270
});
271
}
272
273
// Validate size
274
if (news.size && !["small", "medium", "large"].includes(news.size)) {
275
failures.push({
276
property: "size",
277
reason: "size must be one of: small, medium, large"
278
});
279
}
280
281
// Validate backup retention
282
if (news.backupRetention !== undefined &&
283
(news.backupRetention < 0 || news.backupRetention > 35)) {
284
failures.push({
285
property: "backupRetention",
286
reason: "backupRetention must be between 0 and 35 days"
287
});
288
}
289
290
return { inputs: news, failures };
291
}
292
293
async create(urn: string, inputs: DatabaseResourceInputs): Promise<provider.CreateResult> {
294
// Map size to instance type
295
const instanceTypeMap = {
296
small: "db.t3.micro",
297
medium: "db.t3.small",
298
large: "db.t3.medium",
299
};
300
301
const instanceType = instanceTypeMap[inputs.size || "small"];
302
303
// Create RDS instance (pseudo-code)
304
const dbInstance = new pulumi.aws.rds.Instance(`${inputs.name}-db`, {
305
instanceClass: instanceType,
306
engine: "postgres",
307
dbName: inputs.name,
308
multiAz: inputs.multiAZ || false,
309
backupRetentionPeriod: inputs.backupRetention || 7,
310
// ... other properties
311
});
312
313
return {
314
id: inputs.name,
315
properties: {
316
name: inputs.name,
317
size: inputs.size || "small",
318
instanceType: instanceType,
319
endpoint: dbInstance.endpoint,
320
port: dbInstance.port,
321
},
322
};
323
}
324
325
async invoke(token: string, inputs: any): Promise<provider.InvokeResult> {
326
switch (token) {
327
case "mydb:index:getAvailableVersions":
328
return {
329
properties: {
330
versions: ["13.7", "14.6", "15.2"]
331
}
332
};
333
334
case "mydb:index:validateName":
335
const isValid = /^[a-zA-Z][a-zA-Z0-9_]*$/.test(inputs.name);
336
return {
337
properties: {
338
valid: isValid,
339
message: isValid ? "Name is valid" : "Name must start with letter and contain only alphanumeric characters and underscores"
340
}
341
};
342
343
default:
344
throw new Error(`Unknown invoke: ${token}`);
345
}
346
}
347
}
348
349
provider.main(new DatabaseProvider());
350
```
351
352
### Provider with Configuration Schema
353
354
```typescript
355
import * as pulumi from "@pulumi/pulumi";
356
import * as provider from "@pulumi/pulumi/provider";
357
358
interface ProviderConfig {
359
endpoint: string;
360
apiKey: string;
361
region?: string;
362
timeout?: number;
363
}
364
365
class ConfigurableProvider implements provider.Provider {
366
private config: ProviderConfig;
367
368
async configure(inputs: any): Promise<void> {
369
// Validate configuration
370
if (!inputs.endpoint) {
371
throw new Error("endpoint is required");
372
}
373
374
if (!inputs.apiKey) {
375
throw new Error("apiKey is required");
376
}
377
378
this.config = {
379
endpoint: inputs.endpoint,
380
apiKey: inputs.apiKey,
381
region: inputs.region || "us-east-1",
382
timeout: inputs.timeout || 30000,
383
};
384
385
// Test connection
386
await this.testConnection();
387
}
388
389
private async testConnection(): Promise<void> {
390
try {
391
const response = await fetch(`${this.config.endpoint}/health`, {
392
method: 'GET',
393
headers: {
394
'Authorization': `Bearer ${this.config.apiKey}`,
395
},
396
signal: AbortSignal.timeout(this.config.timeout!),
397
});
398
399
if (!response.ok) {
400
throw new Error(`Health check failed: ${response.status}`);
401
}
402
} catch (error) {
403
throw new Error(`Failed to connect to ${this.config.endpoint}: ${error.message}`);
404
}
405
}
406
407
async check(urn: string, olds: any, news: any): Promise<provider.CheckResult> {
408
// Implementation...
409
return { inputs: news, failures: [] };
410
}
411
412
// Other provider methods...
413
}
414
415
provider.main(new ConfigurableProvider());
416
```
417
418
## Best Practices
419
420
- Implement comprehensive input validation in the `check` method
421
- Handle all error cases gracefully with meaningful error messages
422
- Use appropriate timeouts for external API calls
423
- Implement proper retry logic for transient failures
424
- Cache provider configuration after successful setup
425
- Use structured logging for debugging provider issues
426
- Implement proper cleanup in delete operations
427
- Handle resource state drift in read operations
428
- Use semantic versioning for provider releases
429
- Provide comprehensive documentation and examples
430
- Test providers thoroughly with various input combinations
431
- Implement proper secret handling for sensitive data
432
- Use type-safe interfaces for resource inputs and outputs
433
- Consider implementing preview mode for destructive operations