Why is terraform saying that my object has no attributes? - amazon-web-services

I'm using terraform 0.12.31 and trying to create some AWS SES resources. There's one which is returning an error message:
aws_ses_domain_identity.ses_domain_per_tenant is object with no attributes
each.key is "a.env.account.info"
aws_ses_domain_identity.ses_domain_per_tenant is object with no attributes
each.key is "b.env.account.info"
aws_ses_domain_identity.ses_domain_per_tenant is object with no attributes
each.key is "c.env.account.info"
This is my code:
resource "aws_ses_domain_identity" "ses_domain_per_tenant" {
for_each = toset(var.enable_per_tenant_email_domain ? var.tenants_email_domain : [])
domain = each.key
}
resource "aws_ses_domain_dkim" "dkim_domain_per_tenant" {
for_each = toset(var.enable_per_tenant_email_domain ? var.tenants_email_domain : [])
domain = aws_ses_domain_identity.ses_domain_per_tenant[each.key].domain
}
I also have this resource as part of the same code, which is working fine:
resource "aws_route53_record" "ses_amazonses_verification_record_per_tenant" {
for_each = toset(var.enable_per_tenant_email_domain ? var.tenants_email_domain : [])
records = [aws_ses_domain_identity.ses_domain_per_tenant[each.key].verification_token]
}
Where:
tenants_email_domain = ['a.env.account.info', 'b.env.account.info', 'c.env.account.info']
What's happening here?

Except for having the way you are currently using for_each meta-argument, there is an additional thing you can do which can also help avoiding some of the errors and duplicating code and that is chaining for_each between resources [1]:
resource "aws_ses_domain_identity" "ses_domain_per_tenant" {
for_each = toset(var.enable_per_tenant_email_domain ? var.tenants_email_domain : [])
domain = each.key
}
resource "aws_ses_domain_dkim" "dkim_domain_per_tenant" {
for_each = aws_ses_domain_identity.ses_domain_per_tenant
domain = each.value.domain
}
Of course, in order for this to work, the values assigned to the tenants_email_domain have to be fixed to be strings:
tenants_email_domain = ["a.env.account.info", "b.env.account.info", "c.env.account.info"]
[1] https://developer.hashicorp.com/terraform/language/meta-arguments/for_each#chaining-for_each-between-resources

As suggested in the comments the issue is because of the variable tenants_email_domain value having single quotes '.
On the terraform string documentation also they only mention string literals only with ".
The following code generates a valid plan.
resource "aws_ses_domain_identity" "ses_domain_per_tenant" {
for_each = toset(var.enable_per_tenant_email_domain ? var.tenants_email_domain : [])
domain = each.key
}
resource "aws_ses_domain_dkim" "dkim_domain_per_tenant" {
for_each = toset(var.enable_per_tenant_email_domain ? var.tenants_email_domain : [])
domain = aws_ses_domain_identity.ses_domain_per_tenant[each.key].domain
}
variable "tenants_email_domain" {
type = list(string)
default = ["a.env.account.info", "b.env.account.info", "c.env.account.info"] ## updated ` with "" ##
}
variable "enable_per_tenant_email_domain" {
type = bool
default = true
}

Related

Terraform tuple to id for subnet

I got following output:
output "private_subnets" {
description = "List of IDs of private subnets"
value = module.vpc.private_subnets
}
which returns subnet ids like: [subnet-1***, subnet-2***, subnet-3***]
How can I use this in nlb resource?
dynamic "subnet_mapping" {
#for_each = { for k,v in module.vpc.private_subnets : k => v}
for_each = [for s in module.vpc.private_subnets : s]
content {
subnet_id = s
}
}
I have tried s.id, s.value, nothing works. I get following errors:
A reference to a resource type must be followed by at least one attribute access, specifying the resource name.
It should be:
dynamic "subnet_mapping" {
for_each = toset(module.vpc.private_subnets)
content {
subnet_id = subnet_mapping.value
}
}

How use values of list terraform variable in aws_policy.arn

main.tf
module "iam_assumable_role" {
for_each = var.service_accounts
source = "../../../../../../modules/iam-assumable-role-with-oidc/"
create_role = true
role_name = each.value.name
provider_url = replace(module.eks.cluster_oidc_issuer_url, "https://", "")
// role_policy_arns = [for i in each.value.policies : "aws_iam_policy.${i}.arn"]
oidc_fully_qualified_subjects = each.value.wildcard == "" ? ["system:serviceaccount:${each.value.namespace}:${each.value.name}"] : []
oidc_subjects_with_wildcards = each.value.wildcard != "" ? ["system:serviceaccount:${each.value.namespace}:${each.value.wildcard}"] : []
tags = var.tags
}
resource "aws_iam_policy" "dev-policy1" {
name_prefix = "dev-policy"
description = "some description"
policy = data.aws_iam_policy_document.dev-policy1.json
}
variable "service_accounts" {
type = map(object({
name = string
namespace = string
wildcard = string
policies = list(any)
}))
}
tfvars
service_accounts = {
"dev-sa" = {
"name" = "dev-sa",
"namespace" = "dev",
"wildcard" = "*",
"policies" = ["dev-policy1", "dev-policy2"]
},
"qa-sa" = {
"name" = "qa-sa",
"namespace" = "qa",
"wildcard" = "*",
"policies" = ["qa-policy1", "qa-policy2"]
}
}
My code is iterating over service_accounts variable and creates appropriate resources. The problem is that in the commented line I cannot get the list of aws_iam_policy.arn s for the provided policy names (policy names are provided through service_account variable). My current code returns the aws_iam_policy.PolicyName.arn as string and not the actual value. Note that dev-policy1 resource s just one of the all policy resources. All policy documents exist as well. module itself is working correctly when I provide policy list directly and not through variable.
Is it possible to achieve the desired in terraform at all?
You have to use for_each, to create your policies, as you can't dynamically references individual resources the way you are trying to do:
# get all policy names. Your names are unique, so its fine to use list
locals {
policy_names = flatten(values(var.service_accounts)[*]["policies"])
}
# create policy for each name in `policy_names`
resource "aws_iam_policy" "policy" {
for_each = local.policy_names
name_prefix = "dev-policy"
description = "some description"
# similar must be done below
# policy = data.aws_iam_policy_document.dev-policy1.json
}
Then you refer to them as:
role_policy_arns = [for i in each.value.policies: aws_iam_policy[${i}].arn]

Terraform for loop inside for_each argument

Can someone please explain what a for loop inside a for_each argument does in Terraform? I am trying to create an AWS SSL certificate. I've seen other code like the below but I don't understand it:
resource "aws_acm_certificate" "nonprod_cert" {
domain_name = var.phz_domain_name
validation_method = "DNS"
}
resource "aws_route53_record" "nonprod_cert_record" {
for_each = {
for dvo in aws_acm_certificate.nonprod_cert.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
zone_id = var.phz_id
name = each.value.name
type = each.value.type
records = [each.value.record]
ttl = 60
}
resource "aws_acm_certificate_validation" "nonprod_cert_validated" {
certificate_arn = aws_acm_certificate.nonprod_cert.arn
validation_record_fqdns = [for record in aws_route53_record.nonprod_cert_record : record.fqdn]
depends_on = [
aws_acm_certificate.nonprod_cert,
aws_route53_record.nonprod_cert_record
]
}
The specific line that I don't understand is the one in the route53 record. I get that a for_each argument can be used to create multiple resources from a single block, but I can't find anywhere that explains what this for loop is doing inside of it. If someone could explain that would be great!
The inner for "loop" creates the data that the for_each then iterates over. Specifically the each.key will be the dvo.domain_name and the each.value will be the
{
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
You could simply move that into a locals block beforehand and not have it in a single line:
locals {
records = {
for dvo in aws_acm_certificate.nonprod_cert.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
}
resource "aws_route53_record" "nonprod_cert_record" {
for_each = local.records
zone_id = var.phz_id
name = each.value.name
type = each.value.type
records = [each.value.record]
ttl = 60
}
That will have the exact same effect.
First they are using a for expression to convert one type of object into another type. In this case they are converting the list of domain_validation_options into a list of objects that can be used for creating aws_route53_record resources.
Next they are using for_each to create a new aws_route53_record resource for each element of the list that was generated by the for expression.
The key things to be aware of here are:
for is used to convert a list of objects into a list of different objects.
for_each is used to create multiple resources from a list, set, or map of values.
I highly recommend spending the time to go through the Terraform learning site, or at least the Terraform documentation to learn the basic keywords and overall syntax.

Creating dynamic resources with Terraform for_each

I would like to create AWS SSM Parameters using Terraform, with the parameters being passed in as input variables.
I see there is a for_each feature, but how can this be applied to top level properties within a terraform resource? From the documentation, the use of for_each appears to be restricted to not work on top level properties of a resource, am I misunderstanding?
This is what I am trying to accomplish:
main.tf
resource "aws_ssm_parameter" "ssm_parameters" {
for_each = var.params
content {
name = name.value
type = "String"
overwrite = true
value = paramValue.value
tags = var.tags
lifecycle {
ignore_changes = [
tags,
value
]
}
}
}
variables.tf
variable "params" {
default = [
{
name = "albUrl"
paramValue = "testa"
},
{
name = "rdsUrl1"
paramValue = "testb"
},
{
name = "rdsUrl2"
valparamValueue = "testc"
},
]
}
You can use for each, but you need to modify its syntax and fix syntax in your var.params:
variable "params" {
default = [
{
name = "albUrl"
paramValue = "testa"
},
{
name = "rdsUrl1"
paramValue = "testb"
},
{
name = "rdsUrl2"
paramValue = "testc"
},
]
}
Then to use for each, and create 3 ssm parameters:
resource "aws_ssm_parameter" "ssm_parameters" {
for_each = {for v in var.params: v.name => v.paramValue}
type = "String"
name = each.key
value = each.value
overwrite = true
}
In the above you have to project your list(map) to a map as it is required for for_each.

Terraform - Conditional Data Source

In terraform, is there any way to conditionally use a data source? For example:
data "aws_ami" "application" {
most_recent = true
filter {
name = "tag:environment"
values = ["${var.environment}"]
}
owners = ["self"]
}
I'm hoping to be able to pass in an environment variable via the command line, and based on that, determine whether or not to fetch this data source.
I know with resources you can use the count property, but it doesn't seem you can use that with data sources.
I would consider tucking this code away in a module, but modules also can't use the count parameter.
Lastly, another option would be to provide a "Default" value for the data source, if it returned null, but I don't think that's doable either.
Are there any other potential solutions for this?
You can use a conditional on data sources the same as you can with resources and also from Terraform 0.13+ on modules as well:
variable "lookup_ami" {
default = true
}
data "aws_ami" "application" {
count = var.lookup_ami ? 1 : 0
most_recent = true
filter {
name = "tag:environment"
values = [var.environment]
}
owners = ["self"]
}
One use case for this in Terraform 0.12+ is to utilise the lazy evaluation of ternary statements like with the following:
variable "internal" {
default = true
}
data "aws_route53_zone" "private_zone" {
count = var.internal ? 1 : 0
name = var.domain
vpc_id = var.vpc_id
private_zone = var.internal
}
data "aws_route53_zone" "public_zone" {
count = var.internal ? 0 : 1
name = var.domain
private_zone = var.internal
}
resource "aws_route53_record" "www" {
zone_id = var.internal ? data.aws_route53_zone.private_zone.zone_id : data.aws_route53_zone.public_zone.zone_id
name = "www.${var.domain}"
type = "A"
alias {
name = aws_elb.lb.dns_name
zone_id = aws_elb.lb.zone_id
evaluate_target_health = false
}
}
This would create a record in the private zone when var.internal is true and instead create a record in the public zone when var.internal is false.
For this specific use case you could also use Terraform 0.12+'s null to rewrite this more simply:
variable "internal" {
default = true
}
data "aws_route53_zone" "zone" {
name = var.domain
vpc_id = var.internal ? var.vpc_id : null
private_zone = var.internal
}
resource "aws_route53_record" "www" {
zone_id = data.aws_route53_zone.zone.zone_id
name = "www.${data.aws_route53_zone.zone.name}"
type = "A"
alias {
name = aws_elb.lb.dns_name
zone_id = aws_elb.lb.zone_id
evaluate_target_health = false
}
}
This would only pass the vpc_id parameter to the aws_route53_zone data source if var.internal is set to true as you can't set vpc_id when private_zone is false.
Old Terraform 0.11 and earlier answer:
You can in fact use a conditional on the count of data sources but I've yet to manage to work out a good use case for it when I've tried.
As an example I successfully had this working:
data "aws_route53_zone" "private_zone" {
count = "${var.internal == "true" ? 1 : 0}"
name = "${var.domain}"
vpc_id = "${var.vpc_id}"
private_zone = "true"
}
data "aws_route53_zone" "public_zone" {
count = "${var.internal == "true" ? 0 : 1}"
name = "${var.domain}"
private_zone = "false"
}
But then had issues in how to then select the output of it because Terraform will evaluate any variables in the ternary conditional before deciding which side of the ternary to use (instead of lazy evaluation). So something like this doesn't work:
resource "aws_route53_record" "www" {
zone_id = "${var.internal ? data.aws_route53_zone.private_zone.zone_id : data.aws_route53_zone.public_zone.zone_id}"
name = "www.example.com"
type = "A"
alias {
name = "${aws_elb.lb.dns_name}"
zone_id = "${aws_elb.lb.zone_id }"
evaluate_target_health = "false"
}
}
Because if internal is true then you get the private_zone data source but not the public_zone data source and so the second half of the ternary fails to evaluate because data.aws_route53_zone.public_zone.zone_id isn't defined and equally with the other way around too.
In your case you probably just want to conditionally use the data source so might be able to do something like this:
variable "dynamic_ami" { default = "true" }
variable "default_ami" { default = "ami-123456" }
data "aws_ami" "application" {
most_recent = true
filter {
name = "tag:environment"
values = ["${var.environment}"]
}
owners = ["self"]
}
resource "aws_instance" "app" {
ami = "${var.dynamic_ami == "true" ? data.aws_ami.application.id : var.default_ami}"
instance_type = "t2.micro"
}