Terraform module with repeatable variable - terraform

Several resources, e.g. aws_dynamodb_table have repeatable variables. In the case of the aws_dynamodb_table resource, attribute is repeatable which allows you to specify multiple attributes using either of the following syntax
attribute {
name = "UserId"
type = "S"
}
attribute {
name = "GameTitle"
type = "S"
}
attribute {
name = "TopScore"
type = "N"
}
or
attribute = [{
name = "UserId"
type = "S"
}, {
name = "GameTitle"
type = "S"
}, {
name = "TopScore"
type = "N"
}]
I like this interface and want to provide the same flexibility in my modules but I can't seem to find any documentation on how to do it. Is this possible for modules or is it only the built-in resources that can do this.

It looks like that either allows you to provide attribute multiple times as separate maps (which are then merged) or as a list.
You're going to want to take a look at the documentation related to Input Variable Configuration
In particular, you will want to look at the section titled Variable Merging.
I believe you could do something like this for similar behavior (from the docs above, give them a read :P)
foo {
quux="bar"
}
foo {
bar="baz"
}
This would mean foo returns:
{
quux = "bar"
bar = "baz"
}
Hope that helps!

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 conditionally override module variable

I want to conditionally override a module variable that has a default value at plan time. I.e. when the condition is true an override is provided, when it is false no override is provided and the default value is used. Example:
main.tf:
terraform {
required_version = ">= 0.14.9"
}
variable "random" {
}
module "my_animal_module" {
source = "./my-animal-module"
species = var.random > 7 ? "monkey" : "horse"
}
my-animmal-module/main.tf:
variable species {
default = "horse"
}
resource "local_file" "animal" {
content = "${var.species}"
filename = "./animal.txt"
}
As above, I can just provide the default (species = var.random > 7 ? "monkey" : "horse") but that requires the caller knows the module's default value which breaks encapsulation. An alternative is to use some place holder for the default value like "" then test for that condition in the module and use a different value as suggested in this SO answer. This is slightly better but still tedious and indirect. That SO answer is over 3y old and terraform has changed a lot since then. So I'm wondering, is there is a clean way to solve this yet? Essentially what's needed is the dynamic variable analogy to dynamic blocks but AFAIK it does not yet exist.
I would reorganize your module as shown below. Basically you would use local.species value instead of using var.species directly. The local.species would be set based on the values from the parent.
variable species {
default = null
}
locals {
defaults = {
species = "horse"
}
species = coalesce(var.species, local.defaults["species"])
}
resource "local_file" "animal" {
content = "${local.species}"
filename = "/tmp/animal.txt"
}
Then in the parent:
module "my_animal_module" {
source = "./my-animal-module"
species = var.random > 7 ? "monkey" : null
}
You can use conditional expression. Please refer below page:
https://www.terraform.io/docs/language/expressions/conditionals.html
Or you can use validation inside variable block. Refer below page:
https://www.terraform.io/docs/language/values/variables.html
Let me know if it helps

Terraform: How to Deal with Optional Input Variable

So I came across this general problem and didn't find an answer yet.
Problem: The input value can have optional variables, like the case below, group_memberships is an optional input, at the moment I make it an empty string input for this to work.
But if I comment it out like shown below and run it, I would get the error:
The given key does not identify an element in this collection value.
Basically it's complaining that I don't have list_of_users.test_user.group_memberships.Is there a way to tell terraform if the input is not declared, just ignore it? I know I can leave it the way it is but user can potentially have many optional values, and making lots of empty input doesn't really make sense.
Thanks! First post question, sorry about poor layout for the code : )
in my .tfvars file:
list_of_users = {
regular_user = {
email = "pdv#abc.com",
group_memberships = "regular_group"
},
test_user = {
email = "test#abc.com",
// group_memberships = "" <------ Currently can work if not comment out, looking for solution that I can remove those reduent empty declariation
},
admin_user = {
email = "admin#abc.com",
group_memberships = "admin_group"
}
}
in .tf file:
variable "list_of_users" {}
resource "user_api_from_provider" "user_generate" {
for_each = var.list_of_users
email = each.value["email"]
group_memberships = each.value["group_memberships"] !=""? [user_api_from_provider.group_generate[each.value["group_memberships"]].id] : null
}
There is support for this as a Terraform "experiment" (it's implemented, but could change or be removed in future versions). You have to declare in your module that you're using the experiment:
terraform {
# Optional attributes and the defaults function are
# both experimental, so we must opt in to the experiment.
experiments = [module_variable_optional_attrs]
}
And then you would use it in your case like this:
variable "list_of_users" {
type = map(object({
email = string
group_memberships = optional(string)
}))
}
Now, if group_membership isn't defined for a given user, that field will have the value of null, so you can now do:
resource "user_api_from_provider" "user_generate" {
...
group_memberships = each.value.group_memberships != null ? [user_api_from_provider.group_generate[each.value["group_memberships"]].id] : null
}
Alternatively, if you don't want to use the experiment, you should be able to do this (untested):
resource "user_api_from_provider" "user_generate" {
...
group_memberships = contains(each.value, "group_memberships") ? [user_api_from_provider.group_generate[each.value["group_memberships"]].id] : null
}
As of Terraform v1.3 the Optional Object Type Attributes feature is official, which means it is no longer an experiment and the syntax is considered stable.
As mentioned in previous comments, you can now do something like:
variable "list_of_users" {
type = map(object({
email = string
group_memberships = optional(string, "")
}))
}
In the above example, using the default value ("") allows the Terraform code in the project/module to function as if there is always a value even if it is omitted from the input variables.

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]

Resources