Terraform Actions Explained: The Engineering Guide to the Future of IaC
Introduction
The evolution of Infrastructure as Code (IaC) has always hit a clear architectural limit: the barrier between Declarative (the desired state of infrastructure) and Imperative (the actions needed to operate it).
Terraform dominated the declarative world (Day-0 and Day-1). However, for Day-2 operations — tasks that don’t change infrastructure topology but are necessary for its operation, like running database migrations, clearing caches, or triggering webhooks — we were forced to use anti-patterns. Excessive use of null_resource and local-exec provisioners created fragile codebases, dependent on shell scripts and external tools installed on the CI/CD runner.
With the arrival of Terraform 1.14, HashiCorp introduces a fundamental paradigm shift: Terraform Actions.
This article serves as a guide for professionals who want to understand the architecture behind Actions and how they replace imperative scripts with native IaC constructs.
The Architecture Problem: Side-Effects
In Terraform’s classic mental model, everything is a resource or data source. Resources have CRUD lifecycle (Create, Read, Update, Delete).
However, many operational operations are Side-Effects.
- Invalidating a CloudFront cache doesn’t create a new resource; it’s a one-time action.
- Invoking a Seed Lambda is an event, not a state.
Trying to force these events into resources (as we do with null_resource) breaks the state model. Terraform Actions solves this by introducing a new first-class block: action.
Anatomy of an Action
Unlike local-exec, where you write the script, an Action is defined by the Provider. This transfers implementation responsibility from the user (consumer) to the Provider developer (AWS, Azure, GCP).
This ensures the action is executed using secure authentication and context already configured in the provider, eliminating the need for external CLIs (like aws-cli or az-cli) in the execution environment.
Practical Example: Application Provisioning
Let’s analyze a scenario where, after launching an EC2, we need to run an Ansible Playbook.
The Old Way (Anti-pattern):
1
2
3
4
5
resource "null_resource" "run_ansible" {
provisioner "local-exec" {
command = "ansible-playbook -i ${aws_instance.app.public_ip}, playbook.yml"
}
}
The New Pattern (Terraform Actions):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 1. Action Configuration
action "ansible_playbook" "app_provisioning" {
config {
playbook_path = "${path.module}/playbook.yml"
host = aws_instance.app.public_ip
ssh_public_key = aws_key_pair.app.public_key
}
}
# 2. Lifecycle Trigger
resource "aws_instance" "app" {
# ... instance configurations ...
lifecycle {
action_trigger {
events = [after_create]
actions = [action.ansible_playbook.app_provisioning]
}
}
}
Notice the architectural difference: there’s no bash string concatenation. Data flows natively between Terraform resources and action configuration.
Ad-Hoc Execution: The -invoke Command
Perhaps the most significant innovation for DevOps/Platform Engineer teams is the ability to execute isolated actions, without going through the full plan and apply cycle.
Imagine you need to manually run the database migration, or restart a specific service. With Terraform Actions, this is done via CLI:
1
terraform apply -invoke action.ansible_playbook.app_provisioning
This allows CI/CD pipelines to execute complex operational tasks using the same infrastructure codebase, ensuring the configuration used for the action is identical to that defined in code (Single Source of Truth).
Use Cases and Engineering Patterns
When adopting Terraform Actions, I personally recommend the following patterns:
1. Database Initializers
Replace SQL seed scripts run via psql or mysql client with native Actions that invoke serverless functions (Lambda/Azure Functions) to populate the newly created database.
2. Deploy Observability
Use actions to register deploy events in monitoring tools.
1
2
3
4
5
6
action "datadog_event" "deployment" {
config {
title = "Terraform Apply Finished"
text = "Infrastructure updated successfully"
}
}
3. GitOps for VM Configuration
Instead of using complex and hard-to-debug user_data, use actions to trigger provisioners (Ansible/Chef/Salt) after confirming the instance is online and accessible.
Conclusion
Terraform Actions is not just a new feature; it’s the maturation of the tool to cover the entire Platform Engineering spectrum.
By eliminating dependency on imperative scripts and bringing Day-2 operations into Terraform’s resource graph, we gain in security, portability, and maintainability. It’s the end of the “workaround” era with null_resource and the beginning of truly native automation.