LabHub
배우기 러닝패스 코스

統合とデプロイ

注文の状態がときどき巻き戻る — 受け取る側を作る

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

남의 시스템이 밀어 넣는 웹훅을 받는 쪽을 직접 만든다. HMAC 서명 검증과 상수시간 비교, 재생을 막는 시각 창, 배달 번호와 이벤트 번호 두 열쇠의 중복 제거, 판 비교로 순서 뒤집힘 흡수, 그리고 빨리 200 을 주고 나중에 처리하는 구조까지 붙이고, 하루치 배달을 다시 흘려보내 각 갈래를 집계한다.

왜 중요한가

웹훅은 우리가 부르는 것이 아니라 받는 것이라 통제권이 반대편에 있다. 수신 종단은 열려 있어야 하므로 아무나 POST 할 수 있고, 우리 응답이 늦으면 상대는 재전송하고, 순서는 보장되지 않는다. 그래서 받는 쪽은 네 가지를 스스로 판단해야 한다. 누가 보냈는가(서명), 언제 보낸 것인가(시각 창), 이미 본 것인가(중복), 지금 것보다 새것인가(판). 이 중 하나라도 빠지면 가짜 이벤트가 원장에 들어가거나 주문 상태가 뒤로 간다. 중복의 열쇠가 둘이라는 점이 특히 함정이다. 배달 번호는 한 번의 전송을, 이벤트 번호는 벌어진 사건 하나를 가리킨다. 막아야 하는 것은 사건의 중복 적용이므로 배달 번호만으로 거르면 절반만 막힌다. 채점기는 여러분의 문장을 믿지 않는다. 여러분이 만든 발신기와 수신기를 채점기가 고른 포트에 직접 띄우고, 채점기가 만든 비밀과 배달로 검증기·원장을 다시 실행해 답을 맞춰 본다.

단계

  1. /root/wh/sender.py 를 만들어 포트 8012 에 띄우고, /deliveries/root/wh/deliveries.json 에, 공유 비밀을 /root/wh/secret.txt 에 저장하세요.
  2. /root/wh/verify.py 를 만들어 서명을 상수시간으로 비교하고 {"ok": ..., "reason": ...} 를 내게 하세요.
  3. verify.py 에 시각 창을 붙여 창 밖의 배달을 stale 로 거절하게 하세요.
  4. /root/wh/ledger.py 를 만들어 배달 번호와 이벤트 번호 두 열쇠로 중복을 거르게 하세요.
  5. ledger.py 가 지금 저장된 판보다 새것일 때만 적용하고, 뒤늦게 온 옛 판은 stale_version 으로 남기게 하세요.
  6. /root/wh/receiver.py 를 만들어 검증만 하고 큐에 넣은 뒤 곧바로 200 을 주게 하고, --drain 으로 큐를 원장에 흘려보내게 하세요.
  7. /root/wh/replay_day.py 로 하루치 배달을 모두 흘려보내 /root/wh/day.db/root/wh/result.json 을 만드세요.
  8. /root/wh/wh_report.md 에 네 절로 보고하세요.

참고

하루치 배달 손에 쥐기

/root/wh/sender.py 를 만들어 포트 8012 에 띄우고, /deliveries 응답을 /root/wh/deliveries.json 에, 공유 비밀을 /root/wh/secret.txt 에 저장하세요. 배달은 41건, 사건은 30건입니다.

flask 로 /health 와 /deliveries 두 경로를 만듭니다. 배달 목록은 사건 30건에 재전송·같은 사건의 새 배달·오래된 재생·가짜 서명을 더해 만듭니다. 기준 시각을 응답에 함께 실어 두면 나중에 시험이 시계에 흔들리지 않습니다.

누가 보냈는지 서명으로 가리기

/root/wh/verify.py 를 만들어 배달 하나의 서명을 검증하고 {"ok": ..., "reason": ...} 를 내게 하세요. 서명 모양이 아니면 malformed, 서명이 틀리면 bad_signature 입니다. 비교는 반드시 상수시간으로 하세요.

서명 문자열 t=...,v1=... 을 쪼개 t 와 v1 을 얻고, "<t>.<body>" 를 재료로 HMAC-SHA256 을 계산합니다. body 는 받은 문자열 그대로 써야 합니다. 파이썬 hmac 모듈에는 길이에 비례하는 시간으로 비교하는 함수가 있습니다 — == 는 첫 다른 바이트에서 끝나 시간이 비밀을 흘립니다.

오래된 것을 다시 밀어 넣는 손

verify.py 에 시각 창을 붙이세요. 서명이 맞아도 --now 와 배달의 t 차이가 --tolerance(기본 300초)를 넘으면 stale 로 거절해야 합니다. 앞뒤 양쪽 모두 창 밖입니다.

서명만으로는 예전에 오간 유효한 요청을 그대로 다시 밀어 넣는 것을 막을 수 없습니다. 그래서 서명 재료에 타임스탬프가 들어 있고, 받는 쪽은 지금 시각과의 차이를 봅니다. 미래 쪽도 막아야 합니다 — 상대 시계가 빠른 것과 누군가 t 를 미래로 적어 둔 것은 구별되지 않습니다. 순서는 서명이 먼저입니다.

열쇠가 둘이다

/root/wh/ledger.py 를 만들어 검증을 통과한 배달을 원장에 넣되, 같은 배달 번호가 다시 오면 duplicate_delivery, 배달 번호는 새것인데 이벤트 번호가 이미 있으면 duplicate_event 로 거르게 하세요. 표는 sqlite 파일에 남겨야 합니다.

조회해서 없으면 넣는 방식은 같은 순간 들어온 두 건을 둘 다 통과시킵니다. 두 열쇠를 각각 기본키로 둔 표에 곧바로 INSERT 하고 제약 위반 예외를 '이미 있다' 의 신호로 쓰세요. 배달 번호 검사가 먼저입니다 — 순서를 바꾸면 재전송이 duplicate_event 로 찍힙니다.

주문 상태가 뒤로 가지 않게

ledger.py 가 order_state(order_id, version, status) 를 두고, 지금 저장된 판보다 새것일 때만 적용하게 하세요. 뒤늦게 온 옛 판은 stored 는 true 지만 applied 는 false 이고 reason 은 stale_version 입니다.

도착 순서를 믿으면 안 됩니다. 이벤트가 들고 있는 version 을 지금 저장된 값과 견주세요. 처음 보는 주문이면 그대로 넣고, 이미 있으면 더 클 때만 갱신합니다. 같은 판이 다시 오는 경우도 갱신 대상이 아닙니다.

빨리 200 을 주고 나중에 처리하기

/root/wh/receiver.py 를 만들어 POST /webhook 이 서명과 시각 창만 보고 inbox 큐에 넣은 뒤 곧바로 200 {"queued": true} 를 주게 하세요. 받는 자리에서 원장을 건드리면 안 됩니다. --drain 은 서버를 띄우지 않고 큐를 원장에 흘려보냅니다.

배달 번호는 X-Delivery-Id, 서명은 X-Signature 헤더로 옵니다. 본문은 파싱하지 말고 원본 문자열 그대로 검증에 넘기세요. 검증에서 걸리면 400 입니다. 큐에 넣는 것과 원장에 적용하는 것을 나누면, 처리 시간이 상대의 타임아웃을 건드리지 않게 됩니다.

하루치를 하나도 빼지 않고 다시

/root/wh/replay_day.py/root/wh/deliveries.json 의 배달을 모두 흘려보내 /root/wh/day.db/root/wh/result.json 을 만드세요. 집계 칸은 deliveries·accepted·rejected_signature·rejected_stale·duplicate_delivery·duplicate_event·stored·applied·stale_version·orders 열 개입니다.

배달 목록 파일에 들어 있는 now 와 tolerance 를 그대로 기준 시각으로 쓰세요. 그래야 시험이 시계에 흔들리지 않습니다. 검증에서 떨어진 것은 원장까지 가지 않고, 중복으로 걸린 것은 stored 에 세지 않습니다. 하나도 빼지 말고 전부 흘려보내세요.

받는 쪽 점검 보고서

/root/wh/wh_report.md## 무엇이 들어왔나 ## 중복을 어떻게 걸렀나 ## 순서를 어떻게 다뤘나 ## 남은 위험과 운영 규칙 네 절로 적으세요. result.json 의 숫자가 본문에 들어가야 합니다.

읽는 사람은 우리 팀장이기도 하고 파트너사 담당자이기도 합니다. 각 갈래가 몇 건이었는지 숫자로 적고, 배달 번호만으로 걸렀다면 무엇을 놓쳤을지도 함께 적으세요. 남은 위험에는 비밀 교체와 시각 창의 폭이 반드시 들어갑니다.