Handling variable not found in Terraform - amazon-web-services

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}"
}

Related

How to create if statement on arguments of the resource?

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?

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
}
}

Incorrect attribute value type error in terraform when passing ec2 instance ids

I am using below code to create 3 ec2 instances and register them with a load balancer,
variables
instance_type = "t3.large"
root_block_volume_type = "standard"
root_block_volume_size = "50"
instance_count = "3"
ec2 creation
resource "aws_instance" "ec2" {
count = "${var.instance_count}"
ami = "${var.ami_id}"
instance_type = "${var.instance_type}"
key_name = "${var.key_pair_name}"
subnet_id = "${var.private_subnet_id}"
iam_instance_profile = "${aws_iam_instance_profile.iam_instance_profile.name}"
/*
* CAUTION: changing value of below fields will cause the EC2 instance to be terminated and
* re-created. Think before running the "apply" command.
*/
associate_public_ip_address = false
tags = {
Environment = "${var.env}"
Project = "${var.project}"
Provisioner="different-box"
Name = "${local.name}-1"
}
root_block_device {
volume_type = "${var.root_block_volume_type}"
volume_size = "${var.root_block_volume_size}"
}
}
resource "aws_network_interface_sg_attachment" "sg_attachment" {
count = "${var.instance_count}"
security_group_id = "${aws_security_group.ec2_sg.id}"
network_interface_id = "${aws_instance.ec2[count.index].primary_network_interface_id}"
}
Registering with load balancer
resource "aws_alb_target_group_attachment" "alb_target_group_attachment" {
count = length("${var.ec2_instance_ids}")
target_group_arn = "${aws_alb_target_group.alb_target_group.arn}"
target_id = "${var.ec2_instance_ids[count.index]}"
port = "${var.alb_target_group_port}"
}
But however when I pass ec2 instance ids as below,
module "alb_engine-ui" {
source = "./modules/load-balancer"
env = "${lower(var.env)}"
project = "engine-ui"
vpc_id = "${data.aws_vpc.main.id}"
public_subnet1_id = "${var.public_subnet1_id}"
public_subnet2_id = "${var.public_subnet2_id}"
health_check_target_group_path = "/"
certificate_arn = "${var.certificate_arn}"
alb_target_group_port = "2016"
ec2_instance_ids = ["${aws_instance.ec2[*].id}"]
}
variable "ec2_instance_ids" {
description = "the ec2 instance ids to be used for alb target group"
type = "list"
}
I'm getting the below error,
Error: Incorrect attribute value type
on provisioners/different-box/modules/load-balancer/resources.tf line 109, in resource "aws_alb_target_group_attachment" "alb_target_group_attachment":
109: target_id = "${var.ec2_instance_ids[count.index]}"
Inappropriate value for attribute "target_id": string required.
Is there a way to avoid this error and pass ec2 ids to the list?
"${aws_instance.ec2[*].id}" is already a list of IDs so if you wrap it in square brackets it makes it a list of lists.
Removing the square brackets instead leaves it as a list of strings so when you index it you get a single string which is what the target_id parameter wants.

Terraform applying huge index value for instance EBS block store

I am using Terraform (called via Terragrunt, if that's relevant) to create an instance from an AMI and mount an existing volume:
resource "aws_instance" "jenkins_master_with_snap" {
count = "${var.master_with_snapshot}"
ami = "${var.jenkins_ami}"
instance_type = "${var.jenkins_instance_type}"
iam_instance_profile = "${data.terraform_remote_state.global.jenkins_profile_name}"
subnet_id = "${data.aws_subnet.jenkins_subnet_with_snap.id}"
key_name = "${var.key_name}"
vpc_security_group_ids = [
"${aws_security_group.jenkins_master_target_sg.id}",
"${data.terraform_remote_state.cicd.cicd_sg_ipa}"
]
ebs_block_device {
snapshot_id = "${var.master_snapshot_id}"
device_name = "${var.jenkins_volume_device}"
volume_type = "gp2"
}
}
It's worth noting that the AMI used to create this resource already has a snapshot mapped to it from the build process, so this resource basically just replaces it with a different snapshot. I'm not sure if this is why I'm having the problem or not.
I'm using the resulting resource attributes to populate a Python template that will be zipped and uploaded as a lambda function. The Python script requires the volume-id from this instance's EBS block device.
data "template_file" "ebs_backup_lambda_with_snapshot_template" {
count = "${var.master_with_snapshot}"
template = "${file("${path.module}/jenkins_lambda_ebs_backup.py.tpl")}"
vars {
volume_id = "${aws_instance.jenkins_master_with_snap.ebs_block_device.???.volume_id}"
}
}
Onto the actual problem: I do not know how to properly reference the volume ID in the vars section of the template_file resource above. Here is the resulting state:
ebs_block_device.# = 1
ebs_block_device.1440725774.delete_on_termination = true
ebs_block_device.1440725774.device_name = /dev/xvdf
ebs_block_device.1440725774.encrypted = true
ebs_block_device.1440725774.iops = 900
ebs_block_device.1440725774.snapshot_id = snap-1111111111111
ebs_block_device.1440725774.volume_id = vol-1111111111111
ebs_block_device.1440725774.volume_size = 300
ebs_block_device.1440725774.volume_type = gp2
ebs_optimized = false
root_block_device.# = 1
root_block_device.0.delete_on_termination = false
root_block_device.0.iops = 0
root_block_device.0.volume_id = vol-1111111111111
root_block_device.0.volume_size = 8
root_block_device.0.volume_type = standard
The problem is that the index for the EBS volume is that insane integer 1440725774. I have no idea why that is occuring. In the console, there's only a single map in the list I'm interested in:
> aws_instance.jenkins_master_with_snap.ebs_block_device
[
{ delete_on_termination = 1 device_name = /dev/xvdf encrypted = 1 iops = 900 snapshot_id = snap-1111111111111 volume_id = vol-1111111111111 volume_size = 300 volume_type = gp2}
]
And it appears the only way to reference any of those keys is to use that index value directly:
> aws_instance.jenkins_master_with_snap.ebs_block_device.1440725774.volume_id
vol-1111111111111
Is there any way to reliably reference a single element in a list like this when I have no idea what the index is going to be? I can't just hardcode that integer into the template_file resource above and assume it's going to be the same every time. Does anyone have any clues as to why this is occurring in the first place?
Perhaps instead of inlining ebs_block_device block, create a separate aws_ebs_volume resource, then attach it with an aws_volume_attachment. Then reference the aws_ebs_volume.name.id attribute to get the ID you need.
Example (extended from the example code in aws_volume_attachment):
resource "aws_volume_attachment" "ebs_att" {
device_name = "/dev/sdh"
volume_id = "${aws_ebs_volume.example.id}"
instance_id = "${aws_instance.web.id}"
}
resource "aws_instance" "web" {
ami = "ami-21f78e11"
availability_zone = "us-west-2a"
instance_type = "t1.micro"
tags {
Name = "HelloWorld"
}
subnet_id = "<REDACTED>"
}
resource "aws_ebs_volume" "example" {
availability_zone = "us-west-2a"
size = 1
}
data "template_file" "example" {
template = "Your volume ID is $${volume_id}"
vars {
volume_id = "${aws_ebs_volume.example.id}"
}
}
output "custom_template" {
value = "${data.template_file.example.rendered}"
}
The resultant output:
Outputs:
custom_template = Your volume ID is vol-0b1064d4ca6f89a15
You can then use ${aws_ebs_volume.example.id} in your template vars to populate your lambda.