single terraform module for dependant argumnets - terraform

Here, I have been implementing terraform modules for existing terraform script. I have been facing an issue while interacting with arguments of security_group_rules.
The issue is,in aws_security_group_rule, we have two arguments i.e., source_security_group_id & cidr_block which are incompatible with each other. I mean when we use one of it, we can't use another.
This is my module.
main.tf
resource "aws_security_group_rule" "arvn" {
count = length(var.security_group_rules)
type = var.security_group_rules[count.index].type
from_port = var.security_group_rules[count.index].from_port
to_port = var.security_group_rules[count.index].to_port
protocol = var.security_group_rules[count.index].protocol
cidr_blocks = var.security_group_rules[count.index].cidr_block
description = var.security_group_rules[count.index].description
security_group_id = var.security_group_id
}
variable.tf
variable "security_group_id" {
type = string
}
variable "security_group_rules" {
type = list(object({
type = string
from_port = number
to_port = number
protocol = string
cidr_block = list(string)
description = string
}))
}
usage
sg.tf
module "security_group_ecsInstance" {
source = "./modules/security_group"
vpc_id = aws_vpc.arvn.id
name = "${local.name}-ecsInstance"
}
module "sg_rules_instance" {
source = "./modules/security_group_rules"
security_group_id = module.security_group_instance.id
security_group_rules = [
{ type = "ingress", from_port = 22, to_port = 22, protocol = "tcp", cidr_block = [var.vpc_cidr], description = "ssh" },
{ type = "egress", from_port = 0, to_port = 65535, protocol = "-1", cidr_block = ["0.0.0.0/0"], description = "" },
{ type = "ingress", from_port = 0, to_port = 65535, protocol = "tcp", cidr_block = [module.security_group_alb.id], description = "alb" }
]
}
In this, first two rules are being created and last rule is failing because of invalid cidr block.
I'm aware of the issue here however, It would be great if anyone help me in creating more flexible module that can work on both source_security_group_id & cidr_block such that if one is used another should go blind.

You can represent the dynamic absence of a resource argument by setting it to null. That means you can define a variable that accepts both arguments as long as one of them is null. For example:
variable "security_group_rules" {
type = list(object({
type = string
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
source_security_group_id = string
description = string
}))
}
resource "aws_security_group_rule" "arvn" {
count = length(var.security_group_rules)
type = var.security_group_rules[count.index].type
from_port = var.security_group_rules[count.index].from_port
to_port = var.security_group_rules[count.index].to_port
protocol = var.security_group_rules[count.index].protocol
cidr_blocks = var.security_group_rules[count.index].cidr_blocks
description = var.security_group_rules[count.index].description
source_security_group_id = var.security_group_rules[count.index].source_security_group_id
security_group_id = var.security_group_id
}
When calling the module, the caller must set either cidr_block or source_security_group_id to null in order to avoid the conflict error:
module "sg_rules_instance" {
source = "./modules/security_group_rules"
security_group_id = module.security_group_instance.id
security_group_rules = [
{
type = "ingress"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.vpc_cidr]
source_security_group_id = null
description = "ssh"
},
{
type = "egress"
from_port = 0
to_port = 65535
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
source_security_group_id = null
description = ""
},
{
type = "ingress"
from_port = 0
to_port = 65535
protocol = "tcp"
cidr_blocks = null
source_security_group_id = module.security_group_alb.id
description = "alb"
},
]
}

Related

How to create security group ingress dynamically in terraform

I am creating a security group that has some standard ingress rules. I also want to add additional ingress rules based on a variable.
variable "additional_ingress" {
type = list(object({
protocol = string
from_port = string
to_port = string
cidr_blocks = list(string)
}))
default = []
}
resource "aws_security_group" "ec2" {
name = "my-sg"
description = "SG for ec2"
vpc_id = data.aws_vpc.this.id
egress {
to_port = 0
from_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
protocol = "tcp"
from_port = 22
to_port = 22
cidr_blocks = ["10.0.0.0/8"]
}
# rdp
ingress {
protocol = "tcp"
from_port = 3389
to_port = 3389
cidr_blocks = ["10.0.0.0/8"]
}
# additional ingress rules
ingress {
for_each = var.additional_ingress
protocol = each.value.protocol
from_port = each.value.from_port
to_port = each.value.to_port
cidr_blocks = each.value.cidr_blocks
}
}
I am getting error
A reference to "each.value" has been used in a context in which it
unavailable, such as when the configuration no longer contains the
value in its "for_each" expression. │ Remove this reference to
each.value in your configuration to work around this error.
How do I add ingress rules based on variable
This is most easily managed with the aws_security_group_rule resource and the for_each meta-argument:
resource "aws_security_group_rule" "ec2" {
for_each = var.additional_ingress
type = each.value.type
from_port = each.value.from_port
to_port = each.value.to_port
protocol = each.value.protocol
cidr_blocks = each.value.cidr_blocks
security_group_id = aws_security_group.ec2.id
}
Note that the variable declaration for additional_ingress is missing the type key in its object constructor definition, so that would need to be added:
variable "additional_ingress" {
type = list(object({
type = string
...
}))
default = []
}
You can use dynamic blocks like this:
dynamic "ingress" {
for_each = var.additional_ingress
protocol = ingress.value.protocol
from_port = ingress.value.from_port
to_port = ingress.value.to_port
cidr_blocks = ingress.value.cidr_blocks
}
Provided the additional_ingress is an object (map) of ingress entries.
Of course, I would advise to use aws_security_group_rule resource, but if this is already live project, I understand if you'd want to stay with inline rules. Have fun!

Terraform: Loops to dynamically add acl rules

How can I Use Loops to dynamically add acl rules ? I want to as example:
variable "protocol" will get all protocol values form the list of the objects [protocol1, protocol2, protocol3].
My code structure is :
-- Dev
-- main.tf
-- vars.tf
-- modules
-- acl
-- ressources.tf
This is my vars.tf
variable "acl_rules" {
type = list (object({
protocol = string
rule_no = number
action = string
cidr_block = string
from_port = number
to_port = number
}))
default = [
{ protocol = "tcp", rule_no = 200, action = "allow", cidr_block = "10.3.0.0/18", from_port = 443, to_port = 443 },
{ protocol = "udp", rule_no = 100, action = "allow", cidr_block = "10.3.0.0/18", from_port = 54, to_port = 54 },
{ protocol = "http", rule_no = 300, action = "allow", cidr_block = "10.3.0.0/18", from_port = 80, to_port = 80 }
]
}
This is my main.tf
# ACL for public subnet
module "acl" {
source = "../modules/acl"
vpc_id = module.vpc.vpcId
pub_snId_aza = element(module.pub-sn.snId[*], 0)
for_each = [for rule_obj in var.acl_rules :{
protocol = var.rule_obj.protocol
rule_no = var.rule_obj.rule_no
action = var.rule_obj.action
cidr_block = var.rule_obj.cidr_block
from_port = var.rule_obj.from_port
to_port = var.rule_obj.to_port
}
]
}

terraform: filter list of maps based on key

I'm implementing a security group modules such that it will create security group rules by taking & filtering cidr & source_security_group_id to create a security group rule.
The current module configuration.
securty_group_module.tf
resource "aws_security_group" "this" {
name = var.name
description = var.description
vpc_id = var.vpc_id
revoke_rules_on_delete = var.revoke_rules_on_delete
}
## CIDR Rule
resource "aws_security_group_rule" "cidr_rule" {
count = length(var.security_group_rules)
type = var.security_group_rules[count.index].type
from_port = var.security_group_rules[count.index].from_port
to_port = var.security_group_rules[count.index].to_port
protocol = var.security_group_rules[count.index].protocol
cidr_blocks = var.security_group_rules[count.index].cidr_block
description = var.security_group_rules[count.index].description
security_group_id = aws_security_group.this.id
}
## Source_security_group_id Rule
resource "aws_security_group_rule" "source_sg_id_rule" {
count = length(var.security_group_rules)
type = var.security_group_rules[count.index].type
from_port = var.security_group_rules[count.index].from_port
to_port = var.security_group_rules[count.index].to_port
protocol = var.security_group_rules[count.index].protocol
source_security_group_id = var.security_group_rules[count.index].source_security_group_id
description = var.security_group_rules[count.index].description
security_group_id = aws_security_group.this.id
}
main.tf
module "sample_sg" {
source = "./modules/aws_security_group"
name = "test-sg"
vpc_id = "vpc-xxxxxx"
security_group_rules = [
{ type = "ingress", from_port = 22, to_port = 22, protocol = "tcp", cidr_block = [var.vpc_cidr], description = "ssh" },
{ type = "ingress", from_port = 80, to_port = 80, protocol = "tcp", cidr_block = [var.vpc_cidr], description = "http" },
{ type = "ingress", from_port = 0, to_port = 0, protocol = "-1", source_sg_id = "sg-xxxx", description = "allow all" }
{ type = "egress", from_port = 0, to_port = 0, protocol = "-1", source_sg_id = "sg-xxxx", description = "allow all" }
]
}
So, the problem statement here is when I call the security group rules in the module with the above list of maps, it should check if it is source_sg_id or cidr.
Then filter those maps & pass it to respective resources in the module.
Ex:
module ""{
...
security_group_rules = [
{ type = "ingress", from_port = 22, to_port = 22, protocol = "tcp", cidr_block = [var.vpc_cidr], description = "ssh" },
{ type = "ingress", from_port = 0, to_port = 65535, protocol = "-1", source_sg_id = "sg-xxxx", description = "allow all" }
]
}
These rules should be looked up & pass the first one to CIDR rule & second one to Source_security_group_id rule.
I'm thinking of making it as below
locals {
sid_rules = some_function{var.security_group_rules, "source_security_group_id"}
cidr_rules = some_function{var.security_group_rules, "cidr"}
}
resource "aws_security_group_rule" "cidr_rule" {
count = count(local.cidr_rules)
....
cidr_blocks = local.cidr_rules[count.index].cidr_block
....
}
resource "aws_security_group_rule" "sid_rule" {
count = count(local.sid_rules)
....
source_security_group_id = local.sid_rules[count.index].source_sg_id
....
}
So, I'm looking for a way to filter the maps from list based on a key
I have tried lookup but was no help in case of list of string.
I figured out a clever way to do this.
Let's say I am trying to filter only the pets that are cats kind = "cat" from a list of pets.
variable "pets" {
type = list(object({
name = string
kind = string
}))
default = [
{
name = "Fido"
kind = "dog"
},
{
name = "Max"
kind = "dog"
},
{
name = "Milo"
kind = "cat"
},
{
name = "Simba"
kind = "cat"
}
]
}
First convert the list of pets to a map pets_map of pets using the index tostring(i) as the key.
This will be used in step 3 to lookup the filtered pets.
locals {
pets_map = { for i, pet in var.pets : tostring(i) => pet }
}
Next create a filtered list of the keys that respectively matches the condition pet.kind == "cat"
by looping over the keys in the pets_map and setting the respective keys that do not match to an
empty string. Then compact the list which removes the empty strings from the list.
locals {
cats_keys = compact([for i, pet in local.pets_map : pet.kind == "cat" ? i : ""])
}
Loop over the filtered keys cats_keys and lookup the respective pet from the pets_map. Now you
have the filtered list of pets that are cats kind = "cat".
locals {
cats = [for key in local.cats_keys : lookup(local.pets_map, key)]
}
You can now access the cats with local.cats, which will give you the following map.
{
name = "Milo"
kind = "cat"
},
{
name = "Simba"
kind = "cat"
}
Below is the full example.
variable "pets" {
type = list(object({
name = string
kind = string
}))
default = [
{
name = "Fido"
kind = "dog"
},
{
name = "Max"
kind = "dog"
},
{
name = "Milo"
kind = "cat"
},
{
name = "Simba"
kind = "cat"
}
]
}
locals {
pets_map = { for i, pet in var.pets : tostring(i) => pet }
cats_keys = compact([for i, pet in local.pets_map : pet.kind == "cat" ? i : ""])
cats = [for key in local.cats_keys : lookup(local.pets_map, key)]
}
Consider creating another module to handle the rules, and setting the security group resources inside that module.
module "security_groups" {
count = length(var.security_group_rules)
source_sg_id_rule = var.security_group_rules[count.index].source_sg_id_rule
}
Then, in the new module, use a count statement as a test to create optional items:
resource "aws_security_group_rule" "source_sg_id_rule" {
count = length(var.source_sg_id_rule) == 0 ? 0 : 1
type = var.type
from_port = var.from_port
to_port = var.to_port
protocol = var.protocol
source_security_group_id = var.source_security_group_id
description = var.description
security_group_id = var.security_group_id
}
This will create the resources as an array of one or zero items, and drop any lists of zero.
Thanks for the response #dan-monego.
I sorted it out with single module itslef.
Following is the module file.
aws_sg_module.tf
# Security group
##########################
resource "aws_security_group" "this" {
name = var.name
description = var.description
vpc_id = var.vpc_id
revoke_rules_on_delete = var.revoke_rules_on_delete
tags = merge(
{
"Name" = format("%s", var.name)
},
local.default_tags,
var.additional_tags
)
}
resource "aws_security_group_rule" "cidr" {
count = var.create ? length(var.cidr_sg_rules) : 0
type = var.cidr_sg_rules[count.index].type
from_port = var.cidr_sg_rules[count.index].from
to_port = var.cidr_sg_rules[count.index].to
protocol = var.cidr_sg_rules[count.index].protocol
cidr_blocks = var.cidr_sg_rules[count.index].cidr
description = var.cidr_sg_rules[count.index].description
security_group_id = local.this_sg_id
}
resource "aws_security_group_rule" "source_sg" {
count = var.create ? length(var.source_sg_rules) : 0
type = var.source_sg_rules[count.index].type
from_port = var.source_sg_rules[count.index].from
to_port = var.source_sg_rules[count.index].to
protocol = var.source_sg_rules[count.index].protocol
source_security_group_id = var.source_sg_rules[count.index].source_sg_id
description = var.source_sg_rules[count.index].description
security_group_id = local.this_sg_id
}
resource "aws_security_group_rule" "self" {
count = var.create ? length(var.self_sg_rules) : 0
self = true
type = var.source_sg_rules[count.index].type
from_port = var.source_sg_rules[count.index].from
to_port = var.source_sg_rules[count.index].to
protocol = var.source_sg_rules[count.index].protocol
description = var.source_sg_rules[count.index].description
security_group_id = local.this_sg_id
}
Call it using following module block.
security_groups.tf
module "stack_sg" {
source = "./modules/aws_security_group"
name = "stack-sg"
vpc_id = module.network.vpc_id
cidr_sg_rules = [
{ type = "ingress", from = 80, to = 80, protocol = "tcp", cidr = [module.network.vpc_cidr], description = "http" },
{ type = "egress", from = 0, to = 65535, protocol = "-1", cidr = ["0.0.0.0/0"], description = "allow all " }
]
source_sg_rules = [
{ type = "ingress", from = 0, to = 65535, protocol = "tcp", source_sg_id = module.alb_sg.sg_id, description = "alb" }
]
}
In order to filter a list of maps using a specific key value. You can use the following simple statement:
Assuming:
key is map's key you're filtering on
val is the value of the key
list is the original list of maps
element([
for element in list) : env
if element.key == "val"
], 0)
the result of the above statement will be a map.
Given:
[{foo: "...", baz: "..."}, ...]
And you want the baz of some element in this list, use:
element([
for o in [{"foo"="kick","baz"="5"},{"foo"="bar","baz"="100"}] :
o if o.foo == "bar"
],0).baz

How to access variables in a list / object in Terraform?

In Terraform, how can access the values from the variable below?
variable "egress_rules" {
type = list(object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
}))
default = [
{
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
]
}
I tried:
resource "aws_security_group_rule" "egress" {
security_group_id = aws_security_group.new.id
type = "ingress"
for_each = var.egress_rules
from_port = each.value.from_port
to_port = each.value.to_port
protocol = each.value.protocol
cidr_blocks = each.value.cidr_blocks
}
But got this error:
Error: Invalid for_each argument
What is the correct way to reference this variable?
for_each will not work with a list of maps. You have to convert it to a map. This is commonly done through a for expression:
resource "aws_security_group_rule" "egress" {
security_group_id = aws_security_group.new.id
type = "ingress"
for_each = { for idx, rule in var.egress_rules: idx => rule }
from_port = each.value.from_port
to_port = each.value.to_port
protocol = each.value.protocol
cidr_blocks = each.value.cidr_blocks
}
to set multiple properties of a resource you could use a map of objects like this:
variable "egress_rules" {
type = map(object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
}))
}
Your variable definition would be:
egress_rules = {
{
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
The updated resource definition would be:
resource "aws_security_group_rule" "egress" {
security_group_id = aws_security_group.new.id
type = "egress"
for_each = var.egress_rules
from_port = each.value["from_port"]
to_port = each.value["to_port"]
protocol = each.valuep["protocol"]
cidr_blocks = each.value["cidr_blocks"]
}
Adding to above answer from #Marcin
If you want to use single object instead of list then you can directly access without for_each
Instead of declaring it as list of objects; use single object as shown below
variable "egress_rules" {
type = object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
})
default = ({
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}}
}
Then access in your resource like
resource "aws_security_group_rule" "egress" {
security_group_id = aws_security_group.new.id
from_port = var.egress_rules.from_port
to_port = var.egress_rules.from_port
protocol = var.egress_rules.protocol
cidr_blocks = var.egress_rules.cidr_blocks
}

How do you access a value in a list of maps?

The value of google_compute_subnetwork.subnetwork.secondary_ip_range looks like this:
[
{
ip_cidr_range = 10.1.0.0/16,
range_name = my-range
}
]
I can't figure out how to loop over that, this doesn't work:
resource "aws_security_group_rule" "sdfsdfsdf" {
count = "${length(data.google_compute_subnetwork.mysubnetwork.secondary_ip_range)}"
type = "ingress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["${data.google_compute_subnetwork.mysubnetwork.secondary_ip_range[count.index]}.ip_cidr_range}"]
}
the usage of count.index is in this document:
https://www.terraform.io/docs/configuration-0-11/interpolation.html#element-list-index-
element(aws_subnet.foo.*.id, count.index)
So your code can be changed to
resource "aws_security_group_rule" "sdfsdfsdf" {
count = "${length(data.google_compute_subnetwork.mysubnetwork.secondary_ip_range)}"
type = "ingress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["${element(data.google_compute_subnetwork.mysubnetwork.secondary_ip_range.*.ip_cidr_range, count.index)}"]
}

Resources