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#
- 2026-05-26T08:07:36Z — 최초 slow trace 감지
- 2026-05-26T09:43:28Z — 마지막 slow trace 기록 (6회 발생)
- 2026-05-27 — RCA 분석 완료
Error Log#
{
"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_user→repository_instance.show(params[:id])→permission_joinsSQL 쿼리 실행updateaction →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 호출 다수
def update
@model = repository_instance.update(params.permit!)
super
end
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 콜백이 실행된다:
included do
attr_accessor :skip_cognito_user_creation
after_create :create_cognito_user, unless: :skip_cognito_user_creation
after_update :update_cognito_user
end
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-27—client메서드가 매 호출마다 STS assume role 수행
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
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!에서의 호출 순서:
user_exists?(email)→get_user_by_email(email)→client()(STS assume_role ~200ms) →list_users(~200ms)client()(STS assume_role ~200ms) →admin_update_user_attributes(~200ms)
총 ~800ms가 after_update 콜백에서 동기적으로 소요된다.
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-api "User updated in Cognito"
service:cupixworks-api "UsersController#update"
핵심 로그 — Cognito update가 UsersController#update와 같은 시간대에 발생:
{
"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!"
}
{
"timestamp": "2026-05-26 17:19:29 KST",
"status": "info",
"message": "User updated in Cognito: 342cb45e624e5cd1b8b816f0f9f81963a50cd160@removed user",
"class": "Cognito",
"function": "update_cognito_user"
}
{
"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 응답 — 에러가 아닌 순수 레이턴시 문제):
{
"timestamp": "2026-05-26 18:44:15 KST",
"status": "info",
"message": "[200] PUT /api/v1/users/31550 (Api::V1::UsersController#update)"
}
{
"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-27—client메서드에 credential 캐싱 추가. STS assume role 결과를 TTL(예: 50분, 세션 최대 1시간)과 함께 캐싱하여 매 호출마다 STS 왕복을 방지.lib/cupix/aws/cognito.rb:101-102—update_user!에서user_exists?호출 제거.admin_update_user_attributes는 사용자가 없으면 자체적으로UserNotFoundException을 반환하므로 사전 확인이 불필요. 이를 통해 2회의 HTTP 호출(STS + list_users)을 절감.
단기 개선 (1주 이내)#
lib/cupix/aws/cognito/user.rb:74—update_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#updateP95 레이턴시 모니터링 추가- Cognito API 호출 횟수 및 duration 메트릭 추가
avg:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::userscontroller_update} by {env}
sum:trace.aws.command{service:cupixworks-api,aws_service:cognito-idp}.as_count()
Risk Assessment#
- Risk level: low
- 예상 복잡도: standard — 클라이언트 캐싱 및 비동기 전환은 잘 알려진 패턴이며, 기존 Sidekiq 인프라를 활용할 수 있다.