Is it possible to extend terraform object's (resource) property in other terraform file?

Hi. First time here. I am kind of a newb in terraform and I want some pieces of advice here. I wonder is it possible to extend terraform object’s property in other terraform file? I want to create a module who can update object’s property without overwriting original terraform file where object is defined before, but in other .tf file creation.

Scenario A:
I want to have possibility in b.tf file extend already defined object from a.tf file

files:
a.tf
b.tf

in a.tf:

resource "google_storage_bucket" "auto-expire" {
  name          = "auto-expiring-bucket"
  location      = "US"
  force_destroy = true

  lifecycle_rule {
    condition {
      age = 3
    }
    action {
      type = "Delete"
    }
  }
}

in b.tf:

//This is not actual code. just an idea:
extend resource “google_storage_bucket” “auto-expire” {
uniform_bucket_level_access = true
}

Scenario B:

The same as Scenario A, but additional check if property already exists in a.tf:

example:

in a.tf:

//same code as in Scenario A a.tf

in b.tf:

//this is not actual code. just an idea

if
lifecycle_rule.condition.age not exists
then
lifecycle_rule {
condition {
age = 100
}
else
pass (use property from a.tf)

Sorry for messy pseudo code. I do not know how to format text properly.

Terraform doesn’t have a feature that can do Scenario B.

Scenario A could be implemented using Override Files - Configuration Language | Terraform by HashiCorp, which handles the very simplest case of unconditionally overriding an attribute, but it lacks flexibility if you need to override nested blocks.

1 Like

Hi @monkelis0,

Terraform’s model is that each resource block entirely manages the object it’s describing. There is no mechanism to override one module’s object from outside of that module.

However, since your a.tf and b.tf files seem to just be two files in the same module, and therefore considered by Terraform as a single unit, you could in principle use Override Files as @maxb suggested. I would recommend treating that as a last resort because Override Files are a historical feature we don’t typically recommend for new designs; it tends to make configuration hard to understand for future maintainers, and the merging behavior is complicated and somewhat arbitrary. We preserved that behavior in v0.12 for compatibility with prior versions but do not intend to invest in it further in future Terraform versions.

A more Terraform-idiomatic way to address the situation you described would be to factor out the parts you want to set elsewhere into either input variables (if you want to set them from outside the module) or local values (if you just need to set them elsewhere in the same module).


Here’s an example with local values.

First, an updated version of your a.tf file:

resource "google_storage_bucket" "auto_expire" {
  name          = "auto-expiring-bucket"
  location      = "US"
  force_destroy = true

  uniform_bucket_level_access = local.uniform_bucket_level_access

  lifecycle_rule {
    condition {
      age = local.lifecycle_age
    }
    action {
      type = "Delete"
    }
  }
}

This includes references to local.uniform_bucket_level_access and local.lifecycle_age, which you can then declare and define in your b.tf:

locals {
  uniform_bucket_level_access = true
  lifecycle_age               = 100
}

The readability/maintainability benefits of this strategy compared to override files are:

  • Each value is defined in only one place, rather than there being multiple definitions and requiring the reader to read carefully to understand which one takes effect.
  • A reader looking at resource "google_storage_bucket" "auto_expire" can clearly see the references to local.uniform_bucket_level_access and local.lifecycle_age and know that they should look elsewhere in the module to find those definitions if they need to update those values, rather than trying to update the inline declarations and being confused that the change doesn’t take effect (because it’s shadowed by an override file).

Here’s an equivalent example with input variables, if the actual overridden value should be provided from outside of the module rather than inside it:

variable "uniform_bucket_level_access" {
  type    = bool

  # this makes the variable optional to set
  # without providing a default
  default = null
}

variable "lifecycle_age" {
  type    = number

  # this makes the variable optional to set
  # and uses is default value when it isn't
  default  = 3
  nullable = false
}

resource "google_storage_bucket" "auto-expire" {
  name          = "auto-expiring-bucket"
  location      = "US"
  force_destroy = true

  # NOTE: Setting a resource argument to null is exactly
  # equivalent to not setting it at all, so this will behave
  # as if this argument were not specified at all if
  # var.uniform_bucket_level_access was not set.
  # The provider itself will choose the default behavior
  # in that case.
  uniform_bucket_level_access = var.uniform_bucket_level_access

  lifecycle_rule {
    condition {
      age = var.lifecycle_age
    }
    action {
      type = "Delete"
    }
  }
}

Similar advantages over override files here too, but the additional capability here compared to the local values approach is that if this is a shared reusable module then each call to the module can potentially set a different value for each of the variables, allowing you to use the same module in different ways for different situations.

1 Like

Hi, I just want to elaborate a little bit more what we (me and monkelis0) would like to achieve

We are trying to create a gitlab/terraform module that could be used by different users.

So let’s say a user has already a project with multiple terraform files, and imports our module which does something to his described resources.

i.e user has described a postgreSQL instance in his .tf, but our module tries to add some flags to this instance. So when plan is executed each time it looks at user .tf file and sees that only some flags described there (but our module added additional) so it tries to remove those additional, and then our module adds them again, because of that we get sql instace restart each deployment.

Similar stuff happens with buckets and other resources if we want to add lifecycle rules or some other options, because it is described in two places (user git project and our module).

If override is not so good to use, maybe you have any other suggestions how to extend existing resource so it is not treated twice and doesn’t make confusion during planning and deployment stage?

Ah… what you describe is not possible in Terraform - @apparentlymart’s reply started off with the key information:

You must move the full definition of the resources either to the user project, or to your module.

If you define the resources in your module, you might also define input variables that allow the user to customise specific values, as in the last code sample of @apparentlymart’s reply - but you can only customise values to the extend your module explicitly defines variables for the user of the module to specify.

Yes, this is what I would’ve replied too! :grinning:

One extra thing I would note is that if the specific part that your module needs to “override” could be thought of as a separate object in the remote system, rather than just as a part of some other object, you could share your use-case with the provider developer and see if they would consider offering a separate resource type just for that part. It would be the maintainer’s decision about whether that makes sense in practice, but I mention it just because there have been various examples of breaking “too big” resource types into smaller parts before in other providers.

If the provider or remote system considers this to be just different properties of a single object though, my previous statement still stands: you will need to make an architectural decision about whether that object belongs to the calling module or to your module, and then design around that decision accordingly using input values and output values so that whichever module will actually declare the object will have access to the data it needs to do so.

My previous examples both assumed it would be the child module that would manage the object. There is a third case for if the caller will manage the object but needs some data from the child module to do so: have the child module export the needed data as output values, so that the caller can refer to those from inside the resource block. Of course, this does invert the dependency order so may require other changes to your design if you take this path.