Rule_nos in dynamic ingress in Terraform - terraform

I have a dynamic nested block to create a list of ingress rules in a Network ACL:
resource "aws_network_acl" "network_acl" {
vpc_id = aws_vpc.vpc.id
dynamic "ingress" {
for_each = var.ssh_cidr_blocks
iterator = cidr
content {
rule_no = 100
protocol = "tcp"
action = "allow"
cidr_block = cidr.value
from_port = 22
to_port = 22
}
}
}
As can be seen, I am generating an ingress for each CIDR in var.ssh_cidr_blocks.
This does not work however, and AWS sends back a message that the rule_no needs to be unique:
Error: Error creating ingress entry: NetworkAclEntryAlreadyExists: The network acl entry identified by 100 already exists.
status code: 400, request id: c9b4b5ad-c1a9-4a85-a4e0-b0559e14ea53
I am a bit confused because Network ACLs in dynamic ingress rules are a class use-case for the dynamic nested blocks. Yet this doesn't even seem possible!
Is there any way to do this?

Here is an option using range to avoid the duplicates on rule_no:
locals {
ssh_cidr_blocks = [
"10.0.208.0/20",
"10.0.192.0/20",
"10.0.224.0/20"
]
}
resource "aws_network_acl" "network_acl" {
vpc_id = aws_vpc.myvpc.id
dynamic "ingress" {
for_each = range(length(local.ssh_cidr_blocks))
iterator = i
content {
rule_no = i.value
protocol = "tcp"
action = "allow"
cidr_block = local.ssh_cidr_blocks[i.value]
from_port = 22
to_port = 22
}
}
}
Since all the acl rules are allow the order does not matter much and we can get away with that ...
But as you create more complex rules I'm not sure this will be acceptable since you will be mixing allow and deny and the order is important. You could follow #Kyle comment and use a map instead, in that case, the key is the rule_no, and the code will be something like:
locals {
ssh_cidr_blocks = {
100 = "10.0.208.0/20",
200 = "10.0.192.0/20",
500 = "10.0.224.0/20"
}
}
resource "aws_network_acl" "network_acl" {
vpc_id = aws_vpc.myvpc.id
dynamic "ingress" {
for_each = local.ssh_cidr_blocks
content {
rule_no = ingress.key
protocol = "tcp"
action = "allow"
cidr_block = ingress.value
from_port = 22
to_port = 22
}
}
}

Related

Terraform passing value to list only when a condition is true

I am trying to create a AWS VPC module using Terraform. I am making VPC secondary CIDR an optional feature of the module.
if secondary_cidr = true, then create subnets using the seocandary_cidr and network acls
The issue I am running into is with Network ACLs. Network ACLs creation using terraform uses a list subnet IDs to associate to the NACL. I want to create one network ACL to associate primary subnets and secondary subnets only when secondary_cidr=true
See the code below:
cidr1_subnets = {
CIDR1_SUBNETS = [aws_subnet.app1-az1.id, aws_subnet.app1-az2.id, aws_subnet.app1-az3.id]
}
cidr2_subnets = {
exists = {
CIDR2_SUBNETS = [aws_subnet.app2-az1.id, aws_subnet.app2-az2.id, aws_subnet.app2-az3.id]
}
not_exists = {}
}
}
resource "aws_network_acl" "app" {
vpc_id = aws_vpc.main.id
egress {
protocol = "-1"
rule_no = 100
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 0
to_port = 0
}
ingress {
protocol = "-1"
rule_no = 100
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 0
to_port = 0
}
subnet_ids = merge(
local.cidr1_subnets,
local.cidr2_subnets[var.secondary_cidr == true ? true : false]
)
}```
I think you are after the following:
subnet_ids = var.secondary_cidr == true ? merge(local.cidr1_subnets, local.cidr2_subnets) : local.cidr1_subnets
Btw, your locals and the use of merge will fail anyway, but this is a problem of other issue I guess.

Terraform v0.13 conditional resources with "count"

Having an issue creating a conditional resource based on a variable that's evaluated and used to influence a count in the resource. The issue is that the conditionally created resource is then referred to in other places in the code. For example, this security group:
resource "aws_security_group" "mygroup" {
count = var.deploy_mgroup ? 1 : 0
name = "mygroup-sg"
vpc_id = aws_vpc.main.id
ingress {
description = "Allow something."
from_port = 8111
to_port = 8111
protocol = "tcp"
security_groups = [aws_security_group.anothergroup.id]
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
Then this is referred to in another group:
resource "aws_security_group" "rds" {
name = "rds-sg"
vpc_id = aws_vpc.main.id
ingress {
description = "Allow PGSQL"
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = [var.ingress_src_ip]
security_groups = [aws_security_group.mygroup[0].id,aws_security_group.anothergroup.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
So in this case I recognise that the resource using count has to be referenced as a list, which works OK if the variable deploy_mgroup is set to true. If it's set to false, the resource that has the count is obviously never created, so the list that the second group refers to aws_security_group.mygroup[0].id is empty, which throws me an error.
I'm not sure what I need to do here, maybe this is just a bad approach and there's something better I should be using? I haven't used Terraform for quite a while and I've missed a few versions.
Any pointers would be appreciated!
Thanks
I hastly read your post, and I had no time to try the solution I am going to suggest. For that reason: sorry! :)
I suggest you to change:
security_groups = [aws_security_group.mygroup[0].id,aws_security_group.anothergroup.id]
to
security_groups = var.deploy_mgroup ? [aws_security_group.mygroup[0].id,aws_security_group.anothergroup.id] : null
Errata Corrige:
I suggest you to change:
security_groups = [aws_security_group.mygroup[0].id,aws_security_group.anothergroup.id]
to
security_groups =
var.deploy_mgroup
? [aws_security_group.mygroup[0].id, aws_security_group.anothergroup.id]
: [aws_security_group.anothergroup.id]

Terraform: Creating multiple instances with Terraform with for_each and mapping security_group_rule

I have a scenario where I need to create multiple EC2 instances which are part of a cluster.These hosts have to be accessible on specific ports from one another and need to have two ebs_volumes attached to them of size 16GB and 700GB.
snip of my variable.tf looks like this:-
variable "instances" {
default = {
instance_name = "a"
tcp_ports = ["53","22","2022","80","443"]
udp_ports = ["53","67","68","123","161","162","500"]
"xvdf" = "16"
"xvdg" = "700"
}
}
I am struggling to get this mapping to work with my TF script:-
resource "aws_security_group_rule" "tcp_ingress" {
for_each = {
for inst in local.instances : inst.tcp_ports => {
for i in inst: i.tcp_ports => i
}
}
type = "ingress"
from_port = each.value.tcp_ports
to_port = each.value.tcp_ports
protocol = "tcp"
cidr_blocks = [ for i in aws_instance.instance: format("%s/32", i.private_ip ) ]
security_group_id = aws_security_group.ha-sg.id
}
Is there a way I can iterate through the ports and form the security group rules.
Is there a way I can iterate through the ports and form the security group rules.
Since your variable "instances" is a single map, to access the tcp_ports you can simply do the following:
var.instances.tcp_ports
Then to use it in for_each:
resource "aws_security_group_rule" "tcp_ingress" {
for_each = toset(var.instances.tcp_ports)
type = "ingress"
from_port = each.value
to_port = each.value
protocol = "tcp"
cidr_blocks = [ for i in aws_instance.instance: format("%s/32", i.private_ip ) ]
security_group_id = aws_security_group.ha-sg.id
}

Unable to create a nested dynamic block

I have a set of 9 security group rules that I need to apply to 4 different sources. I wanted to build it as a module so instead of copy/pasting the same block multiple times, I just need to pass the ports and source as variables.
I have tried to create a module that gets the ports as for_each in a dynamic block and also passes the sources with count since I failed to provide an additional dynamic block with for_each also for the sources.
modules/sg/main.tf
resource "aws_security_group" "test" {
name = "test2"
count = length(var.groups)
vpc_id = var.vpc_id
dynamic "ingress_tcp" {
for_each = var.tcp_ports
content {
from_port = ingress_tcp.value
to_port = ingress_tcp.value
protocol = "tcp"
security_groups = [var.groups[*].id]
}
}
dynamic "ingress_udp" {
for_each = var.udp_ports
content {
from_port = ingress_udp.value
to_port = ingress_udp.value
protocol = "udp"
security_groups = [var.groups[*].id]
}
}
}
main.tf
module "rules" {
source = "./module/sg"
vpc_id = var.vpc_id
name = "tomer-test"
tcp_ports = var.tcp_ports
udp_ports = var.udp_ports
groups = [var.groups[*].id]
}
variables.tf
variable "groups" {
description = "source groups"
type = "list"
default = [{
name = "Enforcement-STG",
id = "sg-c9db2183abcd"
},
{
name = "Managment-STG",
id = "sg-b0e71dfa123"
}]
}
variable "name" {
type = string
}
variable "vpc_id" {
type = string
default = ""
}
variable "tcp_ports" {
description = "tcp ports to open"
default = [514,1514, 11514, 12514, 6514]
}
variable "udp_ports" {
description = "tcp ports to open"
default = [514,1514, 11514, 12514]
}
I accept the output to build a set of rules per source groups, but the root module fails to invoke the module.
The error that I'm currently getting is
terraform plan
Error: Unsupported block type
on module/sg/main.tf line 7, in resource "aws_security_group" "test":
7: dynamic "ingress_tcp" {
Blocks of type "ingress_tcp" are not expected here.
Error: Unsupported block type
on module/sg/main.tf line 16, in resource "aws_security_group" "test":
16: dynamic "ingress_udp" {
Blocks of type "ingress_udp" are not expected here.
As the error message suggests, what you tried here is not valid because ingress_tcp is not a block type expected inside an aws_security_group resource. The correct name for this nested block type is ingress:
resource "aws_security_group" "test" {
count = length(var.groups)
name = "test2"
vpc_id = var.vpc_id
dynamic "ingress" {
for_each = var.tcp_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
security_groups = var.groups[*].id
}
}
dynamic "ingress" {
for_each = var.udp_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "udp"
security_groups = var.groups[*].id
}
}
}
If you are using Terraform 0.12.6 or later, you may prefer to write this using resource for_each instead of count, like this:
resource "aws_security_group" "test" {
for_each = { for g in var.groups : g.name => g }
name = "test2-${each.key}"
vpc_id = var.vpc_id
dynamic "ingress" {
for_each = var.tcp_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
security_groups = var.groups[*].id
}
}
dynamic "ingress" {
for_each = var.udp_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "udp"
security_groups = var.groups[*].id
}
}
}
This will have a similar result to your count example, but will produce instances with addresses like aws_security_group.test["Enforcement-STG"] instead of aws_security_group.test[0], which means that when you add and remove elements from var.groups in future Terraform will be able to determine which instance corresponds with each element and only add/remove the individual instances needed.
This map-based resource is likely to be easier to use elsewhere in the configuration too, since you'll be able to easily find the specific security group for each of your symbolic group names.

How to Keep Usage of Terraform aws_security_group DRY

I've written a simple module to provision a variable AZ numbered AWS VPC. It creates the route tables, gateways, routes, etc., but I'm having trouble keeping the security groups part DRY, i.e. keeping the module re-usable when specifying security groups.
This is as close as I can get:
varibles.tf:
variable "staging_security_groups" {
type = "list"
default = [ {
"name" = "staging_ssh"
"from port" = "22"
"to port" = "22"
"protocol" = "tcp"
"cidrs" = "10.0.0.5/32,10.0.0.50/32,10.0.0.200/32"
"description" = "Port 22"
} ]
}
main.tf:
resource "aws_security_group" "this_security_group" {
count = "${length(var.security_groups)}"
name = "${lookup(var.security_groups[count.index], "name")}"
description = "${lookup(var.security_groups[count.index], "description")}"
vpc_id = "${aws_vpc.this_vpc.id}"
ingress {
from_port = "${lookup(var.security_groups[count.index], "from port")}"
to_port = "${lookup(var.security_groups[count.index], "to port")}"
protocol = "${lookup(var.security_groups[count.index], "protocol")}"
cidr_blocks = ["${split(",", lookup(var.security_groups[count.index], "cidrs"))}"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags {
Name = "${lookup(var.security_groups[count.index], "name")}"
environment = "${var.name}"
terraform = "true"
}
}
Now this is fine, as long as what you want is to create a security group per port :) What I really need, is some way to call ingress the number of times that there are values in the variable staging_security_groups[THE SECURITY GROUP].from_port (please excuse the made-up notation).
You could look at using aws_security_group_rule instead of having your rules inline. You can then create a module like this:
module/sg/sg.tf
resource "aws_security_group" "default" {
name = "${var.security_group_name}"
description = "${var.security_group_name} group managed by Terraform"
vpc_id = "${var.vpc_id}"
}
resource "aws_security_group_rule" "egress" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "All egress traffic"
security_group_id = "${aws_security_group.default.id}"
}
resource "aws_security_group_rule" "tcp" {
count = "${var.tcp_ports == "default_null" ? 0 : length(split(",", var.tcp_ports))}"
type = "ingress"
from_port = "${element(split(",", var.tcp_ports), count.index)}"
to_port = "${element(split(",", var.tcp_ports), count.index)}"
protocol = "tcp"
cidr_blocks = ["${var.cidrs}"]
description = ""
security_group_id = "${aws_security_group.default.id}"
}
resource "aws_security_group_rule" "udp" {
count = "${var.udp_ports == "default_null" ? 0 : length(split(",", var.udp_ports))}"
type = "ingress"
from_port = "${element(split(",", var.udp_ports), count.index)}"
to_port = "${element(split(",", var.udp_ports), count.index)}"
protocol = "udp"
cidr_blocks = ["${var.cidrs}"]
description = ""
security_group_id = "${aws_security_group.default.id}"
}
modules/sg/variables.tf
variable "tcp_ports" {
default = "default_null"
}
variable "udp_ports" {
default = "default_null"
}
variable "cidrs" {
type = "list"
}
variable "security_group_name" {}
variable "vpc_id" {}
Use the module in your main.tf
module "sg1" {
source = "modules/sg"
tcp_ports = "22,80,443"
cidrs = ["10.0.0.5/32", "10.0.0.50/32", "10.0.0.200/32"]
security_group_name = "SomeGroup"
vpc_id = "${aws_vpc.this_vpc.id}"
}
module "sg2" {
source = "modules/sg"
tcp_ports = "22,80,443"
cidrs = ["10.0.0.5/32", "10.0.0.50/32", "10.0.0.200/32"]
security_group_name = "SomeOtherGroup"
vpc_id = "${aws_vpc.this_vpc.id}"
}
References:
For why optionally excluding a resource with count looks like this (source):
count = "${var.udp_ports == "default_null" ? 0 : length(split(",", var.udp_ports))}"
And the variable is set to:
variable "udp_ports" {
default = "default_null"
}
I managed to create really simple yet dynamic security group module that you can use. Idea here is to have ability to add any port you desire, and add to that port any range of ips you like. You can even remove egress from module as it will be created by default, or follow idea i used in ingress so you have granular egress rules (if you wish so).
module/sg/sg.tf
data "aws_subnet_ids" "selected" {
vpc_id = "${var.data_vpc_id}"
}
resource "aws_security_group" "main" {
name = "${var.sg_name}-sg"
vpc_id = "${var.data_vpc_id}"
description = "Managed by Terraform"
ingress = ["${var.ingress}"]
lifecycle {
create_before_destroy = true
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
module/sg/vars.tf
variable "sg_name" {}
variable "data_vpc_id" {}
variable "ingress" {
type = "list"
default = []
}
ingress var needs to be type list. If you call vpc id manually you dont need data bit in module, im calling my vpc_id from terraform state that is stored in s3.
main.tf
module "aws_security_group" {
source = "module/sg/"
sg_name = "name_of_sg"
data_vpc_id = "${data.terraform_remote_state.vpc.vpc_id}"
ingress = [
{
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "Managed by Terraform"
},
{
from_port = 0
to_port = 100
protocol = "tcp"
cidr_blocks = ["10.10.10.10/32"]
description = "Managed by Terraform"
},
{
from_port = 2222
to_port = 2222
protocol = "tcp"
cidr_blocks = ["100.100.100.0/24"]
description = "Managed by Terraform"
},
]
}
You can add as many ingress blocks you like, i have only 3 for test purposes. Hope this helps.
Note: You can follow this idea for many resources like RDS, where you need to specify parameters in parameter group or even tags. Cheers
Not sure if it was available at the time Brandon Miller's answer was written, but avoid count loops as they are ordered. So if you add or delete one port, it will cause all rules after it to be rebuilt as they rely on the count index, which changes. Far better to use a for_each loop. Make sure you use set not lists for this eg
variable "tcp_ports" {
default = [ ]
# or maybe default = [ "22", "443" ]
type = set(string)
}
resource "aws_security_group_rule" "tcp" {
for_each = var.tcp_ports
description = "Allow ${var.cdir} to connect to TCP port ${each.key}"
type = "ingress"
from_port = each.key
to_port = each.key
protocol = "tcp"
cidr_blocks = var.cdir
security_group_id = aws_security_group.default.id
}
Now you can add and delete ports without incurring unnecessary create and destroys
you you cant alter your data from lists to sets for any reason just wrap it eg
toset(var.tcp_ports)
or use a local to munge your data accordingly. You can also use maps as well

Resources