github.com/magodo/terraform@v0.11.12-beta1/website/guides/writing-custom-terraform-providers.html.md (about) 1 --- 2 layout: "guides" 3 page_title: "Writing Custom Providers - Guides" 4 sidebar_current: "guides-writing-custom-terraform-providers" 5 description: |- 6 Terraform providers are easy to create and manage. This guide demonstrates 7 authoring a Terraform provider from scratch. 8 --- 9 10 # Writing Custom Providers 11 12 ~> **This is an advanced guide!** Following this guide is not required for 13 regular use of Terraform and is only intended for advance users or Terraform 14 contributors. 15 16 In Terraform, a "provider" is the logical abstraction of an upstream API. This 17 guide details how to build a custom provider for Terraform. 18 19 ## Why? 20 21 There are a few possible reasons for authoring a custom Terraform provider, such 22 as: 23 24 - An internal private cloud whose functionality is either proprietary or would 25 not benefit the open source community. 26 27 - A "work in progress" provider being tested locally before contributing back. 28 29 - Extensions of an existing provider 30 31 ## Local Setup 32 33 Terraform supports a plugin model, and all providers are actually plugins. 34 Plugins are distributed as Go binaries. Although technically possible to write a 35 plugin in another language, almost all Terraform plugins are written in 36 [Go](https://golang.org). For more information on installing and configuring Go, 37 please visit the [Golang installation guide](https://golang.org/doc/install). 38 39 This post assumes familiarity with Golang and basic programming concepts. 40 41 As a reminder, all of Terraform's core providers are open source. When stuck or 42 looking for examples, please feel free to reference 43 [the open source providers](https://github.com/terraform-providers) for help. 44 45 ## The Provider Schema 46 47 To start, create a file named `provider.go`. This is the root of the provider 48 and should include the following boilerplate code: 49 50 ```go 51 package main 52 53 import ( 54 "github.com/hashicorp/terraform/helper/schema" 55 ) 56 57 func Provider() *schema.Provider { 58 return &schema.Provider{ 59 ResourcesMap: map[string]*schema.Resource{}, 60 } 61 } 62 ``` 63 64 The 65 [`helper/schema`](https://godoc.org/github.com/hashicorp/terraform/helper/schema) 66 library is part of Terraform's core. It abstracts many of the complexities and 67 ensures consistency between providers. The example above defines an empty provider (there are no _resources_). 68 69 The `*schema.Provider` type describes the provider's properties including: 70 71 - the configuration keys it accepts 72 - the resources it supports 73 - any callbacks to configure 74 75 ## Building the Plugin 76 77 Go requires a `main.go` file, which is the default executable when the binary is 78 built. Since Terraform plugins are distributed as Go binaries, it is important 79 to define this entry-point with the following code: 80 81 ```go 82 package main 83 84 import ( 85 "github.com/hashicorp/terraform/plugin" 86 "github.com/hashicorp/terraform/terraform" 87 ) 88 89 func main() { 90 plugin.Serve(&plugin.ServeOpts{ 91 ProviderFunc: func() terraform.ResourceProvider { 92 return Provider() 93 }, 94 }) 95 } 96 ``` 97 98 This establishes the main function to produce a valid, executable Go binary. The 99 contents of the main function consume Terraform's `plugin` library. This library 100 deals with all the communication between Terraform core and the plugin. 101 102 Next, build the plugin using the Go toolchain: 103 104 ```shell 105 $ go build -o terraform-provider-example 106 ``` 107 108 The output name (`-o`) is **very important**. Terraform searches for plugins in 109 the format of: 110 111 ```text 112 terraform-<TYPE>-<NAME> 113 ``` 114 115 In the case above, the plugin is of type "provider" and of name "example". 116 117 To verify things are working correctly, execute the binary just created: 118 119 ```shell 120 $ ./terraform-provider-example 121 This binary is a plugin. These are not meant to be executed directly. 122 Please execute the program that consumes these plugins, which will 123 load any plugins automatically 124 ``` 125 126 This is the basic project structure and scaffolding for a Terraform plugin. To 127 recap, the file structure is: 128 129 ```text 130 . 131 ├── main.go 132 └── provider.go 133 ``` 134 135 ## Defining Resources 136 137 Terraform providers manage resources. A provider is an abstraction of an 138 upstream API, and a resource is a component of that provider. As an example, the 139 AWS provider supports `aws_instance` and `aws_elastic_ip`. DNSimple supports 140 `dnsimple_record`. Fastly supports `fastly_service`. Let's add a resource to our 141 fictitious provider. 142 143 As a general convention, Terraform providers put each resource in their own 144 file, named after the resource, prefixed with `resource_`. To create an 145 `example_server`, this would be `resource_server.go` by convention: 146 147 ```go 148 package main 149 150 import ( 151 "github.com/hashicorp/terraform/helper/schema" 152 ) 153 154 func resourceServer() *schema.Resource { 155 return &schema.Resource{ 156 Create: resourceServerCreate, 157 Read: resourceServerRead, 158 Update: resourceServerUpdate, 159 Delete: resourceServerDelete, 160 161 Schema: map[string]*schema.Schema{ 162 "address": &schema.Schema{ 163 Type: schema.TypeString, 164 Required: true, 165 }, 166 }, 167 } 168 } 169 170 ``` 171 172 This uses the 173 [`schema.Resource` type](https://godoc.org/github.com/hashicorp/terraform/helper/schema#Resource). 174 This structure defines the data schema and CRUD operations for the resource. 175 Defining these properties are the only required thing to create a resource. 176 177 The schema above defines one element, `"address"`, which is a required string. 178 Terraform's schema automatically enforces validation and type casting. 179 180 Next there are four "fields" defined - `Create`, `Read`, `Update`, and `Delete`. 181 The `Create`, `Read`, and `Delete` functions are required for a resource to be 182 functional. There are other functions, but these are the only required ones. 183 Terraform itself handles which function to call and with what data. Based on the 184 schema and current state of the resource, Terraform can determine whether it 185 needs to create a new resource, update an existing one, or destroy. 186 187 Each of the four struct fields point to a function. While it is technically 188 possible to inline all functions in the resource schema, best practice dictates 189 pulling each function into its own method. This optimizes for both testing and 190 readability. Fill in those stubs now, paying close attention to method 191 signatures. 192 193 ```golang 194 func resourceServerCreate(d *schema.ResourceData, m interface{}) error { 195 return nil 196 } 197 198 func resourceServerRead(d *schema.ResourceData, m interface{}) error { 199 return nil 200 } 201 202 func resourceServerUpdate(d *schema.ResourceData, m interface{}) error { 203 return nil 204 } 205 206 func resourceServerDelete(d *schema.ResourceData, m interface{}) error { 207 return nil 208 } 209 ``` 210 211 Lastly, update the provider schema in `provider.go` to register this new resource. 212 213 ```golang 214 func Provider() *schema.Provider { 215 return &schema.Provider{ 216 ResourcesMap: map[string]*schema.Resource{ 217 "example_server": resourceServer(), 218 }, 219 } 220 } 221 ``` 222 223 Build and test the plugin. Everything should compile as-is, although all 224 operations are a no-op. 225 226 ```shell 227 $ go build -o terraform-provider-example 228 229 $ ./terraform-provider-example 230 This binary is a plugin. These are not meant to be executed directly. 231 Please execute the program that consumes these plugins, which will 232 load any plugins automatically 233 ``` 234 235 The layout now looks like this: 236 237 ```text 238 . 239 ├── main.go 240 ├── provider.go 241 ├── resource_server.go 242 └── terraform-provider-example 243 ``` 244 245 ## Invoking the Provider 246 247 Previous sections showed running the provider directly via the shell, which 248 outputs a warning message like: 249 250 ```text 251 This binary is a plugin. These are not meant to be executed directly. 252 Please execute the program that consumes these plugins, which will 253 load any plugins automatically 254 ``` 255 256 Terraform plugins should be executed by Terraform directly. To test this, create 257 a `main.tf` in the working directory (the same place where the plugin exists). 258 259 ```hcl 260 resource "example_server" "my-server" {} 261 ``` 262 263 And execute `terraform plan`: 264 265 ```text 266 $ terraform plan 267 268 1 error(s) occurred: 269 270 * example_server.my-server: "address": required field is not set 271 ``` 272 273 This validates Terraform is correctly delegating work to our plugin and that our 274 validation is working as intended. Fix the validation error by adding an 275 `address` field to the resource: 276 277 ```hcl 278 resource "example_server" "my-server" { 279 address = "1.2.3.4" 280 } 281 ``` 282 283 Execute `terraform plan` to verify the validation is passing: 284 285 ```text 286 $ terraform plan 287 288 + example_server.my-server 289 address: "1.2.3.4" 290 291 292 Plan: 1 to add, 0 to change, 0 to destroy. 293 ``` 294 295 It is possible to run `terraform apply`, but it will be a no-op because all of 296 the resource options currently take no action. 297 298 ## Implement Create 299 300 Back in `resource_server.go`, implement the create functionality: 301 302 ```go 303 func resourceServerCreate(d *schema.ResourceData, m interface{}) error { 304 address := d.Get("address").(string) 305 d.SetId(address) 306 return nil 307 } 308 ``` 309 310 This uses the [`schema.ResourceData 311 API`](https://godoc.org/github.com/hashicorp/terraform/helper/schema#ResourceData) 312 to get the value of `"address"` provided by the user in the Terraform 313 configuration. Due to the way Go works, we have to typecast it to string. This 314 is a safe operation, however, since our schema guarantees it will be a string 315 type. 316 317 Next, it uses `SetId`, a built-in function, to set the ID of the resource to the 318 address. The existence of a non-blank ID is what tells Terraform that a resource 319 was created. This ID can be any string value, but should be a value that can be 320 used to read the resource again. 321 322 Recompile the binary, the run `terraform plan` and `terraform apply`. 323 324 ```shell 325 $ go build -o terraform-provider-example 326 # ... 327 ``` 328 329 ```text 330 $ terraform plan 331 332 + example_server.my-server 333 address: "1.2.3.4" 334 335 336 Plan: 1 to add, 0 to change, 0 to destroy. 337 ``` 338 339 ```text 340 $ terraform apply 341 342 example_server.my-server: Creating... 343 address: "" => "1.2.3.4" 344 example_server.my-server: Creation complete (ID: 1.2.3.4) 345 346 Apply complete! Resources: 1 added, 0 changed, 0 destroyed. 347 ``` 348 349 Since the `Create` operation used `SetId`, Terraform believes the resource created successfully. Verify this by running `terraform plan`. 350 351 ```text 352 $ terraform plan 353 Refreshing Terraform state in-memory prior to plan... 354 The refreshed state will be used to calculate this plan, but will not be 355 persisted to local or remote state storage. 356 357 example_server.my-server: Refreshing state... (ID: 1.2.3.4) 358 No changes. Infrastructure is up-to-date. 359 360 This means that Terraform did not detect any differences between your 361 configuration and real physical resources that exist. As a result, Terraform 362 doesn't need to do anything. 363 ``` 364 365 Again, because of the call to `SetId`, Terraform believes the resource was 366 created. When running `plan`, Terraform properly determines there are no changes 367 to apply. 368 369 To verify this behavior, change the value of the `address` field and run 370 `terraform plan` again. You should see output like this: 371 372 ```text 373 $ terraform plan 374 example_server.my-server: Refreshing state... (ID: 1.2.3.4) 375 376 ~ example_server.my-server 377 address: "1.2.3.4" => "5.6.7.8" 378 379 380 Plan: 0 to add, 1 to change, 0 to destroy. 381 ``` 382 383 Terraform detects the change and displays a diff with a `~` prefix, noting the 384 resource will be modified in place, rather than created new. 385 386 Run `terraform apply` to apply the changes. 387 388 ```text 389 $ terraform apply 390 example_server.my-server: Refreshing state... (ID: 1.2.3.4) 391 example_server.my-server: Modifying... (ID: 1.2.3.4) 392 address: "1.2.3.4" => "5.6.7.8" 393 example_server.my-server: Modifications complete (ID: 1.2.3.4) 394 395 Apply complete! Resources: 0 added, 1 changed, 0 destroyed. 396 ``` 397 398 Since we did not implement the `Update` function, you would expect the 399 `terraform plan` operation to report changes, but it does not! How were our 400 changes persisted without the `Update` implementation? 401 402 ## Error Handling & Partial State 403 404 Previously our `Update` operation succeeded and persisted the new state with an 405 empty function definition. Recall the current update function: 406 407 ```golang 408 func resourceServerUpdate(d *schema.ResourceData, m interface{}) error { 409 return nil 410 } 411 ``` 412 413 The `return nil` tells Terraform that the update operation succeeded without 414 error. Terraform assumes this means any changes requested applied without error. 415 Because of this, our state updated and Terraform believes there are no further 416 changes. 417 418 To say it another way: if a callback returns no error, Terraform automatically 419 assumes the entire diff successfully applied, merges the diff into the final 420 state, and persists it. 421 422 Functions should _never_ intentionally `panic` or call `os.Exit` - always return 423 an error. 424 425 In reality, it is a bit more complicated than this. Imagine the scenario where 426 our update function has to update two separate fields which require two separate 427 API calls. What do we do if the first API call succeeds but the second fails? 428 How do we properly tell Terraform to only persist half the diff? This is known 429 as a _partial state_ scenario, and implementing these properly is critical to a 430 well-behaving provider. 431 432 Here are the rules for state updating in Terraform. Note that this mentions 433 callbacks we have not discussed, for the sake of completeness. 434 435 - If the `Create` callback returns with or without an error without an ID set 436 using `SetId`, the resource is assumed to not be created, and no state is 437 saved. 438 439 - If the `Create` callback returns with or without an error and an ID has been 440 set, the resource is assumed created and all state is saved with it. Repeating 441 because it is important: if there is an error, but the ID is set, the state is 442 fully saved. 443 444 - If the `Update` callback returns with or without an error, the full state is 445 saved. If the ID becomes blank, the resource is destroyed (even within an 446 update, though this shouldn't happen except in error scenarios). 447 448 - If the `Destroy` callback returns without an error, the resource is assumed to 449 be destroyed, and all state is removed. 450 451 - If the `Destroy` callback returns with an error, the resource is assumed to 452 still exist, and all prior state is preserved. 453 454 - If partial mode (covered next) is enabled when a create or update returns, 455 only the explicitly enabled configuration keys are persisted, resulting in a 456 partial state. 457 458 _Partial mode_ is a mode that can be enabled by a callback that tells Terraform 459 that it is possible for partial state to occur. When this mode is enabled, the 460 provider must explicitly tell Terraform what is safe to persist and what is not. 461 462 Here is an example of a partial mode with an update function: 463 464 ```go 465 func resourceServerUpdate(d *schema.ResourceData, m interface{}) error { 466 // Enable partial state mode 467 d.Partial(true) 468 469 if d.HasChange("address") { 470 // Try updating the address 471 if err := updateAddress(d, m); err != nil { 472 return err 473 } 474 475 d.SetPartial("address") 476 } 477 478 // If we were to return here, before disabling partial mode below, 479 // then only the "address" field would be saved. 480 481 // We succeeded, disable partial mode. This causes Terraform to save 482 // all fields again. 483 d.Partial(false) 484 485 return nil 486 } 487 ``` 488 489 Note - this code will not compile since there is no `updateAddress` function. 490 You can implement a dummy version of this function to play around with partial 491 state. For this example, partial state does not mean much in this documentation 492 example. If `updateAddress` were to fail, then the address field would not be 493 updated. 494 495 ## Implementing Destroy 496 497 The `Destroy` callback is exactly what it sounds like - it is called to destroy 498 the resource. This operation should never update any state on the resource. It 499 is not necessary to call `d.SetId("")`, since any non-error return value assumes 500 the resource was deleted successfully. 501 502 ```go 503 func resourceServerDelete(d *schema.ResourceData, m interface{}) error { 504 // d.SetId("") is automatically called assuming delete returns no errors, but 505 // it is added here for explicitness. 506 d.SetId("") 507 return nil 508 } 509 ``` 510 511 The destroy function should always handle the case where the resource might 512 already be destroyed (manually, for example). If the resource is already 513 destroyed, this should not return an error. This allows Terraform users to 514 manually delete resources without breaking Terraform. 515 516 ```shell 517 $ go build -o terraform-provider-example 518 ``` 519 520 Run `terraform destroy` to destroy the resource. 521 522 ```text 523 $ terraform destroy 524 Do you really want to destroy? 525 Terraform will delete all your managed infrastructure. 526 There is no undo. Only 'yes' will be accepted to confirm. 527 528 Enter a value: yes 529 530 example_server.my-server: Refreshing state... (ID: 5.6.7.8) 531 example_server.my-server: Destroying... (ID: 5.6.7.8) 532 example_server.my-server: Destruction complete 533 534 Destroy complete! Resources: 1 destroyed. 535 ``` 536 537 ## Implementing Read 538 539 The `Read` callback is used to sync the local state with the actual state 540 (upstream). This is called at various points by Terraform and should be a 541 read-only operation. This callback should never modify the real resource. 542 543 If the ID is updated to blank, this tells Terraform the resource no longer 544 exists (maybe it was destroyed out of band). Just like the destroy callback, the 545 `Read` function should gracefully handle this case. 546 547 ```go 548 func resourceServerRead(d *schema.ResourceData, m interface{}) error { 549 client := m.(*MyClient) 550 551 // Attempt to read from an upstream API 552 obj, ok := client.Get(d.Id()) 553 554 // If the resource does not exist, inform Terraform. We want to immediately 555 // return here to prevent further processing. 556 if !ok { 557 d.SetId("") 558 return nil 559 } 560 561 d.Set("address", obj.Address) 562 return nil 563 } 564 ``` 565 566 ## Next Steps 567 568 This guide covers the schema and structure for implementing a Terraform provider 569 using the provider framework. As next steps, reference the internal providers 570 for examples. Terraform also includes a full framework for testing providers. 571 572 ## General Rules 573 574 ### Dedicated Upstream Libraries 575 576 One of the biggest mistakes new users make is trying to conflate a client 577 library with the Terraform implementation. Terraform should always consume an 578 independent client library which implements the core logic for communicating 579 with the upstream. Do not try to implement this type of logic in the provider 580 itself. 581 582 ### Data Sources 583 584 While not explicitly discussed here, _data sources_ are a special subset of 585 resources which are read-only. They are resolved earlier than regular resources 586 and can be used as part of Terraform's interpolation.