website: Initial draft docs for Preconditions and Postconditions
This commit is contained in:
parent
5573868cd0
commit
f1b7f12f1c
|
@ -0,0 +1,391 @@
|
|||
---
|
||||
page_title: Preconditions and Postconditions - Configuration Language
|
||||
---
|
||||
|
||||
# Preconditions and Postconditions
|
||||
|
||||
Terraform providers can automatically detect and report problems related to
|
||||
the remote system they are interacting with, but they typically do so using
|
||||
language that describes implementation details of the target system, which
|
||||
can sometimes make it hard to find the root cause of the problem in your
|
||||
Terraform configuration.
|
||||
|
||||
Preconditions and postconditions allow you to optionally describe the
|
||||
assumptions you are making as a module author, so that Terraform can detect
|
||||
situations where those assumptions don't hold and potentially return an
|
||||
error earlier or an error with better context about where the problem
|
||||
originated.
|
||||
|
||||
Preconditions and postconditions both follow a similar structure, and differ
|
||||
only in when Terraform evaluates them: Terraform checks a precondition prior
|
||||
to evaluating the object it is associated with, and a postcondition _after_
|
||||
evaluating the object. That means that preconditions are useful for stating
|
||||
assumptions about data from elsewhere that the resource configuration relies
|
||||
on, while postconditions are more useful for stating assumptions about the
|
||||
result of the resource itself.
|
||||
|
||||
The following example shows some different possible uses of preconditions and
|
||||
postconditions.
|
||||
|
||||
```hcl
|
||||
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" {
|
||||
# We can use data resources that refer to other resources in order to
|
||||
# load extra data that isn't directly exported by a resource.
|
||||
#
|
||||
# This example reads 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 input variable validation rule, preconditions, and postconditions in the
|
||||
above example declare explicitly some assumptions and guarantees that the
|
||||
module developer is making in the design of this module:
|
||||
|
||||
* The caller of the module must provide a syntactically-valid AMI ID in the
|
||||
`aws_ami_id` input variable.
|
||||
|
||||
This would detect if the caller accidentally assigned an AMI name to the
|
||||
argument, instead of an AMI ID.
|
||||
|
||||
* The AMI ID must refer to an AMI that exists and that has been tagged as
|
||||
being intended for the component "nomad-server".
|
||||
|
||||
This would detect if the caller accidentally provided an AMI intended for
|
||||
some other system component, which might otherwise be detected only after
|
||||
booting the EC2 instance and noticing that the expected network service
|
||||
isn't running. Terraform can therefore detect that problem earlier and
|
||||
return a more actionable error message for it.
|
||||
|
||||
* The AMI ID must refer to an AMI which contains an operating system for the
|
||||
`x86_64` architecture.
|
||||
|
||||
This would detect if the caller accidentally built an AMI for a different
|
||||
architecture, which might therefore 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 AWS, 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. Therefore Terraform can draw attention to the
|
||||
problem immediately, before any other components rely on the
|
||||
insecurely-configured component.
|
||||
|
||||
Writing explicit preconditions and postconditions is always optional, but it
|
||||
can be helpful to users and future maintainers of a Terraform module by
|
||||
capturing assumptions that might otherwise be only implied, and by allowing
|
||||
Terraform to check those assumptions and halt more quickly if they don't
|
||||
hold in practice for a particular set of input variables.
|
||||
|
||||
## Precondition and Postcondition Locations
|
||||
|
||||
Terraform supports preconditions and postconditions in a number of different
|
||||
locations in a module:
|
||||
|
||||
* The `lifecycle` block inside a `resource` or `data` block can include both
|
||||
`precondition` and `postcondition` blocks associated with the containing
|
||||
resource.
|
||||
|
||||
Terraform evaluates resource preconditions before evaluating the resource's
|
||||
configuration arguments. Resource preconditions can take precedence over
|
||||
argument evaluation errors.
|
||||
|
||||
Terraform evaluates resource postconditions after planning and after
|
||||
applying changes to a managed resource, or after reading from a data
|
||||
resource. Resource postcondition failures will therefore prevent applying
|
||||
changes to other resources that depend on the failing resource.
|
||||
|
||||
* An `output` block declaring an output value can include a `precondition`
|
||||
block.
|
||||
|
||||
Terraform evaluates output value preconditions before evaluating the
|
||||
`value` expression to finalize the result. Output value preconditions
|
||||
can take precedence over potential errors in the `value` expression.
|
||||
|
||||
Output value preconditions can be particularly useful in a root module,
|
||||
to prevent saving an invalid new output value in the state and to preserve
|
||||
the value from the previous apply, if any.
|
||||
|
||||
Output value preconditions can serve a symmetrical purpose to input
|
||||
variable `validation` blocks: whereas input variable validation checks
|
||||
assumptions the module makes about its inputs, output value preconditions
|
||||
check guarantees that the module makes about its outputs.
|
||||
|
||||
## Condition Expressions
|
||||
|
||||
`precondition` and `postcondition` blocks both 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.
|
||||
|
||||
Preconditions and postconditions can both 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.
|
||||
|
||||
Condition expressions are otherwise just normal Terraform expressions, and
|
||||
so you can use any of Terraform's built-in functions or language operators
|
||||
as long as the expression is valid and returns a boolean result.
|
||||
|
||||
### Common Condition Expression Features
|
||||
|
||||
Because condition expressions must produce boolean results, they can often
|
||||
use built-in functions and language features that are less common elsewhere
|
||||
in the Terraform language. The following language features are particularly
|
||||
useful when writing condition expressions:
|
||||
|
||||
* You can use the built-in function `contains` to test whether a given
|
||||
value is one of a set of predefined valid values:
|
||||
|
||||
```hcl
|
||||
condition = contains(["STAGE", "PROD"], var.environment)
|
||||
```
|
||||
|
||||
* You can use the boolean operators `&&` (AND), `||` (OR), and `!` (NOT) to
|
||||
combine multiple simpler conditions together:
|
||||
|
||||
```hcl
|
||||
condition = var.name != "" && lower(var.name) == var.name
|
||||
```
|
||||
|
||||
* You can require a non-empty list or map by testing the collection's length:
|
||||
|
||||
```hcl
|
||||
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.
|
||||
|
||||
* 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:
|
||||
|
||||
```hcl
|
||||
condition = alltrue([
|
||||
for v in var.instances : contains(["t2.micro", "m3.medium"], v.type)
|
||||
])
|
||||
```
|
||||
|
||||
* 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:
|
||||
|
||||
```hcl
|
||||
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:
|
||||
|
||||
```hcl
|
||||
# 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"]))
|
||||
```
|
||||
|
||||
```hcl
|
||||
# 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:
|
||||
|
||||
```hcl
|
||||
# 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.)
|
||||
```
|
||||
|
||||
## Early Evaluation
|
||||
|
||||
Terraform will evaluate conditions as early as possible.
|
||||
|
||||
If the condition expression depends on a resource attribute that won't be known
|
||||
until the apply phase then Terraform will delay checking the condition until
|
||||
the apply phase, but Terraform can check all other expressions during the
|
||||
planning phase, and therefore block applying a plan that would violate the
|
||||
conditions.
|
||||
|
||||
In the earlier example on this page, Terraform would typically be able to
|
||||
detect invalid AMI tags during the planning phase, as long as `var.aws_ami_id`
|
||||
is not itself derived from another resource. However, Terraform will not
|
||||
detect a non-encrypted root volume until the EC2 instance was already created
|
||||
during the apply step, because that condition depends on the root volume's
|
||||
assigned ID, which AWS decides only when the EC2 instance is actually started.
|
||||
|
||||
For conditions which Terraform must defer to the apply phase, a _precondition_
|
||||
will prevent taking whatever action was planned for a related resource, whereas
|
||||
a _postcondition_ will merely halt processing after that action was already
|
||||
taken, preventing any downstream actions that rely on it but not undoing the
|
||||
action.
|
||||
|
||||
Terraform typically has less information during the initial creation of a
|
||||
full configuration than when applying subsequent changes to that configuration.
|
||||
Conditions checked only during apply during initial creation may therefore
|
||||
be checked during planning on subsequent updates, detecting problems sooner
|
||||
in that case.
|
||||
|
||||
## Error Messages
|
||||
|
||||
Each `precondition` or `postcondition` block must include an argument
|
||||
`error_message`, which provides some custom error sentences 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 must always be a literal string, and should
|
||||
typically be written as a full sentence 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.
|
||||
|
||||
## Preconditions or Postconditions?
|
||||
|
||||
Because preconditions can refer to the result attributes of other resources
|
||||
in the same module, it's typically true that a particular check could be
|
||||
implemented either as 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 for a particular situation, consider
|
||||
whether the check is representing either an assumption or a guarantee:
|
||||
|
||||
* An _assumption_ is a condition that must be true in order for the
|
||||
configuration of a particular resource to be usable. In the earlier
|
||||
example on this page, the `aws_instance` configuration had the _assumption_
|
||||
that the given AMI will always be 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 thus know more about what different variations that
|
||||
resource is intended to allow.
|
||||
|
||||
* A _guarantee_ is a characteristic or behavior of an object that the rest of
|
||||
the configuration ought to be able to rely on. In the earlier example on
|
||||
this page, the `aws_instance` configuration had the _guarantee_ that the
|
||||
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.
|
||||
|
||||
In practice though, the distinction between these two is subjective: is the
|
||||
AMI being tagged as Component `"nomad-server"` a guarantee about the AMI or
|
||||
an assumption made by the EC2 instance? To decide, it might help to consider
|
||||
which resource or output value would be most helpful to report in a resulting
|
||||
error message, because Terraform will always report errors in the location
|
||||
where the condition was declared.
|
||||
|
||||
The decision between the two may also be a matter of convenience. If a
|
||||
particular resource has many dependencies that _all_ make an assumption about
|
||||
that resource then it can be pragmatic to declare that just once as a
|
||||
post-condition of the resource, rather than many times as preconditions on
|
||||
each of the dependencies.
|
||||
|
||||
It may sometimes be helpful to declare the same or similar conditions as both
|
||||
preconditions _and_ postconditions, particularly if the postcondition is
|
||||
in a different module than the precondition, so that they can verify one
|
||||
another as the two modules evolve independently.
|
Loading…
Reference in New Issue