I am trying to setup an AWS environment with 2 ec2 instances in a VPC that are configured to run a piece of software that requires a config file containing the IP address of the other ec2. To do this, I am creating the config file in a template that I am running to start the ec2 like this:
data "template_file" "init_relay" {
template = file("${path.module}/initRelay.tpl")
vars = {
port = var.node_communication_port
ip = module.block-producing-node.private_ip[0]
self_ip = module.relay-node.public_ip
}
}
module "relay-node" {
source = "terraform-aws-modules/ec2-instance/aws"
name = "relay-node"
ami = var.node_ami
key_name = "aws-keys"
user_data = data.template_file.init_relay.rendered
instance_type = var.instance_type
subnet_id = module.vpc.public_subnets[0]
vpc_security_group_ids = [module.relay_node_sg.this_security_group_id]
associate_public_ip_address = true
monitoring = true
root_block_device = [
{
volume_type = "gp2"
volume_size = 35
},
]
tags = {
Name = "Relay Node"
Environment = var.environment_tag
Version = var.pool_version
}
}
data "template_file" "init_block_producer" {
template = "${file("${path.module}/initBlockProducer.tpl")}"
vars = {
port = var.node_communication_port
ip = module.relay-node.private_ip
self_ip = module.block-producing-node.private_ip
}
}
module "block-producing-node" {
source = "terraform-aws-modules/ec2-instance/aws"
name = "block-producing-node"
ami = var.node_ami
key_name = "aws-keys"
user_data = data.template_file.init_block_producer.rendered
instance_type = var.instance_type
subnet_id = module.vpc.public_subnets[0]
vpc_security_group_ids = [module.block_producing_node_sg.this_security_group_id]
associate_public_ip_address = true
monitoring = true
root_block_device = [
{
volume_type = "gp2"
volume_size = 35
},
]
tags = {
Name = "Block Producing Node"
Environment = var.environment_tag
Version = var.pool_version
}
}
but that gives me a cyclic dependency error:
» terraform apply
Error: Cycle: module.relay-node.output.public_ip, module.block-producing-node.output.private_ip, data.template_file.init_relay, module.relay-node.var.user_data, module.relay-node.aws_instance.this, module.relay-node.output.private_ip, data.template_file.init_block_producer, module.block-producing-node.var.user_data, module.block-producing-node.aws_instance.this
To me that makes sense why I am getting this error because in order to generate the config file for one ec2, the other ec2 already needs to exist and have a ip address assigned to it. But I don't know how to do this in a way.
How do I reference the IP address of the other EC2 in the template file in a way that doesn't cause a cyclic dependency issue?
Generally-speaking, the user data of an EC2 instance cannot contain any of the IP addresses of the instance because the user data is submitted as part of launching the instance and cannot be changed after the instance is launched, and the IP address (unless you specify an explicit one when launching) is also assigned during instance launch, as part of creating the implied main network interface.
If you have only a single instance and it needs to know its own IP address then the easiest answer is for some software installed in your instance to ask the operating system which IP address has been assigned to the main network interface. The operating system already knows the IP address as part of configuring the interface using DHCP, and so there's no need to also pass it in via user data.
A more common problem, though, is when you have a set of instances that all need to talk to each other, such as to form some sort of cluster, and so they need the IP addresses of their fellows in addition to their own IP addresses. In that situation, there are broadly-speaking two approaches:
Arrange for Terraform to publish the IP addresses somewhere that will allow the software running in the instances to retrieve them after the instance has booted.
For example, you could publish the list in AWS SSM Parameter Store using
aws_ssm_parameterand then have the software in your instance retrieve it from there, or you could assign all of your instances into a VPC security group and then have the software in your instance query the VPC API to enumerate the IP addresses of all of the network interfaces that belong to that security group.All variants of this strategy have the problem that the software in your instances may start up before the IP address data is available or before it's complete. Therefore it's usually necessary to periodically poll whatever data source is providing the IP addresses in case new addresses appear. On the other hand, that capability also lends itself well to autoscaling systems where Terraform is not directly managing the instances.
This is the technique used by ElasticSearch EC2 Discovery, for example, looking for network interfaces belonging to a particular security group, or carrying specific tags, etc.
Reserve IP addresses for your instances ahead of creating them so that the addresses will be known before the instance is created.
When we create an
aws_instancewithout saying anything about network interfaces, the EC2 system implicitly creates a primary network interface and chooses a free IP address from whatever subnet the instance is bound to. However, you have the option to create your own network interfaces that are managed separately from the instances they are attached to, which both allows you to reserve a private IP address without creating an instance and allows a particular network interface to be detached from one instance and then connected to another, preserving the reserved IP address.aws_network_interfaceis the AWS provider resource type for creating an independently-managed network interface. For example:The
aws_network_interfaceresource type has aprivate_ipsattribute whose first element is equivalent to theprivate_ipattribute on anaws_instance, so you can refer toaws_network_interface.example.private_ips[0]to get the IP address that was assigned to the network interface when it was created, even though it's not yet attached to any EC2 instance.When you declare the
aws_instanceyou can include anetwork_interfaceblock to ask EC2 to attach the pre-existing network interface instead of creating a new one:Because the network interface is now a separate resource, you can use its attributes as part of the instance configuration. I showed only a single network interface and a single instance above in order to focus on the question as stated, but you could also use resource
for_eachorcounton both resources to create a set of instances and then useaws_network_interface.example[*].private_ips[0]to pass all of the IP addresses into youruser_datatemplate.A caveat with this approach is that because the network interfaces and instances are separate it is likely that a future change will cause an instance to be replaced without also replacing its associated network interface. That will mean that a new instance will be assigned the same IP address as an old one that was already a member of the cluster, which may be confusing to a system that uses IP addresses to uniquely identify cluster members. Whether that is important and what you'd need to do to accommodate it will depend on what software you are using to form the cluster.
This approach is also not really suitable for use with an autoscaling system, because it requires the number of assigned IP addresses to grow and shrink in accordance with the current number of instances, and for the existing instances to somehow become aware when another instance joins or leaves the cluster.