Terraform Optional Variables and Attributes — Using Null and Optional Flag
I am Nimesh Panchal, a highly skilled and experienced professional with expertise in multiple cloud platforms, including AWS, Azure, and GCP. I am also certified in various cloud and virtualisation technologies, such as CKA, AWS, Azure, GPC, VMware, and Nutanix Throughout my career, I have demonstrated a strong passion for cloud computing and have actively contributed to the success of cloud adoption in diverse industries. My hands-on experience in designing, implementing, and managing cloud-based solutions has allowed me to drive operational efficiency, cost optimization, and scalability for businesses. As a cloud enthusiast, I stay updated with the latest advancements and best practices in the cloud domain. My commitment to continuous learning has enabled me to effectively leverage cloud technologies and deliver impactful solutions to complex challenges. Apart from my cloud expertise, I also possess a solid foundation in networking, virtualization, and data center technologies, making me a well-rounded IT professional. I take pride in collaborating with cross-functional teams and providing leadership in cloud migration, infrastructure design, and cloud security initiatives. My problem-solving skills, coupled with my ability to communicate technical concepts to non-technical stakeholders, have been instrumental in fostering seamless collaboration and driving successful cloud projects.

How to make Terraform variables optional in resources, especially for attributes in object variables?
If you use Terraform modules with other configuration automation tools (e.g. Terragrunt or Ansible) to build your infrastructure, you may need to write generic modules which expose all resource arguments as variables with the same or similar argument structure, for flexibility. However, sometimes when you write the generic modules for some complex Terraform resources, there are usually some arguments with special usage or complex object structure. For example, you may not want an optional variable to even appear in your Terraform plan while you need to expose it in the resource codes. This article will cover how to address these requirements in your Terraform modules.
How to make variables optional in resources (not set when no input is given)
In Terraform, there are some variables that you may want to be unset in a resource (not appearing in the plan) when no input values are provided to them. This is because the variables could cause the resource arguments to be triggered with expected values even though you set nonsense default values such as an empty string.
In this case, you can simply use the null value in the variable usage within the resource block.
For example, in the resource below, the variable “app_id” is an optional argument for the resource. By making the corresponding variable optional, you still need to set a default value, such as “”. However, an empty string will still cause the resource to use the app_id to link to an app. To solve the problem, you can simply set the value of the argument to null in an expression:
variable "app_id" {
type = "string"
default = ""
}
resource "null_resource" "example" {
name = "example-resource"
app_id = var.app_id == "" ? null : var.app_id
}
Terraform will treat any argument with a null as unset and nothing for this argument will be taken to the Terraform plan.
How to make object variables for block arguments optional
Some Terraform block arguments are optional in a resource. Consider you have defined an object variable for a block argument with the same attribute structure as below:
variable "example_block" {
type = object({
name = string
enabled = bool
})
default = null
}
If you want to make it optional in the resource, you can use the for_each argument as a switch in a dynamic block:
dynamic "example_block" {
for_each = var.example_block == null ? [] : [1]
content {
name = var.example_block.name
enabled = var.example_block.enabled
}
}
In this example, when no input is given to the “example_block” variable, the dynamic block will have an empty list (length is 0) for the for_each argument and the block will not be populated.
How to make object variable attributes for block arguments optional
We already know how to use null values to make variables optional from the sections above. But what if the attributes or sub-objects need to be optional for a block argument as well? There are two approaches to achieve this:
Using optional flag
Terraform introduced the “optional()” function in version 0.14 as an experimental feature. This feature forces the attribute default values to null, which means they will not be brought to the plan if no corresponding input is given.
By the time this article was published, it had not been added to the mainstream features, but Terraform does keep supporting and maintaining this feature in all other versions since 0.14. And because using the optional flag is a better approach to building neat variable codes, it is more recommended. If you do not expect to use any experimental feature, please skip this approach and proceed to the next one!
As this is experimental, you first need to declare an “experiments” tag in the terraform block:
terraform {
experiments = [module_variable_optional_attrs]
}
Then, in your variable definition, use the “optional()” in the attribute definition. Taking the “ip_restriction” argument from Terraform’s “azurerm_linux_web_app” resource as an example, the variable can be defined as below:
variable "ip_restriction" {
type = list(object({
action = optional(string)
ip_address = optional(string)
name = optional(string)
priority = optional(number)
service_tag = optional(string)
virtual_network_subnet_id = optional(string)
headers = optional(object({
x_azure_fdid = optional(list(string))
x_fd_health_probe = optional(bool)
x_forwarded_for = optional(list(string))
x_forwarded_host = optional(list(string))
}))
}))
default = []
}
Since in the backstage Terraform assigns a null value to those attributes and sub-objects, you can simply use the “can()” function to check if the optional attributes are set, and use null to compare with the sub-objects, as below:
dynamic "ip_restriction" {
for_each = var.ip_restriction
content {
action = can(ip_restriction.value["action"]) ? ip_restriction.value["action"] : null
ip_address = can(ip_restriction.value["ip_address"]) ? ip_restriction.value["ip_address"] : null
name = can(ip_restriction.value["name"]) ? ip_restriction.value["name"] : null
priority = can(ip_restriction.value["priority"]) ? ip_restriction.value["priority"] : null
service_tag = can(ip_restriction.value["service_tag"]) ? ip_restriction.value["service_tag"] : null
virtual_network_subnet_id = can(ip_restriction.value["virtual_network_subnet_id"]) ? ip_restriction.value["virtual_network_subnet_id"] : null
dynamic "headers" {
for_each = ip_restriction.value["headers"] == null ? [] : [1]
content {
x_azure_fdid = can(ip_restriction.value["headers"].x_azure_fdid) ? ip_restriction.value["headers"].x_azure_fdid : null
x_fd_health_probe = can(ip_restriction.value["headers"].x_fd_health_probe) ? ip_restriction.value["headers"].x_fd_health_probe : null
x_forwarded_for = can(ip_restriction.value["headers"].x_forwarded_for) ? ip_restriction.value["headers"].x_forwarded_for : null
x_forwarded_host = can(ip_restriction.value["headers"].x_forwarded_host) ? ip_restriction.value["headers"].x_forwarded_host : null
}
}
}
}
For more information about this feature, please refer to the documentation.
Decouple the attributes/sub-object from the object variable
An alternative to using the optional feature is to decouple the attributes or sub-objects from the complex object variable and make them individual variables. In this way, you can use the approach from the first section to make them optional in a block. Taking the same example from above, for the “action” attribute, you can simply define an individual variable as below:
variable "ip_restriction_action" {
type = string
default = ""
}. . .
dynamic "ip_restriction" {
for_each = var.ip_restriction
content {
action = var.ip_restriction_action == "" ? null : var.ip_restriction_action
}
. . .
}
The described method is a traditional and conservative approach that allows for full exposure of all attributes. However, it comes with the downside of requiring the definition of numerous variables, potentially leading to messy variable codes. Nonetheless, if your goal is to prioritize stability by avoiding experimental features or ensuring compatibility with older Terraform SDKs or versions, then this approach remains a suitable choice.
