Merging complex types - terraform

I'm struggling with merging maps that have duplicate keys, since the built-in merge function will only keep the latest argument that has matching keys or attributes.
My maps are of the following shape:
{mykey = ["item1","item2","item3"]}
{mykey = ["item4","item5","item6"]}
The merge function simply returns {mykey = ["item4","item5","item6"]} (or whatever is the last argument). I'd like to compose a map like
{mykey = ["item1","item2","item3","item4","item5","item6"]}
I don't believe there's a function I can use to achieve this. However, I suspect that a for loop is the right approach, yet my knowledge is failing me.
Hi #MattSchuchard, thanks for your help. The parent map structure (if I am following you correctly), is a local variable. I am using keys and values from this local in order to retrieve object_ids from the azuread_users and azuread_groups data sources. My locals looks like this:
locals {
roles = {
role1 = {
users = ["user1#domain.com","user2#domain.com"]
groups = ["myGroup1","myGroup2"]
}
}
I am referencing this local in the data sources as follows:
data azuread_users.lookup {
for_each = local.roles
user_principal_names = each.values.users
}
data azuread_groups.lookup {
for_each = local.roles
display_names = each.values.groups
}
These data sources export object_ids as an attribute (which I will then use when adding members to groups in a resource block). The exported attributes are in the shape detailed in my original post.
I would like to compose a combined list of all object_ids exported from both data.azuread_users.lookup and data.azuread_groups.lookup which I can then provide as an argument when creating a group:
resource "azuread_group" "my_group" {
members = $myCombinedListOfObjectIDs
}

Depending on whether you want a list/map you can use this:
locals {
input = { mykey = ["item1", "item2", "item3"] }
input2 = { mykey = ["item4", "item5", "item6"] }
merged = concat(local.input.mykey, local.input2.mykey)
merged_mykey = {
mykeys = concat(local.input.mykey, local.input2.mykey)
}
}
The output is
> local.merged_mykey
{
"mykeys" = [
"item1",
"item2",
"item3",
"item4",
"item5",
"item6",
]
}
> local.merged
[
"item1",
"item2",
"item3",
"item4",
"item5",
"item6",
]
If you're worried about duplicates you can wrap it in a distinct call. https://www.terraform.io/language/functions/distinct

Related

Terraform map transformation

I have been banging my head for a few days trying to do this. I know I have probably seen it before but google is failing me.
I have created a local from a data source.:
locals {
my_new_map = [for group in data.okta_groups.aws_groups.groups: {
id = group.id
name = group.name
} if contains(local.groups_not_to_check, group.name) == false]
}
the result is
[
{
id = <group1id>
name = <group1name>
},
{
id = <group2id>
name = <group2name>
},
{
...
},
]
What i need is to transform it to this:
[
<group1name> = <group1id>,
<group2name> = <group2id>,
...
]
groupnames and ids are all unique so can be used as keys
Don’t know if it is possible in my for statement or not but am willing to create a local to do it.
Any help would be awesome.
Have tried many combinations of the FOR expression but to no avail.
Maybe i am missing a function of some sort that could help or maybe i just haven't hit the correct way to output the For expression.
The following should do what you want:
locals {
my_new_map = {for group in data.okta_groups.aws_groups.groups:
group.name => group.id if contains(local.groups_not_to_check, group.name) == false }
}

adding multiple destinations to new relic workflows using terraform

I am trying to create a newrelic workflow using terraform modules. I am fine with creating a workflow with signle destination. But, I am trying to create a workflow with more than one destination.
slack channel ids
variable "channel_ids" {
type = set(string)
default = ["XXXXXXXXXX","YYYYYYYYY"]
}
creating notification channels using slack channel ids
resource "newrelic_notification_channel" "notification_channel" {
for_each = var.channel_ids
name = "test" # will modify if required
type = "SLACK" # will parameterize this
destination_id = "aaaaaaaaa-bbbbb-cccc-ddddd-eeeeeeeeee"
product = "IINT"
property {
key = "channelId"
value = each.value
}
}
Now I want to create something like below (two destinations)
resource "newrelic_workflow" "newrelic_workflow" {
name = "my-workflow"
muting_rules_handling = "NOTIFY_ALL_ISSUES"
issues_filter {
name = "Filter-name"
type = "FILTER"
predicate {
attribute = "accumulations.policyName"
operator = "EXACTLY_MATCHES"
values = [ "policy_name" ]
}
}
destination {
channel_id = newrelic_notification_channel.notification_channel.id
}
destination {
channel_id = newrelic_notification_channel.notification_channel.id
}
}
I tried using for_each and for loop but no luck. Any idea on how to get my desired output?
Is it possible to loop through and create multiple destinations within the same resource, like attaching multiple destination to a single workflow?
I was able to achieve this by using a dynamic block, which produces a dynamic number of destination blocks based on the number of elements of newrelic_notification_channel.notification_channel.
resource "newrelic_workflow" "newrelic_workflow" {
name = "my-workflow"
muting_rules_handling = "NOTIFY_ALL_ISSUES"
issues_filter {
name = "Filter-name"
type = "FILTER"
predicate {
attribute = "accumulations.policyName"
operator = "EXACTLY_MATCHES"
values = [ "policy_name" ]
}
}
dynamic "destination" {
for_each = newrelic_notification_channel.notification_channel
content {
channel_id = destination.value.id
}
}
}

creating a list of list objects terraform

I'm setting up a terraform repo for my snowflake instance and bringing in a list of users to start managing.
I have a module called users
and have the following files:
I have a variable defined as follows.
variable "users" {
type = list(object(
{
name = string
comment = string
default_role = string
disabled = bool
must_change_password = bool
display_name = string
email = string
first_name = string
last_name = string
default_warehouse = string
}
)
)
}
now inside users.tf I want to hold a list of all my users based on the above variable, I thought I could define it as follows:
users {
user_1 = {
name = 'x'
},
user_2 = {
name = 'y'
}
}
however, when I run Terraform validate on this it gives me the error that a user block is not expected here.
Can someone tell me my error and give me some guidance if I'm doing this correctly?
My intention is to have a file to hold all my users that I then define with a dynamic block inside my main.tf file within this module.
I can then reference the dynamic block inside the outputs.tf which will give me access to the users inside said module in the global project namespace.
Looks to me like you are attempting to configuring your users as an object:
users {
user_1 = {
name = "x"
},
user_2 = {
name = "y"
}
}
but you actually set your variable constraint to a list of objects. So it should be:
users = [
{
name = "user_1"
# other fields
},
{
name = "user_2"
# other fields
}
]
Here is a full working example:
modules/users/variables.tf
variable "users" {
type = list(object({
name = string
}))
}
modules/users/outputs.tf
output "users" {
value = var.users
}
main.tf
module "users" {
source = "./modules/users"
users = [
{ name = "user_1" },
{ name = "user_2" }
]
}
output "users" {
value = module.users.users
}
plan output
Changes to Outputs:
+ users = [
+ {
+ name = "user_1"
},
+ {
+ name = "user_2"
},
]
Your config syntax and usage is completely correct here. Your config file organization is the issue here. users.tf is a Terraform variables file, and therefore should have the .tfvars extension. If you rename the file from users.tf to e.g. users.tfvars, then you can specify it as an input with the -var-file=users.tfvars argument with the CLI or otherwise as per standard usage. You can see more information in the documentation.
On a side note: it is not really best practices to manage an entire module just for managing a set of users for a specific service. If you follow this design pattern in the future, then your codebase will not scale very well, and could easily become unmanageably large.

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

Terraform iterate over list

I would like to replace the 3 indepedent variables (dev_id, prod_id, stage_id), for a single list containing all the three variables, and iterate over them, applying them to the policy.
Is this something terraform can do?
data "aws_iam_policy_document" "iam_policy_document_dynamodb" {
statement {
effect = "Allow"
resources = ["arn:aws:dynamodb:${var.region}:${var.account_id}:table:${var.dynamodb_table_name}"]
actions = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem",
]
principals {
type = "AWS"
identifiers = [
"arn:aws:iam::${var.dev_id}:root",
"arn:aws:iam::${var.prod_id}:root",
"arn:aws:iam::${var.stage_id}:root"
]
}
}
}
I looked into cycles and interpolation, but It seems that 99% of the time the interpolation is done with "count" which only works for the creation of multiple resources (I hope I am not saying a big lie).
For example, I used
principals {
count = "${length(var.list)}"
identifiers = ["arn:aws:iam::${var.list[count.index]}"]
}
but that was unsuccessful.
Is there some way of achieving the final goal of replacing those 3 variables by a single list (or map) and iterate over them?
Given you have the list of account ids, have you tried this?
var "accounts" {
default = ["123", "456", "789"]
type = "list"
}
locals {
accounts_arn = "${formatlist("arn:aws:iam::%s", var.accounts)}"
}
Then in your policy document:
principals {
type = "AWS"
identifiers = ["${local.accounts_arn}"]
}
I haven't actually tried it, but can't think of a reason it wouldn't work.

Resources