AwsS3Manager::uploadDirectoryByCredential | upload failed - {"message":"We encountered an internal e
RCA: AwsS3Manager::uploadDirectoryByCredential upload failed (S3 InternalError 500)
Overview#
What Happened#
2026-06-24 14:00 KST에 cupixworks-pano-postprocessor-instance 에이전트가 리사이즈된 pano 타일을 S3에 업로드하는 중 AWS S3로부터 HTTP 500 InternalError 응답을 받아 업로드가 실패했다. AwsS3Manager.uploadDirectoryByCredential 의 내부 try/catch가 예외를 삼키기 때문에 상위 워크플로우는 실패를 인지하지 못하고 pano를 정상 완료로 처리한다. 24시간 윈도우 안에서 동일 패턴이 여러 capture에서 반복됐다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | S3 InternalError (statusCode 500, retryable:true) |
| exception.message | We encountered an internal error. Please try again. |
| top_frame | applications/agents/packages/cupix-pano-postprocessor/src/manager/aws-s3.manager.ts:130-139 |
| runtime | Node.js (aws-sdk v2, s3.upload(...).promise()) |
| env | production, region us-west-2, tenant cupix |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| gad (pano-postprocessor / 360 사진 파이프라인) | 본 클러스터 2건 (24h 동일 패턴 다수) | 일부 pano 타일이 S3에 누락된 채 pano-postprocess가 Done으로 종료. 프런트엔드에서 해당 pano 타일 로드 실패 가능 |
Timeline#
- 2026-06-24 14:00:06 KST — 첫 발생: requestId
JNN2Q5KZVFHY5KNW, capture postprocess 중 S3 500 InternalError 수신 (clusterfirst_seen). - 2026-06-24 14:00:06 KST — 동일 cluster 두 번째 occurrence (
occurrence_count: 2). - 2026-06-24 16:15:13 KST — 동일 에러 메시지 재발 (requestId
31BRNCNT2J71NNMA). - 2026-06-24 14:11:50 KST — 같은 서비스에서 별도 cluster
4b46df80…(MaskWork::maskPanos) 발생 → status board 가cupixworks-pano-postprocessor-instance service degraded인시던트로 묶음 (2026-06-24-svc-cupixworks-pano-postprocessor-instance--unknown-1). - 분석 시점 (2026-06-24) — RCA 작성. 외부 의존성 (AWS S3) 측 일시 장애로 추정되며, 코드 경로 상의 silent-failure 가 영향 확대 요인.
Error Log#
AwsS3Manager::uploadDirectoryByCredential | upload failed - {"message":"We encountered an internal error. Please try again.","code":"InternalError","region":null,"time":"2026-06-24T05:00:06.167Z","requestId":"JNN2Q5KZVFHY5KNW","extendedRequestId":"ge1RNMTLGzRfu5MBQwP7ICDBJa3lhaYiGae4GmqVfTMbTOTmj8WVM9spXR3h48Ce5MDAuv68xGBo3m2d8Sdbc3fu+CsXQuaS","statusCode":500,"retryable":true}
Impact#
- Service:
cupixworks-pano-postprocessor-instance - Team: gad
- 발생 횟수: 2
- 최초 발생: 2026-06-24 14:00:06 KST
- 최근 발생: 2026-06-24 14:00:06 KST
추가로 동일 시그니처가 24시간 동안 us-west-2 production 에서 반복 관측됨 (예: 14:00, 16:15 KST 등 서로 다른 requestId / extendedRequestId).
Root Cause Summary#
AWS S3 가 s3.upload 요청에 대해 HTTP 500 InternalError (retryable: true)를 일시적으로 반환했고, AwsS3Manager.uploadDirectoryByCredential 은 청크 단위 업로드 안에서 예외를 잡아 로그만 남긴 뒤 그대로 진행한다. 호출자 ResizeWork.uploadResizedImage 와 상위 PanoPostprocessorService.run 의 resizeTasks try/catch 어디에도 예외가 전달되지 않으므로, 일부 pano 타일이 S3 에 올라가지 않은 상태에서 pano 와 capture 가 정상 완료(Done)로 마킹된다. 즉 1차 root cause 는 AWS 측 transient 5xx 이지만, 영향이 사용자 데이터 누락으로 확대되는 이유는 코드 상의 silent-swallow + 재시도 부재 라는 핸들링 결함이다.
Technical Analysis#
Code Path#
- Entry point:
applications/agents/packages/cupix-pano-postprocessor/src/pano-postprocessor-service.ts:165-178— capture 의 각 pano 에 대해 resize → upload → checkStitched 순서로 실행. - Upload 진입점:
applications/agents/packages/cupix-pano-postprocessor/src/work/resize-work.ts:138-143—awsS3Manager.uploadDirectoryByCredential호출. - Failure point:
applications/agents/packages/cupix-pano-postprocessor/src/manager/aws-s3.manager.ts:130-140—s3.upload(...).promise()가 throw 하지만 즉시 잡아서logger.error만 남기고 그대로 다음 파일로 진행.
업로드 호출 위치:
uploadResizedImage = async (cpPano: CPPano): Promise<void> => {
if (!cpPano.panoId) return;
const credentials = await this.cupixApi.pano.createTileUploadCredentials(cpPano.panoId);
await this.awsS3Manager.uploadDirectoryByCredential({ credential: credentials, directory: cpPano.resizeDir!});
await this.cupixApi.pano.checkTileUploading(cpPano.panoId);
};
실제 실패 지점 (예외가 외부로 전달되지 않음):
const uploadChunk = async (filesChunk: string[]) => {
await Promise.all(filesChunk.map(async (filePath) => {
try {
const bucketKeyPath = credential.basepath!;
const bucketKey = path.posix.join(bucketKeyPath, path.relative(directory, filePath));
await s3.upload({
Bucket: credential.bucket_name!,
Key: bucketKey,
Body: fs.createReadStream(filePath)
}).promise();
logger.debug('AwsS3Manager::uploadDirectoryByCredential | uploaded file - %s', filePath);
uploadedFileCount++;
} catch (error) {
logger.error('AwsS3Manager::uploadDirectoryByCredential | upload failed - %s', JSON.stringify(error));
}
}));
};
상위 resize 루프 — try/catch 가 있지만 위에서 throw 가 발생하지 않으므로 도달하지 않는다:
const resizeTasks = cpPanos.map((cpPano) => {
return PARALLEL_TASK_LIMIT(async () => {
try {
await this.resizeWork.resizePano(cpPano);
await this.resizeWork.uploadResizedImage(cpPano);
await this.resizeWork.checkStitched(cpPano);
} catch (error) {
logger.error('PanoPostprocessorService::run | resize pano id:%d | error %s', cpPano.panoId, stringifyError(error));
await this.panoPostprocessorManager.updatePanoState(cpPano.panoId!, TESLA.UpdatePanoRequest.StateEnum.Error);
erroredPanoIds.add(cpPano.panoId!);
}
});
});
또한 new AWS.S3({...}) 에 maxRetries / retryDelayOptions 가 설정돼 있지 않다 (Grep 결과 패키지 내 maxRetries/retry 매칭 없음). aws-sdk v2 의 기본 retry (기본 3회, retryable:true 5xx 포함) 가 동작하긴 하지만, 모두 소진된 뒤의 최종 실패에 대한 application-level 재시도가 전혀 없다.
기대 동작 vs 실제 동작:
- 기대: S3 가
retryable:true500 을 반환하면 충분한 backoff 후 재시도하고, 끝까지 실패하는 파일이 있으면 해당 pano 를Error상태로 표시. - 실제: SDK 기본 재시도 후 실패 → catch 에서 로그만 출력 →
uploadedFileCount만 안 늘어남 → 호출자는 정상 종료로 인식 → pano/capture 가Done으로 마킹.
Log Evidence#
Datadog 쿼리 (재현용):
service:cupixworks-pano-postprocessor-instance "uploadDirectoryByCredential"
service:cupixworks-pano-postprocessor-instance status:error
대표 에러 (cluster 의 representative error 와 동일):
{
"message": "We encountered an internal error. Please try again.",
"code": "InternalError",
"region": null,
"time": "2026-06-24T05:00:06.167Z",
"requestId": "JNN2Q5KZVFHY5KNW",
"extendedRequestId": "ge1RNMTLGzRfu5MBQwP7ICDBJa3lhaYiGae4GmqVfTMbTOTmj8WVM9spXR3h48Ce5MDAuv68xGBo3m2d8Sdbc3fu+CsXQuaS",
"statusCode": 500,
"retryable": true
}
24h 윈도우 내 동일 에러 시그니처 일부 (서로 다른 requestId → 서로 다른 S3 호출이 모두 transient 500):
2026-06-24 16:15:13 KST requestId=31BRNCNT2J71NNMA
2026-06-24 14:00:06 KST requestId=JNN2Q5KZVFHY5KNW ← 본 cluster first_seen
2026-06-24 02:59:25 KST requestId=D2XSNED3C7VT8225
2026-06-24 01:56:00 KST requestId=RSV5YE7NG1DNZGA3 (4건 반복)
2026-06-24 01:27:14 KST requestId=76CD9W4A1KFDMWQ1 (10건 반복)
같은 시간대에 동일 서비스에서 발생한 다른 cluster (status board 가 함께 묶음):
2026-06-24 14:11:50 KST MaskWork::maskPanos | error code:1
(cluster 4b46df80-d1f9-4976-9192-c6c0f7bedaf5)
Status board 결과 — svc-scope 인시던트:
{
"scope": "svc:cupixworks-pano-postprocessor-instance::unknown",
"active": {
"id": "2026-06-24-svc-cupixworks-pano-postprocessor-instance--unknown-1",
"title": "cupixworks-pano-postprocessor-instance service degraded",
"started_at": "2026-06-24T05:00:06.167Z",
"last_event_at": "2026-06-24T05:11:50.294Z"
}
}
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | AWS S3 측 transient 5xx (InternalError) — 외부 의존성 일시 장애 |
응답에 AWS 표준 code:"InternalError", statusCode:500, retryable:true 와 정상적인 requestId / extendedRequestId 포함. 다른 시각/다른 requestId 로 같은 메시지 반복. AWS 가 직접 retry 권장. |
응답이 우리 서비스가 만든 메시지가 아니라 AWS SDK 가 그대로 전달한 형식. 코드 변경 없이도 일시적으로 자체 회복되는 패턴. | Confirmed (1차 원인) |
| H2 | application-level 재시도/에러 전파 부재로 인해 1회 transient 실패가 사용자 데이터 누락으로 확대됨 | aws-s3.manager.ts:138-140 의 catch 가 throw 없이 로그만 출력. 패키지 내 maxRetries 설정 없음 (Grep 결과 무매칭). 상위 resizeTasks 의 try/catch 가 도달하지 않아 panoState=Error 처리 안 됨. |
— | Confirmed (영향 확대 원인) |
| H3 | 자격 증명 / 권한 문제 (AccessDenied, InvalidAccessKeyId) |
— | 에러 코드가 AccessDenied/SignatureDoesNotMatch 가 아니라 InternalError 이며, 같은 credential 으로 직전/이후 파일은 성공 로그가 남음 (uploadedFileCount 증가 패턴). |
Rejected |
| H4 | 네트워크 단절 / DNS / 클라이언트 측 timeout | — | 응답에 requestId 와 extendedRequestId 가 채워져 있어 요청이 S3 에 도달해 서버 처리가 이뤄졌음. timeout / ECONNRESET 류 메시지 아님. |
Rejected |
| H5 | pano resize 산출물 자체가 잘못되어 업로드가 거절됨 | — | S3 가 InternalError 5xx 를 반환 (4xx ValidationException 이 아님). 같은 자료를 재시도하면 보통 성공. | Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
applications/agents/packages/cupix-pano-postprocessor/src/manager/aws-s3.manager.ts:124-148- 청크 내 파일별 실패를 취합 후 호출자에게 전파 하도록 변경. 예: 실패한 파일 목록을 모아 모든 청크 종료 후 한 번이라도 실패가 있으면 throw, 또는 partial failure 정보를 리턴해서
ResizeWork.uploadResizedImage가 판단하도록. - 그렇게 하면
PanoPostprocessorService.run의resizeTaskstry/catch(pano-postprocessor-service.ts:171-175) 가 동작해서 해당 pano 만panoState=Error로 마킹되고, capture 가 잘못된Done으로 끝나지 않는다.
- 청크 내 파일별 실패를 취합 후 호출자에게 전파 하도록 변경. 예: 실패한 파일 목록을 모아 모든 청크 종료 후 한 번이라도 실패가 있으면 throw, 또는 partial failure 정보를 리턴해서
- 동일 위치에서
JSON.stringify(error)대신 메시지/코드/requestId 를 명시적으로 직렬화하거나, 팀의 표준 logger 가 Error 객체를 native 로 처리하면 그쪽으로 변경 (memory 의 logger 컨벤션 참고).
단기 개선 (1주 이내)#
aws-s3.manager.ts:108-117의new AWS.S3({...})에maxRetries,retryDelayOptions: { customBackoff }명시. 5xx +retryable:true에 대해 SDK retry 횟수를 늘리고 (예: 5), 지수 backoff + jitter 적용.- application-level 재시도 래퍼: 청크 단위로 실패한 파일만 한 번 더 재시도 (예:
p-retry또는 자체 루프 1-2회). 대부분의InternalError는 짧은 backoff 후 재시도로 회복됨. - 영향 측정 메트릭 추가 (아래 Monitoring 섹션 참조).
장기 개선 (재발 방지)#
- pano-postprocessor 전체에서 silent-swallow 패턴을 점검.
try { ... } catch { logger.error(...) }가 호출자 컨트랙트와 어긋나는 곳을 표준화 (실패 시Result반환 또는 throw). - pano 후처리 완료 후 "업로드된 타일 수 == 기대 타일 수" 검증 단계를 추가해 silent partial-failure 를 마지막 단계에서라도 잡아낸다.
- S3 5xx /
retryable:true패턴을 자동 격리 (circuit breaker) 하고, 일정 임계치 초과 시 capture 를 재처리 큐로 보내는 운영 로직 검토.
Monitoring#
대시보드 timeseries widget 에 사용할 Datadog 쿼리 (writing-datadog-monitoring-queries 가이드에 따라 count_by_status / rollup 사용, pipe 문법 사용 안 함):
업로드 실패 발생 추이:
sum:logs.hits{service:cupixworks-pano-postprocessor-instance,status:error,@message:*uploadDirectoryByCredential*upload\\ failed*}.as_count()
S3 InternalError 5xx 만 격리:
sum:logs.hits{service:cupixworks-pano-postprocessor-instance,@message:*InternalError*,@message:*statusCode\\:500*}.as_count()
서비스 전체 error 수 (regression 체크용):
sum:logs.hits{service:cupixworks-pano-postprocessor-instance,status:error}.as_count()
알림 제안: 10분 윈도우에서 위 1번째 쿼리 ≥ 5 이면 통보. 동일 시간대에 capture 의 panoPostprocessorState 가 Done 인 비율과 cross-check (장기 개선).
Risk Assessment#
- Risk level: medium — 외부 의존성 transient 장애 자체는 우리가 통제 불가지만, silent-swallow 로 인해 사용자 데이터(누락된 pano 타일) 가 잘못
Done처리되는 경로가 열려 있다. - 예상 복잡도: standard — 즉시 조치(에러 전파)는 단일 함수 변경, 단기 개선(retry/backoff)은 SDK 옵션 추가 + 작은 래퍼 수준.