[CupixAerialMap] postprocess fail Request failed with status code 500
RCA: [CupixAerialMap] postprocess fail Request failed with status code 500
Overview#
What Happened#
2026-04-30 07:30:02 UTC에 aerial-map-service의 postprocess ECS Fargate 태스크에서 aerial photo 썸네일 생성을 위해 Cupix S3 소스 버킷(cupixworks-source-888a512bf858-uswe1)의 presigned URL로 이미지를 다운로드하던 중 AWS S3가 HTTP 500 Internal Server Error를 반환했다. axios 재시도 인터셉터가 최대 3회까지 재시도한 후에도 실패하여 aerial map 379의 전체 postprocess가 실패했다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | AxiosError |
| exception.message | Request failed with status code 500 |
| top_frame | /app/dist/index.js:3961:28602 (minified — maps to processAerialPhoto flow) |
| runtime | Node.js 20 (ECS Fargate, 2 vCPU, 8 GB memory) |
| env | production, us-west-2 |
Timeline#
- 06:43:38 UTC — postprocess Fargate 태스크 시작 (aerial map 379)
- 06:43:53 ~ 06:44:00 UTC — Pix4d 출력 파일 다운로드 및 report/flight_path 후처리 완료
- 07:30:02 UTC — S3 presigned URL로 aerial photo 이미지 다운로드 중 HTTP 500 에러 발생, 3회 재시도 후 실패
- 07:30:02 UTC — postprocess 태스크 exit code 1로 종료
- 07:31:03 UTC — postprocess-check Lambda가
States.TaskFailed감지 - 07:31:08 UTC — process-fail Lambda가 aerial map 379 상태를
fail로 업데이트 - 07:31:10 UTC — data-collect Lambda가 aerial map 379 데이터 수집 완료
Error Log#
[CupixAerialMap] postprocess fail Request failed with status code 500
Impact#
- Service:
aerial-map-service - 발생 횟수: 1
- 최초 발생: 2026-04-30T07:30:02.585Z
- 최근 발생: 2026-04-30T07:30:02.585Z
- 사용자 영향: aerial map 379의 postprocess 실패로 인해 orthomosaic 타일, 썸네일 등 최종 결과물이 생성되지 않았으며, Step Functions에서
fail상태로 전환됨.
Root Cause Summary#
AWS S3가 presigned URL을 통한 aerial photo 이미지 다운로드 요청에 일시적으로 HTTP 500 Internal Server Error를 반환했다. 이는 AWS S3의 알려진 transient 오류이다. aerial-map-service의 axios 인터셉터(axios.ts)가 status >= 500 에 대해 최대 3회 재시도하지만, 3회 모두 실패한 후 에러가 전파되어 processAerialPhoto 루프 전체가 중단되었다. processAerialPhoto는 각 aerial photo를 순차적으로 처리하며(process.ts:56-79), 한 장의 사진 다운로드 실패가 전체 postprocess 태스크를 실패시키는 구조이다.
Technical Analysis#
Code Path#
Entry point: postprocess/index.ts:172 — ECS Fargate 태스크가 CLI 인자로 input JSON을 받아 app() 함수 실행
(async () => {
try {
await app(input);
logger.info(`[CupixAerialMap] postprocess success`);
await sleep(30000); // sleep for log push
process.exit(0);
} catch (error: any) {
logger.error(`[CupixAerialMap] postprocess fail - error:(${JSON.stringify(error)}) / message:(${error.message})`);
await sleep(30000); // sleep for log push
process.exit(1);
}
})();
Orchestration: postprocess/index.ts:93-133 — processReport, processFlightPath, processAerialPhoto가 순차 실행된 후 processTiff, processThumbnail 등이 Promise.all로 병렬 실행됨. processAerialPhoto는 Promise.all 전에 실행되므로 여기서 에러가 발생하면 나머지 작업이 실행되지 않음.
const report = await processReport(cupixApi, aerialMapId, pix4dOutput, downloadCredential);
const [flightPath, calibration] = await processFlightPath(
cupixApi, aerialMapId, pix4dOutput, downloadCredential, aerialPhotos,
);
await processAerialPhoto(cupixApi, aerialMapId, report!, calibration!, aerialPhotos);
Failure point: postprocess/process.ts:66 — downloadStreamByUrl(aerialPhoto.downloadUrl) 호출에서 S3 presigned URL이 HTTP 500을 반환.
export const processAerialPhoto = async (
cupixApi: CupixApi,
aerialMapId: number,
report: IReportInfo,
calibration: ICalibrations,
aerialPhotos: IAerialPhoto[],
) => {
for (let i = 0; i < aerialPhotos.length; i++) {
const aerialPhoto = aerialPhotos[i];
const aerialPhotoUploadCredential = await cupixApi.createAerialPhotoS3UploadCredential(aerialMapId, aerialPhoto.id);
// ...
const stream = await downloadStreamByUrl(aerialPhoto.downloadUrl); // ← HTTP 500 발생 지점
const thumbnailKey = `${aerialPhotoUploadCredential.basepath}/thumbnail/thumbnail`;
const thumbnailStream = await createThumbnail(stream);
// ...
}
};
S3 다운로드 함수: common/s3.ts:164-166 — 커스텀 axios 인스턴스 사용. 이 인스턴스는 common/axios.ts에 정의된 재시도 인터셉터를 포함.
export const downloadStreamByUrl = async (downloadUrl: string) => {
const response = await axios({ method: 'get', url: downloadUrl, responseType: 'stream' });
return response.data;
};
재시도 인터셉터: common/axios.ts:12-33 — status >= 500에 대해 최대 3회, exponential backoff (1s, 2s, 4s)로 재시도. 3회 모두 실패하면 에러를 throw.
const axios = _axios.create({
timeout: REQUEST_TIMEOUT_MS,
});
axios.interceptors.response.use(undefined, async (error) => {
const config = error.config;
if (!config) throw error;
config.__retryCount = config.__retryCount || 0;
const retryable = RETRY_CODES.includes(error.code) || (error.response?.status && error.response.status >= 500);
if (!retryable || config.__retryCount >= MAX_RETRIES) {
throw error;
}
config.__retryCount += 1;
const delay = BASE_DELAY_MS * Math.pow(2, config.__retryCount - 1);
// ...
return axios(config);
});
Presigned URL 생성 (Tesla API): app/models/concerns/aerialable/aerial_photo.rb:19-20 — presigned URL은 expires_in: 1.days.to_i (86400초)로 생성됨. 에러 로그의 URL에서 X-Amz-Expires=86400, X-Amz-Date=20260430T064341Z 확인 — URL 자체는 만료되지 않았음.
else
self.resource.object(ver).presigned_url(:get, expires_in: 1.days.to_i)
end
에러 핸들링: postprocess/index.ts:148-157 — processAerialPhoto에서 throw된 에러는 catch 블록에서 AMB720 (Postprocess Fail) 에러 코드로 Cupix API에 저장된 후 다시 throw되어 process가 exit code 1로 종료.
} catch (error: any) {
if (error instanceof AerialMapError) {
await cupixApi.saveError(aerialMapId, error.code, error.reason);
} else {
logger.error(`[CupixAerialMap] postprocess fail ${error.message}`);
await cupixApi.saveError(aerialMapId, ERROR_CODE['AMB720'].code, ERROR_CODE['AMB720'].reason);
}
throw error;
}
Log Evidence#
Datadog 쿼리:
service:aerial-map-service status:error
에러 발생 시점의 상세 로그 (에러 메시지에 포함된 axios config에서 확인된 요청 정보):
{
"message": "Request failed with status code 500",
"name": "AxiosError",
"code": "ERR_BAD_RESPONSE",
"status": 500,
"config": {
"method": "get",
"url": "https://s3.us-west-1.amazonaws.com/cupixworks-source-888a512bf858-uswe1/resources/k8pw0m/uswe1/v1?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20260430T064341Z&X-Amz-Expires=86400",
"responseType": "stream"
}
}
postprocess-check Lambda가 감지한 ECS 태스크 실패 상태:
{
"StopCode": "EssentialContainerExited",
"ExitCode": 1,
"Image": "002596530511.dkr.ecr.us-west-2.amazonaws.com/cupix-aerial-map-postprocess:prod",
"LastStatus": "STOPPED",
"StoppedReason": "Essential container in task exited"
}
process-fail Lambda 로그:
[CupixAerialMap] cupix-process-fail-production-vdco - pix4d process fail: fail
재시도 로그 확인:
service:aerial-map-service status:warn
결과: 0건 — Fargate ECS 태스크의 console.warn 출력이 Datadog에 수집되지 않았을 가능성이 있음. 인터셉터는 console.warn으로 재시도 로그를 출력하지만, Datadog에서 해당 레벨이 캡처되지 않는 환경일 수 있음. 또는 재시도가 빠르게 실패하여 로그가 배치 전송 전에 유실되었을 수 있음.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | AWS S3 transient 500 에러 — S3가 일시적으로 Internal Server Error를 반환 | 에러 응답 status 500, ERR_BAD_RESPONSE 코드, S3 presigned URL이 만료되지 않았음(X-Amz-Expires=86400, 생성 후 ~47분), AWS S3 서비스 문서에 "S3 may return 500s during transient issues" 명시 |
— | Confirmed |
| H2 | Presigned URL 만료로 인한 접근 거부 | presigned URL의 TTL은 86400초(24시간)임 | 에러 발생 시각(07:30 UTC)은 URL 생성(06:43 UTC) 후 47분으로 만료 시간 내. 만료 시에는 403이 반환되지 500이 아님 | Rejected |
| H3 | 요청 대상 S3 객체가 존재하지 않거나 삭제됨 | — | 존재하지 않는 객체 접근 시 404가 반환됨, 500은 서버측 오류를 의미함 | Rejected |
| H4 | 네트워크 연결 문제 (ECONNRESET, timeout) | — | 에러 코드가 ERR_BAD_RESPONSE이며 status: 500으로, HTTP 응답을 정상적으로 수신함. 네트워크 오류 시에는 ECONNRESET, ETIMEDOUT 등의 코드가 반환됨 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- 없음 — 이 에러는 AWS S3의 transient 오류이며, 현재 재시도 메커니즘(최대 3회)이 이미 구현되어 있다. 1회성 발생이므로 즉시 조치는 불필요.
단기 개선 (1주 이내)#
- 재시도 횟수 증가:
common/axios.ts:5의MAX_RETRIES를 3에서 5로 증가. S3 transient 500은 보통 수 초 내에 해소되므로 2회 추가 재시도로 대부분 커버 가능. processAerialPhoto개별 사진 에러 격리:process.ts:56-79의 for 루프 내에 try-catch를 추가하여 한 장의 aerial photo 썸네일 생성 실패가 전체 postprocess를 중단하지 않도록 개선. 실패한 사진은thumbnail_state: 'error'로 마킹하고 나머지를 계속 진행.- 재시도 로그를
logger로 변경:common/axios.ts:28의console.warn을logger.warn으로 변경하여 Datadog에서 재시도 이벤트를 추적할 수 있도록 개선.
장기 개선 (재발 방지)#
- S3 다운로드에 AWS SDK 사용 검토: presigned URL 대신 AWS SDK의
GetObjectCommand를 사용하면 SDK 내장 재시도 로직(maxAttempts,retryMode: 'adaptive')을 활용할 수 있음. 현재 Pix4d 출력 다운로드(downloadObjectByKey)에서는 이미 SDK를 사용하고 있으나, aerial photo 다운로드(downloadStreamByUrl)에서는 presigned URL + axios를 사용 중.
Monitoring#
- 재시도 로그를
logger.warn으로 변경 후, 아래 Datadog 쿼리로 S3 재시도 빈도 모니터링:
service:aerial-map-service "Request failed" "retrying"
- postprocess 실패 빈도 모니터링:
service:aerial-map-service status:error "postprocess fail"
Risk Assessment#
- Risk level: low
- 예상 복잡도: trivial — AWS S3 transient 오류 1회 발생. 현재 재시도 메커니즘이 있으며, 대부분의 경우 재시도로 해소됨. 재시도 횟수 증가와 에러 격리는 방어적 개선.