How do I iterate over a 'count' resource in Terraform? - amazon-web-services

In this example, I'm trying to create 3 EC2 instances which each have an elastic IP assigned. I want to achieve this by saying the following.
resource "aws_instance" "web_servers" {
ami = "ami-09e67e426f25ce0d7"
instance_type = "t3.micro"
...
count = 3
}
and, along with other networking instances,
resource "aws_eip" "elastic_ip" {
for_each = aws_instance.web_servers
instance = each.key
vpc = true
}
However, this is saying the following:
The given "for_each" argument value is unsuitable: the "for_each" argument must be a map, or set of strings, and you have provided a value of type tuple.
I have tried wrapping the for_each in a toset() which also says there is an issue with an unknown number of instances - I know there are 3 though. Is there something I'm missing around the count & for_each keywords?

If you really want to use for_each, rather then count again, it should be:
resource "aws_eip" "elastic_ip" {
for_each = {for idx, val in aws_instance.web_servers: idx => val}
instance = each.value.id
vpc = true
}
But since you are using count in the first place, it would be probably better to use count for your aws_eip as well:
resource "aws_eip" "elastic_ip" {
count = length(aws_instance.web_servers)
instance = aws_instance.web_servers[count.index].id
vpc = true
}

In AWS is your instance field = EC2 ID = ARN?
If so you may be able to access the arn attribute in the aws_instance resource block.
You could try toset(aws_instance.web_servers.arn) and keep instance = each.key. I generally just use each.value but they should be the same when dealing with a set.

Related

Get first element of a tuple in terraform

I'm trying to deploy my EKS nodes with only one subnet but I dont know how to give to the resource only one. I show you my code:
resource "aws_eks_node_group" "managed_workers" {
for_each = var.nodegroups[terraform.workspace]
cluster_name = aws_eks_cluster.cluster.name
node_group_name = each.value.Name
node_role_arn = aws_iam_role.managed_workers.arn
subnet_ids = aws_subnet.private.*.id
On the other hand I have a normal task to create the subnets and give the ouput to all my code:
resource "aws_subnet" "private" {
count = length(local.subnet_priv)
vpc_id = var.vpc_id[terraform.workspace]
cidr_block = local.subnet_priv[count.index]
availability_zone = element(lookup(var.availability_zones, terraform.workspace), count.index)
map_public_ip_on_launch = false
So.. I don't know how to get from my subnet_ids argument only the first subnet of the tuple. Now, as you can see, I'm getting all of them but I tried different ways to do but with no success (aws_subnet.private[0].*.id , aws_subnet.private[0].id, etc)
Any idea?
Thanks a lot!
EKS node group subnet_ids arguments expects a tuple. In the original example subnet_ids = aws_subnet.private.*.id the splat operator is used. The spear operator (*) essentially creates a tuple with all the available resources, in our case all the available subnets.
If we want to pass only one subnet from all the available ones, we have to create a tuple with a single element. We could do that by taking the first element from all the existing ones, for example:
subnet_ids = [aws_subnet.private[0].id]
Although, this might work, I personally don't really consider it to be elegant solution. Certainly a better way to accomplish the same result is to modify the local.subnet_priv tuple to contain only one subnet id.

Terraform: length() on data source cannot be determined until apply?

I am trying to dynamically declare multiple aws_nat_gateway data sources by retrieving the list of public subnets through the aws_subnet_ids data source. However, when I try to set the count parameter to be equal to the length of the subnet IDs, I get an error saying The "count" value depends on resource attributes that cannot be determined until apply....
This is almost in direct contradiction to the example in their documentation!. How do I fix this? Is their documentation wrong?
I am using Terraform v0.12.
data "aws_vpc" "environment_vpc" {
id = var.vpc_id
}
data "aws_subnet_ids" "public_subnet_ids" {
vpc_id = data.aws_vpc.environment_vpc.id
tags = {
Tier = "public"
}
depends_on = [data.aws_vpc.environment_vpc]
}
data "aws_nat_gateway" "nat_gateway" {
count = length(data.aws_subnet_ids.public_subnet_ids.ids) # <= Error
subnet_id = data.aws_subnet_ids.public_subnet_ids.ids.*[count.index]
depends_on = [data.aws_subnet_ids.public_subnet_ids]
}
I expect to be able to apply this template successfully, but I am getting the following error:
Error: Invalid count argument
on ../src/variables.tf line 78, in data "aws_nat_gateway" "nat_gateway":
78: count = "${length(data.aws_subnet_ids.public_subnet_ids.ids)}"
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.
It seems you are trying to fetch subnets that weren't created yet or they couldn't be determinated, the terraform cmd output suggests you add -target flag to create the VPC and subnets or do another task first, after that, you'll apply the nat_gateway resource. I suggest you use the AZs list instead of subnets ids, I'll add a simple example below.
variable "vpc_azs_list" {
default = [
"us-east-1d",
"us-east-1e"
]
}
resource "aws_nat_gateway" "nat" {
count = var.enable_nat_gateways ? length(var.azs_list) : 0
allocation_id = "xxxxxxxxx"
subnet_id = "xxxxxxxxx"
depends_on = [
aws_internet_gateway.main,
aws_eip.nat_eip,
]
tags = {
"Name" = "nat-gateway-name"
"costCenter" = "xxxxxxxxx"
"owner" = "xxxxxxxxx"
}
}
I hope will be useful to you and other users.

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: order of creating array of resources

I have two ec2 instances defined in terraform using the count method.
resource "aws_instance" "example" {
count = "2"
ami = "ami-2d39803a"
instance_type = "t2.micro"
tags {
Name = "example-${count.index}"
}
}
How can I enforce that they are launched one after the other? e.g. the second instance should be created when the first one finishes.
Attempt 1:
depends_on = [aws_instance.example[0]]
result:
Error: aws_instance.example: resource depends on non-existent resource 'aws_instance.example[0]'
Attempt 2:
tags {
Name = "example-${count.index}"
Active = "${count.index == "1" ? "${aws_instance.example.1.arn}" : "this"}"
}
result:
Error: aws_instance.example[0]: aws_instance.example[0]: self reference not allowed: "aws_instance.example.0.arn"
Which leads me to believe the interpolation is calculated after the instance configurations are complete thus it doesn't see that there isn't in fact a circular dependency.
Any ideas?
Thanks
Use terraform apply -parallelism=1 to limit the number of concurrent operations to 1 at a time.

Multiple availability zones with terraform on AWS

The VPC I'm working on has 3 logical tiers: Web, App and DB. For each tier there is one subnet in each availability zone. Total of 6 subnets in the region I'm using.
I'm trying to create EC2 instances using a module and the count parameter but I don't know how to tell terraform to use the two subnets of the App tier. An additional constraint I have is to use static IP addresses (or a way to have a deterministic private name)
I'm playing around with the resource
resource "aws_instance" "app_server" {
...
count = "${var.app_servers_count}"
# Not all at the same time, though!
availability_zone = ...
subnet_id = ...
private_ip = ...
}
Things I've tried/thought so far:
Use data "aws_subnet" "all_app_subnets" {...}, filter by name, get all the subnets that match and use them as a list. But aws_subnet cannot return a list;
Use data "aws_availability_zones" {...} to find all the zones. But I still have the problem of assigning the correct subnet;
Use data "aws_subnet_ids" {...} which looks like the best option. But apparently it doesn't have a filter option to match the networks namel
Pass the subnets IDs as list of strings to the module. But I don't want to hard code the IDs, it's not automation;
Hard code the subnets as data "aws_subnet" "app_subnet_1" {...}, data "aws_subnet" "app_subnet_2" {...} but then I have to use separate sets of variables for each subnet which I don't like;
Get information for each subnet like in the point above but then create a map to access it as a list. But it's not possibile to use interpolation in variables definition;
Not using modules and hard-code each instance for each environment. Mmmm... really?
I really ran out of ideas. It seems that nobody has to deploy instances in specific subnetworks and keep a good degree of abstration. I see only examples where subnetworks are not specified or where people just use default values for everything. Is this really something so unusual?
Thanks in advance to everyone.
It is possible to evenly distribute instances across multiple zones using modulo.
variable "zone" {
description = "for single zone deployment"
default = "europe-west4-b"
}
variable "zones" {
description = "for multi zone deployment"
default = ["europe-west4-b", "europe-west4-c"]
}
resource "google_compute_instance" "default" {
count = "${var.role.count}"
...
zone = "${var.zone != "" ? var.zone: var.zones[ count.index % length(var.zones) ]}"
...
}
This distribution mechanism allow to distribute nodes evenly across zones.
E.g. zones = [A,B] - instance-1 will be in A, instance-2 will in B, instance-3 will be in A again.
By adding zone C to zones will shift instance-3 to C.
The count index in the resource will throw an error if you have more instances than subnets. Use the element interpolation from Terraform
element(list, index) - Returns a single element from a list at the given index. If the index is greater than the number of elements, this function will wrap using a standard mod algorithm. This function only works on flat lists.
subnet_id = "${element(data.aws_subnet_ids.app_tier_ids.ids, count.index)}"
At the end I figured out how to do it, using data "aws_subnet_ids" {...} and more importantly understanding that terraform creates lists out of resources when using count:
variable "target_vpc" {}
variable "app_server_count" {}
variable "app_server_ip_start" {}
# Discover VPC
data "aws_vpc" "target_vpc" {
filter = {
name = "tag:Name"
values = [var.target_vpc]
}
}
# Discover subnet IDs. This requires the subnetworks to be tagged with Tier = "AppTier"
data "aws_subnet_ids" "app_tier_ids" {
vpc_id = data.aws_vpc.target_vpc.id
tags {
Tier = "AppTier"
}
}
# Discover subnets and create a list, one for each found ID
data "aws_subnet" "app_tier" {
count = length(data.aws_subnet_ids.app_tier_ids.ids)
id = data.aws_subnet_ids.app_tier_ids.ids[count.index]
}
resource "aws_instance" "app_server" {
...
# Create N instances
count = var.app_server_count
# Use the "count.index" subnet
subnet_id = data.aws_subnet_ids.app_tier_ids.ids[count.index]
# Create an IP address using the CIDR of the subnet
private_ip = cidrhost(element(data.aws_subnet.app_tier.*.cidr_block, count.index), var.app_server_ip_start + count.index)
...
}
I get Terraform to loop through the subnets in an availability zone by using the aws_subnet_ids data source and filtering by a tag representing the tier (in my case public/private).
This then looks something like this:
variable "vpc" {}
variable "ami" {}
variable "subnet_tier" {}
variable "instance_count" {}
data "aws_vpc" "selected" {
tags {
Name = "${var.vpc}"
}
}
data "aws_subnet_ids" "selected" {
vpc_id = "${data.aws_vpc.selected.id}"
tags {
Tier = "${var.subnet_tier}"
}
}
resource "aws_instance" "instance" {
count = "${var.instance_count}"
ami = "${var.ami}"
subnet_id = "${data.aws_subnet_ids.selected.ids[count.index]}"
instance_type = "${var.instance_type}"
}
This returns a consistent sort order but not necessarily starting with AZ A in your account. I suspect that the AWS API returns the subnets in AZ order but ordered by their own internal id as the AZs are shuffled by account (presumably to stop AZ A being flooded as humans are predictably bad at putting everything in the first place they can use).
You would have to tie yourself in some horrible knots if for some odd reason you particularly care about instances being placed in AZ A first but this minimal example should at least get instances being round-robined through the AZs you have subnets in by relying on Terraform's looping back through arrays when exceeding the array length.