1. Overview

Terraform is a popular IaC (Infrastructure as Code) tool used to build and maintain a set of resources using a simple declarative language, HCL (HashiCorp Configuration Language). Some examples of resources are servers, databases, networking devices in the cloud, and files in the local file system.

While applying a Terraform plan, we generally target all the resources in the plan — that is, we create all the resources in the plan at once. However, sometimes, we may want to apply only one part of a plan. For example, we may need to troubleshoot a problem with a resource because of a bug in the provider. We don’t have to apply the whole plan for all resources in such a case.

In this tutorial, we’ll discuss how to apply only part of a Terraform plan by providing specific configuration files as input to the terraform apply command. The version of Terraform we’ll use is 1.8.2.

2. Example Configuration

In this section, we’ll set up a Terraform configuration that creates files on the local file system containing greeting messages in different languages.

We have the following files in our configuration:

$ ls
hello_en.tf  hello_it.tf  hello_tr.tf  locals.tf

The files, hello_en.tf, hello_it.tf, and hello_tr.tf, contain the resources that create greeting messages in three different languages, whereas locals.tf contains the local variables we use within the configuration. We’ll discuss them in the following subsections.

2.1. Local Variables

Let’s start with the locals.tf file:

$ cat locals.tf
locals {
    output_dir = "/tmp/hellos_output_dir"
    file_perm = "0744"
    files = {
        English = "hello_en.txt"
        Italian = "hello_it.txt"
        Turkish = "hello_tr.txt"
    }
    hello_messages = {
        English = "Hello Baeldung!\n"
        Italian = "Ciao Baeldung!\n"
        Turkish = "Merhaba Baeldung!\n"
    }
}

We define all the variables in a locals block. The first local variable defined is output_dir, whose value is /tmp/hellos_output_dir. This is the directory in which the files will be created. The second local variable is the file_perm variable, which defines the permissions of the files created. Its value is 0744.

Then, we define the local variable, files, which is a map containing key-value pairs. We have three keys in files, namely English, Italian, and Turkish. The values corresponding to these keys are the names of the files that we’ll create. For example, the value corresponding to English is hello_en.txt.

Finally, we define another map, hello_messages. The keys are the same as the ones in files. The corresponding values are the greeting messages that are written to the corresponding files. For example, we write the “Hello Baeldung!\n” message to the hello_en.txt file for the English key.

2.2. Resources

Now, let’s have a look at the resource file, hello_en.tf:

$ cat hello_en.tf
resource "local_file" "hello_en" {
    content = local.hello_messages["English"]
    filename = "${local.output_dir}/${local.files["English"]}"
    file_permission = local.file_perm
}

The resource keyword specifies the definition of a resource block. The resource type is local_file, which is used for generating a file with the specified content. hello_en is the resource’s name.

The content argument of the local_file resource specifies the content to store in the file. It’s assigned to the value corresponding to the key, English, in the hello_messages map, which is “Hello Baeldung!\n”.

The filename argument specifies the path of the file that will be created. We set it to /tmp/hellos_output_dir/hello_en.txt using the variables in locals.tf. Finally, we use the file_permission argument to set the permissions of the file.

The other resource files, hello_it.tf and hello_tr.tf, are similar to hello_en.tf:

$ cat hello_it.tf
resource "local_file" "hello_it" {
    content = local.hello_messages["Italian"]
    filename = "${local.output_dir}/${local.files["Italian"]}"
    file_permission = local.file_perm
}
$ cat hello_tr.tf
resource "local_file" "hello_tr" {
    content = local.hello_messages["Turkish"]
    filename = "${local.output_dir}/${local.files["Turkish"]}"
    file_permission = local.file_perm
}

Now, it’s time to build the infrastructure.

2.3. Building the Infrastructure

Let’s start with initializing the configuration using terraform init:

$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding latest version of hashicorp/local...
- Installing hashicorp/local v2.5.1...
- Installed hashicorp/local v2.5.1 (signed by HashiCorp)
...
Terraform has been successfully initialized!
...

We omit some of the messages to keep the output short. Terraform downloads the required providers in this step, which is the local provider in our example.

Next, we run terraform apply to create the resources. We skip running the optional terraform plan command, which corresponds to a dry run of the terraform apply command:

$ terraform apply -auto-approve

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols: 
  + create 
...
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

We skip showing some of the output messages again. The -auto-approve option instructs Terraform to apply the plan without asking for confirmation. According to the output, these three resources are created successfully. Let’s check the files:

$ ls /tmp/hellos_output_dir
hello_en.txt  hello_it.txt  hello_tr.txt
$ cat /tmp/hellos_output_dir/hello_en.txt
Hello Baeldung!
$ cat /tmp/hellos_output_dir/hello_it.txt
Ciao Baeldung!
$ cat /tmp/hellos_output_dir/hello_tr.txt
Merhaba Baeldung!

We’ve successfully created the desired files, and their contents are as expected.

3. Using the -target Option

As we saw in the previous section, when we run the terraform apply command, it creates all of the resources in the configuration files. What if we want to create only one of the resources? We can use the -target option of terraform apply in that case.

Let’s first destroy the infrastructure using the terraform destroy command:

$ terraform destroy -auto-approve
local_file.hello_it: Refreshing state... [id=bfb61e843bc14685023f1ffdad4d683d233ea05d]
local_file.hello_en: Refreshing state... [id=962ba848bcf34a1a44f714ed28bdf6efa1350d3f]
local_file.hello_tr: Refreshing state... [id=d7ca653f78f5ac6f0185f09387050d371683ee26]

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  - destroy
...
Destroy complete! Resources: 3 destroyed.

Having destroyed the infrastructure, the previously created files in the /tmp/hellos_output_dir directory no longer exist.

Let’s apply the Terraform plan again but just for the hello_tr resource in hello_tr.tf using the -target option:

$ terraform apply -auto-approve -target=local_file.hello_tr

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create
...
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

There’s only one resource created according to the output. Let’s check the existence of the file:

$ ls /tmp/hellos_output_dir/
hello_tr.txt
$ cat /tmp/hellos_output_dir/hello_tr.txt
Merhaba Baeldung!

The only created file is hello_tr.txt, and its content is as expected. We pass the resource name together with the resource type to the -target option in the form of resource_type.resource_name, which is local_file.hello_tr in this case.

It’s possible to call the -target option multiple times for multiple resources:

$ terraform apply -auto-approve -target=local_file.hello_tr -target=local_file.hello_en

Additionally, we can use the -target option together with terraform plan and terraform destroy for planning and destroying the infrastructure. For example, we can destroy just the hello_en resource using this option:

$ terraform destroy -auto-approve -target=local_file.hello_en

Although we can use the -target option to target specific resources, its use isn’t recommended in a normal workflow.

4. Using Modules

If it’s possible to organize our configuration files using modules, we may not need to target specific resources separately. A module in Terraform is any directory with configuration files inside it. For instance, our example setup is a module since our configuration files are all in the same directory.

Keeping all of the resource files, each corresponding to a different language, in the same directory is reasonable in our example. However, we may also need to compress and package the created files as an archive, which is a logically different task. Therefore, we may organize our configuration files as separate modules.

4.1. Reorganizing Configuration Files

Let’s reorganize our example setup:

$ tree
. 
├── hello_messages
│   ├── hello_en.tf
│   ├── hello_it.tf
│   ├── hello_tr.tf
│   ├── locals.tf
│   └── outputs.tf
└── hello_zipper.tf

1 directory, 6 files

We use the tree command to list the files and directories in the current directory recursively. The hello_messages directory contains our previous configuration files and a new configuration file, outputs.tf. Therefore, hello_messages is a module.

We have another configuration file, hello_zipper.tf in the current directory. The current directory is also a module because of the existence of the configuration file, hello_zipper.tf. This module is named as the root module in the Terraform jargon, whereas hello_messages is a child module. A root module can call child modules to use their resources in the configuration.

Let’s check the content of outputs.tf in the hello_messages module:

$ cat hello_messages/outputs.tf
output output_dir {
    value = local.output_dir
}

The output block, whose name is output_dir, exports the output_dir variable defined in locals.tf so that the root module can use it.

The hello_zipper.tf configuration file contains the definitions of the child module and the resource for archiving the files:

$ cat hello_zipper.tf
module "hellos" {
    source = "./hello_messages"
} 
resource "archive_file" "hellos_zipped" {
    type = "zip"
    output_path = "./hellos.zip"
    source_dir = module.hellos.output_dir
}

We can call modules from other modules using module blocks. The local name of the child module is hellos, which we specify after the module keyword. The source argument within the module block is the path of the child module. It’s the hello_messages directory in the current directory.

After the module block, we define a resource that will archive the files generated by the child module. The resource’s type is archive_file and hellos_zipped is the resource’s name.

The type argument of archive_file specifies the archive file’s type, which is zip in our case. The output_path argument corresponds to the name of the archive file that will be generated. We specify it as hellos.zip in the current directory.

Finally, we use the source_dir argument to specify the directory in which the files we compress reside. We obtain it from the output variable defined in hello_messages/outputs.tf using module.hellos.output_dir.

4.2. Building the Infrastructure

We can build the child module in the same way as before within the hello_messages directory. However, we can build the root module in the root module’s directory instead. Building the root module also builds the child module. As a result, this creates the archive file, hellos.zip, in the root module’s directory:

$ ls hellos.zip
hellos.zip
$ unzip hellos.zip
Archive:  hellos.zip
  inflating: hello_en.txt
  inflating: hello_it.txt
  inflating: hello_tr.txt
$ cat hello_en.txt
Hello Baeldung!
$ cat hello_it.txt
Ciao Baeldung!
$ cat hello_tr.txt
Merhaba Baeldung!

The contents of the archive file are as expected.

Therefore, since we can build each module in its own directory, modules help us target a subset of configuration files. For example, if we update hello_zipper.tf, we can run terraform apply without the -target option to rebuild the resource.

5. Conclusion

In this article, we discussed how to apply part of a Terraform plan by providing specific configuration files as input to the terraform apply command.

First, we saw that we can use the -target option with terraform apply to build specific resources. Additionally, we can use this option with terraform plan and terraform destroy as well. Then, we learned that organizing resources in a configuration as modules enables us to build each module independently, which decreases the need to use the -target option.