Deal will dict Using Maps in terraform - google-cloud-platform

I'm looking for way to define a list of ssh keys in a variables file so that I could retrieve them in the tf module code for my compute instance like this :
metadata = {
ssh-keys = join("\n", [for user, key in var.ssh_keys : "${user}:${key}"])
}
Here is the content of the variables file I wrote to achieve that :
variable "ssh_keys" {
type = "map"
default = {
{
user = "amary"
key = "${file("/Users/nixmind/.ssh/amary.pub")}"
}
{
user = "nixmind"
key = "${file("/Users/nixmind/.ssh/nixmind.pub")}"
}
}
}
But I'm having this error :
Error: Missing attribute value
on variables.tf line 8, in variable "ssh_keys":
4:
5:
6:
7:
8:
9:
Expected an attribute value, introduced by an equals sign ("=").
I'm not sure to really get what to do there.

There are a few different problems here. I'll talk about them one at a time.
The first is that your default expression is not using correct map syntax. Here's a corrected version:
variable "ssh_keys" {
type = map(string)
default = {
amary = file("/Users/nixmind/.ssh/amary.pub")
nixmind = file("/Users/nixmind/.ssh/nixmind.pub")
}
}
The second problem is that a variable default value cannot include function calls, so the calls to file above are invalid. There are a few different options here about how to deal with this, but if this is a variable in a root module then I expect it would be most convenient to have the variable be a map of filenames rather than a map of the contents of those files, and then the module itself can read the contents of those files in a later step:
variable "ssh_key_files" {
type = map(string)
default = {
amary = "/Users/nixmind/.ssh/amary.pub"
nixmind = "/Users/nixmind/.ssh/nixmind.pub"
}
}
Your for expression for building the list of "user:key" strings was correct with how you had the variable defined before, but with the adjustment I've made above to use filenames instead of contents we'll need an extra step to actually read the files:
locals {
ssh_keys = { for u, fn in var.ssh_key_files : u => file(fn) }
}
We can then use local.ssh_keys to get the map from username to key needed for the metadata expression:
metadata = {
ssh-keys = join("\n", [for user, key in local.ssh_keys : "${user}:${key}"])
}
If you do want this module to accept already-loaded SSH key data rather than filenames then that is possible but the variable will need to be required rather than having a default, because it'll be up to the calling module to load the files.
The definition without the default value will look like this:
variable "ssh_keys" {
type = map(string)
}
Then your calling module, if there is one (that is, if this isn't a root module) can be the one to call file to load those in:
module "example" {
source = "./modules/example"
ssh_keys = {
amary = file("/Users/nixmind/.ssh/amary.pub")
nixmind = file("/Users/nixmind/.ssh/nixmind.pub")
}
}
The above is a reasonable interface for a shared module that will be called from another module like this, but it's not a convenient design for a root module because the "caller" in that case is the person or script running the terraform program, and so providing the data from those files would require reading them outside of Terraform and passing in the results.

My problem was semantic not syntactic, cause I can't use a map of map as tried to. Instead I used a liste and the error desapeared.
variable ssh_keys {
type = list(object({
user=string
key=string
}))
default = [
{
"user" = "amaret93"
"key" = "/Users/valerietchala/.ssh/amaret93.pub"
},
{
"user" = "nixmind"
"key" = "/Users/valerietchala/.ssh/nixmind.pub"
}
]
}
But the Martin's answer above is also a more good approach

Related

Terraform map require key to be set and accept any optional keys

I want to allow any key to be set within a dictionary object and require Name to be set. Im passing this object into a variable that forces Name to be set but its ignoring all the other keys
tags = {
"Name" = "EC2_Name_Value" # Required
"AnyKey1" = "value1"
"AnyKey2" = "value2"
...
}
variable "tags" {
type = object({
Name = string
})
}
> var.tags
{
"Name" = "EC2_Name_Value"
}
I know that I'm able to use key = optional(string) however, i want to accept all extra keys and not have to define only the keys i want to accept.
What I would suggest is leaving the tags variable as is (maybe renaming it), and then using either an additional variable or local variables, e.g.:
variable "required_tag" {
type = object({
Name = string
})
}
variable "additional_tags" {
type = object(any)
}
Then, you would use the merge built-in function [1] to bring them all together:
tags = merge(var.required_tag, var.additional_tags)
Alternatively, since you know you will always need that one tag, you could switch it up a bit and remove the required_tag (or tags in your example) variable, and do something like:
tags = merge({ Name = "EC2_Name_Value" }, var.additional_tags)
Last, but not the least, there is an option to use default_tags on the provider level [2], but I am not sure if that fits your case.
[1] https://developer.hashicorp.com/terraform/language/functions/merge
[2] https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/resource-tagging#propagating-tags-to-all-resources

In Terraform, how to output values from a list?

I am trying to get the output to show me the names of the IAM users being created.
resource "aws_iam_user" "lb" {
name = var.elb_names[count.index]
count = 3
path = "/system/"
}
variable "elb_names" {
type = list
default = ["dev-lb", "qa-lb", "prod-lb"]
}
output "elb_names" {
value = aws_iam_user.lb.name[count.index]
}
I expect to get the following as output
dev-lb
qa-lb
prod-lb
But I am getting this error...
Error: Missing resource instance key
on countEC2.tf line 38, in output "elb_names":
38: value = aws_iam_user.lb.name[count.index]
Because aws_iam_user.lb has "count" set, its attributes must be accessed on specific instances.
For example, to correlate with indices of a referring resource, use:
aws_iam_user.lb[count.index]
You would have to use a slightly different approach:
output "elb_names" {
value = aws_iam_user.lb[*].name
}
This is the splat expression [1]. Note that the output will be a list.
[1] https://developer.hashicorp.com/terraform/language/expressions/splat

Reference variable in a for loop in terraform

I have a list of accounts declared under variable "acct". I need to create as many ARNs as the accounts below using this list. How to do it? The below code gives me attribute error:
A reference to a resource type must be followed by at least one attribute access, specifying the resource name.
variable "acct"{
type = list(string)
default = ["111111111111","22222222222",.....]
}
data "aws_arn" "SrcArn" {
account = [for acc in var.acct : acc ]
arn = ["arn:aws:logs:us-east-1:${account}:*"]
}
I need to create a list of arns which then can be used further down in the code. Can these then be referenced below like this:
condition {
test = "ArnLike"
values = data.aws_arn.SrcArn
variable = "aws:SourceArn"
}
If you want to use aws_arn, you can use to this.
variable "acct"{
type = list(string)
default = ["111111111111","22222222222",.....]
}
data "aws_arn" "SrcArn" {
count = var.acct
arn = ["arn:aws:logs:us-east-1:${var.acct[count.index]}:*"]
}
And if you wanna make arn_list,
locals {
# only use to variable.
# not use to data "aws_arn"
arn_list_only_use_to_variable = [for account in var.acct: "arn:aws:logs:us-east-1:${account}:*"]
# use to data "aws_arn"
src_arn = data.aws_arn.SrcArn
arn_list_2_use_to_data_aws_arn = [for account in src_arn: account]
}
...
condition {
test = "ArnLike"
values = local.arn_list_only_use_to_variable
variable = "aws:SourceArn"
}
...

I want to have collections of variables and select which set I choose from, I tried nested variable assignment

I have a module for which I need to pass a set of values via variables.tf, currently, the variables are grouped by the suffix on the name, i.e. -dev or -stg, etc
The module itself doesn't care which set it gets, but I must decide somewhere, so I pass the suffix at terraform invocation time or in a .tfvars file.
How can I get the following code to work, or how else should I do it?
module "alb" {
...
# these work but is ugly and inflexible
# connect_alb_client_id = var.connect_alb_client_id-dev
# connect_alb_client_id = var.connect_alb_client_id-stg
# this doesn't work
connect_alb_client_id = "${var.connect_alb_client_id}${var.suffix}"
# and neither does this
connect_alb_client_id = "${var.connect_alb_client_id${var.suffix}}"
# so what is the correct syntax or alternative way of doing it
...
}
variable "suffix"
type = string
default = "-dev"
# default = "-stg"
}
variable "connect_alb_client_id-dev" {
type = string
default = "abcdef"
}
variable "connect_alb_client_id-stg {
type = string
default = "ghijkl"
}
Your connect_alb_client_id should be a map with keys of dev, stg and so on:
variable "connect_alb_client_id" {
default = {
dev = "abcdef"
stg = "ghijkl"
}
}
then:
module "alb" {
connect_alb_client_id = var.connect_alb_client[var.suffix]
}

How to output all the resources of one type in Terraform?

I have a bunch of aws_ecr_repositories defined in my Terraform code:
resource "aws_ecr_repository" "nginx_images" {
name = "nginx-test"
}
resource "aws_ecr_repository" "oracle_images" {
name = "oracle-test"
}
I want to be able to have an output that can list all the aws_ecr_repository resources into one output. This is what I tried:
output "ecr_repository_urls" {
value = "[${aws_ecr_repository.*.repository_url}]"
}
This does not work because Terraform does not seem to allow wildcards on the resource names. Is it possible to have an output like this? My current solution is to just list outputs for every resource defined.
Terraform's splat syntax is for keeping track of each thing created by a resource using the count meta parameter.
If you want to be able to get at all of the respoitory URLs you could have a single aws_ecr_repository resource and use the count meta parameter with something like this:
variable "images" {
default = [
"nginx-test",
"oracle-test",
]
}
resource "aws_ecr_repository" "images" {
count = "${length(var.images)}"
name = "${var.images[count.index]}"
}
output "ecr_repository_urls" {
value = "[${aws_ecr_repository.images.*.repository_url}]"
}
You can combine them manually as a list:
output "ecr_repository_urls" {
value = ["${aws_ecr_repository.nginx_images.repository_url}", "${aws_ecr_repository.oracle_images.repository_url}"]
}
Although it probably won't be pretty in code.
You could also do something like this:
variable "ecr_repos" {
default = {
"0" = "foo"
"1" = "bar"
}
}
resource "aws_ecr_repository" "images" {
count = "${length(var.ecr_repos)}"
name = "${lookup(var.ecr_repos,count.index)}-test"
}
output "ecr_repository_urls" {
value = "${aws_ecr_repository.images.*.repository_url}"
}
But the problem is if the list order changes it's going to recreate resources and get really ugly really fast since each repo is assigned to an index number.