How to use terraform for_each to separate map of objects and get IDs of resources? - foreach

in my project I have a two clients and each have a couple of databases. How I can create separate failover group for each client with specific client databases?
This is variables for each client:
variable "client" {
type = map (any)
default = {
# Client1 Param
"client1" =
"databases" = {
"db1" = "true",
"db2" = "true",
"db3" = "true",
},
},
# Client2 param
"client2" = {
"databases" = {
"db1" = "true",
"db2" = "false",
},
}
Terraform code to create DBs:
locals {
client_databases = flatten([
for client_key, client in var.client : [
for database, enabled in client.databases : {
client_name = client_key
database_name = database
database_enabled = enabled
}
]
])
}
# ## Create Client Specific Database(s)
resource "azurerm_mssql_database" "client_primary" {
for_each = { for databases in local.client_databases :
"${databases.client_name}-${databases.database_name}" => databases
if databases.database_enabled == "true"
}
name = "${each.key}"
server_id = azurerm_mssql_server.db["primary"].id
elastic_pool_id = azurerm_mssql_elasticpool.db["primary"].id
}
Code to create failover group:
resource "azurerm_sql_failover_group" "db" {
for_each = var.client
name = "${lower(azurerm_resource_group.db.name)}-${each.key}"
resource_group_name = azurerm_resource_group.db.name
server_name = azurerm_mssql_server.db["primary"].name
databases = [for database in azurerm_mssql_database.client_primary : database.id]
partner_servers {
id = azurerm_mssql_server.db["secondary"].id
}
read_write_endpoint_failover_policy {
mode = var.db_failover_policy_mode
grace_minutes = var.db_failover_policy_grace
}
In this case I create two failover group each of them have all databases:
client1-db1
client1-db2
client1-db3
client2-db1
client2-db2
How I can create two failover group with client own databases
Failover group 1:
client1-db1
client1-db2
client1-db3
Failover group 2:
client2-db1
client2-db2
Of course all other needed resources are created!

It sounds like your goal is to declare a separate failover group per client, and then have each failover group include all of the databases for that client.
Your var.client seems to already be in a suitable shape for use in for_each for resources that should have one instance per client, so there's no need to transform it further for use in that resource, as you've already seen:
resource "azurerm_sql_failover_group" "db" {
for_each = var.client
# ...
The remaining problem is to generate a suitable value for databases which uses the provider-chosen id attribute for each `azurerm_mssql_database. That requires reproducing the same compound instance keys used to identify the individual databases:
databases = [
for db_name, enabled in each.value.databases :
azurerm_mssql_database.client_primary["${each.key}:${db_name}"].id
if enabled == "true"
]
Your use of string values to represent boolean values is a little confusing here. I assume you did that because type = map(any) was not able to automatically find a suitable single type to use for all elements when you mixed types.
You can avoid that problem by telling Terraform the precise type constraint you expect, which will then allow Terraform to understand how to interpret the given data structure:
variable "client" {
type = map(
object({
databases = map(bool)
})
)
}
The above type constraint removes all of the ambiguity about what any is supposed to mean in this context, and thus allows using the more appropriate types for this data structure.
If you do this then you'll need to remove all of your == "true" comparisons elsewhere in the module, because true is not equal to "true" in Terraform. The following is an alternative version of the final example I shared above that's written to work with properly-typed boolean values:
databases = [
for db_name, enabled in each.value.databases :
azurerm_mssql_database.client_primary["${each.key}:${db_name}"].id
if enabled
]
Using exactly-specified type constraints will typically make a module easier to use and to maintain in future, by making it clear exactly what it requires and ensuring that the values appearing inside the module will always be of the prescribed types.

Related

How to call a value in map element only when it matches another var

I am using the Terraform provider mrparkers/keycloak to attempt to assign Keycloak groups a list of users.
The snippet below creates realms, groups, and users correctly, but I am stumped on the final line for calling a list of users which should belong to the group being looped through.
Anything to point me in the right direction would be hugely appreciated. :)
vars
variable "realms" {
description = "realms"
type = set(string)
default = ["mrpc"]
}
variable "mrpc-groups" {
type = map(object({
name = string
realm = string
members = set(string)
}))
default = {
"0" = {
realm = "mrpc"
name = "mrpc-admins"
members = ["hellfire", "hellfire2"]
},
"1" = {
realm = "mrpc"
name = "mrpc-mods"
members = ["hellfire2"]
}
}
}
variable "mrpc-users" {
type = map(object({
username = string
email = string
first_name = string
last_name = string
realm = string
}))
default = {
"0" = {
realm = "mrpc"
username = "hellfire"
email = "bla#bla.bla"
first_name = "hell"
last_name = "fire"
}
"1" = {
realm = "mrpc"
username = "hellfire2"
email = "bla2#bla.bla"
first_name = "hell2"
last_name = "fire2"
}
}
}
resources
resource "keycloak_realm" "realm" {
for_each = var.realms
realm = each.value
}
resource "keycloak_group" "group" {
for_each = var.mrpc-groups
realm_id = each.value["realm"]
name = each.value["name"]
depends_on = [keycloak_realm.realm]
}
resource "keycloak_user" "user" {
for_each = var.mrpc-users
realm_id = each.value["realm"]
username = each.value["username"]
email = each.value["email"]
first_name = each.value["first_name"]
last_name = each.value["last_name"]
}
resource "keycloak_group_memberships" "group_members" {
for_each = keycloak_group.group
realm_id = each.value["realm_id"]
group_id = each.value["name"]
members = [ "hellfire2" ]
# i want this to be var.mrpc-groups.*.members (* used incorrectly here i think)
# if
# var.mrpc-groups.*.name == each.value["name"]
#
# so that the correct member list in the vars is used when the matching group is being looped over
# any method to get the final outcome is good :)
}
We can use the distinct and flatten functions in conjunction with a for expression within a list constructor to solve this:
distinct(flatten([for key, attrs in var.mrpc_groups : attrs.members]))
As tested locally, this will return the following for your values exactly as requested in the question indicated by var.mrpc-groups.*.members:
members = [
"hellfire",
"hellfire2",
]
The for expression iterates through the variable mrpc_groups map and returns the list(string) value assigned to the members key within each group's key value pairs. The lambda/closure scope variables are simply key and attrs because the context is unclear to me, so I was unsure what a more descriptive name would be.
The returned structure would be a list where each element would be the list assigned to the members key (i.e. [["hellfire", "hellfire2"], ["hellfire2"]]). We use flatten to flatten the list of lists into a single list comprised of the elements of each nested list.
There would still be duplicates in this flattened list, and therefore we use the distinct function to return a list comprised of only unique elements.
For the additional question about assigning the members associated with the group at the current iteration, we can simply implement the following:
members = flatten([for key, attrs in var.mrpc_groups : attrs.members if attrs.name == each.value["name"]])
This will similarly iterate through the map variable of var.mrpc_groups, and construct a list of the members list filtered to only the group matching the name of the current group iterated through keycloak_group.group. We then flatten again because it is also a nested list similar to the first question and answer.
Note that for this additional question it would be easier for you in general and for this answer if you restructured the variable keys to be the name of the group instead of as a nested key value pair.

Iterating Two Maps Inside A Resource using for_each

I'm Trying to create azure SRV record based on some condition, for this multiple target is needed for a single SRV record. If I iterate inside dynamic each.key and each.value is referring the key value outside the dynamic block. is there a way to iterate this record block for required number of time. Also the count is not supported in record block. Could there be any other ways to achieve this without loop also fine.
NOTE: this is a pseudo code for reference.
resource "azurerm_dns_srv_record" "srv_dns_record" {
for_each = { for key, value in var.clusters : key => value if some condition }
name = name
zone_name = azurerm_dns_zone.cluster_dns_zone[each.key].name
resource_group_name = azurerm_resource_group.main.name
ttl = 60
dynamic "record" {
for_each = var.targets
content {
priority = 1
weight = 5
port = 443
target = each.key
}
}
Thanks!
from the docs
The iterator argument (optional) sets the name of a temporary variable that represents the current element of the complex value. If omitted, the name of the variable defaults to the label of the dynamic block ("setting" in the example above).
so to refer to the value of the iterated field in the dynamic block you should use the label of the block not the each word
resource "azurerm_dns_srv_record" "srv_dns_record" {
for_each = { for key, value in var.clusters : key => value if some condition }
name = name
zone_name = azurerm_dns_zone.cluster_dns_zone[each.key].name
resource_group_name = azurerm_resource_group.main.name
ttl = 60
dynamic "record" {
for_each = var.targets
content {
priority = 1
weight = 5
port = 443
target = record.key
}
}
If you want to iterate over var.targets in side a dynamic block, it should be:
target = record.key

Terraform Azure provider - How do I add via terraform more than one key and/or secret for my key vault?

All the examples I saw provide 1 key and or q secret.
Is there a way to add another one (or more)?
To add multiple keys or secrets for your key vault, you just need to add the resources azurerm_key_vault_key and azurerm_key_vault_secret multiple times.
It's recommended to create such resources in the loop. Terraform offers several different looping constructs, each intended to be used in a slightly different scenario:
count parameter: loop over resources.
for_each expressions: loop over resources and inline blocks within a resource.
for expressions: loop over lists and maps.
For example, create one or more keys and secrets with count parameters.
variable "key_lists" {
type = list(string)
default = ["key1","key2","key3"]
}
variable "secret_maps" {
type = map(string)
default = {
"name1"= "value1"
"aaa" = "111"
"bbb" = "222"
}
}
resource "azurerm_key_vault_key" "generated" {
count = length(var.key_lists)
name = var.key_lists[count.index]
key_vault_id = azurerm_key_vault.example.id
key_type = "RSA"
key_size = 2048
key_opts = [
"decrypt",
"encrypt",
"sign",
"unwrapKey",
"verify",
"wrapKey",
]
}
resource "azurerm_key_vault_secret" "example" {
count = length(var.secret_maps)
name = keys(var.secret_maps)[count.index]
value = values(var.secret_maps)[count.index]
key_vault_id = azurerm_key_vault.example.id
}
You could read this blog for more Terraform loop tips.

Using Terraform how can I create a user for each database as well as for each namespace?

My Terraform script currently creates 2 databases for a set of namespaces (1 or more). I now need to create a user for each respective database, and I am having trouble figuring out the correct method to do this.
This is what I have currently...
variable "namespaces" {
type = set(string)
}
variable "databases" {
type = set("server", "analyzer")
}
resource "postgresql_database" "server_databases" {
for_each = toset(var.namespaces)
name = "server_${each.key}"
}
resource "postgresql_database" "analyzer_databases" {
for_each = toset(var.namespaces)
name = "analyzer_${each.key}"
}
resource "random_password" "postgres_password" {
length = 12
}
resource "postgresql_role" "read_only_user" {
name = "readonlyuser"
login = true
password = random_password.postgres_password.result
skip_reassign_owned = true
}
resource "postgresql_grant" "readonly_tables" {
depends_on = [postgresql_database.server_databases, postgresql_database.analyzer_databases]
for_each = toset(var.namespaces)
database = "server_${each.key}"
object_type = "table"
privileges = ["SELECT"]
role = "readonlyuser"
schema = "public"
}
The problem here is database = "server_${each.key}" will only create a user for my server database in each namespace. I am pretty sure I need a nested for_each but I am not sure how to achieve this.
I think it should even be possible to loop over the postgresql_database resources instead of having 2 separate resource's defined

Passing query parameters in Dapper using OleDb

This query produces an error No value given for one or more required parameters:
using (var conn = new OleDbConnection("Provider=..."))
{
conn.Open();
var result = conn.Query(
"select code, name from mytable where id = ? order by name",
new { id = 1 });
}
If I change the query string to: ... where id = #id ..., I will get an error: Must declare the scalar variable "#id".
How do I construct the query string and how do I pass the parameter?
The following should work:
var result = conn.Query(
"select code, name from mytable where id = ?id? order by name",
new { id = 1 });
Important: see newer answer
In the current build, the answer to that would be "no", for two reasons:
the code attempts to filter unused parameters - and is currently removing all of them because it can't find anything like #id, :id or ?id in the sql
the code for adding values from types uses an arbitrary (well, ok: alphabetical) order for the parameters (because reflection does not make any guarantees about the order of members), making positional anonymous arguments unstable
The good news is that both of these are fixable
we can make the filtering behaviour conditional
we can detect the category of types that has a constructor that matches all the property names, and use the constructor argument positions to determine the synthetic order of the properties - anonymous types fall into this category
Making those changes to my local clone, the following now passes:
// see https://stackoverflow.com/q/18847510/23354
public void TestOleDbParameters()
{
using (var conn = new System.Data.OleDb.OleDbConnection(
Program.OleDbConnectionString))
{
var row = conn.Query("select Id = ?, Age = ?", new DynamicParameters(
new { foo = 12, bar = 23 } // these names DO NOT MATTER!!!
) { RemoveUnused = false } ).Single();
int age = row.Age;
int id = row.Id;
age.IsEqualTo(23);
id.IsEqualTo(12);
}
}
Note that I'm currently using DynamicParameters here to avoid adding even more overloads to Query / Query<T> - because this would need to be added to a considerable number of methods. Adding it to DynamicParameters solves it in one place.
I'm open to feedback before I push this - does that look usable to you?
Edit: with the addition of a funky smellsLikeOleDb (no, not a joke), we can now do this even more directly:
// see https://stackoverflow.com/q/18847510/23354
public void TestOleDbParameters()
{
using (var conn = new System.Data.OleDb.OleDbConnection(
Program.OleDbConnectionString))
{
var row = conn.Query("select Id = ?, Age = ?",
new { foo = 12, bar = 23 } // these names DO NOT MATTER!!!
).Single();
int age = row.Age;
int id = row.Id;
age.IsEqualTo(23);
id.IsEqualTo(12);
}
}
I've trialing use of Dapper within my software product which is using odbc connections (at the moment). However one day I intend to move away from odbc and use a different pattern for supporting different RDBMS products. However, my problem with solution implementation is 2 fold:
I want to write SQL code with parameters that conform to different back-ends, and so I want to be writing named parameters in my SQL now so that I don't have go back and re-do it later.
I don't want to rely on getting the order of my properties in line with my ?. This is bad. So my suggestion is to please add support for Named Parameters for odbc.
In the mean time I have hacked together a solution that allows me to do this with Dapper. Essentially I have a routine that replaces the named parameters with ? and also rebuilds the parameter object making sure the parameters are in the correct order.
However looking at the Dapper code, I can see that I've repeated some of what dapper is doing anyway, effectively it each parameter value is now visited once more than what would be necessary. This becomes more of an issue for bulk updates/inserts.
But at least it seems to work for me o.k...
I borrowed a bit of code from here to form part of my solution...
The ? for parameters was part of the solution for me, but it only works with integers, like ID. It still fails for strings because the parameter length isn't specifed.
OdbcException: ERROR [HY104] [Microsoft][ODBC Microsoft Access Driver]Invalid precision value
System.Data.Odbc. OdbcParameter.Bind(OdbcStatementHandle hstmt,
OdbcCommand command, short ordinal, CNativeBuffer parameterBuffer, bool allowReentrance)
System.Data.Odbc.OdbcParameterCollection.Bind(OdbcCommand command, CMDWrapper cmdWrapper, CNativeBuffer parameterBuffer)
System.Data.Odbc.OdbcCommand.ExecuteReaderObject(CommandBehavior behavior, string method, bool needReader, object[] methodArguments, SQL_API odbcApiMethod)
System.Data.Odbc.OdbcCommand.ExecuteReaderObject(CommandBehavior behavior, string method, bool needReader)
System.Data.Common.DbCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
Dapper.SqlMapper.QueryAsync(IDbConnection cnn, Type effectiveType, CommandDefinition command) in SqlMapper.Async.cs
WebAPI.DataAccess.CustomerRepository.GetByState(string state) in Repository.cs
var result = await conn.QueryAsync(sQuery, new { State = state });
WebAPI.Controllers.CustomerController.GetByState(string state) in CustomerController .cs
return await _customerRepo.GetByState(state);
For Dapper to pass string parameters to ODBC I had to specify the length.
var result = await conn.QueryAsync<Customer>(sQuery, new { State = new DbString { Value = state, IsFixedLength = true, Length = 4} });

Resources