ES /docs

[CupixAerialMap] postprocess fail - error:({}) / message:("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML

RCA: [CupixAerialMap] postprocess fail - CloudFront 504 Gateway Timeout

Error Log#

[Datadog Logs](https://app.datadoghq.com/logs?query=service%3Aaerial-map-service%20status%3Aerror%20%40environment%3Aproduction%20%22%5BCupixAerialMap%5D%20postprocess%20fail%20-%20error%3A(%7B%7D)%20%2F%20message%3A(%3C!DOCTYPE%20HTML%20PUBLIC%20%5C-%2F%2FW3C%2F%2FDTD%20HTML%20%22&from_ts=1775778060000&to_ts=1775785320000&live=false)

text
[CupixAerialMap] postprocess fail - error:({}) / message:("<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<HTML><HEAD><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<TITLE>ERROR: The request could not be satisfied</TITLE>
</HEAD><BODY>
<H1>504 Gateway Timeout ERROR</H1>
<H2>The request could not be satisfied.</H2>
...
Generated by cloudfront (CloudFront) HTTP3 Server
Request ID: FbQ94rSvRHAwVT9hwGTQPa6DB73se6onKaRjaS4pXUOwxZInoWMWfA==
</BODY></HTML>")

Impact#

  • Service: aerial-map-service
  • 발생 횟수: 1
  • 최초 발생: 2026-04-10T00:41:26.564Z
  • 최근 발생: 2026-04-10T00:41:26.564Z

Root Cause Summary#

aerial_map ID 298의 postprocess ECS task가 CupixWorks API에 HTTP 요청을 보내는 과정에서 CloudFront가 504 Gateway Timeout을 반환하여 실패했습니다. axios retry interceptor가 5xx 에러에 대해 최대 3회 재시도를 수행하지만, 모든 재시도가 실패한 후 최종 에러가 발생했습니다. CloudFront 504는 origin 서버(CupixWorks API)가 CloudFront의 timeout 내에 응답하지 못했음을 의미합니다. 동일 시간대에 aerial_map 299, 300은 다른 호스트에서 정상 처리되었으므로, 이 에러는 일시적인 origin 응답 지연 또는 특정 요청의 처리 지연으로 인한 것으로 판단됩니다.

Technical Analysis#

Code Path#

  • Entry point: src/code/src/postprocess/index.ts:172-183 — ECS task가 app(input)을 호출하여 postprocess 시작
  • API 호출 시작: src/code/src/postprocess/index.ts:59cupixApi.saveProcessingState(aerialMapId, 'postprocessing', ...)로 상태 업데이트
  • HTTP 클라이언트: src/code/src/common/axios.ts — 60초 timeout, 5xx에 대해 최대 3회 exponential backoff 재시도
  • API 에러 처리: src/code/src/common/api.ts — 각 메서드의 catch 블록에서 error.response?.data(HTML 응답)를 JSON.stringify로 감싸서 새 Error로 throw
  • Failure point: src/code/src/postprocess/index.ts:179 — 최상위 catch에서 에러 로깅 후 process.exit(1)

Postprocess 진입점 — 최상위 에러 핸들링:

typescript
// src/code/src/postprocess/index.ts:172-183
(async () => {
  try {
    await app(input);
    logger.info(`[CupixAerialMap] postprocess success`);
    await sleep(30000);
    process.exit(0);
  } catch (error: any) {
    logger.error(`[CupixAerialMap] postprocess fail - error:(${JSON.stringify(error)}) / message:(${error.message})`);
    await sleep(30000);
    process.exit(1);
  }
})();

JSON.stringify(error)는 Error 객체의 속성이 non-enumerable이므로 항상 {}를 반환합니다. 실제 에러 정보는 error.message에만 포함됩니다.

Axios retry interceptor:

typescript
// src/code/src/common/axios.ts:12-33
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);
  console.warn(
    `[AerialMap] Request failed (${error.code || error.response?.status}), retrying ${config.__retryCount}/${MAX_RETRIES} after ${delay}ms - ${config.method?.toUpperCase()} ${config.url}`,
  );
  await new Promise((resolve) => setTimeout(resolve, delay));
  return axios(config);
});

504 응답은 status >= 500 조건에 해당하므로 최대 3회 재시도됩니다 (1초, 2초, 4초 backoff). 모든 재시도 실패 후 최종 에러가 throw됩니다.

CupixApi 에러 래핑 (대표 패턴):

typescript
// src/code/src/common/api.ts — updateAerialMap, getAerialMap 등 모든 메서드 동일 패턴
} catch (error: any) {
  console.error(`[CupixApi] updateAerialMap - /api/v1/aerial_maps/${aerialMapId} - ${error.message}`);
  throw new Error(JSON.stringify(error.response?.data || error.message));
}

CloudFront 504 HTML 응답이 error.response.data에 담기고, 이것이 JSON.stringify를 거쳐 새 Error의 message가 됩니다.

에러 전파 경로:

  1. CupixWorks API → CloudFront 504 반환
  2. axios interceptor → 3회 재시도 모두 실패
  3. CupixApi catch 블록 → HTML을 Error.message로 래핑하여 throw
  4. app() 내부 catch (index.ts:148-157) → AMB720 에러 코드로 저장 시도 후 re-throw
  5. 최상위 catch (index.ts:179) → JSON.stringify(error) = {}, error.message = HTML → 로깅 후 process.exit(1)
  6. ECS task 종료 (ExitCode: 1) → Step Function States.TaskFailedcupix-postprocess-check Lambda가 실패 감지 → cupix-process-fail Lambda가 API에 실패 상태 저장

Log Evidence#

Datadog 검색 쿼리:

text
service:aerial-map-service status:error @environment:production

에러 로그 1 (00:41:26.454Z) — 내부 catch 블록 로그:

text
[CupixAerialMap] postprocess fail "<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"...
<H1>504 Gateway Timeout ERROR</H1>..."
  • Host: ip-10-1-109-22.us-west-2.compute.internal
  • aerial_map.id: 298
  • Log file: /tmp/workspace/postprocess-json-2026.04.10.log (offset: 110671)

에러 로그 2 (00:41:26.564Z) — 최상위 catch 블록 로그:

text
[CupixAerialMap] postprocess fail - error:({}) / message:("<!DOCTYPE HTML PUBLIC...
<H1>504 Gateway Timeout ERROR</H1>...")
  • 동일 호스트, 동일 CloudFront Request ID: FbQ94rSvRHAwVT9hwGTQPa6DB73se6onKaRjaS4pXUOwxZInoWMWfA==

Postprocess 타임라인 (aerial_map 298):

Timestamp (UTC) Event
00:36:50 Pix4d process-check → success (28 outputs ready)
00:37:59 ECS postprocess task 시작, workspace 생성
00:38:06 Pix4d output 수신 (12개 항목, project 2613154)
00:41:26.454 ERROR: CloudFront 504 — 내부 catch 로그
00:41:26.564 ERROR: CloudFront 504 — 최상위 catch 로그, process.exit(1)
00:42:26 postprocess-check Lambda: ECS task 실패 감지 (ExitCode: 1)
00:42:27 process-fail Lambda: API에 실패 상태 저장 (/api/v1/aerial_maps/298/processing)
00:42:32 data-collect Lambda: 실패 데이터 S3에 저장

동시간대 다른 aerial_map 처리 결과:

text
service:aerial-map-service status:info @environment:production
  • aerial_map 299: ip-10-1-172-169에서 01:26경 정상 완료
  • aerial_map 300: ip-10-1-47-20에서 01:36경 정상 완료 (oblique method)

다른 호스트에서의 성공 사례는 이 에러가 일시적 origin 응답 지연임을 뒷받침합니다.

Fix Recommendation#

즉시 조치 (Critical)#

에러 로깅 개선src/code/src/postprocess/index.ts:179

현재 JSON.stringify(error)는 항상 {}를 반환하여 디버깅에 도움이 되지 않습니다. error.message, error.stack, 그리고 axios 에러의 경우 error.response?.statuserror.config?.url을 명시적으로 로깅하도록 변경해야 합니다. 이를 통해 어떤 API 호출에서 504가 발생했는지 즉시 파악할 수 있습니다.

단기 개선 (1주 이내)#

  1. Retry 전략 강화src/code/src/common/axios.ts

    • 현재 3회 재시도(총 7초 backoff)는 CloudFront origin timeout 상황에서 충분하지 않을 수 있습니다. 재시도 횟수를 5회로 늘리거나 backoff 간격을 더 길게 설정하는 것을 검토해야 합니다.
    • CloudFront 504는 일반적으로 origin 서버가 30초 이상 응답하지 않을 때 발생하므로, 재시도 간 대기 시간을 origin recovery 시간에 맞춰 조정해야 합니다.
  2. CupixApi 에러 래핑 개선src/code/src/common/api.ts

    • 각 API 메서드의 catch 블록에서 throw new Error(JSON.stringify(error.response?.data || error.message))로 에러를 래핑하면 원본 에러의 status code, URL 등 컨텍스트가 손실됩니다. AxiosError를 그대로 전파하거나 custom error class를 사용하여 status code와 URL을 보존해야 합니다.

장기 개선 (재발 방지)#

  1. Circuit breaker 패턴 도입 — CupixWorks API가 지속적으로 504를 반환할 때 무의미한 재시도를 방지하고, 빠르게 실패하여 리소스를 절약하는 메커니즘이 필요합니다.

  2. Postprocess 재시도 메커니즘 — 현재 ECS task가 실패하면 process-fail Lambda가 바로 실패 상태를 저장합니다. Step Function 레벨에서 일시적 에러(5xx)에 대한 task-level 재시도를 추가하여, CloudFront 504 같은 transient failure 시 자동 복구할 수 있도록 해야 합니다.

  3. CloudFront origin 모니터링 — origin 서버의 응답 시간과 5xx 비율을 모니터링하여, postprocess 서비스에 영향을 미치기 전에 origin 문제를 감지할 수 있어야 합니다.

Monitoring#

  • CloudFront 504 에러 발생 알림:
text
service:aerial-map-service status:error "504 Gateway Timeout"
  • Postprocess 실패율 모니터링:
text
service:aerial-map-service "[CupixAerialMap] postprocess fail"
  • CupixWorks API 응답 시간 메트릭 (CloudFront origin latency):
text
service:aerial-map-service "[CupixApi]" status:error

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard
  • 단일 발생이며, 동시간대 다른 aerial_map은 정상 처리되었습니다. CloudFront origin의 일시적 지연이 원인으로, 구조적 결함보다는 transient infrastructure issue에 해당합니다. 다만 에러 로깅 개선과 Step Function 레벨 재시도 추가는 향후 유사 사례의 자동 복구를 위해 권장됩니다.