The Export Paradox: Reconciling systemd and Bash Environment File Formats in Ansible Deployment

The Message

In a debugging session for Filecoin Gateway (FGW) cluster deployment via Ansible, the assistant wrote the following message (index 1582):

The problem is that: 1. systemd needs plain KEY=VALUE format (no export) 2. But when we source the file in bash for kuri init, we need export to actually export the vars

>

Let me update the init task to use export explicitly when sourcing: [edit] /home/theuser/gw/ansible/roles/kuri/tasks/main.yml Edit applied successfully.

This short message, appearing unremarkable at first glance, captures a pivotal moment in a larger debugging odyssey. It represents the synthesis of two conflicting constraints that had been causing a cascade of failures in the Ansible deployment pipeline, and it demonstrates a kind of systems thinking that separates robust infrastructure code from brittle scripts.

The Context: A Deployment Pipeline Under Siege

To understand why this message was written, we must reconstruct the debugging hell that preceded it. The assistant had been building Ansible playbooks to deploy a distributed Filecoin Gateway (FGW) cluster with two types of nodes: Kuri storage nodes (which manage IPFS data and Filecoin deals) and S3 frontend proxies (stateless API servers). A Docker-based test harness had been constructed to validate these playbooks before production use.

The test harness was failing repeatedly. The kuri init command — which initializes a Kuri node's IPFS repository and database schema — was crashing because it couldn't connect to the database. But the database did exist. The real problem was subtler: the kuri daemon process, started by systemd, was failing because it couldn't read its configuration.

When the assistant inspected the systemd journal (message 1578), the root cause became visible:

Jan 31 17:46:46 kuri-01 systemd[1]: kuri-kuri-01.service: Ignoring invalid environment assignment 'export RIBS_MINIMUM_RETRIEVABLE_COUNT=1': /data/fgw/kuri-01/config/settings.env

Systemd's EnvironmentFile directive does not support the export keyword. It expects plain KEY=VALUE lines. Every line in the generated settings.env began with export, so systemd silently ignored every single environment variable. The Kuri daemon started with no configuration, defaulting to paths like /home/fgw/.ipfs instead of the intended /data/fgw/kuri-01/ipfs, and promptly crashed.

The First Fix: Removing Export

The assistant's immediate response (message 1580) was to rewrite the Jinja2 template settings.env.j2, stripping every export prefix from every line. This was the obvious fix: systemd needs plain assignments, so give systemd plain assignments. The template went from lines like:

export FGW_NODE_ID="kuri-01"
export RIBS_DATA_DIR="/data/fgw/kuri-01/ribs"

to:

FGW_NODE_ID="kuri-01"
RIBS_DATA_DIR="/data/fgw/kuri-01/ribs"

This would make systemd happy. Problem solved, right?

The Second Problem: The Init Command

Not quite. The kuri init command — which must run before the systemd service starts, to initialize the IPFS repository and database schema — was executed via an Ansible task that used a shell command. The relevant task in roles/kuri/tasks/main.yml looked something like:

- name: Initialize Kuri node
  ansible.builtin.shell: |
    source {{ fgw_config_dir }}/settings.env
    kuri init

When bash's source command reads a file, it executes each line as if it were typed at the shell prompt. A line like FGW_NODE_ID="kuri-01" sets a shell variable, but it does not create an environment variable. Only lines prefixed with export cause variables to be inherited by child processes. Since kuri init is a child process of the shell, it would see none of the configuration variables.

This is the crux of the paradox. The same file must serve two masters:

  1. systemd's EnvironmentFile: Requires plain KEY=VALUE format, rejects export.
  2. bash's source command: Requires export prefix for variables to propagate to child processes. You cannot satisfy both with a single file format — unless you change how the file is consumed.

The Synthesis: Explicit Export in the Task

Message 1582 is where the assistant recognized this tension and formulated a solution. The reasoning is concise but complete:

The problem is that: 1. systemd needs plain KEY=VALUE format (no export) 2. But when we source the file in bash for kuri init, we need export to actually export the vars

>

Let me update the init task to use export explicitly when sourcing.

The solution is elegant: keep the settings.env file in the format systemd requires (plain KEY=VALUE), but modify the Ansible task that runs kuri init to explicitly export the variables when sourcing. Instead of:

source /data/fgw/kuri-01/config/settings.env
kuri init

The task would be changed to something like:

set -a
source /data/fgw/kuri-01/config/settings.env
set +a
kuri init

The set -a (or set -allexport) flag tells bash to automatically export any variable that is assigned or modified. When source reads the file under set -a, each KEY=VALUE assignment automatically becomes an environment variable, even without the export keyword. After sourcing, set +a disables this behavior. The child process kuri init inherits all the configuration variables as intended.

This is a textbook example of working with the tools rather than against them. The assistant could have chosen other paths: generating two separate files (one for systemd, one for bash), or adding export back to the template and stripping it in the systemd service definition. But those approaches introduce duplication and maintenance burden. The set -a approach keeps a single source of truth — one settings.env file — and adapts the consumption method to each consumer's needs.

Assumptions and Knowledge Required

To fully understand this message, one needs:

  1. Knowledge of systemd's EnvironmentFile semantics: systemd's parser for environment files is intentionally simple. It does not support shell syntax like export, variable expansion, or command substitution. Each line must be KEY=VALUE or KEY="VALUE" (with optional quoting). This is documented but easy to miss when coming from a shell scripting background.
  2. Knowledge of bash's source behavior: The source command (equivalent to .) reads a file and executes its contents in the current shell. Without export, variables set by source are local to the shell and not passed to child processes. This is a fundamental shell concept but one that often trips up developers who assume that setting a variable in a sourced file is equivalent to exporting it.
  3. Knowledge of the set -a / set -allexport option: This is a less commonly used bash feature. It tells the shell to mark every new or modified variable for export automatically. It's the shell equivalent of putting export before every assignment, but applied dynamically.
  4. Understanding of the Ansible deployment pipeline: The message references "the init task" and "sourcing" in the context of Ansible's shell module. The reader must understand that Ansible runs shell commands on remote hosts, and that the environment of those shell commands determines what the launched processes see.
  5. Awareness of the debugging trail: The message is the culmination of a chain of discoveries: the test harness failing, the systemd journal revealing "Ignoring invalid environment assignment," the template inspection showing export prefixes, and the realization that fixing the template for systemd would break the init task.

What This Message Creates

The output of this message is not just an edit to a YAML file. It creates:

Mistakes and Incorrect Assumptions

The message itself does not contain mistakes — it correctly identifies the problem and proposes the right solution. However, the situation that necessitated this message arose from an earlier incorrect assumption: that a single file format could serve both systemd and bash without adaptation. The original template used export everywhere, which worked for bash but not for systemd. The quick fix of removing export worked for systemd but broke bash. The assumption that "one format fits all" was the real error, and message 1582 corrects it by introducing format-specific consumption logic.

Another implicit assumption worth noting: the assistant initially assumed that kuri init would be run after the systemd service was installed, or that the environment file would be loaded by the service itself. In reality, init must happen before the service starts, and it needs the same configuration. This temporal ordering — init before service, service after init — is a common pattern in stateful service deployment, and the environment file must be accessible to both phases.

The Thinking Process

The reasoning visible in this message is compressed but rich. The assistant states "The problem is that..." and enumerates two points. This is not a guess; it's the synthesis of evidence gathered over multiple previous messages:

  1. Observation: systemd journal shows "Ignoring invalid environment assignment 'export ...'" (message 1578).
  2. Hypothesis: The export prefix is the cause of systemd's failure to load the environment.
  3. Action: Remove export from the template (message 1580).
  4. New observation: But the init task uses source, which needs export for variables to propagate.
  5. Synthesis: The two consumers have incompatible requirements. The solution is not to change the file again, but to change how the file is consumed by the init task. This is classic debugging: gather evidence, form hypotheses, test them, and when a fix creates a new problem, step back and analyze the deeper constraint conflict rather than applying another local fix.

Broader Significance

This message, though small, illustrates a fundamental challenge in infrastructure-as-code: the impedance mismatch between declarative configuration (systemd's EnvironmentFile) and imperative setup (bash shell commands). Modern deployment systems like Ansible, Terraform, and Kubernetes constantly navigate such boundaries. The solution here — using set -a to bridge the gap — is a small but satisfying example of how understanding the semantics of each tool, not just its syntax, leads to robust designs.

The message also demonstrates the value of test harnesses. Without the Docker-based test environment, this bug would likely have been discovered only in production, where debugging systemd environment issues is significantly harder. The test harness forced the issue into the open, where it could be diagnosed and fixed systematically.

In the end, message 1582 is about recognizing that when two systems disagree on format, the file is not the right place to compromise. The right place is in the interface — the consumption logic — where each system can be given what it expects without the other system having to change.