2

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?

BMW
  • 42,880
  • 12
  • 99
  • 116
TyMac
  • 783
  • 2
  • 9
  • 32

1 Answers1

1

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
        }
      })
  }
}
Alex Harvey
  • 14,494
  • 5
  • 61
  • 97
  • Thank you :) I was hoping I could make this list variable depending on the amount of instances I wanted to deploy. Would a nested loop work inside that locals definition to provide that? – TyMac May 26 '19 at 16:41
  • @TyMac. Ok. I see the problem there, you're getting a circular dependency because the AWS instance ID is only available after the provider has created the instances. I'll update my answer with some suggestions when I have a moment. – Alex Harvey May 27 '19 at 05:13
  • @TyMac, much expanded. Does that help? – Alex Harvey May 27 '19 at 12:45
  • 1
    Very much expanded thanks! The reason for the servers field sounds like what you're guessing at - Chef configures consul server's "retry_join" option with the list of instances. I am looking to consolidate the instance names with the hostnames, chef node names, and the DNS names so that the names in the "retry_join" field show up the same in every place, especially when running "consul members". Sounds like I should actually be using something like ".dns_name" instead of ".id". – TyMac May 27 '19 at 14:20
  • @TyMac, there's a `.public_dns` but it isn't going to help because you still can't refer to it inside the resource block. I think you'll need to tell Terraform the DNS names up front some way. – Alex Harvey May 27 '19 at 14:29
  • Looks like I need to base whatever I use (node name, instance name, dns ...etc) off of "count" to keep it dynamic however... otherwise it looks like I'd have to change the names in multiple places. Not sure how to do that with "count" however. – TyMac May 27 '19 at 19:20
  • @TyMac, does [this](https://stackoverflow.com/a/56146510/3787051) help? – Alex Harvey May 27 '19 at 21:12
  • Seems to be on the right path... getting an error however: "Error: Invalid template interpolation value: Cannot include the given value in a string template: string required." – TyMac May 27 '19 at 22:42
  • "servers": "\\\"${local.consul_json}\\\"" almost works after defining a locals block: consul_json = "${join(", ", var.consul_list)}" - have to do this since I'm actually passing this in to a json block... unfortunately it produces this: retry_join = \"consul-server01, consul-server02, consul-server03\" – TyMac May 28 '19 at 00:56