Terraform - Automatically create SGs for CloudFront IPs - terraform

I am trying to automatically create SGs for CloudFront IPs so I can associate them my ALB.
This article has a very good insight on how to achieve it, but unfortunately it didn't work on my environment.
This is the code:
data "aws_ip_ranges" "cloudfront" {
regions = ["global"]
services = ["cloudfront"]
}
locals {
chunks_v4 = chunklist(data.aws_ip_ranges.cloudfront.cidr_blocks, 60)
}
resource "aws_security_group" "cloudfront" {
count = length(local.chunks_v4)
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [local.chunks_v4[count.index]]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
lifecycle {
create_before_destroy = true
}
}
and that is the error message:
╷
│ Error: Incorrect attribute value type
│
│ on main.tf line 34, in resource "aws_security_group" "cloudfront":
│ 34: cidr_blocks = [local.chunks_v4[count.index]]
│ ├────────────────
│ │ count.index is a number, known only after apply
│ │ local.chunks_v4 is a list of list of dynamic, known only after apply
│
│ Inappropriate value for attribute "cidr_blocks": element 0: string required.
╵
Shouldn't it be something like:
local.chunks_v4[count.index][0 to 59???]
How can I achieve it by using Terraform?

Edit: Since there is a hard limit of 60 CIDR blocks, we need to split it into chunks, thanks for the heads up #Marcin!
locals {
chunks_v4 = chunklist(data.aws_ip_ranges.cloudfront.cidr_blocks, 60)
}
data "aws_ip_ranges" "cloudfront" {
regions = ["global"]
services = ["cloudfront"]
}
resource "aws_security_group" "cloudfront" {
count = length(local.chunks_v4)
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = local.chunks_v4[count.index]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
lifecycle {
create_before_destroy = true
}
}

Related

"" is not a valid CIDR block: invalid CIDR address

I have a VPC defined in terraform:
//vpc.tf
resource "aws_vpc" "my_vpc" {
cidr_block = "10.0.0.0/16"
}
And I am trying to create a security group:
// securityGroup.tf
resource "aws_security_group" "allow_tls" {
name = "allow_tls"
description = "Allow TLS inbound traffic"
vpc_id = aws_vpc.my_vpc.id
ingress {
description = "TLS from VPC"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [aws_vpc.my_vpc.cidr_block]
ipv6_cidr_blocks = [aws_vpc.my_vpc.ipv6_cidr_block]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}
}
However, when I run terraform plan I get the error Error: "" is not a valid CIDR block: invalid CIDR address:
I thought that the cidr_block defined in my vpc would be available to the securityGroup, but that seems to not be the case. How do I correctly reference the cidr_block from my vpc?
Full error output:
│ Error: "" is not a valid CIDR block: invalid CIDR address:
│
│ with module.shared.aws_security_group.allow_tls,
│ on modules/shared/securityGroup.tf line 1, in resource "aws_security_group" "allow_tls":
│ 1: resource "aws_security_group" "allow_tls" {
You haven't configured your VPC with any IPV6 addresses. So this line in your security group should be deleted:
ipv6_cidr_blocks = [aws_vpc.my_vpc.ipv6_cidr_block]

How to add security group rule to multiple security groups

I'm creating one security group rule and want to attach it to multiple security groups. How can I do it? For example:
resource "aws_security_group" "test-sg-1" {
name = "Test SG 1"
description = "Test Security Group one"
vpc_id = aws_vpc.test_vpc.id
lifecycle {
create_before_destroy = true
}
}
resource "aws_security_group" "test-sg-2" {
name = "Test SG 2"
description = "Test Security Group two"
vpc_id = aws_vpc.test_vpc.id
lifecycle {
create_before_destroy = true
}
}
resource "aws_security_group_rule" "egress_all" {
from_port = 0
protocol = "-1"
security_group_id = [aws_security_group.test-sg-1.id, aws_security_group.test-sg-2.id]
to_port = 0
type = "egress"
cidr_blocks = ["0.0.0.0/0"]
}
I'm getting error if I try this above way of using a list.
│ Error: Incorrect attribute value type
│
│ on main.tf line 76, in resource "aws_security_group_rule" "egress_all":
│ 76: security_group_id = [aws_security_group.test-sg-1.id, aws_security_group.test-sg-2.id]
│ ├────────────────
│ │ aws_security_group.test-sg-1.id will be known only after apply
│ │ aws_security_group.test-sg-2.id will be known only after apply
│
│ Inappropriate value for attribute "security_group_id": string required.
In this case using the for_each meta-argument [1] might be a good idea to avoid code repetition. So this is what I would do:
locals {
sg_names = ["Test SG 1", "Test SG 2"]
}
resource "aws_security_group" "test_sg" {
for_each = toset(local.sg_names)
name = each.value
description = each.value
vpc_id = aws_vpc.test_vpc.id
lifecycle {
create_before_destroy = true
}
}
resource "aws_security_group_rule" "egress_all" {
for_each = aws_security_group.test_sg
from_port = 0
protocol = "-1"
security_group_id = each.value.id
to_port = 0
type = "egress"
cidr_blocks = ["0.0.0.0/0"]
}
Here the resource chaining is used. You can read more in [2].
[1] https://www.terraform.io/language/meta-arguments/for_each#basic-syntax
[2] https://www.terraform.io/language/meta-arguments/for_each#chaining-for_each-between-resources

Terraform doesn't like my variables that contain lists of strings in my NACL block, can't figure out why

I wanted to make a rule that used a range of cidr blocks to cut down on the number of rules. I can't seem to get terraform to accept the variables or data output as strings
code:
data "aws_ip_ranges" "az_s3" {
regions = ["region-1"]
services = ["s3"]
}
variable "wan_range" {
description = "WAN cidr ranges"
type = list(string)
default = ["10.0.0.0/8", "172.16.0.0/16", "192.168.0.0/24"]
}
resource "aws_network_acl" "NACL_1" {
vpc_id = aws_vpc.sec_vpc.id
subnet_ids = [aws_subnet.private_subnet.id]
count = length(var.sd_wan_range)
egress = [
{
protocol = "tcp"
rule_no = 100
action = "allow"
cidr_block = data.aws_ip_ranges.az_s3.cidr_blocks
from_port = 80
to_port = 80
icmp_code = 0
icmp_type = 0
ipv6_cidr_block = null
},
{
protocol = "tcp"
rule_no = 200
action = "allow"
cidr_block = var.wan_range[count.index]
from_port = 32768
to_port = 65535
icmp_code = 0
icmp_type = 0
ipv6_cidr_block = null
}
]
Here is the error from plan:
├────────────────
│ │ count.index is 0
│ │ data.aws_ip_ranges.az_s3.cidr_blocks is list of string with 6 elements
│ │ var.wan_range is list of string with 3 elements
│
│ Inappropriate value for attribute "egress": element 2: attribute "cidr_block": string required.
You need to index on data.aws_ip_ranges.az_s3.cidr_blocks as well. The cidr_blocks attribute is a list.
e.g.
cidr_block = data.aws_ip_ranges.az_s3.cidr_blocks[count.index]

TF aws_security_group: Inappropriate value for attribute "ingress": attributes "prefix_list_ids", "security_groups", and "self" are required

I'm learning Terraform and AWS and I'm having trouble by creating an aws_security_group resource.
I copied from the Terraform Docs an example of this resource, then I configured with my data (I want to access my EC2 resources via SSH. I know that is not a good practice but I'm only learning and I'm going to destroy after the test)
resource "aws_security_group" "allow_tls_ssh" {
name = "allow_tls"
description = "Allow TLS inbound traffic"
vpc_id = aws_vpc.vpc.id
ingress = [
{
description = "TLS from VPC"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [aws_vpc.vpc.cidr_block]
ipv6_cidr_blocks = [aws_vpc.vpc.ipv6_cidr_block]
prefix_list_ids = []
security_groups = []
self = false
},
{
description = "SSH from VPC"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [aws_vpc.vpc.cidr_block]
ipv6_cidr_blocks = [aws_vpc.vpc.ipv6_cidr_block]
prefix_list_ids = []
security_groups = []
self = false
},
{
description = "HTTP from VPC"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [aws_vpc.vpc.cidr_block]
ipv6_cidr_blocks = [aws_vpc.vpc.ipv6_cidr_block]
prefix_list_ids = []
security_groups = []
self = false
}
]
tags = {
Name = "allow_tls_ssh"
}
}
Then, when I terraform apply I get:
(...)
│ 59: }
│ 60: ]
│ ├────────────────
│ │ aws_vpc.vpc.cidr_block will be known only after apply
│ │ aws_vpc.vpc.ipv6_cidr_block will be known only after apply
│
│ Inappropriate value for attribute "ingress": element 0: attributes "prefix_list_ids", "security_groups", and "self" are required.
╵
This error confuses me. Why do I have to indicate security_groups when I'm creating one?
The error corresponds to the attribute security_groups inside the ingress rules you are defining. A rule in a security group could be based on CIDR block, security groups or prefixed list. Since you want to use CIDR blocks just give null value to the others:
resource "aws_security_group" "allow_tls_ssh" {
name = "allow_tls"
description = "Allow TLS inbound traffic"
vpc_id = aws_vpc.vpc.id
ingress = [
{
description = "TLS from VPC"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [aws_vpc.vpc.cidr_block]
ipv6_cidr_blocks = [aws_vpc.vpc.ipv6_cidr_block]
prefix_list_ids = null
security_groups = null
self = false
},
{
description = "SSH from VPC"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [aws_vpc.vpc.cidr_block]
ipv6_cidr_blocks = [aws_vpc.vpc.ipv6_cidr_block]
prefix_list_ids = null
security_groups = null
self = false
},
{
description = "HTTP from VPC"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [aws_vpc.vpc.cidr_block]
ipv6_cidr_blocks = [aws_vpc.vpc.ipv6_cidr_block]
prefix_list_ids = null
security_groups = null
self = false
}
]
tags = {
Name = "allow_tls_ssh"
}
}
I'm also getting same issue when trying to create a security group to open all traffic but, I have found solution by trying this "null" for the below. I have used it for all traffic,if you want to specify certain traffic feel free to modify it.

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