or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

asset-management.mdautomation.mdconfiguration.mddynamic-resources.mdindex.mdlogging-diagnostics.mdoutput-system.mdprovider-development.mdresource-management.mdruntime-operations.mdstack-references.mdutilities.md

provider-development.mddocs/

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