Terraform decouple Security Group dependency - amazon-web-services

This is tested with Terraform v0.12.9
I generally manage security groups and security group rules as separate resources, as in the below example:
resource "aws_security_group" "this" {
count = var.create ? 1 : 0
name_prefix = "${var.security_group_name}_"
vpc_id = var.vpc_id
lifecycle {
create_before_destroy = true
}
}
resource "aws_security_group_rule" "ingress_rules" {
count = var.create ? length(var.inbound_security_group_ids) : 0
security_group_id = aws_security_group.this[0].id
type = "ingress"
from_port = var.from_port
to_port = var.to_port
protocol = "tcp"
source_security_group_id = var.inbound_security_group_ids[count.index]
}
The implementation for this would look something like the below:
module "test_module" {
source = "../path/to/module/"
create = true
vpc_id = "vpc-xxxxxx"
security_group_name = "${var.service_name}-db"
from_port = 1234
to_port = 1234
inbound_security_group_ids = [
module.service.security_group_id_one,
module.service.security_group_id_two
]
}
Problem
I want this to work if the outputs from the module.service aren't created. In that scenario my expectation is that length(var.inbound_security_group_ids) should evaluate to 0 resulting in the security group rules not being created
What actually happens is that length(var.inbound_security_group_ids) evaluates to 2 when module.service isn't created. This is presumably because it is an array of two blank strings ["", ""]
According to the Terraform documentation I can handle this with the compact function, which removes empty strings from an array.
resource "aws_security_group_rule" "ingress_rules" {
count = var.create ? length(compact(var.inbound_security_group_ids)) : 0
security_group_id = aws_security_group.this[0].id
type = "ingress"
from_port = var.from_port
to_port = var.to_port
protocol = "tcp"
source_security_group_id = var.inbound_security_group_ids[count.index]
}
The problem with this, however, is that Terraform is unable to determine the plan because it doesn't know what var.inbound_security_group_ids evaluates to until apply-time. This is the error message (for context):
The "count" value depends on resource attributes that cannot be determined
until apply, so Terraform cannot predict how many instances will be created.
To work around this, use the -target argument to first apply only the
resources that the count depends on.
Question
Is it possible to decouple a security group like this so that it will still be created even when the source_security_group_id attribute has no values?

It's often easier to work with lists or sets that might be empty in Terraform than to work with individual values that might not be set, for a reason related to what you've observed: it separates whether the value is set from what the value actually is, so that the presence of the value can be known even if the value itself isn't known.
In this case, you might approach that by changing how you return the security group ids from your service module, so that each of those outputs is a list of security group ids rather than a single string that might be either a valid security group id or an empty string:
module "test_module" {
source = "../path/to/module/"
create = true
vpc_id = "vpc-xxxxxx"
security_group_name = "${var.service_name}-db"
from_port = 1234
to_port = 1234
inbound_security_group_ids = concat(
module.service.security_group_ids_one,
module.service.security_group_ids_two,
)
}
If either security_group_ids_one or security_group_ids_two is known to be an empty list then concat will ignore it entirely. If they are enabled then you can arrange for them to be a known list with one element whose value is unknown, and thus length(var.inbound_security_group_ids) will still have a known value in all cases.

Related

Is it possible to attach a security group to a load balancer ONLY IF the security group count is set to 1?

I'm working on a terraform module which creates a load balancer amongst other resources. I would like to attach a security group to the load balancer, but only if a a certain variable is equal to "all" OR "gw".
I currently have a count argument in place for the security group itself:
resource "aws_security_group" "akamai_sg" {
count = var.domain_name_suffix == "all" || var.domain_name_suffix == "gw" ? 1 : 0
name = "akamai-pl-sg"
description = "Manage access from Akamai to ${var.environment} alb"
vpc_id = var.vpc_id
tags = merge(var.common_tags, tomap({ "Name" = "akamai-pl-sg" }))
revoke_rules_on_delete = true
}
This means that the security group will only be created if the domain_name_suffix is set to "all" or "gw".
I would like this same functionality for attaching this security group to the ALB. However, I still want to create the ALB regardless of this variable, it is just the security group attachment which I want to be dependent.
Currently I have this configuration for my ALB:
resource "aws_lb" "internal" {
name = "${var.environment}-${var.domain_name_suffix}"
internal = true
load_balancer_type = "application"
security_groups = [aws_security_group.alb_sg.id, aws_security_group.akamai_sg.id]
subnets = var.alb_subnets
idle_timeout = var.alb_idle_timeout
tags = var.common_tags
drop_invalid_header_fields = true
dynamic "access_logs" {
for_each = var.alb_access_logging_enabled == true ? [var.alb_access_logging_enabled] : []
content {
bucket = var.alb_access_logging_bucket
enabled = true
prefix = "internal"
}
}
}
However, this results in an error:
"Error: Missing resource instance key
on .terraform/modules/comm_common/alb.tf line 5, in resource "aws_lb" "internal":
5: security_groups = [aws_security_group.alb_sg.id, aws_security_group.akamai_sg.id]
Because aws_security_group.alb_sg has "count" set, its attributes must be
accessed on specific instances.
For example, to correlate with indices of a referring resource, use:
aws_security_group.alb_sg[count.index]"
When I add in this [count.index] to the ALB security group reference, I still get the same error.
Could you try the same condition in security_groups field as well? but the value would be a list with two items if condition is true and with one item if condition is false?
resource "aws_lb" "internal" {
name = "${var.environment}-${var.domain_name_suffix}"
internal = true
load_balancer_type = "application"
security_groups = var.domain_name_suffix == "all" || var.domain_name_suffix == "gw" ? [aws_security_group.alb_sg.id, aws_security_group.akamai_sg.id] : [aws_security_group.alb_sg.id]
subnets = var.alb_subnets
idle_timeout = var.alb_idle_timeout
...
}

Terraform: ingress_with_source_security_group_id vs. computed_ingress_with_source_security_group_id

Terraform module terraform-aws-modules/security-group/aws supports two input variables.
1. ingress_with_source_security_group_id
description: List of ingress rules to create where 'source_security_group_id' is used
example: (source code)
ingress_with_source_security_group_id = [
{
rule = "mysql-tcp"
source_security_group_id = data.aws_security_group.default.id
},
{
from_port = 10
to_port = 10
protocol = 6
description = "Service name"
source_security_group_id = data.aws_security_group.default.id
},
]
2. computed_ingress_with_source_security_group_id
description: List of computed ingress rules to create where 'source_security_group_id' is used
Example (source code)
computed_ingress_with_source_security_group_id = [
{
rule = "postgresql-tcp"
source_security_group_id = module.main_sg.security_group_id
},
{
from_port = 23
to_port = 23
protocol = 6
description = "Service name"
source_security_group_id = module.main_sg.security_group_id
},
]
Questions
What's the difference between them?
What are computed ingress rules?
This is explained in Note about "value of 'count' cannot be computed" and is related to limitations of TF 0.11. From the link:
Computed values are values provided as outputs from module. Non-computed values are all others - static values, values referenced as variable and from data-sources.
When you need to specify computed value inside security group rule argument you need to specify it using an argument which starts with computed_ and provide a number of elements in the argument which starts with number_of_computed_
I get it.
#ryan, im sure you know this by now, but for me i was confused also. So hopefully this helps others.
Computed, seems to mean "im going to need to use output data in my group, and wont have all the data available at the time of writing the code". Essentially, it means your going to reference module outputs for values. Module outputs arent available until after they are ran.
So in this example, notice module.main_sg.security_group_id. This is the part that needs computing, because its coming from another module output. E.g. it wont have that value until terraform runs, where it can then grab the output from that module.
computed_ingress_with_source_security_group_id = [
{
rule = "postgresql-tcp"
source_security_group_id = module.main_sg.security_group_id
},
{
from_port = 23
to_port = 23
protocol = 6
description = "Service name"
source_security_group_id = module.main_sg.security_group_id
},
]
So, if you dont use computed_ here, the code breaks, beacuse it cant get the value of module.main_sg.security_group_id.
When you have all the values available before the code runs, as in your first example, e.g.source_security_group_id = data.aws_security_group.default.id then you dont need to use computed_. This means it wont have to pass through extra functionality to resolve the values, because they are already statically specified somewhere. In this case its statically specified in a data resource, etc..
So computed means, when terraform runs this code, it will need to wait until it gets the output data, before calculating what this security groups values will actually be, because those values are dynamic, and rely on the terraform code to run before it can see them, hence it will need to "compute" them when it runs.
I have another example.
Here this code needs a value from another module, you cant tell that, because its grabbing that in the main.tf.
computed_ingress_with_cidr_blocks = [{
from_port = 53
to_port = 53
protocol = "udp"
description = "Domain Name System (DNS) access"
cidr_blocks = var.priv_cidr_block
},{
...
The cidr block is taken from the vpc module.
Then the egress rule is as follows
egress_with_cidr_blocks = [{
from_port = 9389
to_port = 9389
protocol = "tcp"
description = "OPEN egress, all ports, all protocols"
cidr_blocks = "0.0.0.0/0"
}]```
Notice, theres only static values in there, so no need to "compute" any values, it already is, what it is...
HTH.

Security groups should be able to communicate to other security groups

My company requires that I we expressly specify all allowed ports and protocols in security group ingress rules. I would like to have a long list of ports protocols and security groups to allow ingress/egress for
from_port, to_port, protocol, security_group_that_port_protocol_restriction_applies_to
The below example has the problem that the "master-sg-ingress-security-groups" variable needs to have the security groups to be defined.
resource "aws_security_group" "master_lb_sg" {
....
}
resource "aws_security_group" "worker_sg" {
......
}
########
####### list of port protocols and security groups to create ingress blocks for. Problem is that security groups to not exist at variable creation time.
########
variable "master-sg-ingress-security-groups" {
depends_on = [aws_security_group.master_lb_sg, aws_security_group.worker_sg]
description = "List of port numbers for specific security group. company bans allowing all ports and protocols. "
type = map(any)
default = {
"ingress1" = [80, 80, "TCP", aws_security_group.master_lb_sg],
"ingress2" = [443, 443, "TCP", aws_security_group.master_lb_sg],
"ingress3" = [3398,3398, "RDP", aws_security_group.bastion_host_sg],
....
"ingress4" = [1024, 1024, "UDP", aws_security_group.worker_sg]
}
}
#####
#### I want to iterate over the above list of security groups and create dynamic ingress rules but other security groups do not exist
####
resource "aws_security_group" "test" {
depends_on = [aws_security_group.master_lb_sg, aws_security_group.worker_sg]
provider = aws.region_master
name = "master-sg"
description = "security group for Jenkins master"
vpc_id = aws_vpc.vpc_master.id
dynamic "ingress" {
# this for_each is not identical to for_each in line 21
for_each = var.master-sg-ingress-security-groups
content {
from_port = ingress.value[0]
to_port = ingress.value[1]
protocol = ingress.value[2]
security_group = ingress[3]
}
}
}
I am think ing I have to just copy paste blocks of text for each ingress
Is there a way to get around the problem of aws_security_group.worker_sg in a variable
Sadly not from TF itself. Variables must be fully defined when you run your script. But you could maybe modify master-sg-ingress-security-groups into a local variable. This way you could construct your map which includes other variables.
So depending exactly on your use-case, you could maybe have a base variable called base-master-sg-ingress-security-groups, and then in locals construct a final map which would containe references to other existing SGs.
Alternatively, you could split your TF script into two parts. The first one would deploy core SGs and output their IDs. Then these IDs would be used as input variables for the second part which would deploy SGs that reference the core ones.

target_groups optional param is not optional : terraform

Error: Invalid index
on .terraform/modules/database-security-group/main.tf line 70, in resource "aws_security_group_rule" "ingress_rules":
70: to_port = var.rules[var.ingress_rules[count.index]][1]
|----------------
| count.index is 0
| var.ingress_rules is list of string with 1 element
| var.rules is map of list of string with 119 elements
The given key does not identify an element in this collection value
.
It's all Greek to me. We could use the help..
module "database-security-group" {
source = "terraform-aws-modules/security-group/aws"
name = "database-security"
description = "Security group for Database on database subnet."
vpc_id = module.vpc.vpc_id
ingress_cidr_blocks = ["0.0.0.0/0"]
ingress_rules = [ "http-3306-tcp"]
egress_rules = ["all-all"]
tags = {
Name = "Database"
Environment = "spoon"
}
}
I believe the intention of this particular module is that you select from its table of predefined rules when specifying ingress_rules and egress_rules.
At the time I write this I don't see a definition for a rule "http-3306-tcp", and so I think that's the cause of your error. If your intent was to allow TCP port 3306 for MySQL then it seems the rule key for that is "mysql-tcp".

Terrafrom datasource aws_vpcs - count.index error

I am trying to use data source aws_vpcs to get the vpc id having specific tag.
For reference:
https://www.terraform.io/docs/providers/aws/d/vpcs.html
Below is my terraform yaml file.
Terrafrom version used is: 0.12.3
data "aws_vpcs" "foo" {
tags = {
Name = "test1-VPC"
}
}
resource "aws_security_group" "cluster" {
count = "${length(data.aws_vpcs.foo.ids)}"
vpc_id = "${tolist(data.aws_vpcs.foo.ids)[count.index]}"
}
resource "aws_security_group_rule" "cluster-ingress-node-https" {
description = "Rule to do xyz"
from_port = 443
protocol = "tcp"
security_group_id = "${aws_security_group.cluster.id}"
to_port = 443
type = "ingress"
}
I am getting below error. Request for help to fix this
terraform plan
Error: Missing resource instance key
on modules/eks/eks-cluster.tf line 40, in resource "aws_security_group_rule" "cluster-ingress-node-https":
40: security_group_id = "${aws_security_group.cluster.id}"
Because aws_security_group.cluster has "count" set, its attributes must be
accessed on specific instances.
For example, to correlate with indices of a referring resource, use:
aws_security_group.cluster[count.index]
You are creating a list of aws_security_group as you are using count on the aws_security_group resource. The error even mentions it:
Because aws_security_group.cluster has "count" set, its attributes
must be accessed on specific instances.
So either you need to include count on the aws_security_group_rule resource and create one aws_security_group_rule for each aws_security_group created, or in the case you expect only one VPC to be returned, create only one aws_security_group by accessing the returned aws_vpcs.foo.ids with index 0.
You will need to convert the list of security group.
Terraform provides flatten function to do that https://nedinthecloud.com/2018/07/16/terraform-fotd-flatten/
You should not get this error afterwards
I know this was posted a while ago. Stumbled upon this issue.
${aws_security_group.cluster.*.id} should do it.
Since the resource aws_security_group is creating multiple security groups with count, resource block aws_security_group_rule needs to reference the correct index in the list.