ES /docs

Cognito#update_user! — redundant STS assume_role calls

RCA: Api::V1::UsersController#update Latency (avg 1230ms)

Overview#

What Happened#

2026-05-26 08:07~09:43 UTC 사이에 cupixworks-api 서비스의 PUT /api/v1/users/:id 엔드포인트에서 평균 1230ms, 최대 1334ms의 응답 지연이 6회 발생했다. 요청 자체는 모두 HTTP 200으로 성공했으나, 일반적인 API 응답 시간(500ms 미만)을 크게 초과했다.

Quick Facts#

Field Value
resource_name Api::V1::UsersController#update
top_frame lib/cupix/aws/cognito.rb:101 (update_user!)
env production, us-west-2
avg_duration 1230ms
max_duration 1334ms

Timeline#

  1. 2026-05-26T08:07:36Z — 최초 slow trace 감지
  2. 2026-05-26T09:43:28Z — 마지막 slow trace 기록 (6회 발생)
  3. 2026-05-27 — RCA 분석 완료

Error Log#

Datadog Logs

json
{
  "resource_name": "Api::V1::UsersController#update",
  "service": "cupixworks-api",
  "occurrences": 6,
  "avg_ms": 1230,
  "max_ms": 1334,
  "sample_trace_id": "359618823693017151"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 6
  • 최초 발생: 2026-05-26T08:07:36.993Z
  • 최근 발생: 2026-05-26T09:43:28.894Z

Root Cause Summary#

UsersController#update 요청이 @model.save! 호출 시 after_update :update_cognito_user 콜백을 통해 동기적으로 AWS Cognito API를 호출한다. Cupix::Aws::Cognito.update_user! 메서드는 매 호출 시 (1) STS assume_role로 임시 credential 생성, (2) list_users로 사용자 존재 확인, (3) 다시 STS assume_role, (4) admin_update_user_attributes로 속성 업데이트 — 총 4회의 순차적 HTTP 호출을 수행한다. AWS Cognito 클라이언트가 캐싱 없이 매번 새로 생성되므로 STS assume role 비용이 중복 발생하여 약 800~1000ms의 레이턴시가 추가된다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/users_controller.rb:42
  • before_action :set_userrepository_instance.show(params[:id])permission_joins SQL 쿼리 실행
  • update action → repository_instance.update(params.permit!)
  • BaseRepository#update (line 131): 권한 검증 수행
  • UserRepository#update (line 123): set_parameters(params)@model.save!
  • after_update :update_cognito_user 콜백 트리거 (firstname/lastname/locale 변경 시)
  • Cupix::Aws::Cognito.update_user! → 동기 HTTP 호출 다수
app/controllers/api/v1/users_controller.rb:42-46ruby
def update
  @model = repository_instance.update(params.permit!)

  super
end
app/repositories/user_repository.rb:123-139ruby
def update(params = {})
  super

  set_parameters(params)

  begin
    @model.save!

    if params[:password].present? && params[:new_password] && params[:new_password_confirm]
      Cupix::Mailer::UserMailer.password_changed(@model)
    end
  rescue StandardError => e
    raise Cupix::Errors::Parameter.new(code: 'ARG10001', reason: 'Invalid argument', message: e.message)
  end

  @model
end

@model.save!가 호출되면 after_update 콜백이 실행된다:

lib/cupix/aws/cognito/user.rb:7-12ruby
included do
  attr_accessor :skip_cognito_user_creation

  after_create :create_cognito_user, unless: :skip_cognito_user_creation
  after_update :update_cognito_user
end
lib/cupix/aws/cognito/user.rb:74-83ruby
def update_cognito_user
  return if saved_changes.blank? || (saved_changes.keys & %w[firstname lastname locale]).blank?

  flush_cognito_user_cache
  Cupix::Aws::Cognito.update_user!(self)
rescue StandardError => e
  Cupix::Logger.error("Failed to update cognito user: #{self.email}", function: __method__, module: 'Cupix::Aws', class: 'Cognito', error: e.message)
else
  Cupix::Logger.info("User updated in Cognito: #{email}", function: __method__, module: 'Cupix::Aws', class: 'Cognito', changes: saved_changes)
end
  • Failure point: lib/cupix/aws/cognito.rb:9-27client 메서드가 매 호출마다 STS assume role 수행
lib/cupix/aws/cognito.rb:9-27ruby
def client(region: nil)
  region ||= Cupix::Tesla.region

  sts_client = ::Aws::STS::Client.new(region: region)
  assumed_role = sts_client.assume_role(
    role_arn: $AWS.fetch(:cognito).fetch(:role),
    role_session_name: 'CognitoSession'
  )
  credentials = assumed_role[:credentials]

  ::Aws::CognitoIdentityProvider::Client.new(
    region: region,
    credentials: ::Aws::Credentials.new(
      credentials[:access_key_id],
      credentials[:secret_access_key],
      credentials[:session_token]
    )
  )
end
lib/cupix/aws/cognito.rb:101-124ruby
def update_user!(user)
  return unless user_exists?(user.email)  # client() + list_users API call

  client.admin_update_user_attributes(    # client() again + update API call
    user_pool_id: $AWS.fetch(:cognito).fetch(:user_pool_id),
    username: user.email,
    user_attributes: [
      {
        name: 'given_name',
        value: user.firstname.presence || ''
      },
      {
        name: 'family_name',
        value: user.lastname.presence || ''
      },
      {
        name: 'locale',
        value: user.locale.presence || 'en-US'
      }
    ]
  )

  Cupix::Logger.info("User updated in Cognito: #{user.email}", function: __method__, module: 'Cupix::Aws', class: 'Cognito')
end

update_user!에서의 호출 순서:

  1. user_exists?(email)get_user_by_email(email)client() (STS assume_role ~200ms) → list_users (~200ms)
  2. client() (STS assume_role ~200ms) → admin_update_user_attributes (~200ms)

총 ~800ms가 after_update 콜백에서 동기적으로 소요된다.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-api "User updated in Cognito"
text
service:cupixworks-api "UsersController#update"

핵심 로그 — Cognito update가 UsersController#update와 같은 시간대에 발생:

json
{
  "timestamp": "2026-05-26 18:12:14 KST",
  "status": "info",
  "message": "User updated in Cognito: patricia.samaniego@servexternos.repsol.com",
  "class": "Cognito",
  "function": "update_user!"
}
json
{
  "timestamp": "2026-05-26 17:19:29 KST",
  "status": "info",
  "message": "User updated in Cognito: 342cb45e624e5cd1b8b816f0f9f81963a50cd160@removed user",
  "class": "Cognito",
  "function": "update_cognito_user"
}
json
{
  "timestamp": "2026-05-26 17:15:40 KST",
  "status": "info",
  "message": "User updated in Cognito: 3d025bd49e7679fa4e9e545837064b0489e306f3@removed user",
  "class": "Cognito",
  "function": "update_cognito_user"
}

UsersController#update 요청 로그 (모두 200 응답 — 에러가 아닌 순수 레이턴시 문제):

json
{
  "timestamp": "2026-05-26 18:44:15 KST",
  "status": "info",
  "message": "[200] PUT /api/v1/users/31550 (Api::V1::UsersController#update)"
}
json
{
  "timestamp": "2026-05-26 17:21:40 KST",
  "status": "info",
  "message": "[200] PUT /api/v1/users/35668 (Api::V1::UsersController#update)"
}

추가로, UserRepository#remove 메서드도 assign_attributes(firstname: nil, lastname: nil)save 호출 시 update_cognito_user 콜백이 트리거되어 "removed user" 주소에 대해 불필요한 Cognito API 호출이 발생하고 있음을 확인했다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 after_update :update_cognito_user 콜백에서 동기 AWS API 호출이 레이턴시 원인 Cognito update_user! 로그가 동일 시간대에 발생; client() 메서드가 매번 STS assume_role 수행 (코드 lib/cupix/aws/cognito.rb:9-27); 4회 순차 HTTP 호출 구조 확인 Confirmed
H2 permission_joins SQL 쿼리 (3중 LEFT JOIN)가 slow query 원인 복잡한 SQL JOIN 구조 (app/repositories/user_repository.rb:210-264) permission_joins는 모든 User 엔드포인트에서 사용되며, #update에서만 선택적으로 느려지는 것은 SQL 문제가 아님을 시사; Datadog에 slow query 로그 없음 Rejected
H3 CarrierWave avatar 업로드(S3 + MiniMagick resize)가 레이턴시 원인 AvatarUploader가 fog(S3) storage + MiniMagick thumb 생성 사용 (app/uploaders/avatar_uploader.rb:5,31-37) 로그에서 "avatar does not exists" 메시지 확인 — 대부분 avatar 없는 사용자; avatar 업로드 시에는 1230ms보다 훨씬 더 오래 걸릴 것 (이미지 처리 + S3 2회 업로드); 6건 모두 유사한 1230-1334ms 범위는 avatar 처리보다 일관된 네트워크 레이턴시 패턴과 일치 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • lib/cupix/aws/cognito.rb:9-27client 메서드에 credential 캐싱 추가. STS assume role 결과를 TTL(예: 50분, 세션 최대 1시간)과 함께 캐싱하여 매 호출마다 STS 왕복을 방지.
  • lib/cupix/aws/cognito.rb:101-102update_user!에서 user_exists? 호출 제거. admin_update_user_attributes는 사용자가 없으면 자체적으로 UserNotFoundException을 반환하므로 사전 확인이 불필요. 이를 통해 2회의 HTTP 호출(STS + list_users)을 절감.

단기 개선 (1주 이내)#

  • lib/cupix/aws/cognito/user.rb:74update_cognito_user 콜백을 비동기(Sidekiq worker)로 전환. 사용자 프로필 업데이트 API 응답에 Cognito 동기화가 블로킹될 필요가 없다.
  • lib/cupix/aws/cognito/user.rb:75 — removed user(@removed user 이메일)에 대한 Cognito 업데이트를 skip하는 early return 추가. remove 메서드에서 firstname/lastname을 nil로 변경할 때 불필요한 Cognito 호출이 발생하고 있다.

장기 개선 (재발 방지)#

  • Cognito 동기화를 이벤트 기반 아키텍처로 전환 (예: after_commit + pub/sub 또는 dedicated sync worker).
  • AWS SDK 클라이언트를 싱글턴으로 관리하는 공통 인프라 레이어 구축 — 현재 client() 메서드가 캐싱 없이 매번 새 인스턴스를 생성하는 패턴이 다른 AWS 서비스 호출에서도 반복될 가능성이 있다.

Monitoring#

  • UsersController#update P95 레이턴시 모니터링 추가
  • Cognito API 호출 횟수 및 duration 메트릭 추가
text
avg:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::userscontroller_update} by {env}
text
sum:trace.aws.command{service:cupixworks-api,aws_service:cognito-idp}.as_count()

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard — 클라이언트 캐싱 및 비동기 전환은 잘 알려진 패턴이며, 기존 Sidekiq 인프라를 활용할 수 있다.