Describe the Bug
When a component uses JIT source provisioning (source.uri + provision.workdir.enabled: true) together with a store-outputs hook, running atmos terraform apply fails during the before.terraform.apply hook phase.
The hook attempts to read a Terraform output from the component (to fetch the previous value before apply). Doing so requires Atmos to generate the backend file (backend.tf.json) inside the JIT workdir. However, the workdir has not been provisioned yet at this point in the lifecycle — Atmos provisions the workdir for the terraform command itself, not for hooks that precede it. This means the path that backend.tf.json would be written to does not exist, and the operation fails.
atmos terraform init succeeds (workdir is provisioned as part of the init path before any output reads are attempted), but atmos terraform apply fails because the before.terraform.apply hook fires before workdir provisioning happens for that run.
Removing the store-outputs hook allows the apply to succeed, confirming the hook is the trigger.
Expected Behavior
JIT workdir provisioning should occur before any hooks (including before.terraform.apply) that may need to access the Terraform backend for the component. Hooks should not be able to observe an unprovisioned workdir state.
Actual Behavior
The before.terraform.apply hook fires, attempts to run terraform output against the JIT component, which requires generating backend.tf.json inside the (not-yet-provisioned) workdir. This results in:
ℹ Executing command: atmos terraform apply demo-vpc -auto-approve
INFO Running hooks event=before.terraform.apply
Fetching vpc_properties output from demo-vpc in dev
x Fetching vpc_properties output from demo-vpc in dev
# Error
Error: terraform output returned nil: failed to get terraform output for key
vpc_properties: failed to execute terraform output for component demo-vpc in
stack dev: failed to generate backend file: open
/tmp/gitrepo.fFs98Y/components/terraform/demo-vpc/backend.tf.json: no such file
or directory
Steps to Reproduce
The script below is fully self-contained. It requires atmos, tofu, and docker on PATH and network access to GitHub. Redis is started via Docker to serve as the output store. Save it as repro.sh and run it.
The script runs two scenarios back-to-back: a success case (no hooks) to confirm JIT + workdir works on its own, then the failure case (hook on before-terraform-apply).
Run:
cat << 'SCRIPT' > repro.sh
#!/usr/bin/env bash
# ============================================================
# ATMOS REPRO: JIT workdir not provisioned before hooks fire
#
# Stack name: demo (from vars.name + name_template)
# Component: null-label (key under components.terraform)
# Commands: atmos terraform <cmd> null-label -s demo
#
# Requires: atmos, tofu, docker
# ============================================================
set -euo pipefail
# --- 0) Create isolated workspace ---
WORKDIR="$(mktemp -d -t atmos-repro-XXXXXX)"
echo "Working in: ${WORKDIR}"
cd "${WORKDIR}"
# --- 1) Start a local Redis instance for the store ---
echo "== starting redis =="
docker stop atmos-repro-redis 2>/dev/null || true
docker run -d --rm --name atmos-repro-redis -p 6379:6379 redis:7-alpine
trap 'docker stop atmos-repro-redis 2>/dev/null || true' EXIT
sleep 1
# --- 2) Write atmos.yaml (shared across both scenarios) ---
cat <<'EOF' > atmos.yaml
base_path: "."
stores:
local/redis:
type: redis
options:
url: "redis://localhost:6379"
components:
terraform:
base_path: "components/terraform"
command: "tofu"
workspaces_enabled: true
apply_auto_approve: false
deploy_run_init: true
init_run_reconfigure: true
auto_generate_backend_file: true
stacks:
name_template: "{{ .vars.name }}"
base_path: "stacks"
included_paths:
- "**/*"
EOF
mkdir -p stacks
cleanup() {
echo "-- cleanup --"
atmos terraform workdir clean --all 2>/dev/null || true
echo "-- cleanup done --"
}
# ============================================================
# SCENARIO 1 (SUCCESS): JIT + workdir WITHOUT hooks
# Confirms the component itself works fine — the bug is
# introduced by the before-terraform-apply hook.
# ============================================================
echo
echo "================================================="
echo "SCENARIO 1: apply WITHOUT hooks (expect success)"
echo "================================================="
cleanup
cat <<'EOF' > stacks/demo.yaml
vars:
name: demo
terraform:
backend_type: local
components:
terraform:
null-label:
vars:
namespace: "eg"
stage: "test"
name: "demo"
enabled: true
source:
uri: "git::https://github.com/cloudposse/terraform-null-label.git"
version: "0.25.0"
ttl: "0s"
provision:
workdir:
enabled: true
# NO hooks — baseline to confirm JIT + workdir works
EOF
echo "== init =="
atmos terraform init null-label -s demo
echo "== apply =="
atmos terraform apply null-label -s demo -- -auto-approve
echo "SCENARIO 1: PASSED (success as expected)"
# ============================================================
# SCENARIO 2 (FAILURE): JIT + workdir WITH before-apply hook
# The before.terraform.apply hook fires before the workdir is
# provisioned, so backend.tf.json does not exist yet.
# ============================================================
echo
echo "================================================="
echo "SCENARIO 2: apply WITH before-apply hook (expect failure)"
echo "================================================="
cleanup
cat <<'EOF' > stacks/demo.yaml
vars:
name: demo
terraform:
backend_type: local
components:
terraform:
null-label:
vars:
namespace: "eg"
stage: "test"
name: "demo"
enabled: true
source:
uri: "git::https://github.com/cloudposse/terraform-null-label.git"
version: "0.25.0"
ttl: "0s"
provision:
workdir:
enabled: true
hooks:
store-outputs:
events:
- before-terraform-apply
command: store
name: local/redis
outputs:
label_id: .id
EOF
echo "== init =="
atmos terraform init null-label -s demo
echo "== apply (expected to FAIL) =="
set +e
atmos terraform apply null-label -s demo -- -auto-approve
EXIT_CODE=$?
set -e
if [[ $EXIT_CODE -ne 0 ]]; then
echo "SCENARIO 2: FAILED (failure expected — bug confirmed)"
else
echo "SCENARIO 2: UNEXPECTEDLY SUCCEEDED — bug may be fixed"
fi
echo
echo "Done. Workspace preserved at: ${WORKDIR}"
SCRIPT
bash repro.sh 2>&1 | tee repro.log
Note: docker is required only for the local Redis instance. If you already have Redis running on localhost:6379, remove the docker run block and trap. The store type is otherwise not relevant to the bug — the failure occurs when the hook tries to call terraform output, before the store write is even attempted.
Screenshots
No response
Environment
Atmos 1.214.0 on linux/arm64
Additional Context
atmos terraform init null-label -s demo succeeds — workdir is provisioned before init runs.
atmos terraform apply null-label -s demo fails — the before.terraform.apply hook fires before the workdir is provisioned for the apply run.
- Removing the
hooks block from the component config allows apply to succeed.
- The issue is not store-specific; the error occurs before the store is ever contacted. Any hook event on
before.terraform.apply that causes Atmos to call terraform output (and thus generate backend.tf.json) will trigger this failure when the component uses JIT workdir provisioning.
Suspected root cause: The JIT workdir provisioning lifecycle is tied to the terraform command invocation, not to the surrounding hook lifecycle. Hooks that fire on before.terraform.apply should see a fully provisioned workdir.
Describe the Bug
When a component uses JIT source provisioning (
source.uri+provision.workdir.enabled: true) together with astore-outputshook, runningatmos terraform applyfails during thebefore.terraform.applyhook phase.The hook attempts to read a Terraform output from the component (to fetch the previous value before apply). Doing so requires Atmos to generate the backend file (
backend.tf.json) inside the JIT workdir. However, the workdir has not been provisioned yet at this point in the lifecycle — Atmos provisions the workdir for the terraform command itself, not for hooks that precede it. This means the path thatbackend.tf.jsonwould be written to does not exist, and the operation fails.atmos terraform initsucceeds (workdir is provisioned as part of the init path before any output reads are attempted), butatmos terraform applyfails because thebefore.terraform.applyhook fires before workdir provisioning happens for that run.Removing the
store-outputshook allows the apply to succeed, confirming the hook is the trigger.Expected Behavior
JIT workdir provisioning should occur before any hooks (including
before.terraform.apply) that may need to access the Terraform backend for the component. Hooks should not be able to observe an unprovisioned workdir state.Actual Behavior
The
before.terraform.applyhook fires, attempts to runterraform outputagainst the JIT component, which requires generatingbackend.tf.jsoninside the (not-yet-provisioned) workdir. This results in:Steps to Reproduce
The script below is fully self-contained. It requires
atmos,tofu, anddockeronPATHand network access to GitHub. Redis is started via Docker to serve as the output store. Save it asrepro.shand run it.The script runs two scenarios back-to-back: a success case (no hooks) to confirm JIT + workdir works on its own, then the failure case (hook on
before-terraform-apply).Run:
Screenshots
No response
Environment
Atmos 1.214.0 on linux/arm64
Additional Context
atmos terraform init null-label -s demosucceeds — workdir is provisioned before init runs.atmos terraform apply null-label -s demofails — thebefore.terraform.applyhook fires before the workdir is provisioned for the apply run.hooksblock from the component config allows apply to succeed.before.terraform.applythat causes Atmos to callterraform output(and thus generatebackend.tf.json) will trigger this failure when the component uses JIT workdir provisioning.Suspected root cause: The JIT workdir provisioning lifecycle is tied to the terraform command invocation, not to the surrounding hook lifecycle. Hooks that fire on
before.terraform.applyshould see a fully provisioned workdir.