I am trying to generate some terraform for an aws IAM policy. The condition in the policy looks like this
"StringLike": {
"kms:EncryptionContext:aws:cloudtrail:arn": [
"arn:aws:cloudtrail:*:aws-account-id:trail/*"
]
I am looking at the documentation for aws_iam_policy_document: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document, but it's not clear to me as to how to write this in terraform. Any help would be greatly apprecaited. This is my attempt
condition {
test = "StringLike"
variable = "kms:EncryptionContext:aws:cloudtrail:arn"
values = [
"arn:aws:cloudtrail:*:aws-account-id:trail/*"
]
}
Hello Evan you logic is correct just to add :
Each document configuration may have one or more statement
data "aws_iam_policy_document" "example" {
statement {
actions = [
"*", *//specify your actions here*
]
resources = [
"*", *//specify your resources here*
]
condition {
test = "StringLike"
variable = "kms:EncryptionContext:aws:cloudtrail:arn"
values = [
"arn:aws:cloudtrail:*:aws-account-id:trail/*"
]
}
}
Each policy statement may have zero or more condition blocks, which each accept the following arguments:
test (Required) The name of the IAM condition operator to evaluate.
variable (Required) The name of a Context Variable to apply the condition to. Context variables may either be standard AWS variables starting with aws:, or service-specific variables prefixed with the service name.
values (Required) The values to evaluate the condition against. If multiple values are provided, the condition matches if at least one of them applies. (That is, the tests are combined with the "OR" boolean operation.)
When multiple condition blocks are provided, they must all evaluate to true for the policy statement to apply. (In other words, the conditions are combined with the "AND" boolean operation.)
Here's the REF from terraform
IN Addition to create the policy from the document you created you use it like this:
resource "aws_iam_policy" "example" {
policy = data.aws_iam_policy_document.example.json
}
Here's A ref from Hashicorp
Related
I want to create multiple aws_iam_policy_document resources with for_each, to be later assumed by several roles, as follows:
# Policy to allow services to assume the role
data "aws_iam_policy_document" "this" {
for_each = var.lambda_configuration
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = [
"lambda.amazonaws.com",
"apigateway.amazonaws.com",
]
}
}
}
# IAM role for executing the Lambda function
resource "aws_iam_role" "this" {
for_each = var.lambda_configuration
name = "my_lambda_${each.key}_Executor_Role"
description = "Role for executing my_lambda-${each.key} function"
assume_role_policy = data.aws_iam_policy_document.assume_role_policy_[each.key].json
}
How should I interpolate this
assume_role_policy = data.aws_iam_policy_document.assume_role_policy_[each.key].json
to make the correct matching with the roles?
The correct syntax:
data.aws_iam_policy_document.this[each.key].json
Note that this is not interpolation, as you mentioned in your question. It is just a value lookup. And I have no idea where you got assume_role_policy_ from, but that is not valid HCL syntax.
This seems like a good situation for Chaining for_each Between Resources, which (subjectively) tends to lead to a configuration where it's easier for an unfamiliar reader to follow the way data is flowing between the resources.
data "aws_iam_policy_document" "this" {
for_each = var.lambda_configuration
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = [
"lambda.amazonaws.com",
"apigateway.amazonaws.com",
]
}
}
}
# IAM role for executing the Lambda function
resource "aws_iam_role" "this" {
for_each = data.aws_iam_policy_document.this
name = "my_lambda_${each.key}_Executor_Role"
description = "Role for executing my_lambda-${each.key} function"
assume_role_policy = each.value.json
}
Using the first resource itself as the for_each for the second one means that the two will always have exactly the same keys (because a resource with for_each appears in expressions as a map of objects using the keys from the for_each map), and that each.value inside the second block refers to the corresponding instance of the first block.
This has essentially the same behavior as using the index operator to select an element from data.aws_iam_policy_document.this using each.key, but makes the relationship between these two resources a little more direct and so (again, subjectively) easier to read and maintain in future.
I have a iam_policy_document resource with a condition block. In terraform condition blocks have a required argument named values. This is a set of possible values for the condition. However, in my case, the variable is a boolean. So the set is just one value, ["false"].
When I run terraform apply and then later run either terraform plan or terraform apply, terraform always says it will update this value from "false" to ["false"]. Is there a way to get it to stop updating this policy when nothing has actually changed?
Reference: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#values
I am using v0.15.1 on AWS.
Here is output of terraform plan when no terraform code has changed and no manual manipulation of the policy on AWS has happened. Notice it is updating the policy.
Terraform will perform the following actions:
# module.s3_test_bucket.aws_s3_bucket.main will be updated in-place
~ resource "aws_s3_bucket" "main" {
id = "test-for-stackoverflow"
~ policy = jsonencode(
~ {
~ Statement = [
~ {
~ Condition = {
~ Bool = {
~ aws:SecureTransport = "false" -> [
+ "false",
]
}
}
[...]
you type aws:SecureTransport = array of 1, AWS update it to be a string so you have diff between deployed configuration and terrafrom configuration file.
change the value to string
aws:SecureTransport = ["false"]
to
aws:SecureTransport = "false"
And it should solve it.
I'm creating an AWS IAM policy that grants access to a resource to a number of remote accounts. I've got these accounts in a list - all good. However when TF checks the current state on the subsequent plan it comes back in a different order and TF thinks that it must be corrected. How can I ignore the list order?
This is my resource:
resource "aws_ecr_repository_policy" "repo" {
policy = jsonencode({
Statement = [
{
Principal = {
AWS = [
"arn:aws:iam::123456789012:root",
"arn:aws:iam::567890123456:root",
"arn:aws:iam::987654321098:root",
]
...
Now on subsequent terraform plan runs I get some variations of this:
~ {
~ Principal = {
~ AWS = [
+ "arn:aws:iam::987654321098:root", <<< swapped order
"arn:aws:iam::123456789012:root",
"arn:aws:iam::567890123456:root",
- "arn:aws:iam::987654321098:root", <<< and here
]
}
AWS is unpredictable with the order it returns, it changes each time. Can I somehow ignore the order? Ideally without ignoring the whole policy block with lifecycle / ignore_changes.
This was an issue of terraform-provider-aws (fixed with provider version v4.23.0), see
https://github.com/hashicorp/terraform-provider-aws/issues/22274
Will the terraform fail if a user in the data does not exist?
I need to specify a user in the nonproduction environment by the data block:
data "aws_iam_user" "labUser" {
user_name = "gitlab_user"
}
Then I use this user in giving the user permissions:
resource "aws_iam_role" "ApiAccessRole_abc" {
name = "${var.stack}-ApiAccessRole_abc"
tags = "${var.tags}"
assume_role_policy = <<EOF
{
"Version": "2019-11-29",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"AWS": [
"${aws_iam_user.labUser.arn}"
]
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}
In the production environment this user does not exist. Would the terraform break if this user does not exist? What would be a good approach to use the same terraform in both environments?
In Terraform a data block like you showed here is both a mechanism to fetch data and also an assertion by the author (you) that a particular external object is expected to exist in order for this configuration to be applyable.
In your case then, the answer is to ensure that the assertion that the object exists only appears in situations where it should exist. The "big picture" answer to this is to review the Module Composition guide and consider whether this part of your module ought to be decomposed into a separate module if it isn't always a part of the module it's embedded in, but I'll also show a smaller solution that uses conditional expressions to get the behavior you wanted without any refactoring:
variable "lab_user" {
type = string
default = null
}
data "aws_iam_user" "lab_user" {
count = length(var.lab_user[*])
user_name = var.lab_user
}
resource "aws_iam_role" "api_access_role_abc" {
count = length(data.aws_iam_user.lab_user)
name = "${var.stack}-ApiAccessRole_abc"
tags = var.tags
assume_role_policy = jsonencode({
Version = "2019-11-29"
Statement = [
{
Sid = ""
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
AWS = [data.aws_iam_user.lab_user[count.index].arn]
}
},
]
})
}
There's a few different things in the above that I want to draw attention to:
I made the lab username an optional variable rather than a hard-coded value. You can than change the behavior between your environments by assigning a different value to that lab_user variable, or leaving it unset altogether for environments that don't need a "lab user".
In the data "aws_iam_user" I set count to length(var.lab_user[*]). The [*] operator here is asking Terraform to translate the possibly-null string variable var.lab_user into a list of either zero or one elements, and then using the length of that list to decide how many aws_iam_user queries to make. If var.lab_user is null then the length will be zero and so no queries will be made.
Finally, I set the count for the aws_iam_role resource to match the length of the aws_iam_user data result, so that in any situation where there's one user expected there will also be one role created.
If you reflect on the Module Composition guide and conclude that this lab user ought to be a separate concern in a separate module then you'd be able to remove this conditional complexity from the "gitlab user" module itself and instead have the calling module either call that module or not depending on whether such a user is needed for that environment. The effect would be the same, but the decision would be happening in a different part of the configuration and thus it would achieve a different separation of concerns. Which separation of concerns is most appropriate for your system is, in the end, a tradeoff you'll need to make for yourself based on your knowledge of the system and how you expect it might evolve in future.
As suggested in the comments it will fail.
One approach that I can suggest is to supply the username as a var that you pass externally from a file dev.tfvars and prod.tfvars and run terraform with:
terraform apply --var-file example.tfvars
Then in your data resource you can have a count or for_each to check whether the var has been populated or not (if var has not been passed, you can skip the data interpolation)
count = var.enable_gitlab_user ? 1 : 0
The AWS direct approach would be to switch from IAM user in the Principal to tag-based Condition or even Role chaining. You can take a look at this AWS blog post for some ideas. There are examples for both cases.
I am converting my 0.11 code to 0.12. Most things seem to be working out well, but I am really lost on the SSM document.
In my 0.11 code, I had this code:
resource "aws_ssm_document" "ssm_document" {
name = "ssm_document_${terraform.workspace}${var.addomainsuffix}"
document_type = "Command"
content = <<DOC
{
"schemaVersion": "1.0",
"description": "Automatic Domain Join Configuration",
"runtimeConfig": {
"aws:domainJoin": {
"properties": {
"directoryId": "${aws_directory_service_directory.microsoftad-lab.id}",
"directoryName": "${aws_directory_service_directory.microsoftad-lab.name}",
"dnsIpAddresses": [
"${aws_directory_service_directory.microsoftad-lab.dns_ip_addresses[0]}",
"${aws_directory_service_directory.microsoftad-lab.dns_ip_addresses[1]}"
]
}
}
}
}
DOC
depends_on = ["aws_directory_service_directory.microsoftad-lab"]
}
This worked reasonably well. However, Terraform 0.12 does not accept this code, saying
This value does not have any indices.
I have been trying to look up different solutions on the web, but I am encountering countless issues with datatypes. For example, one of the solutions I have seen proposes this:
"dnsIpAddresses": [
"${sort(aws_directory_service_directory.oit-microsoftad-lab.dns_ip_addresses)[0]}",
"${sort(aws_directory_service_directory.oit-microsoftad-lab.dns_ip_addresses)[1]}",
]
}
and I am getting
InvalidDocumentContent: JSON not well-formed
which is kinda weird to me, since if I am looking into trace log, I seem to be getting relatively correct values:
{"Content":"{\n \"schemaVersion\": \"1.0\",\n \"description\": \"Automatic Domain Join Configuration\",\n \"runtimeConfig\": {\n \"aws:domainJoin\": {\n \"properties\": {\n \"directoryId\": \"d-9967245377\",\n \"directoryName\": \"012mig.lab\",\n \"dnsIpAddresses\": [\n \"10.0.0.227\",\n
\"10.0.7.103\",\n ]\n }\n }\n }\n}\n \n","DocumentFormat":"JSON","DocumentType":"Command","Name":"ssm_document_012mig.lab"}
I have tried concat and list to put the values together, but then I am getting the datatype errors. Right now, it looks like I am going around in loops here.
Does anyone have any direction to give me here?
Terraform 0.12 has stricter types than 0.11 and less automatic type coercion going on under the covers so here you're running into the fact that the output of the aws_directory_service_directory resource's dns_ip_addresses attribute isn't a list but a set:
"dns_ip_addresses": {
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
Computed: true,
},
Set's can't be indexed directly and instead must first be converted to a list explicitly in 0.12.
As an example:
variable "example_list" {
type = list(string)
default = [
"foo",
"bar",
]
}
output "list_first_element" {
value = var.example_list[0]
}
Running terraform apply on this will output the following:
Outputs:
list_first_element = foo
However if we use a set variable instead:
variable "example_set" {
type = set(string)
default = [
"foo",
"bar",
]
}
output "set_first_element" {
value = var.example_set[0]
}
Then attempting to run terraform apply will throw the following error:
Error: Invalid index
on main.tf line 22, in output "set_foo":
22: value = var.example_set[0]
This value does not have any indices.
If we convert the set variable into a list with tolist first then it works:
variable "example_set" {
type = set(string)
default = [
"foo",
"bar",
]
}
output "set_first_element" {
value = tolist(var.example_set)[0]
}
Outputs:
set_first_element = bar
Note that sets may have different ordering to what you may expect (in this case it is ordered alphabetically rather than as declared). In your case this isn't an issue but it's worth thinking about when indexing an expecting the elements to be in the order you declared them.
Another possible option here, instead of building the JSON output from the set or list of outputs, you could just directly encode the dns_ip_addresses attribute as JSON with the jsonencode function:
variable "example_set" {
type = set(string)
default = [
"foo",
"bar",
]
}
output "set_first_element" {
value = jsonencode(var.example_set)
}
Which outputs the following after running terraform apply:
Outputs:
set_first_element = ["bar","foo"]
So for your specific example we would want to do something like this:
resource "aws_ssm_document" "ssm_document" {
name = "ssm_document_${terraform.workspace}${var.addomainsuffix}"
document_type = "Command"
content = <<DOC
{
"schemaVersion": "1.0",
"description": "Automatic Domain Join Configuration",
"runtimeConfig": {
"aws:domainJoin": {
"properties": {
"directoryId": "${aws_directory_service_directory.microsoftad-lab.id}",
"directoryName": "${aws_directory_service_directory.microsoftad-lab.name}",
"dnsIpAddresses": ${jsonencode(aws_directory_service_directory.microsoftad-lab.dns_ip_addresses)}
}
}
}
}
DOC
}
Note that I also removed the unnecessary depends_on. If a resource has interpolation in from another resource then Terraform will automatically understand that the interpolated resource needs to be created before the one referencing it.
The resource dependencies documentation goes into this in more detail:
Most resource dependencies are handled automatically. Terraform
analyses any expressions within a resource block to find references to
other objects, and treats those references as implicit ordering
requirements when creating, updating, or destroying resources. Since
most resources with behavioral dependencies on other resources also
refer to those resources' data, it's usually not necessary to manually
specify dependencies between resources.
However, some dependencies cannot be recognized implicitly in
configuration. For example, if Terraform must manage access control
policies and take actions that require those policies to be present,
there is a hidden dependency between the access policy and a resource
whose creation depends on it. In these rare cases, the depends_on
meta-argument can explicitly specify a dependency.