Crohasang Logo
#16

NestJS 백엔드와 EC2 인스턴스로 개인 사이트에 RDS 연결하기

2025년 11월 15일 오후 04:57

지난 주, 나는 개인 사이트에 게시글을 올리기 위해 MySQL RDS를 생성하고 Next.js에서 라우팅을 설정했었다. 하지만 Vercel은 고정 IP를 제공하지 않아 RDS의 인바운드 규칙을 설정할 수 없는 문제가 발생했고, 결국 현재 구조로는 RDS에 접근하지 못한다는 사실을 알게 되었다.

(이전 작성 글: https://quickchabun.tistory.com/191 )

어떻게 하면 Vercel로 배포하면서 RDS에 접근할 수 있을까?

  1. Vercel의 Static IP 기능을 활성화해서 고정 IP를 생성한다. (구매 필요)
  2. AWS Lambda를 VPC 내부 DB 프록시로 두고 HTTP로 호출한다.
  3. 데이터베이스를 AWS RDS에서 고정 IP 없이 접근이 가능한 데이터베이스로 마이그레이션한다.

어떤 방법을 쓸지 고민하다가, 생각해보니 직접 RDS에 접근하는게 아니라, EC2 인스턴스를 생성해서 백엔드를 구축하고 Vercel에서 EC2에 연결하면 되지 않을까? 하는 생각이 들었다.

image 1

마침 예전에 내 개인 페이지에 백엔드를 도입하고 싶어서 모노레포를 도입해 프론트엔드(NextJS) 레포와 백엔드(NestJS) 레포로 분리해놨었다. 만들기만 해놓고 지금까지 아무 작업을 하지 않았었는데, 이번 기회에 백엔드를 구축해보면 좋을 것 같다는 생각이 들었다. 그러면 시작해보자.


1. NestJS로 게시글 조회 API를 만들자

백엔드 레포에 게시글을 조회할 수 있게 API 서버를 만들어야한다. ORM은 prisma를 쓸까 typeorm을 쓸까 고민했었는데 저번에 인프런 NestJS 강의를 들었을 때 TypeORM을 사용했었기 때문에 더 익숙한 TypeORM을 선택했다.

먼저 posts 디렉토리를 생성하고 API 구축에 필요한 파일들을 생성해주었다.

post.entity.ts를 생성해서 데이터베이스의 테이블 구조를 정의해주었다.

import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';

@Entity('posts')
export class Post {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'varchar', length: 255 })
  title: string;

  @Column({ type: 'text' })
  body: string;

  @CreateDateColumn({ name: 'created_at' })
  created_at: Date;

  @UpdateDateColumn({ name: 'updated_at' })
  updated_at: Date;
}

posts.service.ts를 생성해서 게시물 목록과 게시물을 조회하는 비즈니스 로직을 구현했다.

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Post } from './post.entity';

@Injectable()
export class PostsService {
  constructor(
    @InjectRepository(Post)
    private postsRepository: Repository<Post>,
  ) {}

  async findAll() {
    const posts = await this.postsRepository.find({
      order: { created_at: 'DESC' },
    });

    // displayId 추가
    const postsWithDisplayId = posts.map((post, index) => ({
      ...post,
      displayId: posts.length - index,
    }));

    return postsWithDisplayId;
  }

  async findByDisplayId(displayId: number) {
    const posts = await this.postsRepository.find({
      order: { created_at: 'DESC' },
    });

    const postIndex = posts.length - displayId;

    if (postIndex < 0 || postIndex >= posts.length) {
      throw new NotFoundException('Post not found');
    }

    return posts[postIndex];
  }
}

게시글을 조회할 때 displayId 속성을 추가해줬는데, 전체 게시글 수에서 자신의 인덱스를 빼주었다.

→ 글이 10개일 때, 게시글이 가장 오래된 게시글의 인덱스는 10 - 9 = 1이 된다.

그리고 posts.controller.ts에서 요청이 들어오면 posts.service.ts 로직이 실행될 수 있게 연결해주었다.

import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
import { PostsService } from './posts.service';

@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  @Get()
  findAll() {
    return this.postsService.findAll();
  }

  @Get(':displayId')
  findOne(@Param('displayId', ParseIntPipe) displayId: number) {
    return this.postsService.findByDisplayId(displayId);
  }
}

그리고 posts.module.ts에서 entity, service, controller를 하나의 모듈로 묶고 NestJS에 등록해주었다.

→ 그리고 이 모듈은 app.module.ts에 import 해준다.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';
import { Post } from './post.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Post])],
  controllers: [PostsController],
  providers: [PostsService],
})
export class PostsModule {}

2. EC2 인스턴스를 생성하고 RDS와 연결해보자.

/posts api 엔드포인트를 만들었으니 이제 AWS EC2 인스턴스를 생성하고 연결을 하면 된다.

a. AWS EC2 콘솔로 이동해서 EC2 인스턴스를 생성해주었다.

사실 EC2 인스턴스를 생성하는 건 이번이 처음이어서 AMI를 Amazon Linux를 선택할지 Ubuntu를 선택할지와 같은 선택의 기로에서 많이 헷갈렸었는데 LLM의 도움을 받아 하나씩 선택하면서 진행할 수 있었다. (참고로 AMI는 Amazon Linux를 선택했다)

b. 인스턴스를 생성하고 EC2 Instance Connect로 브라우저 터미널로 접속했다.

사실 지금까지는 내장 터미널에서 ssh에 접속했었는데, 브라우저에서 터미널을 접속할 수 있다는 사실이 신기했다.

c. 접속 후 ‘sudo dnf update -y’ 명령어를 입력해 시스템을 업데이트해주었다.

그리고 Node.js, pnpm, git을 설치하고 개인 블로그 레포에서 내가 작업 중인 브랜치를 clone했다.

clone이 끝나고 백엔드 디렉토리로 이동해 pnpm install 명령어를 입력하여 의존성 패키지를 설치하려고 했는데 보안 권한 때문인지 설치가 안되는 패키지들이 있었다.

→ ‘pnpm approve-builds’ 명령어를 입력하고 다시 설치하면 된다.

그리고 ‘nano .env’ 명령어를 입력해 .env 파일을 생성 후 환경변수들을 입력해주면 어느정도 설정은 마무리된다.

d. RDS와 EC2의 보안 그룹 인바운드 규칙을 업데이트했다.

RDS 보안 그룹에 EC2 인스턴스 보안 그룹 ID (launch-wizard-1)를 소스로 추가해줘서 EC2에서 RDS에 접근이 가능하도록 했다.

그리고 EC2 보안그룹의 인바운드 규칙에 SSH(22), HTTP(80)를 추가했는데 이상하게 백엔드 통신이 되지 않았다. 생각해보니 백엔드는 3001번 포트에서 실행이 되고 있었고, Custom TCP의 3001번 포트도 인바운드 규칙에 허용해주니 그 때부터 통신을 할 수 있었다 (모노레포를 사용하고 있어서 프론트엔드는 3000번 포트, 백엔드 포트는 3001번 포트를 사용하고 있었다)

e. 다시 터미널로 돌아와서 PM2 라이브러리를 설치해주었다.

PM2는 Node.js 애플리케이션을 백그라운드에서 실행하고, 손쉽게 관리할 수 있게 해주는 프로세스 매니저다. 일반적으로 터미널에서 Node.js 서버를 실행하면 탭을 닫거나 세션이 종료되면 서버도 함께 꺼지지만, PM2로 구동하면 서버가 계속 백그라운드에서 안정적으로 돌아갑니다.

먼저 ‘pm2 start dist/main.js --name 내 프로젝트 이름’ 명령어를 입력해 PM2로 백엔드를 실행시켜주고, ‘pm2 status’ 명령어를 입력해 잘 실행되고 있는지 확인했다. 그리고 ‘pm2 logs 내 프로젝트 이름’ —line 20’ 명령어를 입력해서 로그를 확인했다.

서버가 잘 켜진 것을 확인했으니 이제 startup 명령어를 입력해서 운영체제의 부팅 시스템에 PM2가 자동으로 실행되도록 등록해야 한다.

‘pm2 startup’ 명령어를 실행하면 서버의 운영체제에 맞는 자동 실행용 스크립트 명령어가 출력된다.

To setup the Startup Script, copy/paste the following command:
sudo env PATH=$PATH:/usr/bin /usr/lib/nodejs18/lib/node_modules/pm2/bin/pm2 startup systemd -u ec2-user --hp /home/ec2-user

저 명령어를 복사 후 실행하면 운영체제에 PM2가 등록되어서 서버를 재부팅해도 PM2가 자동으로 켜진다. 마지막으로 ‘pm2 save’를 입력해서 현재 앱 목록을 저장했다.


3. Next.js에서 라우팅을 RDS → EC2로 변경하자

이제 프론트엔드에서 RDS에서 EC2로 라우팅시키면 된다. 바꾸는 과정은 어렵지 않다. 지금까지는 커넥션 풀을 생성해서 RDS 주소로 연결했었는데, 이제 그 주소를 EC2 인스턴스 IP 주소로 변경하면 된다.

→ 즉, 환경 변수를 새로 만들고 바꾸면 된다. .env 파일만 변경할게 아니라 vercel의 환경변수도 변경해야 배포 하면 된다.


NestJS API 구현 + Next.js 라우팅 작업 Pull Request: https://github.com/crohasang/crohasang_page/pull/20

프론트엔드 코드를 push하고 배포했더니, 지금까지 접속이 안되었던 ‘/post’에 접근할 수 있게 되었다. 이번에 EC2 인스턴스를 직접 생성하고, NestJS API 서버를 구현하면서 그동안 막연하게만 느껴졌던 AWS와 백엔드에 대해서 한 걸음 더 나아갈 수 있었다. 저번 달에 RDS 고정 IP 비용이 청구되었기에 (천원정도) 갑작스럽게 청구서가 날아오지 않을까 살짝 두렵긴 하지만, 그래도 아직은 프리티어가 적용되니까 크게 걱정하지 않기로 했다.

이제 DB에 저장되어 있는 게시물을 볼 수 있는 건 구현했으니, 이제 구조를 더 확장할 차례이다. 좋아요와 댓글을 구현해도 되고, 백오피스를 프론트엔드로 구현해서 게시물 업로드를 개인 웹 사이트에서 직접 해도 재밌을 것 같다. 차례차례 해보자!