hashicorp / terraform

Terraform enables you to safely and predictably create, change, and improve infrastructure. It is a source-available tool that codifies APIs into declarative configuration files that can be shared amongst team members, treated as code, edited, reviewed, and versioned.
https://www.terraform.io
Other
43.15k stars 9.58k forks source link

Allow referring to a `check` resource to use as a feature flag #34250

Open scalp42 opened 1 year ago

scalp42 commented 1 year ago

Terraform Version

Terraform v1.6.3
on darwin_arm64

Use Cases

data sources return an error when checking for something that doesn't exist.

With the new check resource added, it'd be great if we could use it to wrap data source that could fail and use the result of the check as a boolean for count attribute for example.

In the case of an AWS IAM service role, you may end up in a situation where the role might already exist (AWS create the roles "for you" when accessing/enabling certain services through the UI) or not (never use the service).

Writing Terraform config for this use case becomes painful afaik and solutions are limited:

Attempted Solutions

Use a Terraform null_resource resource with local-exec:

#!/bin/bash

ROLE_NAME="AWSServiceRoleForAutoScaling"
OUTPUT_FILE="role_existence.txt"

if aws iam get-role --role-name $ROLE_NAME 2>/dev/null; then
  echo "true" > $OUTPUT_FILE
else
  echo "false" > $OUTPUT_FILE
fi
resource "null_resource" "check_role_existence" {
  provisioner "local-exec" {
    command = "path/to/script/above.sh"
    interpreter = ["bash", "-c"]
  }

  triggers = {
    always_run = "${timestamp()}"
  }
}
data "local_file" "role_existence" {
  depends_on = [null_resource.check_role_existence]
  filename   = "${path.module}/role_existence.txt"
}
resource "aws_iam_service_linked_role" "autoscaling" {
  count = data.local_file.role_existence.content == "true" ? 0 : 1

  aws_service_name = "autoscaling.amazonaws.com"
  description      = "Default Service-Linked Role enables access to AWS Services and Resources used or managed by Auto Scaling."
}
  1. Import the IAM role resource so that Terraform ignores it?

Proposal

It'd be amazing if we could simply just refer to the check resource with some kind of "return" parameter to dictate the behavior:

resource "aws_iam_service_linked_role" "AWSServiceRoleForAutoScaling" {
  count = check.role_AWSServiceRoleForAutoScaling_already_exists ? 0 : 1
  aws_service_name = "autoscaling.amazonaws.com"
  description      = "Default Service-Linked Role enables access to AWS Services and Resources used or managed by Auto Scaling."
}

check "role_AWSServiceRoleForAutoScaling_already_exists" {
  data "aws_iam_role" "AWSServiceRoleForAutoScaling" {
    name = "AWSServiceRoleForAutoScaling"
  }

  assert {
    condition = data.aws_iam_role.AWSServiceRoleForAutoScaling.name == "AWSServiceRoleForAutoScaling"
    error_message = "${data.aws_iam_role.AWSServiceRoleForAutoScaling.name} already exist."
    return = "true"
  }
}

References

scalp42 commented 1 year ago

@4n3w saved the ~day~ night:

locals {
  role_exists = can(data.aws_iam_role.AWSServiceRoleForAutoScaling.name)
}

data "aws_iam_role" "AWSServiceRoleForAutoScaling" {
  name = "AWSServiceRoleForAutoScaling"
}

resource "aws_iam_service_linked_role" "AWSServiceRoleForAutoScaling" {
  count = local.role_exists ? 0 : 1
  aws_service_name = "autoscaling.amazonaws.com"
  description      = "Default Service-Linked Role enables access to AWS Services and Resources used or managed by Auto Scaling."
}

It's not 100% the same behavior but probably can close as can/try help with that AFAIK. If a maintainer can confirm that this is the way to go, I'll take it 🙇

apparentlymart commented 1 year ago

Hi @scalp42,

The primary purpose of check blocks -- what distinguishes them from preconditions and postconditions -- is that they never block forward progress and instead just act as an additional signal about whether the system is functioning as intended.

That means that in practice nothing can refer to a check block, because by definition checks must always be evaluated only after everything else has been dealt with. In a sense, a check block implicitly depends on everything else in the configuration and so if something else refers to it then that would produce a dependency cycle.

Given that, I don't think the specific solution you've proposed is viable, but I'd like to learn more about your use-case regardless of the specific proposed solution, because we might be able to find another way to get there using another language feature.


However, it seems like your goal might be "create this if it doesn't already exist", in which case that is intentionally not allowed because Terraform is a desired state system and so you must tell it what it is supposed to be managing or else its behavior would be unpredictable.

For example, if we were to implement exactly what you proposed (the dependency ordering issue notwithstanding) then on the first run Terraform would find that the object doesn't exist yet and therefore see your declaration that it should exist, but then on the next run Terraform would find that the object already exists and therefore conclude that you want zero instances of the resource. Terraform would see that you currently have one instance of that resource, and so would propose to destroy it to converge with the new desired state. Then the third run would propose to create it again, and so on.

Instead, your configuration should state whether the object is already expected to exist or not, and should therefore fail if that expectation isn't met.

scalp42 commented 1 year ago

@apparentlymart thanks for the detailed answer.

Unfortunately, can won't work here because the data source triggers first. I just went with the import resource for now (which doesn't support interpolation unfortunately).

While I understand the core philosophy of Terraform, I sometimes wish some of the choices could be left to the user. That being said, a lot of progress has been made (like can, import etc) and reflecting back on the earlier years of Terraform, a lot of flexibility was added so I understand. I just wish we could have a more of it at times.

Thanks again to you @apparentlymart and a special mention to @ewbankkit! Your contributions over the years are truly valued and appreciated ❤️

We should probably consider closing this issue.

dgard1981 commented 2 months ago

@scalp42, you could use the aws_iam_roles (plural) data source, then build your aws_iam_role (singular) data source from that, and then use a couple of local variables to check if your IAM Role should be managed.

This solution isn't ideal though

variable "role_name" {
  description = "The name of the role to create."
  type        = string
  default     = "my-role"
}

variable "role_managed_by" {
  description = "The entitiy that manages the role."
  type        = string
  default     = "my-terraform"
}

data "aws_iam_roles" "my_role_only" {
  name_regex = "^${var.role_name}$"
}

data "aws_iam_role" "my_role" {
    for_each = data.aws_iam_roles.my_role_only.names
    name = each.value
}

locals {
  role_exists  = length(data.aws_iam_roles.my_role_only.names) == 1
  role_has_tag = lookup(lookup(lookup(data.aws_iam_role.my_role, var.role_name, {}), "tags", {}), "ManagedBy", "NOT_FOUND") == var.role_managed_by
}

resource "aws_iam_role" "my_role" {
  count = (!local.role_exists) || (local.role_exists && local.role_has_tag) ? 1 : 0

  name = var.role_name

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })

  tags = {
    ManagedBy = var.role_managed_by
  }
}