How to create security group ingress dynamically in terraform - 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!

Related

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
}

Terraform 0.11 get VPC CIDRs from data source and pass to security_group_ingress_rule resource

My requirement is I need to get the CIDR address for vpc-foo and vpc-bar and pass on to the resource "aws_security_group_rule" "ingress"
I tried with the below code:
data "aws_vpcs" -> Get the ID for a given VPC
data "aws_vpc" -> make a list with VPC ids
resource "aws_security_group_rule" "ingress" -> pass VPC CIRDs as an ingress
variable "list_of_vps"{
type = "list"
default = ["vpc-foo", "vpc-bar"]
}
variable "sg_name" {
default = "sg-test"
}
data "aws_vpcs" "get_vpc"{
count = "$length(var.list_of_vps)"
filter {
name = "tag:Name"
values = ["vpc-${element(var.list_of_vps, count.index)}"]
}
}
data "aws_vpc" "get_vpc_ids" {
count = "${length(data.aws_vpcs.get_vpc.ids)}"
id = "${tolist(data.aws_vpcs.prod.ids)[count.index]}"
}
resource "aws_security_group_rule" "ingress" {
count = "${length(var.list_of_vps)}"
type = "ingress"
from_port = 22
to_port = 22
protocol = "TCP"
cidr_blocks = ["${element(data.aws_vpc.get_vpc_ids.*.cidr_block, count.index)}"]
security_group_id = "${var.sg_name}
}
Can someone help with this, please?

How to use resource reference into terraform workspace?

Currently, I am using terraform workspace to deploy the same code into multiple environments. But in right now I am stuck in referring resource in a specific workspace.
example of code
resource "aws_security_group" "testing-ec2" {
name = "${local.env}-testing-ec2"
vpc_id = "${aws_vpc.vpc.id}"
ingress {
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = ["${local.security-groups}"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["${local.bastion_ip}"]
}
egress {
from_port = 0
to_port = 0
protocol = -1
cidr_blocks = ["0.0.0.0/0"]
}
}
workspace security group
local {
tf_security-groups = {
dev = ""
stg = "${aws_security_group.test-private-alb.id}"
qa = "${aws_security_group.test1-private-alb.id}"
prod = "${aws_security_group.test2-private-alb.id}"
}
security-groups = "${lookup(local.tf_security-groups,local.env)}"
}
when I am trying to apply into stg workspace this error appears
* local.tf_security-groups: local.tf_security-groups: Resource 'aws_security_group.test1-private-alb' not found for variable 'aws_security_group.test1-private-alb.id'
You could use the data source terraform_remote_state to sift through the state but you'd also have to make each of your security group ids into outputs.
data "terraform_remote_state" "this" {
backend = "s3"
workspace = "stg"
config {
bucket = ""
key = ""
region = ""
}
}
It would be cleaner to use the aws_security_group data source instead.
locals {
env = "qa"
security_group_map = {
stg = data.aws_security_group.test_private_alb.id
qa = data.aws_security_group.test1_private_alb.id
prod = data.aws_security_group.test2_private_alb.id
}
security_groups = lookup(local.security_group_map, local.env, "")
}
data "aws_security_group" "test_private_alb" {
name = "test_private_alb"
}
data "aws_security_group" "test1_private_alb" {
name = "test1_private_alb"
}
data "aws_security_group" "test2_private_alb" {
name = "test2_private_alb"
}

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