Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow destroy-time provisioners to access variables #23679

Open
shaunc opened this issue Dec 14, 2019 · 63 comments
Open

Allow destroy-time provisioners to access variables #23679

shaunc opened this issue Dec 14, 2019 · 63 comments

Comments

@shaunc
Copy link

shaunc commented Dec 14, 2019

Current Terraform Version

Terraform v0.12.18

Use-cases

Using a local-exec provisioner when=destroy in a null resource, to remove local changes; currently, the setup provisioner has:
interpreter = [var.local_exec_interpreter, "-c"]

Attempted Solutions

Include:
interpreter = [var.local_exec_interpreter, "-c"]
in the 'when=destroy' provisioner.

This works for the moment, but produces a deprecation warning.

Proposal

The most straightforward way to address this would be to allow variables to be used in destroy provisioners. Direct use of passed-in variables does not present the danger of cyclic dependences created by resource references.

A more ambitious alternate solution would be to allow resources to declare arbitrary attributes at create time, which could be referenced at destroy time. E.g.:

resource "null_resource" "foo" {
   ...
   self {
    interpreter = var.local_exec_interpreter
  }
  provisioner {
    when = destroy
    interpreter = self.interpreter
    ...
  }
}

This would allow the provisioner (or other resources) to access values calculated in state
before application. It would require 2-pass evaluation (AFAIK), and thus a much more ambitious change to the code base. Even if the community likes this, it would seem quicker, (and not really hacky) to allow reference to passed in variables in destroy provisioners.

References

#23675

@pkolyvas pkolyvas self-assigned this Dec 16, 2019
@gvcgael
Copy link

gvcgael commented Dec 17, 2019

The second use case seems especially important, to be able to define variables in the state of the null_resource.

In our use case, the null_resource provisionner receives the IP of the VM throught its connection block. But now, since the connection block is built using variables, even if the destroy provisionner does not use any variable, it emits a deprecation warning.

@teamterraform
Copy link
Contributor

teamterraform commented Dec 21, 2019

Hi @shaunc,

You can already use triggers in null_resource as a place to retain data you need at destroy time:

resource "null_resource" "foo" {
  triggers {
    interpreter = var.local_exec_interpreter
  }
  provisioner {
    when = destroy

    interpreter = self.triggers.interpreter
    ...
  }
}

We don't intend to make an exception for referring to variables because within descendant modules a variable is just as likely to cause a dependency as anything else. While it is true that root module variables can never depend on anything else by definition, Terraform treats variables the same way in all modules in order to avoid a situation where a module would be invalid as a descendant module but not as a root module.

We'd be interested to hear what your use-case is for customizing the interpreter via a variable like that. Usually a provisioner's interpreter is fixed by whatever syntax the command itself is written in. It may be more practical to address whatever was causing you to do this in the first place and make it unnecessary to do so.

@teamterraform teamterraform changed the title destroy provisioners should be able to access variables Allow destroy-time provisioners to access variables Dec 21, 2019
@shaunc
Copy link
Author

shaunc commented Dec 21, 2019

Thanks for the explanation. The interpreter is customized just because I was contributing to terraform-aws-eks-cluster -- you'd have to ask them about the design; I do imagine that there are many more uses for variables in when=destroy provisioners, though.

However, I think that using them in triggers may be exactly what I meant when I talked about "two passes". I'm curious in your implementation why referring to variables via triggers doesn't cause loops, while referring to them directly does. And whether direct reference could just be syntactic sugar for reference via trigger. Is there a declarative semantic difference between the two?

@rjhornsby
Copy link

I think I'm running into a similar situation for the same reasons, though perhaps more mundane. @shaunc's proposals would seem to have merit. Like him, I also understand the concerns around possibly unknown state at destroy-time.

While the documentation currently indicates this should be possible, I'm getting the deprecation warning for the connection block when setting the ssh private key from a variable:

  connection {
    host        = coalesce(self.public_ip, self.private_ip)
    type        = "ssh"
    user        = "ec2-user"
    private_key = file(var.u_aws_keypath)
  }

This is problematic because the private key file isn't stored locally in the same place on my local mac as, say, my co-worker's windows laptop. This crops up in a couple of other places for us with u_* user-supplied (ie from terraform.tfvars) variables. For example:

  provisioner "local-exec" {
    when    = destroy
    command = "knife node delete ${var.node_short_name}-${substr(self.id, 2, 6)} -c ${var.u_knife_rb} -y"
    on_failure = continue
  }

The -c /path/to/knife.rb file location, like the ssh private key, will vary. It doesn't affect the resource itself, but it is required to act sort of on behalf of the resource, or in the resource's name.

Relatedly, while not a user-supplied variable, var.node_short_name, can also not be specified here as a variable, per the deprecation. While it would technically be possible to hard-code the value of var.node_short_name - it strongly violates DRY and makes our Terraform code far less flexible.

To elaborate the example briefly, our vault cluster consists of AWS instances whose names all begin with vault- and end with a portion of the AWS instance ID - so vault-12345a, vault-bcdef0 etc. The value vault obviously will change from stack to stack, depending on the application, but the use of var.node_short_name ideally stays constant between modules and different portions of the TF code.

AFAIK, in none of these cases are we able to store these values in the aws_instance resource itself for use later at destroy-time via self.*. Even if we were, that only covers some of the issue.

Local environment configuration (ie the path to the private key or knife.rb) will vary. If I understand it correctly, that's the idea behind terraform.tfvars and --var-file=?

@bigdot
Copy link

bigdot commented Jan 7, 2020

My use case is granting permissions (injecting the connection key) using external variables when running terraform.
On destroy, i have some pre-destroy cleanup to do on the machine and some external services.

How would one accomplish that now ?

connection {
    type        = "ssh"
    user        = var.vm_admin_user
    private_key = var.ops_private_key
    host        = var.vm_ip_address
}

@marcelloromani
Copy link

marcelloromani commented Jan 9, 2020

It would be nice if the error message could be improved.

When you read:
Error: local-exec provisioner command must be a non-empty string
and your config looks like:

provisioner "local-exec" {
    command = "echo ${some_resource.some_attribute}"
}

it's not at all obvious that the root cause is the variable cannot be interpolated.

@sdickhoven
Copy link

sdickhoven commented Jan 17, 2020

You can already use triggers in null_resource as a place to retain data you need at destroy time

Unfortunately, those triggers also cause a new resource to be created if they change. If you have more complex idempotence requirements then this won't work.

In my case I compare data from a data source with my local config and derive a trigger value from that. E.g. make the trigger foo if they differ and bar if they're the same.

If the trigger value changes, then I need access to the data/config so I can do things.

If I were to place the actual data/config into the trigger block then my null_resource would get recreated when it's not necessary (which can be destructive).

I like the fact that terraform will isolate the destruction provisioner. But that does necessitate an additional block in the null_resource for storing values that should not trigger recreation of the null_resource.

@sdickhoven
Copy link

sdickhoven commented Jan 17, 2020

there's also the use case of:

provisioner "local-exec" {
  when    = destroy
  command = format("%s/scripts/tag.sh", path.module)
  ...

if i want to use a local-exec provisioner for a null_resource in a module i'll need access to path.module.

it's not really cool if all my resources get recreated if the path of the module changes (which is what would happen if i stored this value in the trigger config of the null_resource).

@dmrzzz
Copy link

dmrzzz commented Jan 17, 2020

@sdickhoven the path.module use case is addressed in the recently-closed #23675

@shaunc
Copy link
Author

shaunc commented Jan 17, 2020

I like the idea of a separate block of variables that don't trigger create on change, but are usable in destroy. path.module is just one of many possible things that might be needed; even if path.module is what you need, you often want it in combination with something else, and not referencing the computed value means your code isn't DRY.

@jdeluyck
Copy link

I'm having a similar issue with

locals {
  cmd1 = "aws cmd --profile ${var.account_profile} ..."
  cmd2 = "aws cmd --profile ${var.other_account_profile} ..."
}

resource "null_resource" "aws_create_vpc_asocciation_auth" {
  triggers = {
    vpc_id = var.app_vpc_id
  }

  provisioner "local-exec" {
    command = "${local.cmd1} && sleep 30"
  }

  provisioner "local-exec" {
    when    = destroy
    command = "${local.cmd2} && sleep 30"
  }
}

which also results in

reference attributes of the related resource, via 'self', 'count.index', or
'each.key'.

References to other resources during the destroy phase can cause dependency
cycles and interact poorly with create_before_destroy.

This wasn't the case with 0.11, and I'm hitting this now with 0.12 (upgrading atm)

@xanderflood
Copy link

I'm using a destroy provisioner to uninstall an application on a remote device, and the only way to provide connection details currently is to hard-code them.

The triggers workaround is not a proper workaround as it produces significantly different behavior. One solution would be to introduce a new meta-argument that has no effect for storing arbitrary data.

@xanderflood
Copy link

Also, would someone from the TF team mind providing an example of how this could produce a circular reference? It's not really clear to me why preventing direct use but allowing indirect use through triggers would change the range of possible dependency graphs - if there are any troublesome patterns, it seems like you could produce them just as easily going through triggers, no?

userhas404d added a commit to userhas404d/terraform-aws-ldap-maintainer-1 that referenced this issue Feb 4, 2020
Still seeing warnings in the create_layer module due to references to path.module.
This issue is currently being discussed here: hashicorp/terraform#23679
userhas404d added a commit to userhas404d/terraform-aws-ldap-maintainer-1 that referenced this issue Feb 4, 2020
Still seeing warnings in the create_layer module due to references to path.module.
This issue is currently being discussed here: hashicorp/terraform#23679
@adona9
Copy link

adona9 commented Feb 4, 2020

I have yet another use case where I get this warning and I can't tell how to avoid it. I have aws_instance, aws_ebs_volume and aws_volume_attachment. The instances are rebuilt frequently but the EBS volumes are kept. When the aws_volume_attachment is destroyed, which happens before the instance is destroyed, I need to run a command on the instance to stop cleanly the services that rely on the storage, or they'll crash badly and leave corrupted data. I was able to accomplish this with a destroy-time remote-exec provisioner on the aws_volume_attachment. But after upgrading to 0.12.20 I see this deprecation warning which I would like to address.

resource "aws_volume_attachment" "kafka-broker-data-encrypted" {
  count       = var.cluster_size
  device_name = var.encrypted_data_device
  instance_id = aws_instance.kafka-broker.*.id[count.index]
  volume_id   = aws_ebs_volume.kafka-data-encrypted.*.id[count.index]

  provisioner "remote-exec" {
    when = destroy
    connection {
      host        = aws_route53_record.kafka-broker.*.name[count.index]
      type        = "ssh"
      user        = "ec2-user"
      private_key = file("${path.root}/keys/${var.ssh_key_name}.pem")
      script_path = "/home/ec2-user/stop.sh"
    }
    script = "${path.module}/templates/stop.sh"
  }
}

@NeilW
Copy link

NeilW commented Feb 18, 2020

Our use case is bastion hosts/users for remote execs. They work fine for the forward provisioners, but not the destroy time provisioner.

https://github.com/brightbox/kubernetes-cluster/blob/21169f9c575316eda10340c95857904fcca89855/master/main.tf#L108

The bastion user is calculated from the creation of the bastion host with Terraform - which in turn depends upon which cloud operating system image the end user selects.

Again I can't see how to get around this without splitting the runs into two.

@Bowbaq
Copy link
Contributor

Bowbaq commented Feb 25, 2020

We are also running into something similar. Following @apparentlymart's suggestion in this thread I tried to rewrite the following problematic template:

resource "null_resource" "kops_delete_flag" {
  triggers = {
    cluster_delete = module.kops.should_recreate
  }

  provisioner "local-exec" {
    when = destroy

    command = <<CMD
    if kops get cluster "${local.cluster_domain_name}" --state "s3://${local.kops_state_bucket}" 2> /dev/null; then
      kops delete cluster --state "s3://${local.kops_state_bucket}" --name "${local.cluster_domain_name}" --yes
    fi
CMD
  }
}

into

resource "null_resource" "kops_delete_flag" {
  triggers = {
    cluster_delete = module.kops.should_recreate

    cluster_domain_name = local.cluster_domain_name
    kops_state_bucket = local.kops_state_bucket
  }

  provisioner "local-exec" {
    when = destroy

    command = <<CMD
    if kops get cluster "${self.triggers.cluster_domain_name}" --state "s3://${self.triggers.kops_state_bucket}" 2> /dev/null; then
      kops delete cluster --state "s3://${self.triggers.kops_state_bucket}" --name "${self.triggers.cluster_domain_name}" --yes
    fi
CMD
  }
}

This creates two problems:

  • Adding triggers causes the null_resource to be replaced, which would in turn cause the K8S cluster to get deleted, which is highly undesirable
  • Even if deleting the cluster was acceptable, terraform refuses to do it with the following error:
     null_resource.kops_delete_flag: Destroying... [id=2536969715559883194]    
    
     Error: 4 problems:    
    
     - Missing map element: This map does not have an element with the key "cluster_domain_name".
     - Missing map element: This map does not have an element with the key "kops_state_bucket".
     - Missing map element: This map does not have an element with the key "kops_state_bucket".
     - Missing map element: This map does not have an element with the key "cluster_domain_name".
    

@acdha
Copy link

acdha commented Feb 27, 2020

Here's a use-case for variable access: I need to work around #516 / https://support.hashicorp.com/hc/en-us/requests/19325 to avoid Terraform leaking the database master password into the state file.

Note for Hashicorp staff: All of this would be unnecessary if Terraform had the equivalent of CloudFormation's resolve feature or something similar to make aws_ssm_parameter safe to use. We've requested this repeatedly with our account reps. Is there another way we can get that taken seriously?

The solution I have was to store that value in SSM using KMS and have a local-exec provisioner populate the RDS instance as soon as it's created. This works well except that it's now dependent on that destroy provisioner to clean up the SSM parameter before deleting the KMS instance.

resource "aws_kms_key" "SharedSecrets" {
  …
  # See the note in the README for why we're generating the secret which this
  # key is used for outside of Terraform. The new password must comply with the
  # restrictions in the RDS documentation:
  # https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Limits.html

  provisioner "local-exec" {
    command = "./scripts/set-db-password-parameter ${aws_kms_key.SharedSecrets.key_id} ${local.db_password_ssm_parameter_name}"
  }

  provisioner "local-exec" {
    when    = destroy
    command = "aws ssm delete-parameter --name ${local.db_password_ssm_parameter_name}"
  }
}
resource "aws_db_instance" "postgres" {
…
  # This is the other side of the dance described in the README to avoid leaking
  # the real password into the Terraform state file. We set the DB master
  # password to a temporary value and change it immediately after creation. Note
  # that both this and the new password must comply with the AWS RDS policy
  # requirements:
  # https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Limits.html
  password = "rotatemerotatemerotateme"

  provisioner "local-exec" {
    command = "./scripts/set-db-password-from-ssm ${self.identifier} ${local.db_password_ssm_parameter_name}"
  }
}

@armbraggins
Copy link

I had the same problem as @Bowbaq. Presumably it's because the saved tfstate doesn't have an old value saved for the new trigger.
I was able to work around it by commenting out the delete provisioner, "deleting" the resource with terraform destroy --target=module.my_module.null_resource.my_name, actually deleting it by manually executing the local-exec command (in my case, kubectl delete), and then re-applying.
But this is both a pain, and doesn't solve the "Adding triggers causes the null_resource to be replaced" bit of the problem.

(In my case, the local variable I was referencing is based on ${path.root}, so it's possible I can rewrite to use the fix for #23675 instead.)

@sdesousa86
Copy link

sdesousa86 commented Mar 4, 2020

Hello,

Here is my use case:
I'm using a python script in order to empty an S3 bucket containing thousands of objects ("force_destroy = true" option of the aws_s3_bucket Terraform resource is too slow: more than 2h vs ~1min with python script)

locals {
  # Sanitize a resource name prefix:
  resource_name_prefix = replace(replace("${var.product_name}-${terraform.workspace}", "_", ""), " ", "")
  # data bucket name construction:
  data_bucket_name = "${local.resource_name_prefix}-${var.region}-data"
  tags = {
    "Environment" = terraform.workspace
    "Product"     = lower(var.product_name)
    "TechOwner"   = var.product_tech_owner_mail
    "Owner"       = var.product_owner_mail
  }
}

resource "aws_s3_bucket" "data" {
  bucket        = local.data_bucket_name
  force_destroy = true
  tags          = local.tags
  provisioner "local-exec" {
    when    = destroy
    command = "python ${path.module}/scripts/clean_bucket.py ${self.id} ${var.region} ${terraform.workspace}"
  }
}

I'm only referencing mandatory variables ( ${var.region} & ${terraform.workspace} ), so... Where is the cycle dependency risk ?

I have used this kind of stuff many time so far and never get a cycle dependency on it (creating/destroying infrastructure ten times a day).

Furthermore, is it really needed to set this kind of useful way of working "deprecated" ?
I'm ok with the "cycle dependency risk" warning (Cause, yes, it's just a nightmare to solve when it happens...), but, from my point of view, once you are aware about the risk, you can simply make required validation steps and validate the solution once you figured out if there was a real cycle dependency risk or not.

@cathode911
Copy link

Colleagues, is there any decision on the topic from terraform devs? We're facing pretty much the same issue when some cleanup is required before destroying a VM and we need to provide root password that is constantly being changed via PAM system

  connection {
    host                  = self.default_ip_address
    type                  = "ssh"
    user                  = "root"
    password              = var.vm_root_passwd
  }

I would be great to understand whether it's ever going to be fixed or it's a solid 'no'.

@ajchiarello
Copy link

@cathode911 I think they have settled on no; at the very least, it’s gone on long enough that I have had to implement a workaround months ago. Now I only use the destroy time provisioned to trigger an ansible playbook, which is capable of fetching the correct credentials and executing my cleanup. As a side benefit, it doesn’t put anything sensitive into the terraform state.

@rjhornsby
Copy link

@cathode911 like @ajchiarello our intent is to work around it. Once I can find the time, I'm going to implement an AWS Cloudwatch mechanism that will delete a node from chef when the instance is destroyed, rather than trying to make that call from Terraform to Chef in a destroy-time provisioner.

In your case, maybe pulling the password as a vault_generic_secret data object from vault would let your connection{} block use current information? More recent versions of TF allow you set things as "sensitive" which is supposed to help keep from having secrets written to the TF logs/state, I think?

A little bit outside of the direct question you're asking, perhaps you could avoid the situation by

  • disabling root logins entirely (at minimum root ssh login), standard security practice in and of itself. Instead use ssh keys for named users (people or service accounts), and sudo for privilege elevation when necessary
  • Devising a process that doesn't require logging into the target host, but rather use whatever data the host had to externally complete the teardown tasks - ie remove the host from DNS, chef, etc. To put it another way, an independent process that assumes the vm went down or was accidentally deleted, what would it take to clean up the orphaned objects.

Terraform's changes in this area of provisioners, both create- and destroy- time, have made us think harder about how we're handling things that live outside TF's control, like our chef CM. I know at least one of TF's ideas is to use Packer to make an image for everything, but that's not going to happen so we've had to look at other ways, in combination with TF, to manage our stuff.

@cathode911
Copy link

Thanks for the suggestions, colleagues!
I can tell that terraform is the first application that gradually makes my life harder instead making it easier with every new release =) For now we will settle with "semi-automated" approach on destroy, it happens quite rarely so it will suffice for now.

@karolinepauls
Copy link

karolinepauls commented Feb 5, 2021

@sdesousa86 This is a shell injection vulnerability via AWS tags.

In order to do it safely, you have to pass the variables in the environment. Please modify your solution, like:

  provisioner "local-exec" {
    when = destroy
    command = "python ${path.module}/scripts/clean_bucket.py -b ${self.id} -r \"$REGION\" -p \"$PROFILE\""
    environment = {
      REGION  = self.tags["app:Region"]
      PROFILE = self.tags["app:Profile"]
    }
  }

path.module is build-it and self.id is assumed to be clean since they come from the terraform state, though this may also be wrong, and may also benefit from being passed via the environment and quoted to prevent shell splitting.

Also tagging @baurmatt who might have taken advice.

@huangered
Copy link

Hi all,

I finally found a workaround for my use case (s3 bucket resource, where "triggers" block is not allowed), hope it will help some of you:

locals {
  # Sanitize a resource name prefix:
  resource_name_prefix = replace(replace("${var.product_name}-${terraform.workspace}", "_", ""), " ", "")
  tags = {
    "Environment" = terraform.workspace
    "Product"     = lower(var.product_name)
    "TechOwner"   = var.product_tech_owner_mail
    "Owner"       = var.product_owner_mail
  }
}

resource "aws_s3_bucket" "data" {
  bucket            = "${local.resource_name_prefix}-${var.region}-data"
  force_destroy = true
  tags = merge(local.tags, {
    "Name"         = local.incoming_data_bucket_name
    "app:Region" = var.region
    "app:Profile"  = var.aws_cli_profile
  })
  provisioner "local-exec" {
    when        = destroy
    command = "python ${path.module}/scripts/clean_bucket.py -b ${self.id} -r ${self.tags["app:Region"]} -p ${self.tags["app:Profile"]}"
  }
}

that is good idea.

@errordeveloper
Copy link

errordeveloper commented Mar 19, 2021

One option that I found convenient for this was is to store whatever I needed as an output, and call terraform output from the shells script that provisioner invokes.

P.S.: It appears that this method doesn't work with local state backend, it does work with kubernetes backend. With local backend I'm getting Output "<name>" not found.

@murthyanish
Copy link

murthyanish commented Apr 28, 2021

Hi all,

I'm having this same issue. It's something that takes 10 extra lines of code in the resource.

I've added this to the null_resource provider which is where I had faced the issue, however similar changes can be implemented appropriately for any resource.

This allows us to add an extra resource (I call non_triggers) that function identical to triggers, but do not force a recreate.

internal/provider/resource.go
Line 17: Added new operation to function return:

		Update: resourceUpdate,

Line 32: Added lines to schema:

			"non_triggers": {`
				Description: "A map of arbitrary strings that, when changed, will NOT force the null resource to be replaced.",
				Type:        schema.TypeMap,
				Optional:    true,
			},

Line 45: Added function:

func resourceUpdate(d *schema.ResourceData, meta interface{}) error {
	return resourceRead(d, meta)
}

You can find the changes for the same under the main branch in my fork at:
https://github.com/murthyanish/terraform-provider-null

Pls @ teamterraform add this functionality.

I've tested this working in general cases with a custom provider I created.
Unfortunately for my uses, I cannot currently use a custom provider, but I hope this helps anyone else who is stuck at this issue.

I fully understand other work might be necessary to add this or terraform wouldn't want to allow this for some reason, but it works.

@dmrzzz
Copy link

dmrzzz commented Jul 22, 2021

I recently realized in a different context that it is possible to ignore_changes on a particular trigger, which sometimes results in a useful workaround:

variable "foo" { default = "original_foo" }
variable "bar" { default = "original_bar" }

resource "null_resource" "a" {
  triggers = {
    foo = var.foo
    bar = var.bar
  }

  lifecycle {
    ignore_changes = [triggers["bar"]]
  }

  provisioner "local-exec" {
    when    = destroy
    command = "echo foo was ${self.triggers.foo} and bar was ${self.triggers.bar}"
  }
}

output "foo" {
  value = resource.null_resource.a.triggers.foo
}

output "bar" {
  value = resource.null_resource.a.triggers.bar
}

After applying once, changing bar to "new_bar" will not replace the null_resource (good), but it also does not update the stored value of self.triggers.bar (which might be okay for some use cases but unfortunate for others).

Later on, changing foo to "new_foo" does replace the null_resource, but the destroy-time provisioner for the old one still uses original_bar as well as original_foo (even though bar changed a while ago):

$ terraform apply
null_resource.a: Refreshing state... [id=6595475011890936325]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # null_resource.a must be replaced
-/+ resource "null_resource" "a" {
      ~ id       = "6595475011890936325" -> (known after apply)
      ~ triggers = { # forces replacement
          ~ "bar" = "original_bar" -> "new_bar"
          ~ "foo" = "original_foo" -> "new_foo"
        }
    }

Plan: 1 to add, 0 to change, 1 to destroy.

Changes to Outputs:
  ~ bar = "original_bar" -> "new_bar"
  ~ foo = "original_foo" -> "new_foo"

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

null_resource.a: Destroying... [id=6595475011890936325]
null_resource.a: Provisioning with 'local-exec'...
null_resource.a (local-exec): Executing: ["/bin/sh" "-c" "echo foo was original_foo and bar was original_bar"]
null_resource.a (local-exec): foo was original_foo and bar was original_bar
null_resource.a: Destruction complete after 0s
null_resource.a: Creating...
null_resource.a: Creation complete after 0s [id=1459100522563100161]

Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

Outputs:

bar = "new_bar"
foo = "new_foo"

The advantage I see of the proposed self.interpreter style attributes is that an intermediate apply would be able to update the stored value of self.interpreter without destroying the null_resource then and there.

@secureoptions
Copy link

I tweaked the suggestion in the last comment. In my use case, I just need to pass some constant vars to the destroy-provisioner. Here is what worked for me.

resource "null_resource" "a" {
  triggers = {
    invokes_me_everytime = uuid()
    foo                  = local.some_constant
    bar                  = local.some_other_constant
  }
 
 # You might need to add the following if this is a synchronous cleanup provisioner: 
 # depends_on = [<SOME RESOURCE THAT IS ULTIMATELY MAPPED TO MY CONSTANT VARS>] 

  provisioner "local-exec" {
    when    = destroy
    command = "/bin/bash some-script.sh"

    environment = {
      foo = self.triggers.foo
      bar = self.triggers.foo
    }
  }
}

Explanation

  • You have two constant variables in the triggers block, foo and bar. Once these values have been assigned once, they are not anticipated to change.
  • Because the variables are constant, I need something else to trigger the null resource with each terraform apply | destroy . That is where the uuid() function is handy. This will generate an arbitrary string each run.
  • The local-exec provisioner will run only during a terraform destroy. At this point, the provisioner will also have access to the constant variables via self.triggers.<VAR>

mogul added a commit to GSA/terraform-kubernetes-aws-load-balancer-controller that referenced this issue Oct 6, 2021
By referring to the trigger content, the environment and command do not directly refer to external resources. This makes the Terraform dependency resolution happy, because it knows about the lifetime of resources referenced in triggers. Credit for this solution goes to the post here:
hashicorp/terraform#23679 (comment)
mogul added a commit to GSA/terraform-kubernetes-aws-load-balancer-controller that referenced this issue Oct 6, 2021
By referring to the trigger content, the environment and command do not directly refer to external resources. This makes the Terraform dependency resolution happy, because it knows about the lifetime of resources referenced in triggers. Credit for this solution goes to the post here:
hashicorp/terraform#23679 (comment)
@loganmzz
Copy link

loganmzz commented Apr 6, 2022

Is there any reason while data are also not supported ? We fetch all our credentials from Google secrets (SSH keys, passwords, etc.) and need them to connect to remote machine/service.

I tried to add a local_sensitive_file to temporary stores such secrets and reuse path in connection block, however file() functions require file to exist (even during refresh).

What a mess for just preventing bad practices while highly restricting very basic use cases ...

@kundan-avaya
Copy link

@secureoptions , your solution works perfectly. and also it hides the sensitive variables from debug console too. 👍

@vainkop
Copy link

vainkop commented May 27, 2022

Still it's not possible to pass a variable to a NON null resource (triggers are not available) + when tags are not available (in my case it's helm_release resource).

I've implemented that in the following manner (see the command + ${each.key}):

variable "karpenter_provisioner_name" {
  type    = string
  default = "default"
}

resource "helm_release" "karpenter" {
  for_each         = toset(split(",", var.karpenter_provisioner_name))
  name             = "karpenter"
  chart            = "./templates/karpenter"
  namespace        = "karpenter"
  create_namespace = true
  values           = [data.template_file.karpenter.rendered]

  provisioner "local-exec" {
    when    = destroy
    command = "aws ec2 terminate-instances --instance-ids $(aws ec2 describe-instances --query 'Reservations[].Instances[].InstanceId' --filters 'Name=tag:Name,Values=karpenter.sh/provisioner-name/${each.key}' --output text)"
  }
}

resource "kubectl_manifest" "karpenter_provisioner" {
  yaml_body = <<-YAML
  apiVersion: karpenter.sh/v1alpha5
  kind: Provisioner
  metadata:
    name: ${var.karpenter_provisioner_name}
  spec:
    requirements:
...

Is there a better way to do that?

@Andr1500
Copy link

I tweaked the suggestion in the last comment. In my use case, I just need to pass some constant vars to the destroy-provisioner. Here is what worked for me.

resource "null_resource" "a" {
  triggers = {
    invokes_me_everytime = uuid()
    foo                  = local.some_constant
    bar                  = local.some_other_constant
  }
 
 # You might need to add the following if this is a synchronous cleanup provisioner: 
 # depends_on = [<SOME RESOURCE THAT IS ULTIMATELY MAPPED TO MY CONSTANT VARS>] 

  provisioner "local-exec" {
    when    = destroy
    command = "/bin/bash some-script.sh"

    environment = {
      foo = self.triggers.foo
      bar = self.triggers.foo
    }
  }
}

Explanation

  • You have two constant variables in the triggers block, foo and bar. Once these values have been assigned once, they are not anticipated to change.
  • Because the variables are constant, I need something else to trigger the null resource with each terraform apply | destroy . That is where the uuid() function is handy. This will generate an arbitrary string each run.
  • The local-exec provisioner will run only during a terraform destroy. At this point, the provisioner will also have access to the constant variables via self.triggers.<VAR>

@jbardin many thanks for your solution, it's really what I was looking for. I was looking for a solution for deregistering and deleting task definitions revisions in ECS after destroying my infra with terraform .

@sofib-form3
Copy link

Use case: remote server operations through terraform null_resource and a remote-exec provisioner via ssh connection. For this purpose there is a need for destroy-time provisioner which needs access to credentials. Right now this has following implications:

  • credentials for destroy-time provisioner need to be persisted in state on self, which is a security concern
  • rotation of credentials upon expiry renders provisioners useless

There should be a support for short-lived credentials in destroy-time provisioners as well 🙏
Until then destroy-time provisioners will not be usable for us.

@jwhite-ac
Copy link

I am trying to use a destroy-time provisioner within a module to kick off an ansible-playbook to run some clean up tasks when a vsphere virtual machine is destroyed - to do this I need to reference variables in the module to pass to the ansible-playbook command. I have now discovered this used to be possible but now is not, with many examples in this issue showing that it should not have been removed and that any issues arising from circular dependencies are the problem of the end-user to fix - removing the ability to reference variables makes what should be a simple task incredibly painful, because it can no longer be done within the module in my case, and must instead be done as a separate resource alongside each instance of the module (becuase we also cannot put provisioners inside module resources for some reason, which would have been better, if not ideal).

Please re-instate the ability to reference variables in destroy-time provisioners, it is bonkers that this has been removed when you have been presented with multiple cases that prove we need this functionality.

@law

This comment was marked as off-topic.

@jwhite-ac

This comment was marked as off-topic.

@jwhite-ac

This comment was marked as off-topic.

@hashicorp hashicorp locked as too heated and limited conversation to collaborators Feb 15, 2024
@jbardin
Copy link
Member

jbardin commented Feb 15, 2024

Sorry that the direction of this issue has remained unclear. As issues accumulate replies the important updates tend to get hidden. The removal of an existing ability was not undertaken lightly, and much consideration was put into making the decision. You can see the associated PRs and major issues resolved here (#24083) and here (#23252), with more background here (#23559). The assumption that users can avoid the problems on their own is flawed, because the errors this causes are not directly visible within the configuration. That last PR has a diagram of the simplest case which triggers a cycle; the addition of create_before_destroy, multiple providers with dependencies, and composition of the configuration across modules obfuscates things even further. This prevents consumers of shared modules from being able to depend on complicated sets of updates being reliably applied.

Since there is no version of this feature which can be arbitrarily composed within modules, or can be restricted in such a way that users don't find themselves with unresolvable states that can't be applied, it's unlikely that it will be reinstated as it was before.

Given that destroy provisioners are already inadequate for what users want in many cases, and provisioners in general are a tool of last resort, a more general solution is likely only going arise from a new workflow of some sort. The fact that some new workflow could be created in the future to handle this type of situation is primarily why the issue has remained open.

As this issue has gotten quite long, I'm going to lock it to reduce noise for anyone still following. New ideas can submitted separately and linked back here.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests