- Published on
AWS SAM으로 Websocket API Gateway WebSocket과 Lambda 구축하기
- Authors
- Name
- hongreat
- ✉️hongreat95@gmail.com
SAM(Serverless Application Model)은 Serverless 관련 기술을 구축하기 용이한 AWS에서 제공하는 Iac 기반 리소스(CloudFormation) 입니다.
AWS SAM 은 간단한 YAML 파일(혹은 Json) 과 최신버전의 Python3.13(nest나 다른 것도 보유)도 사용이 가능하며 테스트나 빌드, 배포가 매우 빠릅니다.
기존에 Lamdba 를 사용하는 편한 방식 중 Lambda Layer 로 배포하는 것보다 훨씬 테스트 및 작업속도가 빨랐습니다.
WebSocket 을 구성하여 인프라와 비즈니스 로직을 해결하는 미션에서 SAM을 이용해 WSAPIGteway 와 Lambda 구축 과정을 기록합니다.
- 1. SAM Setting
- 2. WebSocket APIGateway 와 Lambda 구조
- 3. template.yaml
- 4. Lambda
- 5. SAM Build and Deploy
- 레퍼런스 링크
1. SAM Setting
구축한 로컬환경 스펙 입니다.
- MacOS M3 Pro
1.1 install
brew 로 AWS SAM CLI를 설치합니다.
brew install aws/tap/aws-sam-cli
1.2 init
현재 경로에서 sam 프로젝트를 구축합니다.
sam init
템플릿이 여러개 제공됩니다.
- Quick Start Template
- Custom Template
여기서 WS을 바로 구축하는 프로젝트는 없습니다.
메인 구조가 구축되어있는 기본 템플릿(1번이 가장 무난)을 사용합니다.
- Python 3.12 런타임을 선택하고, 프로젝트 이름을 입력하면 해당 sam 프로젝트가 생성됩니다.
2. WebSocket APIGateway 와 Lambda 구조
2.1 메커니즘
REST API와 같은 HTTP 기반 API에서는 GET이나 POST 같은 메서드만 상황에 맞게 잘 설계해서 Lambda Handler를 구축하면 됩니다.
반면 WebSocket APIGateway 는 Lambda 사용에 대한 규격이 정해져있습니다.
WebSocket 연결 자체가 특정 메커니즘을 따르고 지켜야하기 때문입니다.
처음에 연결을 맺고 $connect
, 상태를 지속 하면서 메시지를 주고 받으며 $default
연결이 끊어지는 $disconnect
상황이 있기 때문에, 그에 맞는 규칙들이 필요합니다.
따라서, WebSocket API에서 $connect
, $disconnect
, $default
와 같은 규칙은 실시간 연결 관리와 양방향 통신을 처리하며, 해당 규격에 맞는 API Route와 Handler 세팅을 하게 됩니다.
2.2 파이프라인
뼈대가 되는 흐름은 이렇습니다.
로컬테스트 과정은 sam local 명령어로 지원이 되며, WS의 경우 localhost:3000 로 오픈되지만 connection이 안되는 이슈가 있습니다.
- 일반적인 connect 시도 후, 400 & 403 에러가 발생합니다.
- 기본적으로 403 에러가 발생하며, template을 통해서 auth 설정을 None으로 하는 경우 400에러가 발생합니다. 이는 연결과 동시에 connect 핸들러가 동작해야 되는 구조로 파악됩니다.
- 로컬에서 커넥션과 핸들러 테스트를 해볼 수 없는 해당 이슈는 방법을 찾고있으며, 레퍼런스가 부족해 추후 업로드하는 기회를 가져보고자 합니다. (이미 방법이 있지만 제가 못찾은 걸 수도 있습니다.)
3. template.yaml
template.yaml
파일은 SAM을 정의하는 핵심적인 기능 수행 합니다.
이 파일은 프로젝트 루트에 위치시켜 AWS 리소스를 정의하고, 배포 및 관리 모두 담당하는 역할을 합니다.
참고로 SAM으로 AWS Lambda 함수, API Gateway, DynamoDB, S3 등과 같은 AWS 서비스 리소스를 선언하며, 이를 통해 서버리스 애플리케이션 파이프라인을 구축하고 배포할 수 있습니다.
3.1. Resources
먼저 리소스가 어떻게 상호작용하는지 설정합니다.
Lambda 함수, API Gateway, Iam 등을 여기서 정의할 수 있습니다.
WebSocket API는 ApiGatewayV2 에서 ProtocolType을 통해 정의하고 있습니다.
Resources:
WebSocketApi:
Type: AWS::ApiGatewayV2::Api
Properties:
Name: SAM-WS-API
ProtocolType: WEBSOCKET
RouteSelectionExpression: $request.body.action
3.2. AWS Lambda
Lambda 함수의 path 와 env, handler 모듈 지정, 서버 스펙(메모리나 타임아웃 세팅 모두) 등을 지정할 수 있습니다.
WebSocketConnectFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: lambda_function/
Handler: connect.handler
Runtime: python3.11
Role: !GetAtt LambdaExecutionRole.Arn
이렇게 함수를 생성하게 되면 아래처럼 관련된 고유문자열이 수식 됩니다.
이름을 지정하려면 FunctionName
을 사용합니다. (기타 리소스 네임은 레퍼런스 링크 참고)
3.3. Resource Type 과 Connection setting
여기서는 WebSocket API와 Lambda 함수를 연결하는 통합 리소스를 정의하고 있습니다.
라우트, 메서드, 통합 방식 등을 설정하여 API 요청이 Lambda 함수로 전송되도록 합니다.
ConnectIntegration:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref WebSocketApi
IntegrationType: AWS_PROXY
IntegrationUri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${WebSocketConnectFunction.Arn}/invocations
ConnectRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref WebSocketApi
RouteKey: $connect
AuthorizationType: NONE
OperationName: ConnectRoute
Target: !Join
- '/'
- - 'integrations'
- !Ref ConnectIntegration
$connect 이벤트가 발생하면 Lambda 함수가 호출되어야 하고 해당 통합 설정은 ConnectIntegration를 통해서 이뤄집니다.
함수와 $connect이라는 이벤트 발생 시 ConnectRoute 로 맵핑되며, 클라이언트 연결 요청을 처리할 통합ConnectIntegration을 지정합니다.
IntegrationUri는 Lambda 함수의 ARN을 사용하여 API Gateway와 Lambda의 통합을 설정합니다.
3.4. IAM 역할 설정
AWS SAM에서 Lambda 함수가 AWS 리소스에 접근할 수 있도록 하기 위해서는 적절한 IAM 역할을 설정해야 합니다.
비즈니스 로직에 따라 RDS 연결이나 타 리소스 접근에 따라 VPC 설정이 다를 수 있기 때문에, 리소스 ARN 에 대한 문서탐색을 통해 상황에 따른 ARN을 적절하게 찾아야 합니다.
현 상황에서는 ExecutionRole로 충분합니다.
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
3.5. Outputs
배포 후 생성된 리소스의 정보를 출력하는 Outputs
섹션도 정의할 수 있습니다.
이를 통해 배포된 WebSocket API의 URL이나 Lambda 함수의 ARN 등을 확인할 수 있습니다.
Outputs:
WebSocketApiEndpoint:
Description: "WebSocket API Endpoint"
Value: !Sub "wss://${WebSocketApi}.execute-api.${AWS::Region}.amazonaws.com/${WebSocketStage}"
3.6 ENV Variables
빌드 배포 단계나 Lambda 함수에서 template에서 지정한 환경 변수를 사용 할 수 있습니다.
아래 예시는 $default 단계에서 사용하는 함수의 Variables를 적용한 예시입니다.
WebSocketMessageFunction:
Type: AWS::Serverless::Function
...
Environment:
Variables:
SLACK_WEBHOOK_URL: "some_url"
Lambda 내 코드 단에서는 os 환경변수로 가져올 수 있습니다.
os.environ["SLACK_WEBHOOK_URL"]
SAM을 통해서 서버리스 애플리케이션의 자동 배포와 관리를 간소화 하는 것 역시 가능합니다.
SAM CLI로 sam deploy
명령어를 통해 “이 파일”을 기반으로 “리소스를 AWS에 배포”할 수 있는 것 입니다.
AWS 에서는 이 과정에 AWS CloudFormation 리소스를 활용해 프로비저닝하고 배포합니다.
4. Lambda
Lambda Handler에 종속 되는 WS APIGateway 의 모든 이벤트는 위의 template에서 지정한대로 각 Handler를 통해서 상황에 맞는 액션을 구현할 수 있습니다.
4.1. Handler
AWS의 Lambda 인 해당 부분은 SAM이 아닌 Lambda 문서를 참고하는게 좋습니다.
event는 함수 실행 시 전달되는 입력 데이터이고, context는 실행 환경 정보(함수 이름, 메모리, 제한시간 등)를 담고 있는 객체입니다.
아래는 Lambda의 기본 함수 포맷입니다.
import json
def handler(event, context):
print("Connect event:", json.dumps(event))
return {"statusCode": 200, "body": "Connected"}
4.2. connect, default, disconnect
WebSocket의 생애주기를 보면, 연결이 맺어지고 → 데이터를 핸들링 하다가 → 연결이 끊기게 됩니다.
이 과정은 모두 Lambda Handler에서 처리되며 해당 이벤트감지를 통한 후 처리 역시 가능합니다.
template이 주는 handler 지정의 이점으로 인해 아래 구조가 아닌 하나의 모듈로 구축할 수 도 있고, 기능과 목적별로 모듈을 나눌 수 있습니다.
lambda_function 라는 dir 하위에 작성한 connect 함수 예시 입니다.
├sam app root
├── lambda_function
│ ├── __init__.py
│ ├── connect.py
│ ├── disconnect.py
│ ├── ...
│ └── send.py
├── ...
└── template.yaml
/connect.py
import json
from slack import send_to_slack
from validate import get_token_from_query_params, is_validated_token
def handler(event, context):
"""WebSocket $connect 라우트를 위한 핸들러"""
try:
connection_id = event.get("requestContext", {}).get("connectionId")
query_params = event.get("queryStringParameters", {})
input_token = get_token_from_query_params(query_params)
is_valid: bool = is_validated_token(input_token)
if not connection_id:
return {
"statusCode": 400,
"body": json.dumps({"message": "유효하지 않은 웹소켓 연결 요청."}),
}
if not is_valid:
return {
"statusCode": 400,
"body": json.dumps({"message": "유효하지 않은 토큰."}),
}
.....
send_to_slack(
{
"type": "connect",
"event": event,
"connection_id": connection_id,
"query_params": query_params,
"input_token": input_token,
}
)
return {
"statusCode": 200,
"body": json.dumps({
"message": "웹소켓 연결 성공",
"connectionId": connection_id,
"input_token": input_token,
}),
}
except Exception as e:
return {
"statusCode": 500,
"body": json.dumps({"message": "서버 내부 오류가 발생했습니다"}),
}
연결 시 slack 으로 메세지를 보냅니다.
그리고 token 이 일치하지 않는경우 에러를 보내도록 했습니다.
POSTMAN으로 connect를 시도해본 결과, 성공(200)인 경우에는 연결을 지속합니다. 반면 에러인 경우에는 연결되지 않습니다.
4.3. Local test
위에서 작성된 Lambda 함수를 로컬에서 테스트 하는 방법입니다.
Python test 가 아닌, 함수 자체를 실행해보는 테스트 입니다.
먼저 sam local start-lambda 를 통해서 함수를 컨테이너 단으로 실행시켜줍니다.
그리고 아래 코드로 Lambda 함수를 Invoke 합니다.
aws lambda invoke
aws lambda invoke --function-name "WebSocketMessageFunction" --endpoint-url "http://127.0.0.1:3001" --no-verify-ssl out.txt
5. SAM Build and Deploy
SAM Project 메인 경로에서 각 명령어를 입력하면, template에 의해 project가 build 됩니다.
그러면서 samconfig.toml
파일을 생성하고 해당 value 들로 AWS의 Cloudformation 의 네이밍이나 S3의 네이밍 등이 지정됩니다.
5.1. Build
build 단계에서 template에 문법 등에 대한 오류가 발생하면 build 되지 않습니다.
sam build
동일 프로젝트의 SAM에 대해서 다른 build 프로세스가 진행중이라면, build 되지 않습니다.
(build속도가 일반적인 프로젝트 단위보다 가벼워 꽤 빠르기 때문에, 오래 기다리는 상황(5분 이상)이라면, 코드를 의심해볼 필요가 있습니다.)
5.2. Deploy
sam deploy --guided # 처음 시도하는 경우 or 배포 시 입력한 값들이 변경되기를 바라는 경우
sam deploy # 일반적인 경우
sam deploy
에서 --guided
옵션을 사용하면 samconfig.toml
파일이 생성됩니다.
version = 0.1
[default.deploy.parameters]
stack_name = "스택이름"
resolve_s3 = true
s3_prefix = "..."
region = "ap-northeast-2"
profile = "..."
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
image_repositories = []
이 파일에는 CloudFormation 스택 이름, S3 버킷 이름, 리전 등 배포 시 입력했던 값들이 저장됩니다.
이후 배포 시 동일한 파일을 사용하여 설정을 재활용할 수 있습니다.
5.3. CloudFormation 으로 스택삭제
이렇게 SAM 을 이용해 만든 모든 스택들을 완전 삭제 하고 싶은경우 cloudformation 명령어로 삭제 할 수 있습니다.
위에서 작성된 SAM 의 stack_name과 동일하게 파라미터를 지정하고 delete-stack 을 진행합니다.
aws cloudformation delete-stack --stack-name {stack_name}
connect 함수가 라우팅된 APIGateway의 콘솔 모습입니다.
이 모든 과정은 IaC(Infrastructure as Code)를 가능하게 하기 때문에 확장성, 재사용 가능 및 버전 관리가 용이하며 인프라가 중구난방식으로 구조화 되는 것을 방지해주게 됩니다.
Serverless 아키텍쳐가 클라우드 진영의 급격하고 안정적인 발전으로 인해 필요한 곳에 상용된다고 느껴집니다.
괜찮은 템플릿으로 레퍼런스를 잘 쌓아두면 활용할 수 있는 곳이 많을 것 같다는 생각입니다.
레퍼런스 링크
sam local start-lambda (sam local invoke & aws lambda local invoke 관련 내용 포함)