Making API calls from Terraform
Tags: terraform
Published: 2021-05-18 11:37:03

[Terraform](https://www.terraform.io/) is a powerful and intuitive tool for managing [infrastructure as code](https://en.wikipedia.org/wiki/Infrastructure_as_code).  It has excellent community support with a wide range of available [providers](https://www.terraform.io/docs/language/providers/index.html) from their [registry](https://registry.terraform.io/browse/providers).  That said, sometimes a provider may not be up-to-date with the API they interact with, meaning that we can't provision the infrastructure as we like without manual intervention.  

One stopgap (until the provider releases an update) is to call the API yourself from Terraform.

The easiest way to do this is to call a script using [null-resource](https://www.terraform.io/docs/language/resources/provisioners/null_resource.html) and [local-exec](https://www.terraform.io/docs/language/resources/provisioners/local-exec.html)

```hcl
resource "null_resource" "update-my-resource" {
  provisioner "local-exec" {
    command = "node modules/myProvider/do-update.js ${provider_module.my-resource.id}"

    environment = {
      CLIENTID     = var.client_id
      CLIENTSECRET = var.client_secret
    }
  }
}
```
We won't go into the details of the (Node.js) script referenced in the above example, but the script effectively invokes an API and manages the response.  In the above example it takes an argument, the ID of a resource previously created.

Unfortunately when working with [Terraform Cloud](https://www.terraform.io/docs/cloud/index.html) Node.js and other script runners aren't available without installation. 

Thankfully we can achieve the same using `curl` and a few Terraform helpers.

The example below demonstrates a multiple step flow, in this case a client credential flow.

Once again, we can use `null_resource` and `local-exec`, but this time we'll call out to the API using `curl`. We'll use `triggers` to specify an arbitrary set of values, that if changed will cause the resource to be replaced. The path defined in `filename` will be used to store the content of the API call for the following step.

```hcl
resource "null_resource" "get_token" {
  triggers = {
    filename = "${path.module}/_temp_token.txt"
  }
  provisioner "local-exec" {
    command = <<EOT
      curl -X POST https://${var.domain}/oauth/token \
        -H 'content-type: application/x-www-form-urlencoded' \
        -d 'audience=https://${var.domain}/api/&grant_type=client_credentials&client_id=${var.client_id}&client_secret=${var.client_secret}' \
        >${self.triggers.filename}
    EOT
  }
}
```
We can then use [local_file](https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/file) to create a reference to the files content. It'll also ensure that the provisioner completes before Terraform reads the file.

```hcl
data "local_file" "token" {
  filename = "${null_resource.get_token.triggers.filename}"
}
```

One we have the data from the first API call we can call the second.  This example uses [jsondecode](https://www.terraform.io/docs/language/functions/jsondecode.html) to extract an access token needed for the subsequent call.

```hcl
resource "null_resource" "update-resource" {
  provisioner "local-exec" {
    command = <<EOT
      curl -X PATCH https://${var.domain}/api/thing/${provider_module.my-resource.id} \
      -H "Authorization: Bearer ${jsondecode(data.local_file.token.content).access_token}" \
      -H "Content-Type: application/json" \
      -d '{"foo":"bar"}' 
    EOT
  }
}
```

This is a "hack" to get around what is hopefully a short-term limitation, but it's useful to know that these options are available.