Job Environment
Every Terraform/OpenTofu operation runs as a single-container Kubernetes Job the manager builds from internal/jobs. This page documents that Job’s environment, mounts and fixed fields; the runner’s own environment shaping; and the reserved variable names spec.jobs.env may not set.
Main container environment
| Name | Value | Meaning |
|---|---|---|
TF_IN_AUTOMATION | 1 | Tells Terraform/OpenTofu it is running unattended: it skips interactive follow-up hints in its output. |
TF_INPUT | 0 | Disables interactive prompts; the runner always answers non-interactively. |
HOME | /captf/work | The runner’s working directory (render.WorkDir), since the image’s real HOME may not be writable. |
TMPDIR | /tmp | The Job’s /tmp emptyDir, since the image’s root filesystem is read-only. |
KUBE_NAMESPACE | (the object's namespace) | The kubernetes backend’s namespace, so state Secrets land beside the owning object. |
CHECKPOINT_DISABLE | 1 | Stops Terraform from calling checkpoint-api.hashicorp.com on every command: a pod holding cloud credentials otherwise makes that call and can stall on its timeout when the namespace drops egress silently. |
| (envFrom) | (the identity credential mirror Secret) | Mirrors the resolved TerraformClusterIdentity’s provider credentials into the runner’s environment (internal/identity). |
spec.jobs.env entries that reuse a reserved name are dropped and reported as an event; a representative Spec with TF_LOG and KUBE_CONFIG_PATH set drops: TF_LOG, KUBE_CONFIG_PATH.
Volumes and mounts
| Mount path | Read-only | Meaning |
|---|---|---|
/captf/bin | true | The runner binary, copied in by the init container. |
/captf/work | false | Scratch: the generated root, the CLI configuration, plan files, and TF_DATA_DIR. |
/tmp | false | General temporary storage (TMPDIR). |
/captf/config | true | The per-run Secret: the generated inputs root, and for a restore Job, the backup’s state chunks (jobs.RestoreChunkDir). |
/var/run/captf/credentials | true | The identity Secret’s credential files, mode 0440: a non-root image user reads them through the pod’s fsGroup. |
Fixed Job fields
| Field | Value | Meaning |
|---|---|---|
| backoffLimit | 0 | The controller owns retries (the attempt number is in the Job name); the pod itself never retries. |
| terminationGracePeriodSeconds | 600 | SIGTERM lets the runner finish in-flight provider calls and write results before SIGKILL. |
| activeDeadlineSeconds (default) | 3600 | Overridable by spec.jobs.activeDeadlineSeconds. |
| lockTimeoutSeconds (default) | 300 | Overridable by spec.jobs.lockTimeoutSeconds. |
Default resources
| Container | Kind | Values |
|---|---|---|
| init (runner copy) | requests | cpu=10m, memory=32Mi |
| init (runner copy) | limits | cpu=100m, memory=64Mi |
| main (source), when spec.jobs.resources is unset | requests | cpu=250m, memory=512Mi |
| main (source), when spec.jobs.resources is unset | limits | memory=2Gi (no default CPU limit: throttling a slow apply is worse than a slow apply) |
Security contexts
| Scope | Field | Value |
|---|---|---|
| pod | seccompProfile | RuntimeDefault |
| pod | fsGroup | 65532 (so a non-root image user can read the 0440 credential files through the group) |
| init (runner copy) | allowPrivilegeEscalation | false |
| init (runner copy) | capabilities.drop | ALL |
| init (runner copy) | runAsNonRoot / runAsUser | true / 65532 |
| init (runner copy) | readOnlyRootFilesystem | true |
| main (source) | allowPrivilegeEscalation | false |
| main (source) | capabilities.drop | ALL |
| main (source) | readOnlyRootFilesystem | true |
| main (source) | runAsNonRoot | not defaulted: images built FROM hashicorp/terraform run as root |
Runner command and args
The main container’s command is /captf/bin/runner run. Its args, built by jobs.Build, are:
| Flag | Value | Meaning |
|---|---|---|
--op | apply, destroy, refresh, drift, restore or plan | The operation this Job runs. |
--bin | The runtime binary path (render.RuntimePath): the image’s tofu or terraform. | |
--image | (the source image reference) | Recorded in events and logs to identify which image ran. |
--module | The image’s role module (render.ModuleDir). | |
--providers | The image’s optional provider mirror (render.ProvidersDir); the runner checks whether it exists. | |
--workdir | The generated root’s parent (render.WorkDir). | |
--config | The per-run Secret’s mount (jobs.ConfigDir). | |
--lock-timeout | (jobs.DefaultLockTimeoutSeconds, or spec.jobs.lockTimeoutSeconds)s | How long the backend lock acquisition waits before failing. |
--stop-timeout | (terminationGracePeriodSeconds minus a margin)s | How long the runner has, after SIGTERM, to finish in-flight work and write results before the pod is killed. |
--backend-config=secret_suffix | (a per-attempt suffix) | The kubernetes backend’s Secret name suffix. |
--backend-config=namespace | (the object’s namespace) | The kubernetes backend’s namespace. |
--backend-config=in_cluster_config | Tells the kubernetes backend to use the pod’s in-cluster credentials. | |
--backend-config=labels | (the backend Secret labels, as an HCL object) | Labels the backend applies to the state Secrets it manages. |
--force-unlock | (a stale lock ID) | Set only when the controller detected a stale backend lock; the runner force-unlocks it after init. |
--guard-deletes | Set only for a TerraformCluster apply: the runner stops before a plan that deletes or replaces resources unless allowed. | |
--inputs-hash | (the rendered inputs hash) | Paired with –guard-deletes: the hash of the inputs this apply renders. |
--allow-deletes-hash | (an approved inputs hash) | The TerraformCluster’s captf.io/approve-destructive-plan hash, when set: allows a destructive plan for that exact input set. |
--expect-plan | (an approved plan hash) | Under applyPolicy Manual, the TerraformCluster’s captf.io/approve-plan hash the apply must re-plan and match before applying. |
--event-object | (apiVersion/kind/namespace/name/uid of the owner) | Set only when the manager runs with –runner-events: lets the runner report progress as Kubernetes events about the owning object. |
--job-name | (the Job’s name) | Paired with –event-object: the reporting Job’s own name. |
--restore-chunks | (chunk count) | Set only for a restore Job: how many backup state chunks are projected into the config volume. |
--restore-resources | (managed resource count) | Set only for a restore Job: the backup’s recorded managed-resource count; the runner fails the restore if the state list shows none when this is not 0. |
What the runner changes before running the runtime
internal/runner.Prepare calls the exported Environ helper to build the runtime’s step environment from the Job’s. Given an environment carrying every credential-bearing and backend-changing variable the identity Secret’s envFrom or the image’s own ENV might set:
TF_IN_AUTOMATION=1 TF_INPUT=0 KUBE_NAMESPACE=default CHECKPOINT_DISABLE=1 TF_LOG=DEBUG TF_VAR_region=us-east-1 TF_WORKSPACE=default TF_CLI_ARGS_apply=-parallelism=1 KUBE_CONFIG_PATH=/tmp/kubeconfig HOME=/root TMPDIR=/tmp AWS_ACCESS_KEY_ID=AKIA...
Environ produces (TMPDIR was set, so it is kept as-is):
AWS_ACCESS_KEY_ID=AKIA... CHECKPOINT_DISABLE=1 HOME=/captf/work KUBE_NAMESPACE=default TF_DATA_DIR=/captf/work/.terraform TF_INPUT=0 TF_IN_AUTOMATION=1 TMPDIR=/tmp
and reports the dropped names: KUBE_CONFIG_PATH, TF_CLI_ARGS_apply, TF_LOG, TF_VAR_region, TF_WORKSPACE.
Rules:
TF_DATA_DIRis always forced to<workDir>/.terraform, overriding any inherited value.HOMEis always forced to<workDir>.TMPDIRis kept when the Job’s environment already sets it (the Job sets it to/tmp, an emptyDir); otherwise it defaults to<workDir>/tmp, whichPreparecreates.TF_CLI_CONFIG_FILEis set only when the image ships a provider mirror (Preparedetects this by statting the image’s providers directory;Environitself never inspects the filesystem).- Every other
TF_*andKUBE_*variable is dropped, exceptTF_IN_AUTOMATION,TF_INPUTandKUBE_NAMESPACE, which the Job already sets and which never carry identity-Secret or image values because a container’s explicitenvwins overenvFrom.
spec.jobs.env rejected names
spec.jobs.env rejects (drops, with an event) any name starting with TF_ or KUBE_: the runner and the Job above own that namespace (internal/jobs.reservedEnv).
See the image contract for the image side of this: the fixed paths the runner expects, and what the image must not ship.