Terraform v0.13 conditional resources with "count" - terraform

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]

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

Rule_nos in dynamic ingress in 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
}
}
}

How do I send block of the resource module externally

I want to send block of security rule mentioned below to security group as an input,Is it possible in terraform?
ingress {
from_port = 5985
to_port = 5986
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
If we consider variables,we have a provision to read the variable value externally
instance_type = ${var.instance_type}
In variable.tf, we declare instance_type.
Likewise,is there any option to send whole of the below mentioned block to resource "aws_security_group" "allow_al"
ingress {
from_port = 5985
to_port = 5986
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
It isn't possible to send the whole block, but you can define a variable whose type is an object with a similar set of attributes and then use that object to populate the block.
variable "ingress_rule" {
type = object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
})
}
resource "aws_security_group" "example" {
# ...
ingress {
from_port = var.ingress_rule.from_port
to_port = var.ingress_rule.to_port
protocol = var.ingress_rule.protocol
cidr_blocks = var.ingress_rule.cidr_blocks
}
}
The above assumes you want exactly one ingress block and just want to let the caller customize its contents. If you instead want to let the caller specify zero or more ingress blocks then the answer is slightly more complicated, but follows the same principle:
variable "ingress_rules" {
type = list(object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
}))
}
resource "aws_security_group" "example" {
# ...
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
}
The first notable difference here is that the variable is now defined as being a list of object instead of just one object. The second difference is that it's dynamically generating one ingress block for each element of that list using dynamic blocks.
It's always required to fully define your object type and define how it maps to the arguments within the resource block, because this then allows Terraform to validate separately your own module code (whether you spelled the argument names correctly, whether they have the right types) from the calling module's object or list of objects value, and thus report any errors at the correct source location.

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