Skip to main content

Manage Resource Lifecycle

·2962 words·14 mins
Terraform Professional - This article is part of a series.
Part 2: This Article

This post covers the first exam objective of the Terraform Professional certification: Manage Resource Lifecycle.

This exam objective is broken down in the following pieces:

#Exam Objective
1Manage resource lifecycle
Initialize a configuration using terraform init and its options
Generate an execution plan using terraform plan and its options
Apply configuration changes using terraform apply and its options
Destroy resources using terraform destroy and its options
Manage resource state, including importing resources and reconciling resource drift

Introduction
#

Managing the lifecycle of your resources is the whole purpose of Terraform. In its simplest form you will write your Terraform configuration, create the resources using terraform apply, and then live happily ever-after. However, in real life things are never that simple.

If you are planning to take the Terraform Professional certification I assume you are already familiar with the basics of terraform init, terraform plan, terraform apply, and terraform destroy. Hence, you can skim through these sections of this post or even skip them completely. The last part of this exam objective covers managing resource state, this part might be of interest to most readers.

Sample Terraform Configuration
#

To illustrate how to manage the resource lifecycle we need a sample Terraform configuration to work with.

Throughout this course I will work primarily with the AWS provider. This is not because I am most familiar with the AWS provider, but because initially that will be the primary cloud provider used in the actual exam. I believe the plan is to offer the exam with focus on different cloud providers in the future, but initially we are stuck with AWS - for better or for worse.

To keep it simple, while still allowing us to cover everything in this exam objective, we’ll use the following configuration:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.54.0"
    }
  }
}

provider "aws" {
  region = "eu-west-1"
}

resource "aws_security_group" "web" {
  name        = "web"
  description = "Security group for web servers"

  tags = {
    Name = "web"
  }
}

resource "aws_vpc_security_group_ingress_rule" "https" {
  security_group_id = aws_security_group.web.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 443
  to_port           = 443
  ip_protocol       = "Tcp"
}

In this configuration we create two resources, a security group with one associated security group ingress rule.

Store this configuration in main.tf.

Initialize a configuration using terraform init and its options
#

If you have ever used Terraform before (which I assume you have, because you are now reading about the professional certification) you know that the first command you run for all of your Terraform configurations is terraform init.

The terraform init command initializes your configuration by downloading providers and modules, initializes the backend where you will store your state, and creates the .terraform.lock.hcl file (unless it already exists).

Let’s run terraform init for our Terraform configuration:

$ terraform init
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "5.54.0"...
- Installing hashicorp/aws v5.54.0...
- Installed hashicorp/aws v5.54.0 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

The output informs us of what Terraform is doing. Once we have run terraform init we see a new directory named .terraform in our current directory. The contents of this directory is this:

.terraform
└── providers
    └── registry.terraform.io
        └── hashicorp
            └── aws
                └── 5.54.0
                    └── darwin_arm64
                        ├── LICENSE.txt
                        └── terraform-provider-aws_v5.54.0_x5

In the providers directory the required providers are downloaded with the correct architecture for the current system (I am using a mac, i.e. darwin_arm64). In this case we only have the aws provider. If our configuration would have included modules these would have been available under the .terraform directory as well.

If you run terraform init -h you will see all the options that are available for this command. There are a few of these options that might appear on the exam, some of which will be covered in later posts. I want to highlight one option that is important to know: the -upgrade flag.

Currently we have the following terraform block with the required_providers block inside:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.54.0"
    }
  }
}

When we initially ran terraform init, version 5.54.0 of the aws provider was downloaded. Let’s say you want to upgrade the provider version:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.56.0"
    }
  }
}

Just rerunning terraform init will fail:

$ terraform init
Initializing the backend...
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
│ Error: Failed to query available provider packages
│ Could not retrieve the list of available versions for provider hashicorp/aws: locked provider
│ registry.terraform.io/hashicorp/aws 5.54.0 does not match configured version constraint 5.56.0; must use
│ terraform init -upgrade to allow selection of new versions

This is because you have a .terraform.lock.hcl file where the version is set to be 5.54.0. The lock file currently looks like this:

# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

provider "registry.terraform.io/hashicorp/aws" {
  version     = "5.54.0"
  constraints = "5.54.0"
  hashes = [
    "h1:kK9b6AtAdfsZ5dQx/C4/bCL7Nx9RzWQlqDKZXRmqIxg=",
    # ... shortened for brevity
    "zh:fbcc3fcd8a4e49763d60c677e1b1b4acc922d8a0f59ae67e38a78345d706ffdd",
  ]
}

To reconcile this error, add the -upgrade flag to the terraform init command:

$ terraform init -upgrade
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "5.56.0"...
- Installing hashicorp/aws v5.56.0...
- Installed hashicorp/aws v5.56.0 (signed by HashiCorp)
Terraform has made some changes to the provider dependency selections recorded
in the .terraform.lock.hcl file. Review those changes and commit them to your
version control system if they represent changes you intended to make.

Terraform has been successfully initialized!

Is there a -downgrade flag as well? No. It turns out that downgrading to an older version of a provider is also considered to be an upgrade, so you follow the same procedure.

You should also make sure you know how version constraints for providers work. In the sample Terraform configuration we are working with the provider version has been pinned to a specific version. I consider this to be a best practice, but if you want to loosen the constraint a bit you could allow the patch version of the provider to change. You do this with the following version constraint:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.56.0"
    }
  }
}

If we now rerun the terraform init command with the -upgrade flag we see that the patch version changes:

$ terraform init -upgrade
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.56.0"...
- Installing hashicorp/aws v5.56.1...
- Installed hashicorp/aws v5.56.1 (signed by HashiCorp)
Terraform has made some changes to the provider dependency selections recorded
in the .terraform.lock.hcl file. Review those changes and commit them to your
version control system if they represent changes you intended to make.

Terraform has been successfully initialized!

We now have version 5.56.1 of the aws provider.

Generate an execution plan using terraform plan and its options
#

If you are a heavy user of both Terraform and Azure Bicep, you might appreciate the terraform plan command more than others. This is because the corresponding command in Azure Bicep (the what-if operation) leaves you wanting a lot more.

A Terraform plan is a record of changes that would be applied if the current configuration is used in a terraform apply command. There are a number of options for this command that is useful, and might not be something you use every day - so I will go through some of them below.

The basics of terraform plan is to create a plan file, and sometimes convert it to JSON if you need to do something with it (e.g. apply your policy as code to the upcoming changes), and finally apply the plan if it looks OK:

$ terraform plan -out=actions.tfplan
$ terraform show -json actions.tfplan > actions.json
$ terraform apply actions.tfplan

The name of the plan file can be whatever you like. The plan file itself is a binary file, meaning you can’t directly open it to inspect its content. This is why we need to convert it to JSON to be able to see what it contains.

Sometimes it can be useful to plan a single resource, or a few resources that have dependencies between them. You can do this using the -target flag together with the resource address of the resource you want to create a plan for. To do this for our aws_security_group resource we run the following command:

$ terraform plan -target=aws_security_group.web

The output will tell you that using this -target flag is only for exceptional situations, e.g. recovering from errors or mistakes.

Apply configuration changes using terraform apply and its options
#

Once you have a plan file you can tell Terraform to perform the actions that this plan contains with the command:

$ terraform apply actions.tfplan

If you do not have a plan file you can still issue the terraform apply command and Terraform will run a plan operation for you and then ask you to confirm you want to apply the changes. There are situations where you want to just apply the changes without confirming them, this is done by adding the -auto-approve flag:

$ terraform apply -auto-approve

This flag is not recommended for production usage for obvious reasons, but perhaps you will use it in the certification exam just to shave off a few seconds of time!

Something to be aware of for the certification test and real-life is what happens if the plan file you are using is stale? A stale plan file means that something has changed in the infrastructure since the plan file was created. This could be if someone manually edited something outside of Terraform.

To see what this looks like first apply the example Terraform configuration:

$ terraform apply -auto-approve

Next, edit the inbound rule in the Terraform configuration to the following:

resource "aws_vpc_security_group_ingress_rule" "https" {
  security_group_id = aws_security_group.web.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 1
  to_port           = 1024
  ip_protocol       = "Tcp"
}

Note that this change is not something we would actually want to do for our security group, but this is for illustrative purposes only. Now generate a plan file for these changes:

$ terraform plan -out=actions.tfplan

The output will tell us that the ingress rule will be updated in-place, meaning that Terraform expects this ingress rule exists. Before we apply these changes, go to the AWS console and remove the ingress rule from the security group that this Terraform configuration created. Now apply the plan:

$ terraform apply actions.tfplan
aws_vpc_security_group_ingress_rule.https: Modifying... [id=sgr-0775c71f4eff70b37]
│ Error: updating VPC Security Group Rule (sgr-0775c71f4eff70b37)
│   with aws_vpc_security_group_ingress_rule.https,
│   on main.tf line 23, in resource "aws_vpc_security_group_ingress_rule" "https":
│   23: resource "aws_vpc_security_group_ingress_rule" "https" {
│ InvalidSecurityGroupRuleId.NotFound: The security group rule ID 'sgr-0775c71f4eff70b37' does not exist
│ 	status code: 400, request id: b488889c-d13a-461a-97ed-70081058a3de

The error message clearly indicates what is wrong in this case. However, that might not always be the case. Be aware that if you would be given a plan file you have not generated yourself these types of errors could occur. Try to minimize the time between creating a plan file and applying the changes.

Destroy resources using terraform destroy and its options
#

To remove the infrastructure Terraform has created we use terraform destroy. This command is really just an alias for terraform apply -destroy. We could also generate a destroy plan using terraform plan -destroy -out=actions.tfplan that we can then use with terraform apply. Note that in this case we don’t run terraform apply -destroy or terraform destroy, we just run a regular terraform apply actions.tfplan.

Manage resource state, including importing resources and reconciling resource drift
#

Imagine we want to refactor our infrastructure and move our resources into a module so that we can more easily reuse it elsewhere. This is a common operation and includes manipulating the state file. There are two approaches to manipulating the state file:

  • an imperative approach using terraform state CLI commands
  • a declarative approach using HCL blocks (import, moved, etc)

A best practice is to use the declarative approach, so that is what we will do here.

We begin from our original Terraform configuration:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.54.0"
    }
  }
}

provider "aws" {
  region = "eu-west-1"
}

resource "aws_security_group" "web" {
  name        = "web"
  description = "Security group for web servers"

  tags = {
    Name = "web"
  }
}

resource "aws_vpc_security_group_ingress_rule" "https" {
  security_group_id = aws_security_group.web.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 443
  to_port           = 443
  ip_protocol       = "Tcp"
}

The first step is to create the module we want to use. One way to do this is to copy the relevant resources into a new directory modules/security-group. For simplicity we use a single file named main.tf:

// modules/security-group/main.tf
resource "aws_security_group" "web" {
  name        = "web"
  description = "Security group for web servers"

  tags = {
    Name = "web"
  }
}

resource "aws_vpc_security_group_ingress_rule" "https" {
  security_group_id = aws_security_group.web.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 443
  to_port           = 443
  ip_protocol       = "Tcp"
}

To make this a great module we should also parametrize it using variables, but let us skip that step for now because the focus is on migrating state.

In our root main.tf we remove the corresponding resources we just moved to the module and we include a new module block:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.54.0"
    }
  }
}

provider "aws" {
  region = "eu-west-1"
}

module "security_group" {
  source = "./modules/security-group"
}

Are we done? Not yet, if we try to apply this configuration we will run into a lot of problems. Terraform will try to delete the resources we had, and at the same time create the module. There will most likely be a conflict between these operations. We need to tell Terraform how our state has changed. We can do this using the moved block, one block for each resource that we moved:

moved {
  from = aws_security_group.web
  to   = module.security_group.aws_security_group.web
}

moved {
  from = aws_vpc_security_group_ingress_rule.https
  to   = module.security_group.aws_vpc_security_group_ingress_rule.https
}

If we now run a terraform plan we see the following:

$ terraform plan
module.security_group.aws_security_group.web: Refreshing state... [id=sg-0c82a39a040399493]
module.security_group.aws_vpc_security_group_ingress_rule.https: Refreshing state... [id=sgr-019bb6e3b7a65318b]

Terraform will perform the following actions:

  # aws_security_group.web has moved to module.security_group.aws_security_group.web
    resource "aws_security_group" "web" {
        id                     = "sg-0c82a39a040399493"
        name                   = "web"
        tags                   = {
            "Name" = "web"
        }
        # (9 unchanged attributes hidden)
    }

  # aws_vpc_security_group_ingress_rule.https has moved to module.security_group.aws_vpc_security_group_ingress_rule.https
    resource "aws_vpc_security_group_ingress_rule" "https" {
        id                     = "sgr-019bb6e3b7a65318b"
        # (8 unchanged attributes hidden)
    }

Plan: 0 to add, 0 to change, 0 to destroy.

The plan summary says 0 to add, 0 to change, and 0 to destroy. We see that Terraform knows the resources have been moved and will thus only update the addresses for these resources in the state file.

Another common operation you might encounter during the certification exam is the need to import an existing resource into the Terraform state. You can do this with the import block.

To test this out first add a new rule to the security group in the AWS console. In this example I have added an ingress rule for port 22 from anywhere. Next I define the same resource in my Terraform configuration (note that I undid the module refactoring steps from before, starting out from the original configuration instead):

resource "aws_vpc_security_group_ingress_rule" "ssh" {
  security_group_id = aws_security_group.web.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 22
  to_port           = 22
  ip_protocol       = "Tcp"
}

Next we add the import block. Note that you must know the security group rule ID of the rule you want to import. This ID has the form sgr-0286259b52ab2e729. You can find the ID in the AWS console where you created the rule. The import block looks like this:

import {
  id = "sgr-0286259b52ab2e729"
  to = aws_vpc_security_group_ingress_rule.ssh
}

We specify the to address in our Terraform configuration, pointing at the resource block we added previously. If we now run a terraform plan we see the following:

$ terraform plan
aws_security_group.web: Refreshing state... [id=sg-0c82a39a040399493]
aws_vpc_security_group_ingress_rule.ssh: Preparing import... [id=sgr-0286259b52ab2e729]
aws_vpc_security_group_ingress_rule.ssh: Refreshing state... [id=sgr-0286259b52ab2e729]
aws_vpc_security_group_ingress_rule.https: Refreshing state... [id=sgr-019bb6e3b7a65318b]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated
with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # aws_vpc_security_group_ingress_rule.ssh will be updated in-place
  # (imported from "sgr-0286259b52ab2e729")
  ~ resource "aws_vpc_security_group_ingress_rule" "ssh" {
        arn                    = "arn:aws:ec2:eu-west-1:<account-id>:security-group-rule/sgr-0286259b52ab2e729"
        cidr_ipv4              = "0.0.0.0/0"
        from_port              = 22
        id                     = "sgr-0286259b52ab2e729"
      ~ ip_protocol            = "tcp" -> "Tcp"
        security_group_id      = "sg-0c82a39a040399493"
        security_group_rule_id = "sgr-0286259b52ab2e729"
        tags_all               = {}
        to_port                = 22
    }

Plan: 1 to import, 0 to add, 1 to change, 0 to destroy.

Note the plan summary saying that 1 resource will be imported.

These were two examples of state manipulations that you might encounter in real-life and during the certification exam. Before the exam, also familiarize yourself with the removed block which is required if you want to remove a resource from your state file.

Summary
#

This post covered the Manage resource lifecycle exam objective. Most of what this exam objective is about should be well known to you when you set out on the professional certification. This includes the terraform init/plan/apply commands. These are basic commands you should have used a lot already.

The last part of this exam objective is more interesting, it covers manipulating your state file and showed you a few example of how to do this in a declarative way.

In the next post we will cover the Develop and troubleshoot dynamic configuration exam objective.

Mattias Fjellström
Author
Mattias Fjellström
Cloud architect consultant and an HashiCorp Ambassador
Terraform Professional - This article is part of a series.
Part 2: This Article