Terraform

Terraform Optional Variables: optional(), Nullable, Default

How to Use Optional Arguments in Terraform Variables

Terraform’s variable block supports a set of optional arguments that shape how a value is typed, defaulted, validated, and exposed to module callers: type, default, description, validation, sensitive, nullable, ephemeral, const, and deprecated. On top of that, Terraform provides a separate optional() function for marking individual attributes inside an object type constraint as optional.

In this article, we will take a look at Terraform variables and the optional arguments they use. We will explain what they are, how to reference them, why they are useful, and how to use type constraints to enhance the quality of your code.

We will cover:

  1. What are optional arguments for variable declarations?
  2. How to define an optional variable in Terraform?
  3. How to use optional arguments?
  4. How to use the Terraform optional()?
  5. Optional argument usage example
  6. Type constraints

What are the optional arguments for variable declarations in Terraform?

Optional arguments in Terraform can be used when specifying variables to define their behavior and characteristics. These arguments allow you to control how the variables are used and provide defaults, constraints, and metadata or omit certain settings. They enhance the quality of your code and make your infrastructure code more adaptable to different environments without requiring changes to the core logic.

How to define an optional variable in Terraform?

When you define a variable in Terraform, it can simply be defined as:

variable "example_var" {
}

The most commonly used optional arguments for variable declarations in Terraform include:

  • type
  • default
  • description
  • validation
  • sensitive
  • ephemeral
  • const
  • deprecated
  • nullable

An example variable declaration could look like this with all the optional parameters included:

variable "example_var" {
  type        = string
  default     = "chewbacca"
  description = "8 foot tall wookie"
  sensitive   = false
  nullable    = false

  validation {
    condition     = var.example_var == "chewbacca"
    error_message = "Value must be a wookie."
  }
}

Why are the optional parameters useful?

Depending on your use case, the main benefits of using optional parameters or combinations of the optional parameters in your variables are adaptability and code quality.  They are used to further define variables’ behavior and characteristics, making your variables more structured and controlled and your Terraform configurations more robust and user-friendly. 

Read more about Terraform variables.

How to use optional arguments?

Let’s look at each type of Terraform optional variable values.

1. type

The type argument specifies the expected data type of the variable to help Terraform validate that the value provided for the variable matches the expected type.

variable "example_var" {
  type = string
}

2. default

The default argument provides the default value for the variable. If no value is explicitly passed when applying the configuration, Terraform will use the default value.

variable "example_var" {
  type    = string
  default = "default_value"
}

3. description

The description argument allows you to add a human-readable description to the variable. This description can help other users understand the purpose of the variable. As a suggested good practice, you can use the same description as the Terraform docs.

For example, the name variable description from the Azure Virtual Network page:

variable "name" {
  type        = string
  description = "(Required) The name of the virtual network. Changing this forces a new resource to be created."
}

4. validation

The validation optional argument enables you to specify a validation rule using a function that the value of the variable must pass. If the validation fails, Terraform will raise an error.

variable "example_var" {
  type = number

  validation {
    condition     = var.example_var > 0
    error_message = "Value must be greater than 0."
  }
}

5. sensitive

When sensitive optional argument is set to true, the value of the variable is treated as sensitive information. Terraform will take care to prevent the value from being shown in logs and outputs. Note that Terraform will still record sensitive values in the state, so anyone who can access the state data will have access to the sensitive values in cleartext.

variable "password" {
  type      = string
  sensitive = true
}

6. nullable

The nullable argument in a variable block controls whether the module caller may assign the value null to the variable.

Anullvalue is one that represents absence or omission. If you set an argument of a resource to null, Terraform behaves as though you had completely omitted it — it will use the argument’s default value if it has one or raises an error if the argument is mandatory.

variable "example" {
  type     = string
  nullable = false
}

7. ephemeral (Terraform 1.10+)

The ephemeral argument makes a variable available at runtime but prevents its value from being stored in plan or state files. This is ideal for short-lived tokens or secrets that should never be persisted.

variable "session_token" {
  type        = string
  description = "Short-lived session token used during apply."
  sensitive   = true
  ephemeral   = true
}

Ephemeral variables still behave like normal variables inside a single plan/apply run, but you can only use them in certain places (for example, in other ephemeral values, locals, or ephemeral resources).

8. const

The const argument marks a variable as one that must hold a known, constant value at terraform init time, before any plan or apply happens. This lets you reference the variable in places that are evaluated during initialization — most notably a module block’s source and version arguments, which normally can’t take variables at all.

variable "module_version" {
  type        = string
  default     = "6.6.0"
  description = "Version of the VPC module to install."
  const       = true
}

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = var.module_version
}

Because the value has to be known at init, a const = true variable cannot depend on data sources, resource attributes, or anything else that’s only resolved during planning. Its default (or whatever value is supplied via tfvars, CLI, or environment) must be a literal that Terraform can resolve before it walks the configuration graph.

9. deprecated

The deprecated argument (available in Terraform v1.15 and later) lets module authors flag a variable as deprecated and surface a custom warning message whenever a caller sets it. The variable still works; Terraform just prints the message during validate, plan, and apply, so consumers know to migrate before you remove it in a future release.

variable "instance_size" {
  type        = string
  default     = null
  deprecated  = "Use 'instance_type' instead. 'instance_size' will be removed in v3.0."
}

variable "instance_type" {
  type        = string
  default     = "t3.micro"
  description = "EC2 instance type."
}

This is the cleanest way to evolve a module’s public interface without a breaking change: ship the new variable, mark the old one as deprecated, and remove it in the next major version. The same argument works on output blocks, with one restriction. I can only be set on outputs in child modules, not in the root module.

When you call a module whose deprecations you can’t (or don’t want to) fix, set ignore_nested_deprecations = true on the module block to mute warnings from that module and any of its nested modules. Your own code’s deprecation warnings will still surface normally.

module "legacy_vpc" {
  source                     = "./modules/legacy-vpc"
  ignore_nested_deprecations = true
}

How to use the Terraform optional()?

Sections above cover the arguments of the variable block itself. Terraform also provides a related but distinct feature, the optional() modifier (often called a function), which makes individual attributes inside an object type constraint optional.

It was introduced as an experimental feature in Terraform 0.14 and reached general availability in Terraform 1.3. It is also supported in OpenTofu.

The syntax is as follows:

optional(<type>, [<default>])
  • <type> – The type constraint for the attribute (string, number, list(string), another object(...), etc.)
  • <default> (optional) – The value Terraform substitutes when the caller omits the attribute. If you don’t supply a default, Terraform uses null.

Use optional() when you want a single variable to accept structured input where some fields can be left out by the caller. This is common in module design, where you want sensible defaults for most fields and only require the caller to specify the ones that matter to them.

variable "instances" {
  type = map(object({
    instance_type = optional(string, "t3.micro")
    ami_id       = optional(string)         # defaults to null
    tags         = optional(map(string), {})
    monitoring   = optional(bool, false)
  }))

  default = {}
}

A caller can now provide just the fields they care about:

instances = {
  web = {
    ami_id = "ami-0c55b159cbfafe1f0"
  }
  worker = {
    instance_type = "t3.large"
    ami_id        = "ami-0c55b159cbfafe1f0"
    monitoring    = true
  }
}

Both entries are valid. The omitted fields receive their optional() defaults.

Terraform optional variable usage example

Now that we’ve covered the optional() function, here’s how it solves a real module-design problem.

Imagine you’re writing a module that provisions a set of EC2 instances. Each instance might need a different instance_type, ami_id, or set of tags, but most of the time you want sensible defaults. Without optional(), callers either have to fully populate every object, or you fall back on workarounds that sacrifice type safety. With optional(), the variable becomes self-documenting and the call site stays clean:

resource "aws_instance" "this" {
  for_each      = var.instances
  instance_type = each.value.instance_type
  ami           = each.value.ami_id

  tags = merge(
    { Name = each.key },
    each.value.tags,
  )
}

variable "instances" {
  type = map(object({
    instance_type = optional(string, "t3.micro")
    ami_id        = optional(string, "ami-0c55b159cbfafe1f0")
    tags          = optional(map(string), {})
  }))

  default = {}
}
A caller now only has to specify the fields they want to override. Everything else falls back to the defaults declared inside optional():

# terraform.tfvars
instances = {
  web     = {}                                # all defaults
  worker  = { instance_type = "t3.large" }    # one override
  bastion = {
    instance_type = "t3.small"
    ami_id        = "ami-09a9858973b288bdd"
    tags          = { Role = "bastion" }
  }
}

This produces three instances. web uses the defaults for all three attributes. worker keeps the default AMI and tags but uses a larger instance type. bastion overrides everything. The type system still validates each attribute, so a caller who passes instance_type = 42 gets a clear error at plan time rather than a confusing failure deeper in the provider.

The legacy alternative: lookup()

Before Terraform 1.3, the only way to handle missing attributes in an object variable was the lookup() function, which takes a map, a key, and a default value. The same module would have looked like this:

resource "aws_instance" "this" {
  for_each      = var.instances
  instance_type = lookup(each.value, "instance_type", "t3.micro")
  ami           = lookup(each.value, "ami_id", "ami-0c55b159cbfafe1f0")

  tags = merge(
    { Name = each.key },
    lookup(each.value, "tags", {}),
  )
}

variable "instances" {
  type        = map(any)
  description = <<-EOT
    Map of instances. Each value may contain:
      instance_type = string
      ami_id        = string
      tags          = map(string)
  EOT
}

The pattern still works, but it has real downsides:

  • The variable type has to be map(any), so you lose per-attribute type checking. Passing instance_type = 42 slips through at plan time and only fails when the provider rejects it.
  • The expected schema only lives in the description, where it can drift from the resource code without anyone noticing.
  • Every attribute needs a separate lookup() call at the call site, which clutters resource blocks as the variable grows.

If you’re on Terraform 1.3 or later, or any version of OpenTofu, prefer optional(). Use lookup() only when you have to support older Terraform versions.

Type constraints

Using the type constraint when defining an input variable specifies its expected data type. If no type constraint is set, then a value of any type is accepted. If both the type and default arguments are specified, the given default value must be convertible to the specified type.

The Terraform language will automatically convert number and bool values to string values when needed, and vice-versa as long as the string contains a valid representation of a number or boolean value.

variable "example_var" {
  type = number
}

Acceptable ‘primitive’ type values in Terraform are:

  • string
  • bool
  • number
  • any (indicates a value of any type is accepted).

The type constructors also allow you to specify complex types such as collections:

  • list(<TYPE>)
  • set(<TYPE>)
  • map(<TYPE>)
  • object({<ATTR NAME> = <TYPE>, ... })
  • tuple([<TYPE>, ...])

Why use Spacelift with Terraform?

Terraform is really powerful, but to achieve an end-to-end secure GitOps approach, you need a platform that can orchestrate your Terraform workflows. Spacelift is the infrastructure orchestration platform built for the AI-accelerated software era. 

It manages the full lifecycle for both traditional infrastructure as code (IaC) and AI-provisioned infrastructure, giving you access to a powerful CI/CD workflow and unlocking features such as:

  • Policies (based on Open Policy Agent) – You can control how many approvals you need for runs, what kind of resources you can create, and what kind of parameters these resources can have, and you can also control the behavior when a pull request is open or merged.
  • Multi-IaC workflows – Combine Terraform with Kubernetes, Ansible, and other IaC tools such as OpenTofu, Pulumi, and CloudFormation, create dependencies among them, and share outputs.
  • Build self-service infrastructure – You can use Templates and Blueprints to build self-service infrastructure; simply complete a form to provision infrastructure based on Terraform and other supported tools.
  • AI-powered provisioning and diagnosticsSpacelift Intelligence adds an AI-powered layer for natural language provisioning, diagnostics, and operational insight across your infrastructure workflows.
  • Integrations with any third-party tools – You can integrate with your favorite third-party tools and even build policies for them. For example, see how to integrate security tools in your workflows using Custom Inputs.

Spacelift enables you to create private workers inside your infrastructure, which helps you execute Spacelift-related workflows on your end. Read the documentation for more information on configuring private workers.

You can check it for free by creating a trial account or requesting a demo with one of our engineers.

Key points

Variables can be specified with optional parameters to enhance the quality of your code. Optional variables in Terraform modules allow you to customize module behavior without requiring all parameters to be explicitly defined. Including appropriate optional parameters in variables depending on your use case can be useful. As a best practice, you should at least include a description to improve your code’s readability, and you may consider adding other parameters that are unnecessary.

Note: New versions of Terraform are placed under the BUSL license, but everything created before version 1.5.x stays open-source. OpenTofu is an open-source version of Terraform that expands on Terraform’s existing concepts and offerings. It is a viable alternative to HashiCorp’s Terraform, being forked from Terraform version 1.5.6.

Orchestrate Terraform deployments with Spacelift

Orchestrate your Terraform workflows and build governed pipelines using policy as code, programmatic configuration, context sharing, drift detection, resource visualization, and many more.

Learn more

Frequently asked questions

  • What's the difference between nullable and optional() in Terraform?

    nullable is a setting on an input variable that controls whether the variable itself can accept a null value, while optional() is a type modifier used inside object types to mark specific attributes as not required and optionally assign a default.

  • How do you make a Terraform variable optional without a default?

    Set nullable = true on the variable and omit the default argument, then handle the null case in your configuration. Without a default, callers must still pass a value (even if explicitly null), since only variables with a default are truly optional to supply.

  • How do you deprecate a Terraform variable?

    In Terraform 1.15 and later, add a deprecated argument to the variable block with a message, for example deprecated = “Use new_var instead; will be removed in v2.0”. Terraform emits a warning whenever a value is passed to that variable.

Terraform Commands Cheat Sheet

Grab our ultimate cheat sheet PDF
for all the Terraform commands
and concepts you need.

Share your data and download the cheat sheet