Terraform: Ability to raise an error

Created on 4 Jul 2017  路  35Comments  路  Source: hashicorp/terraform

One feature I have not been able to find (or code around) is the lack of ability to raise an intentional error.

There are times where I need certain things to be correct for example 2 lists to be of the same size, or a variable to be one of a certain set of values etc.

So the addition of a raise_error function or similar would be useful. I was thinking of it being an additional interpolation function something that works like this:

raise_error(condition, message)

for example

raise_error(${length(var.array1) == length(var.array2)}, 'The arrays need to be the same length')

if the condition fails, then the message is displayed otherwise the processing continues.

config enhancement

Most helpful comment

Hey guys,

I found a way today how you can hack in asserts into the current version of Terraform.
I have banged out this example https://www.linkedin.com/pulse/devops-how-do-assertion-test-terraform-template-jamie-nelson/

TL;DR

variable "environment_list" {
  description = "Environment ID"
  type = "list"
  default = ["dev", "qa", "prod"]
}
variable "env" {
description = "Which environment do you want (options: dev, qa, prod):"
}
resource "null_resource" "is_environment_name_valid" {
  count = "${contains(var.environment_list, var.env) == true ? 0 : 1}"
  "ERROR: The env value can only be: dev, qa or prod" = true
}

or to use your test:

resource "null_resource" "is_array_length_correct" {
  count = "${length(var.array1) == length(var.array2) ? 0 : 1}"
  "array1 and array2 must be the same length" = true
}

All 35 comments

Hi @timgurney-ee! Thanks for this suggestion.

It sounds like what you're looking to do here is to guarantee certain invariants that your configuration depends on and prevent Terraform from trying to process a configuration if those invariants don't hold.

I've thought before about the idea of having a way to make "assertions" in the Terraform config that get tested before Terraform will take any further action. For example (made-up syntax here just for illustration purposes):

require "matching_arrays" {
  test = "${length(var.array1) == length(var.array2)}"
  message = "array1 and array2 must be the same length"
}

I'd imagined this working by creating a new node in the graph representing the requirement, and then visiting that node during all graph walks. If the test expression returns <computed> (because it refers to a resource attribute we can't know yet) then we'd proceed optimistically assuming the assertion is true, but if it returns a concrete false we'd fail.

In the above example where only var. interpolations are used, this would be able to fail early in the "validate" walk, which would be the most ideal behavior.

The following case is trickier:

require "distinct_foo_and_baz" {
  test = "${aws_instance.foo.id != aws_instance.baz.id}"
  message = "instances foo and baz must be distinct"
}

This is a contrived example, but it illustrates a case where we'd be unable to return the error until after both instances are created in the apply step. Fortunately I think most real-world uses-cases of this would apply to variables and data sources, and would thus generally be taken care of during either the "validate" or the "plan" step.

Another part of this would be defining which resources should only be processed if the invariant is satisfied. This could be achieved by a new special node type in depends_on, like this:

resource "aws_instance" "bar" {
  # ... (normal attributes) ...

  depends_on = [
    "require.matching_arrays",
    "require.distinct_foo_and_baz",
  ]
}

This would then let Terraform know that it mustn't try to real with this instance until after the requirement has been checked. Without this, Terraform's normal concurrent processing of resources could allow the instance to get processed before the assertion is processed, in the event that the assertion is being processed at apply time due to referring to other computed resource attributes.

This is all just a sketch for now. Not sure what this would actually look like, but I'm curious to hear if you think the above would help solve the problem you're trying to solve. I think a first-class block would work better for this than an interpolation function because it gives us this ability to control the processing order via normal dependencies, which would be much harder with an interpolation function.

I think you have definitely hit on what I was trying to do with the suggestion.

For the use case I was thinking of it would normally be known vars however the extended examples with interpolated results also make a lot of sense but also highlight the complexity of the problem.

The overall descriptions would solve the problem I am looking at, assert is effectively what I am thinking of. I didn't take my thoughts as deep or as detailed you, as I was only considering a smaller subset of issues.

I would be happy if this came in in stages, maying stage one just being pre-defined vars with values, and then maybe extending it to handle interpolation at a later stage?

Hey guys,

I found a way today how you can hack in asserts into the current version of Terraform.
I have banged out this example https://www.linkedin.com/pulse/devops-how-do-assertion-test-terraform-template-jamie-nelson/

TL;DR

variable "environment_list" {
  description = "Environment ID"
  type = "list"
  default = ["dev", "qa", "prod"]
}
variable "env" {
description = "Which environment do you want (options: dev, qa, prod):"
}
resource "null_resource" "is_environment_name_valid" {
  count = "${contains(var.environment_list, var.env) == true ? 0 : 1}"
  "ERROR: The env value can only be: dev, qa or prod" = true
}

or to use your test:

resource "null_resource" "is_array_length_correct" {
  count = "${length(var.array1) == length(var.array2) ? 0 : 1}"
  "array1 and array2 must be the same length" = true
}

Nice work around!

Based on discussion in #16848, it would be valuable for it to be possible to raise errors even when terraform is run using -target. The current workaround and proposed solution does not make this possible.

If the require method had an additional flag for always_run or something similar, this would cover additional use cases

Why not using a data source from a provider instead ?

An assert provider with an assert_equals data source for exemple (can be extended later if required) ?

We can still create explicit dependencies if necessary.

Main advantage I see is that most data sources can be checked during the refresh phase for sanity checks (not resource dependent). It also allows to reuse existing terraform logic without adding new grammar to the language.

Does this hack still work with terraform v0.11.10. I keep getting the invalid key error every single time when I run terraform plan. Is this the expected behavior?

@nandac It doesn't work for me either. I've got around it by conditionally executing a failing command with local-exec.

resource "null_resource" "dns_check" {
  count = "${data.external.check_dns_setup.result.valid == true ? 0 : 1}"

  provisioner "local-exec" {
    command     = "false"
    interpreter = ["bash", "-c"]
  }
}

@nandac @ervinb This workaround is working for me on v0.11.13. If the count is 0 it doesn't evaluate the null_resource when planning.

Hey everyone,

I was using @Jamie-BitFlight's workaround but it does not work anymore on Terraform 0.12.
The goal of his solution was to make Terraform fail (by evaluating an invalid key) in the cases we are interested in.
The reason why it does not work anymore is because Terraform evaluates the invalid key even when the count is 0.

I also tried the "local-exec" workaround but it's only executed when applying which is too late for me.

I found a similar approach that works with Terraform 0.12.
The idea is to initialize the "triggers" parameter of the null_resource with a valid map when your assertion is respected and with something else when you need to make it fail.

I first tried with an empty string:

resource "null_resource" "assert_something" {
    triggers = <my_assertion>  ? {} : ""
}

But Terraform want the ternary to be consistent with types. i got this error:

The true and false result expressions must have consistent types. The given
expressions are object and string, respectively.

Then i tried with the "file" function and it worked:

resource "null_resource" "assert_something" {
    triggers = <my_assertion>  ? {} : file("ERROR: your assertion is not ok")
}

The output is a bit weird due to the fact it tries to open a file, but it works ...
Call to function "file" failed: no file exists at ERROR: your assertion is not

After that i noticed that using triggers will generate a change each time you apply.
The last thing i added is a lifecycle rule to ignore changes on triggers.

The final version of the workaround:

resource "null_resource" "assert_something" {
    triggers = <my_assertion>  ? {} : file("your assertion is not ok")
    lifecycle {
        ignore_changes = [
            triggers
        ]
    }
}

May be useful to folks here: I've created a custom provider specifically for triggering failures on plan: https://github.com/rhythmictech/terraform-provider-errorcheck

example:

code:

locals {
  compare     = "success"
  testSuccess = "success"
  testFail    = "fail"
}

resource "errorcheck_is_valid" "shouldMatch" {
  name = "shouldMatch"
  test = local.compare == local.testSuccess
}

resource "errorcheck_is_valid" "Not_valid_if_not_match" {
  name = "Not_valid_if_not_match"
  test = local.compare == local.testFail
}

output:

terraform validate .

Error: Not Valid

  on main.tf line 11, in resource "errorcheck_is_valid" "Not_valid_if_not_match":
  11: resource "errorcheck_is_valid" "Not_valid_if_not_match" {

At some point I'd like to look at setting up a custom error message, but it's already much cleaner and more future-proof than hacking around the HCL parser

@smiller171 looks like we had the same idea :) however I went with data_source as there is no need to save any values in state.
https://github.com/bwoznicki/terraform-provider-assert

It would be nice to have assert-like functionality in terraform as default especially now with all the additional functions in terraform 0.12x

Some time ago I tried to solve it with similar data source approach, but inside a module https://github.com/gordonbondon/terraform-common-verify

@bwoznicki @gordonbondon The problem with doing it in a data source instead of a provider is that it won't error until you try to apply. Doing it in a provider lets you throw an error on plan or validate

@smiller171 data source is validated early look at this: https://www.terraform.io/docs/configuration/data-sources.html#data-source-lifecycle and https://www.terraform.io/docs/extend/writing-custom-providers.html#data-sources unless you are targeting a resource output that is know after apply it will fail at the plan stage. Run an example of workspace test in https://github.com/bwoznicki/terraform-provider-assert and you will see that it fails regardless of how many resources you have there.

@bwoznicki interesting. I went with a provider specifically because I thought data sources weren't evaluated during planning.

I am also struggling with this problem during ansible provisioning inside local_exec. if there are ansible errors, I'd like them to be able to stop terraform from continuing provisioning to make it easier to find out what went wrong (otherwise the log is massive, and no colors doesn't make it easy). Is it not possible to raise an error to terraform and stop it from continuing provisioning?

Thanks @Vince-Chenal for the new workaround.

AFAICT the following simpler variation seems to also work with Terraform 0.12.5:

locals {
  assert_not_default_workspace = terraform.workspace == "default" ? file("ERROR: default workspace not allowed") : null
}
$ terraform-0.12.5 validate

Error: Error in function call

  on main.tf line 2, in locals:
   2:   assert_not_default_workspace = terraform.workspace == "default" ? file("ERROR: default workspace not allowed") : null

Call to function "file" failed: no file exists at ERROR: default workspace not
allowed.

Honestly if we just had an error() interpolation function (as OP suggested) to use instead of file(), I'd be pretty happy with this.

I also would like to use some kind of native resource for assertions, especially when passing generated values to Terraform modules.

The simple variation that @dmrzzz gave above is still working.
But it is a little bit sad that there are still not some better/native way to do asserts and similar.
It would be very useful when writing reusable modules that can be run with different user provided input...

@apparentlymart maybe a first step could be to just have an "assert-inputs" function for var. values like you suggested earlier.
The more complex cases when certain resources and modules need to be validated and to find it out and fail early in planning already would also be very nice, but one small step at a time, right =)

i've started to find that using a local-exec with simply

echo "Failed Message" >&2
exit 1

is working well for me.

....extended to ansible in local-exec I'm doing this now-

resource "null_resource" "fail-test" {
  provisioner "local-exec" {
    command = <<EOT
      exit_test () {
        RED='\033[0;31m' # Red Text
        GREEN='\033[0;32m' # Green Text
        BLUE='\033[0;34m' # Blue Text
        NC='\033[0m' # No Color
        if [ $? -eq 0 ]; then
          printf "\n $GREEN Playbook Succeeded $NC \n"
        else
          printf "\n $RED Failed Playbook $NC \n" >&2
          exit 1
        fi
      }
      ansible-playbook -i "$TF_VAR_inventory" ansible/fail-test.yaml; exit_test

EOT

  }
}

@queglay The problem is this won't fail until apply time. Ideally we should be able to fail the plan

Apologies, I see the value in that too. I was stumped on how to ensure ansible playbooks that failed on apply would stop the rollout from continuing.

This seems like a pretty important feature to have. Are there any thoughts on providing this within Terraform itself in a way that is both compatible with 0.12.x and that doesn't require opening a non-existent file?

I'll definitely take a look at https://github.com/bwoznicki/terraform-provider-assert as well as its fork https://github.com/rvodden/terraform-provider-assert

While this may not completely solve the issue, just wanted to add this reference for anyone looking at the ticket:

https://www.terraform.io/docs/configuration/variables.html#custom-validation-rules

Another workaround is to use the regex function as _"If the given pattern does not match at all, the regex raises an error."_. One downside of this approach is that the error message tells you what pattern was not matched but doesn't tell you why the pattern is not permitted.

@adamday2 it works for basic assertion, but you cannot cross reference other variables. If you must provide either variable a or b, you can't check in a if b has also been provided.

I am just pointing this out for anyone reading this thread, as you said it yourself that it doesn't solve the issue.

I've added a feature proposal in #24269 which (in combination with the newly added try() and can()) should at least in theory address all the core requirements discussed here.

The core idea in my proposal is that we can keep this very simple: raise(error_message) and just leverage the native if-then-else constructs in combination with the newly added (and extremely helpful!) try() wrapper. Importantly, this doesn't modify the core HCL syntax at all. It's very similar to the modulal approach of @gordonbondon, except that it can operate in a locals block or inline anywhere that expressions are accepted.

Please raise a vote there and join the discussion on #24269 if you think this would be a valuable addition. Thanks!

I am actually more in favour of having raise accept just an error message (like proposed in #24269) as opposed to raise accepting both a condition and an error message. IMO the condition should be determined outside the scope of the raise. Keeps the code-readability simpler and easier.

@aaronsteers @syedrakib Do try() and can() get evaluated during plan, or only during apply?

@smiller171 - My understanding is that try() and can() apply when possible, during plan if sufficient information is available or during the apply otherwise.

I have created a new PR to add the raise(error_msg) function, which hits the HCL and Terraform github repos: #25088

I am actually more in favour of having raise accept just an error message (like proposed in #24269) as opposed to raise accepting both a condition and an error message. IMO the condition should be determined outside the scope of the raise. Keeps the code-readability simpler and easier.

@syedrakib - The new PR #25088 attempts to provide this. It still needs a bunch more testing but functionality-wise, I think it's mostly there.

If anyone has Golang experience and can help continue to testing/review, this would be very much appreciated!

May be resolved by: #25088 (PR waiting feedback, CI tests passing)

@timgurney-ee (OP) and subscribers to this issue:

  • Please vote via thumbs up and watch the raise(err_msg) PR (#25088) if you want to see this functionality added to Terraform in a future release.
Was this page helpful?
0 / 5 - 0 ratings