Skip to content

Provisioners

Terraform is an open-source infrastructure as code (IAC) tool that allows you to manage your infrastructure as code. Provisioners in Terraform are used to execute scripts or commands on a resource after it has been created. This can be useful for tasks such as configuring software or setting up a database. However, it is important to be careful when using provisioners, as they can make your infrastructure less predictable and harder to manage.

When to use a Provisioner

Provisioners can be used to automate tasks that need to be executed after a resource has been created. Here are some examples of when you might use a provisioner:

  • When you need to install software on a resource after it has been created
  • When you need to configure software on a resource after it has been created
  • When you need to execute a script on a resource after it has been created
  • When you need to run a test on a resource after it has been created
  • When you need to migrate data to a resource after it has been created

When not to use a Provisioner

While provisioners can be useful, there are some cases where you should avoid using them. Here are some examples:

  • When you need to make changes to an existing resource
  • When you need to configure a resource at creation time
  • When you need to configure a resource using a configuration management tool like Chef or Puppet
  • When you need to manage stateful resources like databases and queues

Examples

Installing Software on an EC2 Instance

Suppose you have an EC2 instance that needs to have some software installed on it. You can use a provisioner to install the software after the instance has been created. Here's an example:

resource "aws_instance" "example" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  tags = {
    Name = "example-instance"
  }

  provisioner "remote-exec" {
    inline = [
      "sudo apt-get update",
      "sudo apt-get install -y nginx",
    ]
  }
}

In this example, we're using the remote-exec provisioner to execute commands on the EC2 instance after it has been created. The commands will install the Nginx web server.

Configuring a Database

Suppose you have a database that needs to be configured after it has been created. You can use a provisioner to execute commands on the database after it has been created. Here's an example:

resource "aws_db_instance" "example" {
  allocated_storage    = 10
  engine               = "mysql"
  engine_version       = "5.7"
  instance_class       = "db.t2.micro"
  name                 = "example-db"
  username             = "admin"
  password             = "password"
  parameter_group_name = "default.mysql5.7"
  skip_final_snapshot  = true

  provisioner "remote-exec" {
    inline = [
      "mysql -u admin -ppassword -e 'CREATE DATABASE example;'",
    ]
  }
}

In this example, we're using the remote-exec provisioner to execute a command on the database after it has been created. The command will create a new database called example.

Warning about Provisioners

  • Be careful when using provisioners in Terraform
  • Provisioners can make infrastructure less predictable and harder to manage
  • Only use provisioners when absolutely necessary
  • Avoid using provisioners to configure resources at creation time
  • Use configuration management tools like Chef or Puppet instead if you need to configure a resource at creation time
  • Use specialized tools like Kubernetes or Docker Swarm instead of Terraform provisioners when working with stateful resources like databases and queues

Types of Provisioners

Create Time Provisioners

These provisioners are invoked as part of resource creation. They run after the resource is created, allowing you to use output data from the resource in your provisioning actions.

Example of a create time provisioner (local-exec):

resource "aws_instance" "example" {
  ami           = "ami-0c94855ba95c574c8" # Update this with latest Amazon Linux 2 AMI ID
  instance_type = "t2.micro"

  provisioner "local-exec" {
    command = "echo 'The instance ID is ${aws_instance.example.id}' > instance_id.txt"
  }
}

In this example, after the instance is created, the local-exec provisioner writes the ID of the instance to a text file.

Destroy Time Provisioners

These provisioners are invoked just before a resource is destroyed. They can be used to gracefully shut down or cleanup an application or service before its resources are removed.

Example of a destroy time provisioner (local-exec):

resource "aws_instance" "example" {
  ami           = "ami-0c94855ba95c574c8" # Update this with latest Amazon Linux 2 AMI ID
  instance_type = "t2.micro"

  provisioner "local-exec" {
    when    = "destroy"
    command = "echo 'Destroying the instance with ID ${self.id}' > destroy.log"
  }
}

In this example, just before the instance is destroyed, the local-exec provisioner writes a log message to a file.

Remember to replace "ami-0c94855ba95c574c8" with the latest Amazon Linux 2 AMI ID for your region. The self.id attribute in the second example refers to the ID of the resource being destroyed.

Usage

Here is the official documentation link for Terraform Provisioners: https://www.terraform.io/docs/provisioners/index.html

This link provides the latest information on Terraform Provisioners and their usage.

Local-Exec Provisioner

  • Executes a command locally on the machine running Terraform
  • Useful for tasks such as generating files or running local scripts
  • Not recommended for production use
  • Here is an example of how to use the local-exec provisioner with the aws_instance resource in Terraform:
resource "aws_instance" "example" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  tags = {
    Name = "example-instance"
  }

  provisioner "local-exec" {
    command = "echo The instance ID is ${aws_instance.example.id}"
  }
}
  • You can use Terraform's local-exec provisioner to run an Ansible playbook. Here's an example of how you might set that up:
resource "null_resource" "ansible_provisioner" {
  triggers = {
    build_number = "${timestamp()}"
  }

  provisioner "local-exec" {
    command = "ansible-playbook -i '${aws_instance.example.public_ip},' --private-key '~/.ssh/id_rsa' playbook.yml"
  }

  depends_on = [aws_instance.example]
}

resource "aws_instance" "example" {
  ami           = "ami-0c94855ba95c574c8" # Update this with latest Amazon Linux 2 AMI ID
  instance_type = "t2.micro"

  key_name = "key-name"  # Update this with your AWS key pair name

  tags = {
    Name = "terraform-example-instance"
  }
}

In this example, the local-exec provisioner runs the ansible-playbook command to execute your playbook. The command includes an inventory (-i) that consists of the public IP address of the AWS instance you want to configure, and --private-key which specifies the SSH key for the Ansible to use when connecting to the instance.

Please note:

  1. You should replace '~/.ssh/id_rsa' with the path to your private SSH key.
  2. Replace 'key-name' with the name of your AWS key pair.
  3. Replace 'playbook.yml' with the path to your Ansible playbook.
  4. Replace "ami-0c94855ba95c574c8" with the latest Amazon Linux 2 AMI ID for your region.

Also, ensure Ansible is installed on the system where you are running Terraform. This method executes Ansible on the same system where Terraform runs, rather than on the provisioned resource.

Remote-Exec Provisioner

  • Executes a command on a resource after it has been created
  • Useful for tasks such as installing software or configuring a resource
  • Can make infrastructure less predictable and harder to manage
  • Here's an example of how to use the remote-exec provisioner with the aws_instance resource in Terraform:
  • Example:
provider "aws" {
  region = "us-west-2"
}

resource "null_resource" "ssh_keygen" {
  provisioner "local-exec" {
    command = "ssh-keygen -t rsa -f ${path.module}/id_rsa -q -N ''"
  }
  triggers = {
    always_run = "${timestamp()}"
  }
}

resource "aws_key_pair" "deployer" {
  key_name   = "deployer-key"
  public_key = file("${path.module}/id_rsa.pub")
  depends_on = [null_resource.ssh_keygen]
}

resource "aws_security_group" "allow_http" {
  name        = "allow_http"
  description = "Allow HTTP inbound traffic"

  ingress {
    description = "HTTP from VPC"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "web" {
  ami           = "ami-0c94855ba95c574c8" # Update this with latest Amazon Linux 2 AMI ID
  instance_type = "t2.micro"

  key_name = aws_key_pair.deployer.key_name

  vpc_security_group_ids = [aws_security_group.allow_http.id]

  provisioner "remote-exec" {
    inline = [
      "sudo yum install -y nginx",
      "sudo service nginx start"
    ]
  }

  connection {
    type        = "ssh"
    user        = "ec2-user"
    private_key = file("${path.module}/id_rsa")
    host        = self.public_ip
  }
}

This script adds a null_resource that triggers a local-exec provisioner to generate the SSH keys. The newly generated keys are then used in the aws_key_pair resource. Be aware that this will overwrite any existing keys in the file paths specified.

Again, be sure to replace "ami-0c94855ba95c574c8" with the latest Amazon Linux 2 AMI ID for your region. The keys will be created in the same directory as your Terraform script (specified by ${path.module}), and they should be kept secure.

File Provisioner

  • Copies files or directories to a resource after it has been created
  • Useful for tasks such as copying configuration files or scripts
  • Can be used in conjunction with other provisioners to configure a resource
  • Here is an example of using the file provisioner in a production scenario:
resource "aws_instance" "example" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  tags = {
    Name = "example-instance"
  }

  provisioner "file" {
    source      = "file.txt"
    destination = "/tmp/file.txt"
  }
}

In this example, the file provisioner is used to copy the file.txt file from the local machine to the /tmp directory on the EC2 instance after it has been created.

Null Provisioner

  • Used to execute local scripts or commands that do not require access to a resource
  • Useful for tasks such as generating random data or writing to a local file
  • Does not interact with a resource and does not have any effect on it
  • To use the null provisioner in Terraform, you must define a null_resource block and then specify the provisioner block within it. The block is used to create a resource that does not perform any action, and the block is used to execute local scripts or commands that do not require access to a resource.

    Here's an example of how to use the null provisioner in Terraform:

resource "null_resource" "example" {

  provisioner "local-exec" {
    command = "echo This is an example of the null provisioner"
  }
}

In this example, we're using the null provisioner to execute a command locally on the machine running Terraform. The command will simply print the message "This is an example of the null provisioner" to the console.

Keep in mind that the null provisioner should be used sparingly, as it does not interact with any resources and does not have any effect on the infrastructure.

To use the null provisioner to generate random data, you can write a script that generates the data and outputs it to a file. Then, you can use the file provisioner to copy the file to the desired location:

resource "null_resource" "example" {

  provisioner "local-exec" {
    command = "echo ${random_integer.value} > random.txt"
  }

  provisioner "file" {
    source      = "random.txt"
    destination = "/tmp/random.txt"
  }
}

resource "random_integer" "value" {
  min = 1
  max = 100
}

In this example, we're using the random_integer resource to generate a random integer between 1 and 100. We then use the local-exec provisioner to output the integer to a file called random.txt. Finally, we use the file provisioner to copy the file to the /tmp directory on the machine running Terraform.

Using Triggers

The null_resource provisioner in Terraform is used to execute scripts or commands that are not directly related to infrastructure management. The triggers argument is used to define conditions under which the provisioner should be run.

  1. Purpose: triggers is a map of arbitrary strings that, when changed, will taint the null_resource and force re-provisioning.
  2. Use Case: Useful in scenarios where you want to perform some action when the value of a variable or output changes.
  3. Example:
resource "aws_instance" "example" {
  ami           = "ami-0c94855ba95c574c8" # Update this with latest Amazon Linux 2 AMI ID
  instance_type = "t2.micro"
}

resource "null_resource" "example_provisioner" {
  triggers = {
    instance_public_ip = aws_instance.example.public_ip
  }

  provisioner "local-exec" {
    command = "echo 'The public IP of the instance has changed to ${self.triggers["instance_public_ip"]}' > change.log"
  }
}

In this example, the null_resource with the local-exec provisioner will run whenever the public IP of the AWS instance changes, writing a log message to a file.

It's important to note that the null_resource does not monitor the aws_instance.example.public_ip for changes in real-time. The changes are only checked when you run a terraform plan or terraform apply.

Chef Provisioner

  • Automates the installation and configuration of software using Chef
  • Requires a Chef server and a cookbook
  • Useful for tasks such as installing and configuring web servers or databases
  • To use the Chef provisioner in Terraform, you need to have a Chef server and a cookbook. Here's an example of how to use the chef provisioner with the aws_instance resource in Terraform:
resource "aws_instance" "example" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  tags = {
    Name = "example-instance"
  }

  provisioner "chef" {
    server_url   = "<https://chef.example.com/organizations/example>"
    node_name    = "example-node"
    run_list     = [ "recipe[example-cookbook::default]" ]
    client_key   = "${file("~/.chef/example.pem")}"
    chef_server_ca_cert = "${file("~/.chef/chef-server.crt")}"
  }
}

In this example, we're using the chef provisioner to install and configure software on the EC2 instance after it has been created. The server_url specifies the URL of the Chef server, the node_name specifies the name of the node, and the run_list specifies the list of recipes to run. The client_key specifies the path to the client key, and the chef_server_ca_cert specifies the path to the Chef server certificate.

Puppet Provisioner

  • Automates the installation and configuration of software using Puppet
  • Requires a Puppet master and a Puppet manifest
  • Useful for tasks such as installing and configuring web servers or databases
  • To use the Puppet provisioner in Terraform, you need to have a Puppet master and a Puppet manifest. Here's an example of how to use the puppet provisioner with the aws_instance resource in Terraform:
resource "aws_instance" "example" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  tags = {
    Name = "example-instance"
  }

  provisioner "puppet" {
    server = "puppet.example.com"
    manifest_file = "/path/to/manifest.pp"
    modules = [
      "/path/to/modules",
    ]
  }
}

In this example, we're using the puppet provisioner to automate the installation and configuration of software on the EC2 instance after it has been created. The server specifies the hostname or IP address of the Puppet master, the manifest_file specifies the path to the Puppet manifest, and the modules specifies the path to the Puppet modules.

Handling Provisioner Failures in Terraform

Normal Behavior:

  • On resource creation: If a provisioner fails, Terraform taints the resource and plans for its destruction and recreation during the next apply.
  • On resource destruction: If a provisioner fails, Terraform logs an error but continues with the resource destruction.

Overriding Normal Behavior:

  • Use on_failure attribute and set it to continue to prevent resource tainting on provisioner failure during creation. Example:
resource "aws_instance" "example" {
  ami           = "ami-0c94855ba95c574c8" # Update this with latest Amazon Linux 2 AMI ID
  instance_type = "t2.micro"

  provisioner "local-exec" {
    command = "echo 'The instance ID is ${aws_instance.example.id}' > instance_id.txt"
    on_failure = "continue"
  }
}

This will allow the resource to be marked as successfully created even if the provisioner fails. Be cautious, as this could leave your resource in an unexpected state.