What I want
In terraform, I have a map service_map:
variable "service_map" {
type = map
description = "Map of some services and their ports."
default = {
"dns" = "53"
"web" = "443"
"ssh" = "22"
"proxy" = ""
}
}
To create LB listeners on AWS, I want to call the resource aws_lb_listener, looping over the map service_map, skipping all items without value (in this case, only proxy):
resource "aws_lb_listener" "listeners" {
for_each = var.service_map
load_balancer_arn = aws_lb.all_lbs[each.key].arn
port = each.value
protocol = each.key != "dns" ? "TCP" : "TCP_UDP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.service_map-tg[each.key].arn
}
}
What I tried
Create a second, local map with all key=value pairs where value is not empty:
locals {
service_map_temp = [ for service, port in var.service_map : service, port if port != "" ]
}
Which does not work: Extra characters after the end of the 'for' expression.. And I guess there are smarter solutions than that approach.
Idea: Skipping empty each.values:
resource "aws_lb_listener" "listeners" {
for_each = var.service_map
load_balancer_arn = aws_lb.all_lbs[each.key].arn
port = each.value != "" # Skipping part
protocol = each.key != "dns" ? "TCP" : "TCP_UDP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.service_map-tg[each.key].arn
}
}
But I doubt that's going to work because I am still calling the resource but with an empty port, which will fail. Since I am just starting out with terraform, I am sure there is a solution I did not think/read about yet.
Your first solution failed because you have used list brackets [ ... ] but you intend to produce a map. To produce a map from a for expression, use map brackets { ... }:
locals {
service_map_temp = {
for service, port in var.service_map :
service => port if port != ""
}
}
The key difference is that a map for expression expects two expressions after the colon (the key and the value), while the list for expression expects only one.
If you like, you can inline that expression directly in the for_each argument, to keep everything together in one block:
resource "aws_lb_listener" "listeners" {
for_each = {
for service, port in var.service_map :
service => port if port != ""
}
load_balancer_arn = aws_lb.all_lbs[each.key].arn
port = each.value
protocol = each.key != "dns" ? "TCP" : "TCP_UDP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.service_map-tg[each.key].arn
}
}
I realized that if you set the parameter to null instead of an empty string "", it will automatically remove the key from the map without having to create a for loop.
variable "service_map" {
type = map
description = "Map of some services and their ports."
default = {
"dns" = "53"
"web" = "443"
"ssh" = "22"
"proxy" = null
}
}
output "service_map" {
value = var.service_map
}
$ terraform apply
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
service_map = {
"dns" = "53"
"ssh" = "22"
"web" = "443"
}
Related
I created a resource that produces a list of VM's using for_each argument. I'm having trouble trying to reference this resource to my web_private_group.
resource "google_compute_instance_group" "web_private_group" {
name = "${format("%s","${var.gcp_resource_name}-${var.gcp_env}-vm-group")}"
description = "Web servers instance group"
zone = var.gcp_zone_1
network = google_compute_network.vpc.self_link
# I've added some attempts I've tried that do not work...
instances = [
//google_compute_instance.compute_engines.self_link
//[for o in google_compute_instance.compute_engines : o.self_link]
{for k, o in google_compute_instance.compute_engines : k => o.self_link}
//google_compute_instance.web_private_2.self_link
]
named_port {
name = "http"
port = "80"
}
named_port {
name = "https"
port = "443"
}
}
# Create Google Cloud VMs
resource "google_compute_instance" "compute_engines" {
for_each = var.vm_instances
name = "${format("%s","${var.gcp_resource_name}-${var.gcp_env}-each.value")}"
machine_type = "e2-micro"
zone = var.gcp_zone_1
tags = ["ssh","http"]
boot_disk {
initialize_params {
image = "debian-10"
}
}
}
variable "vm_instances" {
description = "list of VM's"
type = set(string)
default = ["vm1", "vm2", "vm3"]
}
How can I properly link my compute_engines to my web_private_group resource within the instances= [] block?
Edit: To further clarify, how can I state the fact that there are multiple instances within my compute_engines resource?
You probably just need to use a splat expression as follows:
instances = values(google_compute_instance.compute_engines)[*].id
Moreover, a for expression can be used as well:
instances = [for res in google_compute_instance.compute_engines: res.id]
I am attempting to loop over a module equal to the number of times a map appears inside a nested list of maps as follows:
vars.tf
variable "http_tcp_listeners" {
description = "A list of maps describing the HTTP listeners or TCP ports for this NLB"
type = any
default = [
{
"http_tcp_listener" = [
{
port = "80"
protocol = "TCP"
},
{
port = "7364"
protocol = "TCP"
}
]
},
{
"http_tcp_listener" = [
{
port = "8080"
protocol = "TCP"
},
{
port = "7365"
protocol = "TCP"
}
]
}
]
}
main.tf
module "create_network_lb" {
count = length(var."http_tcp_listeners")
source = "../../modules/lb"
subnets = tolist(data.aws_subnet_ids.private_compute[0].ids)
vpc_id = sort(data.aws_vpcs.platform_private_vpc.ids)[0]
target_groups = lookup(var.target_groups[count.index], "target_group", null)
http_tcp_listeners = lookup(var.http_tcp_listeners[count.index], "http_tcp_listener", null)
module
resource "aws_lb_listener" "frontend_http_tcp" {
count = var.create_lb ? length(var.http_tcp_listeners) : 0
load_balancer_arn = aws_lb.default[0].arn
port = var.http_tcp_listeners[count.index]["port"]
protocol = var.http_tcp_listeners[count.index]["protocol"]
dynamic "default_action" {
for_each = length(keys(var.http_tcp_listeners[count.index])) == 0 ? [] : [var.http_tcp_listeners[count.index]]
content {
type = lookup(default_action.value, "action_type", "forward")
target_group_arn = contains([null, "", "forward"], lookup(default_action.value, "action_type", "")) ? aws_lb_target_group.main[lookup(default_action.value, "target_group_index", count.index)].id : null
dynamic "redirect" {
for_each = length(keys(lookup(default_action.value, "redirect", {}))) == 0 ? [] : [lookup(default_action.value, "redirect", {})]
content {
path = lookup(redirect.value, "path", null)
host = lookup(redirect.value, "host", null)
port = lookup(redirect.value, "port", null)
protocol = lookup(redirect.value, "protocol", null)
query = lookup(redirect.value, "query", null)
status_code = redirect.value["status_code"]
}
}
dynamic "fixed_response" {
for_each = length(keys(lookup(default_action.value, "fixed_response", {}))) == 0 ? [] : [lookup(default_action.value, "fixed_response", {})]
content {
content_type = fixed_response.value["content_type"]
message_body = lookup(fixed_response.value, "message_body", null)
status_code = lookup(fixed_response.value, "status_code", null)
}
}
}
}
}
When performing a "terraform plan", it displays only the last "http_tcp_listener" value. The variable for the module must be in format "[{port=80, protocol="TCP"},{port=7364, protocol="TCP"}]" hence, everything after each iteration of "http_tcp_listener".
During troubleshooting, Terraform seems to think that the variable is a tuple with one element per the error:
Error: Invalid index
on main.tf line 86, in module "create_network_lb":
86: http_tcp_listeners = [lookup(var.http_tcp_listeners[1], "http_tcp_listener")]
|----------------
| var.http_tcp_listeners is tuple with 1 element
The given key does not identify an element in this collection value.
If I manually change one of the keys from "http_tcp_listener" to "http_tcp_listener1", and reflect this in the main.tf lookup value, it will display that value. i.e, if I rename the first key and reference it, terraform plan will display ports 80 and 7364 instead of 8080 and 7365.
Any help would be greatly appreciated.
Solved by re-creating the data structure, and using for_each to call the module. Details 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
}
}
I am creating a module to spin up a basic web server.
I am trying to get it so that if the user does not specify an AMI then the ubuntu image for that region is used.
I have a data block to get the AMI ID of the ubuntu 16.04 image for that region but I cannot set this as the default for a variable as interpolation does not work.
My module is as follows:-
main.tf
resource "aws_instance" "web" {
ami = "${var.aws_ami}"
instance_type = "${var.instance_type}"
security_groups = ["${aws_security_groups.web.id}"]
tags {
Name = "WEB_SERVER"
}
}
resource "aws_security_groups" "web" {
name = "WEB_SERVER-HTTP-HTTPS-SG"
ingress {
from_port = "${var.http_port}"
to_port = "${var.http_port}"
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = "${var.https_port}"
to_port = "${var.https_port}"
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
variables.tf
variable "instance_type" {
description = "The instance size to deploy. Defaults to t2.micro"
default = "t2.micro"
}
variable "http_port" {
description = "The port to use for HTTP traffic. Defaults to 80"
default = "80"
}
variable "https_port" {
description = "The port to use for HTTPS traffic. Defaults to 443"
default = "443"
}
data "aws_ami" "ubuntu" {
filter {
name = "state"
values = ["available"]
}
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["099720109477"]
}
locals {
default_ami = "${data.aws_ami.ubuntu.id}"
}
variable aws_ami {
description = "The AMI used to launch the instance. Defaults to Ubuntu 16.04"
default = "${local.default_ami}"
}
Try using a ternary operator interpolation:
variable "user_specified_ami" {
default = "ami-12345678"
}
resource "aws_instance" "web" {
ami = "${var.user_specified_ami == "" ? data.aws_ami.ubuntu.id : var.user_specified_ami}"
# ... other params omitted ....
}
Set user_specified_ami's default to something to use that AMI. Set it to blank to use the AMI ID Terraform gets from the AWS provider.
i.e. if user_specified_ami is anything other blank (""), then it will be chosen for the AMI, else the AMI Terraform gets the one from AWS.
BTW, maybe you want to use the most_recent = true param in the data "aws_ami" resource?
A similar solution to the other answers, but using the coalesce function:
variable "user_specified_ami" {
default = ""
}
resource "aws_instance" "web" {
ami = coalesce(var.user_specified_ami, data.aws_ami.ubuntu.id)
}
KJH's answer works great, but it felt a bit messy to me to have that logic inline, so I made an abstraction using null_data_source. Here's what that would look like:
variable "ami" {
default = ""
}
data "null_data_source" "data_variables" {
inputs = {
ami = "${var.ami == "" ? data.aws_ami.ubuntu.id : var.ami}"
}
}
resource "aws_instance" "web" {
ami = "${data.null_data_source.data_variables.outputs["ami"]}"
# ... other params omitted ....
}
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