Skip to content

Bug: store-outputs hook fires before JIT workdir is provisioned on terraform apply, causing backend file generation failure #2307

Description

@zack-is-cool

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug🐛 An issue with the system

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions