Loving terraform! How can I create dynamic user_data for my instances?
I'd like to create example1.org and example2.net with corresponding hostname set via cloud-config:
variable "hostnames" {
default = {
"0" = "example1.org"
"1" = "example2.net"
}
}
resource "template_file" "user_data" {
filename = "user_data.tpl"
vars {
hostname = "???" # What should be inserted here?
}
}
resource "aws_instance" "web" {
ami = "..."
instance_type = "..."
count = 2
user_data = "${template_file.user_data.rendered}"
}
user_data.tpl:
#cloud-config
hostname: "${hostname}"
manage_etc_hosts: true
Thanks and keep up the great work!
Good question!
I had to double check locally to be sure; this pattern should work:
variable "count" {
default = 2
}
variable "hostnames" {
default = {
"0" = "example1.org"
"1" = "example2.net"
}
}
resource "template_file" "user_data" {
// here we expand multiple template_files - the same number as we have instances
count = "${var.count}"
filename = "hello.tpl"
vars {
// that gives us access to use count.index to do the lookup
hostname = "${lookup(var.hostnames, count.index)}"
}
}
resource "aws_instance" "web" {
// ...
count = "${var.count}"
// here we link each web instance to the proper template_file
user_data = "${element(template_file.hello.*.rendered, count.index)}"
}
Going to close since I just tested this and it seems to do the trick. Feel free to reopen if that's not the case. :+1:
Edit: Found the issue: when using count we need to use the count index from the caller:
user_data = "${element(template_file.web_init.*.rendered, count.index)}"
@phinze was looking at the docs and doesn't look like count is a documented attribute of template_file.
https://www.terraform.io/docs/providers/template/r/file.html
Has something changed? With the following code I get an error when adding count
resource "template_file" "user_data" {
count = "${var.count}" // script throws error with count attribute
template = "hello.tpl"
vars {
// that gives us access to use count.index to do the lookup
hostname = "${lookup(var.hostnames, count.index)}"
}
}
@harlow - I think your strategy is how it was always supposed to work. Thanks for clarifying.
if the above example is changed to
variable "count" {
default = 2
}
resource "template_file" "user_data" {
// here we expand multiple template_files - the same number as we have instances
count = "${var.count}"
filename = "hello.tpl"
vars {
// that gives us access to use count.index to do the lookup
file-contents = ${file("assets/bla/filename-${count.index}.pem")}"
}
}
resource "aws_instance" "web" {
// ...
count = "${var.count}"
// here we link each web instance to the proper template_file
user_data = "${element(template_file.hello.*.rendered, count.index)}"
}
where i use the template count in test-file = "${file("assets/bla/filename-${count.index}.pem")}", then i get a circular dependency between template and instance back and forth, which doesnt make to much sense to me.
You can actually cascade template expansion quite nicely:
source aws_instance kafka_instance {
count = "${var.kafka_instance_count}"
user_data = "${element(data.template_file.kafka_user_data.*.rendered, count.index)}"
//...
}
data template_file kafka_ansible_requirements_file_content {
template = "${file("${path.module}/files/kafka_ansible_requirements.yml.tpl")}"
vars {
ansible_role_repository_base_url = "https://${data.terraform_remote_state.vpc.ansible_role_repository_s3_bucket_domain_name}"
}
}
data template_file kafka_ansible_playbook_file_content {
count = "${var.kafka_instance_count}"
template = "${file("${path.module}/files/kafka_ansible_playbook.yml.tpl")}"
vars {
hostname = "kafka0${count.index}.internal-service"
}
}
data template_file kafka_user_data {
count = "${var.kafka_instance_count}"
template = "${file("${path.module}/files/user_data.sh.tpl")}"
vars {
ansible_requirements_file_content = "${element(data.template_file.kafka_ansible_requirements_file_content.*.rendered, count.index)}"
ansible_playbook_file_content = "${element(data.template_file.kafka_ansible_playbook_file_content.*.rendered, count.index)}"
}
}
Solutions described seemed only work when hostnames count is fixed. When there's variable for hostnames passed in from cli how do we loop through them in template file without having to declare the N as a variable?
How about to use a pattern for the hostnames? It works in my project.
I use multiple patterns for different groups of servers (kubernetes controlplanes, etcd, worker etc.)
For each group a template_file block with settings specific to them.
hostname_pattern = "worker-node%02d"
The hostnames are than
hostname = format(var.hostname_pattern, count.index + 1)
Or if your template is a shell script you can configure the hostnames with
pattern like in example above and get the hostname using the shell command
$(hostname)
My experience is, its not always a good idea to "count" resources. It makes the scripts short
but is hard to handle sometimes. Making changes forces recreation of all affected ressources.
It's much tricky to do a kind of "rolling update" to apply the changes to the ressources one by one.
For example: Replacing all controlplane nodes in kubernetes at once kills the cluster.
If I replace them one by one, the kubernetes cluster remains intact.
My experience is, its not always a good idea to "count" resources. It makes the scripts short
but is hard to handle sometimes. Making changes forces recreation of all affected ressources.
For what it's worth, for_each is an available alternative that doesn't exhibit the same problems.
@seimic Any chance you could post a small self contained working example?
I'm trying to do exactly what you've posted - but I'm getting nowhere.
As is tradition - as soon as I posted the above I got it working.
Thanks @seimic for the hints!
Below is hopefully the important parts of the script I was working on.
variable "hostname_format" {
type = string
default = "jenkins-agent-%02d"
}
... snip ...
data "template_file" "user_data" {
template = "${file("${path.module}/cloud_init.cfg")}"
count = var.hosts
vars = {
# hostname_pattern = "jenkins-agent-%02d"
hostname = format("${var.hostname_format}", "${count.index + 1}")
}
}
... snip....
resource "libvirt_cloudinit_disk" "commoninit" {
count = var.hosts
name = "commoninit.iso"
user_data = data.template_file.user_data[count.index].rendered
network_config = data.template_file.network_config.rendered
pool = libvirt_pool.centos.name
}
I use the following syntax for the 'counted' part:
user_data = element(data.template_file.user_data.*.rendered, count.index)
Example with format:
variable "config" {
default = {
name_pattern = "server_%02d"
count = 3
}
}
data "template_file" "names" {
template = "Hostname: $${name}"
count = var.config.count
vars = {
name = format(var.config.name_pattern, count.index + 1)
}
}
output "config" {
value = data.template_file.names.*.rendered // All rendered templates
}
output "config1" {
value = element(data.template_file.names.*.rendered, 0) // First rendered template only
}
Hope it helps.
Most helpful comment
Good question!
I had to double check locally to be sure; this pattern should work:
Going to close since I just tested this and it seems to do the trick. Feel free to reopen if that's not the case. :+1: