Search code examples
terraformhcl

Terraform how to use for_each for ssh_keys when creating multiple digitalocean droplets


Background

I am using the digitalocean_droplet resource. I would like to create multiple digitalocean droplets, each with their own SSH key.

The droplets will be created by reading locals variable named droplets at the top of the script for ease of modification, and each droplet must have its own SSH key. I am using the cloudposse/terraform-tls-ssh-key-pair module to create the key pair.

Problem

The digitalocean_droplet resource requires an array of IDs under the ssh_keys key. I know how to provide a single key here, but I am using for_each for both the module and for the digitalocean_ssh_key resource.

I don't know how to tell the digitalocean_droplet resource to, for ssh_keys, use multiple values from the digitalocean_ssh_key resource.

To use a single key, ssh_keys would look like this:

ssh_keys = [digitalocean_ssh_key.ssh_key.id]

To use multiple, I think that I need to tell ssh_keys to "use each of the values generated by the digitalocean_ssh_key resource". But I don't know how to do this.

I expect (and hope) this to simply be a lack of knowledge on HCL.

What I have tried

I have tried what I think should be telling ssh_keys to use each .id from the digitalocean_ssh_key resource:

ssh_keys = {
  for index, v in digitalocean_ssh_key.ssh_key:
    v => v.id
}

However this results in a cycle error on running terraform plan:

Error: Cycle: module.ssh_key_pair.output.private_key (expand), module.ssh_key_pair.var.chmod_command (expand), module.ssh_key_pair.null_resource.chmod, module.ssh_key_pair.output.public_key (expand), module.ssh_key_pair.var.private_key_extension (expand) [etc etc]

I have also tried using module.ssh_key_pair, but this does not provide me with an id, which is what ssh_keys requires.

Conclusion

What do I need to do to tell digitalocean_droplet to, for each of the configuration in my locals variable named "droplets", generate a new SSH key using my module and to assign that SSH key to each individual droplet?

Code

locals {
  // An example of a single droplet. I will add multiple here in the future.
  droplets = {
    "DROPLET_NAME_AS_KEY" : {
      image = "distro image here"
      size  = "droplet size here"
    }
  }
}

// For each droplet above, create a new digitalocean droplet
resource "digitalocean_droplet" "droplets" {
  for_each = local.droplets
  name     = each.key
  region   = "fra1"
  image    = each.value.image
  size     = each.value.size
  tags     = [each.key]
  // Next is my problem, what goes for ssh_keys??
  ssh_keys = ???
}

// Create a new key pair for each droplet.
module "ssh_key_pair" {
  for_each = digitalocean_droplet.droplets

  source              = "git::https://github.com/cloudposse/terraform-tls-ssh-key-pair.git?ref=master"
  ssh_public_key_path = "/users/me/.ssh"
  name                = "${each.value.name}"
}

// The link between the key pair resource and the ssh_key for digital ocean.
resource "digitalocean_ssh_key" "ssh_key" {
  for_each = module.ssh_key_pair

  name       = each.value.key_name
  public_key = each.value.public_key
}

Bonus question if the above gets solved:

I actually use variables for all the values in the locals array. These come from environment variables from docker-compose, as I'm running terraform in a container. Is there a simple way to provide an array in docker-compose which can become this array here without me having to update both the environment key in docker-compose.yml and this droplets array, and instead just update an array in docker-compose.yml?


Solution

  • You should use local.droplets in all three for_each loops to avoid cycles.

    You can then access module.ssh_key_pair module in digitalocean_ssh_key resource with module.ssh_key_pair[each.key].

    The ssh_keys field in digitalocean_droplet resource would be ssh_keys = [digitalocean_ssh_key.ssh_key[each.key].fingerprint].


    Regarding your bonus question - you could declare this variable:

    variable "droplets" {
      type = map(object({
        image = string
        size = string
      }))
    }
    

    And then in for_each use var.droplets instead of locals.droplets.

    You can use env variables to set Terraform variables, however:

    For readability, and to avoid the need to worry about shell escaping, we recommend always setting complex variable values via variable definitions files.

    See details here - https://developer.hashicorp.com/terraform/language/values/variables#assigning-values-to-root-module-variables.