summaryrefslogtreecommitdiff
path: root/vendor/github.com/hashicorp/terraform/website/guides/writing-custom-terraform-providers.html.md
blob: 9088f159a2edc472cae968555f9bfd7bc71b251b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
---
layout: "guides"
page_title: "Writing Custom Providers - Guides"
sidebar_current: "guides-writing-custom-terraform-providers"
description: |-
  Terraform providers are easy to create and manage. This guide demonstrates
  authoring a Terraform provider from scratch.
---

# Writing Custom Providers

~> **This is an advanced guide!** Following this guide is not required for
regular use of Terraform and is only intended for advance users or Terraform
contributors.

In Terraform, a "provider" is the logical abstraction of an upstream API. This
guide details how to build a custom provider for Terraform.

## Why?

There are a few possible reasons for authoring a custom Terraform provider, such
as:

- An internal private cloud whose functionality is either proprietary or would
  not benefit the open source community.

- A "work in progress" provider being tested locally before contributing back.

- Extensions of an existing provider

## Local Setup

Terraform supports a plugin model, and all providers are actually plugins.
Plugins are distributed as Go binaries. Although technically possible to write a
plugin in another language, almost all Terraform plugins are written in
[Go](https://golang.org). For more information on installing and configuring Go,
please visit the [Golang installation guide](https://golang.org/doc/install).

This post assumes familiarity with Golang and basic programming concepts.

As a reminder, all of Terraform's core providers are open source. When stuck or
looking for examples, please feel free to reference
[the open source providers](https://github.com/hashicorp/terraform/tree/master/builtin/providers) for help.

## The Provider Schema

To start, create a file named `provider.go`. This is the root of the provider
and should include the following boilerplate code:

```go
package main

import (
	"github.com/hashicorp/terraform/helper/schema"
)

func Provider() *schema.Provider {
	return &schema.Provider{
		ResourcesMap: map[string]*schema.Resource{},
	}
}
```

The
[`helper/schema`](https://godoc.org/github.com/hashicorp/terraform/helper/schema)
library is part of Terraform's core. It abstracts many of the complexities and
ensures consistency between providers. The example above defines an empty provider (there are no _resources_).

The `*schema.Provider` type describes the provider's properties including:

- the configuration keys it accepts
- the resources it supports
- any callbacks to configure

## Building the Plugin

Go requires a `main.go` file, which is the default executable when the binary is
built. Since Terraform plugins are distributed as Go binaries, it is important
to define this entry-point with the following code:

```go
package main

import (
	"github.com/hashicorp/terraform/plugin"
	"github.com/hashicorp/terraform/terraform"
)

func main() {
	plugin.Serve(&plugin.ServeOpts{
		ProviderFunc: func() terraform.ResourceProvider {
			return Provider()
		},
	})
}
```

This establishes the main function to produce a valid, executable Go binary. The
contents of the main function consume Terraform's `plugin` library. This library
deals with all the communication between Terraform core and the plugin.

Next, build the plugin using the Go toolchain:

```shell
$ go build -o terraform-provider-example
```

The output name (`-o`) is **very important**. Terraform searches for plugins in
the format of:

```text
terraform-<TYPE>-<NAME>
```

In the case above, the plugin is of type "provider" and of name "example".

To verify things are working correctly, execute the binary just created:

```shell
$ ./terraform-provider-example
This binary is a plugin. These are not meant to be executed directly.
Please execute the program that consumes these plugins, which will
load any plugins automatically
```

This is the basic project structure and scaffolding for a Terraform plugin. To
recap, the file structure is:

```text
.
├── main.go
└── provider.go
```

## Defining Resources

Terraform providers manage resources. A provider is an abstraction of an
upstream API, and a resource is a component of that provider. As an example, the
AWS provider supports `aws_instance` and `aws_elastic_ip`. DNSimple supports
`dnsimple_record`. Fastly supports `fastly_service`. Let's add a resource to our
fictitious provider.

As a general convention, Terraform providers put each resource in their own
file, named after the resource, prefixed with `resource_`. To create an
`example_server`, this would be `resource_server.go` by convention:

```go
package main

import (
	"github.com/hashicorp/terraform/helper/schema"
)

func resourceServer() *schema.Resource {
	return &schema.Resource{
		Create: resourceServerCreate,
		Read:   resourceServerRead,
		Update: resourceServerUpdate,
		Delete: resourceServerDelete,

		Schema: map[string]*schema.Schema{
			"address": &schema.Schema{
				Type:     schema.TypeString,
				Required: true,
			},
		},
	}
}

```

This uses the
[`schema.Resource` type](https://godoc.org/github.com/hashicorp/terraform/helper/schema#Resource).
This structure defines the data schema and CRUD operations for the resource.
Defining these properties are the only required thing to create a resource.

The schema above defines one element, `"address"`, which is a required string.
Terraform's schema automatically enforces validation and type casting.

Next there are four "fields" defined - `Create`, `Read`, `Update`, and `Delete`.
The `Create`, `Read`, and `Delete` functions are required for a resource to be
functional. There are other functions, but these are the only required ones.
Terraform itself handles which function to call and with what data. Based on the
schema and current state of the resource, Terraform can determine whether it
needs to create a new resource, update an existing one, or destroy.

Each of the four struct fields point to a function. While it is technically
possible to inline all functions in the resource schema, best practice dictates
pulling each function into its own method. This optimizes for both testing and
readability. Fill in those stubs now, paying close attention to method
signatures.

```golang
func resourceServerCreate(d *schema.ResourceData, m interface{}) error {
	return nil
}

func resourceServerRead(d *schema.ResourceData, m interface{}) error {
	return nil
}

func resourceServerUpdate(d *schema.ResourceData, m interface{}) error {
	return nil
}

func resourceServerDelete(d *schema.ResourceData, m interface{}) error {
	return nil
}
```

Lastly, update the provider schema in `provider.go` to register this new resource.

```golang
func Provider() *schema.Provider {
	return &schema.Provider{
		ResourcesMap: map[string]*schema.Resource{
			"example_server": resourceServer(),
		},
	}
}
```

Build and test the plugin. Everything should compile as-is, although all
operations are a no-op.

```shell
$ go build -o terraform-provider-example

$ ./terraform-provider-example
This binary is a plugin. These are not meant to be executed directly.
Please execute the program that consumes these plugins, which will
load any plugins automatically
```

The layout now looks like this:

```text
.
├── main.go
├── provider.go
├── resource_server.go
└── terraform-provider-example
```

## Invoking the Provider

Previous sections showed running the provider directly via the shell, which
outputs a warning message like:

```text
This binary is a plugin. These are not meant to be executed directly.
Please execute the program that consumes these plugins, which will
load any plugins automatically
```

Terraform plugins should be executed by Terraform directly. To test this, create
a `main.tf` in the working directory (the same place where the plugin exists).

```hcl
resource "example_server" "my-server" {}
```

And execute `terraform plan`:

```text
$ terraform plan

1 error(s) occurred:

* example_server.my-server: "address": required field is not set
```

This validates Terraform is correctly delegating work to our plugin and that our
validation is working as intended. Fix the validation error by adding an
`address` field to the resource:

```hcl
resource "example_server" "my-server" {
  address = "1.2.3.4"
}
```

Execute `terraform plan` to verify the validation is passing:

```text
$ terraform plan

+ example_server.my-server
    address: "1.2.3.4"


Plan: 1 to add, 0 to change, 0 to destroy.
```

It is possible to run `terraform apply`, but it will be a no-op because all of
the resource options currently take no action.

## Implement Create

Back in `resource_server.go`, implement the create functionality:

```go
func resourceServerCreate(d *schema.ResourceData, m interface{}) error {
	address := d.Get("address").(string)
	d.SetId(address)
	return nil
}
```

This uses the [`schema.ResourceData
API`](https://godoc.org/github.com/hashicorp/terraform/helper/schema#ResourceData)
to get the value of `"address"` provided by the user in the Terraform
configuration. Due to the way Go works, we have to typecast it to string. This
is a safe operation, however, since our schema guarantees it will be a string
type.

Next, it uses `SetId`, a built-in function, to set the ID of the resource to the
address. The existence of a non-blank ID is what tells Terraform that a resource
was created. This ID can be any string value, but should be a value that can be
used to read the resource again.

Recompile the binary, the run `terraform plan` and `terraform apply`.

```shell
$ go build -o terraform-provider-example
# ...
```

```text
$ terraform plan

+ example_server.my-server
    address: "1.2.3.4"


Plan: 1 to add, 0 to change, 0 to destroy.
```

```text
$ terraform apply

example_server.my-server: Creating...
  address: "" => "1.2.3.4"
example_server.my-server: Creation complete (ID: 1.2.3.4)

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
```

Since the `Create` operation used `SetId`, Terraform believes the resource created successfully. Verify this by running `terraform plan`.

```text
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

example_server.my-server: Refreshing state... (ID: 1.2.3.4)
No changes. Infrastructure is up-to-date.

This means that Terraform did not detect any differences between your
configuration and real physical resources that exist. As a result, Terraform
doesn't need to do anything.
```

Again, because of the call to `SetId`, Terraform believes the resource was
created. When running `plan`, Terraform properly determines there are no changes
to apply.

To verify this behavior, change the value of the `address` field and run
`terraform plan` again. You should see output like this:

```text
$ terraform plan
example_server.my-server: Refreshing state... (ID: 1.2.3.4)

~ example_server.my-server
    address: "1.2.3.4" => "5.6.7.8"


Plan: 0 to add, 1 to change, 0 to destroy.
```

Terraform detects the change and displays a diff with a `~` prefix, noting the
resource will be modified in place, rather than created new.

Run `terraform apply` to apply the changes.

```text
$ terraform apply
example_server.my-server: Refreshing state... (ID: 1.2.3.4)
example_server.my-server: Modifying... (ID: 1.2.3.4)
  address: "1.2.3.4" => "5.6.7.8"
example_server.my-server: Modifications complete (ID: 1.2.3.4)

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
```

Since we did not implement the `Update` function, you would expect the
`terraform plan` operation to report changes, but it does not! How were our
changes persisted without the `Update` implementation?

## Error Handling &amp; Partial State

Previously our `Update` operation succeeded and persisted the new state with an
empty function definition. Recall the current update function:

```golang
func resourceServerUpdate(d *schema.ResourceData, m interface{}) error {
	return nil
}
```

The `return nil` tells Terraform that the update operation succeeded without
error. Terraform assumes this means any changes requested applied without error.
Because of this, our state updated and Terraform believes there are no further
changes.

To say it another way: if a callback returns no error, Terraform automatically
assumes the entire diff successfully applied, merges the diff into the final
state, and persists it.

Functions should _never_ intentionally `panic` or call `os.Exit` - always return
an error.

In reality, it is a bit more complicated than this. Imagine the scenario where
our update function has to update two separate fields which require two separate
API calls. What do we do if the first API call succeeds but the second fails?
How do we properly tell Terraform to only persist half the diff? This is known
as a _partial state_ scenario, and implementing these properly is critical to a
well-behaving provider.

Here are the rules for state updating in Terraform. Note that this mentions
callbacks we have not discussed, for the sake of completeness.

- If the `Create` callback returns with or without an error without an ID set
  using `SetId`, the resource is assumed to not be created, and no state is
  saved.

- If the `Create` callback returns with or without an error and an ID has been
  set, the resource is assumed created and all state is saved with it. Repeating
  because it is important: if there is an error, but the ID is set, the state is
  fully saved.

- If the `Update` callback returns with or without an error, the full state is
  saved. If the ID becomes blank, the resource is destroyed (even within an
  update, though this shouldn't happen except in error scenarios).

- If the `Destroy` callback returns without an error, the resource is assumed to
  be destroyed, and all state is removed.

- If the `Destroy` callback returns with an error, the resource is assumed to
  still exist, and all prior state is preserved.

- If partial mode (covered next) is enabled when a create or update returns,
  only the explicitly enabled configuration keys are persisted, resulting in a
  partial state.

_Partial mode_ is a mode that can be enabled by a callback that tells Terraform
that it is possible for partial state to occur. When this mode is enabled, the
provider must explicitly tell Terraform what is safe to persist and what is not.

Here is an example of a partial mode with an update function:

```go
func resourceServerUpdate(d *schema.ResourceData, m interface{}) error {
	// Enable partial state mode
	d.Partial(true)

	if d.HasChange("address") {
		// Try updating the address
		if err := updateAddress(d, m); err != nil {
			return err
		}

		d.SetPartial("address")
	}

	// If we were to return here, before disabling partial mode below,
	// then only the "address" field would be saved.

	// We succeeded, disable partial mode. This causes Terraform to save
	// save all fields again.
	d.Partial(false)

	return nil
}
```

Note - this code will not compile since there is no `updateAddress` function.
You can implement a dummy version of this function to play around with partial
state. For this example, partial state does not mean much in this documentation
example. If `updateAddress` were to fail, then the address field would not be
updated.

## Implementing Destroy

The `Destroy` callback is exactly what it sounds like - it is called to destroy
the resource. This operation should never update any state on the resource. It
is not necessary to call `d.SetId("")`, since any non-error return value assumes
the resource was deleted successfully.

```go
func resourceServerDelete(d *schema.ResourceData, m interface{}) error {
  // d.SetId("") is automatically called assuming delete returns no errors, but
  // it is added here for explicitness.
	d.SetId("")
	return nil
}
```

The destroy function should always handle the case where the resource might
already be destroyed (manually, for example). If the resource is already
destroyed, this should not return an error. This allows Terraform users to
manually delete resources without breaking Terraform.

```shell
$ go build -o terraform-provider-example
```

Run `terraform destroy` to destroy the resource.

```text
$ terraform destroy
Do you really want to destroy?
  Terraform will delete all your managed infrastructure.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

example_server.my-server: Refreshing state... (ID: 5.6.7.8)
example_server.my-server: Destroying... (ID: 5.6.7.8)
example_server.my-server: Destruction complete

Destroy complete! Resources: 1 destroyed.
```

## Implementing Read

The `Read` callback is used to sync the local state with the actual state
(upstream). This is called at various points by Terraform and should be a
read-only operation. This callback should never modify the real resource.

If the ID is updated to blank, this tells Terraform the resource no longer
exists (maybe it was destroyed out of band). Just like the destroy callback, the
`Read` function should gracefully handle this case.

```go
func resourceServerRead(d *schema.ResourceData, m interface{}) error {
  client := m.(*MyClient)

  // Attempt to read from an upstream API
  obj, ok := client.Get(d.Id())

  // If the resource does not exist, inform Terraform. We want to immediately
  // return here to prevent further processing.
  if !ok {
    d.SetId("")
    return nil
  }

  d.Set("address", obj.Address)
  return nil
}
```

## Next Steps

This guide covers the schema and structure for implementing a Terraform provider
using the provider framework. As next steps, reference the internal providers
for examples. Terraform also includes a full framework for testing frameworks.

## General Rules

### Dedicated Upstream Libraries

One of the biggest mistakes new users make is trying to conflate a client
library with the Terraform implementation. Terraform should always consume an
independent client library which implements the core logic for communicating
with the upstream. Do not try to implement this type of logic in the provider
itself.

### Data Sources

While not explicitly discussed here, _data sources_ are a special subset of
resources which are read-only. They are resolved earlier than regular resources
and can be used as part of Terraform's interpolation.