Will the terraform fail if the data does not exist? - amazon-web-services

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.

Related

Make Terraform ignore the order of list items returned from the service

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

Creating a StringLike condition with Terraform

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

List of Active Directory DNS servers IP addresses in an SSM document

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.

Want to assign multiple Google cloud IAM roles to a service account via terraform

I want to assign multiple IAM roles to a single service account through terraform. I prepared a TF file to do that, but it has an error. With a single role it can be successfully assigned but with multiple IAM roles, it gave an error.
data "google_iam_policy" "auth1" {
binding {
role = "roles/cloudsql.admin"
members = [
"serviceAccount:${google_service_account.service_account_1.email}",
]
role = "roles/secretmanager.secretAccessor"
members = [
"serviceAccount:${google_service_account.service_account_1.email}",
]
role = "roles/datastore.owner"
members = [
"serviceAccount:${google_service_account.service_account_1.email}",
]
role = "roles/storage.admin"
members = [
"serviceAccount:${google_service_account.service_account_1.email}",
]
}
}
How can I assign multiple roles against a single service account?
I did something like this
resource "google_project_iam_member" "member-role" {
for_each = toset([
"roles/cloudsql.admin",
"roles/secretmanager.secretAccessor",
"roles/datastore.owner",
"roles/storage.admin",
])
role = each.key
member = "serviceAccount:${google_service_account.service_account_1.email}"
project = my_project_id
}
According with the documentation
Each document configuration must have one or more binding blocks, which each accept the following arguments: ....
You have to repeat the binding, like this
data "google_iam_policy" "auth1" {
binding {
role = "roles/cloudsql.admin"
members = [
"serviceAccount:${google_service_account.service_account_1.email}",
]
}
binding {
role = "roles/secretmanager.secretAccessor"
members = [
"serviceAccount:${google_service_account.service_account_1.email}",
]
}
binding {
role = "roles/datastore.owner"
members = [
"serviceAccount:${google_service_account.service_account_1.email}",
]
}
binding {
role = "roles/storage.admin"
members = [
"serviceAccount:${google_service_account.service_account_1.email}",
]
}
}
It's the same thing with you use the gcloud command, you can add only 1 role at the time on a list of email.
I can't comment or upvote yet so here's another answer, but #intotecho is right.
I'd say do not create a policy with Terraform unless you really know what you're doing! In GCP, there's only one policy allowed per project. If you apply that policy, only the service accounts will have access, no humans. :) Even though we don't want humans to do human things, it's helpful to at least have view access to the GCP project you own.
Especccciallyy if you use the model that there are multiple Terraform workspaces performing iam operations on the project. If you use policies it will be similar to how wine is made, it will be a stomping party! The most recently applied policy will win (if the service account TF is using is included in that policy, otherwise it will lock itself out!)
It's possible humans get an inherited viewer role from a folder or the org itself, but assigning multiple roles using the google_project_iam_member is a much much better way and how 95% of the permissions are done with TF in GCP.

Terraform random_string as password for linux machine

I want to use this to generate a random password for the linux machines in gcp.
My question is how I get the password afterwards.
Should I use the output for this or is it stored anywhere else?
I saw this code on the internet and was asking myself how they know the password then.
resource "random_string" "master_password" {
length = 16
special = true
}
resource "google_container_cluster" "test" {
name = "test"
zone = "europe-west1-d"
master_auth {
username = "client"
password = "${random_string.master_password.result}"
}
node_pool = [{
name = "pool"
autoscaling = {
min_node_count = 1
max_node_count = 3
}
node_config {
disk_size_gb = 100
machine_type = "n1-standard-2"
oauth_scopes = [
"https://www.googleapis.com/auth/compute",
"https://www.googleapis.com/auth/devstorage.read_only",
"https://www.googleapis.com/auth/logging.write",
"https://www.googleapis.com/auth/monitoring",
]
labels {
test = "true"
}
}
}]
}
The password will be stored in your state file. You can go digging around in there for it, but it is entirely possible that it's exact location in the file will change between Terraform versions.
The best way to get a consistent output is to, as you mentioned, use an output block. Then when you do a terraform apply there will be a nice human readable output of this password. Please note that anything with access to your state has access to that password, so keep the state secure.
If you use remote state (such as in an S3 bucket), you can also use terraform_remote_state to gain access to this from another Terraform run. You will need to explicitly output values you want to be available from terraform_remote_state.
Finally, be aware that if something captures the output of your terraform apply, it will also capture that output since terraform apply writes to STDOUT. This might happen if you use a CI tool. Just something to be aware of.