rfc/2025-07-22-conditional-enable.md
Issue: https://github.com/opentofu/opentofu/issues/1306
[!NOTE] Every time we refer to resources in this RFC, we're talking about managed resources, data sources and ephemeral resources.
Right now, OpenTofu supports conditional enable/disable of resources by using count as a workaround:
variable "enabled" {
default = true
}
resource "aws_instance" "example" {
count = var.enabled ? 1 : 0
# ...
}
This approach brings a few problems, such as adding indexes to resources that would be single instances, making it harder to manage using these indexes.
This RFC proposes a new way to do this in a cleaner and more semantic manner by using a new field on the lifecycle block called enabled:
resource "aws_instance" "example" {
# ...
lifecycle {
enabled = var.enabled
}
}
This is available not only for resources, but for modules too:
module "modcall" {
source = "./mod"
lifecycle {
enabled = var.env == "production"
}
}
lifecycle block called enabled;lifecycle block for modules but only supporting the new enabled field.lifecycle support on ModulesThe lifecycle block is currently not supported on module block, but in our docs we mention to users that it's reserved for future usage:
OpenTofu does not use the lifecycle argument. However, the lifecycle block is reserved for future versions.
This proposal could be the right time to start supporting this lifecycle block, but only to add the enabled field.
count to enabledAs mentioned before, users were adding support for enabling/disabling resources by using count = var.enabled ? 1 : 0
and this is the most common case where they want to use an enabled field.
To support moving between resources where the count = 0 workaround was used to the new enabled approach,
OpenTofu will automatically support an implicit move of the resource out of the box with no changes needed. An implicit move will be done on the apply phase to remove the
index and turn it into a single instance.
OpenTofu will perform the following actions:
# null_resource.example[0] has moved to null_resource.example
resource "null_resource" "example" {
id = "8975736315378968412"
}
Plan: 0 to add, 0 to change, 0 to destroy.
This behavior only exists for resources at the moment. As part of the RFC, support will be written for implied moves within modules. Until then, this can be done with:
moved {
from = module.enabled_module[0]
to = module.enabled_module
}
for_each to enabledUnlike count, OpenTofu doesn't handle implicit moves when migrating from for_each. Users must explicitly tell OpenTofu what needs to be moved:
moved {
from = aws_s3_bucket.example["for_each_key"] # name of the key being used by for_each
to = aws_s3_bucket.example
}
Here's a complete example of this migration:
# For_each before:
locals {
buckets = ["bucket-1", "bucket-2"]
}
moved {
from = aws_s3_bucket.example["bucket-1"] # name of the key being used by for_each
to = aws_s3_bucket.example
}
resource "aws_s3_bucket" "example" {
for_each = var.enable ? toset(local.buckets) : []
bucket = each.key
}
# To:
resource "aws_s3_bucket" "example" {
lifecycle {
enabled = var.enable
}
}
Moving from for_each to enabled doesn't make sense if more than one resource is being used. In the example above, bucket-2 would be removed.
This migration path should be used only when a single instance is being used and for_each is supporting that conditional.
There's been discussion about this topic, and
the best compromise we found is that a disabled resource can be null, but no errors will
be shown statically, such as when using tofu validate or the tofu-ls language server.
Since we need to evaluate the enabled expression, these errors will be shown dynamically
when running plan or apply:
│ Error: Attempt to get attribute from null value
│
│ on main.tf line 19, in output "enabled_output":
│ 19: value = null_resource.example.id
│ ├────────────────
│ │ null_resource.example is null
│
│ This value is null, so it does not have any attributes.
The only available data access patterns for possibly disabled resources are:
null_resource.example != null ? null_resource.example.id : "default value"try(null_resource.example.id, "default_value")can(null_resource.example.id) ? null_resource.example.id : "default value"This can be extended using custom provider-defined functions. For example, if someone wanted to create a function that adds a warning instead of raising an error when trying to access the attribute, they could do:
provider::custom::warning_enabled(null_resource.example.id, "default value")For now, we offer the three options above.
try is the most concise option, but it should be used carefully since it can silently mask errors not caused by null value access. can and != null are more verbose but more reliable for doing what you're trying to do.
Discarded options:
aws_s3_bucket.example?.bucket. Since this operator is a new HCL2 language feature that's not straightforward to implement and is essentially syntactic sugar for the system proposed in this RFC, we believe this discussion should be postponed until the foundational feature is in place.Suppose we have created a resource using the enabled field and then we want to disable it.
The behavior will be the same as if a resource is being destroyed:
# aws_instance.demo_vm_2 will be destroyed
# (because enabled is false)
- resource "aws_instance" "demo_vm_2" {
- ami = "ami-07df274a488ca9195" -> null
- arn = "arn:aws:ec2:eu-central-1:532199187081:instance/i-0b63433033e61d818" -> null
- associate_public_ip_address = true -> null
- availability_zone = "eu-central-1b" -> null
enabled together with for_each and count.These three arguments behave similarly but with different semantics:
enabled is used for single-instance resources or modules.count creates multiple instances of a resource or module, differentiating them with integer indices.for_each creates multiple instances with added flexibility through access to each.key and each.value.One might argue that count and enabled can be used together, but this would create confusion. The data access patterns for these resources are different:
count and for_each resources can be accessed as: aws_s3_bucket.example[*].bucket;enabled resources can be accessed as: null_resource.example != null ? null_resource.example.id : "default value";If both fields were enabled together, how would a user decide which access pattern to use? If enabled is true and count is greater than zero,
aws_s3_bucket.example[*].bucket could be used, but if enabled is false, the expression would break due to accessing null fields. The same applies to
for_each resources.
To avoid this confusion, we prefer to allow only one of these fields to be used at a time. enabled is designed for single-instance resources or modules.
Several types of values cannot be used in this conditional:
enabled field was placed in the lifecycle block rather than directly on resources/modules to avoid conflicts with
existing resources that use "enabled" as an attribute name. A future language edition could promote it to a top-level field.
See the discussion here.enabled field with count and for_each. In this current iteration, we need to see how users will use the feature, but for now, consider this section.