2 Life cycle of a Terraform resource

This chapter covers

  • Generating and applying execution plans
  • Analyzing when Terraform triggers function hooks
  • Using the Local provider to create and manage files
  • Simulating, detecting, and correcting for configuration drift
  • Understanding the basics of Terraform state management

When you do away with all the bells and whistles, Terraform is a surprisingly simple technology. Fundamentally, Terraform is a state management tool that performs CRUD operations (create, read, update, delete) on managed resources. Often, managed resources are cloud-based resources, but they don’t have to be. Anything that can be represented as CRUD can be managed as a Terraform resource.

In this chapter, we deep-dive into the internals of Terraform by walking through the life cycle of a single resource. We can use any resource for this task, so let’s use a resource that doesn’t call any remote network APIs. These special resources are called local-only resources and exist within the confines of Terraform or the machine running Terraform. Local-only resources typically serve marginal purposes, such as to glue “real” infrastructure together, but they also make a great teaching aid. Examples of local-only resources include resources for creating private keys, self-signed TLS certificates, and random ids.

2.1 Process overview

We will use the local_file resource from the Local provider for Terraform to create, read, update, and delete a text file containing the first few passages of Sun Tzu’s The Art of War. Our high-level architecture diagram is shown in figure 2.1.

CH02_F01_Winkler

Figure 2.1 Inputs and outputs of the Sun Tzu scenario

Note Although a text file isn’t normally considered infrastructure, you can still deploy it the same way you would an EC2 instance. Does that mean that it’s real infrastructure? Does the distinction even matter? I’ll leave it for you to decide.

First, we’ll create the resource. Next, we’ll simulate configuration drift and perform an update. Finally, we’ll clean up with terraform destroy. The procedure is shown in figure 2.2.

CH02_F02_Winkler

Figure 2.2 (1) We create the resource, then (2) read and (3) update it, and finally (4) delete it.

2.1.1 Life cycle function hooks

All Terraform resources implement the resource schema interface. The resource schema mandates, among other things, that resources define CRUD functions hooks, one each for Create(), Read(), Update(), and Delete(). Terraform invokes these hooks when certain conditions are met. Generally speaking, Create() is called during resource creation, Read() during plan generation, Update() during resource updates, and Delete() during deletes. There’s a bit more to it than that, but you get the idea.

Because it’s a resource, local_file also implements the resource schema interface. That means it defines function hooks for Create(), Read(), Update(), and Delete(). This is in contrast to the local_file data source, which only implements Read() (see figure 2.3). In this scenario, I will point out when and why each of these function hooks is called.

CH02_F03_Winkler

Figure 2.3 The two resources in the Local provider are a managed resource and an unmanaged data source. The managed resource implements full CRUD, while the data source only implements Read().

2.2 Declaring a local file resource

Let’s get started by creating a new workspace for Terraform. Do this by creating a new empty directory somewhere on your computer. Make sure the folder doesn’t contain any existing configuration code, because Terraform concatenates all .tf files together. In this workspace, make a new file called main.tf and add the following code.

Listing 2.1 main.tf

terraform {                                                                
  required_version = ">= 0.15"
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.0"
    }
  }
}
 
resource "local_file" "literature" {
    filename = "art_of_war.txt"
<<-EOT                                                   
      Sun Tzu said: The art of war is of vital importance to the State.
 
      It is a matter of life and death, a road either to safety or to 
      ruin. Hence it is a subject of inquiry which can on no account be
      neglected.
    EOT
}

Terraform settings blocks

Heredoc syntax for multi-line strings

Tip The <<- sequence indicates an indented heredoc string. Anything between the opening identifier and the closing identifier (EOT) is interpreted literally. Leading whitespace, however, is ignored (unlike traditional heredoc syntax).

There are two configuration blocks in listing 2.1. The first block, terraform {...}, is a special configuration block responsible for configuring Terraform. Its primary use is version-locking your code, but it can also configure where your state file is stored and where providers are downloaded (we discuss this more in chapter 6). As a reminder, the Local provider has not yet been installed. To do that, we first need to perform terraform init.

The second configuration block is a resource block that declares a local_file resource. It provisions a text file with a given filename and content value. In this scenario, the content will contain the first couple stanzas of Sun Tzu’s masterpiece, The Art of War, and the filename will be art_of_war.txt. We will use heredoc syntax (<<-) to input a multiline string literal.

2.3 Initializing the workspace

At this point, Terraform isn’t aware of your workspace, let alone that it’s supposed to create or manage anything, because it hasn’t been initialized. Terraform configuration must always be initialized at least once, but you may have to initialize again if you add new providers or modules. Don’t fret about when to run terraform init, because Terraform will always remind you. Moreover, terraform init is an idempotent command, which means you can call it as many times as you want in a row with no side effects.

Run terraform init now:

$ terraform init
 
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/local versions matching "~> 2.0"...
- Installing hashicorp/local v2.0.0...
- Installed hashicorp/local v2.0.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!
 
You may now begin working with Terraform. Try running "terraform plan" to 
see any changes that are required for your infrastructure. All Terraform 
commands should now work.
 
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, 
other commands will detect it and remind you to do so if necessary.

After initialization, Terraform creates a hidden .terraform directory for installing plugins and modules. The directory structure for the current Terraform workspace is the following:

.
├── .terraform
    └── providers
        └── registry.terraform.io
            └── hashicorp
                └── local
                    └── 2.0.0
                        └── darwin_amd64
                            └── terraform-provider-local_v2.0.0_x5
├── .terraform.lock.hcl
└── main.tf
 
7 directories, 3 files

Because we declared a local_file resource in main.tf, Terraform is smart enough to realize that there is an implicit dependency on the Local provider. So Terraform looks up the resource and downloads it from the provider registry. You don’t have to declare an empty provider block (i.e. provider "local" {}) unless you want to.

TIP Version lock any providers you use, whether they are implicitly or explicitly defined, to ensure that any deployment you make is repeatable.

2.4 Generating an execution plan

Before we create the local_file resource with terraform apply, we can preview what Terraform intends to do by running terraform plan. You should always run terraform plan before deploying. I often skip this step in the book for the sake of brevity, but you should still do it, even if I do not call it out. terraform plan informs you about what Terraform intends to do and acts as a linter, letting you know about any syntax or dependency errors. It’s a read-only action that does not alter the state of deployed infrastructure, and like terraform init, it’s idempotent.

Generate an execution plan now by running terraform plan:

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
 
__________________________________________________________________________
 
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create
 
Terraform will perform the following actions:
 
  # local_file.literature will be created
  + resource "local_file" "literature" {
      + content              = <<~EOT
            Sun Tzu said: The art of war is of vital importance to the State.
 
            It is a matter of life and death, a road either to safety or to
            ruin. Hence it is a subject of inquiry which can on no account be
            neglected.
        EOT
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "art_of_war.txt"
      + id                   = (known after apply)        
}
 
Plan: 1 to add, 0 to change, 0 to destroy.
__________________________________________________________________________
 
Note: You didn't specify an "-out" parameter to save this plan, so 
Terraform can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

Computed meta-attribute

When might my plan fail?

Terraform plans can fail for many reasons, such as if your configuration code is invalid or if there’s a versioning issue or network-related problems. Sometimes, albeit rarely, a plan fails due to a bug in the provider’s source code. You need to carefully read whatever error message you receive to know for sure. For more verbose logs, you can turn on trace-level logging by setting the environment variable TF_LOG =trace to a non-zero value, e.g. export TF_LOG=trace.

As you can see from the output, Terraform is letting us know that it wants to create a local_file resource. Besides the attributes that we supply, it also wants to set a computed attribute called id, which is a meta-attribute that Terraform sets on all resources. It’s used to uniquely identify real-world resources and for internal calculations.

Although this particular terraform plan should have exited quickly, some plans take a while to complete. It all has to do with how many resources you are deploying and how many resources you already have in your state file.

Tip If terraform plan is running slowly, turn off trace-level logging and consider increasing parallelism (-parallelism=n).

Although the output of the plan is fairly straightforward, a lot is going on that you should be aware of. The three main stages of a terraform plan are as follows:

  1. Read the configuration and state. Terraform reads your configuration and state files (if they exist).

  2. Determine actions to take. Terraform performs a calculation to determine what needs to be done to achieve the desired state. This can be one of Create(), Read(), Update(), Delete(), or No-op.

  3. Output the plan. An execution plan ensures that actions occur in the right order to avoid dependency problems. This is more relevant when you have lots of resources.

Figure 2.4 is a detailed flow diagram showing what happens during terraform plan.

CH02_F04_Winkler

Figure 2.4 Steps that Terraform performs when generating an execution plan for a new deployment

We haven’t yet talked about the dependency graph, but it’s a big part of Terraform, and every terraform plan generates one for respecting implicit and explicit dependencies between resource and provider nodes. Terraform has a special command for visualizing the dependency graph: terraform graph. This command outputs a dotfile that can be converted to a digraph using a variety of tools. Figure 2.5 shows the produced DOT graph.

CH02_F05_Winkler

Figure 2.5 Dependency graph for this workspace

Note DOT is a graph description language. DOT graphs are files with the filename extension .dot. Various programs can process and render DOT files in graphical form.

The dependency graph for this workspace has a few nodes, including one for the Local provider, one for the local_file resource, and a few other meta nodes that correspond to housekeeping actions. During an apply, Terraform walks the dependency graph to ensure that everything is done in the correct order. We examine a more complex digraph in the next chapter.

2.4.1 Inspecting the plan

It’s possible to read the output of terraform plan in JSON format, which can be useful when integrating with custom tools or enforcing policy as code (we discuss policy as code in chapter 13).

First, save the output of the plan by setting the optional -out flag:

$ terraform plan -out plan.out
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
 
__________________________________________________________________________
 
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create
 
Terraform will perform the following actions:
 
  # local_file.literature will be created
  + resource "local_file" "literature" {
      + content              = <<~EOT
            Sun Tzu said: The art of war is of vital importance to the State.
 
            It is a matter of life and death, a road either to safety or to
            ruin. Hence it is a subject of inquiry which can on no account be
            neglected.
        EOT
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "art_of_war.txt"
      + id                   = (known after apply)
}
 
Plan: 1 to add, 0 to change, 0 to destroy.
__________________________________________________________________________
 
This plan was saved to: plan.out
 
To perform exactly these actions, run the following command to apply:
terraform apply "plan.out"

plan.out is now saved as a binary file, so the next step is to convert it to JSON format. This can be done (rather unintuitively) by ingesting it with terraform show and piping it to an output file:

$ terraform show -json plan.out > plan.json

Finally, we have the plan in human-readable format:

$ cat plan.json
{"format_version":"0.1","terraform_version":"0.15.0","planned_values":{"roo
t_module":{"resources":[{"address":"local_file.literature","mode":"managed"
,"type":"local_file","name":"literature","provider_name":"registry.terrafor
m.io/hashicorp/local","schema_version":0,"values":{"content":"Sun Tzu said: 
The art of war is of vital importance to the State.

It is a matter of 
life and death, a road either to safety or to 
ruin. Hence it is a subject 
of inquiry which can on no account 
be
neglected.
","content_base64":null,"directory_permission":"0777","file
_permission":"0777","filename":"art_of_war.txt","sensitive_content":null}}]
}},"resource_changes":[{"address":"local_file.literature","mode":"managed",
"type":"local_file","name":"literature","provider_name":"registry.terraform
.io/hashicorp/local","change":{"actions":["create"],"before":null,"after":{
"content":"Sun Tzu said: The art of war is of vital importance to the 
State.

It is a matter of life and death, a road either to safety or to 

ruin. Hence it is a subject of inquiry which can on no account 
be
neglected.
","content_base64":null,"directory_permission":"0777","file
_permission":"0777","filename":"art_of_war.txt","sensitive_content":null},"
after_unknown":{"id":true}}}],"configuration":{"root_module":{"resources":[
{"address":"local_file.literature","mode":"managed","type":"local_file","na
me":"literature","provider_config_key":"local","expressions":{"content":{"c
onstant_value":"Sun Tzu said: The art of war is of vital importance to the 
State.

It is a matter of life and death, a road either to safety or to 

ruin. Hence it is a subject of inquiry which can on no account 
be
neglected.
"},"filename":{"constant_value":"art_of_war.txt"}},"schema_
version":0}]}}}

2.5 Creating the local file resource

Now let’s run terraform apply to compare the output against the generated execution plan. The command and output are as follows:

$ terraform apply
 
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create
 
Terraform will perform the following actions:
 
  # local_file.literature will be created
  + resource "local_file" "literature" {
      + content              = <<-EOT
            Sun Tzu said: The art of war is of vital importance to the State.
 
            It is a matter of life and death, a road either to safety or to
            ruin. Hence it is a subject of inquiry which can on no account be
            neglected.
        EOT
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "art_of_war.txt"
      + id                   = (known after apply)
}
 
Plan: 1 to add, 0 to change, 0 to destroy.
 
Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.
 
  Enter a value:

Do they look similar? It’s no coincidence. The execution plan generated by terraform apply is exactly the same as the plan generated by terraform plan. In fact, you can even apply the results of terraform plan explicitly:

$ terraform plan -out plan.out && terraform apply "plan.out"

TIP Separating plan and apply like this could be useful when running Terraform in automation, something we will explore in chapter 12.

Regardless of how you generate an execution plan, it’s always a good idea to review the contents of the plan before applying. During an apply, Terraform creates and destroys real infrastructure, which of course has real-world consequences. If you are not careful, then a simple mistake or typo could wipe out your entire infrastructure before you even have a chance to react. For this workspace, there’s nothing to worry about because we aren’t creating “real” infrastructure.

Returning to the command line, enter yes at the prompt to approve the manual confirmation step. Your output will be as follows:

$ terraform apply
...
  Enter a value: yes
 
local_file.literature: Creating...
local_file.literature: Creation complete after 0s [id=df1bf9d6-c6cf-f9cb-
34b7-dc0ba10d5a1d]
 
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Two files were created as a result of this command: art_of_war.txt and terraform .tfstate. Your current directory (excluding hidden files) is now as follows:

    .
    ├── art_of_war.txt
    ├── main.tf
    └── terraform.tfstate

The terraform.tfstate file you see here is the state file that Terraform uses to keep track of the resources it manages. It’s used to perform diffs during the plan and detect configuration drift. Here’s what the current state file looks like.

Listing 2.2 terraform.tfstate

{
  "version": 4,                                           
  "terraform_version": "0.15.0",                          
  "serial": 1,                                            
  "lineage": "df1bf9d6-c6cf-f9cb-34b7-dc0ba10d5a1d",      
  "outputs": {},
  "resources": [                                          
    {
      "mode": "managed",
      "type": "local_file",
      "name": "literature",
      "provider": "provider["registry.terraform.io/hashicorp/local"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "content": "Sun Tzu said: The art of war is of vital importance 
to the State.

It is a matter of life and death, a road either to safety 
or to 
ruin. Hence it is a subject of inquiry which can on no account 
be
neglected.
",
            "content_base64": null,
            "directory_permission": "0777",
            "file_permission": "0777",
            "filename": "art_of_war.txt",
            "id": "907b35148fa2bce6c92cba32410c25b06d24e9af",
            "sensitive_content": null,
            "source": null
          },
          "sensitive_attributes": [],
          "private": "bnVsbA=="
        }
      ]
    }
  ]
}

Metadata about Terraform run

Resource state data

Warning It’s important not to edit, delete, or otherwise tamper with the terraform.tfstate file, or Terraform could potentially lose track of the resources it manages. It is possible to restore a corrupted or missing state file, but doing so is difficult and time-consuming.

We can verify that art_of_war.txt matches what we expect by cat-ing the file. The command and output are as follows:

$ cat art_of_war.txt
Sun Tzu said: The art of war is of vital importance to the State.
 
It is a matter of life and death, a road either to safety or to
ruin. Hence it is a subject of inquiry which can on no account be
neglected.

How did Terraform create this file? During the apply, Terraform called Create() on local_file (see figure 2.6).

CH02_F06_Winkler

Figure 2.6 Calling Create()on local_file during terraform apply

To give you an idea of what Create() does, the following listing shows the source code from the provider.

Note Relax and don’t worry about understanding the code just yet. We will examine the inner workings of providers in chapter 11.

Listing 2.3 Local file create

func resourceLocalFileCreate(d *schema.ResourceData, _ interface{}) error {
  content, err := resourceLocalFileContent(d)
  if err != nil {
    return err
  }
 
  destination := d.Get("filename").(string)
 
  destinationDir := path.Dir(destination)
  if _, err := os.Stat(destinationDir); err != nil {
    dirPerm := d.Get("directory_permission").(string)
    dirMode, _ := strconv.ParseInt(dirPerm, 8, 64)
    if err := os.MkdirAll(destinationDir, os.FileMode(dirMode)); err != nil {
      return err
    }
  }
 
  filePerm := d.Get("file_permission").(string)
 
  fileMode, _ := strconv.ParseInt(filePerm, 8, 64)
 
  if err := ioutil.WriteFile(destination, []byte(content), os.FileMode(fileMode)); 
      err != nil {
    return err
  }
 
  checksum := sha1.Sum([]byte(content))
  d.SetId(hex.EncodeToString(checksum[:]))
 
  return nil
}

2.6 Performing No-Op

Terraform can read existing resources to ensure that they are in a desired configu-ration state. One way to do this is by running terraform plan. When terraform

plan is run, Terraform calls Read() on each resource in the state file. Since our state file has only one resource, Terraform calls Read() on just local _file. Figure 2.7 shows what this looks like.

CH02_F07_Winkler

Figure 2.7 Terraform plan calls Read() on the local_file resource.

Let’s run terraform plan now:

$ terraform plan
local_file.literature: Refreshing state... 
[id=907b35148fa2bce6c92cba32410c25b06d24e9af]
 
No changes. Infrastructure is up-to-date.
 
That Terraform did not detect any differences between your configuration 
and the remote system(s). As a result, there are no actions to take.

There are no changes, as we would expect. When a Read() returns no changes, the resulting action is a no-operation (no-op). This is shown in figure 2.8.

CH02_F08_Winkler

Figure 2.8 Steps that Terraform performs when generating an execution plan for an existing deployment already in the desired state

Finally, here is the code from the provider that is performing Read(). Again, don’t worry about understanding it completely.

Listing 2.4 Local file read

func resourceLocalFileRead(d *schema.ResourceData, _ interface{}) error {
    // If the output file doesn't exist, mark the resource for creation.
    outputPath := d.Get("filename").(string)
    if _, err := os.Stat(outputPath); os.IsNotExist(err) {
        d.SetId("")
        return nil
    }
 
    // Verify that the content of the destination file matches the content we
    // expect. Otherwise, the file might have been modified externally and we
    // must reconcile.
    outputContent, err := ioutil.ReadFile(outputPath)
    if err != nil {
        return err
    }
 
    outputChecksum := sha1.Sum([]byte(outputContent))
    if hex.EncodeToString(outputChecksum[:]) != d.Id() {
        d.SetId("")
        return nil
    }
 
    return nil
}

2.7 Updating the local file resource

You know what’s better than having a file containing the first two stanzas of The Art of War? Having a file containing the first four stanzas of The Art of War! Updates are integral to Terraform, and it’s important to understand how they work. Update your main.tf code to look like the following listing.

Listing 2.5 main.tf

terraform { 
  required_version = ">= 0.15"
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.0"
    }
  }
}
 
resource "local_file" "literature" {
  filename = "art_of_war.txt"
  content     = <<-EOT 
    Sun Tzu said: The art of war is of vital importance to the State.
 
    It is a matter of life and death, a road either to safety or to 
    ruin. Hence it is a subject of inquiry which can on no account be
    neglected.
 
    The art of war, then, is governed by five constant factors, to be      
    taken into account in one's deliberations, when seeking to             
    determine the conditions obtaining in the field.                       
                                                                           
    These are: (1) The Moral Law; (2) Heaven; (3) Earth; (4) The           
    Commander; (5) Method and discipline.                                  
  EOT
}

Adding two additional stanzas

There isn’t a special command for performing an update; all that needs to happen is a terraform apply. Before we do that, though, let’s run terraform plan to see what the generated execution plan looks like. The command and output are as follows:

$ terraform plan
local_file.literature: Refreshing state...  [id=907b35148fa2bce6c92cba32410c25b06d24e9af]                         
 
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement
 
Terraform will perform the following actions:
 
  # local_file.literature must be replaced
-/+ resource "local_file" "literature" {
      ~ content              = <<-EOT # forces replacement                 
            Sun Tzu said: The art of war is of vital importance to the State.
 
            It is a matter of life and death, a road either to safety or to
            ruin. Hence it is a subject of inquiry which can on no account be
            neglected.
          +
          + The art of war, then, is governed by five constant factors, to be
          + taken into account in one's deliberations, when seeking to
          + determine the conditions obtaining in the field.
          +
          + These are: (1) The Moral Law; (2) Heaven; (3) Earth; (4) The
          + Commander; (5) Method and discipline.
        EOT
      ~ id                   = "907b35148fa2bce6c92cba32410c25b06d24e9af" 
-> (known after apply)
        # (3 unchanged attributes hidden)
    }
 
Plan: 1 to add, 0 to change, 1 to destroy.
 
____________________________________________________________________________
 
Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

Read() happens first.

Force new re-creates the resource.

As you can see, Terraform has noticed that we altered the content attribute and is therefore proposing to destroy the old resource and create a new resource in its stead. This is done rather than updating the attribute in place because content is marked as a force new attribute, which means if you change it, the whole resource is tainted. To achieve the new desired state, Terraform must re-create the resource from scratch. This is a classic example of immutable infrastructure, although not all attributes of managed Terraform resources behave like this. In fact, most resources have regular in-place (i.e. mutable) updates. The difference between mutable and immutable updates is shown in figure 2.9.

“Force new” updates sound terrifying!

Although destroying and re-creating tainted infrastructure may sound disturbing at first, terraform plan will always let you know what Terraform is going to do ahead of time, so it will never come as a surprise. Furthermore, Terraform is great at creating repeatable environments, so re-creating a single piece of infrastructure is not a problem. The only potential issue is if there is downtime for your service. If you absolutely cannot tolerate any downtime, then stick around for chapter 9 when we cover how to perform zero-downtime deployments with Terraform.

CH02_F09_Winkler

Figure 2.9 Difference between immutable and mutable updates

The flow chart for the execution plan is shown in figure 2.10.

CH02_F10_Winkler

Figure 2.10 Steps that Terraform performs when generating an execution plan for an update

Go ahead and apply the proposed changes from the execution plan by running the command terraform apply -auto-approve. The optional -auto-approve flag tells Terraform to skip the manual approval step and immediately apply changes:

$ terraform apply -auto-approve
local_file.literature: Refreshing state... [id=907b35148fa2bce6c92cba32410c25b06d24e9af]
local_file.literature: Destroying... [id=907b35148fa2bce6c92cba32410c25b06d24e9af]
local_file.literature: Destruction complete after 0s
local_file.literature: Creating...
local_file.literature: Creation complete after 0s [id=657f681ea1991bc54967362324b5cc9e07c06ba5]
 
Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

Warning -auto-approve can be dangerous if you have not already reviewed the results of the plan.

You can verify that the file is now up to date by cat-ing the file once more. The command and output are as follows:

$ cat art_of_war.txt
Sun Tzu said: The art of war is of vital importance to the State.
 
It is a matter of life and death, a road either to safety or to
ruin. Hence it is a subject of inquiry which can on no account be
neglected.
 
The art of war, then, is governed by five constant factors, to be
taken into account in one's deliberations, when seeking to
determine the conditions obtaining in the field.
 
These are: (1) The Moral Law; (2) Heaven; (3) Earth; (4) The
Commander; (5) Method and discipline.

2.7.1 Detecting configuration drift

So far, we’ve been able to create and update a text file resource. But what happens if there are ad hoc changes to the file through means outside of Terraform? Configuration drift is a common occurrence in situations where multiple privileged users are on the same file system. If you have cloud-based resources, this is equivalent to someone making point-and-click changes to deployed infrastructure in the console. How does Terraform deal with configuration drift? By calculating the difference between the current state and the desired state and performing an update.

We can simulate configuration drift by directly modifying art_of_war.txt. In this file, replace all occurrences of “Sun Tzu” with “Napoleon”.

The contents of our art_of_war.txt file will now be

    Napoleon said: The art of war is of vital importance to the
    State.
 
    It is a matter of life and death, a road either to safety or to
    ruin. Hence it is a subject of inquiry which can on no account be
    neglected.
 
    The art of war, then, is governed by five constant factors, to be
    taken into account in one's deliberations, when seeking to
    determine the conditions obtaining in the field.
 
    These are: (1) The Moral Law; (2) Heaven; (3) Earth; (4) The
    Commander; (5) Method and discipline.

This misquote is patently untrue, so we’d like Terraform to detect that configuration drift has occurred and fix it. Run terraform plan to see what Terraform has to say for itself:

$ terraform plan
local_file.literature: Refreshing state... [id=657f681ea1991bc54967362324b5cc9e07c06ba5]
 
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create
 
Terraform will perform the following actions:
 
  # local_file.literature will be created                           
  + resource "local_file" "literature" {
      + content              = <<-EOT
            Sun Tzu said: The art of war is of vital importance to the State.
 
            It is a matter of life and death, a road either to safety or to
            ruin. Hence it is a subject of inquiry which can on no account be
            neglected.
 
            The art of war, then, is governed by five constant factors, to be
            taken into account in one's deliberations, when seeking to
            determine the conditions obtaining in the field.
 
            These are: (1) The Moral Law; (2) Heaven; (3) Earth; (4) The
            Commander; (5) Method and discipline.
        EOT
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "art_of_war.txt"
      + id                   = (known after apply)
    }
 
Plan: 1 to add, 0 to change, 0 to destroy.
 
____________________________________________________________________________
 
Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

This is suprising!

Wait, what just happened? Terraform appears to have forgotten that the resource it manages even exists and is therefore proposing to create a new resource. In fact, Terraform has not forgotten that the resource it manages exists—the resource is still present in the state file, and you can verify by running terraform show:

$ terraform show
# local_file.literature:
resource "local_file" "literature" {
    content              = <<-EOT
        Sun Tzu said: The art of war is of vital importance to the State.
 
        It is a matter of life and death, a road either to safety or to
        ruin. Hence it is a subject of inquiry which can on no account be
        neglected.
        The art of war, then, is governed by five constant factors, to be
        taken into account in one's deliberations, when seeking to
        determine the conditions obtaining in the field.
 
        These are: (1) The Moral Law; (2) Heaven; (3) Earth; (4) The
        Commander; (5) Method and discipline.
    EOT
    directory_permission = "0777"
    file_permission      = "0777"
    filename             = "art_of_war.txt"
    id                   = "657f681ea1991bc54967362324b5cc9e07c06ba5"
}

The surprising outcome of terraform plan is merely the result of the provider choosing to do something a little odd with the way Read() was implemented. I don’t know why the provider chose to do it that way, but the provider decided that if the file contents don’t exactly match what’s in the state file, then the resource no longer exists. The consequence is that Terraform thinks the resource no longer exists, even though there’s still a file with the same name. It won’t make a difference when the apply happens because the existing file will be overridden, but is surprising nonetheless.

2.7.2 Terraform refresh

How can we fix configuration drift? Well, Terraform automatically fixes it if you run terraform apply, but let’s not do that right away. For now, let’s have Terraform reconcile the state that it knows about with what is currently deployed. This can be done with terraform refresh.

You can think of terraform refresh like a terraform plan that also alters the state file. It’s a read-only operation that does not modify managed existing infrastructure—just Terraform state.

Returning to the command line, run terraform refresh to reconcile the Terraform state:

$ terraform refresh
local_file.literature: Refreshing state... [id=657f681ea1991bc54967362324b5cc9e07c06ba5]

Now, if you run terraform show, you can see that the state file has been updated:

$ terraform show

However, nothing is returned because this is part of the weirdness of how local_ file works (it thinks the old file no longer exists). At least it is now consistent.

Note I rarely find terraform refresh useful, but some people really like it.

Returning to the command line, we can correct the art_of_war.txt file with terraform apply:

$ terraform apply -auto-approve
local_file.literature: Creating...
local_file.literature: Creation complete after 0s [id=657f681ea1991bc54967362324b5cc9e07c06ba5]
 
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Now the contents of art_of_war.txt have been restored to what they should be. If this was a cloud-based resource provisioned in Amazon Web Services (AWS), Google Cloud Platform (GCP), or Azure, any point-and-click changes made in the console would be gone at this point. You can verify that the file was successfully restored by cat-ing the file once more:

$ cat art_of_war.txt
Sun Tzu said: The art of war is of vital importance to the State.
 
It is a matter of life and death, a road either to safety or to
ruin. Hence it is a subject of inquiry which can on no account be
neglected.
 
The art of war, then, is governed by five constant factors, to be
taken into account in one's deliberations, when seeking to
determine the conditions obtaining in the field.
 
These are: (1) The Moral Law; (2) Heaven; (3) Earth; (4) The
Commander; (5) Method and discipline.

2.8 Deleting the local file resource

Our Art of War file has served us well, but now it’s time to say goodbye. Let’s clean up by running terraform destroy:

$ terraform destroy -auto-approve
local_file.literature: Refreshing state... [id=657f681ea1991bc54967362324b5cc9e07c06ba5]
local_file.literature: Destroying... [id=657f681ea1991bc54967362324b5cc9e07c06ba5]
local_file.literature: Destruction complete after 0s
 
Destroy complete! Resources: 1 destroyed.

Note The optional flag -auto-approve for terraform destroy is exactly the same as for terraform apply; it automatically approves the result of the execution plan.

The terraform destroy command first generates an execution plan as if there were no resources in the configuration files by performing a Read() on each resource and marking all existing resources for deletion. This can be seen in figure 2.11.

 

CH02_F11_Winkler

Figure 2.11 Steps that Terraform performs when generating an execution plan for a delete

During the actual execution of the destroy operation, Terraform invokes Delete() on each resource in the state file. Again, since there’s only one resource in the state file, Terraform effectively just calls Delete() on local_file. This is illustrated in figure 2.12.

CH02_F12_Winkler

Figure 2.12 Terraform destroy calls Delete() on each resource in the state file.

So now the art_of_war.txt file has been deleted. The current directory is the following:

    .
    ├── main.tf
    ├── terraform.tfstate
    └── terraform.tfstate.backup

Note Deleting all configuration files and running terraform apply is equivalent to terraform destroy.

Although it’s gone, its memory lives on in a new file, terraform.tfstate.backup. This backup file is a copy of the previous state file and is there for purely archival purposes. This file typically is not needed and can be safely deleted if you wish, but I usually leave it be. Our current state file is empty (as far as Terraform is concerned) and is shown next.

Listing 2.6 terraform.tfstate

{
  "version": 4,
  "terraform_version": "0.15.0",
  "serial": 9,
  "lineage": "df1bf9d6-c6cf-f9cb-34b7-dc0ba10d5a1d",
  "outputs": {},
  "resources": []
}

Finally, for your personal edification, here is the Delete() code from the Local provider (it’s quite simple).

Listing 2.7 Local file delete

func resourceLocalFileDelete(d *schema.ResourceData, _ interface{}) error {
    os.Remove(d.Get("filename").(string))
    return nil
}

2.9 Fireside chat

In this chapter, we dove into the internals of Terraform, how it works, how it provisions infrastructure, and how it calculates diffs. Terraform is fundamentally a state management tool for performing CRUD operations on managed resources. This can seem perplexing in the context of the cloud, which is already magic, but it’s not as difficult as it appears. Terraform uses the same APIs you would use if you were writing an automation script to deploy infrastructure. The difference is that Terraform doesn’t just deploy infrastructure: Terraform manages it. Terraform intrinsically understands dependencies between resources and can even detect and correct for configuration drift. Terraform is a simple state management engine. The value of Terraform derives mainly from the many providers that are published and available on the Terraform Registry. In the next chapter, we look at two new such providers: the Random and Archive providers for Terraform.

Summary

  • The Local provider for Terraform allows you to create and manage text files on your machine. This is normally used to glue together “real” infrastructure but can also be useful by itself as a teaching aid.

  • Resources are created in a certain sequence as dictated by the execution plan. The sequence is calculated automatically based on implicit dependencies.

  • Each managed resource has life cycle function hooks associated with it: Create(), Read(), Update(), and Delete(). Terraform invokes these function hooks as part of its normal operations.

  • Changing Terraform configuration code and running terraform apply will update an existing managed resource. You can also use terraform refresh to update the state file based on what is currently deployed.

  • Terraform reads the state file during a plan to decide what actions to take during an apply. It’s important not to lose the state file, or Terraform will lose track of all the resources it’s managing.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset