Amazon Cognito 도입기
Lambda Trigger와 API Gateway Proxy로 AWS 아키텍처 완성하기
목차
1. Amazon Cognito 소개
2. ALB + Cognito 연동 - 실패기
3. Lambda 트리거를 활용하여 인증 흐름 커스터마이징하기
4. 외부 API 호출 문제 NAT Gateway 없이 해결하기
5. Cognito 도입 시 고려할 점
Amazon Cognito는 AWS에서 제공하는 인증 플랫폼이다. 웹이나 모바일 앱에서 로그인, 회원가입, 권한 제어 같은 기능을 쉽게 구현할 수 있도록 해준다.
Cognito는 크게 user pool, identity pool로 구성된다.
쉽게 말해, user pool은 사용자 인증, identity pool은 aws 리소스 권한 관리를 담당한다. 우리 프로젝트에서는 앱에 대한 사용자의 인증에 활용하고자, User Pool을 활용했다.
user pool 인증 흐름은 위와 같다.
ALB와 Cognito를 연동한 구조에서는 사용자가 ALB에 요청을 보내면, ALB가 인증되지 않은 사용자는 Cognito로 리디렉션하고, 인증 후에는 트래픽이 컨테이너로 전달된다.
ALB가 인증을 직접 처리해주기 때문에 백엔드에서 따로 인증 로직을 구현할 필요가 없고, 따라서 인증 과정에서 외부 api 호출이 필요 없다는 점에서 이 아키텍처를 선택했다.
ALB + Cognito 연동 구조를 구축하고,
테스트 컨테이너를 통해 인증, REST API 호출, WebSocket 통신 모두 성공적으로 수행되는 것을 확인했다.
https://<ALB DNS>/oauth2/idpresponse
로 설정해야 한다.⟹ 프론트가 Cognito와 직접 통신하고, 받은 토큰을 백엔드에 전달하는 구조로 변경했다.
이 과정을 통해 아키텍처 설계 시에 실제 개발 흐름도 고려해야 된다는 교훈을 얻었다. (프론트 코드가 백엔드 프로젝트 내에 위치할 것이라고 생각했지만, 실제로는 따로 배포해서 문제 발생했으니..)
이후, Cognito를 이용한 인증 흐름을 구현하면서 Cognito 기본 기능만으로는 개발 요구사항들을 만족시키기 어려웠다.
따라서, Cognito에 Lambda Trigger를 연결하여 인증 흐름을 커스터마이징했다.
위는 최종 아키텍처 다이어그램이다. Cognito와 Lambda가 연동된 구조를 확인할 수 있다. 크게 보면 다음과 같은 구조이다.
Cognito에 두가지 Lambda trigger가 연결되어 있다. 간단히 설명하자면 다음과 같다.
updateUser
람다함수가 호출된다.customToken
람다함수가 실행된다.이제 각 람다 함수와 트리거가 왜 필요한지, 어떻게 구현했는지 자세히 설명하겠다.
updateUser()
아키텍처 내 위치는 좌측과 같고, 람다 코드는 우측에 있다.
이 람다 함수와 트리거는 회원가입 직후, Cognito 사용자 정보를 내부 DB로 업데이트한다.
Cognito의 경우, 사용자 정보를 자체 사용자 디렉토리에 저장하지만, 우리 백엔드에도 사용자 데이터가 필요해서 이를 가져와야 했다.
따라서 람다함수를 생성하고 Cognito에 트리거를 연결하여, Dynamodb에 가입이 완료된 사용자의 정보를 저장했다.
회원가입이 발생하면
Post confirmation trigger
가 발동되고, updateUser()
람다함수가 호출된다. 이 람다함수는 회원 정보를 DynamoDB에 저장한다.
customToken()
아키텍처 내 위치는 좌측과 같고, 람다 코드는 우측에 있다.
이 람다함수는 access token 생성시에 헤더에 email 필드를 추가한다.
실제 배포 테스트를 진행하면서, 백엔드에서는 access token에 email 헤더가 필요하지만, cognito에서는 access token에 email 필드를 제공하지 않는 이슈가 발생했다.
Identity token을 발급해서 백엔드 로직을 수정할 수도 있었지만, 이미 배포가 완료된 상태였기에 백엔드를 수정하는것보다 람다 함수로 토큰을 수정하는것이 효율적이라고 판단해서 아래 흐름을 도입했다.
토큰이 생성되기 전에
Pre token generation trigger
가 발동되면, 연결된 customToken()
람다가 호출되어, 토큰의 헤더에 email 클레임을 추가한다. 이를 통해 별도의 백엔드 로직 수정 없이 문제를 해결했다.
* 이외에도 다양한 람다 트리거들이 있어, 아래 공식문서를 참고하여 각자의 요구사항에 맞는 트리거를 적용하면 된다.
docs.aws.amazon.com
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를 추가하여 문제를 해결했지만, 이로 인해 인프라 비용 크게 증가했다.
이 케이스에서 외부 api 호출은 특정 시점에만, 특정 api로 발생했다. 따라서 api gateway와 lambda로 프록시를 구성하여 NAT Gateway를 대체해보고자 시도했다.
그 방법은 다음과 같다.
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) }
private api gateway와 컨테이너가 배포되는 private subnet을 연결하는 vpc endpoint를 생성한다.
execute-api
DNS 이름 활성화
활성화Lambda 프록시 통합
옵션을 비활성화한다.{
"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}"
}
}
}
]
}
application-cognito.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://{api-id}.execute-api.{region}.amazonaws.com/{stage}/jwks
{api-id}
: api gateway idNAT Gateway를 제거했다. (*pingpong은 다른 팀 리소스이다.)
로그인 후 인증에 성공했다!
NAT Gateway를 제거해도 인증 흐름이 정상 작동했다.
이렇게 API Gateway와 Lambda를 통한 api 요청 프록시에 성공해서, nat gateway로 인한 비용을 줄일 수 있었다.
마지막으로, Cognito를 도입하며 느낀 장단점을 정리해봤다.
장점 | 주의점 |
---|---|
• JWT 발급 로직을 직접 구현할 필요 없음 • Lambda Trigger로 인증 로직 확장 가능 • AWS 리소스들과의 연동이 쉬움 | • 전체 인증 워크플로우를 Cognito 기준으로 재설계해야 함 • 사용자 정보를 자체적으로 저장하기 때문에, DB 구조를 고려해야 함 • 사용자 데이터 변경이 잦다면, 직접 구현이 더 효율적일 수 있음 |
정리하자면, MVP 개발에는 빠르고 강력한 인증 플랫폼으로 Cognito를 추천하지만 장기적인 유연성과 관리 비용까지 고려해 선택하시는 것을 권장한다. (개인적인 생각)