[AWS] Amazon Cognito 도입기

Jiwoo Jung·2025년 8월 24일
0

Amazon Cloud Club

목록 보기
6/6

Amazon Cognito 도입기
Lambda Trigger와 API Gateway Proxy로 AWS 아키텍처 완성하기

목차

1. Amazon Cognito 소개
2. ALB + Cognito 연동 - 실패기
3. Lambda 트리거를 활용하여 인증 흐름 커스터마이징하기
4. 외부 API 호출 문제 NAT Gateway 없이 해결하기
5. Cognito 도입 시 고려할 점

1. Amazon Cognito 소개

Amazon Cognito는 AWS에서 제공하는 인증 플랫폼이다. 웹이나 모바일 앱에서 로그인, 회원가입, 권한 제어 같은 기능을 쉽게 구현할 수 있도록 해준다.
Cognito는 크게 user pool, identity pool로 구성된다.

  • User pool을 통해 앱 또는 API에 사용자를 인증하고 권한을 부여한다.
  • Identity pool을 통해 사용자가 aws 리소스에 액세스 할 수 있도록 권한을 부여한다.

쉽게 말해, user pool은 사용자 인증, identity pool은 aws 리소스 권한 관리를 담당한다. 우리 프로젝트에서는 앱에 대한 사용자의 인증에 활용하고자, User Pool을 활용했다.

User pool 인증 흐름


user pool 인증 흐름은 위와 같다.

  • 앱에서 로그인을 시도하면 Cognito 관리형 로그인 페이지로 연결되고, 사용자가 인증을 마치면 토큰을 앱에 전달한다.
  • 이 토큰은 백엔드 서버로 전달되어 API 호출에 사용된다.


2. ALB + Cognito 연동 - 실패기

2-1. Application Load Balancer + Cognito 연동 구조 선택

ALB와 Cognito를 연동한 구조에서는 사용자가 ALB에 요청을 보내면, ALB가 인증되지 않은 사용자는 Cognito로 리디렉션하고, 인증 후에는 트래픽이 컨테이너로 전달된다.
ALB가 인증을 직접 처리해주기 때문에 백엔드에서 따로 인증 로직을 구현할 필요가 없고, 따라서 인증 과정에서 외부 api 호출이 필요 없다는 점에서 이 아키텍처를 선택했다.

2-2. ALB + Cognito 연동 시도

기술적 구현 가능성 확인

ALB + Cognito 연동 구조를 구축하고,
테스트 컨테이너를 통해 인증, REST API 호출, WebSocket 통신 모두 성공적으로 수행되는 것을 확인했다.

실제 프론트 배포 단계에서 리디렉션 URL 제약

  • ALB + Cognito 연동 시, Callback URL을 https://<ALB DNS>/oauth2/idpresponse 로 설정해야 한다.
  • 하지만 실제 서비스는 인증 완료 후 프론트엔드 도메인으로 리디렉션되어야 한다.

프론트가 Cognito와 직접 통신하고, 받은 토큰을 백엔드에 전달하는 구조로 변경했다.

이 과정을 통해 아키텍처 설계 시에 실제 개발 흐름도 고려해야 된다는 교훈을 얻었다. (프론트 코드가 백엔드 프로젝트 내에 위치할 것이라고 생각했지만, 실제로는 따로 배포해서 문제 발생했으니..)



3. Lambda 트리거를 활용하여 인증 흐름 커스터마이징하기

이후, Cognito를 이용한 인증 흐름을 구현하면서 Cognito 기본 기능만으로는 개발 요구사항들을 만족시키기 어려웠다.
따라서, Cognito에 Lambda Trigger를 연결하여 인증 흐름을 커스터마이징했다.

위는 최종 아키텍처 다이어그램이다. Cognito와 Lambda가 연동된 구조를 확인할 수 있다. 크게 보면 다음과 같은 구조이다.


Cognito에 두가지 Lambda trigger가 연결되어 있다. 간단히 설명하자면 다음과 같다.

  • 사용자가 회원가입을 완료하면 Post Confirmation 트리거가 작동하고, 이때 updateUser 람다함수가 호출된다.
  • 액세스 토큰이 발급되기 전에 Pre Token Generation 트리거가 작동해, JWT의 email 같은 클레임을 수정하는 customToken 람다함수가 실행된다.

이제 각 람다 함수와 트리거가 왜 필요한지, 어떻게 구현했는지 자세히 설명하겠다.

1. updateUser()

아키텍처 내 위치는 좌측과 같고, 람다 코드는 우측에 있다.
이 람다 함수와 트리거는 회원가입 직후, Cognito 사용자 정보를 내부 DB로 업데이트한다.

Cognito의 경우, 사용자 정보를 자체 사용자 디렉토리에 저장하지만, 우리 백엔드에도 사용자 데이터가 필요해서 이를 가져와야 했다.
따라서 람다함수를 생성하고 Cognito에 트리거를 연결하여, Dynamodb에 가입이 완료된 사용자의 정보를 저장했다.

회원가입이 발생하면 Post confirmation trigger가 발동되고, updateUser() 람다함수가 호출된다. 이 람다함수는 회원 정보를 DynamoDB에 저장한다.

2. customToken()

아키텍처 내 위치는 좌측과 같고, 람다 코드는 우측에 있다.
이 람다함수는 access token 생성시에 헤더에 email 필드를 추가한다.

실제 배포 테스트를 진행하면서, 백엔드에서는 access token에 email 헤더가 필요하지만, cognito에서는 access token에 email 필드를 제공하지 않는 이슈가 발생했다.
Identity token을 발급해서 백엔드 로직을 수정할 수도 있었지만, 이미 배포가 완료된 상태였기에 백엔드를 수정하는것보다 람다 함수로 토큰을 수정하는것이 효율적이라고 판단해서 아래 흐름을 도입했다.

토큰이 생성되기 전에 Pre token generation trigger가 발동되면, 연결된 customToken() 람다가 호출되어, 토큰의 헤더에 email 클레임을 추가한다. 이를 통해 별도의 백엔드 로직 수정 없이 문제를 해결했다.


* 이외에도 다양한 람다 트리거들이 있어, 아래 공식문서를 참고하여 각자의 요구사항에 맞는 트리거를 적용하면 된다.
docs.aws.amazon.com


4. 외부 API 호출 문제: NAT Gateway 없이 해결하기

4-1. Private subnet 내부에서 외부 API 호출 발생

application-cognito.yml

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://cognito-idp.ap-northeast-2.amazonaws.com/ap-northeast-2_*********

위 코드는 스프링 애플리케이션의 Cognito 설정 파일의 일부이다. Spring Security는 Cognito가 서명한 JWT를 검증할 때, 서명에 사용된 공개 키 목록(JWKS)을 Cognito의 Public Endpoint를 통해 동적으로 가져온다.

문제는 우리 애플리케이션이 보안 강화를 위해 외부 인터넷 접속이 차단된 Private Subnet 내의 ECS 컨테이너에 배포되었다는 점이었다. 초기 아키텍처 설계 시, 외부 API 호출이 필요 없다고 판단하여 비용 효율을 위해 NAT Gateway를 도입하지 않았다.

그러나 이 설계로 인해 정작 인증에 필수적인 JWKS 요청이 외부로 나가지 못하고 차단되는 문제가 발생했다. 결국 임시 조치로 NAT Gateway를 추가하여 문제를 해결했지만, 이로 인해 인프라 비용 크게 증가했다.

4-2. API Gateway + Lambda 프록시로 NAT Gateway 대체하기

이 케이스에서 외부 api 호출은 특정 시점에만, 특정 api로 발생했다. 따라서 api gateway와 lambda로 프록시를 구성하여 NAT Gateway를 대체해보고자 시도했다.

그 방법은 다음과 같다.

1. Lambda 생성

jwks를 반환하는 api를 호출하는 lambda 함수를 생성한다.
jwkProxy()

import json
import urllib.request
COGNITO_JWKS_URL = "https://cognito-idp.ap-northeast-2.amazonaws.com/ap-northeast-2_*********/.well-known/jwks.json"
def handler(event, context):
    try:
        with urllib.request.urlopen(COGNITO_JWKS_URL) as resp:
            jwks_dict = json.loads(resp.read().decode("utf-8"))
        return jwks_dict
    except Exception as exc:
        return { "error": str(exc) }

2. VPC Endpoint 생성

private api gateway와 컨테이너가 배포되는 private subnet을 연결하는 vpc endpoint를 생성한다.

  • 서비스: execute-api
  • DNS 이름 활성화 활성화

3. REST API Private Gateway 생성 및 Lambda 연결

  1. REST API Private Gateway 생성
    2번에서 만든 VPC Endpoint와 연결
  2. 리소스 생성
  3. 메서드 생성 + Lambda 연결
    메서드 생성 시, Lambda 프록시 통합 옵션을 비활성화한다.
  4. 리소스 정책 생성
    리소스 정책은 다음과 같이 설정한다.
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "AllowAllFromThisVpce",
          "Effect": "Allow",
          "Principal": "*",
          "Action": "execute-api:Invoke",
          "Resource": "arn:aws:execute-api:{region}:{account-id}:{api-id}/*/*/*",
          "Condition": {
            "StringEquals": {
              "aws:SourceVpce": "{vpc-endpoint-id}"
            }
          }
        }
      ]
    }

4. Spring Configuration 수정

application-cognito.yml

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://{api-id}.execute-api.{region}.amazonaws.com/{stage}/jwks
  • {api-id}: api gateway id

실행 확인

NAT Gateway를 제거했다. (*pingpong은 다른 팀 리소스이다.)
로그인 후 인증에 성공했다!

NAT Gateway를 제거해도 인증 흐름이 정상 작동했다.
이렇게 API Gateway와 Lambda를 통한 api 요청 프록시에 성공해서, nat gateway로 인한 비용을 줄일 수 있었다.


5. Cognito 도입 시 고려할 점

마지막으로, Cognito를 도입하며 느낀 장단점을 정리해봤다.

장점주의점
• JWT 발급 로직을 직접 구현할 필요 없음
• Lambda Trigger로 인증 로직 확장 가능
• AWS 리소스들과의 연동이 쉬움
• 전체 인증 워크플로우를 Cognito 기준으로 재설계해야 함
• 사용자 정보를 자체적으로 저장하기 때문에, DB 구조를 고려해야 함
• 사용자 데이터 변경이 잦다면, 직접 구현이 더 효율적일 수 있음

정리하자면, MVP 개발에는 빠르고 강력한 인증 플랫폼으로 Cognito를 추천하지만 장기적인 유연성과 관리 비용까지 고려해 선택하시는 것을 권장한다. (개인적인 생각)

0개의 댓글