Terraform: Loops to dynamically add acl rules - terraform

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
}
]
}

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!

how to iterate though list of numbers and use key as string in terraform

Terraform syntax question. I am trying to minimise the logic creating security groups by simply looping over a list of local variables, here is the example
locals {
egress = {
udp = [
53,
123
]
}
ingress = {
tcp = [
443,
22,
]
}
}
and a resource:
resource "aws_security_group_rule" "in_tcp" {
#for_each = [ for k, v in local.ingress : local.ingress.k => v ]
#for_each = [ for port in local.ingress : port => port ]
#for_each = [ for proto in local.ingress : proto ]
#for_each = [ for k, v in local.ingress : k ]
for_each = local.ingress
type = "ingress"
from_port = each.value
to_port = each.value
protocol = tostring(each.key)
self = true
security_group_id = aws_security_group.main.id
}
I have tried everything but nothing works. I guess I do not understand the meaning of [ for ... ] and { for ... }
The best result so far is as above, using simple for_each = local.ingress, but I get the error:
Error: Incorrect attribute value type
│
│ on sg-rules/main.tf line 38, in resource "aws_security_group_rule" "in_tcp":
│ 38: from_port = each.value
│ ├────────────────
│ │ each.value is tuple with 2 elements
│
│ Inappropriate value for attribute "from_port": number required.
when I convert each.value to number using from_port = tonumber(each.value) I get another error:
│ Error: Invalid function argument
│
│ on sg-rules/main.tf line 38, in resource "aws_security_group_rule" "in_tcp":
│ 38: from_port = tonumber(each.value)
│ ├────────────────
│ │ while calling tonumber(v)
│ │ each.value is tuple with 2 elements
│
│ Invalid value for "v" parameter: cannot convert tuple to number.
How about changing the local to be a bit more loop friendly...
Something like this
locals {
ingress = {
443 : "tcp",
22 : "tcp"
}
}
resource "aws_security_group_rule" "in_tcp" {
for_each = local.ingress
type = "ingress"
from_port = each.key
to_port = each.key
protocol = each.value
self = true
security_group_id = "sg-123456"
}
The TF plan output now looks like:
Terraform will perform the following actions:
# aws_security_group_rule.in_tcp["22"] will be created
+ resource "aws_security_group_rule" "in_tcp" {
+ from_port = 22
+ id = (known after apply)
+ protocol = "tcp"
+ security_group_id = "sg-123456"
+ security_group_rule_id = (known after apply)
+ self = true
+ source_security_group_id = (known after apply)
+ to_port = 22
+ type = "ingress"
}
# aws_security_group_rule.in_tcp["443"] will be created
+ resource "aws_security_group_rule" "in_tcp" {
+ from_port = 443
+ id = (known after apply)
+ protocol = "tcp"
+ security_group_id = "sg-123456"
+ security_group_rule_id = (known after apply)
+ self = true
+ source_security_group_id = (known after apply)
+ to_port = 443
+ type = "ingress"
}
Plan: 2 to add, 0 to change, 0 to destroy.
First thing: local.ingress is an object with key = "tcp" and value = [443, 22]. Second: foreach only accepts map or set of strings. You want a map indexed by "<proto_port>" like this:
{
"tcp_22" = {
"port" = 22
"proto" = "tcp"
}
"tcp_443" = {
"port" = 443
"proto" = "tcp"
}
}
This is how you do it:
resource "aws_security_group_rule" "in_tcp" {
for_each = {
for p in flatten([
for proto, ports in local.ingress: [
for port in ports: { proto = proto, port = port }
]
]): "${p.proto}_${p.port}" => p
}
type = "ingress"
from_port = tonumber(each.value.port)
to_port = tonumber(each.value.port)
protocol = tostring(each.value.proto)
self = true
security_group_id = aws_security_group.main.id
}
How about using strings instead of numbers within your locals and using "toset" function in the for_each block. Then you create a aws_security_group_rule for each protocol respectively:
locals {
egress = {
udp = [
"53",
"123"
]
}
ingress = {
tcp = [
"443",
"22",
]
}
}
resource "aws_security_group_rule" "in_tcp" {
for_each = toset(local.ingress.tcp)
type = "ingress"
from_port = tonumber(each.value)
to_port = tonumber(each.value)
protocol = "tcp"
self = true
security_group_id = aws_security_group.main.id
}
resource "aws_security_group_rule" "out_udp" {
for_each = toset(local.egress.udp)
type = "egress"
from_port = tonumber(each.value)
to_port = tonumber(each.value)
protocol = "udp"
self = true
security_group_id = aws_security_group.main.id
}

single terraform module for dependant argumnets

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"
},
]
}

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