Fix formatting issues that would prevent website from loading
This commit is contained in:
parent
d1ac8b71d4
commit
834f65e4f0
|
@ -278,6 +278,10 @@
|
||||||
{
|
{
|
||||||
"title": "Version Constraints",
|
"title": "Version Constraints",
|
||||||
"path": "expressions/version-constraints"
|
"path": "expressions/version-constraints"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Pre and Postconditions",
|
||||||
|
"path": "expressions/preconditions-postconditions"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,391 +0,0 @@
|
||||||
---
|
|
||||||
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