Next.js 초간단 배포 자동화 구축 이야기

Dengo·2023년 10월 9일
2

Project

목록 보기
3/3
post-thumbnail

지난 4개월 가까이 팀 프로젝트(with Next.js 13) 배포 자동화를 구축하면서 정말 가벼운 형태로 유지되게 구현했는데, 희노애락이 가득했던 그 과정을 소개하고자 합니다.

계기

팀원들과 두 번째 사이드 프로젝트를 진행하면서 초기에 배포에 대한 이야기가 나오게 되었습니다.

👤 : 이번 프로젝트에서는 개발 서버를 두면 어떨까요? 반영된 내용을 실 배포 환경에서 확인할 수 있으면 좋을거 같아요!

저를 비롯하여 모두 수긍 했고 당시의 작업 분배에서 제가 담당하게 되었습니다.

👤 : 이번에는 PR이 머지 되는 단위로 자동 배포까지 이루어지면 어떨까요?

추가적으로 이런 요구사항이 팀 내에서 나오게 되었고 지난 프로젝트에선 수동 배포만 구축되어있던 터라 해보고 싶은 도전이라고 생각하게 되어 어떻게 구축할 것 인지 생각하게 되었습니다.

계획

먼저 수동 배포가 정상적으로 이루어져야 한다고 생각했습니다.
그리고 나서 수동 배포 과정을 그대로 Github Action에 옮겨서 자동화가 이루어지게 한다는 계획을 세웠습니다.

준비를 위한 준비

사실 앞선 프로젝트에서 저희는 이미 배포 파이프라인이 구축되어있는 상태였습니다.
하지만 지난번 프로젝트에서는 모노레포 기반으로 서비스를 영역 별로 쪼개어 독립된 배포를 구축하려 했던게 가장 큰 도전 과제 였었죠.
결과적으로는 사이드 프로젝트 환경에선 어려움이 많아 쪼갰던 서비스를 다시 하나로 합쳐 하나의 EC2 인스턴스 만을 운영하도록 변경했습니다.

이렇게 길게 앞선 이야기를 한 이유는 제가 새로 구축하려 했던 시점에는 모노레포를 위한 잔재 또한 많아서 새로운 프로젝트에서 가져가야 할 부분, 버려야 할 부분을 솎아내야 했습니다.

개괄

추후 본격적인 설명을 위해서 전체적인 개괄을 앞서 설명하고자 합니다.

로컬에서 일어나는 일

우리가 사용하는 각자의 컴퓨터에서 일어나는 일을 먼저 살펴보겠습니다.
1. EC2와의 연결 확인 (EC2가 healthy한 상태인지)
2. Next.js프로젝트 빌드
3. 빌드한 내용을 압축
4. EC2로 전송
5. EC2에 배포 요청

EC2에서 일어나는 일

배포의 대상이 되는 서버에서 일어나는 일을 이어서 살펴보겠습니다.
6. EC2내부에서 서비스를 실행할 폴더에 수신한 압축된 빌드 파일 압축 해제
7. EC2내부에서 서비스 재시작

대략적인 과정이 이렇고 자세한 내용을 살펴보도록 하겠습니다.

수동 배포 구축 과정

준비

저희 프로젝트의 배포 프로세스 중 가장 큰 특징은 배포를 위한 도구(Jenkins, CodeDeploy같은 툴)를 사용하지 않았다는 점 입니다.

대신에 Express와 Shell Script를 사용해서 구현을 했고 이 구성을 다시 한 번 채택하게 되었습니다.
따라서 준비할 내용은 다음과 같아졌습니다.

  • 서비스를 공급할 EC2 인스턴스
  • 배포환경을 제어할 Express와 Routing Code
  • 배포 요청을 수행할 Shell Script (EC2 내부의)
  • 배포를 요청할 Shell Script (로컬에서의)

각각이 왜 필요한지, 어떻게 쓰이는지 하나씩 보겠습니다.

AWS EC2 Instance

간단히 소개하면 EC2는 하나의 컴퓨터로써 우리의 Next.js빌드 파일이 24시간 내내 공급시켜줄 도구 입니다.

EC2에 프로젝트를 올려서 다른 사람들이 접근 가능하게 하는 것을 배포라고 하는데 이것을 배포하는 과정을 다루기엔 복잡해서 제가 과거에 작성한 글(AWS EC2 인스턴스에 Next.js 프로젝트 올려보기)을 비롯해서 다른 자료를 참고하여 배포를 하면 되겠습니다.

특이사항이 있다면 Node.js프로세스 관리자인 pm2를 사용하여 Next.js빌드 파일을 실행시켰습니다.

(실행 예시)
pm2 start "yarn start" // EC2 내부에서 최초에 직접 실행
pm2 restart "yarn start" // 외부 인터페이스를 통해 재배포할때 유용함
pm2 start index.js // js파일은 node command를 안 붙여도 됨
pm2 monit // 프로세스 상태 확인

또한 Next.js프로젝트가 3000번 포트로 공급되고 있는데
http:IP주소 로 단순히 웹 브라우저에서 접속하게 되면 80포트로 접근하게 되기 때문에 80포트 -> 3000포트로 리다이렉팅 해주는 명령어 또한 실행시켰습니다.

sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 3000

여기까지 진행했다면 3000포트, 80포트 둘 다 정상적으로 웹 페이지를 띄어주고 있어야 합니다.

Express와 Routing Code

외부에서 우리가 만든 EC2에 명령을 내릴 인터페이스가 필요한데 그것을 Express를 통해 구현했습니다.

예를 들어

curl http:IP주소:8080/deploy

이런 Linux명령을 통해서 우리의 EC2가 내부적으로 배포를 실행할 수 있게 해주는 것 이죠.

EC2 인스턴스 안에 Express를 설치하고 코드를 작성해야합니다.
이 코드에서 수행해줄 일은 위에 처럼 /deploy같은 경로로 밖에서 접근한다면 원하는 Shell Script파일이 실행되게, 또는 명령어가 실행되도록 하는 것 입니다.

따라서 JavaScript코드로 Linux명령을 실행시킬 도구가 필요한데 Shell.js라이브러리를 사용하였습니다.

// index.js
const PORT = 8080;

function handleListen() {
  console.log("Listen on 8080 port");
}
// 배포를 위해 EC2와의 연결 확인 + 준비
function handleInit(req, res) {
  shell.exec(`mkdir -p /home/ubuntu/builds/`, (err) => {
    res.status(200).send("success");
  });
}
// EC2 내부 배포 실행 Shell Script 실행
function handleHome(req, res) {
  shell.exec(
    `env /home/ubuntu/deployer/deploy.sh`,
    (err) => {
      res.status(200).send("success");
    }
  );
}

shell.chmod("+x", "/home/ubuntu/deployer/deploy.sh");


app.get("/init", handleInit)
app.get("/deploy", handleHome);

app.listen(PORT, handleListen);

그리고 마찬가지로 이 파일을 pm2로 실행시켜줍니다. (pm2 start index.js)
그리고 또 마찬가지로 이것은 8080포트로 접근해야하니 EC2를 생성할때 설정했던 보안 그룹에 8080포트 또한 열어주면 접근 가능해집니다.

Tip
8080포트가 제대로 열려있는지 확인하기 위해
curl http:IP주소:8080/init 실행으로 확인 가능합니다.

배포를 직접 수행할 Shell Script (EC2 내부의)

위의 Routing Code에서 /deploy경로 접근에 대해서 deploy.sh라는 Shell Script를 실행시켜주는 것을 볼 수 있습니다.
이게 실질적으로 EC2가 배포를 수행하게 할 내용인거죠.

echo "##################배포를 시작합니다.##################"
  
cd /home/ubuntu/toks-web/
rm -rf .next
git checkout dev
git add .
git stash
git stash clear
git pull origin dev

cd /home/ubuntu/builds/

tar -xzvf .next.tar.gz -C /home/ubuntu/toks-web/

cd /home/ubuntu/toks-web
pnpm install
pm2 restart "pnpm start"

echo "##################배포를 완료.##################"

위의 개괄에서 간략하게 살펴본 6-7번 과정이 여기에 해당됩니다.
지금은 일단 압축된 빌드 파일을 수신했다는 가정하에 위의 스크립트를 보면 좋겠네요.

스크립트에서 압축을 해제하기 전에 과정들은 EC2 내부적으로 이전 실행 상황을 현재 실행 상황에 맞추기 위해 영점을 맞추는 과정이라고 보면 되겠습니다.

이미 실행되고 있는 상황에서 재배포를 한다고 가정하면 기존 실행 상황에 대해서 .next가 새로 전달받은 빌드폴더로 문제 없이 실행되기 위해서 rm -rf 과정을 거쳤다고 보면 되고 이후 git명령들도 전부 같은 맥락입니다.

압축을 해제한 뒤에는 pnpm install을 EC2 내부적으로 실행해주는 것을 볼 수 있는데
빌드된 파일을 외부에서 전달받았다고 해도 의존성 업데이트 만큼은 내부에서 직접 해주어야 하기 때문입니다.
이로 인해서 프로젝트 폴더의 내부 상황과 전달받은 빌드 파일의 시점이 동기화 되어서 실행에 차질이 없게 되는거죠.

마지막에는 이미 실행되어있다고 가정하고 pm2 restart로 재실행을 시켜줍니다.

배포 요청을 수행할 Shell Script (로컬 내부의)

마지막으로 배포를 요청할 Shell Script입니다.
(위의 개괄에서 1-5번까지의 내용이죠.)

git checkout dev //dev브랜치에서 배포한다고 가정
git pull

pnpm install
pnpm build

ip="EC2 Ip주소"
instance="EC2 도메인"

curl http://${ip}:8080/init
mkdir -p ./builds/
tar -czv .next > ./builds/.next.tar.gz
scp -r -i ../EC2키이름.pem ./builds/.next.tar.gz ubuntu@${instance}:/home/ubuntu/builds/.next.tar.gz

curl http://${ip}:8080/deploy

의존성 설치, 프로젝트 빌드, 압축, 전송, 배포 요청 위에서 설명드렸던 바를 스크립트 코드로 옮긴 모습입니다.

별도로 설명할 부분이 있다면 ip주소와 instance의 도메인 주소를 변수에 담아서 사용했다는 점과

tar명령을 통해서 압축을 하고 scp명령을 통해서 EC2에 전송한 내용을 설명드릴 수 있겠습니다.

scp명령을 통해서 EC2에 파일을 전송할때는 EC2에 접속하기 위한 pem키가 필요하다는 점도 잊으면 안되겠습니다.

마지막으로 앞서 우리가 준비한 Express포트에 deploy요청을 보냄으로써 배포 요청이 완료됩니다.

bash명령으로 이 스크립트를 실행시키면 수동 배포를 진행할 수 있습니다.

주의
위의 모든 코드는 제 개인 환경에서 작업한 결과물이니 당연히 그대로 따라했을 경우 동작을 안할 수 있습니다. 코드는 참고만 해주세요.

자동 배포 구축 과정

수동 배포까지 구축했다면 자동 배포 구축은 사실 정말 단순 합니다.

앞선 설명을 전부 읽었다면 위 그림의 과정이 한번에 이해가 되실텐데요.
수동 배포 과정에서 마지막에 소개한 배포 요청을 수행할 Shell Script만 Github Action을 통해 일어나도록 하면 되겠습니다.

Github Action 소개

인터넷 검색을 통해 더 자세한 설명을 접할 수 있겠지만,
이번 글을 기준으로 Github Action을 간단히 설명드리자면
dev브랜치에서 Github Repository로 push가 일어날때 무언가의 동작(이번 글에선 배포 요청)이 일어나게 하는 Github의 서비스 중 하나입니다.

.github폴더를 프로젝트 루트에 생성하고 그 안에 workflow폴더를 또 생성한 뒤에 yml확장자 파일을 만들면 되겠습니다.

deploy.yml

name: Deploy

on:
  push:
    branches:
      - 'dev'

jobs:
  ci:
    name: Dev
    runs-on: ubuntu-latest

    env:
      NEXT_PUBLIC_BASE_URL: ${{ secrets.BACKEND_BASE_URL }}

    strategy:
      matrix:
        node-version: [16.x]

    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}

      - name: Install pnpm
        run: npm install -g pnpm

      - name: Install dependencies
        run: pnpm install

      - name: Run lint
        run: pnpm lint

      - name: Run Build
        run: pnpm build

      - name: Express Init
        run: curl http://${{ secrets.HOST_DEV }}:8080/init

      - name: 빌드 폴더 생성
        run: mkdir -p ./builds/

      - name: 빌드 파일을 압축
        run: tar -czv .next > ./builds/.next.tar.gz

      - name: 빌드 파일을 EC2로 전송
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.HOST_DEV }}
          username: ${{ secrets.AWS_USER_NAME }}
          key: ${{ secrets.AWS_DEV_KEY }}
          source: './builds/.next.tar.gz'
          target: '/home/${{ secrets.AWS_USER_NAME }}/'

      - name: Express Deploy
        run: curl http://${{ secrets.HOST_DEV }}:8080/deploy

해당 workflow파일은 사실 저 또한 모든 것을 이해하고 작성한게 아닙니다.
따라서 이번 글을 통해서 각 명령어가 의미하는 바가 뭔지 설명하긴 어렵지만 앞서 소개된 로컬에서의 Shell Script가 workflow파일에선 어떻게 변경 되는지 살펴보겠습니다.

on: push: branch:

가장 상단의 on은 얼핏 보아도 dev브랜치에 push 되었을때 이 파일이 실행된다는 설정으로 이해가능합니다.

이번 글은 배포 자동화를 위한 글이었죠.
저희 프로젝트에서는 dev브랜치는 직접 push를 하는 브랜치가 아니고 Pull Request Merge를 통해서만 합쳐질 수 있는 브랜치 입니다.

따라서 PR이 dev브랜치에 합쳐지는 시점에 자동으로 배포가 이루어지게 할 수 있게됩니다.

env

process.env를 프로젝트에서 사용하였는데 이것을 세팅하기 위함입니다.
자세한 내용은 밑의 시행 착오 코너에서 이야기 하도록 하겠습니다.

의존성 설치 ~ 압축

얼핏 보아도 기존 Shell Script의 과정을 그대로 실행하고 있습니다.
따로 설명은 적지 않겠습니다.

전송 (appleboy/scp-action@master)

앞서는 로컬에서 배포 요청을 수행했기에 scp명령시에 EC2의 pem key를 로컬에 저장해놓고 이것을 참조하게 하면 됐었습니다.

하지만 지금은 배포 요청을 하는 주체가 로컬에서 Github으로 변경된 상황 입니다.

따라서 pem key를 안전한 외부 공간(Github Secrets) 에 저장해놓고 이것을 참조하도록 해야하는데 이것을 appleboy/scp-action툴을 사용하여 해결할 수 있습니다.

Github Secrets

해당 yml파일이 Github에 그대로 올라가게 될 경우 pem key를 비롯해서 IP주소나 도메인 정보등 노출에 민감한 정보들이 그대로 올라가게 됩니다.

저는 이 문제를 해결하기 위해서 Github에서 제공하는 Secrets를 통해 저장하고 workflow가 참조할 수 있게 했습니다.

사진과 같이 Repository의 Setting탭에서 설정 가능합니다.

참고로 pem키의 경우 pem키를 VS Code와 같은 에디터를 통해서 열어주게 되면 내용이 노출되는데 이것을 복사해서 넣어주었습니다.

여기까지 자동 배포를 위한 여정을 살펴보았습니다.
테스트는 dev브랜치에 변경 사항을 push시킨 후 Actions탭에서 잘 돌아가는지 확인하면 되겠죠?

시행착오 ⛰️

지금까지 최대한 간결한(?)설명을 위해 정말로 과정만을 적었는데
사실 이 배포환경을 운영하면서 여러가지 문제가 있었습니다.

대표적으로 생각나는 문제들을 소개해보겠습니다.

환경변수 참조 문제 (process.env)

저희 프로젝트에서는 Backend Base Url을 노출에 민감한 데이터로 간주하여 .env파일에 넣고 process.env를 통해서 이 값을 가져와 API call을 수행하였습니다.

하지만 이 방법은 곧 문제가 되었는데,
정작 배포가 된 프로덕트에서는 이 값을 가져오지 못하는 문제가 발생했습니다.

원인은 현재 구조가 Github 에서 빌드를 해주고 있기 때문에 .env파일의 내용 또한 별도로 처리해야 했던 것이었습니다. 앞서 pem key와 비슷한 맥락이랄까요.

어쨌든 이를 위해서 deploy.yml에서 env에 직접 Backend Base Url을 넣어주고 있습니다. 그리고 이 데이터 역시 Github Secrets에 담아서 가져오게 했습니다.

env:
      NEXT_PUBLIC_BASE_URL: ${{ secrets.BACKEND_BASE_URL }}

Package Manager

현재는 pnpm을 사용하고 있지만 저희 팀에서는 기존에 yarn berry를 통해서 Package Managing을 수행해 왔습니다.

초반에는 문제가 없었는데,
어느 순간부터 EC2 내부에서 yarn을 이용하여 의존성을 설치하거나, 빌드를 하려 하면 진행 도중에 멈춰 버리는 문제가 일어났습니다.

이 문제 덕분에 저는 EC2를 굉장히 자주 껐다 켰고 나중에는 껐다 켜도 배포를 할 수 없는 상황이 일어났습니다.

사실 시원하게 해결했으면 좋았겠지만... 결국에는 해결하지 못했습니다.

때마침 팀원 중 한분이 윈도우를 사용하면서 yarn berry가 원할하게 동작하지 않는 이슈로 인해 pnpm으로 Package Manager를 변경하게 되었는데

이때부터 다시 원할하게 내부 동작이 수행될 수 있었습니다.

추가로 이번 글을 통해서 소개한 배포과정 중에 굳이 빌드된 파일을 압축해서 전송하는 과정이 있는데, 이것은 EC2에서 모든 동작을 수행하지 않기 위함입니다.

EC2 내부에서 동작을 수행하는 것이 개인적으로는 불안정하다고 생각이 되었고 의존을 덜어내고자 압축을 시킨 후 전송하는 방식을 택했습니다.
(대신에 앞서 설명했다시피 의존성 설치는 EC2에서 별도로 시켜주어야 합니다.)

Next Image(13버전)

저희는 웹 프레임워크로 Next.js 13버전을 선택하여 프로젝트를 제작해왔습니다.
Next.js의 이전 버전에선 자체 컴포넌트인 Image가 로컬의 이미지만 가져올 수 밖에 없었다면 이번 버전에선 외부 URL을 통해서도 가져올 수 있게 수정해놓았습니다.

따라서 이미지를 보여줘야할땐 img태그를 사용하지 말고 Image컴포넌트를 사용하라고 공식적으로 권고하고 있습니다.

프로젝트 내부에서 모든 이미지는 Image컴포넌트를 사용하였고
로컬에서 실행할때는 전혀 문제가 없었으나, EC2에 올린 서비스에서는 일부 이미지를 출력하지 못하는 문제가 있었습니다.

더 정확히는 Next.js에서 최적화를 위해 변환한 webp형식의 이미지를 내부적으로 가져오게 되어있는데 이 파일을 일부 사진에서 가져오지 못하고 무한 대기중 현상이 일어나게 되어 EC2가 급기야 정지되는 문제였습니다.

이 역시 완벽한 문제 파악은 어려웠으나 이미지의 확장자 혹은 반응형 이미지 였기 때문으로 예상하고 있습니다.

일단은 img태그를 사용해서 문제를 해결했으나 추후 개선사항으로 팀원들에게 공유하며 마무리 지었습니다.

마무리

프로젝트 배포를 처음 맡아보면서 여러 문제도 많았지만 정말 많은 것을 알아가는 계기가 되었습니다.
단순 프로젝트 구현이 아닌 환경을 구축하면서 더 편하게 구축하려면 어떻게 해야할지, 더 의존성을 덜어내어서 안전하게 구축하려면 어떻게 해야할지를 고민하게된 계기로 남을 것 같습니다.

긴 글 읽어주셔서 감사하고 지적사항 있다면 댓글에 남겨주시면 감사드리겠습니다!!🙇‍♂️

profile
Software Engineer (전산쟁이)

0개의 댓글