이슈 발생
사이드 프로젝트 배포를 수동으로 진행하고 있었다.
코드 변경 사항이 발생한다면 EC2 인스턴스에 접속하여 git pull origin main으로 코드를 내려 받아서,
npx prisma generate, npm run build를 실행하여 빌드하고, pm2로 restart 시켰다.
어느날 평소와 같이 변경 사항을 pull 받고, build를 실행하는데
cpu 사용량이 100%에 도달하여 인스턴스가 먹통이 됐다.
인스턴스를 재부팅해야 했다.
사용 중인 인스턴스 유형은 t2.micro인데, 비용 상 인스턴스 유형을 업그레이드 하고 싶지는 않았다.
결국 build는 다른 서버에서 하고,
build 산출물을 EC2 인스턴스에 업로드하여 EC2에서는 서버 실행만 하는 것이 옳다고 판단했다.
이를 Github Actions로 구현해보자.
Github Actions 배포 자동화
github의 Actions 기능은 CI/CD 등을 자동화할 수 있는 툴이다.
프로젝트 루트 디렉토리 .github/workflows 경로에 자동화 스크립트를 작성하면
조건에 따라 자동으로 실행시킬 수 있다.
예를 들어, on: 조건이 아래와 같이 작성된 경우 main 브랜치에 push가 된 경우
jobs 이하의 작업이 실행된다는 의미이다.
name: Build and Deploy NestJS App
on:
push:
branches: ['main']
이번 배포 자동화를 위해 작성한 스크립트는 아래와 같다.
전체를 한 번에 이해하기 보다는 단계별로 쪼개어 살펴보자.
build-and-deploy.yml
name: Build and Deploy NestJS App
on:
push:
branches: ['main']
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js 18.x
uses: actions/setup-node@v4
with:
node-version: 18.x
cache: 'npm'
- name: Install Dependencies (Production)
run: npm ci --omit=dev
- name: Build Nest App
run: npm run build
- name: Generate Prisma Client
run: npx prisma generate
- name: Check Prisma Client generated
run: |
test -f node_modules/@prisma/client/index.js || (echo "❌ Prisma client not found!" && exit 1)
test -f node_modules/.prisma/client/default.js || echo "⚠️ .prisma/client/default.js 없음 (Prisma 5.x에서는 선택적일 수 있음)"
- name: Prepare Output Directory
run: |
mkdir output
cp -r dist node_modules prisma package*.json output/
if [ -d .prisma ]; then cp -r .prisma output/; fi
- name: Archive Build Output
run: |
cd output && tar czf ../nest-app.tar.gz .
- name: Get Public IP
id: ip
uses: haythem/public-ip@v1.3
- name: Print Public IP
run: |
echo "GitHub Actions IP: ${{ steps.ip.outputs.ipv4 }}"
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: 'ap-northeast-2'
- name: Add GitHub Actions IP to Security Group
run: |
aws ec2 authorize-security-group-ingress \
--group-id ${{ secrets.SECURITY_GROUP_ID }} \
--protocol tcp \
--port 22 \
--cidr ${{ steps.ip.outputs.ipv4 }}/32
- name: Upload Build to Server via SCP
uses: appleboy/scp-action@v0.1.6
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
port: 22
source: 'nest-app.tar.gz'
target: '~/deploy'
- name: Execute Remote Deploy Script via SSH
uses: appleboy/ssh-action@v0.1.10
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
port: 22
script: |
bash ~/deploy/deploy.sh
- name: Remove GitHub Actions IP from Security Group
if: always()
run: |
aws ec2 revoke-security-group-ingress \
--group-id ${{ secrets.SECURITY_GROUP_ID }} \
--protocol tcp \
--port 22 \
--cidr ${{ steps.ip.outputs.ipv4 }}/32
전체 흐름 요약
위 배포 자동화 스크립트의 전체적인 흐름은 아래와 같다.
- GitHub Actions: CI 파이프라인에서 npm ci, nest build, npx prsima generate 실행
→ node_modules, prisma, dist/ 디렉토리 생성 - 빌드 산출물(zip/tar): dist/, node_modules, prisma, 필요한 설정파일만 포함하여 압축
- 서버 전송: scp로 대상 Linux 서버에 업로드
- 서버에서 실행: 서버에 미리 작성해둔 deploy.sh 파일로 pm2 서버 실행
1. node_modules, 빌드 산출물 생성
배포 자동화 작업의 첫 번째 단계는 서버를 구동하기 위해 필요한 파일들을 빌드하는 것이다.
1) node 설치
steps:
- uses: actions/checkout@v4
- name: Setup Node.js 18.x
uses: actions/setup-node@v4
with:
node-version: 18.x
cache: 'npm'
먼저 필요한 node 버전을 설치한다.
2) npm ci
- name: Install Dependencies (Production)
run: npm ci --omit=dev
그 다음 패키지를 install 한다.
여기서 --omit=dev 는 상용 서버 구동에 불필요한 dev dependencies 패키지를 제외한다는 의미다.
즉, 서버 구동에 필요한 패키지는 모두 dependencies로 설치되어 있어야 한다.
나는 @nestjs/cli, prisma, @prisma/client 패키지가 dev dependencies 로 설치되어 있었어서 수정해주었다.
3) npm run build
- name: Build Nest App
run: npm run build
npm run build( =nest build) 로 typescript 코드를 빌드한다.
4) npx prisma generate
- name: Generate Prisma Client
run: npx prisma generate
- name: Check Prisma Client generated
run: |
test -f node_modules/@prisma/client/index.js || (echo "❌ Prisma client not found!" && exit 1)
test -f node_modules/.prisma/client/default.js || echo "⚠️ .prisma/client/default.js 없음 (Prisma 5.x에서는 선택적일 수 있음)"
prisma를 사용하는 프로젝트여서 prisma generate 산출물도 필요하다.
처음에는 EC2에서 npx prisma를 실행하려고 했으나,
prsima 모듈을 찾을 수 없다는 에러가 계속 발생해서,
(아마 node_modules를 압축하고 해제하는 과정에서 이슈가 발생한 게 아닐까 싶다)
github actions 파이프라인에서 npx prisma generate 까지 실행하여 산출물을 서버로 전송하는 방식으로 변경했다.
generate 결과물이 잘 생성되었는지 확인하기 위해 test 절차를 추가했다.
prisma 버전에 따라 .prisma 는 없을 수 있다.
2. 산출물 압축
1) 필요한 결과물만 output 디렉토리로 이동
- name: Prepare Output Directory
run: |
mkdir output
cp -r dist node_modules prisma package*.json output/
if [ -d .prisma ]; then cp -r .prisma output/; fi
output이라는 이름의 디렉토리를 생성하여
dist, node_modules, prisma, package.json, package-lock.json 등
서버 구동에 필요한 파일/디렉토리만 담는다.
.prisma는 버전에 따라 있거나 없을 수 있으므로 분기처리 했다.
EC2 서버로 보낼 대상 디렉토리의 구조는 아래와 같다.
output/
├── dist/
├── node_modules/
├── package.json
├── package-lock.json
├── prisma/
└── .prisma/
2) 압축
- name: Archive Build Output
run: |
cd output && tar czf ../nest-app.tar.gz .
그리고 output 디렉토리로 이동하여 그 안의 모든 파일을
EC2 서버에 전송할 nest-app.tar.gz 파일로 압축한다.
3. 산출물 서버 전송
위 단계까지 실행하면 EC2 서버에 전송할 압축 파일이 완성된다.
rsync, scp 등의 방식으로 파일을 서버로 전송할 수 있는데,
나는 scp 방식을 사용했다.
1) github actions 서버 public ip 접근 허용하기
파일을 전송하기 전에 점검해야 하는 부분이 있다.
scp는 ssh 프로토콜을 사용한다.
즉, EC2 서버의 보안 그룹에서 github actions 서버의 ssh port 접근이 허용되어 있어야 한다.
응답의 actions 배열을 확인하면
github actions 서버들의 public ip 리스트를 확인할 수 있다.
매번 사용되는 인스턴스 ip는 변경될 수 있는데, 리스트의 ip 수가 매우 많아서
모두 보안 그룹에서 허용해두기에는 부담이 된다.
이 때 사용 가능한 방식이
동적으로 현재의 github actions 인스턴스의 public ip를 확인하여
이 ip를 보안그룹 허용 ip에 추가하고
배포가 완료되면 보안그룹에서 ip를 제거하는 것이다.
현재 사용 중인 인스턴스의 public ip를 확인한다.
- name: Get Public IP
id: ip
uses: haythem/public-ip@v1.3
- name: Print Public IP
run: |
echo "GitHub Actions IP: ${{ steps.ip.outputs.ipv4 }}"
그리고 AWS에 접근하여 보안 그룹에 현재 public ip를 추가한다.
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: 'ap-northeast-2'
- name: Add GitHub Actions IP to Security Group
run: |
aws ec2 authorize-security-group-ingress \
--group-id ${{ secrets.SECURITY_GROUP_ID }} \
--protocol tcp \
--port 22 \
--cidr ${{ steps.ip.outputs.ipv4 }}/32
이 과정에서 아래 세 가지 환경 변수가 필요하다.
- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY
- SECURITY_GROUP_ID
Github Actions에서 사용되는 환경변수는
레포지토리의 Settings > Security > Secrets and variables > Actions 에서 설정할 수 있다.
아래 두 값은 AWS IAM 에서 사용자를 생성하여 발급할 수 있다.
권한 관리를 위해 github actions에서만 사용할 사용자를 별도로 발급하는 것이 좋다.
- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY
아래 값은 EC2 인스턴스의 보안 그룹 ID이다.
- SECURITY_GROUP_ID
위 step을 거치면 보안 그룹 22포트 허용 ip에 현재 github actions 서버 public ip가 추가된다.
2) 압축 파일 전송
접근이 허용되면 scp로 압축 파일을 전송한다.
- name: Upload Build to Server via SCP
uses: appleboy/scp-action@v0.1.6
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
port: 22
source: 'nest-app.tar.gz'
target: '~/deploy'
EC2 서버의 디렉토리 구조는 아래와 같이 구성했다.
따라서 ~/deploy 디렉토리로 압축 파일을 전송한다.
디렉토리 구조
~/deploy/nest-app.tar.gz ← GitHub Actions에서 전송
~/app/nest-app/ ← 실제 실행 위치
~/app/nest-app/dist/main.js ← 엔트리포인트
~/app/nest-app/prisma/ ← schema 및 migrations
파일 전송 시 필요한 환경 변수는 아래 세 가지다.
- SERVER_HOST: EC2 서버의 ip 또는 도메인
- SERVER_USER: EC2 서버 접속 시 사용하는 user
- SERVER_SSH_KEY
이 중 SERVER_SSH_KEY는 공개키-개인키 방식으로 생성 방법은 아래와 같다.
ssh key 생성 방법
1. 터미널에서 키 생성 명령어 실행
ssh-keygen -t rsa -b 4096 -C "github-deploy"
GitHub Actions는 비밀번호 없이 사용해야 하므로, 패스프레이즈는 비워둬야 한다.
생성 결과:
- github_deploy_key: 개인 키 (이걸 GitHub Secret에 등록)
- github_deploy_key.pub: 공개 키 (이걸 서버에 등록)
2. 서버에 공개키 등록
서버에서 ~/.ssh/authorized_keys에 공개 키 추가하거나
cat github_deploy_key.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
직접 편집할 수 있다.
vi ~/.ssh/authorized_keys
3. Github Actions Secrets에 개인키 등록
SERVER_SSH_KEY로 github_deploy_key 파일의 내용 전체를 복사한다. (예: cat github_deploy_key 결과)
4. 권한 확인(선택 사항)
GitHub Actions에서 사용할 사용자 계정이 서버에 실제로 접근 가능한지 사전에 확인 가능하다.
ssh -i github_deploy_key youruser@your.server.ip
4. 서버 실행
위 단계들을 통해 EC2 인스턴스로 서버 실행에 필요한 파일들이 모두 옮겨졌다면,
서버에 미리 작성해둔 deploy.sh 파일로 pm2 서버를 실행한다.
~/deploy/deploy.sh
#!/bin/bash
set -e
APP_DIR=~/app/nest-app
DEPLOY_DIR=~/deploy
ARCHIVE=$DEPLOY_DIR/nest-app.tar.gz
echo "[1/3] 아카이브 해제 중..."
ENV_BACKUP="$APP_DIR/.env"
if [ -f "$ENV_BACKUP" ]; then
cp "$ENV_BACKUP" /tmp/.env.bak
fi
rm -rf $APP_DIR
mkdir -p $APP_DIR
tar -xzf $ARCHIVE -C $APP_DIR
# 복원
if [ -f /tmp/.env.bak ]; then
mv /tmp/.env.bak "$APP_DIR/.env"
fi
cd $APP_DIR
echo "[2/3] PM2로 앱 시작 또는 재시작..."
if pm2 describe nest-app > /dev/null; then
pm2 restart nest-app
else
pm2 start dist/main.js --name nest-app
fi
echo "[3/3] 배포 완료!"
deploy.sh의 내용은 압축을 해제하고, pm2로 서버를 실행하는 것이다.
다만 여기서 주의할 점은 .env 파일은 미리 해당 디렉토리 내에 저장되어 있어야 하고,
삭제되면 안되기 때문에 .env 파일이 삭제되지 않도록 예외처리 해주었다.
또, PM2는 인스턴스 내에 글로벌 설치 되어 있어야 한다. (npm install -g pm2)
deploy.sh가 잘 실행되면 서버가 구동된다.
그럼 마지막으로 EC2 보안 그룹 허용 ip에 추가되었던 github actions 서버 ip를 제거해주면 끝이다.
- name: Remove GitHub Actions IP from Security Group
if: always()
run: |
aws ec2 revoke-security-group-ingress \
--group-id ${{ secrets.SECURITY_GROUP_ID }} \
--protocol tcp \
--port 22 \
--cidr ${{ steps.ip.outputs.ipv4 }}/32
'개발 팁 > Git & Github' 카테고리의 다른 글
[Git&Github] merge대신 깔끔하게 rebase! (1) | 2023.04.20 |
---|---|
[Git&Github] 잘못된 branch에 작업했을 때, git stash (2) | 2023.04.19 |
[git&github] 가장 최근 commit 메시지 변경하기 (0) | 2023.03.24 |
[git&github] branch 이름 변경하기 (1) | 2023.03.24 |
[git&github] 이미 git으로 관리돼버린 파일 내리고 cache 삭제하기 (0) | 2023.03.24 |