The .gitkeep That Broke the Cluster: A Case Study in Ansible Deployment Debugging

Introduction

In the trenches of infrastructure-as-code development, the most frustrating bugs often come from the smallest, most innocuous sources. A single dotfile. A misplaced export keyword. A log level format string that looks right but isn't. This article examines a single message from an extended debugging session in which an engineer (the assistant) was building and iteratively fixing Ansible deployment scripts for a distributed Filecoin Gateway (FGW) cluster. The target message — message index 1590 — captures a moment of insight: the realization that a .gitkeep file, left behind by version control as a directory placeholder, was being copied into production-like environments and causing the Kuri storage node binary to crash.

The Message

The subject message reads in full:

[assistant] I see - the real wallet directory only has .gitkeep. The test creates dummy wallet files, but the .gitkeep is being copied too and causing issues. Let me check what the test setup does: [read] /home/theuser/gw/ansible/test/setup.sh

This is followed by a file read of the test setup script. The message is short — barely a paragraph — but it represents the culmination of a chain of diagnostic reasoning that had been building over the previous dozen messages.

The Context: A Deployment Pipeline Under Construction

To understand why this message was written, we need to step back and look at the broader context. The assistant had been building an Ansible-based deployment system for Filecoin Gateway (FGW) clusters, a horizontally scalable S3 storage architecture. The system involved multiple roles: common for base setup, wallet for distributing cryptographic wallet files, yugabyte_init for database initialization, kuri for storage node deployment, and s3_frontend for stateless S3 proxy servers.

A Docker-based test harness had been created to validate these playbooks before running them against real infrastructure. The test harness used Docker containers with systemd and SSH, simulating target nodes. The test flow was: connectivity check → YugabyteDB initialization → Kuri node deployment → S3 frontend deployment → verification.

The session had already encountered and fixed several issues. The settings.env template had export prefixes that systemd's EnvironmentFile directive couldn't parse. The log level format used *:*=debug which was invalid regex. The database initialization was creating tables that conflicted with the Kuri application's own migration logic. Each fix had required rebuilding the Docker images or resetting state on the test containers.

By message 1589, the assistant had identified two remaining problems from a test run: an invalid log level format and a wallet .gitkeep issue. The log level problem was that *:*=debug needed to be .*:.*=debug (proper regex). The wallet issue was more subtle.

The Diagnostic Chain: How We Got Here

The assistant had run the deploy-kuri.yml playbook against the kuri-01 test container and observed failures. The journal logs showed that kuri daemon was crashing with exit code 1. The wallet role had copied files to /home/fgw/.ribswallet/, but the Kuri binary was trying to parse every file in that directory as a wallet key — including .gitkeep, a hidden file used by Git to track empty directories.

The assistant's reasoning, visible in the preceding messages, went like this:

  1. Observation: The kuri init command fails with errors about wallet parsing.
  2. Hypothesis: The wallet directory contains files that aren't valid wallet keys.
  3. Investigation: The assistant checks the wallet source directory (ls -la /home/theuser/gw/ansible/files/wallet/) and finds only a .gitkeep file there — 218 bytes, created by Git.
  4. Cross-reference: The test setup script copies both /ansible-src/* (which includes files/wallet/.gitkeep) and /test-wallet/* (which has actual wallet files) into the target container's /ansible/files/wallet/ directory.
  5. Confirmation: The assistant checks the container and finds both .gitkeep and the real wallet files present. The subject message is the moment this connection crystallizes: "I see — the real wallet directory only has .gitkeep. The test creates dummy wallet files, but the .gitkeep is being copied too and causing issues."

Assumptions Made

Several assumptions underpin this debugging session, some correct and some not:

Correct assumptions:

Mistakes and Incorrect Assumptions

The most significant mistake was architectural rather than tactical. The wallet directory in the source tree (ansible/files/wallet/) was never meant to contain real wallet files in a test scenario — the test harness provides its own wallet files via the test-wallet/ directory. But the setup script's dual-copy approach meant that the .gitkeep from the source tree's wallet directory leaked into the test environment.

A secondary mistake was not anticipating that the Kuri binary would be strict about file formats in the wallet directory. The .gitkeep file, being 218 bytes of arbitrary content, was being parsed as a wallet key and failing. This is actually reasonable behavior from the Kuri binary — a wallet directory should only contain valid key files — but it meant the deployment pipeline needed to be more careful about what it placed there.

The log level format issue (*:*=debug vs .*:.*=debug) was a separate but related mistake. The assistant had used * as a wildcard in the log level pattern, but the underlying regex parser required .* (dot-star) syntax. This is a classic configuration error where a user-facing wildcard syntax doesn't match the implementation's regex expectations.

Input Knowledge Required

To understand this message fully, one needs:

  1. Knowledge of Git conventions: .gitkeep is a widely-used convention for tracking empty directories in Git (which doesn't track directories themselves). Without knowing this, the presence of a .gitkeep file in a wallet directory would be mysterious.
  2. Understanding of Ansible deployment patterns: The message references Ansible roles, playbooks, inventory files, and the test harness structure. The reader needs to know that Ansible copies files from a control machine to target nodes, and that the wallet role is responsible for distributing cryptographic material.
  3. Awareness of the Kuri binary's behavior: The Kuri storage node reads all files in ~/.ribswallet/ as potential wallet keys. This is a design choice — some systems use file naming conventions or extensions to filter, but Kuri appears to enumerate the directory and attempt to parse each file.
  4. Docker Compose and test harness architecture: The test environment uses multiple Docker containers (YugabyteDB, two Kuri nodes, one S3 frontend, and an Ansible controller) connected via a custom network. The setup script orchestrates the provisioning of these containers.
  5. Systemd integration details: Earlier in the session, the assistant had discovered that systemd's EnvironmentFile directive doesn't support export prefixes in environment files — a subtle but critical detail for anyone deploying applications via systemd.

Output Knowledge Created

This message and its surrounding session produced several valuable pieces of knowledge:

  1. A hardened Ansible wallet role: The fix involved adding a cleanup step to remove .gitkeep files from the wallet directory after copying. This pattern — copy, then sanitize — is a useful idiom for deployment scripts that need to exclude hidden files.
  2. A reproducible debugging methodology: The assistant's approach — observe failure, form hypothesis, inspect source, trace the provisioning pipeline, confirm in the target environment — is a model for infrastructure debugging. Each step is documented with shell commands and their outputs.
  3. Documentation of systemd's environment file format: The discovery that export prefixes break systemd's EnvironmentFile parsing is a piece of operational knowledge that would save future engineers hours of debugging.
  4. Log level format documentation: The correct regex syntax for the application's log level configuration (.*:.*=debug instead of *:*=debug) is now captured in both the test inventory and production defaults.
  5. A validated test harness: By the end of the session, the Docker-based test harness could successfully run the full deployment pipeline end-to-end, providing a repeatable validation mechanism for future changes.

The Thinking Process

What's most interesting about this message is what it reveals about the assistant's thinking process. The message begins with "I see" — a phrase that signals a gestalt shift, a moment where previously disconnected observations snap into coherence.

The chain of reasoning visible in the surrounding messages shows a methodical approach:

  1. Observe the symptom: The Kuri service is crashing. Journal logs show it's trying to parse wallet files.
  2. Inspect the target: Check what files actually exist in the wallet directory on the target node.
  3. Trace backward: Look at the source wallet directory in the repository.
  4. Trace the provisioning path: Read the test setup script to understand how files flow from source to target.
  5. Confirm the hypothesis: Verify that both .gitkeep and real wallet files coexist in the target container.
  6. Formulate the fix: Modify the wallet role to exclude .gitkeep (or delete it after copying). This is textbook root cause analysis. The assistant doesn't stop at the surface-level symptom ("wallet parsing error") but digs into the provisioning pipeline to find where the contamination enters.

Broader Significance

The .gitkeep bug is a classic example of a class of infrastructure problems: environment leakage. Files, environment variables, or configuration intended for one context (development, version control) leak into another context (deployment, production) and cause failures. The .gitkeep file is harmless in a Git repository but catastrophic in a wallet directory.

This pattern appears everywhere in infrastructure engineering: .env.example files being loaded as actual configuration, .gitignore patterns causing files to be unexpectedly absent, README files being parsed by configuration loaders. The solution is always the same: be explicit about what gets deployed, and sanitize your artifacts.

The session also demonstrates the value of a test harness. Without the Docker-based test environment, these bugs would have been discovered in production — or worse, during a critical deployment. The test harness caught the .gitkeep issue, the export prefix issue, the log level format issue, and the database initialization conflict before any real infrastructure was affected.

Conclusion

Message 1590 is a small but pivotal moment in a larger debugging narrative. It captures the instant when an engineer connects a symptom to its cause, tracing a .gitkeep file from a Git repository through a test harness and into a production-like environment where it breaks a distributed storage node. The message is a testament to the value of methodical debugging, the importance of test harnesses, and the surprising ways that small artifacts can cause big problems in infrastructure-as-code systems.