DEV Community

Cover image for Terraform 1.14 — The Complete Guide to List Resources (.tfquery.hcl), Actions Block, and `terraform query`: IaC's First Paradigm Shift in 5 Years
daniel jeong
daniel jeong

Posted on • Originally published at manoit.co.kr

Terraform 1.14 — The Complete Guide to List Resources (.tfquery.hcl), Actions Block, and `terraform query`: IaC's First Paradigm Shift in 5 Years

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"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
# 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
Enter fullscreen mode Exit fullscreen mode

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 list schemas 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 support list (issue). Verify terraform query support 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
Enter fullscreen mode Exit fullscreen mode

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 like after_create, after_update, before_destroy
  • Explicit invocationterraform plan -invoke=action.aws_lambda_invoke.warmup (or the same flag on apply)
# 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]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
# 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
Enter fullscreen mode Exit fullscreen mode
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's condition must be a known boolean at plan time. Plan-time-unknown values like timestamp() or uuid() 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]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 discover workspace. Keep .tfquery.hcl in 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 with workflow_dispatch + label checks so CI can't carpet-invoke side effects.
  • Lambda warmup pattern. Attach after_create/after_update invoke 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 paths narrow (explicit paths, not broad wildcards) before attaching an action to a lifecycle.
  • State visibility audits. After adopting ephemeral/write-only, periodically run terraform show -json for a while to confirm nothing leaks — a misbehaving third-party module can still expose values.
  • OpenTofu compatibility matrix. Add # requires: terraform >= 1.14 as 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)