terraform
cloud-engineering
iac
devops
hashicorp
infrastructure-automation
day-2-ops

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_invoke

Invoke Lambda functions with custom payloads

aws_cloudfront_create_invalidation

Invalidate CloudFront distribution caches

aws_ec2_stop_instance

Gracefully stop EC2 instances with polling

Azure Provider

azurerm_virtual_machine_power

Power 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

1

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.

2

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-execaction "aws_lambda_invoke"Uses provider credentials, no local CLI tools required
Makefile / Bash wrapper scriptslifecycle.action_triggerVisible in terraform plan output
CI pipeline post-stepsafter_create / after_update triggersCoupled to the resource change, not the pipeline
terraform_data + triggers hackStandalone -invoke CLINo phantom state, no drift, explicit invocation
Provisioners (remote-exec)Provider-native actionsNo 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

Invoke provider-defined operations (Lambda, VM power, cache invalidation)
Fire before or after resource lifecycle events
Execute on-demand via the CLI
Use provider credentials and configuration
Support count, for_each, and provider meta-arguments

Actions DO NOT

Create, update, or destroy infrastructure resources
Store state or track drift
Allow other resources to depend on their output
Support arbitrary shell commands (they're provider-defined)
Replace the need for proper resource design

“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

1

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]
  }
}
2

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
    })
  }
}
3

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_dev
4

Post-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
    }
  }
}
5

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.

FeatureScopeAuthBest For
ActionsProvider-defined operationsProvider credentialsDay 2 ops, side-effects, notifications
ProvisionersArbitrary commandsSSH / WinRMLegacy bootstrapping (deprecated pattern)
Run TasksTerraform Cloud webhooksHCP tokensPolicy, compliance, third-party gates

Your Action Plan

Implement This Week

1

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.

2

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.

3

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.

4

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.

5

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.

6

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.