CodeIt Fs 2nd/개발 막혔던 것들

EC2 배포

localslave 2024. 11. 20. 00:22

며칠간 세팅하고 오늘 하루 종일 고생한 경험을 대충이나마 남겨볼까 해서 써봄

 

코드잇 부트캠프에선 배포처를 Render라는 서비스를 추천하고 있다.

그런데 트래픽이 없으면 자주 잠들어서 사용할 때마다 잠깐 쉬다 온다거나 하면 로딩이 안되는 문제가 자주 생겼다.

 

이게 불편한 것도 있고 실제로 현업에서 aws를 자주 사용한다고 들었기에 AWS의 EC2를 이용해 배포를 하기로 했다.

 

지금은 실제 배포할 타이밍은 아니지만, 프론트엔드에서 사용할 수 있는 서버를 제공하기 위해 배포연습 겸 ec2를 세팅하기 시작했다.

 

aws 서비스를 처음으로 사용하는거라거 처음에 많이 헤맸다. aws에서 iam이란 계정 관리 시스템으로 팀원들의 권한을 나눠서 계정을 관리할 수 있기 때문에 이것부터 적용하고 있었다. ec2 배포에 있어서 내가 맡아서 할 것 같다는걸 생각하니 이 작업은 ec2 사용에 있어서는 큰 의미가 없는 것 같지만, 파일 입출력을 위해 s3 서비스도 같이 사용하기로 해서 이쪽에 iam 계정을 사용하기로 했다.

 

사실 ec2에 서버를 올리는 것 자체는 어렵지 않았다. ec2 인스턴스를 생성하고, 우분투로 생성했으니 우분투 명령어만 쓸 줄 알면 됐다.

 

...라면 좀 더 쉬웠겠지. 지금까지 ssh 키 같은걸 사용해본 적이 없어서 키페어 사용하는데부터 문제가 생겼다. 인스턴스 생성 시 키 페어를 발급받았는데 어떻게 사용하는지 몰라서 여기저기 찾아다녀야했다.

이걸 이용해서 ec2의 인스턴스에 접근할 수 있게 됐다. 그런데 ec2 내에선 pm2라는 라이브러리로 서버를 관리할 수 있는 것 같아서 그 라이브러리를 이용하려고 했다.

 

여기까진 그래도 쉬웠다. 실제로 ssh키 사용법을 몰라서 헤맸던걸 빼면 어렵지 않게 성공했다. 문제는 서버를 올리려고 하자마자 발생했다. 환경변수가 없어졌다.

 

당연한 일이다. .env 파일은 .gitignore에 등록되어있으니, 레포지토리에 있을리가 없다. 그러니 서버에선 환경변수를 따로 세팅해줘야한다. 그런데 .env파일을 프로젝트 내에 생성해준다고 해도 프로젝트 폴더가 크게 변경된다던가 하면 문제가 생길 수 있다고 판단했다. 거기에 node js에서 dotenv를 공식 기능으로 편입해줬기 때문에 dotenv 라이브러리를 사용하지 않았는데, nodejs의 env 플래그를 사용할 수 있을지 확신도 서지 않았고.

 

그래서 우선 ec2에서 사용할 수 있는 환경변수 세팅법을 이것저것 알아봤다. ecosystem을 이용한 방법이 있었는데, 이건 프로젝트 내에 파일을 추가하는 방법이라 .env와 마찬가지로 사용하기 꺼려졌다. 기본적으로 cjs이기도 하고.

 

다음으로 알게된 건 /etc/environment에 추가해주는 방법이었다. 그런데 source로 지정해줬는데 어째선지 환경변수를 인식하질 못했다. 이건 해결하려고 해봣는데 결국 원인을 찾지 못했으니 포기.

 

다음으로 해본건 /etc/profile.d/project-env.sh를 사용해봤다. 여기에 추가해주고 source로 지정해준 뒤 env update를 해주니까 환경변수를 인식하기 시작했다.

 

그런데 여기서 끝이 아니었다. ec2에서 제공하는 dns 주소는 너무 길었다. 거기에 포트번호까지 지정해주면 더더욱 길어졌다. 프론트엔드 팀원들에게 가독성이 안좋은 주소를 준다는 느낌이 들어서 뭔가 미안한? 그런 기분이 들었다.

그리고 어짜피 현업에서 이런 세팅을 하게 된다면 당연히 80포트와 443포트를 사용하게 될거라고 생각했다. 그러니 80포트를 열어보자고 생각했다. 하나 할줄 알면 443포트도 비슷하게 할 수 있겠지 싶어서.

 

그래서 nginx를 알아보자...고 했더니 너무 복잡했다. 사실 배우려고 하면 배울 수 있을 것 같은데 당장 프로젝트 기간이 많이 남은 것도 아니고, 내가 맡은 다른 담당 파트도 남아있다. 심지어 기존에 사용하지 않던 타입스크립트를 프로젝트에 적용했기에 여기에 익숙해지는 시간도 필요하고 S3도 세팅해야한다.

 

여유가 없으니 좀 더 간단한 방법을 찾아봤다. authbind라는 라이브러리가 있었다. 사용 방법이 상상 이상으로 간단했다. 정해진 위치에 정해진 파일을 두고, 읽기/쓰기 설정을 주기만 하면 됐다. 그래서 80번 파일을 만들어주고 +x 권한을 줬다. 처음엔 안되는 것 같았는데, 서버를 켜두고 재시작을 안했더라. 다시 켜보니 됐다.

 

사실 여기까지 시간도 어마어마하게 걸리고 서버 재시작 횟수가 천번을 넘지 않나 로그가 계속 쌓이고 난리가 났었는데, 어짜피 해결 됐으니 상관 없다. 그렇게 다들 행복하게 살았

 

으면 참 좋았겠지. 말했다시피 이건 단순한 배포서버가 아니라 프론트에 제공하기 위한 반개발 테스트용 서버다. 메인 브랜치에 dev가 병합될 때마다 내가 이 작업을 해야한다고? 갈수록 내가 해야할 일도 많아질텐데?

 

안된다. 그래서 github action을 사용하기로 했다. 프로젝트 시작 전까진 전혀 모르던 기술이었지만 프로젝트 내에서 리뷰 멤버를 랜덤으로 뽑는 기능을 만든다고 프로젝트 시작 전에 한참 고생했더니 그래도 몸에 좀 익었다.

 

우선 dev브랜치를 자동으로 넘기는 방법을 만들어야 했다. 테스트 코드를 계속 메인 브랜치로 쌓는건 너무 비효율적이라고 생각했다. 그래서 처음으로 만들어진 워크플로우가

name: Deploy to EC2

on:
  push:
    branches:
      - dev

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Deploy to EC2
        uses: appleboy/ssh-action@v0.1.8
        with:
          host: 15.165.235.207
          username: ubuntu
          key: ${{ secrets.EC2_SSH_KEY }}
          port: 22
          script: |
            cd ~/HanCook
            git pull origin dev
            npm install
            npx tsc
            pm2 restart HanCook

이거다. 심플하다. 주어진 ec2 호스트로 ec2 ssh private key(앞에서 죽어라 헤맸던거)를 이용해서 접속한 뒤, 깃헙 주소를 풀받고 빌드작업하고 재시작하는거.

 

당연히 문제가 생겼다. 위 코드는 적당히 예제를 보고 복붙했던거다. 깃헙 액션의 양식에 대해선 어느정도 이해했지만 실제로 이 코드라 무슨 동작을 하는지 잘 이해하지 못한 상태였다.

우선 프로젝트 경로가 틀렸었다. 저건 팀이 정한 프로젝트 명이고, 디렉토리는 전혀 다른 이름이었다. 이 외에도 에러가 많았는데 진짜로 "너무" 많았기에 제대로 기억도 나지 않는다.

 

그래서 스크립트를 다시 읽어보고, 좀 더 고민했다.
우선 호스트 IP가 저기에 대놓고 박혀있는게 보기 싫었다. 보안적으로 문제가 될수도 있고. 두번째로 authbind를 사용하지 않았다.
사실 먼저 ec2를 다 올리고 github action 작업에 들어온 것 처럼 서술했지만 실제로는 양쪽을 같이 했다. 이때 당시에는 80번 포트를 연다는 발상을 못하고 있었다.
그래서 다음으로 개선한게 이거다.

name: Deploy to EC2

on:
  push:
    branches:
      - dev

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Deploy to EC2
        uses: appleboy/ssh-action@v0.1.8
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ubuntu
          key: ${{ secrets.EC2_SSH_KEY }}
          port: 22
          script: |
            cd ~/2-Docthru-team1-BE
            git pull origin dev
            npm install
            npx tsc
            authbind --deep pm2 reload HanCook || (authbind --deep pm2 start dist/src/app.js --name "HanCook")

여전히 문제는 있다.
일단 로그 중간중간 타입에러 같은게 보였다. 왜 타입에러가 나오냐 싶었는데, 잘 생각해보면 팀 레포에서도 로컬 코드를 받고나서 스키마 타입이 변경되거나 하면 마이그레이션을 해줘야한다. 이걸 배포서버에 올린다고 그게 달라질까?

그래서

npx prisma migrate dev --name init

요 파트를 추가해줬다. 당시엔 개발서버로 올리는거니까 개발버전으로 해야지 싶었는데, 잘 생각해보니 이건 배포서버니까 아니었다. 그래서 나중에 변경했다.

 

그런데 여전히 문제가 계속 발생했다. 마이그레이션을 하려는데 에러가 계속 발생하고 있었다. 당시에는 테스트로 쓰던 dev 브랜치에 타입 에러가 제대로 잡히지 않은 코드가 일부 있었다. 그 에러 로그에 가려서 제대로 보지 못했었는데, DATABASE_URL을 인지하지 못하고 있었다.

그렇다. 환경변수 설정이 제대로 안되고 있었다. 위에서 말한 환경변수 문제를 이때 깨달았다. 그래서 부랴부랴 찾아봤는데 그 도중 시도했던게 이 버전이다.

name: Deploy to EC2

on:
  push:
    branches:
      - dev

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install Dependencies
        run: npm ci

      - name: Deploy to EC2
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ubuntu
          key: ${{ secrets.EC2_SSH_KEY }}
          port: 22
          script: |
            cd ~/2-Docthru-team1-BE
            git fetch origin dev
            git reset --hard origin/dev

            npm ci
            npx prisma migrate dev
            npx tsc

            export DATABASE_URL='${{ secrets.DATABASE_URL }}'
            export JWT_SECRET='${{ secrets.JWT_SECRET }}'

            authbind --deep pm2 reload HanCook || authbind --deep pm2 start dist/src/app.js --name "HanCook"
            pm2 status

많은게 바뀌었다. 우선 노드 버전이 달라졌다. 찾아보니까 내 코드, 너무 올드했다. 스타일이 아니라 사용하는 것들이.

ec2의 uses 부분의 액션도 버전이 한참 달라졌다. 그리고 git pull 대신 fetch 후 하드리셋을 해봤다. pull로 하니까 에러가 나던게 일부 있었는데 이거로 해결이 되더라. 왜인지는 지금도 잘 모르겠다. 알아보기엔 너무 뒤늦기도 했고 여유가 없었다.

 

그리고 npm ci를 시도해봤다. package-lock이 있기만 하면 그냥 npm install하는것보다 빠를거라 기대했다.

가장 크게 달라진 부분이 그 아래다. DB_URL과 JWT_SECRET 같은 보안상 중요한 변수를 github action 환경변수로 추가하고, 이걸 넘겨주려고 했다.
마지막엔 pm2 status를 통해 서버가 어떤 상태인지 확인하는걸 추가했다.

 

여기서 내가 가장 고민하게 되는 문제가 터졌다. Prisma에서 에러를 뿜는데, DATABASE_URL이 비었다고 한다.
??? 난 분명히 줬는데? 저게 순서 문제인가? 싶어서 순서도 앞으로 빼봤지만 여전히 안되더라.

그래서 이리저리 시도를 해봤다. 내가 내린 결론은, github action 변수를 통해 환경변수를 넘기는건 별로 좋은 선택지가 아니란 것이다. 차라리 환경변수 파일 내에 보안과 직결될 수 있는 키를 보관하더라도, 이 액션 코드에서 넘기는 건 너무나도 번거롭고, 제대로 실행도 되지 않는 것이다. 그래서 환경변수는 전부 ec2 인스턴스 안에 들어가버렸다.

그래서 대충 정리된 다음 버전은 이렇다.

name: Deploy to EC2

on:
  push:
    branches:
      - dev

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Deploy to EC2
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ubuntu
          key: ${{ secrets.EC2_SSH_KEY }}
          port: 22
          script: |
            cd ~/2-Docthru-team1-BE
            git fetch origin dev
            git reset --hard origin/dev

            npm install
            npx prisma migrate dev
            npx tsc

            authbind --deep pm2 describe "HanCook" > /dev/null
            if [ $? -eq 0 ]; then
              authbind --deep pm2 reload "HanCook" --env /etc/profile.d/HanCook-env.sh
            else
              authbind --deep pm2 start dist/src/app.js --name "HanCook" --env /etc/profile.d/HanCook-env.sh
            fi

            pm2 status

다른건 별로 차이가 없고, --env 옵션으로 환경변수 파일을 인자로 넘겨준 거랑 재시작/시작을 if문으로 나눴다. 깃헙 액션에서 if문을 쓸 수 있다는걸 모르고 있었는데 찾아보니 되긴 하더라. 물론 아직 문법은 잘 모른다. 어찌됐던 python에서 몇번 봤던 느낌의 형식으로 쓸 수 있단걸 알았으니 가로로 너무 길어지지 않도록 if문으로 분리했다.

 

이걸로 해결됐으면 그나마 좋았겠지만... 환경변수가 다시 없어졌다.

뭐지? 왜? 로그를 찬찬히 뜯어봤다. --env 옵션은 ecosystem이 아니면 사용할 수 없는 모양이다. 

그래서 나온 다음 버전이 이거다.

name: Deploy to EC2

on:
  push:
    branches:
      - dev

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Deploy to EC2
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ubuntu
          key: ${{ secrets.EC2_SSH_KEY }}
          port: 22
          script: |
            cd ~/2-Docthru-team1-BE
            git fetch origin dev
            git reset --hard origin/dev

            source /etc/profile.d/HanCook-env.sh

            npm install
            # npx prisma migrate dev
            export DATABASE_URL="$DATABASE_URL" && npx prisma migrate dev
            npx tsc

            authbind --deep pm2 describe "HanCook" > /dev/null
            if [ $? -eq 0 ]; then
              authbind --deep pm2 reload "HanCook" --update-env
            else
              authbind --deep pm2 start dist/src/app.js --name "HanCook" --update-env
            fi

            pm2 status

환경변수 파일을 source로 선언해줬다. 그리고 DATABASE_URL을 환경변수 파일에서 뽑아내기로 했다. 기존 마이그레이션 코드가 주석인건 고치다가 되돌리는걸 썼다 지웠다 하니까 점점 지쳐서 주석으로만 가려놨던거고, 마이그레이션 직전에 DATABASE_URL을 추출해서 선언해버리고, 이걸 그대로 migrate할때 넘겨줬다. 이러니까 인식을 하더라.

 

더해서 --env 대신에 source로 선언한 env 파일을 --update-env로 연결해줬다. 이러니 환경변수가 제대로 인식되기 시작했다.

사실상 이게 최종버전이다. 그 뒤로 한거라곤 migrate dev를 production으로, 브랜치를 main으로 옮긴게 전부다.

 

이것으로 만 24시간 이상의 기나긴 ec2 여정이 끝났다.

사실 끝났는지는 모른다. 배포만 한거지 실제 사용하면서 어떤 문제가 생길지는 봐야하니까.

 

그래도 너무 고생했다. 내가 어떤 고생을 했는지 솔직히 이 작업이 끝나고 바로 쓰는 나조차 잘 기억을 못하는데, 기억력이 금붕어만한 미래의 내가 이걸 기억할리가 없으니 추억이라도 남기란 의미에서 끄적여봤다.