Skip to main content

Command Palette

Search for a command to run...

DAY #44: [DAY-19] AWS Terraform Provisioners

Let’s learn how Terraform provisioners help us go beyond just creating infrastructure

Published
11 min readView as Markdown
DAY #44: [DAY-19] AWS Terraform Provisioners
A
Sharing real-time progress, roadblocks, and open source lessons. Hoping my notes help someone else feel less stuck. "Stop waiting to feel ready, stop waiting for the perfect moment, start messy, start scared and start before you feel qualified"

Introduction

Hello and welcome back to 30 Days of AWS Terraform. This is Day 19 of the series.

If you’ve been following along from the early days, give yourself a quiet pat on the back, showing up consistently and learning something new every day is no small thing.

So far, we’ve covered quite a lot. We didn’t just talk about concepts in isolation; we slowly built them up and even worked through a few mini projects and a couple of real-world–style scenarios. Most recently, we spent time on a mini project around Image Processing using AWS Lambda, where we saw how serverless components can work together to process images efficiently. That project was an important step because it helped us move from “writing Terraform code” to actually thinking in Terraform, orchestrating multiple resources to achieve a real task.

Today, we’re stepping into another concept that often comes up once our infrastructure starts feeling more real. We can think of this as another mini project if you like, or simply as a focused learning exercise that adds one more useful tool to our Terraform toolbox. We’ll be talking about Terraform provisioners.

Terraform vs Bicep: the differences you should really know | Xavier Mignot

Provisioners tend to show up right at that moment when creating infrastructure alone is not enough. The reason we’re learning this topic is simple, sometimes creating resources is not enough. Once an EC2 instance, server, or other infrastructure exists, we often need to perform additional tasks automatically, like running commands, installing software, or copying files. Provisioners give us a safe, structured way to handle these tasks without manually logging into the server each time. i.e. maybe run a command, copy a file, or prepare a server just enough so that it’s ready for use.

By the end, provisioners would feel like another practical option we can reach for when the situation calls for it.

Fact #19: I usually avoid plain socks and prefer ones with small patterns, subtle designs, or little motifs. Maybe a tiny spaceship, a little Mickey Mouse logo, or something fun like that.

What is a Provisioner?

Let’s start with a very simple question.

What exactly is a provisioner in Terraform?

At its core, a provisioner is something that performs a task. That’s it. A task could be as small as running a single command, executing a script, copying a file, or doing some kind of operation at a particular moment in time. When Terraform creates or recreates a resource, a provisioner gives us a way to say, “Now that this thing exists, please do this extra step.”

This idea becomes easier to grasp if we think back to what we’ve already been doing. Until now, most of our Terraform work has focused on creating infrastructure i.e. VPCs, subnets, security groups, EC2 instances, and so on. Terraform is excellent at that. But sometimes, creating the resource is only part of the story. Sometimes we also want to prepare that resource in a small way.

That’s where provisioners come in.

Terraform gives us three types of provisioners, and each one is meant for a slightly different situation. Understanding which one to use and why is much more important than memorizing syntax. So we’ll take them one at a time and connect them back to real situations.

The first thing to keep in mind is that all provisioners exist to do some work, not to define infrastructure itself. They are helpers, not the foundation. With that mental model in place, the differences between them start to make sense.

The three provisioners we’ll be working with today are:

  • local-exec

  • remote-exec

  • file

Let’s begin with the local-exec provisioner, because it’s the easiest place to start and helps set the stage for everything that comes after.

What is a Local Provisioner?

Let’s talk about the local-exec provisioner.

As the name suggests, local-exec is used when we want to run a command locally. And by locally, we mean on the machine where Terraform itself is running. In our case, throughout this series, we’ve been running Terraform from our own laptops. For example, if you’re following along on a MacBook or a personal system, that machine becomes the “local” environment for Terraform.

So when we use local-exec, Terraform is not reaching out to AWS, and it’s not logging into any EC2 instance. It’s simply running a command right there on our own system.

This is an important distinction to pause on.

If we think about how we’ve been working so far, every time we run terraform apply, those commands are already being executed locally. The local-exec provisioner just gives us a structured way to attach an extra local command to the lifecycle of a resource. It might be logging something, printing output, or triggering a small helper script.

To see this in action, let’s build on the infrastructure we already created.

We already have our provider block in place:

We also use a data source to fetch the Ubuntu AMI, because we don’t want to hardcode image IDs:

This gives us a clean and reliable way to always get the latest Ubuntu image. Nothing new here, we’ve already seen this pattern before.

Next, we create a security group that allows SSH access, simply so we can connect to the instance later:

With that in place, we move on to creating the EC2 instance itself:

Up to this point, everything should feel familiar. We’re pulling the AMI from the data source, reading values from variables, and attaching the security group we just created.

Now, before we add any provisioners, we need one more important piece: the connection block.

Even though local-exec itself doesn’t need SSH, this connection block becomes important as we move forward. It tells Terraform how to connect to this specific resource. The keyword self simply means “this resource right here.” So when we say self.public_ip, we’re referring to the public IP of this EC2 instance.

Now comes the local-exec provisioner itself:

Let’s slow down and really look at what’s happening here.

The command field is just a normal shell command. We’re using echo to print a message. Inside that message, we’re interpolating values from the EC2 instance itself, like the instance ID and its public IP. Even though the command runs locally, Terraform already knows about the resource, so it can safely substitute those values.

When this provisioner runs, the command is executed on our local machine, not on the EC2 instance. That’s the key idea to remember.

If we run terraform apply at this point, Terraform may say there are no changes, because provisioners don’t change the infrastructure itself. To force Terraform to recreate the resource and trigger the provisioner again, we can mark the instance as tainted:

This tells Terraform that the resource should be destroyed and recreated.

When we apply again, Terraform destroys the old instance, creates a new one, and during that creation, we see the local-exec provisioner run. In the output, we’ll see the echoed message showing the instance ID and IP address.

This is a small example, but it clearly shows what local-exec is good at: running local helper commands that react to infrastructure events.

With that foundation in place, we’re ready to move from “local” actions to running commands on the remote machine itself. That’s where the next provisioner comes in.

What is a Remote-exec provisioner?

Let’s now move on to the remote-exec provisioner.

Now that we’re comfortable with what local-exec does, the next step feels very natural.

Until now, every command we talked about was running on our own machine. But at some point, we usually want to go one step further. We don’t just want to know that an EC2 instance was created, we want to actually do something on that instance.

This is where the remote-exec provisioner comes in.

As the name suggests, remote-exec allows us to run commands remotely on a machine. In our case, that remote machine is the EC2 instance we just created using Terraform. Unlike local-exec, this provisioner does not run on our laptop. Instead, Terraform connects to the instance over SSH and executes the commands there.

This is an important shift, so let’s pause and absorb it.

For remote-exec to work, Terraform needs a way to log in to the server. That’s why the connection block we added earlier suddenly becomes very important. The SSH user, private key, and host information tell Terraform exactly how to reach the instance and authenticate itself. Without this, remote execution simply wouldn’t be possible.

Now, think about common real-life situations. When a server comes up for the first time, we often want to do some basic setup. Maybe we want to update packages, create a file, or install something simple. That initial preparation step is a very common use case for remote-exec.

Let’s see how this looks in our existing EC2 resource.

We add a new provisioner block, this time of type remote-exec:

Instead of a single command, we now use inline. This lets us provide a list of commands that Terraform will run one after another on the remote machine. These commands are executed over SSH, exactly as if we had logged into the instance ourselves and typed them manually.

The first command updates the package list. The second command creates a file inside the /tmp directory and writes a simple message into it. There’s nothing complex here and that’s intentional. The goal is not to do something fancy, but to clearly see the difference between local execution and remote execution.

At this point, if we run terraform apply again, Terraform may still say there are no changes. Just like before, provisioners don’t count as infrastructure changes. So once again, we mark the instance as tainted:

And then we apply the changes.

Terraform destroys the existing instance, creates a new one, and during that creation process, it connects to the EC2 instance over SSH and runs the remote-exec commands. This time, the work is happening inside the server, not on our local machine.

To really confirm this, we can log into the EC2 instance after it’s created. Once connected, we check the /tmp directory and see the file remote_exec.txt. When we open it, the content matches exactly what we defined in the Terraform configuration.

That small file is our proof.

It shows us that remote-exec truly runs commands on the remote machine, using the connection details we provided. This is the key difference between local-exec and remote-exec, and once that difference is clear, both concepts feel much easier to reason about.

Now that we’ve seen how to run commands locally and remotely, there’s one more very practical scenario left, copying files from our machine to the server.

That’s where the file provisioner comes into the picture, and that’s what we’ll explore next.

What is a File Provisioner?

Finally, let’s explore the file provisioner, which builds naturally on what we’ve learned so far.

Where remote-exec lets us run commands on the remote machine, the file provisioner is all about transferring files from our local environment to the instance. This is useful when we want scripts, configuration files, or other assets to be available on the machine right after it’s created.

Here’s a simple example based on our setup:

Let’s break this down:

  • source points to a file in our local project directory, in this case, a script welcome.sh inside a scripts folder.

  • destination specifies where that file should be placed on the EC2 instance. We’re putting it in /tmp/welcome.sh, but it could be any absolute path you like.

Once Terraform applies this provisioner, the file is copied over SSH to the remote machine. Notice that, just like remote-exec, the connection block we defined earlier is required for authentication.

After the file is transferred, we could even run it using another remote-exec provisioner if needed, for example, to configure the system or create additional files based on the script.

To see it in action, we taint the resource again:

After the instance is recreated, logging into it will show our welcome.sh script in the /tmp directory. Alongside the file created by remote-exec, we now have tangible evidence that the provisioners executed successfully.

The file provisioner is simple, yet powerful. It’s a straightforward way to move files where they’re needed immediately after creating a resource, no manual copying, no extra steps.

Conclusion

And with that, we’ve completed Day 19 of the 30 Days of Terraform Challenge! Today’s session introduced us to Terraform provisioners, which might seem like small helpers at first glance, but they are actually powerful tools that allow us to go beyond just creating infrastructure. They let us automate tasks that happen right after a resource is created, whether it’s running commands locally, configuring a server remotely, or copying important files over. In other words, provisioners help bridge the gap between infrastructure as code and infrastructure ready to use. And that’s why this topic is so important, it gives us the ability to make our resources immediately practical without manual intervention, saving time and reducing errors.

Compared to some of the previous blogs, today’s session was short and fun, and the concepts were very approachable. Even if you’re just starting out, you can follow along easily. The examples were simple enough to understand the mechanics, but also practical enough that you can apply them in real-world scenarios.

For those who like a step-by-step visual guide, there’s a helpful video by Piyush Sachdeva where he explains all the concepts covered today. The video also goes through some common troubleshooting steps, which can be a lifesaver if your first attempt at running provisioners doesn’t go exactly as expected.

So, that wraps up Day 19. Tomorrow, we’ll continue our journey, building on this foundation and exploring more exciting features of Terraform. Until then, take a moment to reflect on what you’ve learned, maybe even experiment a little with your own scripts and provisioners, and enjoy the process.

Happy Terraforming!