The Terraform Plan
In this post
I recently started working on Terraform Provider plugins, but there doesn’t seem to be much information about plugin-development out there on the internet. So I decided to collect some of my experience in a couple of posts.
When applying a Terraform configuration,
-
Terraform starts with a refresh of its state-file, reading the information about its resources from the infrastructure, and updating its state-file.
-
It will look for differences between its state-file and its configuration-files, to make a plan.
-
It will apply the plan by creating, updating or deleting resources,
-
Finally, it will refresh its state-file with the information from the updated infrastructure.
The Terraform plan is calculated based on a function called Diff
, which calculates the differences between the state of an attribute - Terraform’s view on the infrastructure - and the configuration of this attribute in the Terraform configuration-files (*.tf
). In this post, I am going to discuss the relation between the outcome of the Diff
-function, and what you can see in a Terraform plan.
Remark that the result from aDiff
can be customized by a Terraform Provider implementation, changing the general rules for an attribute. I may discuss this in a future post.
It is important that the plan is representing the result from applying that plan as close as possible. When implementing a provider and when the plan doesn’t correspond to the result for an attribute, either the plan must be customized, or the implementation must be changed. For instance, if a Computed
value changes when updating a resource, the plan needs to be customized, because the default Terraform plan assumes the value will not change.
A good place to find issues is the Terraform log file, look for The following problems may be the cause of any confusing errors from downstream operations:
. It is good practise to reduce the messages under this heading to the minimum.
A lot of the issues mentioned under this heading can be resolved, however some cannot. For instance, when not specifying a value for an Optional
attribute with a Default
, you will get a message like - .my_attribute: planned value cty.StringVal("default") does not match config value cty.NullVal(cty.String)
.
Attributes 🔗
The Terraform plan depends on how attributes are defined in the resource schema:
attribute | Required | Optional | Computed | |
---|---|---|---|---|
- not in tf state | ||||
– not in config | 1 | <error> | null | + (known after apply) |
– in config | 2 | + config |
+ config |
<error> |
- in tf state | ||||
– not in tf config | 3 | <error> | - state -> null |
state |
– in tf config | ||||
> config == state | 4a | state |
state |
<error> |
> config != state | 4b | ~ state -> config |
~ state -> config |
<error> |
A Required
attribute has to be specified in a Terraform configuration. When not specifying it - see line 1 or 3 - Terraform will throw an error. When specifying it - see line 2, 4a or 4b - Terraform plans to create it or replace its value in the state with the value of the configuration.
An Optional
attribute doesn’t have to be specified in a Terraform configuration. When not specifying it - see line 1 or 3 - Terraform plans to remove it or set its value to null
in the state.
A Computed
attribute must not be specified in a Terraform configuration. When specifying it - see line 2, 4a or 4b - Terraform will throw an error. When not specifying it - see line 1 - its value will be known after the plan is applied. However, when this attribute already exists in the Terraform state - see line 3 - Terraform assumes its value will not change. The Terraform Provider implementation can and should customize this when it expects this attribute’s value will change after apply.
To avoid an Optional
attribute gets removed, we can make the attribute Optional
& Default
attribute | Optional | Optional & Default |
|
---|---|---|---|
- not in tf state | |||
– not in tf config | 1 | null | + default |
– in tf config | 2 | + config |
+ config |
- in tf state | |||
– not in tf config | 3 | - state -> null |
|
> default == state | 3a | state |
|
> default != state | 3b | ~ state -> default |
|
– in tf config | |||
> config == state | 4a | state |
state |
> config != state | 4b | ~ state -> config |
~ state -> config |
Or we can make the attribute Optional
& Computed
.
attribute | Optional | Optional & Computed |
Computed | |
---|---|---|---|---|
- not in tf state | ||||
– not in tf config | 1 | null | + (known after apply) |
+ (known after apply) |
– in tf config | 2 | + config |
+ config |
<error> |
- in tf state | ||||
– not in tf config | 3 | - state -> null |
state |
state |
– in tf config | ||||
> config == state | 4a | state |
state |
<error> |
> config != state | 4b | ~ state -> config |
~ state -> config |
<error> |
Similar to Optional
attributes, the value of an Optional
& Computed
attribute that is not configured will be known after the plan is applied - see line 1. However, when this attribute already exists in the Terraform state - see line 3 - Terraform assumes its value will not change. The Terraform Provider implementation can and should customize this when it expects this attribute’s value will change after apply.
Embedded Resources 🔗
Embedded resources behave in much the same way as attributes.
resource | Required | Optional | |
---|---|---|---|
- not in state | |||
– not in config | 1 | <error> | null |
– in config | 2 | + { + config… } |
+ { + config… } |
- in state | |||
– not in config | 3 | <error> | - { - state -> null } |
– in config | |||
> config == state | 4a | { state } |
{ state } |
> config != state | 4b | ~ { ? state -> config… } |
~ { ? state -> config… } |
Remark that changes to a resource - see
?
in line 4b - can be+
,-
,or
~
, depending on the resource’s attributes.
resource attribute | Required | Optional | Computed | |
---|---|---|---|---|
- not in tf state | ||||
– not in tf config | 1 | <error> | null | + (known after apply) |
– in tf config | 2 | + config |
+ config |
<error> |
- in tf state | ||||
– not in tf config | 3 | <error> | - state -> null |
state |
– in tf config | ||||
> config == state | 4a | state |
state |
<error> |
> config != state | 4b | ~ state -> config |
~ state -> config |
<error> |
Required
and Optional
resources are presented as blocks in the Terraform plan - this is called “block” config-mode. When the resource isn’t in the Terraform state or when resource config is not equal to the resource state - see line 2 or 4b - the plan depends on the configuration of the resource’s attributes. The resource’s attributes behave in the same way as top-level attributes.
resource | Computed | |
---|---|---|
- not in state | ||
– not in config | 1 | + (known after apply) |
– in config | 2 | <error> |
- in state | ||
– not in config | 3 | [ { state } ] |
– in config | ||
> config == state | 4a | <error> |
> config != state | 4b | <error> |
Computed
resources are presented as a list of resource-blocks in the Terraform plan - this is called “attribute” config-mode. The whole list of resource-blocks behaves like a single top-level attribute.
Remark that the latest Terraform versions don’t officially support embeddedComputed
resources. Although this does work without problems, it is better to use a schema with a list ofTypeMap
-elements instead of a list of resource-elements.
Remark also that it doesn’t make sense to haveRequired
orOptional
resource attributes in aComputed
resource, all attributes will behave likeComputed
attributes - the resource-block is basically “demoted” to a simple map-object (TypeMap
).However, specifying
Required
orOptional
resource attributes does make sense forOptional
&Computed
resources, as we will discuss below.
Terraform doesn’t support embedded Optional
& Default
resources yet. There is an issue with enhancement label for this.
To avoid an Optional
resource gets removed, we can make the resource Optional
& Computed
.
resource | Optional | Optional & Computed |
|
---|---|---|---|
- not in tf state | |||
– not in tf config | 1 | null | + { + (known after apply) } |
– in tf config | 2 | + { + config… } |
+ { + config… } |
- in tf state | |||
– not in tf config | 3 | - { - state -> null } |
{ state } |
– in tf config | |||
> config == state | 4a | { state } |
{ state } |
> config != state | 4b | ~ { ? state -> config… } |
~ { ? state -> config… } |
By default, an Optional
& Computed
resource uses “block” config-mode, as shown above. However, this can be changed to “attribute” config-mode by setting ConfigMode: schema.SchemaConfigModeAttr,
in the resource’s schema. This will also slightly change the behaviour of Optional
resource attributes.
resource | Optional & Computed using block config mode |
Optional & Computed using attribute config mode |
|
---|---|---|---|
- not in tf state | |||
– not in tf config | 1 | + { + (known after apply) } |
+ (known after apply) |
– in tf config | 2 | + { + config… } |
+ [ + { + config… } ] |
- in tf state | |||
– not in tf config | 3 | { state } |
[ { state } ] |
– in tf config | |||
> config == state | 4a | { state } |
[ { state } ] |
> config != state | 4b | ~ { ? state -> config… } |
~ [ ? { ? state -> config… } ] |
resource attribute | Optional using block config mode |
Optional using attribute config mode |
|
---|---|---|---|
- not in tf state | |||
– not in tf config | 1 | null | |
» block == null | 1a | null | |
» block != null | 1b | + null |
|
– in tf config | 2 | + config |
+ config |
- in terraform state | |||
– not in tf config | 3 | - state -> null |
|
» block == null | 3a | - state -> null |
|
» block != null | 3b | state |
|
– in tf config | |||
> config == state | 4a | state |
state |
> config != state | 4b | ~ state -> config |
~ state -> config |
Remark that “
+
null” in this table means that the plan will present the zeroed value for the attribute. For a string this is""
, for an integer this is0
, for a boolean this isfalse
and for an aggregate this isnull
.
Remark also that for line 3a, Terraform currently reports “
-
state” instead of “-
state -> null”, but this must be a bug since this is not in line with what is usually reported when destroying resources.
This choice of config mode is particularly important when configuring embedded_resource = []
. It allows to make a distinction between absence of resources and an empty list of resources.
-
When setting
ConfigMode: schema.SchemaConfigModeBlock,
(default) in the resource’s schema, by defaultembedded_resource = []
cannot be used for embedded resources. Terraform will throw an error. -
When setting
ConfigMode: schema.SchemaConfigModeAttr,
in the resource’s schema, and when changing the config toembedded_resource = []
, Terraform will create an empty list of resource-blocks or change to an empty list of resource-blocks - see lines 2 & 1a or 4b & 3a.
Notes On Config Mode 🔗
From the schema documentation / "ConfigMode SchemaConfigMode"
:
ConfigMode allows for overriding the default behaviors for mapping schema entries onto configuration constructs.
By default, the
Elem
field is used to choose whether a particular schema is represented in configuration as an attribute or as a nested block - an embedded resource. IfElem
is a*schema.Resource
then it’s a block and it’s an attribute otherwise.If
Elem
is a*schema.Resource
then settingConfigMode
toSchemaConfigModeAttr
will force it to be represented in configuration as an attribute, which means that theComputed
flag can be used to provide default [edit: computed] elements when the argument isn’t set at all, while still allowing the user to force zero elements by explicitly assigning an empty list.When
Computed
is set withoutOptional
, the attribute is not settable in configuration at all and soSchemaConfigModeAttr
is the automatic behavior, andSchemaConfigModeBlock
is not permitted.
Some more info:
Trying It Out 🔗
I prepared a small package for a provider, in case you want to play with this. For embedded resources, you may want to change the schema-options in /abc/resource_abc_xyz.go
To build the provider:
-
Create a repository, for instance called
terraform-provider-abc
-
Download the content from the
terraform-provider-abc
in theabc
package into your repository -
Assuming you have
go
installed and properly configured,
in theterraform-provider-abc
directory,- run
go mod tidy
- run
go build -o "$env:APPDATA\terraform.d\plugins"
(on Windows using Powershell)
orgo build -o "%APPDATA%\terraform.d\plugins"
(on Windows using CMD)
orgo build -o ~/.terraform.d/plugins
(on Linux)
- run
To run the provider:
-
Assuming you have
terraform
installed and properly configured,
in theterraform-provider-abc/examples
directory,- run
terraform init
- run
terraform plan
- run
terraform apply
- run
terraform destroy
- run
Leave a comment