Security groups should be able to communicate to other security groups - amazon-web-services

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.

Related

Mapping multiple Security Groups into ELB

I'm trying to attach multiple security groups containing Cloudfront CIDRs to my AWS ALB.
locals {
chunks = chunklist(data.aws_ip_ranges.cloudfront.cidr_blocks, 60)
chunks_map = { for i in range(length(local.chunks)): i => local.chunks[i] }
}
resource "aws_security_group" "sg" {
for_each = local.chunks_map
name = "{each.key}"
egress {
....
}
}
resource "aws_elb" "load" {
name = "test"
security_groups = aws_security_group.sg.id // This is wrong
My error that I'm receiving is
Because aws_security_group.sg has for_each se, its attributes must be access on specific instances
Using for_each again doesn't make sense because i don't want to create multiple resources, I just want to ensure that all security groups created are attached to the load balancer. Any ideas?
Since you've used for_each there will be more than instance of aws_security_group.sg. To get id from all of them you can use splat operator:
security_groups = values(aws_security_group.sg)[*].id

Filter out Subnet IDs based on sufficient capacity in availability zones in Terraform

I'm trying to deploy an EKS cluster and everything seems to be fine except for one!
The facade module looks like this:
module "eks" {
source = "../../../../infrastructure_modules/eks"
## EKS ##
create_eks = var.create_eks
cluster_version = var.cluster_version
cluster_name = local.cluster_name
vpc_id = data.aws_vpc.this.id
subnets = data.aws_subnet_ids.this.ids
# note: either pass worker_groups or node_groups
# this is for (EKSCTL API) unmanaged node group
worker_groups = var.worker_groups
# this is for (EKS API) managed node group
node_groups = var.node_groups
## Common tag metadata ##
env = var.env
app_name = var.app_name
tags = local.eks_tags
region = var.region
}
The VPC id is retrieved through the following block :
data "aws_vpc" "this" {
tags = {
Name = "tagName"
}
}
Which then is used to retrieve the subnet_IDs as following:
data "aws_subnet_ids" "this" {
vpc_id = data.aws_vpc.this.id
}
Nevertheless, deploying this results in error stating:
Error: error creating EKS Cluster (data-layer-eks):
UnsupportedAvailabilityZoneException: Cannot create cluster
'data-layer-eks' because us-east-1e, the targeted availability zone,
does not currently have sufficient capacity to support the cluster.
Which is a well known error, and anybody can come across this for even EC2s.
I could solve this by simply hardcoding the subnet value, but that's really undesirable and hardly maintainable.
So the question is, how can I filter out subnet_IDs based on availability zones that have sufficient capacity?
First you need to collect the subnets with all of their attributes:
data "aws_subnets" "this" {
filter {
name = "vpc-id"
values = [data.aws_vpc.this.id]
}
}
data "aws_subnet" "this" {
for_each = toset(data.aws_subnets.this.ids)
id = each.value
}
data.aws_subnet.this is now a map(object) with all of the subnets and their attributes. You can now filter by availability zone accordingly:
subnets = [for subnet in data.aws_subnet.this : subnet.id if subnet.availability_zone != "us-east-1e"]
You can also filter by truthy conditionals if that condition is easier for you:
subnets = [for subnet in data.aws_subnet.this : subnet.id if contains(["us-east-1a", "us-east-1b", "us-east-1c", "us-east-1d"], subnet.availability_zone)]
It depends on your personal use case.

Terraform decouple Security Group dependency

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.

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.

Terraform and GCS : how to create a multi zone LB

I want to create a load balancer adressing two or more instances on several zones of the same region in GCP
I start like that :
- create a backend service adressing two instance groups :
resource "google_compute_backend_service" "www-service" {
name = "${var.environment}-www-service"
protocol = "HTTP"
port_name = "http"
backend {
group = "${google_compute_instance_group.instance-group-0.self_link}"
}
backend {
group = "${google_compute_instance_group.instance-group-1.self_link}"
}
health_checks = ["${google_compute_health_check.health-check1.self_link}"]
}
Then I have two instance groups, each one with a one instance with that syntax :
resource "google_compute_instance_group" "instance-group-0" {
count = "${var.web_count}"
name = "${var.environment}-instance-group-0"
instances = ["${google_compute_instance.www.self_link}"]
named_port {
name = "http"
port = "80"
}
network = "${google_compute_network.platform-network.self_link}"
}
I get an error :
google_compute_backend_service.www-service: Resource 'google_compute_instance_group.instance-group-0' not found for variable 'google_compute_instance_group.instance-group-0.self_link'
I see that switching the backend/group declarations in the backend_service moves the error to group-1, so I can guess that this is not the proper syntax, although you can create a backend_service with multiple instance groups in the Google GUI.
I have two questions :
Q1. How can I create backend_service with multiple instance groups ?
what is the right syntax ?
Q2. Is it possible to reference a compute_instance in a compute_instance_group via a syntax like :
instances = ["${google_compute_instance.www.[count.index].self_link}"]
(the above syntax does not work)
Thanks for your answer
Finally I found the syntax across multiple Github tickets :
resource "google_compute_instance_group" "instance-group-0" {
name = "${var.environment}-instance-group-0"
zone = "${data.google_compute_zones.available.names[0]}"
instances = ["${slice(google_compute_instance.www.*.self_link, 0, floor(var.web_count/2)-1)}"]
named_port {
name = "http"
port = "80"
}
network = "${google_compute_network.platform-network.self_link}"
}
same for instance group 1, but different slice
Then :
resource "google_compute_instance" "www" {
count = "${var.web_count}"
zone = "${data.google_compute_zones.available.names[floor((2*count.index)/var.web_count)]"}
I must say that different design decisions can be taken :
- use a region managed instance group is simpler, except the template is static
- use Kubernetes
A1 - each backend service can only use a single instance group. You need to create a new backend for each group. You can then assign multiple backends to your load balancer.
On another note, the error message is with regards to the resource type you are using. Here is the resource page for unmanaged Instance groups, Managed instance groups and for backend services. You can find a list of the different compute APIs here.
A2- you should use $(ref.name.selfLink) instead, I believe this syntax should work and you can still use your variable. Also, make sure to update the API call syntax for adding instances to the group