Terraform Inappropriate value for attribute "route" - amazon-web-services

relatively new to terraform and currently trying to build cloud infrastructure in AWS.
I get an error when I use an official example (little bit changed) from the documentation for the resource aws_route_table (https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table)
resource "aws_route_table" "prod-route-table" {
vpc_id = aws_vpc.prod-vpc.id
route = [{
# Route all Traffic to the internet gateway
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.gw.id
},{
ipv6_cidr_block = "::/0"
gateway_id = aws_internet_gateway.gw.id
}]
}
I get the following error message
Error: Incorrect attribute value type
│ Inappropriate value for attribute "route": element 0: attributes "carrier_gateway_id",
│ "destination_prefix_list_id", "egress_only_gateway_id", "instance_id", "ipv6_cidr_block",
│ "local_gateway_id", "nat_gateway_id", "network_interface_id", "transit_gateway_id", "vpc_endpoint_id",
│ and "vpc_peering_connection_id" are required.
Adding all these attributes solves the error however this blows up the code massively.
Writing it differently (see the following) results in no errors, is the terraform AWS documentation incorrect, as they clearly state the first way of writing it?
resource "aws_route_table" "prod-route-table" {
vpc_id = aws_vpc.prod-vpc.id
route {
# Route all Traffic to the internet gateway
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.gw.id
}
route{
ipv6_cidr_block = "::/0"
gateway_id = aws_internet_gateway.gw.id
}
}
I am using terraform v1.0.10 and aws provider version = "3.63.0"
Thanks in advance

The documentation for that argument mentions that it's using the legacy attributes as blocks mode, which is a holdover from Terraform v0.12 for some situations where providers were depending on being able to write certain arguments in both the nested block syntax (like in your second example) and in the attribute syntax (like in your first example).
The syntax currently shown in the doc example -- and your first example in your question -- is contrary to the advice about how to write a fixed value (as opposed to a dynamic value), so indeed the second example you showed here would be the preferred way as far as the general Terraform documentation is concerned.
resource "aws_route_table" "example" {
vpc_id = aws_vpc.example.id
route {
cidr_block = "10.0.1.0/24"
gateway_id = aws_internet_gateway.example.id
}
route {
ipv6_cidr_block = "::/0"
egress_only_gateway_id = aws_egress_only_internet_gateway.example.id
}
tags = {
Name = "example"
}
}
Possibly the AWS provider documentation author here used the attribute syntax in order to show symmetry with the special case of setting route = [] to explicitly state that there should be no routes at all, because entirely omitting this argument unfortunately (for historical reasons) means to ignore any existing routes in the remote API, rather than to remove all existing routes in the remote API.
There's a little more about the behavior you saw in the subsequent section arbitrary expressions with argument syntax:
Because of the rule that argument declarations like this fully override any default value, when creating a list-of-objects expression directly the usual handling of optional arguments does not apply, so all of the arguments must be assigned a value, even if it's an explicit null:
example = [
{
# Cannot omit foo in this case, even though it would be optional in the
# nested block syntax.
foo = null
},
]
Over time providers will gradually phase out this legacy mode, but must do so cautiously because it can be a breaking change for some existing configurations. Until then this is unfortunately a confusing rough edge for some particular provider attributes, though they should at least all link to the relevant documentation page I linked above to note that their behavior doesn't match the normal Terraform argument-handling behaviors.

Related

Value attribute is expected when creating aws internet gateway with terraform

I want to create an internet gateway with terraform. Following the [terraform documentation][1] I have the following block
resource "aws_internet_gateway" "prod-igw" {
vpc_id = "${aws_vpc.prod-vpc.id}"
tags = {{
Name = "pos-igw"
}
}
After applying I get this error message.
Error: Missing attribute value
Expected an attribute value, introduced by an equals sign ("=").
There's nothing about value in the documentation though.
[1]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/internet_gateway
You have a typo:
resource "aws_internet_gateway" "prod-igw" {
vpc_id = aws_vpc.prod-vpc.id
tags = { # <--- there was an extra {
Name = "pos-igw"
}
}
Please make sure to use a modern IDE (e.g., VScode) where there is a terraform extension which will prevent these things from happening and you will not have to ask a question on SO.

Is setting an attribute to null the same as not setting it in Terraform configuration?

I was looking at the code of terraform VPC module and found
resource "aws_subnet" "public" {
vpc_id = aws_vpc_ipv4_cidr_block_association.secondary_cidr.vpc_id
cidr_block = "172.2.0.0/24"
availability_zone = null
}
availability_zone attribute is optional based on the docs of AWS provider.
Is setting availability_zone to null is the same as not listing it all? For example, this is the same way to write the following configuration:
resource "aws_subnet" "in_secondary_cidr" {
vpc_id = aws_vpc_ipv4_cidr_block_association.secondary_cidr.vpc_id
cidr_block = "172.2.0.0/24"
}
I've never seen it before. Seems like it's a popular way to declare an optional variable in a module:
variable "private_subnet_assign_ipv6_address_on_creation" {
description = "Assign IPv6 address on private subnet, must be disabled to change IPv6 CIDRs. This is the IPv6 equivalent of map_public_ip_on_launch"
type = bool
default = null
}
Yes, if you use null is the same as not listing it all. From docs:
a value that represents absence or omission. If you set an argument of a resource to null, Terraform behaves as though you had completely omitted it — it will use the argument's default value if it has one, or raise an error if the argument is mandatory. null is most useful in conditional expressions, so you can dynamically omit an argument if a condition isn't met.

terraform throw error: "Incorrect attribute value type" when I run plan

I using terraform from https://github.com/Oxalide/terraform-fargate-example.
When I run as it is I get error:
Error: Incorrect attribute value type
on main.tf line, in resource "aws_alb" "main":
: subnets = ["${aws_subnet.public.*.id}"]
Inappropriate value for attribute "subnets": element 0: string required.
The code in this line:
resource "aws_alb" "main" {
name = "tf-ecs-chat"
subnets = ["${aws_subnet.public.*.id}"] # <--- here is the error
security_groups = ["${aws_security_group.lb.id}"]
}
According to the docs example it seems right.
resource "aws_lb" "test" {
name = "test-lb-tf"
internal = false
load_balancer_type = "application"
security_groups = ["${aws_security_group.lb_sg.id}"]
subnets = ["${aws_subnet.public.*.id}"]
...
What the error means? how to solve it?
This example seems to have been written for an older version of Terraform and is, unfortunately, relying on a configuration language bug from that earlier version that has since been fixed.
The expression aws_subnet.public.*.id produces a list of the id attribute values from all of the instances of aws_subnet.public. If you put that expression inside the list construction brackets [ ] then Terraform understands that as a list of lists, like this:
[
[
"i-abc123",
"i-def456",
"i-ghi789",
],
]
The aws_alb resource type then rejects that as a value for subnets, because that argument is defined as expecting a list of strings, not a list of lists of strings.
Because aws_subnet.public.*.id already returns a list, you can just use it directly as the value of subnets:
subnets = aws_subnet.public.*.id
Notice also that we don't need the string interpolation syntax, because we're just using a list value directly, not constructing a string from a template.
The security_groups argument in your example uses similar syntax, but in that case aws_security_group.lb.id is already a single ID string rather than a list, and so the [ ] list brackets to construct a list are needed in that case. However, we can still clean that up by removing the unnecessary template interpolation syntax, giving the following result:
resource "aws_alb" "main" {
name = "tf-ecs-chat"
subnets = aws_subnet.public.*.id
security_groups = [aws_security_group.lb.id]
}
Some of the examples in the Terraform provider documentation are still using syntax from older versions of Terraform, particularly in situations where the provider in question is still compatible with the older version, which I believe is the case for the AWS provider at the time I'm writing this.
In most cases that just results in unnecessarily-verbose syntax, but there are unfortunately some cases like this where the example was relying on a bug in the former version that has now been fixed, and so the example itself no longer works correctly in current Terraform versions.
If you notice cases like this, you can report a bug by following the "Report an Issue" link on the provider's page in the Terraform Registry. In this case, the provider in question is hashicorp/aws.

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: Creating and validating multiple ACM certificates

I'm running into a really confusing Terraform resource issue automating the generation and DNS validation of SSL certificates in ACM for a list of (Terraform-managed) hosted zones. Code can also be found in this gist.
I'm starting by bootstrapping hosted zones referencing this environment-specific variable.
hosted_zones = [
{
domain = "site1.com"
zone_id = "MANUALLY FILL"
}
]
The block I am using to build the zones seems to work reliably.
resource "aws_route53_zone" "zones" {
count = "${length(var.hosted_zones)}"
name = "${lookup(var.hosted_zones[count.index], "domain")}"
}
After the zones are built, I am manually copying the zone ID into the variable because I haven't come up with a clever way to automate it given a combination of limitations of HCL and my lack of experience with it.
I can reliably generate naked and splat certificates for each hosted zone using...
resource "aws_acm_certificate" "cert" {
count = "${length(var.hosted_zones)}"
domain_name = "${lookup(var.hosted_zones[count.index], "domain")}"
subject_alternative_names = ["*.${lookup(var.hosted_zones[count.index], "domain")}"]
validation_method = "DNS"
tags {
Project = "${var.project}"
Environment = "${var.environment}"
}
}
Where things get hairy is when I try to automate the DNS validation for the certs. There is a good example in the documentation for a single hosted zone, but I haven't been able to successfully port it to multiple hosted-zones. My attempt...
resource "aws_route53_record" "cert_validation" {
count = "${length(var.hosted_zones)}"
name = "${aws_acm_certificate.cert.*.domain_validation_options.0.resource_record_name[count.index]}"
type = "${aws_acm_certificate.cert.*.domain_validation_options.0.resource_record_type[count.index]}"
zone_id = "${var.zone_override != "" ? var.zone_override : lookup(var.hosted_zones[count.index], "zone_id")}"
records = ["${aws_acm_certificate.cert.*.domain_validation_options.0.resource_record_value[count.index]}"]
ttl = 60
}
resource "aws_acm_certificate_validation" "cert" {
count = "${length(var.hosted_zones)}"
certificate_arn = "${aws_acm_certificate.cert.*.arn[count.index]}"
validation_record_fqdns = ["${aws_route53_record.cert_validation.*.fqdn[count.index]}"]
}
The error I am seeing on first run is:
* module.acm.aws_route53_record.cert_validation: 1 error(s) occurred:
* module.acm.aws_route53_record.cert_validation: Resource 'aws_acm_certificate.cert' does not have attribute 'domain_validation_options.0.resource_record_value' for variable 'aws_acm_certificate.cert.*.domain_validation_options.0.resource_record_value'
The obnoxious part is that if I comment the validation resources, the apply succeeds, and then uncommenting them and re-running also succeeds.
I've tried (what feels like) every permutation of element() lookup(), list() and map() to target certificates by index in the output from the first resource block, but am running into documented "flat list" limitations and this is the closest I've gotten to success. I'd like to understand why the workaround is necessary so I can eliminate it. This feels like a syntax issue or me trying to get HCL to behave more like an OO language than it is.
Thank you for any experience that may help!
I had a similar scenario and the key to solving it was the use of locals and flatten(). The approach should also work for you such that you shouldn't need two passes to create the resources.
In this scenario there are multiple domains that each have subdomains that will appear in the subjectAltName section of the certificate. For example:
├── preview.example.com
│ ├── app.preview.example.com
│ └── www.preview.example.com
├── demo.example.com
│ ├── app.demo.example.com
│ └── www.demo.example.com
├── staging.example.com
│ ├── app.staging.example.com
│ └── www.staging.example.com
└── example.com
├── app.example.com
└── www.example.com
To achieve this we first set some variables:
variable "domains" {
type = "list"
default = [
"demo.example.com",
"preview.example.com",
"staging.example.com",
"example.com"
]
}
variable "subdomains" {
type = "list"
default = [
"app",
"www"
]
}
Next we create the certificate resources that contain the subdomains as SANs.
resource "aws_acm_certificate" "cert" {
count = "${length(var.domains)}"
domain_name = "${element(var.domains, count.index)}"
validation_method = "DNS"
subject_alternative_names = ["${
formatlist("%s.%s",
var.subdomains,
element(var.domains, count.index)
)
}"]
}
Next we're going to need a local variable to flatten the resulting set of domains and subdomains.
This is needed because terraform doesn't support nested list syntax as of version 0.11.7, neither
via the element() interpolation nor the `list[count].
locals {
dvo = "${flatten(aws_acm_certificate.cert.*.domain_validation_options)}"
}
We'll next need a lookup of the Route 53 zone that we can use in the subsequent Route 53 records:
data "aws_route53_zone" "zone" {
count = "${length(var.domains) > 0 ? 1 : 0}"
name = "example.com."
private_zone = false
}
We then create the Route 53 DNS records that will be populated with data from the certificate
resource for DNS validation. We're adding one to the subdomains so that we also have a
record for the base domain not included in the list of subdomains.
resource "aws_route53_record" "cert_validation" {
count = "${length(var.domains) * (length(var.subdomains) + 1)}"
zone_id = "${data.aws_route53_zone.zone.id}"
ttl = 60
name = "${lookup(local.dvo[count.index], "resource_record_name")}"
type = "${lookup(local.dvo[count.index], "resource_record_type")}"
records = ["${lookup(local.dvo[count.index], "resource_record_value")}"]
}
Finally we create the certificate validation resource that will wait for the certificate to be
issued.
resource "aws_acm_certificate_validation" "cert" {
count = "${length(var.domains) * (length(var.subdomains) + 1)}"
certificate_arn = "${element(aws_acm_certificate.cert.*.arn, count.index)}"
validation_record_fqdns = ["${aws_route53_record.cert_validation.*.fqdn}"]
}
The one caveat for this last resource is that it'll create one instance of the resource for every
certificate requested, but each instance will depend on all the FQDNs across all domains and
subdomains. This won't affect anything in AWS but the terraform code won't continue/complete
until all certs are issued.
This should work in a single apply run with no need to -target any resources in a first pass,
though there is an apparently known issue around how long it takes for the validations to
complete when
performed via terraform, and for this reason it may require a second pass, albeit without changing the code or plan/apply invocation.
So after a bit of experimenting, I ended up leveraging -target=aws_acm_certificate.cert as a workaround to avoid the missing attribute errors I was seeing. The syntax I was using above was correct, and the error was a result of the apply needing to complete for the certificate before the validation steps could reference the generated attributes.
In addition, I found an elegant solution for the MANUAL FILL step using zipmap. The result looks like this...
Variable:
hosted_zones = [
"foo.com"
]
Output from hosted_zones module:
output "hosted_zone_ids" {
value = "${zipmap(var.hosted_zones, aws_route53_zone.zones.*.zone_id)}"
}
Then, my certificate generation/validation module looks like this, where var.hosted_zone_map is the output of the previous zipmap that creates a map of hosted zone domain name to assigned zone ID:
resource "aws_acm_certificate" "cert" {
count = "${length(keys(var.hosted_zone_map))}"
domain_name = "${element(keys(var.hosted_zone_map), count.index)}"
subject_alternative_names = ["*.${element(keys(var.hosted_zone_map), count.index)}"]
validation_method = "DNS"
tags {
Project = "${var.project}"
Environment = "${var.environment}"
}
}
resource "aws_route53_record" "cert_validation" {
count = "${length(keys(var.hosted_zone_map))}"
zone_id = "${lookup(var.hosted_zone_map, element(keys(var.hosted_zone_map), count.index))}"
name = "${aws_acm_certificate.cert.*.domain_validation_options.0.resource_record_name[count.index]}"
type = "${aws_acm_certificate.cert.*.domain_validation_options.0.resource_record_type[count.index]}"
records = ["${aws_acm_certificate.cert.*.domain_validation_options.0.resource_record_value[count.index]}"]
ttl = 60
}
resource "aws_acm_certificate_validation" "cert" {
count = "${length(keys(var.hosted_zone_map))}"
certificate_arn = "${aws_acm_certificate.cert.*.arn[count.index]}"
validation_record_fqdns = ["${aws_route53_record.cert_validation.*.fqdn[count.index]}"]
}
The positioning of the splat was definitely the trickiest and least documented part of tracking this down, so hopefully this helps someone else out.
I know this question is old, but for anyone searching for answers today, Terraform's updated documentation for the AWS provider has a great example for this using a for_each loop. This applies to terraform 1.x, obviously, but maybe other recent Check out https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate_validation#dns-validation-with-route-53