Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resource Instance Random ID Generator #32593

Open
tgoodsell-tempus opened this issue Jan 27, 2023 · 6 comments
Open

Resource Instance Random ID Generator #32593

tgoodsell-tempus opened this issue Jan 27, 2023 · 6 comments
Labels
enhancement functions new new issue not yet triaged

Comments

@tgoodsell-tempus
Copy link

tgoodsell-tempus commented Jan 27, 2023

Terraform Version

Terraform v1.3.7

Use Cases

Main Use Case:

  • Providing an instance bound random string/int used in unique/primary key naming/id fields of resources. For example a google_compute_disk.
  • This should work such that resources created/replaced (Create), particularly when create_before_destroy is set, this value changes / is rotated. However on normal plan/apply operations (Read/Update), this value does not change. Should be disposed on a delete.

Attempted Solutions

  1. A module with one of the random_xxx resources, including adding a keepers value on specific inputs variables we expect have ForceNewIf set on them. This has the draw backs of:

    • The user must use a variable or child module to use this.
    • The user must know all the variables which would trigger a ForceNew on the resource
    • It does not have room for nuance, for the compute_disk example: A disk can increase in size normally, decreasing in size requires a recreate. Therefore, a keepers on the "size" field would not work; as any change in the "name" field would trigger an undesired recreate.
    • It does not work when one of the "vars" we're passing into the resource is NOT the recreate trigger. For example an outside change such as an associated resource where we've intentionally or indirectly (only known by the API) not accounted for it changing in the keepers.
    • Without a keepers, requires the full resource name/path to change, which would produce undesired changes in more complex multi-resource scenarios.
  2. Using a random_xxx resource, with it having the resource we want this behavior for placed in the replace_triggered_by lifecycle argument:

    • This isn't actually possible due to the cycle issue this causes
    • Since this lifecycle is triggered on any update, this would produce undesired recreates similar to the situations listed above.
  3. Resource specific support. Some resources, such as aws_iam_policy and google_compute_instance_template, provide schema specific solutions to this problem. By either allowing for full auto-generation of these fields, or offering a "prefix" name where it autogenerates a remainder of the "true" name/ID passed to the API. It's drawbacks are:

    • Requires the providers to implement this themselves
    • Not customizable for your specific desired conventions

Proposal

  1. A new resource instance meta field which produces an instance specific random ID for use. For example:
resource "google_compute_disk" "example" {
    name = "example-name-${self.rand_id}"
    description = "I'm referencing an internal random id: ${self.rand_id}"
    size = 100
}

For controlling what the self.rand_id would emit, I'm thinking either:

  • At the root module or child module configuration level (the terraform block), there is a new setting struct which lets you set the properties of this. Ideally based on one or more of the random_xxx resource configurations, such as:
terraform {
    rand_config {
        type = string
        length = 3
        uppercase = false
        lowercase = true
        number = true
        special = false
    }
}
  • In a new meta block in the resource you want this value produced for, a similar struct where you can set the properties of this. Such as:
resource "google_compute_disk" "example" {
     name = "example-${self.rand_id}"
     
     random_id {
         type = string
         length = 3
         uppercase = false
         lowercase = true
         number = true
         special = false
     }
}
  1. Some sort of terraform-provider-sdk / terraform-provider-framework enforced mechanism that resource schema where ForceNew is used, also requires that resources have or produce any accompanying "randomizer" for the main primary key fields. Either requiring that these key fields are tagged and tested as supporting this behavior, or instead of building this into terraform, the providers include an option to configure a "random" element similar to the above, which is available for reference on resources. For example:
resource "google_compute_disk" "example" {
    rand_id = rand_string(3, false, true, true, false)
    name = "example-${self.rand_id}"
}

References

#31707

@tgoodsell-tempus tgoodsell-tempus added enhancement new new issue not yet triaged labels Jan 27, 2023
@tgoodsell-tempus tgoodsell-tempus changed the title Resource Instance Random Data Generator Resource Instance Random ID Generator Jan 27, 2023
@apparentlymart
Copy link
Contributor

Thanks for sharing this use-case, @tgoodsell-tempus!

This reminds me of an idea I posted a long time ago (before I was a member of the Terraform team at HashiCorp), which I mention only because there's some discussion there which might be interesting background context as we think about different ways to solve this problem.

In that other issue I proposed using the prefix meta. for this sort of metadata about objects, because that then avoids conflicting with existing references to "non-meta data" (the actual values returned by the providers). So for example: meta.self.unique_id means the unique ID metadata for the current object, and meta.aws_instance.example.unique_id might mean the unique ID metadata for some other resource instance, assuming we wanted to allow cross-references like that. (I'm not sure if it's actually required or desireable.)

I also like your idea of using function syntax for this, because it makes it a bit clearer how we'd specify different arguments to customize the ID if needed. However, I think we'd need to be careful about how flexible we make that because otherwise I think it would raise questions about how much you're allowed to change the settings without forcing Terraform to generate a new ID.

I was imagining keeping the ID for each resource would be stored in the Terraform state and then Terraform would change it only when replacing an object, but that approach suggests that there would only be one possible ID for each object and all modified versions would need to be somehow derived from it. One way to achieve that would be for Terraform to generate some arbitrary random bytes as the main ID and then have all of the possible ID variations be deterministically derived from those random bytes, essentially using the random data as a seed for predictably generating data in other formats.

If Terraform can also see the rules for the derived ID at planning time then it could verify that the new value is different than the old one (pseudo-random generation could collide when generating short strings) and preemptively generate a new set of random bytes to avoid the collision if so.

@tgoodsell-tempus
Copy link
Author

tgoodsell-tempus commented Jan 30, 2023

@apparentlymart Thanks for the comments! Yes I think your more fleshed out idea is probably the best way to implement. For example here is a sample of the google_compute_disk state which spawned my request:

{
      "module": "module.stateful_instance",
      "mode": "managed",
      "type": "google_compute_disk",
      "name": "stateful_disks",
      "provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
      "instances": [
        {
          "index_key": "stateful-instance-wn-st-1",
          "schema_version": 0,
          "attributes": {
            "creation_timestamp": "2023-01-25T16:48:21.435-08:00",
            "description": "Stateful disk for instance stateful-instance",
            "disk_encryption_key": [],
            "id": "projects/example/zones/us-central1-b/disks/stateful-instance-wn-st-1",
            "image": "",
            "label_fingerprint": "sample",
            "labels": {}
            "last_attach_timestamp": "2023-01-26T12:30:11.498-08:00",
            "last_detach_timestamp": "2023-01-26T12:30:10.687-08:00",
            "name": "stateful-instance-wn-st-1",
            "physical_block_size_bytes": 4096,
            "project": "example",
            "provisioned_iops": 0,
            "self_link": "https://www.googleapis.com/compute/v1/projects/example/zones/us-central1-b/disks/stateful-instance-wn-st-1",
            "size": 1000,
            "snapshot": "",
            "source_disk": "",
            "source_disk_id": "",
            "source_image_encryption_key": [],
            "source_image_id": "",
            "source_snapshot_encryption_key": [],
            "source_snapshot_id": "",
            "timeouts": null,
            "type": "pd-balanced",
            "users": [
              "https://www.googleapis.com/compute/v1/projects/example/zones/us-central1-b/instances/stateful-instance"
            ],
            "zone": "us-central1-b"
          },
          "sensitive_attributes": [],
          "private": "senitive",
          "dependencies": [list of things]
        }
      ]
    },

If this added another field similar to schema_version or index_key which lives in the instance object but is only accessible by terraform (not directly modifiable by the provider); named seed / id / etc, which acted as the seed value for a new type of function we could use in the terraform configuration.

Ideally this value would be "generated" when the plan detected a Create/Recreate so uses of it could be visible at plan time.

Then the new functions may not even need to require the user to reference it directly, maybe a function such as:
rand_string(3, true, true, false, false) where rand_string(length, lower, upper, number, special); which automatically assumes that as the seed value so it produces a consistent result against it each time.

@apparentlymart
Copy link
Contributor

Thanks for confirming, @tgoodsell-tempus!

One interesting thing for us to consider here is that we've not previously had any function whose behavior varies depending on which block it's being called from, and so we'd probably want to evaluate whether folks would find that intuitive or confusing before deciding on that particular syntax.

If it does seem like it would be confusing then a possible variation would be to expose something like meta.self.unique_id as I'd orignially proposed and have that be the original random seed, and then offer one or more other functions which can take a random seed value and return a derived value. That way the part whose value varies depending on context would hopefully be more obvious (since it refers to meta.self) but we can avoid the need to somehow expose all of the different possible variations as special language features.


It's interesting to note that this "derive an ID from some random data" function (or functions) is already what some of the resource types in the hashicorp/random provider do, via their optional seed arguments that allow forcing a particular seed.

I suppose that actually does make a bunch of sense, because the hashicorp/random provider's resource types are currently serving as the random number generator, the transformation function for deriving the final result from the random number, and the persistent storage for the random results. Under this proposal Terraform Core would take over the random number generator duties and the persistent storage duties, and so that leaves only the transformation functions to be handled elsewhere somehow.

If we implement the ability for provider plugins to contribute new functions to Terraform (#2771) then the existing hashicorp/random provider could potentially offer function-shaped equivalents of the transformation functions it already supports and thus we could keep all that functionality together in once place so it's easier to understand the relationships between these parts.

// INVALID: This is a hypothetical example of one way to address this feature request

resource "google_compute_disk" "example" {
  name = "example-name-${provider::random::string(meta.self_unique_id, { length = 5 })}"
  size = 100
}

(The provider::random::string part of the above is one of the syntaxes we've considered for allowing provider-contributed functions while avoiding namespace conflicts between providers and with the built-in functions.)

@tgoodsell-tempus
Copy link
Author

tgoodsell-tempus commented Jan 30, 2023

@apparentlymart Cheers! Yes reading your commentary I agree that is may be better to just say that said value will be exposed under a meta. block (or whatever makes the most sense to your team) and needs to be explicitly called. I think that does increase the extensibility of that functionality longer term (in case there are other meta values you want to expose for use in the future).

Additionally your idea to have these types of functions also be wrapped into the work of allow providers to provider terraform functions makes a lot of sense considering:

  1. Decouples the function logic from Terraform Core to allow better maintainability
  2. Allows providers to add their own custom logic, using this as an example: A provider may release their own function where the random id generator better conforms to their own requirements regarding these IDs/fields.

Certainly both of these features are something I would love to see integrated into the tool whenever feasible. The ID thing especially would help an org like mine with a large number of objects with a complex dependency set have a good tool to ensure our terraform config is clean/clear on its use as well as ensure we don't have to do any funny business with plans / applies where these unique name resources are used.

@TechIsCool
Copy link

I spent a few hours today reading through the old threads from @apparentlymart before finding this thread.

I wanted to add some more examples where uniqueness needs to be generated before the DAG is complete.

We currently work around these by using static uniqueness with substr(sha256("octogon-db-dev"),0,8) to generate 8af1ade5. But we still run into problems where this only works when a single resource exists and they aren't colliding on the key octogon-db-dev in this example.

  • When you want to create multiple buckets with the same name for code readability but each one is random. For example a terraform-states-23af5301 but when run in a different folder it should get a unique name terraform-states-735aef78

  • When creating an Aurora Cluster, each instance of the cluster should be uniquely named. But as @tgoodsell-tempus stated you need to keep track of every ForceNew value. We accomplish this by stringing together variables from the module

substr(sha256(format("%s%s%s%s%s%s%s",
  var.master_username != null ? var.master_username : "null",
  local.cluster_identifier,
  var.database_name != null ? var.database_name : "null",
  var.engine_mode,
  var.engine,
  var.db_subnet_group_name,
  k
)), 0, 8)
  • When Restoring an Aurora Cluster, you might expect that adding a snapshot_identifier to the new module would be all that was needed but users manually have to update the identifier_name so they don't collide.

  • AWS Auto Scaling Groups take more than a few minutes to destroy and as such should have some type of uniqueness. AWS under the hood doesn't handle this well and throws a cryptic 400 for you to puzzle about because the resource still exists on the backend while its being destroyed.

These are the ones that I have recently encountered. My end goal is improving usability for our Terraform users while abstracting away meaningless hoop jumping if possible.

(The provider::random::string part of the above is one of the syntaxes we've considered for allowing provider-contributed functions while avoiding namespace conflicts between providers and with the built-in functions.)

I am super excited that it would be possible to access providers directly for helper functions.

@nomeelnoj
Copy link

+1. We have run into this a lot, and implemented a sort of workaround/hack. Our setup uses 1000s of very tiny terraform states (it speeds things up and keeps things separate), but that also means that we have a LOT of dependencies between states and use mostly data blocks to refer to objects between states (the dependency chain this creates is not ideal, but it is what it is).

Across multiple teams we have services that have similar or same names, as each team is implementing their part of a shared service, or simply they both refer to their workload with the same name. Since we want to keep terraform as self-service as possible and still allow the flexibility for devs to name things whatever they like within our naming conventions ("${local.team}-${local.name}-${local.env}"), we found a solution using a combination of Terraform functions to generate reproducible unique IDs that we can easily call in a data block elsewhere without having to know anything about the actual random characters.

module "rds_aurora" {
  cluster_identifier = "${local.team}-${local.name}-${local.env}-${substr(sha256("${local.team}-${local.name}-${local.env}"), 0. 8)}"
}

The issue with the above is that our org kept having re-orgs, and systems would change ownership, and then the local.team name was no longer correct, and could not easily be changed due to field immutability in AWS. We thought about just standardizing on GUIDs for all resource names and using tags for identification, but that detracted from the user experience in the console and no one could find their resources.

The solution was to drop the local.team and just use "${local.name}-${local.env}-${substr(sha256("${local.name}-${local.env}"), 0. 8)}. However, if two teams have a same-named system in the same environment, we run into naming conflicts yet again.

Having something that is truly random and yet still repeatable in some way and also built into the framework would make this considerably easier for us.

@crw crw added the functions label Feb 22, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement functions new new issue not yet triaged
Projects
None yet
Development

No branches or pull requests

5 participants