프론트엔드 개발자의 Nest.js API 서버 개발기(2)

Ethan Yu·2024년 2월 11일
0
post-thumbnail

AWS App Runner, VPC, ECR, RDS를 연동하여 API 서버를 배포했습니다. 적은 레퍼런스에 다른 개발자분들이 고생하지 않기를 바라면서 글을 정리합니다.

Nest.js로 구축한 API서버를 클라우드에 배포하게 되었습니다. AWS Elastic BeanstalkAWS App Runner가 최종적으로 고려되었지만, 결국 App Runner의 손을 들어주었습니다. 두 서비스 모두 '완전 관리형' 서버를 지향하지만 App Runner의 '더 쉽고 빠른 배포'라는 모토에 설득되었고, 무엇보다 팀원분들의 강력한 지지가 있었기 때문이었습니다. CI/CD 관련한 세팅을 따로 하지 않아도 된다는 점도 큰 장점이었습니다.(물론 나중에 Github Actions를 통해 세팅을 해주긴 했습니다).

이번 글에서는 설계한 App Runner 배포 아키텍처를 돌아보면서 마주친 문제들을 복기해보고, App Runner를 사용하면서 느꼈던 소감을 정리해보도록 하겠습니다.


App Runner로 배포하기

기본적인 배포 구성도는 아래의 도식처럼 가져갔습니다. Github 레포지토리 main브랜치에 코드를 푸시하면 Github Actions에 의해 AWS ECR에 이미지가 푸시되고, 해당 이미지를 통해 App Runner에 서비스가 배포됩니다. 배포된 서비스는 AWS VPC 내부에 위치한 RDS와 통신을 통해 데이터를 가져오고, 사용자에게 이를 전달합니다. 서비스가 프라이빗 서브넷에 위치하기 때문에 퍼블릭 서브넷에 위치한 EC2와 게이트웨이를 통해 플로우를 제어합니다.

💬 헷갈리는 지점
App Runner가 프라이빗 서브넷에 존재한다고 단언하기는 조금 어려울 것 같은 것이, 콘솔을 통해 인바운드 네트워킹은 public하게, 아웃바운드 네트워킹은 private하게 설정할 수 있다는 점입니다.
콘솔을 보면 App Runner가 프라이빗 서브넷 안쪽에 존재한다고 표기되어 더 애매합니다.
이 지점은 App Runner 내부 동작 원리를 살펴보아야 확실하게 알 수 있을 것 같습니다.

이렇게 구성해보았어요.
구성도

소스코드 레포지토리를 이용해서 배포하려 했으나 ECR을 이용하여 이미지를 통해 배포하기로 방향을 선회하면서 아래와 같은 간단한 DockerFile을 추가해주어야 했습니다. alpine 이미지를 사용해 이미지 크기를 줄였고, pnpm을 설치해주었습니다.

FROM node:18-alpine

RUN npm i -g pnpm
WORKDIR /usr/src/app
COPY package*.json pnpm-lock.yaml ./

RUN pnpm install

COPY . .
RUN pnpm build
CMD pnpm start:prod

Github Actions 훅은 블로그를 참고하여 아래와 같이 생성해주었습니다. M1으로 빌드하는지라 이미지 생성 시점에 플랫폼을 amd64로 설정해주어야 했고, 아래 명령어로 이미지를 빌드하여 프라이빗 ECR에 푸시하였습니다.

name: Dockerize
on:
  push:
    branches:
      - main

env:
  AWS_REGION: "ap-northeast-1"
  ECR_REPOSITORY: "tables_api"
  IMAGE_TAG: latest

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    environment: production
  
    steps:
      - name: Checkout
        uses: actions/checkout@v3
  
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}
  
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1
  
      - name: Build, tag and push Image to ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        run: |
          
          docker build --platform linux/amd64 -t $ECR_REGISTRY/$ECR_REPOSITORY:${{ env.IMAGE_TAG }} .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:${{ env.IMAGE_TAG }}
          echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:${{ env.IMAGE_TAG }}"

VPC는 아래와 같이 설정해주었는데, App Runner의 아웃바운드 네트워킹으로 지정된 private2-...-1c 서브넷을 tables-ngw nat 게이트웨이와 연결되도록 라우팅 테이블을 수정해주어 인증/인가를 위해 외부 API를 호출할 수 있도록 조정해주었습니다. RDS에 접근할 수 있도록 private1-...-1a에 위치한 EC2 서버의 라우팅 테이블도 조정해주어 public 서브넷으로 교체해주었습니다(이름만 private).

VPC 리소스 맵

배포하며 마주친 문제들

  1. DB SSH 터널링 수행 문제

현재(2024.02.11기준) App Runner는 도쿄 리전에서만 구축이 가능합니다. 하지만 서비스에서 활용할 DB가 서울 리전의 VPC 내부에 존재했기 때문에 Bastion 서버와의 SSH 터널링을 통해서만 접근이 가능했습니다. 따라서 App Runner 서비스를 배포하는 시점에 SSH 터널링 작업을 해주어야 했는데, 이 부분이 App Runner 단독으로는 쉽지 않았습니다. ssh-tunnel을 통해 코드 상에서 포트포워딩을 수행하든, 터미널에서 포트포워딩을 수행하든 어떠한 방식으로든 App Runner 서비스 내부에서 활용하기 어려웠습니다. 더구나 리전이 다른 두 서비스간 통신에서 발생하는 비용도 무시할 수 없는 바, 도쿄 리전에 VPC를 구성하고 터널링 없이 프라이빗 서브넷 내에서 RDS와 통신할 수 있도록 도쿄 리전의 VPC 내부에 RDS를 추가로 구축해주었습니다.

  1. 소스코드 레포지토리를 이용한 CI/CD 구성의 한계

App Runner를 통해 서비스를 배포하는 데에는 두 가지 방식이 있습니다. 최초에는 소스 코드 리포지토리 방식을 채택하여 컨테이너 이미지를 사용하지 않고 Github 레포지토리에 푸시된 소스를 활용하려 했습니다.

서비스 배포의 두 방식

다만 실제 배포를 진행하면서 다음과 같은 문제가 있어 ECR(컨테이너 레지스트리)을 사용하는 방식으로 선회했습니다.

  1. 패키지 매니저로 pnpm을 사용하고 있었던 점.
  2. 빌드 히스토리를 관리하기 어려웠던 점.
  3. 소스 코드 리포지토리의 배포 시간이 참을 수 없이 느렸던 점(10분 이상이 소요되었습니다).
  4. 소스 코드를 통해 빌드한 이미지를 배포해보니 로컬에서 빌드할 때와는 달리 예상치 못한 에러가 발생하는 점(@nest/swagger 로직에서 순환참조 이슈가 발생하였습니다).

이러한 이유로 리포지토리 레지스트리 방식을 채택하였고, Github Actions를 통해 이미지를 빌드하고 프라이빗 ECR에 배포할 수 있도록 훅을 추가해주었습니다. 추가적으로 husky를 이용해 커밋 직전에 로컬에서 테스트를 수행하여 테스트를 통과한 코드만 maindevelop 브랜치에 병합될 수 있도록 설정하여 코드 안정성을 높였습니다.

  1. VPC 구성 이슈

서울 리전의 기존 DB를 도쿄 리전으로 옮기면서, 덤프한 데이터를 밀어넣어주어야 했습니다. VPC 내부의 RDS는 프라이빗 서브넷 내부에 존재했기 때문에 퍼블릭 서브넷에 EC2를 두어 이를 통해 RDS에 접속할 수 있도록 추가적인 EC2 인스턴스를 구동해주었습니다.
인증/인가를 위해 외부 API를 호출하는 로직에서 upstream request timeout 에러가 발생하여 반나절을 헤맸습니다. 범인은 NAT 게이트웨이의 설정이었습니다. NAT 게이트웨이를 생성하면서 서브넷으로 프라이빗 서브넷을 연결해둔 것이었습니다. '해당 설정을 통해 게이트웨이가 프라이빗 서브넷의 아웃바운드 요청을 처리할 수 있다'라는 의미로 잘못 이해해 발생한 이슈였습니다. NAT 게이트웨이를 퍼블릭 서브넷과 연결해두니 timeout 에러 없이 외부로 요청이 잘 되었습니다.


App Runner, 이런 점은 조금 아쉬웠어요.

  1. 배포 히스토리에 대한 관리를 지원하지 않아 아쉬웠어요.

Vercel, BeanStalk과 달리 배포 히스토리에 대한 관리가 App Runner 단독으로는 가능하지 않았습니다. 배포가 수행될 때마다 로그를 남기긴 하지만, 재배포와 이전으로 롤백하는 것이 가능하지 않았습니다. 더구나 소스코드 레포지토리와 연동하였을 때는 해당 이미지의 소스 코드 정보를 알 수 없어 무엇이 배포되었는지 더 알기 어려웠습니다(Vercel처럼 커밋이나 브랜치가 표기되면 더 좋았을 것 같아요). 결국 배포 히스토리 관리를 위해 ECR과 연동하여 이미지를 관리하여야 했는데요, 상용 서비스에서 사용하려면 이러한 방식이 반강제된다는 점이 무엇보다 아쉬웠습니다. BeanStalk에 비해서 가격적인 측면, 배포적인 측면에서 우위가 있다고 기술한 AWS의 다큐먼트가 무색해지는 순간이었습니다.

App Runner의 단촐한 배포 히스토리
App Runner의 배포 로그
  1. 웹 서버에 대한 한정적인 제어가 아쉬웠어요.

App Runner는 내부적으로 Envoy 웹 서버를 구동하고 있는 듯 보입니다(서버로 전달되는 리퀘스트 헤더를 보니 HostEnvoy였어요). 다만 이 프록시 서버에 대한 제어가 불가능하다는 점이 매우 아쉬웠습니다. 물론 완전 관리형 서비스에서 제어가 웬 말이냐, 하시겠지만 단순 배포 그 이상의 의미를 가지려면 많은 부분들의 제어가 필요하기도 합니다. HTTPS 오프로딩과 오토 스케일링은 자동으로 수행해줘서 편하긴 하지만, CORS 설정, 로드 밸런싱 방식 제어, 웹서버 캐싱 등을 제어할 수 없다는 점이 아쉬웠습니다. 하다못해 배포된 서버에 SSH로 접속할 수 있는 루트만 제공해줬어도 제어가 더 편했을텐데, 하는 아쉬움이 남습니다. 애초에 제어하지 못하도록 만든 서비스니까 받아들여야 하는 걸까요.

오토스케일링만 제어할 수 있었어요.
제어가 오토스케일링뿐?
  1. 로깅 서비스가 아쉬웠어요.

서버 로직 내부에서 발생하는 console.log 로직에만 의존하는 점이 아쉬웠습니다. 컨테이너의 생성 과정에서 발생하는 에러가 충분히 자세하게 로깅되는 것 같지 않아 배포시에 골머리를 앓았습니다. 배포 과정에서 에러들이 발생하기도 하였었는데, 기본 제공되는 로그로는 에러가 왜 발생했는지 원인 파악이 쉽지 않아 문제 해결에 오랜 시간이 걸렸습니다.

로그가 충분하지 못했어요.
서비스 로그만 장황해
에러 로그는 없어
  1. 여러 환경(테스트 등)을 동시에 구축할 수 없다는 점이 아쉬웠어요.

실제 서비스의 배포 외에도 테스트를 위한 stage, 개발을 위한 dev 등의 여러 배포 환경이 필요할 수 있습니다. 다만 App Runner에서는 하나의 서비스 내에서 다양한 배포 환경을 설정해줄 수 없었습니다. 따라서 2개 이상의 App Runner 서비스가 필요하여 복잡하고 관리가 어려웠습니다. Vercel에서 다양한 배포 환경을 설정할 수 있다는 점과 비교되어 더 아쉬움이 남았습니다.

하나의 App Runner 서비스는 하나의 배포 환경만 가질 수 있었어요.

마치며: 쉬워서 아, 쉽다

AWS App Runner을 사용해보면서 든 생각을 한 문장으로 표현하자면 '아, 편하긴 한데...' 라고 정리할 수 있을 것 같습니다. 저렴한 가격에 쉽고 빠르게 배포가 가능하다고 하지만, 살을 붙여갈수록 더 복잡해지고 특정 부분들은 아예 제어가 되지 않는 점 때문에 큰 규모의 프로젝트에서는 선뜻 채택하기가 어려워 보입니다.

아쉽다

그래도 쉽고 빠른 배포라는 목표에 충실한 서비스인 만큼, 적재적소에 잘 활용하면 효과적일 것 같다는 생각입니다. 지난 2주간 적은 레퍼런스에도 끝내 작업을 끝마친 스스로를 다독이면서 글을 끝마칩니다.

profile
🧐 사용자와 개발자를 모두 배려하고 싶은 개발자. 백엔드부터 임베디드까지 다양하게 개발하다가 지금은 🎨 프런트엔드에 자리잡았어요.

0개의 댓글

관련 채용 정보