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#
- 2026-06-22 14:02 KST —
Reviewer모델 변경에 따른SavePartialJsonToFileWorker실행,/var/data_changes/reviewers/partial/2026-06-22/euce1쓰기 시도 →Errno::EACCES최초 발생 (first_seen). - 2026-06-22 14:02 ~ 16:06 KST — 동일 디렉터리에 대한 쓰기 실패가 37회 반복 (
occurrence_count: 37). - 2026-06-22 16:06 KST — 가장 최근 발생 (
last_seen). 디렉터리 소유권/권한이 갱신되지 않는 한 다음 일자 디렉터리(2026-06-23/...)에서도 재현 가능성 있음. - (연관) 이 클러스터는 status-board의 active 인시던트
2026-06-22-svc-cupixworks-worker-2에 묶여 있으나, 본 root cause는 해당 서비스 인시던트와 독립적인 파일 시스템 권한 문제로 확인됨.
Error Log#
Directory not writable before file write attempt
대표 attributes (Datadog log id AwAAAZ7uJ9FQ9goiQQ...):
{
"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.
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_file → generate_and_save_partial_json → save_partial_json_file. Directory 생성과 writability 체크는 다음 위치에서 이루어진다.
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를 하지 않는다.
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만 보정한다.
# 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은 최상위만 수행한다.
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.sh는 chown -R을 시도하지만, 마운트포인트가 이미 마운트되어 있으면(10번 라인) early-return하여 chown도 건너뛴다. 따라서 이전에 root 권한으로 만들어진 하위 디렉터리는 그대로 남는다.
Failure point: app/models/concerns/data_ware_house/partial_json.rb:194 — raise 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 쿼리:
service:cupixworks-worker "Directory not writable before file write attempt"
추가 컨텍스트 쿼리(같은 worker의 EACCES 전체):
service:cupixworks-worker "/var/data_changes" status:error
대표 후속 에러 로그 (같은 worker, 같은 시각):
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=0 ≠ process_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으로 회수한다.
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:33의chown webapp:webapp /var/data_changes를chown -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-29의create_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 전체):
service:cupixworks-worker status:error "Directory not writable before file write attempt"
partial JSON write 실패 (rescue 후 후속 로그):
service:cupixworks-worker status:error "Failed to save Partial JSON"
리전별 분포 추적용:
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줄 수정으로 충분.