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 &amp; 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.