Terraform's random_shuffle don't shuffle as expected - terraform

I have this configuration :
variable "sub_list" {
type = "map"
default = {
"data.dev" = ["data1", "data2", "data3", "data4"]
"data.dev2" = ["data1", "data2", "data3", "data4"]
}
}
resource "random_shuffle" "az" {
input = "${var.sub_list[local.data]}"
result_count = "${length(var.VM_count)}"
}
data "vsphere_sub" "sub" {
count = "${length(var.VM_count)}"
name = "${random_shuffle.az.result[count.index]}"
}
resource "vsphere_virtual_machine" "VM" {
name = "${var.VM_name}
folder = "${var.folder}"
count = "${length(var.VM_count)}"
sub_id = "${element(data.vsphere_sub.sub.*.id, (count.index)%length(data.vsphere_sub.sub.id))}"
num_cpus = "${var.VM_vcpu}"
memory = "${var.VM_memory}"
}
When I launch with VM_count=2 for example, I expect to have a subnet for every VM but it creates the 2 VMs in the same subnet, and it shuffles just one time and not 2. How we could select randomly an item from a map based on the number of VMs to be created ?
Thank you for your help

A couple issues. You cannot get the length of a number so this
count = length(var.VM_count)
should be
count = var.VM_count
Unsure what this line's intention is but this
sub_id = element(data.vsphere_sub.sub.*.id, (count.index)%length(data.vsphere_sub.sub.id))
should be this if we want a different subnet
sub_id = element(data.vsphere_sub.sub.*.id, count.index)
so the final result would be
resource "random_shuffle" "az" {
input = "${var.sub_list[local.data]}"
result_count = "${var.VM_count}"
}
data "vsphere_sub" "sub" {
count = "${var.VM_count}"
name = "${random_shuffle.az.result[count.index]}"
}
resource "vsphere_virtual_machine" "VM" {
name = "${var.VM_name}"
folder = "${var.folder}"
count = "${var.VM_count}"
sub_id = "${element(data.vsphere_sub.sub.*.id, count.index)}"
num_cpus = "${var.VM_vcpu}"
memory = "${var.VM_memory}"
}
Now when you apply with a VM_count=2, it should grab 2 random subnets from sub_list and create 2 VMs with each having a different subnet.

Related

Referencing a created subnet and associate it with a given EIP for a NLB (aws provider)

I'm trying to parametrize the creation of a NLB, and provision in the same plan the necessary public subnets.
The subnets are specified as a variable of the plan:
variable "nlb_public_subnets" {
type = list(object({
name = string
network_number = number
availability_zone = string
elastic_ip = string
}))
default = [
{
name = "sftp_sub_A"
network_number = 1
availability_zone = "eu-west-1a"
elastic_ip = "X.Y.Z.T"
},
{
name = "sftp_sub_B"
network_number = 2
availability_zone = "eu-west-1b"
elastic_ip = "XX.YY.ZZ.TT"
}
]
}
variable "common_tags" {
description = "A map containing the common tags to apply to all resources"
type = map(string)
default = {}
}
locals {
vpc_id = "dummy"
base_cidr = "10.85.23.0/24"
publicSubnets = { for s in var.nlb_public_subnets :
s.name => {
name = s.name
cidr_block = cidrsubnet(var.base_public_subnet_cidr_block, 6,
s.network_number )
availability_zone = s.availability_zone
elastic_ip = s.elastic_ip
}
}
}
I'm specifying a name, a network number (to compute the cidr block), an availability zone, and an elastic IP to map to when creating the NLB.
Here I'm creating the subnets:
#Comment added after solution was given
#This will result in a Map indexed by subnet.name provided in var.nlb_public_subnets
resource "aws_subnet" "sftp_nlb_subnets" {
for_each = { for subnet in local.publicSubnets :
subnet.name => subnet
}
cidr_block = each.value.cidr_block
vpc_id = local.vpc_id
availability_zone = each.value.availability_zone
tags = {
Name = each.key
Visibility = "public"
Purpose = "NLB"
}
}
Now I need to create my NLB, and this is where I'm struggling on how to associate the freshly created subnets with the Elastic IP provided in the configuration:
resource "aws_lb" "sftp" {
name = var.name
internal = false
load_balancer_type = "network"
subnets = [for subnet in aws_subnet.sftp_nlb_subnets: subnet.id]
enable_deletion_protection = true
tags = merge(var.common_tags,{
Name=var.name
})
dynamic "subnet_mapping" {
for_each = aws_subnet.sftp_nlb_subnets
content {
subnet_id = subnet_mapping.value.id
allocation_id = ????Help???
}
}
}
Could I somehow look up the configuration object with the help of the subnet name in the tags?
UPDATE1
Updated the dynamic block, as it had a typo.
UPDATE2
#tmatilai nailed the answer!
Here's the modified aws_lb block:
#
#This will result in a Map indexed by subnet.name provided in var.nlb_public_subnets
data "aws_eip" "nlb" {
for_each = local.publicSubnets
public_ip = each.value.elastic_ip
}
resource "aws_lb" "sftp" {
name = var.name
internal = false
load_balancer_type = "network"
subnets = [for subnet in aws_subnet.sftp_nlb_subnets : subnet.id]
enable_deletion_protection = true
tags = merge(var.common_tags, {
Name = var.name
})
dynamic "subnet_mapping" {
#subnet_mapping.key will contain subnet.name, so we can use it to access the Map data.aws_eip.nlb (also indexed by subnet.name) to get the eip allocation_id
for_each = aws_subnet.sftp_nlb_subnets
content {
subnet_id = subnet_mapping.value.id
allocation_id = data.aws_eip.nlb[subnet_mapping.key].id
}
}
}
The trick is to realize that both aws_subnet.sftp_nlb_subnets and data.aws_eip.nlb are a Map, indexed by the key of local.publicSubnets. This allows us to use this common key (the subnet name) in the map aws_subnet.sftp to look up information in the data (data.aws_eip.nlb) obtained from the original input, local.publicSubnets.
Thanks. This is a neat trick.
Passing the IP address of the elastic IPs sounds strange. If you create the EIPs elsewhere, why not pass the (allocation) ID of them instead?
But with this setup, you can get the allocation ID with the aws_eip data source:
data "aws_eip" "nlb" {
for_each = local.publicSubnets
public_ip = each.value.elastic_ip
}
resource "aws_lb" "sftp" {
# ...
dynamic "subnet_mapping" {
for_each = aws_subnet.sftp_nlb_subnets
content {
subnet_id = subnet_mapping.value.id
allocation_id = data.aws_eip.nlb[subnet_mapping.key].id
}
}
}
But maybe it would make more sense to create the EIPs also here. For example something like this:
resource "aws_eip" "nlb" {
for_each = local.publicSubnets
vpc = true
}
resource "aws_lb" "sftp" {
# ...
dynamic "subnet_mapping" {
for_each = aws_subnet.sftp_nlb_subnets
content {
subnet_id = subnet_mapping.value.id
allocation_id = aws_eip.nlb[subnet_mapping.key].id
}
}
}

if condition in terraform in count

I am adding autoscale settings in the Azure cosmosdb database, My problem is not all our db requires autoscale only a selection of database require autoscalse rest are manual. I will not be able to specify the autoscalse block also the throughout in the same resource as there are conflicts between those two. so I thought of using the count but I will be not be able to run the resouece block for only one of the DB. for the below example
variable
variable "databases" {
description = "The list of Cosmos DB SQL Databases."
type = list(object({
name = string
throughput = number
autoscale = bool
max_throughput = number
}))
default = [
{
name = "testcoll1"
throughput = 400
autoscale = false
max_throughput = 0
},
{
name = "testcoll2"
throughput = 400
autoscale = true
max_throughput = 1000
}
]
}
For the first I dont need autoscale and next one I need. My main.tf code
resource "azurerm_cosmosdb_mongo_database" "database_manual" {
count = length(var.databases)
name = var.databases[count.index].name
resource_group_name = azurerm_cosmosdb_account.cosmosdb.resource_group_name
account_name = local.account_name
throughput = var.databases[count.index].throughput
}
resource "azurerm_cosmosdb_mongo_database" "database_autoscale" {
count = length(var.databases)
name = var.databases[count.index].name
resource_group_name = azurerm_cosmosdb_account.cosmosdb.resource_group_name
account_name = local.account_name
autoscale_settings {
max_throughput = var.databases[count.index].max_throughput
}
}
First I thought of running two blocks one with scale and on without, but I will not be able to proceed because it requires the count numbers
count = var.autoscale_required == true ? len(databases) : 0
at the start but in my case I will only know at the time of iteration. I have tried to use dynamic within the block but errored out.
*Update
I have switched to foreach and able to run the condition but still it requires 2 blocks
resource "azurerm_cosmosdb_mongo_database" "database_autoscale"
resource "azurerm_cosmosdb_mongo_database" "database_manual"
resource "azurerm_cosmosdb_mongo_database" "database_autoscale" {
for_each = {
for key, value in var.databases : key => value
if value.autoscale_required == true }
name = each.value.name
resource_group_name = azurerm_cosmosdb_account.cosmosdb.resource_group_name
account_name = local.account_name
autoscale_settings {
max_throughput = each.value.max_throughput
}
}
If I understand correctly, I think you could do what you want using the following:
resource "azurerm_cosmosdb_mongo_database" "database_autoscale" {
count = length(var.databases)
name = var.databases[count.index].name
resource_group_name = azurerm_cosmosdb_account.cosmosdb.resource_group_name
account_name = local.account_name
throughput = var.databases[count.index].autoscale == false ? var.databases[count.index].throughput : null
dynamic "autoscale_settings" {
for_each = var.databases[count.index].autoscale == false ? [] : [1]
content {
max_throughput = var.databases[count.index].max_throughput
}
}
}

looping over local map of bojects with for expression on a resouce that already has a for_loop

How could i achieve looping over local.daily map so i could create the backup policy name based on the map value on a resource that already uses a for_each loop ?
On the following example, on the resource azurerm_backup_policy_file_share i would like to populate the name field with the value of local.daily["name"] value.
locals {
regions = [
"centralus",
"northeurope"
]
}
resource "azurerm_resource_group" "recovery_vault" {
name = "recovery-vault-${terraform.workspace}-rg"
location = var.azure_region
tags = {
environment = terraform.workspace
source = "terraform"
service = "Backup Vault"
}
}
resource "azurerm_recovery_services_vault" "vaults" {
for_each = toset(local.regions)
name = "recovery-vault-${terraform.workspace}-${each.key}"
location = each.key
resource_group_name = azurerm_resource_group.recovery_vault.name
sku = "Standard"
soft_delete_enabled = true
}
locals {
daily = [{
name = "Every23h"
frequency = "Daily"
time = "23:00"
count = 30
}
]
}
resource "azurerm_backup_policy_file_share" "daily" {
for_each = azurerm_recovery_services_vault.vaults
name = "need this field to be name retrieved from local.daily"
resource_group_name = each.value["resource_group_name"]
recovery_vault_name = each.value["name"]
timezone = "UTC"
dynamic "backup" {
for_each = local.daily
content {
frequency = backup.value["frequency"]
time = backup.value["time"]
}
}
dynamic "retention_daily" {
for_each = local.daily
content {
count = retention_daily.value["count"]
}
}
}
I guess what you need is setproduct.
It should look something like this:
resource "azurerm_backup_policy_file_share" "daily" {
for_each = toset(setproduct(azurerm_recovery_services_vault.vaults, local.daily))
name = each.value[1].name
resource_group_name = each.value[0]["resource_group_name"]
# ...

Terraform: Retrieving each subnet gateway address from a list. GCP

Had a question answered earlier to help with creating a set of subnets off a list. Now I am trying to output each assigned ip address.
module "subnets" {
source = "../../../Modules/subnets-test/"
network_name = module.vpc.network_name
subnet_region = "europe-west2"
subnets = {
lister = "192.2.128.0/18",
kryten = "192.2.0.0/17",
rimmer = "192.2.208.0/20",
cat = "192.2.192.0/20",
holly = "192.2.224.0/20"
}
}
I can successfully output the list of subnets and their values
output "private_subnets" {
description = "List of IDs of private subnets"
value = ["${module.subnets.subnets}"]
}
Giving me all of the subnet outputs (one as example below)
"rimmer" = {
"creation_timestamp" = "2020-06-06T03:13:30.244-07:00"
"description" = ""
"gateway_address" = "192.2.208.1"
"id" = "projects/red-dwarf/regions/europe-west2/subnetworks/rimmer"
"ip_cidr_range" = "192.2.208.0/20"
"log_config" = []
"name" = "rimmer"
"network" = "https://www.googleapis.com/compute/v1/projects/red-dwarf/global/networks/red-dwarf-vpc"
"private_ip_google_access" = false
"project" = "red-dwarf"
"region" = "europe-west2"
"secondary_ip_range" = []
"self_link" = "https://www.googleapis.com/compute/v1/projects/red-dwarf/regions/europe-west2/subnetworks/rimmer"
But now I just want to extract the gateway address as a single output. But anything I try to do to the module gives me an error saying the list has 5 attributes.
How can I pull the attributes out of the created subnets when they've been provisioned via map(string) ?
Edit - Subnet Module
resource "google_compute_subnetwork" "subnet" {
network = var.network_name
for_each = var.subnets
name = each.key
ip_cidr_range = each.value
}
Edit - Subnet Output - This works to output everything as a whole.
output "subnets" {
value = google_compute_subnetwork.subnet
description = "The created subnet resources"
}
Here is what I would do:
locals {
subnets = {
cow = "10.0.208.0/20",
cat = "10.0.192.0/20",
dog = "10.0.224.0/20"
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_vpc" "myvpc" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "subnet" {
vpc_id = aws_vpc.myvpc.id
for_each = local.subnets
cidr_block = each.value
tags = { Name = each.key }
}
output "subnets" {
value = aws_subnet.subnet
}
output "subnets_arn" {
value = { for k, v in aws_subnet.subnet : k => v.arn }
}
The key there is the for loop:
value = { for k, v in aws_subnet.subnet : k => v.arn }
that creates a new object with key the name and value any property we want.
The terraform output of an apply is:
subnets_arn = {
"cat" = "arn:aws:ec2:us-east-1:841836440307:subnet/subnet-046fff167cdc81e9f"
"cow" = "arn:aws:ec2:us-east-1:841836440307:subnet/subnet-00217a1ec0531d2c6"
"dog" = "arn:aws:ec2:us-east-1:841836440307:subnet/subnet-0ac82ef0fd87bcee2"
}
In my case I'm using AWS (that is what I got access to right now) but the same should translate to GCP, just use the property you need, educated guess should be something like:
output "subnets_gateway_address" {
value = { for k, v in aws_subnet.subnet : k => v.gateway_address }
}

Terraform: How to set variables in a module based on a conditional?

I would like to pass a variable that will allow me to specify the list of VPC and subnet settings for an AWS instance. There are fixed VPC and subnet settings that make sense so I just want to allow a user to pick one using a single variable, i.e. use A or B.
For instance, let's say I have two available VPCs, and these are specified in a variables.tf file for a module my_instance:
variable "a_vpc_cidr_block" { default = "105.191.44.0/22" }
variable "a_vpc_id" { default = "id_a"}
variable "a_vpc_name" { default = "vpc_a" }
variable "a_subnet_availability_zone" { default = "us-east-1a" }
variable "a_subnet_cidr_block" { default = "105.191.25.0/25" }
variable "a_subnet_name" { default = "instance_A" }
variable "b_vpc_cidr_block" { default = "105.191.45.0/22" }
variable "b_vpc_id" { default = "id_b"}
variable "b_vpc_name" { default = "vpc_b" }
variable "b_subnet_availability_zone" { default = "us-east-1a" }
variable "b_subnet_cidr_block" { default = "105.191.35.0/25" }
variable "b_subnet_name" { default = "instance_B" }
The my_instance module will take a single input variable that an environment will specify, with a value of either 'A' or 'B' (is there a way to limit options for a variable to a list of values such as options=['A', 'B']?), and will be called like so in the terraform.tf for a Terraform configuration with a single instance:
module "my_instance" {
source = "../../modules/my_instance"
option = "A"
}
I want to now implement some logic within the module's main file (modules/my_instance/my_instance.tf) where it decides on which of the two collections of VPC and subnet settings it should use from the ones in modules/my_instance/variables.tf. I want to something like this (pseudocode):
if var.option == 'A'
vpc_cidr_block = var.a_vpc_cidr_block
vpc_id = var.a_vpc_id
vpc_name = var.a_vpc_name
subnet_availability_zone = var.a_subnet_availability_zone
subnet_cidr_block = var.a_subnet_cidr_block
subnet_name = var.a_subnet_name
else if var.option == 'B'
vpc_cidr_block = var.b_vpc_cidr_block
vpc_id = var.b_vpc_id
vpc_name = var.b_vpc_name
subnet_availability_zone = var.b_subnet_availability_zone
subnet_cidr_block = var.b_subnet_cidr_block
subnet_name = var.b_subnet_name
else
raise an error
# get a data resource identified by the VPC variables
data "aws_vpc" "instance_vpc" {
cidr_block = var.vpc_cidr_block
tags = {
Name = var.vpc_name
}
}
# get a data resource identified by the VPC variables
data "aws_subnet" "instance_subnet" {
vpc_id = var.vpc_id
cidr_block = var.subnet_cidr_block
availability_zone = var.subnet_availability_zone
tags = {
Name = var.subnet_name
}
}
# create an AWS key pair resource
resource "aws_key_pair" "instance_aws_key_pair" {
key_name = "component_key_${terraform.workspace}"
public_key = file("~/.ssh/terraform.pub")
}
# create the AWS EC2 instance
resource "aws_instance" "my_aws_instance" {
key_name = aws_key_pair.instance_aws_key_pair.key_name
ami = "ami-b12345"
instance_type = "t2.micro"
subnet_id = data.aws_subnet.instance_subnet.id
connection {
type = "ssh"
user = "terraform"
private_key = file("~/.ssh/terraform")
host = self.public_ip
}
tags = {
"Name" : "my_instance_name"
"Terraform" : "true"
}
}
Is this a matter of somehow using a count, something like this:
count = var.option == 'A'? 1 : 0
Is there a way to do this, or is there a better approach? I am very new to Terraform so I may be missing something obvious.
You have a couple of questions here.
Firstly, you should be able to use the newer, experimental custom validation rules to assert that a value is in a specific list of values.
Secondly, for determining which set of variables to use, I'd recommend going with a good old map in a local value.
For example,
locals {
vpc_info = {
"A" = {
vpc_cidr_block = var.a_vpc_cidr_block
vpc_id = var.a_vpc_id
vpc_name = var.a_vpc_name
subnet_availability_zone = var.a_subnet_availability_zone
subnet_cidr_block = var.a_subnet_cidr_block
subnet_name = var.a_subnet_name
}
"B" = {
vpc_cidr_block = var.b_vpc_cidr_block
vpc_id = var.b_vpc_id
vpc_name = var.b_vpc_name
subnet_availability_zone = var.b_subnet_availability_zone
subnet_cidr_block = var.b_subnet_cidr_block
subnet_name = var.b_subnet_name
}
}
}
Then you should be able to reference a specific field, within the chose option like the following
local.vpc_info[var.option].vpc_name
Let me know if this hits all your questions.

Resources