Summary
In --bootstrap mode, when -e/--environment is omitted (as the README's
bootstrapping usage explicitly instructs), the environment variable is never
declared, so the first bare ${environment} reference aborts the script with
environment: unbound variable under set -u (enabled in v2.3.0).
Root Cause
set -uo pipefail is enabled at bin/terraform.sh:8.
In the bootstrap branch, environment is only declared readonly when
environment_arg is non-empty:
bin/terraform.sh:316-321
if [ "${bootstrap}" == 'true' ]; then
...
[ -n "${environment_arg}" ] && readonly environment="${environment_arg}";
else
...
readonly environment="${environment_arg}"; # always declared in non-bootstrap
fi;
When bootstrapping without -e, environment_arg is empty, the && branch is
skipped, and environment is never created. Every subsequent bare
${environment} reference then violates set -u.
Notably, the v2.3.0 hardening commit (1932b57b) already anticipated this and
defensively guarded exactly one line:
bin/terraform.sh:357
[ -n "${environment:-}" ] && export TF_VAR_environment="${environment}";
…but every other reference was left unguarded, so the fix is incomplete. The
comment at bin/terraform.sh:584 even documents the
intent — "Environment is normally expected, but in bootstrapping it may not be
provided" — yet the guard on the very next line uses a bare ${environment},
which is itself the crash.
Unguarded ${environment} references reachable in bootstrap mode
All of the following use bare ${environment} (not ${environment:-}) and are
reachable during a --bootstrap run, in execution order:
| Line |
Context |
Reachability |
| 437 |
Global pre.sh sourcing |
Only if a pre.sh exists |
| 474 |
Component pre.sh sourcing |
Only if component pre.sh exists |
| 487 |
S3 secrets aws s3 ls path |
Unconditional — first guaranteed crash |
| 490-491 |
S3 secrets aws s3 cp path |
If secrets file present |
| 520 |
remote_vars_path construction |
Unconditional |
| 537 |
versions_file_name construction |
Unconditional |
| 541 |
Env file [ -n "${environment}" ] guard |
Unconditional |
| 585 |
"may not be provided" guard |
Unconditional |
The first guaranteed abort is bin/terraform.sh:487
(the S3 secrets existence check runs regardless of any hook files). If a global
pre.sh exists, bin/terraform.sh:437 aborts even
earlier.
Evidence
Minimal reproduction of the exact mechanism (mirrors lines 316-321 then a later
bare reference):
$ bash -c 'set -uo pipefail
declare environment_arg=""
bootstrap="true"
[ -n "${environment_arg}" ] && readonly environment="${environment_arg}"
echo "reached line before use"
echo "env is: ${environment}"'
reached line before use
bash: line 1: environment: unbound variable
# exit code 127
Expected: bootstrapping succeeds without -e, because environment is
genuinely irrelevant to creating the state bucket (the bootstrap backend prefix
is hard-coded to .../bootstrap at
bin/terraform.sh:676, not .../${environment}).
Actual: script aborts with environment: unbound variable.
Reproduction Steps
Follow the README bootstrapping usage verbatim (no -e). Because of #49 you
must currently also pass a -- argument to get past line 284:
bin/terraform.sh \
-p myproject \
-b myproject-tfscaffold \
-r eu-west-2 \
--bootstrap \
-a apply \
-- -input=false
With valid AWS credentials this proceeds past argument parsing and account
lookup, then aborts at the first bare ${environment} reference
(line 487, or line 437 if a
global pre.sh exists) with line NNN: environment: unbound variable.
Suggested Fix
The documented behaviour is that environment is optional for bootstrapping, so
the intent is for an unset environment to be valid — not to error. Two viable
directions:
-
Declare a safe default once (preferred, matches documented intent):
after the bootstrap/non-bootstrap block, ensure environment always exists,
e.g. declare it as empty in the bootstrap branch when environment_arg is
empty. Then audit every bare ${environment} reference and make the ones
that are merely presence checks tolerant of empty (the [ -n ... ] guards
at 541 and 585 already do
the right thing once the variable simply exists). The S3 path
constructions (487, 520)
would then build .../<account>/<region>// — which is harmless for the
secrets/remote-vars lookups, but the fixer should confirm the trailing-slash
behaviour is acceptable or skip those lookups entirely when bootstrapping.
-
Use ${environment:-} consistently at every reference reachable in
bootstrap mode (mirroring the existing guard at
line 357).
Whichever approach is chosen, the fixer should also confirm the interaction
with the secrets and remote-tfvars S3 lookups during bootstrap — these arguably
should be skipped entirely when bootstrap == 'true', since a not-yet-created
bucket cannot contain them, which would sidestep several of the unguarded
references at their source.
Note: this must remain compatible with the readonly declarations — do not
declare environment twice.
Affected Platforms
All platforms (Linux, macOS, any bash with set -u). The bug is in
shell-level variable handling, not platform-specific tooling.
Related
Summary
In
--bootstrapmode, when-e/--environmentis omitted (as the README'sbootstrapping usage explicitly instructs), the
environmentvariable is neverdeclared, so the first bare
${environment}reference aborts the script withenvironment: unbound variableunderset -u(enabled in v2.3.0).Root Cause
set -uo pipefailis enabled at bin/terraform.sh:8.In the bootstrap branch,
environmentis only declaredreadonlywhenenvironment_argis non-empty:bin/terraform.sh:316-321
When bootstrapping without
-e,environment_argis empty, the&&branch isskipped, and
environmentis never created. Every subsequent bare${environment}reference then violatesset -u.Notably, the v2.3.0 hardening commit (
1932b57b) already anticipated this anddefensively guarded exactly one line:
bin/terraform.sh:357
…but every other reference was left unguarded, so the fix is incomplete. The
comment at bin/terraform.sh:584 even documents the
intent — "Environment is normally expected, but in bootstrapping it may not be
provided" — yet the guard on the very next line uses a bare
${environment},which is itself the crash.
Unguarded
${environment}references reachable in bootstrap modeAll of the following use bare
${environment}(not${environment:-}) and arereachable during a
--bootstraprun, in execution order:pre.shsourcingpre.shexistspre.shsourcingpre.shexistsaws s3 lspathaws s3 cppathremote_vars_pathconstructionversions_file_nameconstruction[ -n "${environment}" ]guardThe first guaranteed abort is bin/terraform.sh:487
(the S3 secrets existence check runs regardless of any hook files). If a global
pre.shexists, bin/terraform.sh:437 aborts evenearlier.
Evidence
Minimal reproduction of the exact mechanism (mirrors lines 316-321 then a later
bare reference):
Expected: bootstrapping succeeds without
-e, because environment isgenuinely irrelevant to creating the state bucket (the bootstrap backend prefix
is hard-coded to
.../bootstrapatbin/terraform.sh:676, not
.../${environment}).Actual: script aborts with
environment: unbound variable.Reproduction Steps
Follow the README bootstrapping usage verbatim (no
-e). Because of #49 youmust currently also pass a
--argument to get past line 284:With valid AWS credentials this proceeds past argument parsing and account
lookup, then aborts at the first bare
${environment}reference(line 487, or line 437 if a
global
pre.shexists) withline NNN: environment: unbound variable.Suggested Fix
The documented behaviour is that environment is optional for bootstrapping, so
the intent is for an unset environment to be valid — not to error. Two viable
directions:
Declare a safe default once (preferred, matches documented intent):
after the bootstrap/non-bootstrap block, ensure
environmentalways exists,e.g. declare it as empty in the bootstrap branch when
environment_argisempty. Then audit every bare
${environment}reference and make the onesthat are merely presence checks tolerant of empty (the
[ -n ... ]guardsat 541 and 585 already do
the right thing once the variable simply exists). The S3 path
constructions (487, 520)
would then build
.../<account>/<region>//— which is harmless for thesecrets/remote-vars lookups, but the fixer should confirm the trailing-slash
behaviour is acceptable or skip those lookups entirely when bootstrapping.
Use
${environment:-}consistently at every reference reachable inbootstrap mode (mirroring the existing guard at
line 357).
Whichever approach is chosen, the fixer should also confirm the interaction
with the secrets and remote-tfvars S3 lookups during bootstrap — these arguably
should be skipped entirely when
bootstrap == 'true', since a not-yet-createdbucket cannot contain them, which would sidestep several of the unguarded
references at their source.
Note: this must remain compatible with the
readonlydeclarations — do notdeclare
environmenttwice.Affected Platforms
All platforms (Linux, macOS, any bash with
set -u). The bug is inshell-level variable handling, not platform-specific tooling.
Related
root cause. Unbound variable access breaks deployment #49 concerns
${@}being unbound atbin/terraform.sh:284 when no
--arguments aresupplied. Both are
set -uregressions introduced by enablingset -u, andUnbound variable access breaks deployment #49's crash occurs earlier in execution (line 284) — so a reporter working
around Unbound variable access breaks deployment #49 with a dummy
-- -var=...argument is exactly who will next hitthis bug. They should likely be fixed together as a "complete the
set -uhardening" pass, but they are separate defects with separate fixes.