On November 19, 2025, HashiCorp shipped Terraform v1.14.0, and on April 20, 2026 they released the v1.14.9 patch that fixes a Stacks plugin installation bug. This minor release is not just a feature bump — it's the first crack in a five-year-old IaC consensus that "declarative + one-way provisioning is enough." Two new primitives do the cracking. First, List Resources (a new *.tfquery.hcl file type + the terraform query command) let you discover, filter, and bulk-import resources that already exist in your cloud from outside your codebase. Second, the Actions Block and -invoke CLI flag let you attach imperative side-effects — Lambda invocations, CloudFront invalidations, Ansible playbooks — to resource lifecycles in a declarative way. This article is a production-oriented tour of Terraform 1.14 with HCL examples, operational scenarios, and a comparison against OpenTofu.
1. Why Terraform 1.14 Matters — A Turning Point in IaC
CNCF Q1 2026 data shows Terraform still holds a 71% share of cloud IaC tooling, with OpenTofu closing fast. In that environment, two 1.14 changes finally pull two long-outsourced workflows into the core.
| Long-standing pain | Pre-1.13 workaround | 1.14 answer | Operational gain |
|---|---|---|---|
| Bulk discovery/import of existing resources | aws-cli + custom scripts + hand-written import blocks |
List Resources + terraform query -generate-config-out |
Discover, filter, and generate HCL with one command |
| Post-deploy Lambda warmup, CloudFront invalidation, Slack alerts |
local-exec + null_resource + GitHub Actions post-steps |
action_trigger { events = [after_create] } |
Side effects visible in plan/apply |
| One-shot secrets (passwords, tokens) | Risk of plaintext in state | (1.11) Ephemeral resources + write-only attributes, stabilized in 1.14 | One-shot values without polluting state |
| Splitting large module graphs |
terragrunt run-all + dir sharding |
HCP Terraform Stacks, plugin bug fixed in 1.14.9 | Deploy/rollback per component |
| Modules blocked by unknown values | Multi-step apply | (Experimental) Deferred actions scheduled for 1.16 | Path to count/for_each with unknowns |
The key insight: 1.14 breaks Terraform's "it only manages what it knows" limit from both directions at once. List Resources pulls outside resources in; Actions push inside intent out. The line between IaC and automation scripts just narrowed.
2. List Resources — .tfquery.hcl and terraform query
List Resources live in a separate .tfquery.hcl file for a clear reason: they are a read-only discovery graph that must not be mixed into the normal plan graph. The terraform query command evaluates .tfquery.hcl files in the current directory, calls the cloud API, and either prints results to the console or — with -generate-config-out — emits import + resource blocks to a new file.
# find-unmanaged.tfquery.hcl
# Find running EC2 in us-east-2 tagged ManagedBy=unmanaged
provider "aws" {
region = "us-east-2"
}
list "aws_instance" "unmanaged" {
provider = aws
config {
filter {
name = "tag:ManagedBy"
values = ["unmanaged"]
}
filter {
name = "instance-state-name"
values = ["running"]
}
}
}
# 1) Print results to console
terraform query
# 2) Generate import + resource blocks automatically
terraform query -generate-config-out=to-import/unmanaged.tf
# 3) JSON output (for CI)
terraform query -json > query.json
# 4) Variable injection
terraform query -var 'env=prod'
terraform query -var-file=prod.tfvars
The heart of the feature is -generate-config-out. The 1.5-era import block told Terraform where to map an existing resource, but you still had to know what existed. 1.14's List Resources express the "what exists" as code, and the command spits out ready-to-use import + resource scaffolds. A four-step workflow collapses into one command.
list block argument |
Role | Notes |
|---|---|---|
provider |
Provider alias used for discovery | Required |
config { ... } |
Provider-specific filters / region / tags | Per-resource schema |
limit |
Max results | Safety net for large accounts |
include_resource |
Emit resource blocks alongside import
|
Default false
|
count / for_each
|
Scan multiple scopes (accounts/regions) | Same semantics as normal resources |
⚠️ Warning: Not every provider exposes
listschemas for every resource yet. The AWS provider supports EC2/S3/IAM and is expanding gradually; some community and official providers (e.g.terraform-provider-github) still do not supportlist(issue). Verifyterraform querysupport for your target providers before standardizing this workflow.
2.1 Real workflow — Bulk-importing 100 EC2 instances
# Step 1: separate discovery dir
mkdir -p discover && cd discover
cat > find-prod-ec2.tfquery.hcl <<'EOF'
provider "aws" {
region = "ap-northeast-2"
}
list "aws_instance" "prod" {
provider = aws
config {
filter {
name = "tag:Environment"
values = ["prod"]
}
}
include_resource = true
limit = 200
}
EOF
# Step 2: init provider
terraform init
# Step 3: generate results + scaffolds
terraform query -generate-config-out=generated/prod-ec2.tf
# Step 4: move into main workspace and verify
mv generated/prod-ec2.tf ../live/imported-prod-ec2.tf
cd ../live
terraform plan -out=plan.tfplan # verify state alignment
terraform apply plan.tfplan # execute import
100 instances produce 100 import + 100 resource blocks in one shot. Previously you'd aws ec2 describe-instances | jq your way into a hand-written import script and then manually fill empty resource bodies one by one. terraform query compresses that into one command.
3. Actions Block — Reconciling Declarative IaC with Imperative Side Effects
The second new primitive is a top-level action block. Actions are provider-defined, non-CRUD operations. For example, aws_lambda_invoke calls a Lambda once; aws_cloudfront_create_invalidation triggers a CDN cache purge. Actions fire in two ways:
-
Resource lifecycle triggers — a resource's
lifecycle { action_trigger { ... } }binds an action to events likeafter_create,after_update,before_destroy -
Explicit invocation —
terraform plan -invoke=action.aws_lambda_invoke.warmup(or the same flag onapply)
# actions.tf — Lambda warmup action
action "aws_lambda_invoke" "warmup" {
config {
function_name = aws_lambda_function.api.function_name
payload = jsonencode({
type = "warmup"
ts = "${timestamp()}" # ⚠️ do NOT use inside trigger conditions
})
}
}
# actions.tf — CloudFront invalidation action
action "aws_cloudfront_create_invalidation" "purge_static" {
config {
distribution_id = aws_cloudfront_distribution.main.id
paths = ["/static/*", "/index.html"]
}
}
# api.tf — attach lifecycle triggers
resource "aws_lambda_function" "api" {
function_name = "manoit-api"
role = aws_iam_role.lambda_exec.arn
handler = "index.handler"
runtime = "nodejs22.x"
filename = "build/api.zip"
lifecycle {
action_trigger {
events = [after_create, after_update]
actions = [action.aws_lambda_invoke.warmup]
}
}
}
# cdn.tf — CloudFront distribution with invalidation trigger
resource "aws_cloudfront_distribution" "main" {
# ... (origin/behaviors config omitted)
lifecycle {
action_trigger {
events = [after_update]
actions = [action.aws_cloudfront_create_invalidation.purge_static]
}
}
}
# 1) Normal apply — lifecycle triggers run automatically
terraform apply
# 2) Invoke an action immediately without infra changes
terraform apply -invoke=action.aws_lambda_invoke.warmup
# 3) Multiple actions at once
terraform apply \
-invoke=action.aws_lambda_invoke.warmup \
-invoke=action.aws_cloudfront_create_invalidation.purge_static
| Event | Fires when | Typical use |
|---|---|---|
after_create |
Immediately after resource creation | Lambda warmup, initial data seeding |
after_update |
Immediately after resource update | CloudFront invalidation, cache purge |
before_destroy |
Just before deletion | Snapshots, external deregistration |
after_destroy |
Immediately after deletion | Slack alerts, monitor cleanup |
⚠️ Warning: An
action_trigger'sconditionmust be a known boolean at plan time. Plan-time-unknown values liketimestamp()oruuid()are not allowed. Actions also do not fail the plan by default — a failed action shouldn't mean damaged infra. For hard-critical integrations (payment systems, etc.), prefer a separate reconcile workflow over an action.
3.1 The anti-pattern actions replace
# ❌ Pre-1.13 anti-pattern — null_resource + local-exec
resource "null_resource" "warmup" {
triggers = {
lambda_version = aws_lambda_function.api.version
}
provisioner "local-exec" {
command = "aws lambda invoke --function-name manoit-api /tmp/out.json"
}
}
# ✅ 1.14 recommendation — action block + action_trigger
action "aws_lambda_invoke" "warmup" {
config {
function_name = aws_lambda_function.api.function_name
payload = jsonencode({ type = "warmup" })
}
}
resource "aws_lambda_function" "api" {
# ...
lifecycle {
action_trigger {
events = [after_create, after_update]
actions = [action.aws_lambda_invoke.warmup]
}
}
}
Three reasons null_resource + local-exec was an anti-pattern: (1) plan output doesn't show the intent — only a trigger hash; (2) local-exec depends on the runner having the AWS CLI, credentials, and network access; (3) failure handling is ambiguous. 1.14 actions print an explicit "WILL INVOKE" line in plan output, reuse provider credentials, and surface failures as separate exit codes.
4. 1.14's Silent Improvements — Stacks, Ephemeral, Import
List Resources and Actions are the headline, but 1.14 quietly tidies a pile of features that had been in beta/experimental. Operationally, these are not optional reading.
| Area | 1.14 change | Operational impact |
|---|---|---|
terraform import |
Workspace variable lookups now include variable-set inheritance (#37241) | Fewer import failures in HCP Terraform |
| OSS backend | Dedicated proxy layer (#36897) | OSS state over corporate HTTP proxies works again |
terraform test |
Verbose mode prints expected diagnostics (#37362); cleanup ignores prevent_destroy (#37364) |
Cleanup no longer blocks tests |
terraform validate |
-query flag for offline .tfquery.hcl validation (#37671) |
CI-side syntax pre-check |
| AWS European Sovereign Cloud | New partition (#37721) | Use the new EU-regulated regions |
| Filesystem functions | Handle inconsistent results in provider config (#37854) | External-file modules stabilize |
| Stacks | Progress events on plan failure; 1.14.9 fixes plugin install bug (#38406) | Stacks UI no longer stalls |
| Build environment | Go 1.25, macOS Monterey or later required (#37436) | Upgrade old build machines |
Ephemeral resources and write-only attributes (introduced in 1.11) get broader provider support in 1.14. The defining property is that their values never get written to state or plan files, making them the standard tool for patterns like "fetch a rotating password from Vault and apply it to RDS once."
# ephemeral + write-only — Vault one-shot password for RDS master
ephemeral "random_password" "db" {
length = 32
special = true
}
resource "aws_db_instance" "main" {
identifier = "manoit-prod"
engine = "postgres"
engine_version = "17.2"
instance_class = "db.r7g.xlarge"
allocated_storage = 200
username = "manoit"
# ⚠️ write-only — never stored in state/plan in plaintext
password_wo = ephemeral.random_password.db.result
password_wo_version = 1 # bump on rotation
skip_final_snapshot = false
}
5. Compatibility with OpenTofu
Since the 2024 fork, OpenTofu has preserved the HCL surface and provider protocol while staying free of BSL licensing. List Resources and Actions from 1.14 are not yet implemented in OpenTofu (opentofu/opentofu#3787, #3309). Adoption is less about feature love and more about operational policy.
| Consideration | Terraform 1.14 | OpenTofu 1.10.x |
|---|---|---|
| License | BSL 1.1 (commercial restrictions) | MPL 2.0 (fully open) |
| List Resources / Actions | Supported | Not yet (tracking issues) |
| HCP Terraform integration | First-class | None |
| State encryption | HCP backend encryption | Native state encryption (1.7+) |
| Provider compatibility | Full | Full (same registry) |
| Governance | HashiCorp (IBM) | Linux Foundation |
Two realistic takes: a legacy migration project where List Resources immediately saves import cost should adopt Terraform 1.14 today. An organization with a heavy BSL burden, or one redistributing IaC to multi-vendor OEMs, should stay on OpenTofu and compensate for List Resources with a custom import script.
6. 1.13 → 1.14 Upgrade Checklist
| Step | Check | Tool/Command |
|---|---|---|
| ① Environment | Go build machine macOS version (Monterey 12+), container CPU quota impact (#37436) |
sw_vers, cat /proc/cpuinfo
|
| ② Workspaces | If HCP variable sets inform imports, validate behavior change | Review PR #37241 |
| ③ Modules | Inventory null_resource + local-exec sites → migrate to action blocks |
grep -RIn "null_resource" |
| ④ Secrets | RDS/Redshift/ElasticSearch passwords → ephemeral + write-only | Provider *_wo args |
| ⑤ Imports | Separate .tfquery.hcl dirs, add terraform validate -query to CI |
GitHub Actions matrix |
| ⑥ Actions | Action permissions inherit from provider creds → re-audit IAM | aws-iam-analyzer / OPA |
| ⑦ Stacks | If using HCP Stacks, require 1.14.9+ (#38406 fix) | terraform stacks --version |
| ⑧ Drift | Schedule terraform query runs that flag ManagedBy=unmanaged
|
Scheduled job + Slack |
7. ManoIT Production Patterns
-
Separate a
discoverworkspace. Keep.tfquery.hclin its own workspace with a read-only IAM role. Results flow into the main workspace only as PRs. -
Gate action invocation.
terraform apply -invoke=...is powerful; guard it withworkflow_dispatch+ label checks so CI can't carpet-invoke side effects. -
Lambda warmup pattern. Attach
after_create/after_updateinvoke only to functions where cold start has real cost. High-traffic functions don't need it — skip to avoid needless invocation charges. -
CloudFront invalidation cost hygiene. Invalidations are free for the first 1000/month, then billed. Keep
pathsnarrow (explicit paths, not broad wildcards) before attaching an action to a lifecycle. -
State visibility audits. After adopting ephemeral/write-only, periodically run
terraform show -jsonfor a while to confirm nothing leaks — a misbehaving third-party module can still expose values. -
OpenTofu compatibility matrix. Add
# requires: terraform >= 1.14as a header comment to modules that use new features, so OpenTofu workspaces don't accidentally execute them.
8. Conclusion — Questions IaC Must Answer in the Next Five Years
Terraform 1.14 is the first release to crack the five-year "pure declarative is enough" consensus. List Resources admit that resources the code doesn't know about exist and pull them inward; Actions admit that side effects the code must express exist and refuse to offload them to external automation. Both point the same way — IaC is becoming a system that continuously observes and intervenes in its environment, not a one-shot drawing tool. ManoIT recommends every 1.14 adoption come with a separate discover workspace and action-invocation guards on day one. The experimental deferred actions shipping in 1.16 will close the gaps that unknown values carve into the graph, and that's when the combination of List Resources + Actions + Deferred will reveal the IaC model of the next five years in full.
This article was co-authored by the ManoIT engineering team and Anthropic's Claude Opus 4.7 LLM. Sources: Terraform v1.14.0 Release Notes, v1.14.9 Patch Notes, terraform query CLI Reference, list block reference, Invoke an action, action block reference.
Originally published at ManoIT Tech Blog.
Top comments (0)