Getting the CIDR block of a set of subnets - terraform

I am trying to get the CIDR block of a set of subnets IDs provided a parameter.
data "aws_subnet" "target" {
for_each = "${toset(var.subnet_ids)}"
id = "${each.value}"
}
resource "aws_security_group" "registry" {
vpc_id = "${var.vpc_id}"
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["${data.aws_subnet.target.*.cidr_block}"]
}
tags = {
Name = "${var.name}"
}
}
The error I am getting is:
cidr_blocks = "${data.aws_subnet.target.*.cidr_block}"
This object does not have an attribute named "cidr_block".
Terraform configuration:
Terraform v0.12.24
+ provider.aws v2.55.0
+ provider.template v2.1.2
Thanks to whoever can help!

You should use a splat expression to the values of the map.
resource "aws_security_group" "registry" {
vpc_id = "${var.vpc_id}"
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = values(data.aws_subnet.target).*.cidr_block
}
tags = {
Name = "${var.name}"
}
}

I think the problem here is that the for_each argument makes data.aws_subnet.target appear as a map value, and the splat operator .* is defined to behave as a no-op if it's applied to anything other than a list or set. Therefore expression evaluation here is taking the following steps:
First evaluate data.aws_subnet.target, producing a map of objects whose keys are the strings from var.subnet_ids, which presumably look like subnet-12345.
Then apply .* to that map. Because it's a map, that produces the same result again.
Then apply .cidr_block to that map, which fails because its keys are the subnet ids, not the attributes of the subnet objects.
To get the desired result here, we can use values to disregard the keys and take the values from the map as a list:
cidr_blocks = values(data.aws_subnet.target).*.cidr_block
Because the result of values(...) is a list, the .* operator will behave as expected and try to find the cidr_block attribute in each of the elements of the list.
Another option is to use a for expression, which is a generalization of the splat syntax that works for any collection-typed value:
cidr_blocks = [for s in data.aws_subnet.target : s.cidr_block]

Related

Benefit of having Concat function in terraform

I am trying to understand the benefit of having a Concat function in the output section.
as I have experiment output section with and without Concat. But I cannot see any difference.
example
resource "aws_security_group" "sg_22" {
name = "sgx_22"
vpc_id = var.vpc
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group" "sg_8080" {
name = "sgx_8080"
vpc_id = var.vpc
ingress {
from_port = 8080
to_port = 8080
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
output "web_security_group_ids" {
value = concat([aws_security_group.sg_22.id, aws_security_group.sg_8080.id])
}
VS
resource "aws_security_group" "sg_22" {
name = "sgx_22"
vpc_id = var.vpc
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group" "sg_8080" {
name = "sgx_8080"
vpc_id = var.vpc
ingress {
from_port = 8080
to_port = 8080
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
output "web_security_group_ids" {
value = [aws_security_group.sg_22.id, aws_security_group.sg_8080.id]
}
In both case, Terraform give the same output in the same format.
Any idea?
That is a mistake in the learning guide. The concat function "concatenates" two different argument lists into a single list containing all the elements of each. In this case, you have a single list of two lists as an argument to the concat function. I believe that the actual function the guide should have mentioned is flatten. This function will "flatten" redundant nested lists into a single list:
output "web_security_group_ids" {
value = flatten([aws_security_group.sg_22.id, aws_security_group.sg_8080.id])
}
However, this is still rather pointless as the only reason it is a nested list is because the two lists are arguments to a list constructor. Alternatively, the guide may actually have meant to concatenate the two lists directly:
output "web_security_group_ids" {
value = concat(aws_security_group.sg_22.id, aws_security_group.sg_8080.id)
}
The actual best way to define this output would be to simply use the resource attribute related to the resource argument. This will then be a simple definition in the output, a single list, and guaranteed to update dynamically based on changes to your arguments.
output "web_security_group_ids" {
value = aws_instance.web.vpc_security_group_ids
}

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
}

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

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