Unable to create a nested dynamic block - terraform

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.

Related

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

Terraform dynamic tagging of EC2 resource fails with `Blocks of type "tag" are not expected here`

➜ terraform -v
Terraform v0.12.24
+ provider.aws v2.60.0
My terraform example.tf:
locals {
standard_tags = {
team = var.team
project = var.project
component = var.component
environment = var.environment
}
}
provider "aws" {
profile = "profile"
region = var.region
}
resource "aws_key_pair" "security_key" {
key_name = "security_key"
public_key = file(".ssh/key.pub")
}
# New resource for the S3 bucket our application will use.
resource "aws_s3_bucket" "project_bucket" {
# NOTE: S3 bucket names must be unique across _all_ AWS accounts, so
# this name must be changed before applying this example to avoid naming
# conflicts.
bucket = "project-bucket"
acl = "private"
}
resource "aws_security_group" "ssh_allow" {
name = "allow-all-ssh"
ingress {
cidr_blocks = [
"0.0.0.0/0"
]
from_port = 22
to_port = 22
protocol = "tcp"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group" "http_allow" {
name = "allow-all-http"
ingress {
cidr_blocks = [
"0.0.0.0/0"
]
from_port = 80
to_port = 80
protocol = "tcp"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "example" {
ami = "ami-08ee2516c7709ea48"
instance_type = "t2.micro"
security_groups = [aws_security_group.ssh_allow.name, aws_security_group.http_allow.name]
key_name = aws_key_pair.security_key.key_name
connection {
type = "ssh"
user = "centos"
private_key = file(".ssh/key")
host = self.public_ip
}
provisioner "local-exec" {
command = "echo ${aws_instance.example.public_ip} > ip_address.txt"
}
provisioner "remote-exec" {
inline = [
"sudo yum -y install nginx",
"sudo systemctl start nginx"
]
}
depends_on = [aws_s3_bucket.project_bucket, aws_key_pair.security_key]
dynamic "tag" {
for_each = local.standard_tags
content {
key = tag.key
value = tag.value
propagate_at_launch = true
}
}
}
And when I run terraform plan
I got the following error:
➜ terraform plan
Error: Unsupported block type
on example.tf line 94, in resource "aws_instance" "example":
94: dynamic "tag" {
Blocks of type "tag" are not expected here.
There isn't a block type called tag defined in the schema for the aws_instance resource type. There is an argument called tags, which is I think the way to get the result you were looking for here:
tags = local.standard_tags
I expect you are thinking of the tag block in aws_autoscaling_group, which deviates from the usual design of tags arguments in AWS provider resources because for this resource type in particular each tag has the additional attribute propagate_at_launch. That attribute only applies to autoscaling groups because it decides whether instances launched from the autoscaling group will inherit a particular tag from the group itself.
unfortunately since the aws_instance resource's tags attribute is a map, w/in the HCL constructs atm, it cannot exist as repeatable blocks like a tag attribute in the aws_autoscaling_group example seen here in the Dynamic Nested Blocks section: https://www.hashicorp.com/blog/hashicorp-terraform-0-12-preview-for-and-for-each/
but from your comment, it seems you're trying to set the tags attribute with perhaps a map of key/value pairs? in this case, this is certainly doable 😄 you should be able to directly set the field with tags = local.standard_tags
OR if you intend to set the tags attribute with a list of key/value pairs, a for loop can work as well by doing something like:
locals {
standard_tags = [
{
name = "a"
number = 1
},
{
name = "b"
number = 2
},
{
name = "c"
number = 3
},
]
}
resource "aws_instance" "test" {
...
tags = {
for tag in local.standard_tags:
tag.name => tag.number
}
}

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

Terraform Interpolation Into a var with map lookup

I have exported my current resources using Terraforming and got a huge file which holds all the security groups.
The thing is, that in each security group there are some rules which refers to the security groups IDs - which doesnt exists in the new region i'm planning to run terraform on. for example:
resource "aws_security_group" "my-group" {
name = "my-group"
description = ""
vpc_id = "${var.vpc["production"]}"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = ["sg-25bee542"] <-- this ID doesnt exists in the new region i'm planning to work on
self = false
}
I've created a map with all the old security groups:
variable "security_groups" {
type = "map"
default = {
"sg-acd22fdb" = "default"
"sg-52cd3025" = "my-group"
"sg-25bee542" = "my-group2"
...
}
}
Now I am trying to resolve the hard coded sg-*id* to the corresponding security group name and interpolate that into a variable so the first example will work this way:
resource "aws_security_group" "my-group" {
name = "my-group"
description = ""
vpc_id = "${var.vpc["production"]}"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = ["${aws_security_group.my-group2.id}"] <-- the 'my-group2' should be resolved from the map variable
self = false
}
Something like:
resource "aws_security_group" "my-group" {
name = "my-group"
description = ""
vpc_id = "${var.vpc["production"]}"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = ["${aws_security_group.[lookup(security_groups,sg-25bee542]].id}"] <-- the 'my-group2' string should be resolved from the map variable by looking its sg ID
self = false
}
I hope I made myself clear on that issue...any ideas?
The way you access a map variable in terraform is like this
${var.security_groups["sg-acd22fdb"]}
If you want to get the sg_ID, you can create the map the other way around.
variable "security_groups" {
type = "map"
default = {
"default = "sg-acd22fdb"
"my-group" = "sg-52cd3025"
"my-group2" = "sg-25bee542"
...
}
}
And then use
${var.security_groups["my-group2"]}
As suggested, you need to reverse the map. you can either reverse it at the origin (variable declaration) or use the transpose(map) function.
something like
${transpose(var.security_groups)["sg-acd22fdb"]}
might work

Resources