싱글벙글 서버리스 이전 삽질기(w. CloudFront)
2023년 5월 09일이전 글에서 서버리스 이전을 위해 다음의 일을 진행했습니다.
- 기존에 쓰고 있었던 Notion API 제거
- DynamoDB 설계
- 프론트 렌더링 방식 변경(Notion API → md 렌더링)
- Lambda 생성
- API Gateway 생성 및 Lambda 연결
백엔드 쪽 부분을 만들고 배포까지 하였으니 이제 프론트 배포만 남았습니다. 프론트 배포를 위해 S3와 CloudFront를 사용할 겁니다. 기존 프론트 배포는 Nginx를 이용했습니다. Nginx 컨테이너에 배포할 프론트 정적 파일들을 두고 Nginx가 서비스하는 형태였습니다. 기존의 형태를 대입해보면, S3가 정적 파일들을 보관하고 CloudFront가 Nginx의 웹 서버 역할과 캐싱 기능까지 수행한다고 생각하면 될 거 같습니다.
설명탕의 프론트는 NextJS로 작성되어 있으며, SSR(Server-side Rendering)은 사용하지 않고 SSG(Static Site Generation) 기능만 사용합니다. 전 시간에 서버 배포 작업을 하면서 변경된 API 주소를 넣고, NextJS 빌드 후 나온 정적 파일들을 S3에 업로드합니다. 마지막으로 S3와 CloudFront를 연동만 하면 배포가 됩니다. CloudFront 배포는 여러 블로그에서 자세하게 설명한 것을 참고하면 좋을 거 같아 이 부분은 생략하겠습니다. 제가 추가적으로 따로 설정한 것은 S3와 직접적인 통신을 막기 위한 ‘원본 엑세스(OAI)’와 ‘대체 도메인 이름’ 설정입니다.

드디어 기존 EC2에서 Lambda, API Gateway, S3, CloudFront를 이용한 서버리스 환경 구축을 완료하였습니다. 감격의 눈물을 쏟으려는데… 어? 뭔가 이상합니다. Dynamic route 페이지에 대한 새로고침이 되지 않았습니다. 정확히는 게시글 페이지에서 새로고침을 하면 403 에러(Access Denied)가 나타났습니다.

여러 추측과 원인을 분석해본 결과, 설명탕 URL은 redundant4u.com/post/cpu
이런 식으로 맨 뒤에 cpu
와 같이 dynamic route 값이 붙습니다. 이 페이지에서 새로고침을 하게 되면 S3에서 cpu
이름을 가진 파일을 찾습니다. NextJS를 빌드하면 cpu
가 아닌 cpu.html
파일을 생성하므로 cpu
는 찾을 수 없는 파일로 인식하여 403을 뜬다는 사실을 알았습니다.
기존에는 Nginx에서 프론트 정적 파일들을 서비스 했습니다. 그리고 NextJS를 빌드 할 때 trailingSlash
옵션을 주어 dynamic route 값 이름으로 디렉터리를 만들고 그 안에 index.html
파일이 생성하도록 구성했습니다(trailingSlash 옵션을 주지 않으면 [dynamic route 값].html
형식으로 파일이 생성됩니다). Nginx의 설정 파일에서 try_files
옵션으로 ${postId}.html
와 같은 형식으로 지정할 수 있어 해당 오류가 발생하지 않았습니다.
TEXTtry_files $uri $uri.html $uri/ /index.html;
여러 고민 끝에 해결한 방법은 html 확장자를 떼어버렸습니다. 처음에 이 방법을 도입하려 했을 때, 돌아가기 위해 욱여넣는 느낌이 강해서 이게 맞나 싶었습니다. Lambda Edge를 통해 해결하기, NextJS 설정 건드리기 등등의 방법을 생각해봤는데 최종적으로 html 확장자를 없애는 방법이 가장 깔끔하다고 판단했습니다. 프론트 배포할 때 빌드 후 스크립트를 통해 html 확장자를 없앤 뒤 S3에 업로드 하도록 CICD를 구성했습니다. 여기서 주의할 점은 html 확장자를 없앤 파일을 그대로 S3로 업로드하면 binary 파일로 인식합니다. 이를 방지하기 위해 html이라는 메타데이터를 설정해주기 위해 aws s3 cli의 --content-type
옵션으로 text/html
을 지정해줘야합니다. 마지막으로 CloudFront 캐싱 무효화 처리까지 해주면 배포가 완료됩니다.

YAML- name: Build
run: yarn build
- name: Compress
run: tar -zcf ${GITHUB_SHA::8}.tar.gz out
- name: Delete html extension
run: |
for file in $(find ./out -name "*.html"); do mv "$file" "${file%%.html}"; done
mv out/index out/index.html
- name: Upload to S3
run: |
aws s3 mv --region ap-northeast-2 \
${GITHUB_SHA::8}.tar.gz \
${{ secrets.S3_CLIENT_LOCATION }}/${GITHUB_SHA::8}.tar.gz
- name: Sync S3
run: |
aws s3 sync out ${{ secrets.S3_CLIENT_DEPLOYMENT }} \
--exclude "*.*" \
--content-type "text/html" \
--delete
aws s3 sync out ${{ secrets.S3_CLIENT_DEPLOYMENT }} \
--exclude "*" \
--include "*.png" --include "*.jpeg" --include "*.json" --include "*.js" --include "*.css" --include "*.ico" --include "*.html" \
--delete
- name: Invalidate CloudFront Cache
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.AWS_DISTRIBUTION_ID }} \
--paths "/*"
이렇게 해서 드디어 설명탕은 서버리스 환경으로 이전을 하였습니다. 서버리스 개념이 익숙하지 않고 까다로운 부분들이 많아서 도중에 ‘기존에 잘 돌아가는 서버 엎고 무슨 부귀영화를 누리겠다고… 이게 무슨 짓이지’ 와 같은 고민을 했습니다. 막상 다 구축하고 나서 고생한 만큼 큰 장점이 보이지 않아(?) 조금은 허무하기도 한 거 같습니다.
이번 경험을 통해 서버리스를 구축하면서 다음과 같은 생각을 해서 좋았습니다.
- AWS CLI와 쉘 스크립트의 힘은 대단하다
- 데이터가 작은(혹은 트래픽이 낮은) 서비스는 Lambda로도 충분할 거 같다(cold start의 문제점을 느끼지 못했다)
- API Gateway 관리가 편하다(특히 배포 부분)
- ‘효율적인 NoSQL 설계란 무엇인가’를 생각해서 좋았다
망할 NextJS
그리고 이전의 환경과 비교하여 다음과 같은 효과를 얻을 수 있었습니다.
- API 호출 속도 개선
- 대략 1000ms → 60ms
- Cold start 일 때, 대략 600ms 정도로 이전과 비슷
- API 데이터 크기 최소화
- 홈화면 글 데이터 조회 시 대략 데이터 크기 50% 감소
- 통신하는데 불필요한 데이터 삭제
- 요금 문제
- EC2에서 서버리스로 옮기면서 한 달에 치킨 한 마리 값 정도 절약
하지만 여전히 서버리스에 대하여 의문점과 구현해보고 싶은 것들이 있습니다.
- 서버리스에서 SSR 운영은 어떻게 해야하나?
- Lambda Edge로 한다고 하던데 어떻게 하는거지?
- 서버리스 자동 배포 구성은 어떻게 해야하나?
- AWS CLI와 쉘 스크립트로 아름답게 작성할 수 있을 거 같다
- Lambda 함수 생성 혹은 업데이트 → API Gateway 연결 → API Gateway 배포
- 로직이 복잡한 백엔드를 어떻게 서버리스에 올리지?
- 로직이 복잡해도 결합도가 낮다면 가능할지도?
- API Gateway로 인증 관리를 경험해 보고 싶다
- 로컬에서 서버리스 테스트는 어떻게 해야하나?
- serverless-offline와 같은 물건이 있다
이번에 구축한 서버리스는 HTTP 통신을 하는 서비스만 이전했습니다. 웹소켓 통신하여 웹 터미널을 띄우는 설명탕의 자기소개 페이지에는 기존의 EC2와 통신합니다. 사실 EC2를 1년 예약 인스턴스를 구입해서 남는 EC2 자원을 쓰는 겸 해서 웹소켓 통신 부분은 천천히 서버리스로 옮길려고 합니다. 기회가 된다면 웹소켓 서버리스 이전도 정리하면 좋을 거 같습니다.
서버 비용으로 고생하고 있거나 운영하고 있는 서비스 규모가 작다면 서버리스 도입을 추천드리고 싶습니다. 하지만 서비스 안정성 추구, 대규모 트래픽이 발생하는 서비스는 서버리스보다 EC2와 같은 24시간 대기 서버가 효율이 더 좋다고 판단되니 상황에 따라 선택하면 될 거 같습니다.
23.05.13 추가
Trailing slash 옵션 해제로 https://redundant4u.com/post/${postId}/
와 같이 맨 끝에 /
로 끝나면 403 에러가 떴습니다. ${postId}/
로 끝나면 /
를 지워서 ${postId}
리다이렉트 하도록 구성하고 싶었습니다. 이를 구현하기 위해 Lambda Edge를 만들었습니다. Lambda Edge는 CloudFront 기능 중 하나로, 사용자의 요청(viewer request, origin request)과 응답(viewer response, origin response)을 코드를 통해 조절, 관리할 수 있습니다.

사용자의 요청이 CloudFront로 가기전에(viewer request) URL를 검사하고 맨 끝에 /
가 존재한다면 /
를 삭제하는 로직을 Lambda Edge에 써주면 됩니다. 직접 구현하려다가 LambdaEdgeRemoveTrailingSlash라는 템플릿이 발견하고 적용했습니다.
설치 과정을 참고하면 쉽게 Lambda Edge를 구축할 수 있습니다. 다만 주의할 점이 Lambda Function을 만들 때 버지니아 북부(us-east-1)로 생성해야 CloudFront와 연동할 수 있으니 이 점 주의해야합니다. Lambda Edge와 CloudFront를 연동하면 자동으로 CloudFront가 배포가 진행됩니다.
배포까지 마쳤다면, URL이 /
로 끝나면 /
를 지우고 리다이렉트하는 모습을 볼 수 있습니다.