Terraform: Trigger new resource when external file changes

Created on 25 Jan 2017  路  4Comments  路  Source: hashicorp/terraform

For context, see #8099, where I stumbled across @apparentlymart 's call for use cases. This bit in particular inspired me, as this is exactly the functionality I'm looking for:

It might work out conceptually simpler to generalize the triggers idea from null_resource or keepers from the random provider, so that it can be used on any resource

Generically speaking, I'd like to force a new resource when the contents of a file it depends on changes.

More specifically, say I have a compute instance resource with a Chef provisioner using a Policyfile to define its cookbooks, run-list, etc. In a test workflow I'd like to detect changes to policy JSON and force re-create the compute instances to ensure the new policy is effective on a clean slate (without disrupting other resources that aren't affected by the policy change).

resource "compute_instance" "my_policy" {
    ...
    provisioner "chef" {
        ...    
        use_policyfile = true
        policy_group = "staging"
        policy_name = "my_policy"
    }

    lifecycle {
        replace_on_change {
            policyfile = "${file("policies/my_policy.rb")}"
        }
    }
}

Consider another workflow, which is probably more common, where a simple shell script is sufficient for provisioning a compute instance and it is desirable to start with a fresh instance anytime the provisioning logic changes:

resource "compute_instance" "foo" {
    ...
    provisioner "file" {
        source = "setup.sh"
        destination = "/tmp/setup.sh"
    }
   provisioner "remote-exec" {
        inline = [
          "chmod +x /tmp/setup.sh",
          "/tmp/setup.sh"
        ]
    }

    lifecycle {
        replace_on_change {
            policyfile = "${file("setup.sh")}"
        }
    }
}

Currently, if the contents of setup.sh change, Terraform has no visibility into this and just leaves the instance be. Allowing users to hash the contents would make for easy re-provisioning without the need to care about the idempotence issues of just re-running the provisioner as discussed in #745 and elsewhere (assuming one is willing to take the hit on re-creating the resource from scratch).

config enhancement

Most helpful comment

@allandrick possibly I'm not understanding your use-case properly, but I think you can achieve what you need today using the triggers attribute:

resource "null_resource" "subnetwork-association-one-eleventy-one-one-bang1" {
  triggers = {
    policy_sha1 = "${sha1(file("subnet-policy.json"))}"
  }
  provisioner "local-exec" {
    command = "yes | gcloud beta compute networks subnets set-iam-policy one-eleventy-one-one-bang1 subnet-policy.json --region ${var.gcp-us-west} --project my-xpn-host-project"
  }
}

This issue is asking essentially for this triggers mechanism to be generalized to all resources, but since you happen to be using null_resource you can use the already-existing version of this built in to that resource.

All 4 comments

Retagging as enhancement. A good idea though.

I could really use this too - my use case is similar but related to GCP XPN (cross project networking) policy bindings (JSON). XPN subnet policy bindings are how XPN (host) subnetworks are shared to a project - right now, in the beta at least, you associate a service project with an XPN Host project with a single gcloud command, however, each subnetwork you want users in that service project to be able to use needs to be bound with a gcloud command and a JSON policy document (another gcloud command per network per binding).

Until that gets sorted out, we're binding all networks to each new service project's users' groups' as they come online - however, today that means I have to taint some 42 null_resource objects to get the gcloud command to run again - I'd much rather tag them with a dependency on a policy file like the above. E.g.:

resource "null_resource" "subnetwork-association-one-eleventy-one-one-bang1" {
  provisioner "local-exec" {
    command = "yes | gcloud beta compute networks subnets set-iam-policy one-eleventy-one-one-bang1 subnet-policy.json --region ${var.gcp-us-west} --project my-xpn-host-project"
  }
}

where subnet-policy.json is the file being updated each time a new service project is added:

{
  "bindings": [
    {
      "members": [
        "group:[email protected]",
        "group:[email protected]",
        "serviceAccount:[email protected]",
        "serviceAccount:[email protected]",
        "serviceAccount:[email protected]",

        "group:[email protected]",
        "group:[email protected]",
        "serviceAccount:[email protected]",
        "serviceAccount:[email protected]",
        "serviceAccount:[email protected]"

      ],
      "role": "roles/compute.networkUser"
    }
  ],
}

@allandrick possibly I'm not understanding your use-case properly, but I think you can achieve what you need today using the triggers attribute:

resource "null_resource" "subnetwork-association-one-eleventy-one-one-bang1" {
  triggers = {
    policy_sha1 = "${sha1(file("subnet-policy.json"))}"
  }
  provisioner "local-exec" {
    command = "yes | gcloud beta compute networks subnets set-iam-policy one-eleventy-one-one-bang1 subnet-policy.json --region ${var.gcp-us-west} --project my-xpn-host-project"
  }
}

This issue is asking essentially for this triggers mechanism to be generalized to all resources, but since you happen to be using null_resource you can use the already-existing version of this built in to that resource.

@apparentlymart thank you very much - that does the trick nicely!

I could have also used a data template_file resource with or without vars to pick up any changes that way... but your way is much more elegant!

Regards,

Leigh

Was this page helpful?
0 / 5 - 0 ratings