ES /docs

Api::V1::AssetsController#update (avg 14630ms, max 14630ms)

RCA: Api::V1::AssetsController#update (avg 14630ms, max 14630ms)

Overview#

What Happened#

2026-07-01 11:05 KST 경 cupixworks-apiApi::V1::AssetsController#update 트레이스 1건이 14.6초간 지속된 뒤 HTTP 500으로 종료됐다. 원인은 cupix-tesla-static S3 버킷(리전: us-west-2)에 대한 PUT 요청이 잘못된 리전(us-east-1)으로 서명되어 AuthorizationHeaderMalformed로 거절됐고, Excon/Fog가 여러 차례 재시도하면서 요청 전체가 14초대까지 늘어난 것이다.

Quick Facts#

Field Value
resource_name Api::V1::AssetsController#update
upstream error Excon::Error::BadRequest (HTTP 400 from S3)
S3 error code AuthorizationHeaderMalformed
S3 message The authorization header is malformed; the region 'us-east-1' is wrong; expecting 'us-west-2'
bucket / host cupix-tesla-static / cupix-tesla-static.s3.amazonaws.com
trace_id 1807238531335885042
avg / max duration 14630ms / 14630ms
env production, us-west-2, tenant cupix

Affected Teams#

Team / Domain Error Count Impact
cupixworks-api / Assets 1 단일 자산(kucudjj1zg8k)의 PUT 요청이 14.6초 뒤 500으로 실패. 동일 창의 다른 Asset#update 수천 건은 200 응답.

Timeline#

  1. 2026-07-01 11:02:16 KST — 직전 svc:cupixworks-api::unknown service-degraded 인시던트(2026-07-01-svc-cupixworks-api--unknown-1, cluster 5건) 해소.
  2. 2026-07-01 11:05:19 KST — 문제의 Api::V1::AssetsController#update 트레이스 시작 (트레이스 ID 1807238531335885042).
  3. 2026-07-01 11:05:33 KST — S3가 AuthorizationHeaderMalformed로 응답 (Date: Wed, 01 Jul 2026 02:05:33 GMT). Excon/Fog 재시도 후 최종 실패.
  4. 2026-07-01 11:05:35 KST — Rails가 500 응답. Datadog 로그에 [500] PUT /api/v1/assets/kucudjj1zg8k (Api::V1::AssetsController#update) 기록.
  5. 2026-07-01 11:05:37 ~ 11:05:41 KST — 클라이언트가 cover_upload_urlcheck_cover_uploading으로 폴백. 이후 동일 asset 대한 GET /api/v1/assets/kucudjj1zg8k는 정상.

Error Log#

Datadog Logs

Datadog log — 2026-07-01 11:05:35 KSTtext
[500] PUT /api/v1/assets/kucudjj1zg8k (Api::V1::AssetsController#update)

Excon::Error::BadRequest
Expected(200) <=> Actual(400 Bad Request)
excon.error.response
  :body   => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
              <Error>
                <Code>AuthorizationHeaderMalformed</Code>
                <Message>The authorization header is malformed;
                         the region 'us-east-1' is wrong;
                         expecting 'us-west-2'</Message>
                <Region>us-west-2</Region>
                <RequestId>ANJADEW33FQVVSKD</RequestId>
              </Error>"
  :host   => "cupix-tesla-static.s3.amazonaws.com"
  :method => "PUT"
  :path   => "/cupix-tesla-static/cupix-tesla-static/uploads/tmp/1782871521-183424823984822-1247-0930/kucudjj1zg8k_thumbnail.jpg"
  :port   => 443
  :status => 400
  :status_line => "HTTP/1.1 400 Bad Request\r\n"

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-07-01 11:05:19 KST
  • 최근 발생: 2026-07-01 11:05:19 KST
  • 사용자 영향: 단일 자산의 썸네일 업로드 실패로 UI에서 저장 실패로 관측. 15초 지연 후 500. 같은 창의 다른 Asset#update 요청은 정상이었으므로 대규모 영향은 아님.

Root Cause Summary#

CarrierWave 글로벌 초기화가 cupix-tesla-static(리전 us-west-2) 버킷을 fog_directory로 사용하면서도 fog_credentialsregion 키를 지정하지 않았다. 결과적으로 Fog가 기본값(us-east-1)으로 SigV4 서명을 만들고 글로벌 엔드포인트 cupix-tesla-static.s3.amazonaws.com으로 PUT을 시도했다. 실제 버킷이 us-west-2에 존재하므로 S3가 AuthorizationHeaderMalformed(HTTP 400)로 거절했고, Excon/Fog의 자동 재시도가 반복되며 요청 전체가 14.6초 지연 후 500으로 종료됐다. CoverUploader, LogoUploader, AvatarUploader 등 개별 uploader는 initialize에서 명시적으로 region을 설정해 이 문제를 피하지만, 이번 요청은 이 보정이 없는 CarrierWave global 경로(캐시 저장, uploads/tmp/...)를 통해 S3로 나갔기 때문에 그대로 리전 불일치가 노출됐다. 요청 경로에 버킷명이 두 번(/cupix-tesla-static/cupix-tesla-static/uploads/tmp/...) 들어간 것도 path_style: true가 글로벌 호스트와 결합했을 때의 Fog 특성이며, 리전 문제와 함께 이 경로 구성이 잘못됐음을 시사한다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/assets_controller.rb:33-37AssetsController#updaterepository_instance.update(params)
  • Persistence: app/repositories/asset_repository.rb:14-26AssetRepository#updatesuperset_parameters@model.save!
  • Uploader mount: app/models/concerns/thumbnailable.rb:12mount_uploader :thumbnail, ThumbnailUploader
  • CarrierWave global config (문제 지점): config/initializers/carrierwave.rb:2-10fog_credentialsregion 없음
  • Uploader init: app/uploaders/thumbnail_uploader.rb:13-23self.fog_credentials.merge!(model.fog_credentials)로 model 값과 병합
  • Failure point: PUT https://cupix-tesla-static.s3.amazonaws.com/... → S3가 AuthorizationHeaderMalformed로 400 반환 (Fog 재시도 후 500)
config/initializers/carrierwave.rb:1-11ruby
Rails.application.config.to_prepare do
  CarrierWave.configure do |config|
    config.fog_credentials = {
      provider: 'AWS',
      aws_access_key_id: ENV['AWS_ACCESS_KEY_ID'] || Rails.application.credentials[Rails.env.to_sym][:aws][:access_key_id],
      aws_secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'] || Rails.application.credentials[Rails.env.to_sym][:aws][:secret_access_key],
      path_style: true
      # ← region 지정이 없음. Fog는 us-east-1로 폴백함.
    }
    config.fog_directory = $AWS[:s3][:static_bucket_name]  # 'cupix-tesla-static' — us-west-2 소재
  end
end
config/aws.yaml:396-397 (production 블록)yaml
      static_bucket_name: 'cupix-tesla-static'
      static_bucket_region: 'us-west-2'
app/uploaders/cover_uploader.rb:17-25 (비교: 개별 uploader의 리전 보정)ruby
def initialize(*)
  super

  self.fog_credentials['region'] = $AWS[:s3][:default_bucket_region]
  self.asset_host = "https://#{$AWS[:cloudfront][:default_hostname]}"
  self.fog_attributes = {
    cache_control: 'max-age=315576000'
  }
end
app/uploaders/thumbnail_uploader.rb:13-23ruby
def initialize(*)
  super

  self.asset_host = model.asset_host
  self.fog_credentials.merge!(model.fog_credentials)
  self.fog_directory = model.fog_directory

  self.fog_attributes = {
    cache_control: 'max-age=315576000'
  }
end

기대 동작: ThumbnailUploader#initializemodel.fog_credentials(= thumbnail_settings[:fog_credentials])로 base config를 병합하므로 리전이 static_bucket_region (=us-west-2)으로 덮여야 한다.

실제 동작: 문제의 요청 경로는 모델에 mount된 uploader의 store가 아니라 CarrierWave의 캐시(cache) 단계 (uploads/tmp/<cache_id>/<filename> 패턴)에서 S3로 PUT을 시도했다. 캐시 저장은 initializer의 global fog_credentials만 사용하며, region merge를 못 받으므로 us-east-1 서명이 그대로 나갔다. 도메인이 가상 호스팅 스타일(cupix-tesla-static.s3.amazonaws.com)로 잡히면서 path_style: true가 함께 적용돼 경로에 버킷명이 중복(/cupix-tesla-static/cupix-tesla-static/...) 출력된 것도 이 캐시 코드경로가 개별 uploader의 endpoint/asset_host 세팅을 우회했음을 뒷받침한다.

Uncertainty — 정확히 어느 시점(직접 파일 업로드? move_to_store 전 캐시?)에서 캐시 PUT이 발생했는지는 트레이스에 span 상세가 남지 않아 최종 확정하지 못했다 (@dd.trace_id:1807238531335885042 검색 시 로그 0건). 다만 실패 URL의 uploads/tmp/<CarrierWave CACHE_ID> 패턴은 CarrierWave Storage::Fog#cache!의 기본 경로와 일치한다.

Log Evidence#

Datadog 쿼리 (실제로 사용한 쿼리):

text
service:cupixworks-api "AssetsController" "update"

시간창: 2026-07-01T01:50:00Z ~ 2026-07-01T02:15:00Z

text
service:cupixworks-api "kucudjj1zg8k"

시간창: 2026-07-01T01:50:00Z ~ 2026-07-01T02:15:00Z

핵심 로그 (해당 asset의 시간 순서):

text
2026-07-01 11:05:17 KST  [200] GET /api/v1/assets/kucudjj1zg8k (show)
2026-07-01 11:05:17 KST  [302] GET /api/v1/assets/kucudjj1zg8k/download
2026-07-01 11:05:35 KST  [500] PUT /api/v1/assets/kucudjj1zg8k  ← 문제 발생 (14.6s 트레이스)
2026-07-01 11:05:37 KST  [200] POST /api/v1/assets/kucudjj1zg8k/cover_upload_url
2026-07-01 11:05:41 KST  [200] PUT /api/v1/assets/kucudjj1zg8k/check_cover_uploading

500 응답의 error 페이로드 (원문 발췌):

json
{
  "class": "Excon::Error::BadRequest",
  "message": "Expected(200) <=> Actual(400 Bad Request)",
  "body": "<Error><Code>AuthorizationHeaderMalformed</Code><Message>The authorization header is malformed; the region 'us-east-1' is wrong; expecting 'us-west-2'</Message><Region>us-west-2</Region><RequestId>ANJADEW33FQVVSKD</RequestId></Error>",
  "host": "cupix-tesla-static.s3.amazonaws.com",
  "method": "PUT",
  "path": "/cupix-tesla-static/cupix-tesla-static/uploads/tmp/1782871521-183424823984822-1247-0930/kucudjj1zg8k_thumbnail.jpg",
  "status": 400
}

동일 시간창의 대조 로그 — 다른 cupix-tesla-static 호출은 올바른 리전 엔드포인트(s3.us-west-2.amazonaws.com) 사용:

json
{
  "timestamp": "2026-07-01 11:31:17 KST",
  "message": "[400] PUT /api/v1/captures/724249 (Api::V1::CapturesController#update)",
  "host": "s3.us-west-2.amazonaws.com",
  "path": "/cupix-tesla-static/8ebe4fde3b207eaa/thumbnail/captures/724249/weoneil.724249_1418035_pointcloud_thumbnail_1.jpg",
  "status": 503,
  "s3_code": "SlowDown"
}

같은 버킷에 대한 정상 요청이 s3.us-west-2.amazonaws.com (path-style, region 명시)로 나가는데 비해, 실패한 요청은 cupix-tesla-static.s3.amazonaws.com (virtual-hosted, region 미명시)로 나갔다. 두 경로의 Fog 설정이 다르다는 것을 실측 로그로 확인.

Status board 조회 결과:

json
{
  "scope": "svc:cupixworks-api::unknown",
  "active": null,
  "recent_incident_id": "2026-07-01-svc-cupixworks-api--unknown-1",
  "resolved_at": "2026-07-01T02:02:16.858Z"
}

문제 트레이스는 직전 서비스 저하 인시던트 해소(11:02:16 KST) 이후 약 3분 뒤에 발생. 별개 이슈로 판단.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 CarrierWave global fog_credentialsregion 미지정 → Fog가 us-east-1로 서명 → us-west-2 버킷이 AuthorizationHeaderMalformed로 거절, Excon/Fog 재시도로 14.6s 소요 후 500 S3 응답 원문 region 'us-east-1' is wrong; expecting 'us-west-2' (500 로그). config/initializers/carrierwave.rb:3-8에 region 없음. aws.yaml:396-397 static_bucket_region: us-west-2. 실패 URL host cupix-tesla-static.s3.amazonaws.com (region 미명시 글로벌 엔드포인트). 다른 정상 호출은 s3.us-west-2.amazonaws.com 사용. Confirmed
H2 최근 배포에서 리전 설정을 잘못 변경 배포 SHA 미확인. git log로 확인한 카리어웨이브 initializer 히스토리 미조사. 단, 실패는 1건이고 동일 시간창에서 다른 Asset#update 수천 건이 성공 → 광범위 배포 회귀는 아님. 동일 창의 성공 트래픽 대량 존재. 이전 14일 내 AuthorizationHeaderMalformed 로그 검색 시 결과 0건이지만, 이는 로그 인덱싱 시간차일 가능성도 있음. Rejected (증거 부족, 광범위 회귀 아님)
H3 S3 us-west-2 리전 자체의 일시적 장애 svc:cupixworks-api::unknown 최근 인시던트가 11:02:16 KST에 해소된 직후 발생 S3는 명확한 400을 즉시 응답 (2xx도 있어야 할 시점의 5xx가 아님). 리전 코드 문제 메시지는 클라이언트 서명 오류이지 서버측 장애가 아님. 동시각 다른 us-west-2 요청은 성공. Rejected
H4 애플리케이션 재시도(Sidekiq re-enqueue 등)가 14s를 만든 원인 트레이스 시작 02:05:19 → S3 응답 헤더 Date 02:05:33 → 500 로그 02:05:35 (16s span) 이는 Excon/Fog retry_limit(기본 4)와 retry_interval의 지수 백오프에 부합. 애플리케이션 레벨 재시도 로그 없음. Rejected (Fog 내부 재시도로 충분히 설명됨)
H5 요청 경로에 버킷명이 중복(/cupix-tesla-static/cupix-tesla-static/uploads/tmp/...) — 별도의 경로 조합 버그 실패 URL path에 실제로 중복 확인. 중복 경로 자체는 서명 오류(us-east-1 vs us-west-2)의 원인은 아니며, path_style: true + virtual-hosted host 결합의 부산물일 수 있음. Region이 올바르면 S3는 405/404를 낼 가능성. 개별 문제이나 이번 500의 root cause는 아님. Inconclusive (동반 이슈로 추적 필요)

Fix Recommendation#

즉시 조치 (Critical)#

  • config/initializers/carrierwave.rb:3-8fog_credentialsregion: $AWS[:s3][:static_bucket_region]을 추가. fog_directorystatic_bucket_name이므로 대응하는 static_bucket_region을 기본으로 두는 것이 자연스럽다. 이렇게 하면 개별 uploader가 region을 덮어씌우지 않는 코드 경로(캐시, CarrierWave 내부 store 이동 등)에서도 올바른 SigV4 서명을 사용한다.
  • 이미 개별 uploader들이 initialize에서 region을 덮고 있어 회귀 위험은 낮다. 그러나 최소한 QA 환경에서 배포 전 검증 필요.

단기 개선 (1주 이내)#

  • 실패 URL의 /cupix-tesla-static/cupix-tesla-static/uploads/tmp/... 이중 버킷명 경로가 진짜로 나가는지 로그 재현 (Fog path_style + endpoint 조합 검증). 필요시 CarrierWave initializer에서 endpoint를 명시적으로 https://s3.<region>.amazonaws.com으로 지정해 virtual-hosted/global 혼용을 제거.
  • Fog/Excon 재시도로 인해 클라이언트 관점에서 14초 지연이 발생. connection_options: { retry_limit: 1 } 정도로 낮춰 리전/권한 오류처럼 재시도 무의미한 4xx를 빠르게 실패시키고 사용자 대기시간을 줄이는 것을 검토.
  • AuthorizationHeaderMalformed 발생 시 즉시 알림이 갈 수 있도록 로그에 명시적 태그(s3_error_code:AuthorizationHeaderMalformed)로 추출해 Datadog monitor에 태우기.

장기 개선 (재발 방지)#

  • CarrierWave/Fog 설정을 한 곳(예: Cupix::StorageService)에서 중앙 관리하고 uploader마다 region을 개별 세팅하는 관행을 제거. 리전이 항상 base config에서 결정되도록 하면 이번 같은 "누락된 세팅" 회귀가 재발하지 않는다.
  • 배포 시 S3 접근을 대상으로 하는 헬스체크(작은 오브젝트 PUT/DELETE)를 추가해 리전 오설정을 즉시 감지.

Monitoring#

Datadog timeseries widget에 넣을 쿼리 (writing-datadog-monitoring-queries 규칙 준수 — pipe/stats 등 monitor-only 문법 금지):

text
sum:trace.rack.request.errors{service:cupixworks-api,resource_name:api::v1::assetscontroller#update}.as_count()
text
avg:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::assetscontroller#update}

로그 기반 카운트 (facet 기반, 이미 표시되는 로그가 있음):

text
service:cupixworks-api "AuthorizationHeaderMalformed"
text
service:cupixworks-api "cupix-tesla-static" status:error

권장 알림:

  • Api::V1::AssetsController#update의 p95 duration이 5s를 넘거나 5xx율이 1%를 초과하면 알림.
  • 로그에 AuthorizationHeaderMalformed가 5분에 1건 이상이면 알림 (실질적으로 0건이 정상).

Risk Assessment#

  • Risk level: low (수정 범위는 initializer 1개 파일, 개별 uploader의 명시적 region 세팅을 그대로 상속받는 형태라 회귀 위험 최소)
  • 예상 복잡도: trivial (한 줄 추가 + QA 배포 검증)