ES /docs

User#create_cognito_user — invalid email format passed to Cognito

RCA: Failed to create cognito user: asdf@sdaaljsd.3

Overview#

What Happened#

2026-07-07 10:30:15 KST cupixvista-api (us-west-2, production)에서 신규 User 저장 후 after_create :create_cognito_user 콜백이 AWS Cognito admin_create_user를 호출했으나, Cognito가 Username should be an email. (Aws::CognitoIdentityProvider::Errors::InvalidParameterException) 로 요청을 거부했다. 본 클러스터의 이메일 값은 asdf@sdaaljsd.3 로 TLD가 한 자리 숫자(.3)라 실제 이메일로 볼 수 없는 값이다. 같은 시간대에 유사한 잘못된 형식(123@232.231c, 2@23.2, 33@3.21 등)으로 총 7건이 status-board에서 2026-07-07-svc-cupixvista-api--unknown-1 인시던트로 그룹화되었다. create_cognito_user는 예외를 rescue한 뒤 error 로그만 남기고 종료하므로 User 레코드는 DB에 커밋된 채 Cognito 계정만 부재한 상태 부정합이 발생할 수 있다.

Quick Facts#

Field Value
exception.class Aws::CognitoIdentityProvider::Errors::InvalidParameterException (rescue된 StandardError)
exception.message Username should be an email.
top_frame lib/cupix/aws/cognito/user.rb:69 (create_cognito_user rescue 지점)
env production, us-west-2
service cupixvista-api

Affected Teams#

Team / Domain Error Count Impact
cupixvista-api (Cognito integration) 1 (본 클러스터), 7 (동일 원인 24h) 잘못된 이메일 형식으로 User 생성 시 Cognito 등록 실패. DB에는 User가 남지만 Cognito 계정 부재로 이후 로그인·MFA·비밀번호 재설정 흐름에서 조용한 실패 발생 가능.

Timeline#

  1. 2026-07-07 10:30:15 KST — 본 클러스터 이벤트 (asdf@sdaaljsd.3) 발생 (first_seen / last_seen). 그룹화된 인시던트의 최초 이벤트.
  2. 2026-07-07 10:30:49 KST — 관련 클러스터 2c20f518-7ea0-4ed6-bfff-75d94400a8fc (123@232.231c).
  3. 2026-07-07 10:31:33 KST — 마지막 관련 실패 (33@3.21). status-board가 세 개의 클러스터 (381fd5d8..., 2c20f518..., b48cdcdd...)를 2026-07-07-svc-cupixvista-api--unknown-1 로 그룹화.

Error Log#

Datadog Logs

text
Failed to create cognito user: asdf@sdaaljsd.3

Datadog에서 확인한 구조화된 로그:

json
{
  "timestamp": "2026-07-07 10:30:15",
  "status": "error",
  "message": "Failed to create cognito user: asdf@sdaaljsd.3",
  "class": "Cognito",
  "function": "create_cognito_user",
  "error": { "msg": "Username should be an email." }
}

바로 직전 info 로그(같은 밀리초):

json
{
  "timestamp": "2026-07-07 10:30:15",
  "status": "info",
  "message": "Creating user in Cognito: asdf@sdaaljsd.3",
  "class": "Cognito",
  "function": "create_cognito_user"
}

Impact#

  • Service: cupixvista-api
  • Team: cupix
  • 발생 횟수: 1 (본 클러스터), 관련 클러스터 포함 24h 내 총 7건
  • 최초 발생: 2026-07-07 10:30 KST
  • 최근 발생: 2026-07-07 10:30 KST

Root Cause Summary#

Rails User 모델의 이메일 validation은 URI::MailTo::EMAIL_REGEXP를 사용하는데, 이 정규식은 local@domain 형태만 검사할 뿐 TLD의 실제 유효성(길이·문자 종류·숫자만으로 구성 금지 등)은 검사하지 않는다. 그래서 asdf@sdaaljsd.3 처럼 사실상 TLD가 유효하지 않은 값도 통과된다. 이후 after_create :create_cognito_user 콜백이 AWS Cognito admin_create_user를 호출할 때 Cognito 측이 훨씬 엄격한 이메일 검증을 수행해 InvalidParameterException: Username should be an email.을 반환하고, create_cognito_user는 이를 rescue StandardError로 삼키고 error 로그만 남긴 채 종료한다. 결과적으로 (1) 매 요청마다 error 로그가 쌓이고, (2) User 레코드는 이미 커밋되어 있어 Cognito와의 상태 부정합이 발생한다. 즉 root cause는 Rails validation과 Cognito의 이메일 형식 기준이 일치하지 않으며, Cognito 호출 실패를 트랜잭션 롤백이 아닌 로그로만 처리한다는 점이다.

Technical Analysis#

Code Path#

Entry point: app/models/user.rb User 저장 (컨트롤러/폼에서 save/create).

  1. 이메일 validation 통과 — URI::MailTo::EMAIL_REGEXP 는 TLD 유효성 검증이 없다.
app/models/user.rb:80-86ruby
validates :team_id, presence: true
validates :email,
          uniqueness: { scope: :team_id, case_sensitive: true },
          format: { with: URI::MailTo::EMAIL_REGEXP },
          presence: true,
          unless: :deleted_user?
  1. after_create :create_cognito_user 콜백 등록 — User가 DB에 저장된 뒤 호출된다.
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
  1. Failure point: create_cognito_user 는 Cognito 호출에서 발생한 예외를 rescue하고 error 로그만 남긴 채 조용히 종료한다. 예외를 재발생시키지 않으므로 트랜잭션이 롤백되지 않는다.
lib/cupix/aws/cognito/user.rb:54-72ruby
def create_cognito_user
  if Cupix::Aws::Cognito.user_exists?(self.email)
    Cupix::Logger.info("User already exists in Cognito: #{self.email}", function: __method__, module: 'Cupix::Aws', class: 'Cognito')

    cognito_user = Cupix::Aws::Cognito.get_user_by_email(self.email)

    self.update!(cognito_user_id: cognito_user.attributes.find { |attr| attr.name == 'sub' }.value)
  else
    Cupix::Logger.info("Creating user in Cognito: #{self.email}", function: __method__, module: 'Cupix::Aws', class: 'Cognito')

    cognito_user_creation_response = Cupix::Aws::Cognito.create_user!(self)

    self.update!(cognito_user_id: cognito_user_creation_response.user.attributes.find { |attr| attr.name == 'sub' }.value)
  end
rescue StandardError => e
  Cupix::Logger.error("Failed to create cognito user: #{self.email}", function: __method__, module: 'Cupix::Aws', class: 'Cognito', error: e.message)
else
  Cupix::Logger.info("User created in Cognito: #{email}", function: __method__, module: 'Cupix::Aws', class: 'Cognito')
end
  1. 실제 Cognito 호출: admin_create_userusername: user.email 로 요청되고 AWS 측이 Username should be an email. 을 반환한다.
lib/cupix/aws/cognito.rb:42-76ruby
def create_user!(user)
  if user.email.include?('removed user')
    raise Cupix::Errors::Argument.new(code: 'AUTH20013', reason: "Can't create an user on Cognito: removed_user")
  end

  raise Cupix::Errors::Argument.new(code: 'AUTH20012', reason: "User #{user.email} already exists on Cognito") if user_exists?(user.email)

  response = client.admin_create_user(
    username: user.email,
    message_action: 'SUPPRESS',
    temporary_password: random_password,
    user_attributes: [
      { name: 'email_verified', value: 'true' },
      { name: 'given_name', value: user.firstname.presence || '' },
      { name: 'family_name', value: user.lastname.presence || '' },
      { name: 'locale', value: user.locale.presence || 'en-US' },
      { name: 'email', value: user.email }
    ],
    user_pool_id: $AWS.fetch(:cognito).fetch(:user_pool_id)
  )
  ...
end

기대 동작 vs 실제 동작

  • 기대: 이메일이 실제로 유효하지 않다면 저장 자체가 거부되어 사용자에게 명확한 검증 오류가 반환되어야 한다.
  • 실제: Rails validation은 통과, Cognito 호출은 실패, User는 DB에 남고 로그에 error 만 기록. Cognito 계정이 없는 "유령 User"가 생성될 수 있고, caller(컨트롤러/폼)는 200/성공 응답을 받는다.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixvista-api "asdf@sdaaljsd.3"
text
service:cupixvista-api "Failed to create cognito user"

본 클러스터의 로그 페어(같은 밀리초에 info → error, 즉 else 분기 진입 후 create_user! 실패):

text
2026-07-07 10:30:15  info   Creating user in Cognito: asdf@sdaaljsd.3
2026-07-07 10:30:15  error  Failed to create cognito user: asdf@sdaaljsd.3   error.msg="Username should be an email."

동일 원인 24시간 창 7건 (모두 Username should be an email.):

text
2026-07-07 10:30:15  error  asdf@sdaaljsd.3     <-- 본 클러스터
2026-07-07 10:30:49  error  123@232.231c
2026-07-07 10:31:01  error  12@12.23
2026-07-07 10:31:29  error  123@123.22
2026-07-07 10:31:31  error  321@3.21
2026-07-07 10:31:31  error  2@23.2
2026-07-07 10:31:33  error  33@3.21

Status-board 는 세 개 클러스터를 묶어 2026-07-07-svc-cupixvista-api--unknown-1 로 그룹화했다 (cluster_ids: 381fd5d8... [본 클러스터], 2c20f518..., b48cdcdd...).

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Rails 이메일 validation(URI::MailTo::EMAIL_REGEXP)이 Cognito 대비 느슨해 통과시킨 잘못된 이메일이 Cognito에서 거부됨 app/models/user.rb:83URI::MailTo::EMAIL_REGEXP 는 TLD 유효성 미검증. 본 이메일 asdf@sdaaljsd.3 는 TLD가 .3 로 숫자 한 자리. Cognito 오류 메시지가 정확히 Username should be an email.. 같은 시간대 7건 모두 짧은/숫자 TLD 패턴 Confirmed
H2 Cognito 서비스/네트워크 장애 짧은 시간에 다수 실패 오류 메시지가 Username should be an email. 로 400 계열 검증 오류(5xx/timeout/ServiceUnavailable 아님). status-board scope는 svc:cupixvista-api::unknown 이며 외부 의존성(dep:*) 인시던트 아님. bun run cli/incident-board.ts for-cluster 결과에서 확인 Rejected
H3 사용자 이메일이 유효한데 Cognito가 잘못 판단 이메일 값 asdf@sdaaljsd.3 는 사람 눈으로도 유효 도메인이 아님. 같은 창의 다른 실패 이메일도 모두 유사한 잘못된 TLD 패턴 → 실제 잘못된 입력 Rejected
H4 after_create 트랜잭션 순서 문제 (User는 커밋되는데 Cognito만 실패) lib/cupix/aws/cognito/user.rb:68-69 에서 rescue StandardError 로 예외를 삼키므로 rollback 발생 안 함. Rails after_create 는 트랜잭션 내부에서 실행되지만 rescue 후 정상 종료되면 트랜잭션이 커밋됨 이는 root cause 자체는 아니고 H1 의 부작용 Confirmed (부작용)

Fix Recommendation#

즉시 조치 (Critical)#

  • 없음. 개별 요청이 명확한 잘못된 입력에 대한 검증 오류이며 서비스 전체 장애는 아니다. Slack/status-board 알림 노이즈만 발생. 다만 아래 데이터 정합성 이슈(H4) 때문에 짧은 시간 안에 후속 조치 필요.

단기 개선 (1주 이내)#

  • app/models/user.rb:81-86 — 이메일 형식 validation을 Cognito 기준에 맞게 강화. URI::MailTo::EMAIL_REGEXP 외에 TLD 최소 길이/문자 종류 검사를 추가하거나, Devise :validatable 이 제공하는 기본 정규식/외부 검증 라이브러리(예: email_address, truemail)로 교체 검토. 목표는 "Rails가 통과시켰는데 Cognito가 거부"하는 케이스 제거.
  • lib/cupix/aws/cognito/user.rb:54-72 — Cognito 실패를 rescue한 뒤 User 레코드가 남는 부정합 정리:
    • 방향 A) create_cognito_user 에서 실패 시 예외를 재발생(또는 errors.add + raise ActiveRecord::Rollback)해 트랜잭션을 롤백. caller가 400을 받아 사용자에게 재입력을 요구.
    • 방향 B) 해당 클러스터의 실제 호출 지점(가입/초대/관리자 생성)을 확인해 로그 레벨을 errorwarn 으로 낮추고 caller가 명시적 검증 실패로 응답하도록 조정. 사용자 입력성 실패는 memory 의 AUTH20022/23 사례와 동일하게 warn 이 더 적절할 수 있음.
  • 관련 클러스터 2c20f518-..., b48cdcdd-... 도 같은 원인이므로 일괄 처리.

장기 개선 (재발 방지)#

  • 이메일 형식의 "single source of truth" 정의: Rails validation, Cognito, Warden/Cognito Lambda 트리거가 모두 동일 규칙을 참조하도록 공통 helper 도입.
  • 신규 계정 생성 흐름에 대한 e2e 테스트에 "Rails validation은 통과하지만 Cognito가 거부하는 이메일" 케이스 추가로 회귀 방지.
  • 로그 레벨/severity 재정의: 사용자 입력 오류로 인한 Cognito 실패는 warn 으로 낮추고, 실제 서비스 문제(5xx, throttle)만 error 로 남겨 status-board 노이즈 축소.

Monitoring#

  • Cognito 사용자 생성 실패율 시계열:
text
sum:logs.hits{service:cupixvista-api,@class:Cognito,@function:create_cognito_user,status:error}.as_count()
  • 원인별 breakdown(Username should be an email. vs 기타 실패):
text
sum:logs.hits{service:cupixvista-api,status:error,@error.msg:"Username should be an email."}.as_count()
  • Cognito 성공 로그(대비용):
text
sum:logs.hits{service:cupixvista-api,@class:Cognito,@function:create_cognito_user,status:info}.as_count()

주: 위 쿼리들은 timeseries widget 삽입용 metric 계열 문법만 사용했다(| stats, count by(...), threshold suffix 미사용).

Risk Assessment#

  • Risk level: medium — 즉시 서비스 중단은 없으나 Cognito와 DB의 상태 부정합으로 인해 이후 로그인/비밀번호 재설정 흐름에서 조용한 실패가 재발할 위험. error 레벨 로그가 status-board 알림 노이즈를 유발.
  • 예상 복잡도: standard — validation 규칙 조정과 콜백 실패 처리 방식 변경 두 축이 필요하며, caller 흐름 파악과 회귀 테스트 병행 필요.