Terraform 1.14 Actions
When Declarative IaC Goes Imperative
Terraform 1.14 introduces Actions — first-class imperative blocks that let you invoke shell commands, HTTP requests, and custom provider logic directly within the plan/apply lifecycle. No more 500-line Bash wrappers. Here's what Actions are, how they work, where the boundaries are, and how to adopt them without turning your Terraform into Ansible.
The Problem Everyone Pretends Doesn't Exist
Ever wonder why every “mature” Terraform repo comes with a scripts/ directory that's bigger than the .tf files? Because pure declarative has limits.
Validation before plan. API calls after apply. Pre-flight checks, post-deploy smoke tests, cache invalidations, Lambda invocations, VM restarts, database password rotations—these tasks have always lived outside the plan/apply lifecycle. Hacked into Makefiles, CI pipelines, null_resource with local-exec, or 500-line Bash wrappers.
Terraform 1.14 finally acknowledges the elephant in the room: not everything in infrastructure management is CRUD.
What Are Actions?
First-Class Imperative Blocks in a Declarative World
Actions are a new language concept introduced in Terraform 1.14 (GA since November 19, 2025). They let providers define imperative operations—things that do something rather than manage state—as first-class citizens in your .tf files.
Think of actions as the infrastructure equivalent of middleware. They execute around the lifecycle of your resources but don't create, update, or destroy them. They orchestrate around your state graph without becoming part of it.
action "aws_lambda_invoke" "notify_deploy" {
config {
function_name = "deployment-notifier"
payload = jsonencode({
environment = var.environment
timestamp = plantimestamp()
resources = "updated"
})
}
}The action block takes two labels: a provider-defined type (like aws_lambda_invoke) and a user-defined name. Inside, the config block holds the action-specific arguments. The provider decides what actions are available—just like it decides what resources and data sources exist.
Provider Actions Available Today
AWS Provider
aws_lambda_invokeInvoke Lambda functions with custom payloads
aws_cloudfront_create_invalidationInvalidate CloudFront distribution caches
aws_ec2_stop_instanceGracefully stop EC2 instances with polling
Azure Provider
azurerm_virtual_machine_powerPower on, off, or restart Azure VMs
More coming: Actions are provider-defined, so the catalog will expand as AWS, Azure, GCP, and community providers add support.
Two Ways to Invoke Actions
Lifecycle Triggers (Automatic)
Bind Actions to Resource Events
The action_trigger block inside a resource's lifecycle meta-argument lets you fire actions before or after resource events. This is the automation sweet spot—your CDN cache gets invalidated every time you update the S3 origin, your monitoring gets pinged after every deploy, your Lambda runs validation after every database change.
resource "aws_s3_object" "app_bundle" {
bucket = aws_s3_bucket.static.id
key = "app/bundle.js"
source = "dist/bundle.js"
etag = filemd5("dist/bundle.js")
lifecycle {
action_trigger {
events = [after_create, after_update]
actions = [action.aws_cloudfront_create_invalidation.clear_cache]
}
}
}
action "aws_cloudfront_create_invalidation" "clear_cache" {
config {
distribution_id = aws_cloudfront_distribution.cdn.id
paths = ["/*"]
}
}Supported Events
before_create
after_create
before_update
after_update
Optional Condition
Add a condition expression to gate execution. Must resolve at plan time—no timestamp() or runtime-dynamic values.
CLI Invocation (On-Demand)
Trigger Actions Manually via -invoke
Standalone actions—those not referenced in any action_trigger—can be fired ad hoc from the CLI. This is your Day 2 operations workhorse: restart a VM, rotate a secret, invoke a cleanup function, all without leaving Terraform.
# Invoke a standalone action terraform apply -auto-approve \ -invoke=action.aws_lambda_invoke.rotate_secrets # Works with plan too terraform plan \ -invoke=action.aws_ec2_stop_instance.maintenance_window # Indexed actions use bracket notation terraform apply -auto-approve \ -invoke=action.aws_lambda_invoke.batch[0]
Limitation: Only one action can be invoked per CLI command. You can't chain multiple -invoke flags in a single run.
Actions Support Meta-Arguments
Actions aren't just fire-and-forget blocks. They support three meta-arguments that make them composable with the rest of your Terraform configuration:
provider
Specify which provider configuration or alias to use. Essential for multi-region or multi-account setups.
action "aws_lambda_invoke" "eu" {
provider = aws.eu_west
config { ... }
}count
Create multiple action instances with numeric indexing. Useful for batch operations across numbered resources.
action "aws_lambda_invoke" "batch" {
count = 3
config {
function_name = "processor"
payload = jsonencode({
index = count.index
})
}
}for_each
Create instances that match your resource iterations. Keep actions in sync with dynamic infrastructure.
action "aws_lambda_invoke" "notify" {
for_each = local.services
config {
function_name = "deployer"
payload = jsonencode({
service = each.key
})
}
}What Actions Replace
| Before (The Hack) | After (Actions) | Why It's Better |
|---|---|---|
null_resource + local-exec | action "aws_lambda_invoke" | Uses provider credentials, no local CLI tools required |
| Makefile / Bash wrapper scripts | lifecycle.action_trigger | Visible in terraform plan output |
| CI pipeline post-steps | after_create / after_update triggers | Coupled to the resource change, not the pipeline |
terraform_data + triggers hack | Standalone -invoke CLI | No phantom state, no drift, explicit invocation |
Provisioners (remote-exec) | Provider-native actions | No SSH, no agent, provider API path |
Where the Line Is
Actions Are Not Resources. That's the Point.
This is the most important design decision in the feature. Actions execute outside the state graph. They don't create resources, they don't track state, and other resources cannot depend on their outcomes. You can't do depends_on = [action.aws_lambda_invoke.validate]. That's intentional.
Actions are side-effects, not building blocks. The moment you start trying to chain action results into resource attributes, you're fighting the design—and you'll lose.
Actions DO
Actions DO NOT
“Actions execute outside the state graph—they don't create resources, they orchestrate around them. Treat them like middleware, not business logic.”
5 Practical Patterns to Implement Today
Start with These Use Cases
Post-Deploy CDN Cache Invalidation
Bind aws_cloudfront_create_invalidation to after_update on your S3 origin objects. Every time your static assets change, the CDN cache clears automatically. No more stale bundles in production.
lifecycle {
action_trigger {
events = [after_create, after_update]
actions = [action.aws_cloudfront_create_invalidation.bust]
}
}Post-Deploy Monitoring Notification
Use aws_lambda_invoke with after_create and after_update to ping your monitoring system (Datadog, PagerDuty, Grafana) whenever infrastructure changes. Your on-call team sees the change before the alert fires.
action "aws_lambda_invoke" "notify_monitoring" {
config {
function_name = "infra-change-notifier"
payload = jsonencode({
event = "deploy"
env = var.environment
})
}
}VM Power Management for Cost Savings
Use azurerm_virtual_machine_power to shut down dev/staging VMs during off-hours. Invoke via CLI in a scheduled pipeline or bind to terraform_data triggers for automated FinOps wins.
action "azurerm_virtual_machine_power" "stop_dev" {
config {
virtual_machine_id = azurerm_linux_virtual_machine.dev.id
power_off = true
}
}
# terraform apply -invoke=action.azurerm_virtual_machine_power.stop_devPost-Apply Smoke Tests via Lambda
After creating or updating an API Gateway or ALB, invoke a Lambda that hits the health endpoint and validates the deployment. If the smoke test fails, you know immediately — not when customers report it.
resource "aws_lb" "api" {
# ... config ...
lifecycle {
action_trigger {
events = [after_create, after_update]
actions = [action.aws_lambda_invoke.smoke_test]
condition = var.run_smoke_tests
}
}
}Chained Actions for Deploy Orchestration
Multiple actions can fire on the same event, in order. Invalidate cache, then notify Slack, then trigger a canary analysis — all from a single resource update.
lifecycle {
action_trigger {
events = [after_update]
actions = [
action.aws_cloudfront_create_invalidation.bust,
action.aws_lambda_invoke.notify_slack,
action.aws_lambda_invoke.trigger_canary,
]
}
}What NOT to Do with Actions
The teams that'll win with Actions are the ones that scope them tight. The teams that'll regret it are the ones that turn their Terraform into Ansible.
Don't Use Actions for Configuration Management
If you're tempted to install packages, configure services, or manage files on VMs through actions, stop. That's what Ansible, Chef, Puppet, or cloud-init are for. Actions are for infrastructure orchestration, not server configuration.
Don't Chain Action Results into Resources
Actions don't produce outputs that other resources can consume. If you need an API call's response to inform a resource attribute, use a data source or an external data provider.
Don't Build Complex Workflows in Actions
If your action needs conditional branching, retries, or error handling beyond what the provider offers, you've outgrown actions. Use Step Functions, Temporal, or a proper workflow engine.
Don't Forget: Actions Are Provider-Defined
You can only use actions that providers expose. You can't define arbitrary shell commands in an action block. If you need something custom, write a Lambda/Function and invoke it via the appropriate provider action.
Don't Make Actions Stateful
Actions should be idempotent. If invoking the same action twice causes problems, you're using it wrong. Treat them like HTTP middleware — safe to replay, no side-channel state.
Actions vs Provisioners vs Run Tasks
Three features, three different use cases. Don't confuse them.
| Feature | Scope | Auth | Best For |
|---|---|---|---|
| Actions | Provider-defined operations | Provider credentials | Day 2 ops, side-effects, notifications |
| Provisioners | Arbitrary commands | SSH / WinRM | Legacy bootstrapping (deprecated pattern) |
| Run Tasks | Terraform Cloud webhooks | HCP tokens | Policy, compliance, third-party gates |
Your Action Plan
Implement This Week
Upgrade to Terraform 1.14+
The current latest is v1.14.4 (January 28, 2026). Upgrade your CLI and update your CI/CD pipelines. Test with terraform version in every environment.
Audit Your Scripts Directory
List every Bash script, Makefile target, and CI post-step that wraps Terraform. Categorize which ones are doing Day 2 operations that could become actions.
Start with Two Actions
Pick a post_apply notification (ping Slack/monitoring after deploy) and a cache invalidation (CloudFront/CDN after asset update). Keep them stateless and idempotent.
Update Your Provider Versions
Check that your AWS, Azure, or GCP provider versions support actions. Check the Terraform Registry for available action types in your provider.
Write a Team ADR
Document your team's decision on when to use actions vs. external scripts vs. provisioners vs. run tasks. Set boundaries early before actions sprawl.
Remove One Bash Script
Replace your most painful wrapper script with a native action. Measure the improvement in clarity, maintainability, and team onboarding speed.
Pro Tip
Start with two use cases: a lifecycle-triggered action that invalidates your CDN cache after static asset updates, and a standalone CLI action that stops dev VMs for off-hours cost savings. Keep actions stateless and idempotent—treat them like middleware, not business logic.
Once your team has confidence with those two patterns, expand to post-deploy notifications and smoke tests. The golden rule: if an action needs retry logic, error handling, or conditional branching, it's too complex for an action. Move it to a proper workflow engine.
Quick Validation Checklist
Is this action idempotent? (Safe to run twice with same result)
Does it depend on runtime values that won't exist at plan time?
Could this be a data source instead? (If you need the result)
Is the provider action available in your provider version?
Have you documented when this action runs and why?
What About OpenTofu?
OpenTofu has an open issue (#3309) tracking action block support, currently labeled “pending decision.” If you're on OpenTofu, actions are not available yet. Watch that issue for updates.
Key Takeaways
Terraform 1.14 Actions (GA since November 2025) bring imperative, provider-defined operations into the declarative workflow as first-class citizens
Actions execute outside the state graph — they don't create resources, they orchestrate around them
Two invocation methods: lifecycle triggers (automatic, bound to resource events) and CLI -invoke (on-demand, Day 2 operations)
Supported lifecycle events: before_create, after_create, before_update, after_update
Actions support meta-arguments: provider, count, and for_each for composability
Provider actions are still early — AWS has 3, Azure has 1. The catalog will grow as providers mature
Actions replace null_resource hacks, Bash wrapper scripts, and CI post-steps for Day 2 operations
Keep actions stateless and idempotent — if you need retries, branching, or error handling, use a workflow engine instead
OpenTofu does not yet support actions — the feature request is pending decision
Declarative Got a Taste of Imperative. Use It Wisely.
Actions close the gap between what Terraform manages and what your team actually needs to do. The winners will scope them tight. The regretters will turn their .tf files into Ansible playbooks.
Related Posts
5 Terraform Mistakes That Will Wreck Your Day (And How to Avoid Them)
Terraform is powerful, but one wrong move and you could wipe out production, corrupt your state, or lock yourself out of critical resources. Learn the five most common—and most dangerous—mistakes that even experienced engineers make, and how to avoid them.
Claude Code Hit $2.5B. Amazon Engineers Can't Use It. Welcome to AI Agent Lock-In.
Claude Code just hit a $2.5 billion run-rate — doubled since January 1st. Yet 1,500 Amazon engineers are fighting for permission to use it, steered toward AWS Kiro instead. This is vendor lock-in repackaged for the AI agent era. Platform-native vs platform-agnostic is the new architectural fault line.
GitHub Agentic Workflows: The Decision Framework Nobody's Talking About
Everyone's excited about AI in CI/CD. Nobody's asking when to use it vs when not to. GitHub Agentic Workflows just entered technical preview — the architecture is solid. But the real decision isn't which agent to pick. It's when to use agentic workflows vs deterministic ones. Here's the decision framework, the adoption pattern, and the three questions to answer before you deploy.