Skip to content

Latest commit

 

History

History
313 lines (223 loc) · 16.5 KB

custom-conditions.mdx

File metadata and controls

313 lines (223 loc) · 16.5 KB
page_title description
Custom Conditions - Configuration Language
Validate requirements for variables, outputs, and within lifecycle blocks so Terraform can produce better error messages in context.

Custom Conditions

You can create conditions that produce custom error messages for several types of objects in a configuration. For example, you can add a condition to an input variable that checks whether incoming image IDs are formatted properly.

Custom conditions can help capture assumptions that might be only implied, helping future maintainers understand the configuration design and intent. They also return useful information about errors earlier and in context, helping consumers more easily diagnose issues in their configurations.

You can create custom conditions with the following types of expressions.

-> Note: Input variable validation is available in Terraform CLI v0.13.0 and later. Preconditions and postconditions are available in Terraform CLI v1.2.0 and later.

Input Variable Validation

To specify custom validation rules for a variable, add one or more validation blocks within the corresponding variable block.

The condition argument is an expression that must use the value of the variable to return true if the value is valid, or false if it is invalid. If condition evaluates to false, Terraform will produce an error message that includes the result of the error_message expression. If you declare multiple validation blocks, Terraform returns error messages for all failed conditions.

The following example checks whether the AMI ID has valid syntax.

variable "image_id" {
  type        = string
  description = "The id of the machine image (AMI) to use for the server."

  validation {
    condition     = length(var.image_id) > 4 && substr(var.image_id, 0, 4) == "ami-"
    error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
  }
}

If the failure of an expression is the basis of the validation decision, use the can function to detect such errors, as demonstrated in the following example.

variable "image_id" {
  type        = string
  description = "The id of the machine image (AMI) to use for the server."

  validation {
    # regex(...) fails if it cannot find a match
    condition     = can(regex("^ami-", var.image_id))
    error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
  }
}

Preconditions and Postconditions

Terraform checks a precondition before evaluating the object it is associated with, and checks a postcondition after evaluating the object. You can add preconditions and postconditions within the following configuration blocks.

Resources and Data Sources

The lifecycle block inside a resource or data block can include both precondition and postcondition blocks associated with the containing resource.

  • Terraform evaluates preconditions before evaluating the resource's configuration arguments. Preconditions can take precedence over argument evaluation errors.
  • Terraform evaluates postconditions after planning and applying changes to a managed resource, or after reading from a data source. Postcondition failures prevent changes to other resources that depend on the failing resource.

Outputs

An output block can include a precondition block.

  • Terraform evaluates output value preconditions before evaluating the value expression to finalize the result. Preconditions can take precedence over potential errors in the value expression.
  • Preconditions can be particularly useful in a root module to prevent saving an invalid new output value in the state. You can also use them to preserve the value from the previous apply.
  • Preconditions can serve a symmetrical purpose to input variable validation blocks. Whereas input variable validation checks assumptions the module makes about its inputs, preconditions check guarantees that the module makes about its outputs.

Usage Examples

The following example shows several possible uses for preconditions and postconditions.

variable "aws_ami_id" {
  type = string

  # Input variable validation can check that the AMI ID is syntactically valid.
  validation {
    condition     = can(regex("^ami-", var.aws_ami_id))
    error_message = "The AMI ID must have the prefix \"ami-\"."
  }
}

data "aws_ami" "example" {
  id = var.aws_ami_id

  lifecycle {
    # A data resource with a postcondition can ensure that the selected AMI
    # meets this module's expectations, by reacting to the dynamically-loaded
    # AMI attributes.
    postcondition {
      condition     = self.tags["Component"] == "nomad-server"
      error_message = "The selected AMI must be tagged with the
      Component value \"nomad-server\"."
    }
  }
}

resource "aws_instance" "example" {
  instance_type = "t2.micro"
  ami           = "ami-abc123"

  lifecycle {
    # A resource with a precondition can ensure that the selected AMI
    # is set up correctly to work with the instance configuration.
    precondition {
      condition     = data.aws_ami.example.architecture == "x86_64"
      error_message = "The selected AMI must be for the x86_64 architecture."
    }

    # A resource with a postcondition can react to server-decided values
    # during the apply step and halt work immediately if the result doesn't
    # meet expectations.
    postcondition {
      condition     = self.private_dns != ""
      error_message = "EC2 instance must be in a VPC that has private DNS hostnames enabled."
    }
  }
}

data "aws_ebs_volume" "example" {
  # Use data resources that refer to other resources in order to
  # load extra data that isn't directly exported by a resource.
  #
  # Read the details about the root storage volume for the EC2 instance
  # declared by aws_instance.example, using the exported ID.

  filter {
    name = "volume-id"
    values = [aws_instance.example.root_block_device.volume_id]
  }
}

output "api_base_url" {
  value = "https://${aws_instance.example.private_dns}:8433/"

  # An output value with a precondition can check the object that the
  # output value is describing to make sure it meets expectations before
  # any caller of this module can use it.
  precondition {
    condition     = data.aws_ebs_volume.example.encrypted
    error_message = "The server's root volume is not encrypted."
  }
}

The preconditions and postconditions declare the following assumptions and guarantees.

  • The AMI ID must refer to an AMI that exists and that has been tagged with "nomad-server". This would detect if the caller accidentally provided an AMI intended for some other system component. This might otherwise be detected only after booting the EC2 instance and noticing that the expected network service is not running.

  • The AMI ID must refer to an AMI that contains an operating system for the x86_64 architecture. This would detect if the caller accidentally built an AMI for a different architecture, which may not be able to run the software this virtual machine is intended to host.

  • The EC2 instance must be allocated a private DNS hostname. In Amazon Web Services, EC2 instances are assigned private DNS hostnames only if they belong to a virtual network configured in a certain way. This would detect if the selected virtual network is not configured correctly, giving explicit feedback to prompt the user to debug the network settings.

  • The EC2 instance will have an encrypted root volume. This ensures that the root volume is encrypted even though the software running in this EC2 instance would probably still operate as expected on an unencrypted volume. This lets Terraform produce an error immediately, before any other components rely on the component.

Choosing Between Preconditions and PostConditions

You can often implement a validation check as either a postcondition of the resource producing the data or as a precondition of a resource or output value using the data. To decide which is most appropriate, consider whether the check is representing either an assumption or a guarantee.

  • Assumption: A condition that must be true in order for the configuration of a particular resource to be usable. For example, an aws_instance configuration can have the assumption that the given AMI will always be configured for the x86_64 CPU architecture.

    Assumptions should typically be written as preconditions, so that future maintainers can find them close to the other expressions that rely on that condition, and know more about what different variations that resource is intended to allow.

  • Guarantee: A characteristic or behavior of an object that the rest of the configuration should be able to rely on. For example, an aws_instance configuration can have the guarantee that an EC2 instance will be running in a network that assigns it a private DNS record.

    Guarantees should typically be written as postconditions, so that future maintainers can find them close to the resource configuration that is responsible for implementing those guarantees and more easily see which behaviors are important to preserve when changing the configuration.

We recommend also considering the following factors.

  • Which resource or output value would be most helpful to report in the error message. Terraform will always report errors in the location where the condition was declared.
  • Which approach is more convenient. If a particular resource has many dependencies that all make an assumption about that resource, it can be pragmatic to declare that once as a post-condition of the resource, rather than declaring it many times as preconditions on each of the dependencies.
  • Whether it is helpful to declare the same or similar conditions as both preconditions and postconditions. This can be useful if the postcondition is in a different module than the precondition because it lets the modules verify one another as they evolve independently.

Condition Expressions

The variable validation block and the precondition and postcondition blocks all require an argument named condition, whose value is a boolean expression which should return true if the intended assumption holds or false if it does not.

Condition expressions have the following requirements.

  • For variable validation blocks, the expression can refer only to the variable that the condition applies to and must not produce errors.
  • For precondition and postcondition blocks, the expression can refer to any other objects in the same module, as long as the references don't create any cyclic dependencies. Resource postconditions can additionally refer to attributes of each instance of the resource where they are configured, using the special symbol self. For example, self.private_dns refers to the private_dns attribute of each instance of the containing resource.

You can use any of Terraform's built-in functions or language operators in a condition as long as the expression is valid and returns a boolean result. The following language features are particularly useful when writing condition expressions.

contains Function

You can use the built-in function contains to test whether a given value is one of a set of predefined valid values.

  condition = contains(["STAGE", "PROD"], var.environment)

Boolean Operators

You can use the boolean operators && (AND), || (OR), and ! (NOT) to combine multiple simpler conditions together.

  condition = var.name != "" && lower(var.name) == var.name

length Function

You can require a non-empty list or map by testing the collection's length.

  condition = length(var.items) != 0

This is a better approach than directly comparing with another collection using == or !=, because the comparison operators can only return true if both operands have exactly the same type, which is often ambiguous for empty collections.

for Expressions

You can use for expressions which produce lists of boolean results themselves in conjunction with the functions alltrue and anytrue to test whether a condition holds for all or for any elements of a collection.

  condition = alltrue([
    for v in var.instances : contains(["t2.micro", "m3.medium"], v.type)
  ])

can Function

You can use the can function to concisely use the validity of an expression as a condition. It returns true if its given expression evaluates successfully and false if it returns any error, so you can use various other functions that typically return errors as a part of your condition expressions.

For example, you can use can with regex to test if a string matches a particular pattern, because regex returns an error when given a non-matching string.

  condition = can(regex("^[a-z]+$", var.name)

You can also use can with the type conversion functions to test whether a value is convertible to a type or type constraint.

  # This remote output value must have a value that can
  # be used as a string, which includes strings themselves
  # but also allows numbers and boolean values.
  condition = can(tostring(data.terraform_remote_state.example.outputs["name"]))
  # This remote output value must be convertible to a list
  # type of with element type.
  condition = can(tolist(data.terraform_remote_state.example.outputs["items"]))

You can also use can with attribute access or index operators to concisely test whether a collection or structural value has a particular element or index.

  # var.example must have an attribute named "foo"
  condition = can(var.example.foo)  ```

```hcl
  # var.example must be a sequence with at least one element
  condition = can(var.example[0])
  # (although it would typically be clearer to write this as a
  # test like length(var.example) > 0 to better represent the
  # intent of the condition.)

Error Messages

Each validation, precondition or postcondition block must include an argument error_message, which contains the text that Terraform will include as part of error messages when it detects an unmet condition.

Error: Resource postcondition failed

  with data.aws_ami.example,
  on ec2.tf line 19, in data "aws_ami" "example":
  72:       condition     = self.tags["Component"] == "nomad-server"
    |----------------
    | self.tags["Component"] is "consul-server"

The selected AMI must be tagged with the Component value "nomad-server".

The error_message argument can be any expression that evaluates to a string. This includes literal strings, heredocs, and template expressions. Multi-line error messages are supported, and lines with leading whitespace will not be word wrapped.

We recommend writing error messages as one or more full sentences in a style similar to Terraform's own error messages. Terraform will show the given message alongside the name of the resource that detected the problem and any outside values used as part of the condition expression.

When Terraform Evaluates Custom Conditions

Terraform evaluates custom conditions as early as possible.

Input variable validations can only refer to the variable value, so Terraform always evaluates them immediately. When Terraform evaluates preconditions and postconditions depends on whether the value(s) associated with the condition are known before or after applying the configuration.

  • Known before apply: Terraform checks the condition during the planning phase. For example, Terraform can know the value of an image ID during planning as long as it is not generated from another resource.
  • Known after apply: Terraform delays checking that condition until the apply phase. For example, AWS only assigns the root volume ID when it starts an EC2 instance, so Terraform cannot know this value until apply.

During the apply phase, a failed precondition will prevent Terraform from implementing planned actions for the associated resource. However, a failed postcondition will halt processing after Terraform has already implemented these actions. The failed postcondition prevents any further downstream actions that rely on the resource, but does not undo the actions Terraform has already taken.

Terraform typically has less information during the initial creation of a full configuration than when applying subsequent changes. Therefore, conditions checked during apply for initial creation may be checked earlier during planning for subsequent updates.