Terraform - Variable inside a variable - terraform

I would like to use a variable inside a variable.
This is my resource:
resource "aws_route" "vpc_peering_accepter" {
provider = "aws.accepter"
count = length(data.terraform_remote_state.vpc.outputs.${var.region}-vpc-private_routing_tables)
route_table_id = tolist(data.terraform_remote_state.vpc.outputs.${var.region}-vpc-private_routing_tables)[count.index]
destination_cidr_block = var.vpc_cidr
vpc_peering_connection_id = aws_vpc_peering_connection.peer.*.id[0]
}
Of course this one is not working.
What's the best practice to do it?
Thanks,
Elad

You can combine Local Values with the lookup function to accomplish this.
In the following example the null datasource is mimicking data.terraform_remote_state.vpc.outputs:
variable "region" {
default = "us-east1"
}
locals {
vpc_private_routing_tables = "${var.region}-vpc-private_routing_tables"
}
data "null_data_source" "values" {
inputs = {
us-east1-vpc-private_routing_tables = "11111111"
us-east2-vpc-private_routing_tables = "22222222"
}
}
output "vpc_peering" {
value = lookup(data.null_data_source.values.inputs, local.vpc_private_routing_tables)
}

Because data.terraform_remote_state.vpc.outputs is a mapping, you can use either attribute syntax or index syntax to access the values inside:
Attribute syntax: data.terraform_remote_state.vpc.outputs.us-west-1-vpc-private_routing_tables
Index syntax: data.terraform_remote_state.vpc.outputs["us-west-1-vpc-private_routing_tables"]
An advantage of index syntax is that you can use any expression within those brackets as long as its result is a string. In particular, you can use the template interpolation syntax:
data.terraform_remote_state.vpc.outputs["${var.region}-vpc-private_routing_tables"]
With that said, in this sort of situation where you are producing the same information for a number of different objects -- regions, in this case -- it's more conventional to gather all of these values into a single mapping when you declare the output, so that these related values are explicitly grouped together in a single collection. For example:
output "vpc_private_routing_table_ids" {
value = {
us-east-1 = aws_route_table.us-east-1.id
us-west-2 = aws_route_table.us-west-2.id
}
}
Then from the perspective of the consumer -- that is, the module that is using data "terraform_remote_state" to access these outputs -- this appears as a simple map keyed by region:
data.terraform_remote_state.vpc.outputs.vpc_private_routing_table_ids[var.region]
If you are producing many different objects on a per-region basis then you might choose to gather all of their ids together into a single output, which might be more convenient to use elsewhere:
output "regions" {
value = {
us-east-1 = {
vpc_id = aws_vpc.us-east-1.id
subnet_ids = aws_subnet.us-east-1[*].id
private_route_table_id = aws_route_table.us-east-1.id
}
us-west-1 = {
vpc_id = aws_vpc.us-west-1.id
subnet_ids = aws_subnet.us-west-1[*].id
private_route_table_id = aws_route_table.us-west-1.id
}
}
}
...which would then appear as follows in the consumer module:
data.terraform_remote_state.vpc.outputs.regions[var.region].private_route_table_id
Ultimately you can structure your output values however you like, but I'd recommend choosing a shape that optimizes for clarity in the configuration that is referring to the data. That usually means making the referring expressions as simple as possible, and ideally avoiding complex expressions like string template syntax whenever possible.

Related

How to access a local using a variable in Terraform

I have the following code.
mymodule
variable "senses" {
type = string
}
locals {
sounds = {
"cat" = "meow"
"dog" = ["bark", "woof"]
}
}
output "noise" {
value = local[var.senses]["cat"]
}
call mymodule
module "mymodule" {
source = "../../../modules/mymodule"
senses = "sound"
}
returns error:
Error: Invalid reference
on ../../../modules/mymodule/outputs.tf line 62, in output "noise":
62: value = local[var.senses]["cat"]
The "local" object cannot be accessed directly. Instead, access one of its
attributes.
my code can't seem to handle
value = local[var.senses]["cat"]
Any suggestions on how i can get this to work?
I don't believe it's possible to use a variable to switch which local you're reading. I.e. local[var.senses] is the root of the issue.
If you refactor slightly and put your values inside a single, known, value--such as local.senses it should then let you do a key lookup within that value.
So, if you modify your locals to place your values in a senses key:
locals {
senses = {
"sounds" = {
"cat" = "meow"
"dog" = ["bark", "woof"]
}
}
}
and update your lookup to use that field:
value = local.senses[var.senses]["cat"]
Then I believe it will work, since your are doing a key lookup against a specific local rather than trying to dynamically select the local.

terraform - how to use variables inside attributes

I am not sure if this is the right approach to do this but I want to use a variable as an attribute.
For example, I have a variable that changes based on user input: os_name = ubuntu.
I want to use this variable name like the following,
resource "aws_instance" "workerNode" {
..................
ami = data.aws_ami.${var.os_name}.image_id
..................
}
Following is an example of the data block,
data "aws_ami" "suse" {
count = "${var.os_name == "suse" ? 1 : 0}"
owners = ["amazon"]
most_recent = true
filter {
name = "name"
values = ["suse-sles-${var.os_version}-sp*-v????????-hvm-ssd-x86_64"]
}
}
Which result the following,
"architecture" = "x86_64"
"hypervisor" = "xen"
"id" = "ami-0d3905203a039e3b0"
"image_id" = "ami-0d3905203a039e3b0"
But terraform is not allowing me to do this. Is there any way I can do this or I have to change the workflow?
In situations where it's not appropriate to gather all of your instances under a single resource using for_each (which would implicitly make that resource appear as a map of objects), you can get a similar result explicitly by writing a local value expression to construct an equivalent map:
locals {
amis = {
suse = data.aws_ami.suse
ubuntu = data.aws_ami.ubuntu
}
}
Then you can refer to local.amis["ubuntu"] or local.amis["suse"] (possibly replacing the element key with a variable, if you need to.
With that said, it does seem like there is a possible different approach for your case which would get there with only one data block:
locals {
os_ami_queries = {
suse = {
owners = ["amazon"]
filters = {
name = ["suse-sles-${var.os_version}-sp*-v????????-hvm-ssd-x86_64"]
}
}
ubuntu = {
owners = ["amazon"]
filters = {
name = ["ubuntu-${var.os_version}-something-something"]
}
}
}
ami_query = local.os_ami_queries[var.os_name]
}
data "aws_ami" "selected" {
owners = local.ami_query.owners
dynamic "filter" {
for_each = local.ami_query.filters
content {
name = filter.key
values = filter.value
}
}
}
This different permutation does the OS selection before the data "aws_ami" lookup, so it can use the settings associated with whichever OS was selected by the caller. The AMI id would then be in data.aws_ami.selected.id.
With that said, this approach has the disadvantage of being quite indirect and using a dynamic block, so I'd weigh that against the readability of the alternatives to pick the one which seems subjectively easiest to follow for someone who isn't familiar with this configuration. There isn't a single answer to that because to some extent it's a matter of taste, and so if you are working in a team setting this could be something to discuss with colleagues to see which approach best suits tradeoffs like how often you expect to be adding and removing supported operating systems vs. changing the details of how you use the result.
You can make it work by specifying your AMI's with a for_each and thus getting a map which you can access by key.
My data.aws_ami.myamis looks like this:
data "aws_ami" "myamis" {
for_each = toset(["suse", "ubuntu"])
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["${each.value}*"]
}
}
For test purposes I define a variable foo like this:
variable "foo" {
type = string
default = "suse"
}
Now I can access the AMI like this:
$ tf console
> data.aws_ami.myamis[var.foo].image_id
"ami-0ea50c090ba6e85c5"
You can adapt this to suit your needs for os_name and os_version.
I have solved the issue just by using conditional expression.
I am not sure if it is a standard way of doing things but it works for me.
I have tried to emulate if/elif/else with nested conditional expression.
output "name" {
value = "${ var.os_name == "ubuntu" ? data.aws_ami.ubuntu[0].image_id : (var.os_name == "redhat" ? data.aws_ami.redhat[0].image_id : (var.os_name == "centos" ? data.aws_ami.suse[0].image_id : data.aws_ami.suse[0].image_id ))}"
}

How to pass a dynamic value in a variable name on run-time?

Here is what I have:
locals {
timeseries = "desktop"
}
dynamic "request" {
for_each = var.query_"#{local.timeseries}"_timeseries
content {
q = request.value.q
type = request.value.type
style = request.value.style
}
}
What I expect:
for_each = var.query_desktop_timeseries
If I'm understanding your question correctly, you're trying to resolve a variable name via interpolation. In terraform, there's is no way to do this.
If you're looking to resolve to a particular list of values, based on the value of variables, you could do that using a map to, well, map from your value to the variables they resolve to.
For example you could have something like
locals {
timeseries = "desktop"
timeseries_lookup = {
desktop = var.query_desktop_timeseries
# Other mappings would go here
}
}
This could then be used, very similarly to your desired use-case, like the following
for_each = local.timeseries_lookup[local.timeseries]

Construct list of maps from a csv in terraform

I have the following variable
variable "whitelisted_ips" {
default = "xx.xxx.xx.x/21,xxx.xx.xxx.x/20"
}
I use this some places where a list of CIDRS is needed using the following
cidr_blocks = ["${split(",", var.whitelisted_ips)}"]
That all works fine.
I want to reuse these values and end up with the following structure (expressed as JSON to give you an idea)
waf_ips = [
{ value = "xx.xxx.xx.x/21", type="IPV4"},
{ value = "xxx.xx.xxx.x/20", type="IPV4"},
]
So I am looking to create a list of maps from the string (the IPV4 is hardcoded and repeats on every line).
If I feed my current JSON to an aws_waf_rule and treat it as a list it succeeds, but I'd rather not repeat the data in the tfvars file as its the same and I'd like to reuse that string separated list.
Ok so having learned more and read more it turns out you can do this with a null resource for static data so.
locals {
cidr_blocks = ["xxx.xxx.xxx/23", "xxx.xxx.xxx/23", "xxx.xxx.xxx/23"]
}
resource "null_resource" "cidr_map_to_protocol" {
count = "${length(local.cidr_blocks)}"
triggers = {
value = "${element(local.cidr_blocks, count.index)}"
type = "IPV4"
}
}
output "mapped_cidr_to_protocol" {
value = "${null_resource.cidr_map_to_protocol.*.triggers}"
}
this will not work for a computed resource unfortunately.

Terraform dynamic variable

I'm creating subnets as part of a seperate terraform template and exporting the IDs as follows.
output "subnet-aza-dev" {
value = "${aws_subnet.subnet-aza-dev.id}"
}
output "subnet-azb-dev" {
value = "${aws_subnet.subnet-azb-dev.id}"
}
output "subnet-aza-test" {
value = "${aws_subnet.subnet-aza-test.id}"
}
output "subnet-azb-test" {
value = "${aws_subnet.subnet-azb-test.id}"
}
...
I'm then intending to lookup these IDs in another template which is reused to provision multiple environments. Example below shows my second template is calling a module to provision an EC2 instance and is passing through the subnet_id.
variable "environment" {
description = "Environment name"
default = "dev"
}
module "sql-1-ec2" {
source = "../modules/ec2winserver_sql"
...
subnet_id = "${data.terraform_remote_state.env-shared.subnet-aza-dev}"
}
What I'd like to do is pass the environment variable as part of the lookup for the subnet_id e.g.
subnet_id = "${data.terraform_remote_state.env-shared.subnet-aza-${var.environment}"
However I'm aware that variable interpolation isn't supported. I've tried using a map inside of the first terraform template to export them all to a 'subnet' which I could then use to lookup from the second template. This didn't work as I was unable to output variables inside of the map.
This sort of design pattern is something I've used previously with CloudFormation, however I'm much newer to terraform. Am I missing something obvious here?
Worked out a way to do this using data sources
variable "environment" {
description = "Environment name"
default = "dev"
}
module "sql-1-ec2" {
source = "../modules/ec2winserver_sql"
...
subnet_id = "${data.aws_subnet.subnet-aza.id}"
}
data "aws_subnet" "subnet-aza" {
filter {
name = "tag:Name"
values = ["${var.product}-${var.environment}-${var.environmentno}-subnet-aza"]
}
}
data "aws_subnet" "subnet-azb" {
filter {
name = "tag:Name"
values = ["${var.product}-${var.environment}-${var.environmentno}-subnet-azb"]
}
}
Whilst this works and fulfils my original need, I'd like to improve on this by moving the data blocks to within the module, so that there's less repetition. Still working on that one though...

Resources