ES /docs

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#

  1. 2026-07-21 19:22:06 KSTCupix::Aws::Kinesis.put_records!kinesis.eu-central-1.amazonaws.com:443put_records 를 호출. AWS Ruby SDK 내부 Net::HTTP 가 open_timeout 초과로 execution expired 발생.
  2. 2026-07-21 19:22:06 KSTlib/cupix/aws/kinesis.rb:13-16rescue => e 절이 "Failed to put records: ..." 로 error 로깅 후 raise e 로 재발생 (본 클러스터, 2 건).
  3. 2026-07-21 19:22:06 KST — 상위 Cupix::EventService.publish_eventrescue RestClient::Exception 이 이 예외를 잡지 못하고 통과.
  4. 2026-07-21 19:22:06 KSTEventable::Events::Base.create_eventrescue StandardError 가 "Failed to create event: ..." 로 로깅 후 재발생 (sibling cluster 20291be6-..., 2 건).
  5. 이후 24 시간 창 (Datadog service:cupixworks-api "kinesis.eu-central-1") 에서 위 4 건 외 추가 발생 없음.

Error Log#

[Datadog Logs](https://app.datadoghq.com/logs?query=service%3Acupixworks-api%20status%3Aerror%20%40environment%3Aproduction%20%22Failed%20to%20put%20records%3A%20Failed%20to%20open%20TCP%20connection%20to%20kinesis.eu-central-1.amazonaws.com%3A443%20(exec%22&from_ts=1784625720000&to_ts=1784632980000&live=false)

text
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_eventrescue RestClient::Exception 필터에 의해 잡히지 않아 (Seahorse::Client::NetworkingError/Net::OpenTimeoutRestClient::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_eventCupix::EventService.publish_eventCupix::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::OpenTimeoutSeahorse::Client::NetworkingError 로 감싸 재발생.
lib/cupix/aws/kinesis.rb:5-17ruby
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):

lib/cupix/aws/kinesis.rb:19-21ruby
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 계층 (원인 사슬 재현):

lib/cupix/event_service.rb:21-54ruby
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::Exceptionrest-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 쿼리:

text
service:cupixworks-api status:error @environment:production "Failed to put records"

결과 (2 건, 모두 2026-07-21 19:22:06 KST = 10:22:06 UTC):

json
{
  "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):

text
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 를 raw e.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):

text
count:logs("service:cupixworks-api status:error @environment:production @class:Cupix::Aws::Kinesis @function:put_records!").rollup("count").by("region")

Kinesis 관련 raw 메시지 기반 (태그 미지원 옛 로그 fallback):

text
count:logs("service:cupixworks-api status:error @environment:production \"Failed to put records\"").rollup("count").by("region")

Kinesis endpoint TCP 오픈 실패 시그니처 (open_timeout 특정):

text
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 확장/트랜잭션 경계 재설계는 이벤트 소비자 계약 확인 필요.