ES /docs

Directory not writable before file write attempt

RCA: Directory not writable before file write attempt

Overview#

What Happened#

2026-06-22 14:02 KST부터 약 2시간 동안 eu-central-1 리전의 cupixworks-worker(Sidekiq) 인스턴스에서 SavePartialJsonToFileWorker/var/data_changes/reviewers/partial/2026-06-22/euce1 디렉터리에 partial JSON gz 파일을 쓰지 못해 Errno::EACCES가 37건 기록되었다. 디렉터리 소유자는 root:root(uid 0/gid 0)인데 worker 프로세스는 webapp(uid 900)으로 실행되어, 0755 권한 하에서 write 권한이 없는 상태였다. 결과적으로 CDC(Change Data Capture)용 partial JSON이 디스크에 기록되지 않아 데이터 레이크 동기화에서 누락된다.

Quick Facts#

Field Value
exception.class Errno::EACCES
exception.message Permission denied - Directory /var/data_changes/reviewers/partial/2026-06-22/euce1 is not writable (permissions: 755)
top_frame app/models/concerns/data_ware_house/partial_json.rb:187
runtime Sidekiq 7.3.9 / Ruby 3.3.0 / Rails 7.2.2
deploy production-eu-central-1-20260618T0620Z0-0938bde6-cupixworks
env production / eu-central-1 (host ip-10-1-80-125.eu-central-1.compute.internal)

Affected Teams#

Team / Domain Error Count Impact
DataWareHouse / CDC (Reviewer model) 37 partial JSON이 디스크에 저장되지 않아 sync_datalake.sh가 S3 데이터 레이크에 업로드하지 못함. Reviewer 모델 변경분이 데이터 레이크에서 누락됨.

Timeline#

  1. 2026-06-22 14:02 KSTReviewer 모델 변경에 따른 SavePartialJsonToFileWorker 실행, /var/data_changes/reviewers/partial/2026-06-22/euce1 쓰기 시도 → Errno::EACCES 최초 발생 (first_seen).
  2. 2026-06-22 14:02 ~ 16:06 KST — 동일 디렉터리에 대한 쓰기 실패가 37회 반복 (occurrence_count: 37).
  3. 2026-06-22 16:06 KST — 가장 최근 발생 (last_seen). 디렉터리 소유권/권한이 갱신되지 않는 한 다음 일자 디렉터리(2026-06-23/...)에서도 재현 가능성 있음.
  4. (연관) 이 클러스터는 status-board의 active 인시던트 2026-06-22-svc-cupixworks-worker-2에 묶여 있으나, 본 root cause는 해당 서비스 인시던트와 독립적인 파일 시스템 권한 문제로 확인됨.

Error Log#

Datadog Logs

text
Directory not writable before file write attempt

대표 attributes (Datadog log id AwAAAZ7uJ9FQ9goiQQ...):

datadog log attributesjson
{
  "message": "Directory not writable before file write attempt",
  "class": "Reviewer",
  "module": "DataWareHouse",
  "function": "save_partial_json_file",
  "directory": "/var/data_changes/reviewers/partial/2026-06-22/euce1",
  "permissions": "755",
  "uid": 0,
  "gid": 0,
  "process_uid": 900,
  "process_euid": 900,
  "environment": "production",
  "service": "cupixworks-worker",
  "host": "ip-10-1-80-125.eu-central-1.compute.internal"
}

Impact#

  • Service: cupixworks-worker
  • 발생 횟수: 37
  • 최초 발생: 2026-06-22 14:02 KST
  • 최근 발생: 2026-06-22 16:06 KST

Root Cause Summary#

PartialJson concern은 /var/data_changes/{model}/partial/{date}/{region}/ 하위 경로에 partial JSON gz 파일을 기록한다. 디렉터리가 없으면 FileUtils.mkdir_p로 생성하는데, eu-central-1의 해당 호스트(ip-10-1-80-125)에서는 날짜/리전 하위 디렉터리(reviewers/partial/2026-06-22/euce1)가 root:root 소유, 0755 모드로 존재한다. Sidekiq worker 프로세스는 webapp(uid 900)로 실행되므로 0755 + 다른 소유자 조합에서는 write 권한이 없다 → File.writable?(dirname) 체크가 false를 반환하고 Errno::EACCES가 raise → save_partial_json_file의 rescue 블록이 에러 로그를 남긴다. TSLA-12288(2026-03-26)에서 추가한 chmod 755 hook은 최상위 /var/data_changes 대상으로 하므로, 이후 root 권한 컨텍스트(예: postdeploy hook, 마운트 직후 mkfs/format 흐름, 또는 root cron)에서 만들어진 하위 디렉터리의 소유권 문제는 해결되지 않았다.

Technical Analysis#

Code Path#

Entry point: Sidekiq dispatches SavePartialJsonToFileWorker#perform.

app/workers/save_partial_json_to_file_worker.rb:62-66ruby
begin
  model.save_partial_json_to_file(all_data: all_data, operation: operation, changes: changes, timestamp: timestamp)
rescue NoMethodError, Errno::ENOSPC, Errno::EACCES => e
  Cupix::Logger.error(e.message, class: self.class, function: 'perform', error_class: e.class.name)
end

save_partial_json_to_filegenerate_and_save_partial_jsonsave_partial_json_file. Directory 생성과 writability 체크는 다음 위치에서 이루어진다.

app/models/concerns/data_ware_house/partial_json.rb:174-197ruby
def save_partial_json_file(content, path)
  filepath = Rails.env.development? ? "./tmp/storage/#{path}" : "/var/data_changes/#{path}"
  create_directory_if_not_exists(filepath)

  # Verify directory is writable before attempting file write
  dirname = File.dirname(filepath)
  unless File.writable?(dirname)
    dir_stat = File.stat(dirname)
    Cupix::Logger.error(
      'Directory not writable before file write attempt',
      class: self.class.name,
      module: 'DataWareHouse',
      function: 'save_partial_json_file',
      directory: dirname,
      permissions: format('%o', dir_stat.mode & 0o777),
      uid: dir_stat.uid,
      gid: dir_stat.gid,
      process_uid: Process.uid,
      process_euid: Process.euid
    )
    raise Errno::EACCES, "Directory #{dirname} is not writable (permissions: #{format('%o', dir_stat.mode & 0o777)})"
  end

  File.open(filepath, 'wb') { |file| file.write(content) }

create_directory_if_not_exists는 단순히 mkdir_p만 호출하고 chown/chmod를 하지 않는다.

app/models/concerns/data_ware_house/base.rb:26-29ruby
def create_directory_if_not_exists(filepath)
  dirname = File.dirname(filepath)
  FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
end

deploy hook는 최상위 /var/data_changes만 보정한다.

.platform/hooks/postdeploy/91_prepare_cdc.sh:28-34bash
# Create the directory if it doesn't exist
if [ ! -d /var/data_changes ]; then
  mkdir -p /var/data_changes
fi

chown webapp:webapp /var/data_changes
chmod 755 /var/data_changes

worker 인스턴스는 추가로 EBS 볼륨을 마운트하는데, 마운트 직후 chown은 최상위만 수행한다.

.platform/hooks/postdeploy/81_mount_volume.sh:10-34bash
if mountpoint -q /var/data_changes; then
  echo "EBS volume is already mounted at /var/data_changes"
  exit 0
fi
# ... (mkfs/format if needed) ...
mount /dev/xvdf /var/data_changes

if [ $? -eq 0 ]; then
  echo "EBS volume successfully mounted at /var/data_changes"

  chown -R webapp:webapp /var/data_changes
  chmod 755 /var/data_changes

81_mount_volume.shchown -R을 시도하지만, 마운트포인트가 이미 마운트되어 있으면(10번 라인) early-return하여 chown도 건너뛴다. 따라서 이전에 root 권한으로 만들어진 하위 디렉터리는 그대로 남는다.

Failure point: app/models/concerns/data_ware_house/partial_json.rb:194raise Errno::EACCES. 실제 write 호출(File.open ... 'wb')에 도달하지 못한다.

기대 동작: webapp 사용자(uid 900)가 partial JSON gz 파일을 디렉터리에 기록한다. 실제 동작: 디렉터리가 root:root 0755로 생성되어 있어 File.writable?가 false → 에러 로그 + Errno::EACCES raise → worker rescue 블록에서 또 한번 에러 로그 → 파일은 끝내 생성되지 않음.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-worker "Directory not writable before file write attempt"

추가 컨텍스트 쿼리(같은 worker의 EACCES 전체):

text
service:cupixworks-worker "/var/data_changes" status:error

대표 후속 에러 로그 (같은 worker, 같은 시각):

text
Failed to save Partial JSON Reviewer 66906 to file
/var/data_changes/reviewers/partial/2026-06-22/euce1/1782112045909153-reviewers-66906.json.gz
error.msg: "Permission denied - Directory /var/data_changes/reviewers/partial/2026-06-22/euce1
            is not writable (permissions: 755)"
stack[0]:  app/models/concerns/data_ware_house/partial_json.rb:187:in `save_partial_json_file'
stack[1]:  app/models/concerns/data_ware_house/partial_json.rb:164:in `generate_and_save_partial_json'
stack[2]:  app/models/concerns/data_ware_house/partial_json.rb:70:in `save_partial_json_to_file'
stack[3]:  app/workers/save_partial_json_to_file_worker.rb:63:in `perform'

핵심 증거 요약 (Datadog log attribute):

Attribute Value 의미
directory /var/data_changes/reviewers/partial/2026-06-22/euce1 실패 대상 디렉터리
uid / gid 0 / 0 디렉터리 소유자 = root
permissions 755 other는 read+execute, write 없음
process_uid / process_euid 900 / 900 worker 프로세스 = webapp
host ip-10-1-80-125.eu-central-1.compute.internal EBS 볼륨이 마운트된 worker 호스트

uid=0process_uid=900이고 permissions=755이므로 worker는 해당 디렉터리에 write가 불가능하다 — 이것이 root cause의 단정적 근거이다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 날짜/리전 하위 디렉터리가 root:root 0755로 생성되어 webapp worker가 write 불가 Datadog 로그의 uid=0 gid=0 permissions=755 process_uid=900, 코드상 create_directory_if_not_exists는 chown/chmod 미수행, postdeploy chmod는 최상위에만 적용 Confirmed
H2 외부 의존성(EBS, S3) 장애 status-board가 동일 서비스 인시던트 그룹에 묶음 에러 메시지는 명백한 로컬 파일시스템 EACCES, directory가 마운트된 EBS 경로이며 다른 디렉터리 작업(상위 mkdir)은 성공한 것으로 추정됨. 외부 dep 인시던트 스코프가 아님 Rejected
H3 디스크 풀 (ENOSPC) rescue 절이 ENOSPC도 처리 에러 메시지가 Permission denied이며 permissions=755가 함께 기록됨 (ENOSPC 메시지와 무관) Rejected
H4 TSLA-12288의 기존 chmod 755 hook 미배포 deploy version production-eu-central-1-20260618T0620Z0에 hook이 포함되어 있고 worker가 정상 부팅됨 (다른 partial json 디렉터리는 정상). 문제는 hook이 최상위만 보정한다는 점이며 hook 자체는 동작 중 Rejected
H5 delete_old_data_changes.sh가 root 권한으로 디렉터리 재생성 스크립트가 rm -rf으로 디렉터리를 지움. 이후 worker가 mkdir_p할 때 root 컨텍스트라면 root 소유로 생성됨 직접적인 증거는 부족 — uncertain -- needs verification (디렉터리 생성 시각과 worker 프로세스 컨텍스트의 정확한 추적 필요) Inconclusive

Fix Recommendation#

즉시 조치 (Critical)#

운영 조치 (코드 변경 불필요): 영향받은 호스트(ip-10-1-80-125.eu-central-1.compute.internal)에 SSH 접속 후 아래 명령으로 소유권을 복구한다. 디스크의 기존 root 소유 하위 디렉터리를 한 번에 webapp으로 회수한다.

text
sudo chown -R webapp:webapp /var/data_changes
sudo find /var/data_changes -type d -exec chmod 755 {} \;

실행 후 partial json 디렉터리(예: /var/data_changes/reviewers/partial/2026-06-22/euce1)의 stat 결과가 webapp webapp으로 바뀌었는지 확인한다. 이 조치만으로도 현재 발생 중인 37건 에러는 즉시 멈춘다.

단기 개선 (1주 이내)#

  • .platform/hooks/postdeploy/91_prepare_cdc.sh:33chown webapp:webapp /var/data_changeschown -R webapp:webapp /var/data_changes로 변경 (재귀). 매 배포 시 누적된 root 소유 하위 디렉터리도 함께 복구된다.

    .platform/hooks/postdeploy/91_prepare_cdc.sh:33
    -chown webapp:webapp /var/data_changes+chown -R webapp:webapp /var/data_changes chmod 755 /var/data_changes
  • .platform/hooks/postdeploy/81_mount_volume.sh:10-13의 "이미 마운트됨" early-return 분기에서도 chown -R webapp:webapp /var/data_changes를 한 번 실행하도록 변경해, 이미 마운트된 케이스에서 누락된 하위 디렉터리 소유권을 매 배포마다 회복시킨다.

  • app/models/concerns/data_ware_house/base.rb:26-29create_directory_if_not_exists에 모드 명시(FileUtils.mkdir_p(dir, mode: 0o755))를 검토. 다만 mode만으로는 소유자 문제를 해결할 수 없으므로 핵심은 hook측 chown 보장이다.

장기 개선 (재발 방지)#

  • root 권한으로 /var/data_changes 하위에 디렉터리를 만드는 모든 흐름을 식별·제거한다. 후보: cron으로 도는 delete_old_data_changes.sh의 실행 사용자, fswatch / sync_datalake 관련 hook, EBS 마운트 직후 mkfs 흐름. 각 hook이 root로 실행될 때는 끝에 chown -R webapp:webapp /var/data_changes를 일관되게 호출하는 컨벤션을 적용한다.
  • 무권한 디렉터리 발견 즉시 알람이 울리도록 아래 Monitoring 섹션의 쿼리를 Datadog Monitor로 등록한다.

Monitoring#

Errno::EACCES 발생 추이 (production 전체):

text
service:cupixworks-worker status:error "Directory not writable before file write attempt"

partial JSON write 실패 (rescue 후 후속 로그):

text
service:cupixworks-worker status:error "Failed to save Partial JSON"

리전별 분포 추적용:

text
service:cupixworks-worker status:error "Directory not writable before file write attempt" region:eu-central-1

알람 임계: 5분 윈도우 내 발생 횟수 > 5면 P3 알림. 발생 시 영향 호스트의 /var/data_changes 권한을 즉시 점검한다.

Risk Assessment#

  • Risk level: medium. 데이터 손실은 발생하지 않지만(Reviewer 변경분이 RDS에는 정상 반영), 데이터 레이크 동기화에서 누락이 발생하여 분석/BI 정확성에 영향. 매 일자별 디렉터리 진입 시(자정 KST 또는 UTC) 재발 가능성이 있어 만성화 위험.
  • 예상 복잡도: standard. 즉시 조치는 한 줄의 chown 명령. 코드 변경은 hook 1~2줄 수정으로 충분.