Terraform Provisioners - Comprehensive Guide

Terraform Provisioners – Comprehensive Guide

Welcome to our comprehensive guide to Terraform provisioners. Whether you are an experienced DevOps professional or an enthusiastic beginner, understanding the nuances of Terraform can transform your approach to infrastructure automation.

In this guide, we’re focusing on a crucial component: Terraform provisioners. Here’s a quick glance at what we’ll cover:

  • A deep dive into what Terraform provisioners are and how they function.
  • The debate around why provisioners are considered a last resort.
  • The nitty-gritty of effectively using Terraform provisioners, from handling logs in the CLI output to managing failure behavior.
  • A comprehensive exploration of various types of provisioners available in Terraform, including the local-exec, file, remote-exec, and more specialized provisions, such as chef, habitat, puppet, and salt-masterless.
  • A practical walkthrough of installing an Nginx web server using provisioners.

By the end of this guide, you’ll understand the power and limitations of Terraform provisioners and how to incorporate them into your Infrastructure as Code (IaC) practices.

Let’s start with a fundamental question: What exactly are Terraform provisioners?

Understanding Terraform provisioners

Terraform provisioners are specific built-in components that allow you to execute scripts on a local or remote machine as part of the resource creation or destruction process. They enable additional configuration and setup tasks that can’t be accomplished with Terraform’s declarative syntax alone. Examples of provisioners include local-exec, remote-exec, file, and several others, each serving a unique purpose.

Terraform Provisioners are used as a “last resort” when executing specific commands or scripts on a local or remote machine during resource creation or destruction. They can be helpful for certain system setup tasks where there’s no existing Terraform resource or existing configuration management tool.

In this section, we’ll break down the following aspects of Terraform provisioners:

  • Their role in the Terraform ecosystem
  • How they interact with resources
  • Typical use cases

Role in the Terraform Ecosystem

At its core, Terraform is designed to declare resources, not to handle the configuration of those resources. However, sometimes we may need to perform specific actions on the resources. This is where Terraform provisioners come into play. They act as bridges, filling gaps that Terraform or the resources can’t cover.

Interaction with Resources

Provisioners are added within the resource block. For instance, the following local-exec provisioner would execute a simple echo command on the local machine when a file resource is created:

resource "local_file" "example" {
  content  = "Hello, Terraform Provisioners!"
  filename = "example.txt"
  provisioner "local-exec" {
    command = "echo 'File resource created!'"
  }
}

This provisioner will trigger when Terraform creates the example.txt file.

Typical Use Cases

Despite being considered a last resort, provisioners can be incredibly useful in the following scenarios:

  • Bootstrapping a system for configuration management tools such as Chef, Puppet, or Ansible. For example, a remote-exec provisioner could install Chef on a newly created EC2 instance.
  • Performing system-specific setup tasks where there’s no corresponding Terraform resource. An example of this might be a file provisioner used to upload specific configuration files to a server.

As we progress through this guide, we’ll delve deeper into these use cases, showcasing how Terraform provisioners can enhance and streamline your IaC practices.

Why are Provisioners Considered a Last Resort?

Although Terraform provisioners can be beneficial, they are often considered a last resort in the Terraform ecosystem. The main reasons for this perception are:

  1. Idempotency and Convergence: Provisioners execute scripts that may not guarantee idempotency – the ability to apply the same operation multiple times without changing the result beyond the initial application. Most resources in Terraform are idempotent and converge towards a specific state, but provisioners could break this behavior, leading to inconsistencies.
  2. Error Recovery: If a provisioner fails, Terraform will mark the resource as tainted and plan to recreate it on the next “apply” operation. This behavior can lead to cascading failures and make recovering from errors hard.
  3. Not Suitable for Modifying Resources: Provisioners cannot modify resources after creation; they only run during resource creation or destruction.

Let’s illustrate these points with a simple example. Consider the following local-exec provisioner:

resource "local_file" "example" {
  content  = "Hello, Terraform Provisioners!"
  filename = "example.txt"
  provisioner "local-exec" {
    command = "echo 'File resource created!'"
  }
}

Here, if the local-exec provisioner fails to execute the echo command for any reason, Terraform will mark the local_file resource as tainted. The next time you run terraform apply, it will attempt to destroy and recreate the resource, potentially leading to cascading failures.

Moreover, if we wanted to modify the example.txt file content, the provisioner would not be rerun.

Despite these concerns, there are scenarios where the use of provisioners is justified, particularly when alternative approaches are impractical. In the following sections, we’ll explore how to use Terraform Provisioners effectively, highlighting their utility in real-world scenarios.

How to Effectively Use Terraform Provisioners

Even though provisioners are considered a last resort, they can still be beneficial when used judiciously and effectively. Here’s how to make the most out of Terraform provisioners:

  1. Understand the self object: The self object represents the resource to which the provisioner is attached. For instance, in a local-exec provisioner, you can use the self object to reference attributes of the resource:
resource "aws_instance" "example" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"
  provisioner "local-exec" {
    command = "echo The instance ID is ${self.id}"
  }
}

In this example, ${self.id} will output the ID of the AWS instance.

  1. Suppressing Provisioner Logs in CLI Output: If you don’t want to show provisioner logs in your CLI output, you can set the TF_LOG environment variable to "".
  2. Creation-Time and Destroy-Time Provisioners: Remember that provisioners can run either during resource creation (when = "create") or destruction (when = "destroy"). For example, to run a script only when an AWS instance is destroyed:
resource "aws_instance" "example" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"
  provisioner "local-exec" {
    when    = "destroy"
    command = "echo 'Instance ${self.id} is being destroyed'"
  }
}
  1. Handling Multiple Provisioners: You can attach multiple provisioners to a single resource, and they will execute in the order they are declared.
  2. Handling Failure Behavior: If a provisioner fails, the resource will be marked as tainted by default. However, you can adjust this behavior with the on_failure attribute. If on_failure is set to continue, Terraform will ignore provisioner failures.

These tips should help you make effective use of Terraform provisioners. In the next section, we’ll delve into different types of provisioners and explore how each can be leveraged to enhance your IaC practices.

The self Object

One crucial aspect of Terraform provisioners is understanding the self object. The self object represents the resource to which the provisioner is attached and allows us to reference the resource’s attributes within the provisioner block.

Below, we illustrate the self object’s use in a few scenarios:

  1. Reference Resource Attributes: The self object can be used to reference resource attributes. For example, consider the following aws_instance resource with an attached local-exec provisioned:
resource "aws_instance" "example" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"
  provisioner "local-exec" {
    command = "echo The instance ID is ${self.id}"
  }
}

Here, ${self.id} will output the ID of the AWS instance.

  1. Pass Resource Attributes to Scripts: The self object can also be useful for passing resource attributes as arguments to scripts. For example, suppose we have a script that takes an instance’s public IP address as an argument:
resource "aws_instance" "example" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"
  provisioner "local-exec" {
    command = "./script.sh ${self.public_ip}"
  }
}

Here, ${self.public_ip} will pass the instance’s public IP to the script.

Understanding and effectively using the self object can significantly enhance your capability to manage and configure resources using Terraform provisioners.

Suppressing Provisioner Logs in CLI Output

While the logs provided by Terraform provisioners can be valuable for debugging, they might also clutter the output or expose sensitive information in certain cases. Luckily, you can suppress these logs from your CLI output.

Here’s how you can accomplish this:

  1. Set the TF_LOG environment variable: The easiest way to suppress provisioner logs in the CLI output is to set the TF_LOG environment variable to an empty value. Here’s an example in a bash environment:
export TF_LOG=""
terraform apply

With TF_LOG set to an empty value, running terraform apply will execute without displaying the provisioner logs in the output.

  1. Using a separate log file: If you want to keep the logs for later debugging but don’t want them in your CLI output, you can direct the logs to a separate file using the TF_LOG_PATH environment variable. Here’s an example:
export TF_LOG="TRACE"
export TF_LOG_PATH="terraform.log"
terraform apply

This will execute terraform apply without displaying the provisioner logs in the CLI output, but all logs will be saved in the terraform.log file.

Remember that while it can be useful to suppress provisioner logs in some cases, they often provide valuable insights, especially when troubleshooting. So, consider your use case carefully before deciding to suppress them.

Creation-Time Provisioners

In Terraform, provisioners can run at two phases in a resource’s lifecycle: creation-time and destroy-time. Creation-time provisioners are executed when a resource is first created.

Here’s how to work with creation-time provisioners:

  1. Default behavior: By default, all provisioners are creation-time provisioners. They execute right after the resource is created. Here’s an example:
resource "aws_instance" "example" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"
  provisioner "local-exec" {
    command = "echo Instance ${self.id} has been created"
  }
}

In this example, the local-exec provisioner will execute after the AWS instance is created.

  1. Explicitly Setting Creation-Time: Although it’s the default behavior, you can explicitly set a provisioner to run at creation-time by using the when = "create" directive. This can be helpful for clarity, especially when your Terraform code also includes destroy-time provisioners.
resource "aws_instance" "example" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"
  provisioner "local-exec" {
    when    = "create"
    command = "echo Instance ${self.id} has been created"
  }
}

Creation-time provisioners are particularly useful for configuring a newly created resource or triggering certain actions right after a resource is created.

Destroy-Time Provisioners

Unlike creation-time provisioners, which execute when a resource is first created, destroy-time provisioners are designed to run just before a resource is destroyed. They’re useful for performing cleanup tasks or triggering other actions before a resource’s deletion.

Here’s how to work with destroy-time provisioners:

Defining a Destroy-Time Provisioner: To set a provisioner to run at destroy-time, you’ll use the when = "destroy" directive. Here’s an example:

resource "aws_instance" "example" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"
  provisioner "local-exec" {
    when    = "destroy"
    command = "echo Instance ${self.id} is being destroyed"
  }
}

In this example, the local-exec provisioner will execute right before the AWS instance is destroyed.

  1. Caveats of Destroy-Time Provisioners: If a creation-time provisioner fails, the associated resource gets tainted but not deleted. However, if a destroy-time provisioner fails, the resource is still destroyed.

Also note that if a resource is tainted and being recreated, the destroy-time provisioner won’t run because Terraform avoids destroying a resource that hasn’t been successfully created.

Destroy-time provisioners provide an excellent way to manage tasks that need to be performed before a resource’s deletion. However, use them judiciously to avoid unintended consequences.

Multiple Provisioners

In Terraform, you aren’t limited to using just one provisioner per resource. You can define multiple provisioners for a single resource, and they’ll be executed in the order they are defined.

Here’s how to work with multiple provisioners:

  1. Defining Multiple Provisioners: List the provisioners one after the other in your resource definition. Here’s an example with an aws_instance resource:
resource "aws_instance" "example" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"
  provisioner "local-exec" {
    command = "echo Creating instance ${self.id}"
  }
  provisioner "remote-exec" {
    inline = ["sudo apt-get update", "sudo apt-get install nginx"]
  }
  provisioner "local-exec" {
    when    = "destroy"
    command = "echo Destroying instance ${self.id}"
  }
}

Here, the first local-exec provisioner will run upon creation of the AWS instance, the remote-exec provisioner will run commands on the instance, and the second local-exec provisioner will run when the instance is destroyed.

  1. Order of Execution: Remember, the order of execution matters. Provisioners are executed in the order they are listed in the resource block. In the example above, the remote-exec provisioner will not run until the first local-exec provisioner has completed.

Using multiple provisioners provides flexibility in configuring and managing resources. However, keep in mind that the failure of any provisioner will taint the resource, so ensure your provisioners are robust and resilient.

Failure Behavior

Understanding how Terraform reacts to provisioner failure is vital. As the execution of provisioners carries the potential for things to go wrong, it’s important to know how to control the resulting behavior.

Here are some important points about provisioner failure behavior:

  1. Tainting of Resources: By default, if a provisioner fails during the creation of a resource, the resource gets ‘tainted’. A tainted resource will be destroyed and recreated in the next terraform apply. Here’s an example:
resource "aws_instance" "example" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"
  provisioner "local-exec" {
    command = "false"  // This will fail
  }
}

In this example, because the local-exec provisioner command will fail, the aws_instance resource will be tainted upon creation.

  1. Failure Behavior Directives: Terraform allows you to control the failure behavior by setting the on_failure directive to either "continue", "fail" (default), or "destroy".
resource "aws_instance" "example" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"
  provisioner "local-exec" {
    command    = "false"  // This will fail
    on_failure = "continue"
  }
}

In this example, even though the local-exec provisioner fails, the aws_instance resource will not be tainted thanks to the on_failure = "continue" directive.

Understanding provisioner failure behavior and knowing how to control it helps ensure your Terraform code behaves as expected in the face of potential provisioner failures.

Various Types of Terraform Provisioners

Terraform offers a variety of provisioners to suit your infrastructure needs. Understanding these different types is vital to manage your resources effectively.

Here are some of the key provisioners available:

  1. local-exec Provisioner: Executes a command on the machine where Terraform is run.
  2. remote-exec Provisioner: Executes a command on the remote resource.
  3. file Provisioner: Copies files or directories from the local machine to the remote resource.
  4. Chef Provisioner: Executes Chef recipes on the remote resource.
  5. Puppet Provisioner: Executes Puppet configurations on the remote resource.
  6. Habitat Provisioner: Installs and configures the Habitat supervisor on the remote resource.
  7. Salt-masterless Provisioner: Executes SaltStack states on the remote resource without requiring a Salt master.

These provisioners are powerful tools that can help you configure and manage your resources. However, they should be used sparingly, as they can lead to procedural complexity and non-declarative behaviors.

For a deeper understanding and practical application of these provisioners, the subsequent sections of this blog post will cover each type in detail.

The local-exec Provisioner

The local-exec provisioner in Terraform invokes a local executable, typically a shell script, on the same machine where Terraform is running. This can be useful for various tasks, such as invoking CLI commands, running scripts, or invoking other tools.

Here’s how you can use it:

  1. Basic Usage: To use the local-exec provisioner, define it in a resource block and provide a command to execute. Here’s an example:
resource "aws_instance" "example" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"
  provisioner "local-exec" {
    command = "echo Created instance ${self.id}"
  }
}

In this example, the local-exec provisioner executes the echo command after creating an AWS instance.

  1. Accessing Resource Attributes: Note that in the command, we can reference the resource’s attributes. For instance, ${self.id} refers to the ID of the AWS instance being created.

Remember, the local-exec provisioner executes on the machine running Terraform, not on the resource being created. Hence, it should be used for tasks related to local setup or configuration, and not for configuring the remote resource.

The Connection Block (Prerequisites)

Before diving into remote provisioners, it’s crucial to understand the connection block in Terraform. This block is used to specify settings needed for remote connections, which are essential for the remote-exec and file provisioners.

Here are the key details:

  1. Purpose of the Connection Block: The connection block provides Terraform with the necessary information to connect to a resource remotely. It tells Terraform how to communicate with the resource.
  1. Supported Connection Types: Terraform currently supports ssh and winrm types of connections.
  1. Defining a Connection Block: The connection block is defined within a resource or provisioner block. Here’s an example with an aws_instance resource:
resource "aws_instance" "example" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"
  connection {
    type        = "ssh"
    user        = "admin"
    private_key = file("~/.ssh/id_rsa")
    host        = self.public_ip
  }
  provisioner "file" {
    source      = "script.sh"
    destination = "/home/admin/script.sh"
  }
}

In this example, an aws_instance is created, and a file (script.sh) is copied from the local system to the instance using the file provisioner. The connection block details how Terraform should connect to the instance for this purpose.

Please note that you need a proper SSH setup with the necessary keys and permissions for the connection block to work correctly.

The File Provisioner

The file provisioner in Terraform is used to manage files and directories on a machine created through Terraform. This provisioner can copy files from your local system to a remote resource.

Key aspects of the file provisioner include:

  1. Copying Files: This provisioner can copy a file from the local system to the remote resource. Here’s an example:
resource "aws_instance" "example" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"
  connection {
    type        = "ssh"
    user        = "admin"
    private_key = file("~/.ssh/id_rsa")
    host        = self.public_ip
  }
  provisioner "file" {
    source      = "localfile.txt"
    destination = "/home/admin/remotefile.txt"
  }
}

In this example, the file provisioner copies the localfile.txt from your local system to the AWS instance at the specified destination path.

  1. Copying Directories: The file provisioner can also copy entire directories. The source and destination parameters function in the same way:
resource "aws_instance" "example" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"
  connection {
    type        = "ssh"
    user        = "admin"
    private_key = file("~/.ssh/id_rsa")
    host        = self.public_ip
  }
  provisioner "file" {
    source      = "config/"
    destination = "/home/admin/config/"
  }
}

This example copies the entire config/ directory to the remote AWS instance.

Remember, the file provisioner requires a connection block to define how Terraform should connect to the remote resource.

The remote-exec Provisioner

The remote-exec provisioner in Terraform allows you to execute commands on a remote resource after it’s created. This can be useful to perform initial setup tasks on a new resource.

Here’s how you can use it:

  1. Basic Usage: The remote-exec provisioner requires a connection block to specify how to connect to the remote resource. Here’s an example:
resource "aws_instance" "example" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"
  connection {
    type        = "ssh"
    user        = "admin"
    private_key = file("~/.ssh/id_rsa")
    host        = self.public_ip
  }
  provisioner "remote-exec" {
    inline = [
      "sudo apt-get update",
      "sudo apt-get install -y nginx"
    ]
  }
}

In this example, the remote-exec provisioner executes a series of commands on the AWS instance after it’s created. These commands update the system and install Nginx on the instance.

  1. Accessing Resource Attributes: Similar to the local-exec provisioner, you can reference the resource’s attributes in the command. For example, ${self.public_ip} refers to the public IP of the AWS instance being created.
  2. Inline and Script: The remote-exec provisioner supports executing an array of commands inline or running a script located on the local machine or available via a URL.

The remote-exec provisioner can be a powerful tool for initial resource setup, but it should be used sparingly to avoid procedural complexity.

Chef Provisioner

Terraform’s chef provisioner allows you to integrate your infrastructure’s creation with the powerful configuration management tool, Chef. This provisioner bootstraps Chef Client on a newly created or existing machine, allowing you to manage its configuration through Chef Server.

Here’s what you need to know:

  1. Chef Server: Chef Server is a hub for configuration data. The Chef Infra Server stores cookbooks, the policies applied to nodes, and metadata describing each registered node in your infrastructure.
  2. Chef Client: Chef Client is an agent that runs locally on every node managed by Chef.
  3. Bootstrapping with Terraform: Terraform’s chef provisioner allows you to bootstrap the Chef Client on a new node, enabling it to communicate with the Chef Server.

Here’s a basic example:

resource "aws_instance" "web" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"
  provisioner "chef" {
    server_url          = "https://api.chef.io/organizations/org_name"
    run_list            = ["cookbook::recipe"]
    node_name           = "webserver"
    recreate_client     = true
    user_name           = "admin"
    user_key            = file("~/.chef/admin.pem")
    validation_client_name = "chef-validator"
    validation_key_path    = "~/.chef/chef-validator.pem"
    environment          = "_default"
    ssl_verify_mode      = ":verify_peer"
  }
}

In this example, an aws_instance is created, and the chef provisioner bootstraps Chef Client on this instance. It connects to the specified Chef Server (server_url) and applies the specified run list to the node.

It’s essential to manage your Chef Server, clients, and keys securely to maintain the integrity of your infrastructure.

Remember, the chef provisioner is deprecated as of Terraform 0.13.0 and above. You are encouraged to use other tools like the remote-exec provisioner or configuration management software such as Ansible for more complex setups.

Habitat Provisioner

The habitat provisioner in Terraform is used for installing and managing services in a resource via Habitat. Habitat is an open-source project by Chef that provides automation capabilities for defining, packaging, and delivering applications to almost any environment with any operating system, platform, or cloud.

Here’s how you can use it:

  1. Installation: The habitat provisioner installs Habitat in a remote resource, either from a specific release URL or the latest stable version.
  2. Service Management: After installation, the provisioner can start, load, or unload services, control service groups, and bind services to each other.

Here’s an example:

resource "aws_instance" "example" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"
  provisioner "habitat" {
    version     = "0.85.0"
    peers       = ["1.2.3.4", "5.6.7.8"]
    use_sudo    = true
    service {
      name     = "core/nginx"
      topology = "standalone"
      user_toml = <<-USER_TOML
      [server]
      listen = "0.0.0.0"
      port = 80
      USER_TOML
    }
  }
}

In this example, the habitat provisioner installs a specific version of Habitat on an AWS instance, specifies peers for the supervisor, and starts an Nginx service with specific configurations.

Keep in mind that the habitat provisioner requires a connection block to specify how to connect to the remote resource.

Puppet Provisioner

The puppet provisioner in Terraform is an excellent tool for those who use Puppet for configuration management. Puppet is an open-source software configuration management and deployment tool available in free and commercial versions.

Here are some key aspects:

  1. Puppet Master: Puppet follows a master-agent architecture. The Puppet master server controls the configuration information for Puppet agents.
  2. Puppet Agent: Puppet agents are the servers managed by the Puppet master server.
  3. Puppet Integration with Terraform: Terraform’s puppet provisioner allows you to bootstrap the Puppet agent on a new node, enabling it to communicate with the Puppet master.

Here’s an example:

resource "aws_instance" "web" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"
  provisioner "puppet" {
    server            = "puppet.example.com"
    extension_requests = {
      pp_role  = "webserver"
      pp_image = "ami-0c94855ba95c574c8"
    }
  }
}

In this example, an aws_instance is created, and the puppet provisioner bootstraps a Puppet agent on this instance. It connects to the specified Puppet master (server) and sends extension requests.

Please note that as of Terraform 0.13.0 and later, the puppet provisioner is deprecated. It is recommended to use other tools like the remote-exec provisioner or configuration management software such as Ansible for more complex setups.

Salt-masterless Provisioner

The salt-masterless provisioner in Terraform allows you to configure your infrastructure using Salt in a masterless setup. Salt is a Python-based open-source configuration management software and remote execution engine.

Here’s how it works:

  1. Local Salt States: This provisioner allows you to upload your local Salt States to the remote resource. Salt States represent the configuration of each component of your infrastructure.
  2. Applying Configuration: Once uploaded, the provisioner will apply the states with salt-call, effectively configuring your resource.

Here’s an example:

resource "aws_instance" "web" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"
  provisioner "salt-masterless" {
    local_state_tree = "${path.module}/salt-states"
    remote_state_tree = "/srv/salt"
  }
}

In this example, the salt-masterless provisioner is used to upload local Salt States from ${path.module}/salt-states directory to the /srv/salt directory on an AWS instance. After the states are uploaded, salt-call applies them to configure the resource.

Please note that the salt-masterless provisioner requires a connection block to specify how to connect to the remote resource.

Practical Application: Installing Nginx Web Server Using Provisioners

In this section, we will demonstrate a practical application of Terraform provisioners by installing an Nginx web server on an AWS EC2 instance using the remote-exec provisioner.

Here’s how we do it:

  1. Creating AWS Instance: We first create an AWS instance using the aws_instance resource.
  2. Using remote-exec Provisioner: The remote-exec provisioner is then used to execute commands on the remote instance to install Nginx.

Here’s an example:

resource "aws_instance" "nginx" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"
  provisioner "remote-exec" {
    inline = [
      "sudo apt-get update",
      "sudo apt-get install -y nginx",
      "sudo systemctl start nginx"
    ]
  }
  connection {
    type        = "ssh"
    user        = "ubuntu"
    private_key = file("~/.ssh/id_rsa")
    host        = self.public_ip
  }
}

In this example, we first create an AWS EC2 instance. We then use the remote-exec provisioner to run a sequence of commands. These commands update the package lists, install Nginx, and start the Nginx service. The connection block specifies the connection details to the remote instance.

Please note that you need to replace "ami-0c94855ba95c574c8" with the appropriate Amazon Machine Image (AMI) ID for your region and the path to your SSH private key file in the private_key field of the connection block.

Conclusion: The Power and Limitations of Terraform Provisioners

As we’ve seen throughout this blog post, Terraform provisioners are powerful tools that allow us to perform specific actions on our resources during the creation or destruction stages. This functionality can be a boon in many scenarios, enabling us to automate many aspects of infrastructure management.

However, it’s essential to be aware of the following:

  1. Power of Provisioners: Provisioners can perform various actions, from running scripts on local and remote instances to managing specific configurations using management tools such as Chef, Puppet, or Salt. They provide an added layer of flexibility and control over your infrastructure.
  2. Last Resort: Despite their power, Terraform’s official documentation often refers to provisioners as a ‘last resort.’ This is because provisioners introduce an imperative element into what is otherwise a declarative system, complicating the understanding of your infrastructure’s desired state.
  3. No Error Recovery: If a provisioner fails, Terraform will mark the resource as ‘tainted’ but won’t be able to recover or retry automatically.
  4. Limited Scope: Provisioners can only run on resources that provide a physical server. They won’t work for database resources, VPCs, or other non-server resources.

By understanding these limitations and being judicious in our use of provisioners, we can leverage their strengths without compromising the integrity and reliability of our infrastructure.

References

To learn more about Terraform provisioners and related topics, you may find the following resources helpful:

  1. Terraform Provisioners: This is the official documentation page for Terraform provisioners. It provides in-depth explanations, additional examples, and insights into many supported provisioners.
  2. Terraform AWS Provider: This documentation page contains detailed information about creating and managing AWS resources using Terraform.
  3. Terraform Style Guide: This guide offers best practices for writing clear, maintainable, and idiomatic Terraform code.
  4. Infrastructure as Code (IaC): Martin Fowler’s detailed article on Infrastructure as Code, the philosophy behind tools like Terraform.
  5. Introduction to the Remote-Exec Provisioner: This tutorial provides a practical, step-by-step guide to using the remote-exec provisioner.

Please remember that Terraform provisioners are a last resort in most scenarios, and there may be other methods for managing configurations and performing operations on your resources.

Similar Posts