Reputation: 1709
I have the following local list:
locals {
default_iam_policies = [
"arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy",
"arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore",
"arn:aws:iam::aws:policy/AWSDeviceFarmFullAccess"
]
}
I am planning to attach these policies to a role along with a custom policy using this:
resource "aws_iam_role_policy_attachment" "default-policy-attachment" {
for_each = toset(concat(
local.default_iam_policies,
[aws_iam_policy.custom-policy.arn]
))
role = aws_iam_role.this.name
policy_arn = each.value
}
but I'm getting this error message:
│ Error: Invalid for_each argument
│
│ on main.tf line 113, in resource "aws_iam_role_policy_attachment" "default-policy-attachment":
│ 113: for_each = toset(concat(
│ 114: local.default_iam_policies,
│ 115: [aws_iam_policy.custom-policy.arn]
│ 116: ))
│ ├────────────────
│ │ aws_iam_policy.custom-policy.arn is a string, known only after apply
│ │ local.default_iam_policies is tuple with 3 elements
│
│ The "for_each" 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 for_each depends on.
I think I can split this into two aws_iam_role_policy_attachment
blocks but I wanted to see if it's possible to just use one.
Upvotes: 2
Views: 1669
Reputation: 74064
Using for_each
in a situation like this involves making a distinction between the labels you give to these objects within your Terraform configuration and the labels/ids that the remote system assigns to them, because the remote system's identifiers can change over time and are typically not known until after apply, but Terraform needs these keys to stay consistent throughout the lifecycle of these instances, including during the plan phase for originally creating them.
Making that concrete for this case, that means that each of these policy ARNs ought to have a logical name that's decided directly inside the Terraform configuration, which is separate from the ARN that the AWS API decides for you.
The local.default_iam_policy
values are defined statically in the configuration, and so technically this distinction isn't required for those, but since we'll be combining that with ARNs generated dynamically it'll be easier to apply the same convention to both, and so I'll start by adjusting the local value to be a map where the keys will be our local identifiers for those policies:
locals {
default_iam_policies = tomap({
CloudWatchAgentServerPolicy = "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy",
AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore",
AWSDeviceFarmFullAccess = "arn:aws:iam::aws:policy/AWSDeviceFarmFullAccess"
})
}
Since your original example suggests that there isn't really any special nuance to capture here in the split between local and remote IDs, I just kept it simple here and established a convention that the local name is the name
of the policy, which is something we can then derive systematically from the "custom" ones too:
resource "aws_iam_role_policy_attachment" "default-policy-attachment" {
for_each = merge(
local.default_iam_policies,
{ for p in aws_iam_policy.custom-policy.arn : p.name => p.arn },
)
role = aws_iam_role.this.name
policy_arn = each.value
}
The above is assuming that your resource "aws_iam_policy" "custom-policy"
block assigns static name
values to each of the policies, so that those will be known by Terraform at planning time. That's typically true, but if you are deriving your set of "custom policies" from some other dynamic source then it is possible in principle for the name
values to also be "known after apply", in which case we'd need a different strategy to assign a static key to each instance.
The upshot of all of this is that your resource "aws_iam_role_policy_attachment" "default-policy-attachment"
block will declare instances with keys named after the policy names:
aws_iam_role_policy_attachment.default-policy-attachment["CloudWatchAgentServerPolicy"]
aws_iam_role_policy_attachment.default-policy-attachment["AmazonSSMManagedInstanceCore"]
aws_iam_role_policy_attachment.default-policy-attachment["AWSDeviceFarmFullAccess"]
Upvotes: 1
Reputation: 238081
As already explained, you can't use for_each
. But in your case you can use count
:
locals {
default_iam_policies = [
"arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy",
"arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore",
"arn:aws:iam::aws:policy/AWSDeviceFarmFullAccess"
]
full_list = concat(local.default_iam_policies,[aws_iam_policy.this.arn])
}
resource "aws_iam_role_policy_attachment" "default-policy-attachment" {
count = length(local.full_list)
role = aws_iam_role.this.name
policy_arn = local.full_list[count.index]
}
Upvotes: 4
Reputation: 3144
You've hit a limitation of Terraform:
Limitations on values used in for_each
The keys of the map (or all the values in the case of a set of strings) must be known values, or you will get an error message that for_each has dependencies that cannot be determined before apply, and a -target may be needed.
Expressions have the same limitation:
The for_each meta-argument accepts map or set expressions. However, unlike most arguments, the for_each value must be known before Terraform performs any remote resource actions. This means for_each can't refer to any resource attributes that aren't known until after a configuration is applied (such as a unique ID generated by the remote API when an object is created).
Upvotes: 1