Terraform: Instance specific variable in template_file

Created on 1 Jun 2015  ยท  11Comments  ยท  Source: hashicorp/terraform

Back before the "template_file" resource existed, I used Heredoc for my instance user_data.

Specifically, I could pass the "self" variable, or use other instance specific data in the user_data templating.

eg.

resource "aws_instance" "openvpn" {
...
  user_data = <<EOF
#cloud-config 
hostname: ${self.name}
number: ${count.index}
EOF
}

With the template_file resource, it seems this is not possible?

It looks like I can only use variables outside the instance.

resource "template_file" "client_cloud_config" {
    filename = "../../modules/openstack/client_cloud_config.template"
    vars {
        global_var = "${var.global}"
        lookup_var = "${lookup(map,key)}"
        instance_specific = "${var.????????}"
    }
}

resource "openstack_compute_instance_v2" "openvpn" {
...
  user_data = "${template_file.client_cloud_config.rendered}"
}

What I would like is something like....

resource "openstack_compute_instance_v2" "openvpn" {
...
  user_data = "${template_file.client_cloud_config.rendered(instance_specific=${self.name})}"
}

At the moment, I'm thinking of wrapping the template_file function in a search/replace - to give me access to the instance specific attributes - but this does seem a bit hacky.

Anybody got any other ideas?

providetemplate question thinking

Most helpful comment

@phinze @james-masson not sure if it's helpful but my current workaround involves replace.

Resource:

{
resource "template_file" "userdata_packer" {
    filename = "userdata_packer.tpl"
    vars {
        chef_version = "${var.chef_version}"
        chef_host = "${var.chef_host}"
    }
}

Example userdata_packer.tpl:

#!/bin/bash -v

CHEF_VERSION=${chef_version}

NODE_NAME="#ROLE-$${EC2_INSTANCE_ID}"

cat >> /etc/chef/client.rb <<EOF
node_name "$${NODE_NAME}"
EOF

/usr/bin/chef-client -r "role[#ROLE]" -E #ENVIRONMENT

Then I just replace what I need:

resource "aws_launch_configuration" "fortytwo-default" {
   name = "fortytwo-default-${var.lc_uid}"
    image_id = "ami-XXXX"
    instance_type = "m1.small"
    iam_instance_profile = "${var.iam_profile}"
    key_name = "${var.key_name}"
    user_data = "${replace(replace(template_file.userdata_packer.rendered, "#ROLE", "default"), "#ENVIRONMENT", "fortytwo")}"
    security_groups = ["${aws_security_group.default.id}", "${aws_security_group.jumphost-clients.id}", "${aws_security_group.nat-clients.id}"]
    lifecycle { create_before_destroy = true }
}

It's not super pretty but it gets the job done for anything that I can't pass through template_file resource (dozen of LCs, so in my case only chef_version and chef_host are generic "enough").

My Go-fu is limited so I replace as many as X vars that I need to sed pretty much.

All 11 comments

Ah this is a good point. Will have to think about how best to address this.

@phinze @james-masson not sure if it's helpful but my current workaround involves replace.

Resource:

{
resource "template_file" "userdata_packer" {
    filename = "userdata_packer.tpl"
    vars {
        chef_version = "${var.chef_version}"
        chef_host = "${var.chef_host}"
    }
}

Example userdata_packer.tpl:

#!/bin/bash -v

CHEF_VERSION=${chef_version}

NODE_NAME="#ROLE-$${EC2_INSTANCE_ID}"

cat >> /etc/chef/client.rb <<EOF
node_name "$${NODE_NAME}"
EOF

/usr/bin/chef-client -r "role[#ROLE]" -E #ENVIRONMENT

Then I just replace what I need:

resource "aws_launch_configuration" "fortytwo-default" {
   name = "fortytwo-default-${var.lc_uid}"
    image_id = "ami-XXXX"
    instance_type = "m1.small"
    iam_instance_profile = "${var.iam_profile}"
    key_name = "${var.key_name}"
    user_data = "${replace(replace(template_file.userdata_packer.rendered, "#ROLE", "default"), "#ENVIRONMENT", "fortytwo")}"
    security_groups = ["${aws_security_group.default.id}", "${aws_security_group.jumphost-clients.id}", "${aws_security_group.nat-clients.id}"]
    lifecycle { create_before_destroy = true }
}

It's not super pretty but it gets the job done for anything that I can't pass through template_file resource (dozen of LCs, so in my case only chef_version and chef_host are generic "enough").

My Go-fu is limited so I replace as many as X vars that I need to sed pretty much.

I wonder if there have been any improvements here while I've been distracted by other things?

I currently have code like:

 user_data = "${replace("${replace("${template_file.client_cloud_config.rendered}", "REPLACE_HOSTNAME", "ms${lookup(var.two_digit_count, count.index)}")}", "REPLACE_ANSIBLE_PLAYBOOK", "mesos_slave")}"

... and it's pretty ugly.

Is there a better way of doing this now?

The replace() workaround is still the only thing that works today. To be honest I'm not sure what the best solution is here, since the nature of Terraform's model means that the template_file is processed before the instance is reached.

Best guess I can come up with given current core architecture would be a combination of an ignore_unknown_variables option for template file to allow variables to be lazily evaluated, and a template() interpolation function that would allow assignment of variables at call time.

#!/bin/bash -v

CHEF_VERSION=${chef_version}

NODE_NAME="#ROLE-$${EC2_INSTANCE_ID}"

cat >> /etc/chef/client.rb <<EOF
node_name "$${NODE_NAME}"
EOF

/usr/bin/chef-client -r "role[${role}]" -E ${environment}
resource "template_file" "userdata_packer" {
  filename = "userdata_packer.tpl"
  vars {   
    chef_version = "${var.chef_version}"
    chef_host = "${var.chef_host}"
  }
  ignore_unknown_vars = true
}
resource "aws_launch_configuration" "fortytwo-default" {
  # ...
  user_data = "${template(template_file.userdata_packer.rendered, "role=default", "environment=fortytwo")}" 
  # ...
}

Not sure how I feel about it. Comments welcome!

+1, to do this for my cloud-configs I just have different template_file resources for each cluster node "type" that use the same base cloud-config file like so:

resource "template_file" "general_cluster_node_cloud_config" {
  template = "${file("path/to/cloudconfig/cloud-config.yml")}"
  vars { host_type = "general" }
}

resource "template_file" "db_cluster_node_cloud_config" {
  template = "${file("path/to/cloudconfig/cloud-config.yml")}"
  vars { host_type = "database" }
}

But a better approach would be much appreciated, even if it was just syntactic sugar around this way of doing it.

@james-masson Here's an example that I believe does exactly what you were requesting in your initial question:
(Note: I have the hostname and number combined, but it should be simple enough to change to your preferred setup)

Set up the template_file from the cloud init template

resource "template_file" "client-config" {
  template = "${file("files/general-init.conf")}"
  count = "${var.client-count}"

  vars {
    index = "${count.index}"
    hostname = "client-${count.index}"
  }
}

Note: I don't recall what the index var is there for in my use case. In your example you would probably just want to rename it number, and it would then be available in your cloud init template.

the resource that uses the template_file


resource "aws_instance" "client" {
  ...
  user_data = "${element(template_file.client-config.*.rendered, count.index)}"

  count = 2
  ...
}

NOTE: My understanding of the above is that the .*. section treats the template file as a list of template files, with the same number of items as the count, and element then gets the desired version based on the current index of the count

The cloud inite template: general-init.conf

#cloud-config

hostname: ${hostname}

Nice solution @agbodike - I'll give it a try next time I'm building something. I never considered using count on the template itself!

The only downside I can see, is that I'd have to have a different template for every server class, as you're not really passing instance data to the template, you've just got a synchronised count.

@james-masson

Yes, it's not the ideal approach. I think it might be possible to make it more generic, but I didn't go any further as it suited my needs at the time. It would be nice if it was easier to pass the variables directly to the template, rather than have to use an interstitial step.

@agbodike's solution is the right one. Its slightly more elegant today with native lists/maps. This can still become better, but closing this since we have a solution. Things like parameterized configs and reusble config blocks are covered in other issues like #8616

Unfortunately in Terraform 0.11.2 and provider.template v1.0.0 @phinze solution no longer works :(

I'm going to lock this issue because it has been closed for _30 days_ โณ. This helps our maintainers find and focus on the active issues.

If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

Was this page helpful?
0 / 5 - 0 ratings