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#
- 2026-07-07 10:30:15 KST — 본 클러스터 이벤트 (
asdf@sdaaljsd.3) 발생 (first_seen/last_seen). 그룹화된 인시던트의 최초 이벤트. - 2026-07-07 10:30:49 KST — 관련 클러스터
2c20f518-7ea0-4ed6-bfff-75d94400a8fc(123@232.231c). - 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#
Failed to create cognito user: asdf@sdaaljsd.3
Datadog에서 확인한 구조화된 로그:
{
"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 로그(같은 밀리초):
{
"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).
- 이메일 validation 통과 —
URI::MailTo::EMAIL_REGEXP는 TLD 유효성 검증이 없다.
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?
after_create :create_cognito_user콜백 등록 — User가 DB에 저장된 뒤 호출된다.
included do
attr_accessor :skip_cognito_user_creation
after_create :create_cognito_user, unless: :skip_cognito_user_creation
after_update :update_cognito_user
end
- Failure point:
create_cognito_user는 Cognito 호출에서 발생한 예외를 rescue하고 error 로그만 남긴 채 조용히 종료한다. 예외를 재발생시키지 않으므로 트랜잭션이 롤백되지 않는다.
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
- 실제 Cognito 호출:
admin_create_user가username: user.email로 요청되고 AWS 측이Username should be an email.을 반환한다.
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 쿼리:
service:cupixvista-api "asdf@sdaaljsd.3"
service:cupixvista-api "Failed to create cognito user"
본 클러스터의 로그 페어(같은 밀리초에 info → error, 즉 else 분기 진입 후 create_user! 실패):
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.):
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:83 의 URI::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) 해당 클러스터의 실제 호출 지점(가입/초대/관리자 생성)을 확인해 로그 레벨을
error→warn으로 낮추고 caller가 명시적 검증 실패로 응답하도록 조정. 사용자 입력성 실패는 memory 의 AUTH20022/23 사례와 동일하게warn이 더 적절할 수 있음.
- 방향 A)
- 관련 클러스터
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 사용자 생성 실패율 시계열:
sum:logs.hits{service:cupixvista-api,@class:Cognito,@function:create_cognito_user,status:error}.as_count()
- 원인별 breakdown(
Username should be an email.vs 기타 실패):
sum:logs.hits{service:cupixvista-api,status:error,@error.msg:"Username should be an email."}.as_count()
- Cognito 성공 로그(대비용):
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 흐름 파악과 회귀 테스트 병행 필요.