Terraform
Terraform
August 5, 2022

The Three Stages of Terraform's Lifecycle Meta Argument

By
Brendan Thompson

Every resource that is managed by Terraform has a lifecycle, this lifecycle contains three stages; Apply (Create), Update, and Destroy. The Apply stage is where the resource is actually created by Terraform, Update is when a change is made to a preexisting resource - this might be incremental or a full recreate - and finally Destroy where the resource is removed from the environment. Sometimes however we may want to control these stages of the resources lifecycle a little more for this Terraform gives us the lifecycle meta-argument.

The below diagram is a simple articulation of the three stages of the lifecycle at work!

Three Stages of the Terraform Lifecycle

What can we control?

As I have said above we can control - to some extent - the lifecycle further than the three above stages for our resources. Terraform gives us the following options that we can use in the lifecycle meta-argument:

  • create_before_destroy — when an in-place update has to occur Terraform will create the new instance prior to destroying the old
  • prevent_destroy — do not allow the destroy flow to actually destruct the resource
  • ignore_changes — ignore any changes on specified fields or an entire object
  • replace_triggered_by
  • precondition — check some thing before performing the action on the resource
  • postcondition — validate some thing after performing an action on the resource

We will go through most of these in a little more detail, although for the *condition you can check out this post.

Create Before Destroy

The create_before_destroy option is extremely useful in cases where the new instance of the resource must be there before destroy the old one. For example perhaps a public IP needs to be recreated but you don’t want the service to be inaccessible so you would ensure that the new address is created prior to the old one being destroyed.

Using the Scratch provider we can mock out an example of this:

resource "scratch_string" "this" {
  in = "create_before_destroy"

  lifecycle {
    create_before_destroy = true
  }
}

The above will now ensure that in the event this resource is required to be replaced in- place that it will create the new instance first.

Prevent Destroy

prevent_destroy is another bool option which we can switch on, we would use this to ensure that Terraform never destroys the particular resource. On destroy the resource would be removed from state but still exist in the real world. This is useful in scenarios where perhaps not all your resources are managed by Terraform, or you do not want anyone to accidentally delete a particular resource.

Let’s dive into an example to better understand this concept.

resource "azurerm_resource_group" "this" {
  name     = "rg-prod"
  location = "australiasoutheast"

  lifecycle {
    prevent_destroy = true
  }
}

In the above we are creating a resource group, and we have informed Terraform we want to prevent its destruction through the lifecycle meta-argument. In this scenario let’s assume that we are only managing a portion of the resources within the resource group (RG) via Terraform and other via some other mechanism. If we were to not have prevent_destroy when we eventually did a destruction those resources created out of Terraform would also be destroyed. By having prevent_destroy we are now required to be more assertive when we want to destroy the RG, we would either have to remove it manually or commit a change removing the lifecycle attribute.

I find that prevent_destroy is a favourite to security folks as it helps to add an extra level of assurance around destructive operations, especially on resource types that have such a large blast area like a resource group.

Ignore Changes

Now we come to one of the more commonly used and in my opinion the most dangerous, ignore_changes. A reason why you might want to use ignore_changes is if some outside force / process is going to be modifying your resources, an example of this might be mutation of tags or tag values via Azure policy or potentially the number of instances of a resource due to a scaling event. Both of those examples are what I would consider good reasons to utilise ignore_changes. Lets look at a very basic example:

resource "scratch_block" "this" {
  in {
    string = "Meow"
    number = 42
    bool = true
  }

  lifecycle {
    ignore_changes = [
      in
    ]
  }
}

In the above scratch_block we are ignoring any changes to the in block, and we cannot ignore a specific property on that block as the block is actually represented as a set which does not have an index or referenceable value. It is important to note that the values provided here must be static, you cannot pass in a variable or a splat. The only exception is the use of the special all keyword in place of a list which will then ignore all attributes on the resource.

ignore_changes becomes dangerous when you start ignoring entire resources as then changes you make to the code won’t alter the resource this means you’re only managing two stages of the resource Apply and Destroy all alteration would then have to be managed by an external system.

Many organisations uses tags for managing or attributing cost with cloud resources so ignore changing to particular tags or the tags property can be very valuable as it allows an external system to manage the tags on resources for you without Terraform overwriting the changes. When using ignore_changes my advice to be as specific about the property you’re wanting to ignore as you possibly can be!

Replace Triggered By

replace_triggered_by is a very new addition to language, only coming out with Terraform v1.2, it is also a very powerful argument. This will replace a particular resource based on another resource. Below is an example:

resource "scratch_bool" "this" {
  in = false
}

resource "scratch_string" "this" {
  in = "create_before_destroy"

  lifecycle {
    replace_triggered_by = [
      scratch_bool.this
    ]
  }
}

In the above example we have two resources scratch_bool.this and scratch_string.this we are tying a replace to the scratch_boo.this. What this means is that if we were to update scratch_bool.this.in to be true the entire scratch_string.this resource would be replaced!

This allows us to create really tight dependancies on resources that may not be otherwise related in our Terraform code. You should however be very careful using this as if the referenced resource changes then your resource will be replaced.

Final Thoughts

So that closes out the options we can implement to augment the standard lifecycle state within Terraform. Altering the lifecycle allows for some very powerful control over our resources but it does come with risk, when you’re making further changes to resources you have to ensure that you’ve considered what could happen based on any lifecycle modifications you’ve made.

I am very interested to see what additional things HashiCorp come out with in the lifecycle realm!

You can follow Brendan @BrendanLiamT on Twitter.

Note: While this blog references Terraform, everything mentioned in here also applies to OpenTofu. New to OpenTofu? It is a fork of Terraform 1.5.7 as a result of the license change from MPL to BUSL by HashiCorp. OpenTofu is an open-source alternative to Terraform that is governed by the Linux Foundation. All features available in Terraform 1.5.7 or earlier are also available in OpenTofu. Find out the history of OpenTofu here.

Start using the Terraform platform of the future.

A screenshot of the modules page in the Scalr Platform