Terraform
Dynamic Data
Note
Static types should always be preferred over dynamic types, when possible.
Dynamic data handling uses the framework dynamic type to communicate to Terraform that the value type of a specific field will be determined at runtime. This allows a provider developer to handle multiple value types of data with a single attribute, parameter, or return.
Dynamic data can be defined with:
- Dynamic attribute
- A dynamic attribute type in an object attribute
- Dynamic function parameter
- Dynamic function return
- A dynamic attribute type in an object parameter
- A dynamic attribute type in an object return
Using dynamic data has a negative impact on practitioner experience when using Terraform and downstream tooling, like practitioner configuration editor integrations. Dynamics do not change how Terraform's static type system behaves and all data consistency rules are applied the same as static types. Provider developers should understand all the below considerations when creating a provider with a dynamic type.
Only use a dynamic type when there is not a suitable static type alternative.
Considerations
When dynamic data is used, Terraform will no longer have any static information about the value types expected for a given attribute, function parameter, or function return. This results in behaviors that the provider developer will need to account for with additional documentation, code, error messaging, etc.
Downstream Tooling
Practitioner configuration editor integrations, like the Terraform VSCode extension and language server, cannot provide any static information when using dynamic data in configurations. This can result in practitioners using dynamic data in expressions (like for
) incorrectly that will only error at runtime.
Given this example, a resource schema defines a top level computed dynamic attribute named example_attribute
:
func (r ThingResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"example_attribute": schema.DynamicAttribute{
Computed: true,
// ... potentially other fields ...
},
// ... potentially other attributes ...
},
}
}
The configuration below would be valid until a practitioner runs an apply. If the type of example_attribute
is not iterable, then the practitioner will receive an error only when they run a command:
resource "examplecloud_thing" "example" {}
output "dynamic_output" {
value = [for val in examplecloud_thing.example.example_attribute : val]
}
Results in the following error:
│ Error: Iteration over non-iterable value
│
│ on resource.tf line 15, in output "dynamic_output":
│ 15: value = [for val in examplecloud_thing.example.example_attribute : val]
│ ├────────────────
│ │ examplecloud_thing.example.example_attribute is "string value"
│
│ A value of type string cannot be used as the collection in a 'for' expression.
Dynamic data that is meant for practitioners to utilize in configurations should document all potential output types and expected usage to avoid confusing errors.
Handling All Possible Types
Terraform will not automatically convert values to conform to a static type, exposing provider developers to the Terraform type system directly. Provider developers will need to deal with this lack of type conversion by writing logic that handles every possible type that Terraform supports.
In this example, a resource schema defines a top level required dynamic attribute named example_attribute
:
func (r ThingResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"example_attribute": schema.DynamicAttribute{
Required: true,
// ... potentially other fields ...
},
// ... potentially other attributes ...
},
}
}
An example of handling every possible Terraform type that could be provided to a configuration would be:
// Example data model definition
// type ExampleModel struct {
// ExampleAttribute types.Dynamic `tfsdk:"example_attribute"`
// }
switch value := data.ExampleAttribute.UnderlyingValue().(type) {
case types.Bool:
// Handle boolean value
case types.Number:
// Handle float64, int64, and number values
case types.List:
// Handle list value
case types.Map:
// Handle map value
case types.Object:
// Handle object value
case types.Set:
// Handle set value
case types.String:
// Handle string value
case types.Tuple:
// Handle tuple value
}
When writing test configurations and debugging provider issues, developers will also want to understand how Terraform represents complex type literals. For example, Terraform does not provide any way to directly represent lists, maps, or sets.
Handling Underlying Null and Unknown Values
With dynamic data, in addition to typical null and unknown value handling, provider developers will need to implement additional logic to determine if an underlying value for a dynamic is null or unknown.
Underlying Null
In the configuration below, Terraform knows the underlying value type, string
, but the underlying string value is null:
resource "examplecloud_thing" "example" {
example_attribute = var.null_string
}
variable "null_string" {
type = string
default = null
}
This will result in a known dynamic value, with an underlying value that is a null string type. This can be detected utilizing the (types.Dynamic).IsUnderlyingValueNull()
method. An equivalent framework value to this scenario would be:
dynValWithNullString := types.DynamicValue(types.StringNull())
Underlying Unknown
In the configuration below, Terraform knows the underlying value type of random_shuffle.result
, a list(string)
, but the underlying list value is unknown:
resource "random_shuffle" "example" {
input = ["one", "two"]
result_count = 2
}
resource "examplecloud_thing" "this" {
example_attribute = random_shuffle.example.result
}
This will result in a known dynamic value, with an underlying value that is an unknown list of string types. This can be detected utilizing the (types.Dynamic).IsUnderlyingValueUnknown()
method. An equivalent framework value to this scenario would be:
dynValWithUnknownList := types.DynamicValue(types.ListUnknown(types.StringType))
Understanding Type Consistency
For managed resources, Terraform core implements data consistency rules between configuration, plan, and state data. With dynamic attributes, these consistency rules are also applied to the type of data.
For example, given a dynamic example_attribute
that is computed and optional:
func (r ThingResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"example_attribute": schema.DynamicAttribute{
Computed: true,
Optional: true,
// ... potentially other fields ...
},
// ... potentially other attributes ...
},
}
}
If a practitioner configures this resource as:
resource "examplecloud_thing" "example" {
# This literal expression is a tuple[string, string]
example_attribute = ["one", "two"]
}
Then the exact type must be planned and stored in state during apply
as a tuple with two string element types. If provider code attempts to store this attribute as a different type, like a list of strings, even with the same data values, Terraform will produce an error during apply:
│ Error: Provider produced inconsistent result after apply
│
│ When applying changes to examplecloud_thing.example, provider "provider[\"TYPE\"]" produced an unexpected new value: .example_attribute: wrong final value type: tuple required.
│
│ This is a bug in the provider, which should be reported in the providers own issue tracker.
If a practitioner configures this same resource as:
resource "examplecloud_thing" "example" {
example_attribute = tolist(["one", "two"])
}
Then the exact type must be planned and stored in state during apply
as a list of strings.