rimo030 / nestjs-e-commerce-frame

✏️ NestJS로 구현한 Commerce API
47 stars 1 forks source link

AWS EC2와 shell script를 이용한 배포 자동화 #20

Open kakasoo opened 1 year ago

kakasoo commented 1 year ago

요즘은 배포를 전부 github action을 이용해서 합니다만, 과거에는 어떤 걸로 했을까요?

kakasoo commented 1 year ago

shellp script를 통한 배포 자동화

npm i -g pm2

pm2를 설치했다는 가정 하에 아래와 같은 shell script를 작성합니다.

# script.sh
cd /server/folder

git fetch

local=$(git rev-parse HEAD)
echo $local

target=$(git rev-parse origin/main)
echo $target

if [ $local != $target ]
then
        git pull origin main
        echo '풀 완료'
        npm install
        npm run build
        pm2 reload 0
fi

이 스크립트는 git fetch 후 main origin branch의 가장 마지막 커밋이 무엇인지 확인하고, 지금 현재 local의 가장 마지막 커밋과 서로 다르다면 Pull을 받은 다음 build, 서버 재실행을 하게 하는 명령어가 작성되어 있습니다. 재실행이라는 말은, 이전 최초의 서버 실행은 개발자가 직접 ssh로 접속해서 실행해줘야 한다는 의미겠죠?

crontab

리눅스 crontab에는 아래와 같이 작성합니다. crontab은 리눅스 운영체제에 크론식을 작성해서 특정 시간마다 특정 스크립트를 실행하게 할 수 있어요. 아래는 * * * * * 라는 크론식으로 매 분 마다 라는 의미를 가지고 있는데, 매 분 마다 스크립트를 실행하라고 작성한 거에요.

SHELL=/bin/bash
PATH=/root/.nvm/versions/node/v14.15.5/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin

* * * * *  /server/folder/script.sh
kakasoo commented 1 year ago

저도 쉘 스크립트는 짜는 법을 다 까먹었기 때문에 대충 이런 흐름이겠거니 하고만 기억해두고 있습니다. 쉘 스크립트로 배포를 하지도 않지만, 서버 ( = 하드웨어 ) 만 가지고도 가능한 가장 간단한 방법이기 때문에 한 번 적용해봤으면 해서 공유합니다.

kakasoo commented 1 year ago

배포 단계까지 가게 되면 직접 적용해보고, 그 다음에는 github action으로 다시 옮겨봅시다.

kakasoo commented 1 year ago
rimo030 commented 1 year ago

Node.js의 Cluster Module과, 로드 밸런싱

  1. Node.js는 기본적으로 싱글스레드이기 때문에 코어를 전부 활용할 수 없다.
  2. 이를 보완하기 위해 Cluster Module을 지원.
  3. 그러나 기대와 달리 대부분의 경우, 각 클러스터의 프로세스의 부하는 균등하지 못하다.
  4. 운영체제의 판단하에 작동하는 것이지만, 트래픽 요청이 증가할 경우 메모리 누수 가능성이 높아짐.
  5. 이렇듯 쏟아지는 트래픽을 분산시켜 최적의 퍼포먼스를 뽑내도록 하는 것이 로드밸런싱.

제가 잘 이해했는지 모르겠지만... 읽어보니 왜 CI/CD 서비스들이 등장했는지 알 것 같습니다. 현대의 분산처리 시스템에서 배포는 정말 복잡한 문제군요.

kakasoo commented 1 year ago

이해좋네요. 근데 로드밸런싱도 "너 더 견딜 수 있어?" 라고 계속 서버에게 헬스체크하다보니 운영체제처럼 맡기는 것보다 부하가 있긴 합니다. 장단이 명확해요.

kakasoo commented 10 months ago

EC2 인스턴스의 생성

저는 노출되더라도 바로 삭제할 거니까 상관없지만, 절대 이 아이피나 서버의 정보를 드러내지마세요. 이 스레드는 인스턴스의 생성, 탄력적 IP의 등록까지 설명합니다.

image

인스턴스를 생성합니다.

image

인스턴스가 생성되었고, 아이피가 할당된 것을 볼 수 있습니다. 하지만 이것은 탄력적 아이피가 아니기 때문에 서버를 껐다 켤 때마다, 또는 서버가 문제가 발생하여 강제 재부팅될 때마다 아이피가 바뀝니다. 그러면 환경 변수를 연동해서 써놓을 수 없기 때문에 문제가 되겠죠. 그래서 고정 아이피를 할당할 것이고 이걸 탄력적 아이피라고 합니다.

kakasoo commented 10 months ago

EIP : 탄력적 아이피

image

EC2 대시보드 사이드 탭에서 [네트워크 보안] - [탄력적 IP]로 가 탄력적 IP를 생성합니다. 생성 버튼을 누르고 나서 별로 클릭할 것은 없습니다.

image

방금 만든 EC2 인스턴스에 고정된 아이피를 할당하기 위해 일단 작업 탭에서 [탄력적 IP 주소 연결]을 클릭합니다.

image

주소를 할당합니다. 재연결은 아이피 주소를 다른 인스턴스에게 뺏겨도 되냐는 질문이므로 체크를 해제합니다.

image

할당 하고 다시 대시보드로 이동하면 아이피 주소에 EIP의 아이피가 기록되어 있는 게 보일겁니다.

kakasoo commented 10 months ago

인스턴스 접속

저는 키를 Desktop에 두고, 퍼블릭 아이피로 접속을 했습니다. 명령어는 아래와 같습니다.

> $ ssh -i /Users/kakasoo/Desktop/testKey.pem ec2-user@3.37.199.24
The authenticity of host '3.37.199.24 (3.37.199.24)' can't be established.
ED25519 key fingerprint is SHA256:ovF2U9tTO7TnYgByIge2nxvMIPdE0O3337Npa2+vAlk.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '3.37.199.24' (ED25519) to the list of known hosts.

위와 같은 문장이 나와 yes를 입력했습니다만,

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@         WARNING: UNPROTECTED PRIVATE KEY FILE!          @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for '/Users/kakasoo/Desktop/testKey.pem' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "/Users/kakasoo/Desktop/testKey.pem": bad permissions
ec2-user@3.37.199.24: Permission denied (publickey,gssapi-keyex,gssapi-with-mic).

키가 너무 개방되어 있기 때문에 숨기라는 말이 나오네요. 이거는 파일 접근 권한을 수정하라는 말입니다.

$ chmod 400 Desktop/testKey.pem # 읽기만 가능하게 파일을 수정합니다.
$ ssh -i /Users/kakasoo/Desktop/testKey.pem ec2-user@3.37.199.24 # 재접속

   ,     #_
   ~\_  ####_        Amazon Linux 2023
  ~~  \_#####\
  ~~     \###|
  ~~       \#/ ___   https://aws.amazon.com/linux/amazon-linux-2023
   ~~       V~' '->
    ~~~         /
      ~~._.   _/
         _/ _/
       _/m/'
[ec2-user@ip-172-31-36-135 ~]$

성공해서 ec2 인스턴스에 접속했습니다.

kakasoo commented 10 months ago

git으로 서버 폴더 clone

여기서부터는 EC2 인스턴스 내부입니다.

[ec2-user@ip-172-31-36-135 ~]$ sudo yum update -y
[ec2-user@ip-172-31-36-135 ~]$ sudo yum install git -y

먼저 linux update를 진행하고 git을 설치합니다.

[ec2-user@ip-172-31-36-135]$ mkdir github # github 폴더 생성
[ec2-user@ip-172-31-36-135]$ cd github # github 폴더로 이동
[ec2-user@ip-172-31-36-135 github]$ git clone https://github.com/rimo030/nestjs-e-commerce-frame # 이 레포를 clone합니다.
kakasoo commented 10 months ago

EC2 인스턴스 내부에서 Node.js 설치

$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
$ . ~/.nvm/nvm.sh 
$ nvm install --lts
$ node -e "console.log('Running Node.js ' + process.version)"

설치 후 아래와 같은 메시지들이 나옵니다.

[ec2-user@ip-172-31-36-135 nestjs-e-commerce-frame]$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
. ~/.nvm/nvm.sh
nvm install --lts
node -e "console.log('Running Node.js ' + process.version)"
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 15916  100 15916    0     0  53042      0 --:--:-- --:--:-- --:--:-- 53230
=> Downloading nvm from git to '/home/ec2-user/.nvm'
=> Cloning into '/home/ec2-user/.nvm'...
remote: Enumerating objects: 365, done.
remote: Counting objects: 100% (365/365), done.
remote: Compressing objects: 100% (314/314), done.
remote: Total 365 (delta 43), reused 159 (delta 25), pack-reused 0
Receiving objects: 100% (365/365), 364.78 KiB | 13.51 MiB/s, done.
Resolving deltas: 100% (43/43), done.
* (HEAD detached at FETCH_HEAD)
  master
=> Compressing and cleaning up git repository

=> Appending nvm source string to /home/ec2-user/.bashrc
=> Appending bash_completion source string to /home/ec2-user/.bashrc
=> Close and reopen your terminal to start using nvm or run the following to use it now:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion
Installing latest LTS version.
Downloading and installing node v20.10.0...
Downloading https://nodejs.org/dist/v20.10.0/node-v20.10.0-linux-x64.tar.xz...
################################################################################################### 100.0%
Computing checksum with sha256sum
Checksums matched!
Now using node v20.10.0 (npm v10.2.3)
Creating default alias: default -> lts/* (-> v20.10.0)
Running Node.js v20.10.0
kakasoo commented 10 months ago

서버 실행

npm i # 해당 레포의 패키지들을 설치합니다.
npm run start:dev # 서버를 실행합니다.

브라우저에 가서 3.37.199.24:3000 를 실행해봅니다. app.controller.ts 에 ROOT 경로에 대한 API가 있다면 hello 라는 문장이 나올 겁니다. 만약 없더라도, 어쨌든 API 응답이 없다는 것이니 404 라도 나오기는 하겠죠? 퍼블릭한 IP로 접속이 가능한지 체크합니다.

kakasoo commented 10 months ago

EC2 인스턴스 보안 그룹 설정

깜빡하고 보안그룹 설정을 안했네요.

image

보안그룹 설정으로 들어갑니다.

image

TEST라는 이름으로 보안그룹을 만듭니다. 저는 3천번 포트를 곧장 열어버리지만, EC2를 사용한다면 내부에 nginx 와 같이 웹서버를 두고 80번 포트를 열어버릴 수 있습니다. 이게 더 안전할 겁니다. 다만 여기서는 빠르게 하기 위해 바로 3000번 포트를 쓰도록 할게요.

image

3000번 포트를 열어놓은 보안 그룹을 연결했기 때문에 바깥쪽에서 접근이 가능해졌습니다.

kakasoo commented 10 months ago

swap memory 설정

여기까지 하고 난 다음 서버를 실행시키고 나니 저는 EC2 서버가 죽어 있었습니다. 보통 이런 경우에는 프리티어의 메모리 부족으로 인한 것일 확률이 큽니다. EC2에서 무료로 제공하는 메모리를 늘려줄 수는 없으니, 디스크 공간을 메모리로 활용할 수 있게 스왑 메모리를 설정할 겁니다.

echo "start!"
sudo dd if=/dev/zero of=/mnt/swapfile bs=1M count=4096
echo "메모리할당 성공"
sudo chmod 600 /mnt/swapfile
echo "스왑 파일 권한 변경 성공"
sudo mkswap /mnt/swapfile
echo "스왑 영역 설정"
sudo swapon /mnt/swapfile
echo "스왑 영역 사용 추가"
sudo swapon -s
echo "스왑 영역 사용 체크"

혹시나 해제할 일도 생기면 아래 스크립트를 사용해주세요.

sudo swapoff -v /mnt/swapfile
# echo "스왑 메모리 해제"
sudo rm /mnt/swapfile
# echo "해제 완료"

빌드 과정에서 서버가 죽는 것이기 때문에, 프리티어를 사용할 때 저는 빌드 레포지토리를 따로 만들기도 했습니다. 서버를 빌드한 dist 폴더만 따로 업로드해서, EC2는 빌드 없이 서버를 실행할 수 있게 하곤 했습니다.

kakasoo commented 10 months ago

docker 설치

$ sudo yum install docker -y

docker compose 설치

$ sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose

docker 실행

$ sudo systemctl start docker # 방금 설치한 도커를 실행합니다.
$ sudo docker-compose up -d # 도커 컴포즈 파일을 이용해 DB를 생성합니다.

Docker 내 MySQL에 접속해서 데이터베이스 생성

우리는 데이터베이스에 자동 실행되는 스크립트를 넣진 않았죠.

$ sudo docker ps;
$ sudo docker exec -it CommerceDB bash;
$ mysql -u root -p;
$ Password: # 패스워드 입력 후 실행

$ mysql> create database commerce;

생성을 마저 해줍시다.

$ npm run schema:sync:local # package.json에 명시된 대로 스키마 생성!
kakasoo commented 10 months ago

서버 접속 확인

ec2 내부에서 서버를 실행해봅시다.

$ npm run build # 빌드
$ npm run start:prod # dist 파일의 main.js를 실행합니다.

앞으로 프로덕션에서는 ts 파일이 아닌, main.js 파일을 실행함으로써 서버를 열 겁니다. 여기는 더 이상 개발이 되는 환경이 아니기 때문이죠.

image

404긴 하지만 서버 접속이 되는 걸 확인했습니다!

kakasoo commented 10 months ago

github action 없이 서버 자동배포하기

SHELL=/bin/bash
PATH=/root/.nvm/versions/node/v14.15.5/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin

* * * * * /home/github/repo-name/init.sh

/home/github/repo-name/init.sh 파일의 정체

cd /home/github/repo-name # 이게 없으면 제대로 실행될 수가 없습니다! ( 절대 경로로 넣으셔야 합니다. )

git fetch

local=$(git rev-parse HEAD)
echo $local

target=$(git rev-parse origin/main)
echo $target

if [ $local != $target ]
then
        git stash # 혹시라도, 다른 파일들이 추가되었을까봐 날리는 로직을 추가
        git pull origin main
        echo '풀 완료'
        npm install # 새로운 패키지가 추가되어 있을지 모르므로 install을 다시 합니다.
        echo "npm install 완료"
        npm run build # 빌드를 하여 dist/main.js 파일을 생성합니다.
        echo "빌드 완료"
        pm2 reload app # app 이라는 이름으로 pm2 서버가 실행되어 있다고 가정하고, app을 다시 실행합니다.
fi

이 파일을 1분마다 실행하면, 서버는 자동으로 재배포됩니다. 추후에는 github action을 사용한 방식을 배워봅시다.

kakasoo commented 10 months ago

pm2에 대해서도 학습합시다.

이 방식에서는 Pm2를 쓰는데 Pm은 프로세스 매니저라는 뜻입니다. 서버를 자동으로 재실행해서, 이전 서버를 갈아끼우는 데에 도움을 주는데요, reload는 서버를 순차적으로 재실행하는 것이고 restart는 동시에 껐다 키라는 명령어입니다. 여기서는 서버가 1대고, 내부에 코어가 1개라 내부 서버도 1개만 실행되고 있어서 사실 reload, restart 구분이 무의미합니다.

추후에 pm2를 사용할 것은 아니기 때문에 개념만 이해해둡시다.