Creating a custom resource

Hi, I have been trying to write a module that is effectively implementing a custom resource without having to resort to looking into developing provisioners or providers.

What I have is not 100% working in that a second apply execution has an unwanted side effect.

I would appreciate it if anyone has a view as to why this approach falls over on a second apply execution.

The idea is to use local-exec and external data. There are two null_resources and one external data definition. One of the null_resources is responsible for creating the underlying resource, the second for destroying it. The external data definition enables obtaining information on the resource to use in other areas during the apply/destroy execution.

To ensure sequence of execution, the external data definition depends on the create null_resource, and the destroy null_resource depends on the external data definition.

This works fine for the normal cycle of apply, destroy etc. On apply the create null_resource fires, creating the underlying resource, then the external data fires, picking up the information from that resource, finally the destroy null_resource is created but the ‘when destroy’ provisioner does not fire (as expected). On Destroy only the ‘when destroy’ provisioner fires, removing the resource. All good so far.

On the double apply test, the second apply always fires the ‘when destroy’ provisioner. This removes the underlying resource and is the unwanted side effect. I can force the create null_resource to fire every time, but this simply means that the underlying resource is constantly removed and created on every execution.

My understanding is the the ‘when destroy’ clause should force Terraform not to execute on an apply phase but this is not the case.

My question really is; is this a bug or is my understanding not right, or is there a better way to achieve this?

Here is a distilled version of what I am doing, which simply creates a file, puts some json in it defining the file path, and then uses that data to set up the destruction of the resource.

Creating these files in the same directory and executing ‘terraform apply’ twice should exhibit the problem.

Version of Terraform used: v0.12.28

The terraform main.tf:
variable “file_path” {
type = string
default = “/tmp/test-custom-resource”
}

variable "force" {
  type = bool
  default = false
}

resource "null_resource" "create" {
  triggers = {
    always_run  = var.force ? timestamp() : "no"
  }
  provisioner "local-exec" {
    command = "${path.module}/create-script.sh ${var.file_path}"
  }
}

data "external" "data" {
  program = ["bash", "${path.module}/data-script.sh", var.file_path]
  depends_on = [null_resource.create]
}

resource "null_resource" "remove" {
  triggers = {
    file_path = data.external.data.result.data_file_path
  }
  provisioner "local-exec" {
    command = "echo 'CREATE DRESTRUCTOR:: Data: ${data.external.data.result.data_file_path}, Trigger file path: ${self.triggers.file_path}'"
  }
  provisioner "local-exec" {
    when    = destroy
    command = "${path.module}/remove-script.sh ${self.triggers.file_path}"
  }
}

output "data" {
  value = data.external.data.result
}

The create-script.sh:
#!/usr/bin/env bash
[[ $# -lt 1 ]] && { echo “need to pass the data file path”; exit 0; }

data_file_path=${1}

echo "PROVISIONING:: Data file path: ${data_file_path}"

[[ -f "${data_file_path}" ]] && { echo "${data_file_path} already exists"; return 0; }
echo '{"data_file_path": "'${data_file_path}'"}' > ${data_file_path}

The data-script.sh:
#!/usr/bin/env bash
[[ $# -lt 1 ]] && { echo “need to pass the data file path”; exit 0; }

file_path=$1
cat ${file_path}

The remove-script.sh
#!/usr/bin/env bash
[[ $# -lt 1 ]] && { echo “need to pass the trigger file path”; exit 0; }

trigger_file_path=${1}

echo "DESTROYING:: Trigger file path: ${trigger_file_path}"

[[ ! -f "${trigger_file_path}" ]] && { echo "${trigger_file_path} does not exist"; return 0; }
rm -f ${trigger_file_path}

That formatting did not come out as expected!

The terraform main.tf:

variable "file_path" {
  type = string
  default = "/tmp/test-custom-resource"
}

variable "force" {
  type = bool
  default = false
}

resource "null_resource" "create" {
  triggers = {
    always_run  = var.force ? timestamp() : "no"
  }
  provisioner "local-exec" {
    command = "${path.module}/create-script.sh ${var.file_path}"
  }
}

data "external" "data" {
  program = ["bash", "${path.module}/data-script.sh", var.file_path]
  depends_on = [null_resource.create]
}

resource "null_resource" "remove" {
  triggers = {
    file_path = data.external.data.result.data_file_path
  }
  provisioner "local-exec" {
    command = "echo 'CREATE DRESTRUCTOR:: Data: ${data.external.data.result.data_file_path}, Trigger file path: ${self.triggers.file_path}'"
  }
  provisioner "local-exec" {
    when    = destroy
    command = "${path.module}/remove-script.sh ${self.triggers.file_path}"
  }
}

output "data" {
  value = data.external.data.result
}

The create-script.sh:

#!/usr/bin/env bash
[[ $# -lt 1 ]] && { echo "need to pass the data file path"; exit 0; }

data_file_path=${1}

echo "PROVISIONING:: Data file path: ${data_file_path}"

[[ -f "${data_file_path}" ]] && { echo "${data_file_path} already exists"; return 0; }
echo '{"data_file_path": "'${data_file_path}'"}' > ${data_file_path}

The data-script.sh:

#!/usr/bin/env bash
[[ $# -lt 1 ]] && { echo "need to pass the data file path"; exit 0; }

file_path=$1
cat ${file_path}

The remove-script.sh:

#!/usr/bin/env bash
[[ $# -lt 1 ]] && { echo "need to pass the trigger file path"; exit 0; }

trigger_file_path=${1}

echo "DESTROYING:: Trigger file path: ${trigger_file_path}"

[[ ! -f "${trigger_file_path}" ]] && { echo "${trigger_file_path} does not exist"; return 0; }
rm -f ${trigger_file_path}