ES /docs

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#

  1. 2026-06-24 14:00:06 KST — 첫 발생: requestId JNN2Q5KZVFHY5KNW, capture postprocess 중 S3 500 InternalError 수신 (cluster first_seen).
  2. 2026-06-24 14:00:06 KST — 동일 cluster 두 번째 occurrence (occurrence_count: 2).
  3. 2026-06-24 16:15:13 KST — 동일 에러 메시지 재발 (requestId 31BRNCNT2J71NNMA).
  4. 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).
  5. 분석 시점 (2026-06-24) — RCA 작성. 외부 의존성 (AWS S3) 측 일시 장애로 추정되며, 코드 경로 상의 silent-failure 가 영향 확대 요인.

Error Log#

Datadog Logs

text
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.runresizeTasks 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-143awsS3Manager.uploadDirectoryByCredential 호출.
  • Failure point: applications/agents/packages/cupix-pano-postprocessor/src/manager/aws-s3.manager.ts:130-140s3.upload(...).promise() 가 throw 하지만 즉시 잡아서 logger.error 만 남기고 그대로 다음 파일로 진행.

업로드 호출 위치:

applications/agents/packages/cupix-pano-postprocessor/src/work/resize-work.ts:138-143typescript
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);
};

실제 실패 지점 (예외가 외부로 전달되지 않음):

applications/agents/packages/cupix-pano-postprocessor/src/manager/aws-s3.manager.ts:124-142typescript
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 가 발생하지 않으므로 도달하지 않는다:

applications/agents/packages/cupix-pano-postprocessor/src/pano-postprocessor-service.ts:165-178typescript
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:true 500 을 반환하면 충분한 backoff 후 재시도하고, 끝까지 실패하는 파일이 있으면 해당 pano 를 Error 상태로 표시.
  • 실제: SDK 기본 재시도 후 실패 → catch 에서 로그만 출력 → uploadedFileCount 만 안 늘어남 → 호출자는 정상 종료로 인식 → pano/capture 가 Done 으로 마킹.

Log Evidence#

Datadog 쿼리 (재현용):

text
service:cupixworks-pano-postprocessor-instance "uploadDirectoryByCredential"
text
service:cupixworks-pano-postprocessor-instance status:error

대표 에러 (cluster 의 representative error 와 동일):

json
{
  "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):

text
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 가 함께 묶음):

text
2026-06-24 14:11:50 KST  MaskWork::maskPanos | error code:1
  (cluster 4b46df80-d1f9-4976-9192-c6c0f7bedaf5)

Status board 결과 — svc-scope 인시던트:

json
{
  "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-140catch 가 throw 없이 로그만 출력. 패키지 내 maxRetries 설정 없음 (Grep 결과 무매칭). 상위 resizeTaskstry/catch 가 도달하지 않아 panoState=Error 처리 안 됨. Confirmed (영향 확대 원인)
H3 자격 증명 / 권한 문제 (AccessDenied, InvalidAccessKeyId) 에러 코드가 AccessDenied/SignatureDoesNotMatch 가 아니라 InternalError 이며, 같은 credential 으로 직전/이후 파일은 성공 로그가 남음 (uploadedFileCount 증가 패턴). Rejected
H4 네트워크 단절 / DNS / 클라이언트 측 timeout 응답에 requestIdextendedRequestId 가 채워져 있어 요청이 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.runresizeTasks try/catch (pano-postprocessor-service.ts:171-175) 가 동작해서 해당 pano 만 panoState=Error 로 마킹되고, capture 가 잘못된 Done 으로 끝나지 않는다.
  • 동일 위치에서 JSON.stringify(error) 대신 메시지/코드/requestId 를 명시적으로 직렬화하거나, 팀의 표준 logger 가 Error 객체를 native 로 처리하면 그쪽으로 변경 (memory 의 logger 컨벤션 참고).

단기 개선 (1주 이내)#

  • aws-s3.manager.ts:108-117new 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 문법 사용 안 함):

업로드 실패 발생 추이:

text
sum:logs.hits{service:cupixworks-pano-postprocessor-instance,status:error,@message:*uploadDirectoryByCredential*upload\\ failed*}.as_count()

S3 InternalError 5xx 만 격리:

text
sum:logs.hits{service:cupixworks-pano-postprocessor-instance,@message:*InternalError*,@message:*statusCode\\:500*}.as_count()

서비스 전체 error 수 (regression 체크용):

text
sum:logs.hits{service:cupixworks-pano-postprocessor-instance,status:error}.as_count()

알림 제안: 10분 윈도우에서 위 1번째 쿼리 ≥ 5 이면 통보. 동일 시간대에 capture 의 panoPostprocessorStateDone 인 비율과 cross-check (장기 개선).

Risk Assessment#

  • Risk level: medium — 외부 의존성 transient 장애 자체는 우리가 통제 불가지만, silent-swallow 로 인해 사용자 데이터(누락된 pano 타일) 가 잘못 Done 처리되는 경로가 열려 있다.
  • 예상 복잡도: standard — 즉시 조치(에러 전파)는 단일 함수 변경, 단기 개선(retry/backoff)은 SDK 옵션 추가 + 작은 래퍼 수준.