How to create if statement on arguments of the resource? - amazon-web-services

I have for loop that creates 2 ec2s on aws. I want to pass user_data argument on the only one of them, so my idea is to create if statement to accomplish this.
Something like this:
Instance EC2
resource "aws_instance" "web" {
count = length(var.vms)
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
key_name = var.key_name
get_password_data = false
associate_public_ip_address = true
vpc_security_group_ids = [var.secgr_id]
iam_instance_profile = aws_iam_instance_profile.ec2_instance_profile.name
user_data = var.vms[count.index] == "some-vm-name" ? "${file(var.file_name)}" : null
tags = {
Name = var.vms[count.index]
}
lifecycle {
prevent_destroy = true
}
}
It actually choses VM that i want, but the script is never executed that i pass through file function?
Is this possible to do?

Related

Terraform multiple instances but by separate execution

I am trying to create AWS instances with load balancer, security group and three instances ---> GROUP 1
I can do this by declaring appropriate resources.
Now I want to create multiples of such instances which are independent of previous instances ---> GROUP 2
I want this because of security of the GROUPS that one group's information should not overlap with other's.
I tried to look up a lot, but couldn't find an approach.
Below is an example of instance:
resource "aws_instance" "node" {
ami = data.aws_ami.ubuntu.id
subnet_id = aws_subnet.development-private-1a.id
key_name = aws_key_pair.nodes.key_name
instance_type = var.instance_type
vpc_security_group_ids = [aws_security_group.dev-ec2-sg.id]
tags = {
Name = "${var.app_name}"
#Environment = "production"
}
root_block_device {
volume_type = "gp2"
volume_size = 8
delete_on_termination = true
}
user_data = file("install_apache.sh")
}
resource "aws_lb_target_group_attachment" "node" {
target_group_arn = aws_lb_target_group.dev.arn
target_id = aws_instance.node.id
port = 80
}
I want to add multiple of these instances with different security groups and load balancers and all other stuff. but I dont want to add additional copies of the same in the terraform file. I want that those instances are independent of this one but then the problem I am facing is that terraform manipulates this instance only.
Based on the comments, you could consider organization of your instance code and its dependents (e.g. target group attachment) as terraform (TF) modules. Also since you are wish to create multiple instance of the same type, you could consider using aws_autoscaling_group which would allow you to not only easily create multiple instance but also easily to manage them.
Subsequently, you could define a module as followed. Below is only partial example. I also do not use aws_autoscaling_group, but create multiple instance using count:
./module/ec2/main.tf
variable "subnet_id" {}
variable "app_name" {}
variable "key_pair" {}
variable "security_group_id" {}
variable "target_group_arn" {}
variable "instance_count" {
default = 1
}
data "aws_ami" "ubuntu" {
# ...
}
resource "aws_instance" "node" {
count = var.instance_count
ami = data.aws_ami.ubuntu.id
subnet_id = var.subnet_id
key_name = var.key_pair
instance_type = var.instance_type
vpc_security_group_ids = [var.security_group_id]
tags = {
Name = "${var.app_name}"
#Environment = "production"
}
root_block_device {
volume_type = "gp2"
volume_size = 8
delete_on_termination = true
}
user_data = file("install_apache.sh")
}
resource "aws_lb_target_group_attachment" "node" {
count = var.instance_count
target_group_arn = var.target_group_arn
target_id = aws_instance.node[count.index].id
port = 80
}
# some outputs skipped
Having such module, in your parent file/module you would create GROUP 1 and 2 instance as follows (again, just partial example):
./main.tf
# resoruces such as LB, SGs, subnets, etc.
module "group1" {
source = "./module/ec2/"
instance_count = 3
security_group_id = <security-group-id1>
target_group_arn = aws_lb_target_group.dev.arn
# other parameters
}
module "group2" {
source = "./module/ec2/"
instance_count = 3
security_group_id = <security-group-id2>
target_group_arn = aws_lb_target_group.dev.arn
# other parameters
}

How to create ansible inventory from terraform?

I relay stack with that simple question.
Assume i need create few instance resources so how can i iterate from from tf variables to gather all private ips and pass it to ansible inventory file.
As i found i have to use * like here:
k8s_master_name = "${join("\n", azurerm_virtual_machine.k8s-master.*.name)}"
But i as think for me it will look like:
inst_ip = "${join("\n", ${aws_instance.*.private_ip})}"
But i got error:
Error: Invalid reference
on crc.cloud.connect.tf line 72, in resource "local_file" "servers1":
72: inst_ip = "${join("\n", aws_instance.*.private_ip)}"
A reference to a resource type must be followed by at least one attribute
access, specifying the resource name.
Full tf file:
resource "aws_instance" "sp-1" {
ami = "cmi-993E674A"
instance_type = "c5.large"
monitoring = true
source_dest_check = false
user_data = file("user_data.sh")
subnet_id = "subnet-F6C45280"
private_ip = "172.31.16.18"
vpc_security_group_ids = ["sg-230C7615"]
key_name = "mmk-key"
#network_interface {
# network_interface_id = "${aws_network_interface.ni-sp-1.id}"
# device_index = 0
#}
tags = {
desc = "sp-1"
group_name = "sp"
}
}
resource "aws_instance" "sp-2" {
ami = "cmi-993E674A"
instance_type = "c5.large"
monitoring = true
source_dest_check = false
user_data = file("user_data.sh")
subnet_id = "subnet-F6C45280"
private_ip = "172.31.16.19"
vpc_security_group_ids = ["sg-230C7615"]
key_name = "mmk-key"
tags = {
desc = "sp-2"
group_name = "sp"
}
}
resource "local_file" "servers1" {
content = templatefile("${path.module}/templates/servers1.tpl",
{
inst_ip = "${join("\n", ${aws_instance.*.private_ip})}"
}
)
filename = "../ansible/inventory/servers1"
}
Per the Terraform documentation, you need to reference the resource type and its associated name.
In your configuration file, you have an aws_instance resource with the name sp-1. If you wish to access the private_ip attribute of the resource, you need to do it like so: aws_instance.sp-1[*].private_ip.
You are creating a single instance aws_instance.sp-1, not multiple instances. To create multiple instance you would need to use count or for_each, or provision instances through aws_autoscaling_group.
Therefore, to access private_ip you don't really need splat * and join in your case (but still can use them if you want) as you have only one instance and will have only one private_ip. The following should be enough instead:
inst_ip = aws_instance.sp-1.private_ip

Terraform looping with for_each

How would one get the subnet id if you're using for_each as opposed to count?
In my case, I'm doing something like this
resource "aws_instance" "k8s" {
for_each = var.profiles
ami = data.aws_ami.latest-ubuntu.id
instance_type = "t2.medium"
iam_instance_profile = "${each.value}"
subnet_id = ??????????????
vpc_security_group_ids = [var.security_group]
key_name = var.keyname
connection {
type = "ssh"
host = self.public_ip
user = "ubuntu"
private_key = file(var.private_key_path)
}
tags = {
Name = "${each.key}"
}
}
And this is because I'm creating similar instances but need to assign them different instance profiles.
Ideally, I'd have done something like
subnet_id = element(var.subnets, count.index )
to place the instances in different subnets but I don't think count and for_each can be used in the same block definition.
I have to subnets and 4 instances and would like to loop through the subnets, placing each instance in one.
Any ideas, please?
Thanks.
If var.profiles is a list, you can use each.key to get the index of each element.
A good general strategy with resource for_each is to design the data structure you pass it so that each.value contains all of the per-instance data you need inside the resource block.
In this case, that means that the for_each expression for your aws_instance resource would be a map of objects where each object has both an instance profile and a subnet id.
One way to achieve that would be to write a for expression that transforms var.profiles (which is presumably a set(string) value) into a map of objects that would get the result you want. For example:
resource "aws_instance" "k8s" {
for_each = {
# This assigns a subnet to each of the profiles
# by first sorting them by name to produce a list
# and then selecting subnets based on the order
# of the sort result.
for i, profile_name in sort(var.profiles) : profile_name => {
iam_instance_profile = profile_name
subnet_id = element(var.subnets, i)
}
}
ami = data.aws_ami.latest-ubuntu.id
instance_type = "t2.medium"
iam_instance_profile = each.value.iam_instance_profile
subnet_id = each.value.subnet_id
vpc_security_group_ids = [var.security_group]
key_name = var.keyname
}
Using count.index with the element element relied on each item having its own index, but that isn't the case for a set of strings so in the above I used sort to convert to a list under the assumption that for this situation it doesn't really matter which subnet is assigned to each instance as long as the instances end up roughly evenly distributed between the subnets.
However, there is a big implication of that to keep in mind: if you add a new item to var.profiles later then it may cause the subnet_id for existing instances to be reassigned, and would thus require those instances to be recreated. If you don't want that to be true then you'll need to make the selection of subnet per profile more explicit somehow, which could be done by making var.profiles be a list(string) instead of a set(string) and then documenting that new profiles should only be added to the end of that list, or alternatively you could move the decision up into the caller of your module by making var.profiles itself a map of objects where the caller would then specify one subnet per profile:
variable "profiles" {
type = map(object({
subnet_id = string
}))
}
In that case, your resource block would become simpler because the variable value would already be of a suitable shape:
resource "aws_instance" "k8s" {
for_each = var.profiles
ami = data.aws_ami.latest-ubuntu.id
instance_type = "t2.medium"
iam_instance_profile = each.key
subnet_id = each.value.subnet_id
vpc_security_group_ids = [var.security_group]
key_name = var.keyname
}
you can provide you profiles as a list of objects :
for exemple :
variable "profiles" {
type = list(object({
name = string
key = string
}))
default =
[
{name = "exemple" , key = "exemplekey" },
{name = "exemple2" , key = "exemplekey2" }
]
}
like this the each.key will containes the index of the element in the list and each.value will contain the object {name,key}
so your code would be like this
resource "aws_instance" "k8s" {
for_each = var.profiles
ami = data.aws_ami.latest-ubuntu.id
instance_type = "t2.medium"
iam_instance_profile = each.value
subnet_id = element(var.subnets , each.key)
vpc_security_group_ids = [var.security_group]
key_name = var.keyname
connection {
type = "ssh"
host = self.public_ip
user = "ubuntu"
private_key = file(var.private_key_path)
}
tags = {
Name = each.value.key
}
}

Handling variable not found in Terraform

I have a two Launch Config creation resources in Terraform: one for spot pricing and one for reserve pricing - with the choice on which to use based on a "use_spot_pricing" boolean variable. I need to return the Launch Config ID from whichever resource was used. The problem is that the conditional blows up saying the Launch Config ID was not found for the resource that wasn't created.
My code looks like:
resource "aws_launch_configuration" "launch_config_reserved_pricing" {
// If use_spot_pricing is true (which translates to 1), this resource is not created (i.e. count = 0).
count = "${1 - var.use_spot_pricing}"
name_prefix = "${var.resource_name_prefix}${var.envSuffix}-"
image_id = "${var.generic_ami_id}"
instance_type = "${var.instance_type}"
key_name = "${var.key_name}"
security_groups = ["${var.vpc_security_group_ids}"]
iam_instance_profile = "${var.iam_instance_profile}"
user_data = "${data.template_file.lc_user_data.rendered}"
}
resource "aws_launch_configuration" "launch_config_spot_pricing" {
// If use_spot_pricing is true (which translates to 1), this resource is created once. Otherwise the previous one is.
count = "${var.use_spot_pricing}"
name_prefix = "${var.resource_name_prefix}${var.envSuffix}-"
image_id = "${var.generic_ami_id}"
instance_type = "${var.instance_type}"
key_name = "${var.key_name}"
security_groups = ["${var.vpc_security_group_ids}"]
iam_instance_profile = "${var.iam_instance_profile}"
user_data = "${data.template_file.lc_user_data.rendered}"
spot_price = "${var.spot_price}"
}
output "launch_config_id" {
value = "${ var.use_spot_pricing == true ? aws_launch_configuration.launch_config_spot_pricing.id : aws_launch_configuration.launch_config_reserved_pricing.id }"
}
This results in the errors (the first when I the spot pricing resource is used and the second when the reserve pricing resource is used):
* module.create_launch_configs.module.parser.output.launch_config_id: Resource 'aws_launch_configuration.launch_config_reserved_pricing' not found for variable 'aws_launch_configuration.launch_config_reserved_pricing.id'
* module.create_launch_configs.module.filter.output.launch_config_id: Resource 'aws_launch_configuration.launch_config_spot_pricing' not found for variable 'aws_launch_configuration.launch_config_spot_pricing.id'
This work around failed:
output "launch_config_id" {
value = "${coalesce(aws_launch_configuration.launch_config_spot_pricing.id , aws_launch_configuration.launch_config_reserved_pricing.id ) }"
}
This work around failed too:
output "launch_config_id" {
value = "${coalesce( join( "" , aws_launch_configuration.launch_config_spot_pricing.id ) , join( "" , aws_launch_configuration.launch_config_reserved_pricing.id ) ) }"
}
Also tried using 1 instead of true, no luck:
output "launch_config_id" {
value = "${ var.use_spot_pricing == 1 ? aws_launch_configuration.launch_config_spot_pricing.id : aws_launch_configuration.launch_config_reserved_pricing.id }"
}
In case anyone from Hashicorp is reading, a conditional shouldn't fail if the value not selected is undefined. It doesn't need to be examined, only the value for the condition that passes.
While it can be annoying that Terraform won't shortcut and not evaluate the false side of a conditional in this particular case you don't need it because you can just default the spot price to an empty string and then if it's not provided you get an on demand instance launch config.
So instead of having to do what you're currently doing instead you can just do:
variable "spot_price" {
default = ""
}
resource "aws_launch_configuration" "launch_config" {
name_prefix = "${var.resource_name_prefix}${var.envSuffix}-"
image_id = "${var.generic_ami_id}"
instance_type = "${var.instance_type}"
key_name = "${var.key_name}"
security_groups = ["${var.vpc_security_group_ids}"]
iam_instance_profile = "${var.iam_instance_profile}"
user_data = "${data.template_file.lc_user_data.rendered}"
spot_price = "${var.spot_price}"
}
output "launch_config_id" {
value = "${aws_launch_configuration.launch_config.id}"
}

Terraform: How to read the volume ID of one instance?

I create instances with a default CentOS 7 AMI. This AMI creates automatically a volume and attached to the instance. Is it possible to read thats volume ID using terraform? I create the instance using the next code:
resource "aws_instance" "DCOS-master3" {
ami = "${var.aws_centos_ami}"
availability_zone = "eu-west-1b"
instance_type = "t2.medium"
key_name = "${var.aws_key_name}"
security_groups = ["${aws_security_group.bastion.id}"]
associate_public_ip_address = true
private_ip = "10.0.0.13"
source_dest_check = false
subnet_id = "${aws_subnet.eu-west-1b-public.id}"
tags {
Name = "master3"
}
}
You won't be able to extract EBS details from aws_instance since it's AWS side that provides an EBS volume to the resource.
But you can define a EBS data source with some filter.
data "aws_ebs_volume" "ebs_volume" {
most_recent = true
filter {
name = "attachment.instance-id"
values = ["${aws_instance.DCOS-master3.id}"]
}
}
output "ebs_volume_id" {
value = "${data.aws_ebs_volume.ebs_volume.id}"
}
You can refer EBS filters here:
http://docs.aws.amazon.com/cli/latest/reference/ec2/describe-volumes.html
You can: aws_instance.DCOS-master3.root_block_device.0.volume_id
As described in Terraform docs:
For any root_block_device and ebs_block_device the volume_id is exported. e.g. aws_instance.web.root_block_device.0.volume_id
output "volume-id-C" {
description = "root volume-id"
#get the root volume id form the instance
value = element(tolist(data.aws_instance.DCOS-master3.root_block_device.*.volume_id),0)
}
output "volume-id-D" {
description = "ebs-volume-id"
#get the 1st esb volume id form the instance
value = element(tolist(data.aws_instance.DCOS-master3.ebs_block_device.*.volume_id),0)
}
You can get the volume name of an aws_instance like this:
output "instance" {
value = aws_instance.ec2_instance.volume_tags["Name"]
}
And you can set it as follows:
resource "aws_instance" "ec2_instance" {
ami = var.instance_ami
instance_type = var.instance_type
key_name = var.instance_key
...
tags = {
Name = "${var.server_name}_${var.instance_name[count.index]}"
}
volume_tags = {
Name = "local_${var.instance_name[count.index]}"
}
}