Search code examples
terraformhclterraform0.12+

How can I pass a comma separated array to a resource in terraform v0.12.0?


In the following code block I'm trying to pass an array of server names to the attributes_json block:

resource "aws_instance" "consul-server" {
    ami = var.consul-server
    instance_type = "t2.nano"
    key_name = var.aws_key_name
    iam_instance_profile = "dna_inst_mgmt"
    vpc_security_group_ids = [
        "${aws_security_group.yutani_consul.id}",
        "${aws_security_group.yutani_ssh.id}"
    ]
        subnet_id = "${aws_subnet.public_1_subnet_us_east_1c.id}"
        associate_public_ip_address = true
      tags = {
        Name = "consul-server${count.index}"
    }

    root_block_device {
        volume_size = "30"
        delete_on_termination = "true"
    }

    connection {
        type = "ssh"
        user = "chef"
        private_key = "${file("${var.aws_key_path}")}"
        timeout = "2m"
        agent = false
        host = self.public_ip
    }

   count = var.consul-server_count

   provisioner "chef" {
         attributes_json = <<-EOF
                {
                    "consul": {
                            "servers": ["${split(",",aws_instance.consul-server[count.index].id)}"]
                      }
                }
                EOF
        use_policyfile = true
        policy_name = "consul_server"
        policy_group = "aws_stage_enc"
        node_name       = "consul-server${count.index}"
        server_url      = var.chef_server_url
        recreate_client = true
        skip_install = true
        user_name       = var.chef_username
        user_key        = "${file("${var.chef_user_key}")}"
       version         = "14"
    }
   }

Running this gives me an error:

Error: Cycle: aws_instance.consul-server[1], aws_instance.consul-server[0]

(This is after declaring a count of 2 in a variable for var.consul-server_count)

Can anyone tell me what the proper way is to do this?


Solution

  • There are two issues here: (1) How to interpolate a comma-separated list in a JSON string ; and (2) What is causing the cyclic dependency error.

    How to interpolate a list to make a valid JSON array

    Use jsonencode

    The cleanest method is to not use a heredoc at all and just use the jsonencode function.

    You could do this:

    locals {
      arr = ["host1", "host2", "host3"]
    }
    
    output "test" {
      value = jsonencode(
        {
          "consul" = {
            "servers" = local.arr
          }
        })
    }
    

    And this yields as output:

    Outputs:
    
    test = {"consul":{"servers":["host1","host2","host3"]}}
    

    Use the join function and a heredoc

    The Chef provisioner's docs suggest to use a heredoc for the JSON string, so you can also do this:

    locals {
      arr = ["host1", "host2", "host3"]
      sep = "\", \""
    }
    
    output "test" {
      value = <<-EOF
        {
          "consul": {
            "servers": ["${join(local.sep, local.arr)}"]
          }
        }
      EOF
    }
    

    If I apply that:

    Outputs:
    
    test = {
      "consul": {
        "servers": ["host1", "host2", "host3"]
      }
    }
    

    Some things to pay attention to here:

    • You are trying to join your hosts so that they become valid JSON in the context of a JSON array. You need to join them with ",", not just a comma. That's why I've defined a local variable sep = "\", \"".

    • You seem to be trying to split there when you apparently need join.

    Cyclic dependency issue

    The cause of the error message:

    Error: Cycle: aws_instance.consul-server[1], aws_instance.consul-server[0]
    

    Is that you have a cyclic dependency. Consider this simplified example:

    resource "aws_instance" "example" {
      count         = 3
      ami           = "ami-08589eca6dcc9b39c"
      instance_type = "t2.micro"
      user_data     = <<-EOF
        hosts="${join(",", aws_instance.example[count.index].id)}"
      EOF
    }
    

    Or you could use splat notation there too for the same result i.e. aws_instance.example.*.id.

    Terraform plan then yields:

    ▶ terraform012 plan 
    ...
    Error: Cycle: aws_instance.example[2], aws_instance.example[1], aws_instance.example[0]
    

    So you get a cycle error there because aws_instance.example.*.id depends on the aws_instance.example being created, so the resource depends on itself. In other words, you can't use a resources exported values inside the resource itself.

    What to do

    I don't know much about Consul, but all the same, I'm a bit confused tbh why you want the EC2 instance IDs in the servers field. Wouldn't the Consul config be expecting IP addresses or hostnames there?

    In any case, you probably need to calculate the host names yourself outside of this resource, either as a static input parameter or something that you can calculate somehow. And I imagine you'll end up with something like:

    variable "host_names" {
      type    = list
      default = ["myhost1"]
    }
    
    resource "aws_instance" "consul_server" {
      ...
      provisioner "chef" {
        attributes_json = jsonencode(
          {
            "consul" = {
              "servers" = var.host_names
            }
          })
      }
    }