어느 날 프로덕션 환경의 AWS Lambda에서 DynamoDB UpdateCommand 호출이 HTTP 400 에러로 실패했습니다. 처음 마주했을 때 꽤 당황스러웠던 에러였습니다.
{
"name": "InvalidSignatureException",
"$fault": "client",
"$metadata": { "httpStatusCode": 400, "attempts": 1, "totalRetryDelay": 0 },
"__type": "com.amazon.coral.service#InvalidSignatureException",
"message": "Signature expired: 20260909T222410Z is now earlier than 20260909T222411Z (20260909T222911Z - 5 min.)"
}
이 에러를 구글링하면 대부분 "서버의 시스템 시계(NTP)를 동기화하세요"라는 답변이 나옵니다. 하지만 우리가 운영하는 환경은 서버리스인 AWS Lambda입니다. 개발자가 직접 접근해 시계를 맞출 수 있는 OS 환경이 아닙니다.
그리고 원인을 깊이 파고들어 본 결과, 이 문제는 시스템 시계와 전혀 무관했습니다.
1. 에러 메시지가 말해주는 것
모든 AWS API 요청은 SigV4(Signature Version 4) 서명을 거칩니다. 요청 헤더에는 요청을 생성한 시각(x-amz-date)이 포함되며, AWS 서버는 요청을 수신한 시점을 기준으로 서명 시각이 5분 이상 과거이면 보안상 400 에러로 요청을 거부합니다. (Replay Attack 방지 목적)
에러 메시지를 분해해보면 상황이 한눈에 보입니다.
- 서명 시각 (
x-amz-date):2026-09-09 22:24:10Z - 서버 현재 시각:
2026-09-09 22:29:11Z - 시각 차이: 5분 1초
허용 오차 범위인 5분에서 딱 1초를 초과하여 거부되었습니다.
이 문제는 DynamoDB에 국한되지 않습니다. SigV4 서명을 사용하는 모든 AWS 서비스(Secrets Manager, Step Functions 등)에서 발생할 수 있으며, 실제로 aws-sdk-js-v3 공식 저장소에도 동일한 현상에 대한 이슈가 여러 건 보고되어 있습니다.
2. 결정적 단서: payload 타임스탬프 vs 서명 시각 대조
원인을 추적하던 중 중요한 단서를 발견했습니다. 실패한 Lambda 핸들러는 DynamoDB 업데이트 항목에 updated_at: new Date().toISOString() 형태로 현재 시각을 직접 기록하고 있었습니다.
실패한 요청의 요청 본문(payload)과 서명 헤더를 대조해보았습니다.
- payload의
updated_at:2026-09-09 22:29:11Z(서버 현재 시각과 정확히 일치) - SigV4 서명 시각:
2026-09-09 22:24:10Z(정확히 5분 전 시각)
new Date()는 정상적으로 현재 시각을 가리키고 있었습니다. 즉, Lambda 컨테이너의 로컬 시계는 멀쩡했습니다.
로컬 시계는 정상인데, AWS SDK가 서명을 생성할 때만 5분 전 시각을 가져다 쓴 것입니다. 이 차이는 향후 동일한 에러를 디버깅할 때 아주 유용한 판별 기준이 됩니다.
| 관찰 결과 | 원인 진단 |
|---|---|
| payload 시각은 정상, 서명 시각만 ~5분 과거 | SDK 내부 clock skew 보정값(systemClockOffset) 오염 |
| payload 시각과 서명 시각 모두 과거 | freeze된 in-flight 요청이 thaw 후 뒤늦게 전송됨 (fire-and-forget 코드 의심) |
| 둘 다 과거 (EC2/ECS 등 컨테이너 환경) | 실제 호스트 머신의 시계 드리프트 (NTP 동기화 필요) |
3. SDK의 systemClockOffset은 왜 오염되었을까?
AWS SDK v3에는 유용한 기능이 하나 내장되어 있습니다. 클라이언트와 AWS 서버 간의 시계가 어긋나 있을 경우를 대비해, 서버 응답 헤더의 Date 값과 로컬 시각의 차이가 일정 수준을 넘으면 systemClockOffset을 계산하여 자동으로 시계를 보정해 줍니다.
평소에는 매우 유용한 이 자동 보정 기능이 **Lambda의 라이프사이클(Freeze & Thaw)**과 만나면 치명적인 오작동을 일으킵니다.
시나리오: 범인은 await 없는 비동기 호출
- 핸들러 실행 및 미완료 요청:
코드에await를 걸지 않은 fire-and-forget 호출이 존재합니다.// 핸들러 내부 updateMetricsAsync(data).catch(console.error); // await 누락! return { statusCode: 200 }; - Lambda Freeze:
핸들러가 값을 반환하는 즉시 Lambda 런타임은 실행 환경을 정지(freeze)시킵니다. 이때 백그라운드로 날아간 네트워크 요청의 응답 처리가 멈춥니다. - Thaw 후 뒤늦은 응답 처리:
몇 분 뒤 다른 이벤트로 인해 동일한 warm 컨테이너가 깨어납니다(thaw).
정지되어 있던 런타임이 재개되면서 이전 요청의 응답을 그제서야 읽어 들입니다. - 시계 오판 및 Offset 오염:
응답 헤더의Date는 몇 분 전(freeze 이전) 시각인데, 응답을 처리하는 현재 로컬 시각은 몇 분 뒤(thaw 이후)입니다.
SDK는 이를 보고 **"아, 로컬 시계가 서버보다 5분이나 빠르구나!"**라고 잘못 판단하여 내부systemClockOffset을 약 -5분으로 보정해 버립니다. - 이후 모든 요청의 서명 만료:
일반적으로 AWS SDK 클라이언트는 성능 최적화를 위해 모듈 스코프(전역)에 싱글톤으로 선언해 재사용합니다.
따라서 한 번 오염된 offset은 해당 컨테이너가 살아있는 동안 이후 발생하는 모든 요청에 적용됩니다. 서명 시각이 계속 5분 과거로 생성되다가, 허용 한계(5분)를 단 1초라도 넘기는 순간InvalidSignatureException이 발생합니다.
4. 복구와 근본적인 예방
1) 왜 간헐적인 1회성 실패로 끝나는가?
다행히 이 문제는 지속되지 않고 스스로 복구되는 경우가 많습니다. InvalidSignatureException 에러 응답을 받을 때도 서버의 최신 Date 헤더가 함께 오기 때문에, SDK가 이를 바탕으로 systemClockOffset을 다시 정상 시각으로 재보정(self-healing)하기 때문입니다.
만약 SQS 트리거로 동작하는 Lambda라면, 실패한 메시지를 batchItemFailures에 담아 반환하여 SQS 재시도로 넘겨주면 데이터 유실 없이 정상 처리됩니다.
2) 근본 예방: 백그라운드 호출을 절대 남기지 말 것
해결책은 명확합니다. Lambda 핸들러 내의 모든 비동기 AWS 호출은 반드시 await 하거나 Promise.all로 완료를 보장해야 합니다.
// AS-IS: Freeze 경계에서 응답이 지연되어 SDK 시계 오프셋을 오염시킬 위험
sendAuditLog(event).catch(console.error);
return response;
// TO-BE: 반드시 완료를 기다린 후 핸들러 종료
try {
await sendAuditLog(event);
} catch (error) {
console.error("Audit log failed", error);
}
return response;
핸들러가 리턴하기 전에 실행 흐름을 완전히 끝내지 않으면, 그 대가는 몇 분 뒤 전혀 엉뚱한 비즈니스 로직의 서명 만료 에러로 돌아옵니다.
마치며
서버리스 환경에서 "시계가 맞지 않는다"는 에러를 만나면 인프라 수준의 문제로 생각하기 쉽습니다. 하지만 원인을 쪼개어 보면 런타임 라이프사이클의 특성과 비동기 코드의 사소한 누락이 빚어낸 나비효과인 경우가 많습니다.
비슷한 에러로 고민하고 계신 분들이라면, 시스템 시계를 탓하기 전에 코드 어딘가에 await 없이 날아가고 있는 AWS 호출이 없는지 먼저 점검해 보시길 권합니다.
참고 링크
- aws-sdk-js-v3 #6222 — InvalidSignatureException: Signature expired
- aws-sdk-js-v3 #7605 — Intermittent InvalidSignatureException in AWS Lambda
- aws-sdk-js-v3 #7135 — SFN Client throws InvalidSignatureException in Lambda
- aws-sdk-js-v3 #4429 — Occasional Signature Expired errors fetching secrets in Lambda
- AWS re:Post — Resolve the Lambda "Signature expired" error
'Concurrency & Execution' 카테고리의 다른 글
| AI 에이전트 on AWS Lambda — cold start 줄이는 5가지 패턴 (0) | 2026.05.19 |
|---|---|
| [EXPRESS] 413 - Payload Too Large (0) | 2024.10.14 |