Failed to put records: Failed to open TCP connection to kinesis.eu-central-1.amazonaws.com:443 (exec
RCA: Failed to put records — Kinesis TCP connect timeout (eu-central-1)
Overview#
What Happened#
2026-07-21 19:22 KST 시점에 cupixworks-api (eu-central-1 / cupix tenant) 에서 Cupix::Aws::Kinesis.put_records! 가 AWS Ruby SDK 를 통해 kinesis.eu-central-1.amazonaws.com:443 로 TCP connect 를 시도하다 execution expired (Net::HTTP open_timeout 초과) 로 실패했다. 같은 순간 2 건이 관측되었고, 짝을 이루는 상위 클러스터 20291be6-43c6-498d-9e5c-fc558101b94b ("Failed to create event") 와 함께 총 4 개의 로그가 남았다. 본 클러스터는 예외 사슬의 최하위 log site (Cupix::Aws::Kinesis#put_records! 의 rescue 지점) 에 해당한다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Seahorse::Client::NetworkingError (wrapping Net::OpenTimeout) — inferred from message |
| exception.message | Failed to open TCP connection to kinesis.eu-central-1.amazonaws.com:443 (execution expired) |
| top_frame | lib/cupix/aws/kinesis.rb:14 (log site) — underlying raise from kinesis_client.put_records at lib/cupix/aws/kinesis.rb:9 |
| env | production, region eu-central-1, tenant cupix |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| cupixworks-api (eu-central-1) | 2 (본 클러스터) + 2 (sibling Eventable::Events::Update#create_event) = 4 |
동일 요청 2 건에서 Kinesis 발행 실패가 상위 after_update 콜백까지 전파되어 사용자 트랜잭션이 롤백됨 |
Timeline#
- 2026-07-21 19:22:06 KST —
Cupix::Aws::Kinesis.put_records!가kinesis.eu-central-1.amazonaws.com:443로put_records를 호출. AWS Ruby SDK 내부 Net::HTTP 가 open_timeout 초과로execution expired발생. - 2026-07-21 19:22:06 KST —
lib/cupix/aws/kinesis.rb:13-16의rescue => e절이 "Failed to put records: ..." 로 error 로깅 후raise e로 재발생 (본 클러스터, 2 건). - 2026-07-21 19:22:06 KST — 상위
Cupix::EventService.publish_event의rescue RestClient::Exception이 이 예외를 잡지 못하고 통과. - 2026-07-21 19:22:06 KST —
Eventable::Events::Base.create_event의rescue StandardError가 "Failed to create event: ..." 로 로깅 후 재발생 (sibling cluster20291be6-..., 2 건). - 이후 24 시간 창 (Datadog
service:cupixworks-api "kinesis.eu-central-1") 에서 위 4 건 외 추가 발생 없음.
Error Log#
Failed to put records: Failed to open TCP connection to kinesis.eu-central-1.amazonaws.com:443 (execution expired)
Impact#
- Service:
cupixworks-api - 발생 횟수: 2
- 최초 발생: 2026-07-21 19:22 KST
- 최근 발생: 2026-07-21 19:22 KST
- Region/Tenant: eu-central-1 / cupix
Root Cause Summary#
Cupix::Aws::Kinesis.put_records! 는 AWS Ruby SDK (aws-sdk-kinesis, Seahorse HTTP stack) 를 통해 kinesis.eu-central-1.amazonaws.com:443 로 PutRecords 요청을 보내는 얇은 wrapper 이다. 이 wrapper 는 kinesis_client.put_records 호출을 감싸는 rescue => e 에서 실패 로그를 남기고 예외를 그대로 재발생시킨다. 2026-07-21 10:22:06 UTC 에 이 호출이 소켓 오픈 단계에서 실패했다 — 에러 메시지 Failed to open TCP connection to ...:443 (execution expired) 는 Ruby Net::HTTP open_timeout 시그니처로, AWS SDK 응답을 받기 이전 단계다. 재발생된 예외는 상위 Cupix::EventService.publish_event 의 rescue RestClient::Exception 필터에 의해 잡히지 않아 (Seahorse::Client::NetworkingError/Net::OpenTimeout 은 RestClient::Exception 계층이 아님) 그대로 통과하며 최종적으로 Eventable::Events::Base.create_event 의 rescue 로그(20291be6-...) 로 남는다. 24 시간 창 내 동일 리전 재발이 없어 순간적 네트워크/endpoint 반응 지연으로 판단되며, put_records! wrapper 자체가 timeout/retry 옵션을 지정하지 않고 SDK 기본값에 의존한다는 점과 로그가 raw 메시지만 남긴다는 점이 신뢰성 관점의 구조적 결함이다.
Technical Analysis#
Code Path#
- Entry point: ActiveRecord
after_update콜백 →Eventable::Events::Update.create_event→Cupix::EventService.publish_event→Cupix::Aws::Kinesis.put_records!(sibling RCA 참조) - Failure point (본 클러스터의 log site):
lib/cupix/aws/kinesis.rb:9-16 - Underlying raise: AWS Ruby SDK 의 Seahorse HTTP 클라이언트가
Net::OpenTimeout을Seahorse::Client::NetworkingError로 감싸 재발생.
def put_records!(opts = {})
raise Cupix::Errors::Argument.new(code: 'ARG10001', reason: 'stream_name is blank') if opts[:stream_name].blank?
raise Cupix::Errors::Argument.new(code: 'ARG10001', reason: 'records is blank') if opts[:records].blank?
kinesis_client.put_records({
stream_name: opts[:stream_name],
records: opts[:records]
})
rescue => e
Cupix::Logger.error("Failed to put records: #{e.message}", class: self.name, function: __method__, stream_name: opts[:stream_name], record_count: opts[:records]&.size, record_sizes: opts[:records]&.map { |r| r[:data]&.bytesize })
raise e
end
기대 동작: 호출자가 지정한 records 를 Kinesis stream 에 성공적으로 발행하거나, 실패 시 계층별 rescue 가 예외를 도메인 에러 (Cupix::Errors::System 등) 로 변환한다. 실제 동작: 네트워크 오픈 단계 실패 시 raw Seahorse::Client::NetworkingError 를 그대로 상위로 재발생시키며, 상위 rescue 필터가 이를 처리하지 못해 after_update 콜백을 뚫고 나가 사용자 트랜잭션을 롤백한다.
Kinesis 클라이언트 생성 (lib/cupix/aws/kinesis.rb:19-21):
def kinesis_client
@kinesis_client ||= ::Aws::Kinesis::Client.new(region: ::Cupix::Tesla.region)
end
region 외 어떤 옵션(retry_limit, retry_backoff, http_open_timeout, http_read_timeout) 도 지정하지 않아 AWS SDK 기본값에 의존한다. Ruby aws-sdk-core 기본값은 http_open_timeout=15s, retry_limit=3, adaptive backoff 이므로 단발성 연결 실패에 대한 방어 폭이 제한적이며 이번 사건처럼 open_timeout 만료가 그대로 표면화될 수 있다.
상위 rescue 계층 (원인 사슬 재현):
def self.publish_event(events = [])
return if events.blank?
return if %w[development test].include?(Rails.env)
return unless release_date_by(Rails.env)
# ...
response = Cupix::Aws::Kinesis.put_records!({ stream_name: stream_name, records: records })
# ...
rescue RestClient::Exception => e
Cupix::Logger.error("Failed to publish event: #{e.message}", ...)
raise Cupix::Errors::System.new(code: 'SYS20000', reason: "Failed to publish event: #{e.message}")
end
RestClient::Exception 은 rest-client gem 계열 예외 클래스이며 AWS Ruby SDK 예외 계층(Seahorse::Client::NetworkingError, Aws::Kinesis::Errors::ServiceError, Net::OpenTimeout 등) 과 무관하다. 따라서 이 rescue 는 실제로 발동되지 않고, SYS20000 으로의 매핑 경로가 죽어있다. Datadog 상 "Failed to publish event" 로그가 남지 않고 상위 Eventable::Events::Base rescue 로그만 남은 것이 이 사실을 방증한다.
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-api status:error @environment:production "Failed to put records"
결과 (2 건, 모두 2026-07-21 19:22:06 KST = 10:22:06 UTC):
{
"timestamp": "2026-07-21 19:22:06",
"status": "error",
"message": "Failed to put records: Failed to open TCP connection to kinesis.eu-central-1.amazonaws.com:443 (execution expired)",
"class": "Cupix::Aws::Kinesis",
"function": "put_records!"
}
원인 사슬 확장 쿼리 (본 클러스터 + sibling):
service:cupixworks-api status:error @environment:production "kinesis.eu-central-1"
24 시간 창에서 총 4 건, 모두 동일 timestamp 에 집중. 2 건은 본 클러스터 (class Cupix::Aws::Kinesis, function put_records!), 2 건은 sibling cluster 20291be6-... (class Eventable::Events::Update, function create_event, error.msg 필드에 동일 raw 메시지 재현).
Status board 확인 (bun run cli/incident-board.ts for-cluster 3539f442-...) 결과 scope: "svc:cupixworks-api::unknown", active: null. dep:* 스코프 외부 종속성 인시던트가 아니며, 최근 자동 감지된 인시던트 2026-07-21-svc-cupixworks-api--unknown-2 는 본 클러스터와 sibling 20291be6-... 만 포함하고 이미 resolved 상태다.
확인되지 않은 항목 (uncertain — needs verification):
- AWS Health Dashboard 상 eu-central-1 Kinesis 부분 장애 유무.
- 해당 호스트 outbound DNS/네트워크 지연 메트릭 및 VPC endpoint 상태.
- 동시각의 다른 리전(us-east-1 등) 이나 다른 서비스에서 Kinesis 관련 이상 유무는 이 스킬 범위 밖.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | kinesis.eu-central-1.amazonaws.com:443 로의 TCP connect 이 순간적 네트워크/endpoint 반응 지연으로 open_timeout 을 초과, AWS SDK 가 Seahorse::Client::NetworkingError(underlying Net::OpenTimeout) 을 던져 Cupix::Aws::Kinesis#put_records! 의 rescue 로그가 남고 그대로 재발생됨 |
로그 메시지 Failed to open TCP connection to ...:443 (execution expired) 는 Ruby Net::HTTP open_timeout 시그니처. lib/cupix/aws/kinesis.rb:9 는 AWS SDK(Seahorse) 사용. 동일 timestamp 에 sibling cluster (상위 rescue 로그) 가 함께 발생. Status board dep-scope 아님 |
— | Confirmed |
| H2 | Kinesis eu-central-1 지속적 리전 장애 | 에러 텍스트가 리전 endpoint 를 지목 | 24h 창 (service:cupixworks-api "kinesis.eu-central-1") 에서 동시각 4 건 외 재발 0 건. 지속 장애면 수십/수백 건 관측되어야 함 |
Rejected |
| H3 | 애플리케이션 인자(stream_name, records) 유효성 문제로 인한 실패 |
— | 실패 문구가 TCP connect 단계 (open TCP connection) 로 AWS 응답 이전. put_records! 는 stream_name/records blank 시 Cupix::Errors::Argument(ARG10001) 을 먼저 raise 하므로 이 경로면 다른 에러 텍스트가 남음 |
Rejected |
| H4 | Cupix::Aws::Kinesis#put_records! 의 rescue => e 자체가 원인 (예: 로깅 중 예외) |
— | rescue 는 raw e.message 만 로그로 남기고 즉시 raise e — 로깅 실패 시 재발생 흐름이 깨질 뿐 open_timeout 시그니처 메시지가 남을 이유가 없음 |
Rejected |
| H5 | Status board 에서 이미 open 된 외부 종속성 인시던트가 있어 별도 조사 불필요 | — | for-cluster 응답 scope: "svc:cupixworks-api::unknown", active: null. dep-scope 아님이며 이 스킬은 svc-scope 는 정상 RCA 진행을 지시 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
lib/cupix/aws/kinesis.rb:14— 로그 payload 를 rawe.message대신 예외 객체(error: e) 를 함께 넘겨 상위 관찰성 개선. 현재는 예외 클래스(예:Seahorse::Client::NetworkingError)가 로그에 남지 않아 사건 분류가 어렵다. (구현 방향만 — 코드 작성 금지)lib/cupix/event_service.rb:50— sibling RCA (20291be6-...) 와 동일한 근본 결함.rescue RestClient::Exception은 AWS SDK 예외 계층(Seahorse::Client::NetworkingError,Aws::Kinesis::Errors::ServiceError,Net::OpenTimeout,Net::ReadTimeout)을 잡지 못한다. rescue 를 확장해 AWS SDK 계열까지Cupix::Errors::System(code: 'SYS20000', ...)로 일관 변환해야 한다. 이 조치가 없으면put_records!재발생 예외는 계속 도메인 트랜잭션을 롤백시킨다.- 위 두 조치는 sibling RCA 의 즉시 조치(2 항 —
Eventable::Events::Base트랜잭션 경계 재설계) 와 상호 보완적이며 병행 가능.
단기 개선 (1주 이내)#
lib/cupix/aws/kinesis.rb:20—::Aws::Kinesis::Client.new옵션 명시화. 최소한http_open_timeout,http_read_timeout,retry_limit,retry_backoff을 팀 표준값으로 지정해 SDK 기본값 의존을 제거하고 순간적 open_timeout 에 대해 SDK 레벨 재시도가 효과적으로 동작하도록 한다. 값 자체는 Kinesis PutRecords 특성(짧은 latency 요구, thundering herd 회피) 을 반영해 결정 필요.- 발행 경로를 요청 스레드에서 분리. 이미 존재하는
KinesisPutRecordsWorker(app/workers/kinesis_put_records_worker.rb:8-32,queue: :aws, retry: 3) 를Cupix::EventService.publish_event경로에서도 활용하도록 조정하는 방향 검토. 이 경우 API 응답 시간이 Kinesis 네트워크 지연에 종속되지 않으며 실패 재시도가 Sidekiq 큐로 흡수된다.
장기 개선 (재발 방지)#
- 도메인 트랜잭션과 audit-event 발행의 결합 해제 (outbox 패턴 또는
after_commit+ 지속성 있는 재시도 큐). 발행 실패가 사용자 요청 성공 여부를 결정하지 않아야 한다. - AWS Ruby SDK 예외 계층 → Cupix 도메인 에러 매핑을 공통 유틸로 추출 (
Seahorse::Client::NetworkingError,Aws::Errors::ServiceError등). 각 wrapper (Kinesis, S3, Lambda 등) 가 개별적으로 raw 예외를 재발생하지 않도록. - Kinesis 발행 신뢰성 SLO 정의 및 실패율 알람. 리전 단위 (
region:eu-central-1등) 로 분해된 대시보드.
Monitoring#
release dashboard timeseries widget 에 추가할 Datadog 쿼리:
Cupix::Aws::Kinesis#put_records! 자체 실패 카운트 (log-based, 리전별 breakdown):
count:logs("service:cupixworks-api status:error @environment:production @class:Cupix::Aws::Kinesis @function:put_records!").rollup("count").by("region")
Kinesis 관련 raw 메시지 기반 (태그 미지원 옛 로그 fallback):
count:logs("service:cupixworks-api status:error @environment:production \"Failed to put records\"").rollup("count").by("region")
Kinesis endpoint TCP 오픈 실패 시그니처 (open_timeout 특정):
count:logs("service:cupixworks-api status:error \"Failed to open TCP connection to kinesis\" \"execution expired\"").rollup("count").by("region")
알림 임계 예시: 5 분 창에서 10 건 초과 warn, 30 건 초과 alert. 현재 사건(2 건) 은 임계 아래이며 반복 발생 시 노출 목적.
Risk Assessment#
- Risk level: medium — 단발성 네트워크 이벤트 자체는 low-severity 이나,
put_records!예외가 상위 rescue 필터의 결함으로 인해 사용자 트랜잭션 롤백까지 확산되는 구조적 결합이 medium-severity. 리전 단위 지속 장애 재발 시 blast radius 가 크게 증폭될 수 있다. - 예상 복잡도: standard — 로그 payload 개선과 SDK 옵션 명시화는 trivial, rescue 확장/트랜잭션 경계 재설계는 이벤트 소비자 계약 확인 필요.