[Infra] NestJS & Github Action 빌드 개선하기 (2 : CD build - cd.yml 파일 개선)
목차
- 목적
- CD 빌드 살펴보기
- CD 빌드에서 불필요한 점 찾기
Dockerfile
실행 시간 최적화
a.Dockerfile
살펴보기
b. 불필요한 부분 제거 및 변경AWS EC2
배포
a.cd.yml
파일의AWS EC2
단계 살펴보기
b. 불필요한 부분 제거 및 변경
- 결론
- RECOMMEND FOR YOU & REFERENCE
목적
저번 포스팅에서 CI 배포 시간을 약 15% 이상 줄였다.
성능이 상당히 향상되었지만, 여전히 이를 실제로 배포하는 CD 빌드의 속도는 확연히 느리다.
CD 빌드 성능을 개선해보자.
CD 빌드 살펴보기
우선 cd.yml
파일이 어떤 구조를 갖고 있는지 알아보자.
name: CD
on:
push:
branches:
- prod
jobs:
cd-build:
runs-on: ubuntu-20.04
steps:
- name: checkout
uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 20
- name: install package manager - pnpm
run: npm install -g pnpm
- name: env set
working-directory: ./
run: |
touch .env
echo POSTGRESQL_DB_URL=${{secrets.POSTGRESQL_DB_URL}} >> .env
echo JWT_PRIVATE=${{secrets.JWT_PRIVATE}} >> .env
echo JWT_SECRET=${{secrets.JWT_SECRET}} >> .env
echo PORT=${{secrets.PORT}} >> .env
echo SALT=${{secrets.SALT}} >> .env
echo ORIGIN=${{ secrets.ORIGIN }} >> .env
echo AWS_USER=${{ secrets.PROD_AWS_USER }} >> .env
echo AWS_HOST=${{ secrets.PROD_AWS_HOST }} >> .env
- name: Install package
run: pnpm i --no-frozen-lockfile
- name: prisma set
run: npx prisma generate
- name: build
run: |
pnpm build
cp -r src/prisma/client dist/prisma/client
cp package.prod.json dist/package.json
cp ecosystem.config.js dist/ecosystem.config.js
- name: docker
run: docker build ./dist -t ${{ secrets.DOCKERHUB_USERNAME }}/dauth:latest -f ./Dockerfile
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: push to Docker Hub
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/dauth:latest
- name: Get GitHub Actions IP
id: ip
uses: haythem/public-ip@v1.2
- name: configuring
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Add Github Actions IP to Security group
env:
AWS_DEFAULT_REGION: ap-northeast-2
run: |
aws ec2 authorize-security-group-ingress \
--group-id ${{ secrets.AWS_EC2_SG_ID }} \
--protocol tcp --port 22 \
--cidr ${{ steps.ip.outputs.ipv4 }}/32 \
- name: EC2 Production Server Deploy
uses: appleboy/ssh-action@master
with:
key: ${{ secrets.PROD_AWS_SSH_KEY }}
host: ${{ secrets.PROD_AWS_HOST }}
port: ${{ secrets.AWS_PORT }}
username: ubuntu
script: |
groupadd docker
sudo usermod -aG docker $USER
newgrp docker
echo "${{ secrets.DOCKERHUB_PASSWORD }}" >> ~/my_password.txt
cat ~/my_password.txt | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/dauth:latest
touch .env
echo POSTGRESQL_DB_URL=${{secrets.POSTGRESQL_DB_URL}} >> .env
echo JWT_PRIVATE=${{secrets.JWT_PRIVATE}} >> .env
echo JWT_SECRET=${{secrets.JWT_SECRET}} >> .env
echo PORT=${{secrets.PORT}} >> .env
echo SALT=${{secrets.SALT}} >> .env
echo ORIGIN=${{ secrets.ORIGIN }} >> .env
echo AWS_USER=${{ secrets.PROD_AWS_USER }} >> .env
echo AWS_HOST=${{ secrets.PROD_AWS_HOST }} >> .env
docker run -d -p 8080:8080 --env-file .env iixanx/dauth:latest
y | docker rm $(docker ps -a -f status=exited -q)
y | docker image prune
- name: Remove Github Actions IP From Security Group
run: |
aws ec2 revoke-security-group-ingress \
--group-id ${{ secrets.AWS_EC2_SG_ID }} \
--protocol tcp --port 22 \
--cidr ${{ steps.ip.outputs.ipv4 }}/32
이 yml
파일의 작동 방식은 다음과 같다.
우선 Node.js
를 세팅해주고, pnpm
을 설치한다. 환경변수를 설정하고 pnpm
으로 다른 패키지도 같이 설치해준 이후 PrismaORM
을 세팅한다.
그 후 빌드해서 ts
파일을 js
로 컴파일해주고, prisma client
파일과 package.json
파일, ecosystem
파일을 복사해서 dist
폴더로 옮긴다.
다음으로, Dockerfile
을 실행 및 빌드하고 도커허브로 push한 뒤, AWS EC2
인바운드 보안 그룹에 GH Actions
의 IP를 추가해 접근 권한을 만든다.
마지막으로 EC2
에 새 도커 이미지를 pull 받아 컨테이너를 실행시키며, 작업이 끝나면 AWS EC2
보안 그룹에서 GH Actions
의 IP 주소를 제거한다.
CD Build에서 불필요한 점 찾기
매우 길다...
그리고 문제점이 한 눈에 보인다.
- .env 파일에 두 번씩이나 값을 넣고 있다... 메모리 낭비같아 보인다.
- 아직 패키지 캐싱을 하지 않아 매 push마다 새로이 패키지를 다운로드받고 있다.
- 아직 PrismaORM 명령어도 간소화하지 않아 매 번 DB에 새로 push 및 migration을 진행하고 있다.
또한, CD 빌드 실행 시의 시간 비율을 확인해보면 추가로 문제점을 찾을 수 있다.
가장 최근에 실행시킨 CD 빌드를 기준으로 보자.
Dockerfile
실행 시간이 너무 길다. 무려 전체 실행 시간의 27% 이상을 차지하고 있었다. (50s)Docker Hub
에 push하는 시간도 상당히 길다. 전체 실행 시간의 19% 이상을 차지한다. (35s)AWS EC2
에 실제로 액세스해 배포하는 시간이 꽤나 걸렸다. 전체 실행 시간 중 25% 이상을 차지한다. (47s)
3개 스텝만 줄여도 실행 시간이 절반 이하로 줄어들 것으로 추측할 수 있다.
우선 Dockerfile
은 직접 파일을 수정해서 실행 시간을 줄여야 할 것으로 추측한다. 다만 단계를 줄이면 레이어가 줄어들기 때문에, 이를 통해 시간을 단축할 수 있을 것으로 예상한다.
또한 Docker Hub push
는 Dockerfile
의 레이어를 줄인다면 push할 용량 역시 줄어 자연스레 시간이 줄 것으로 추측할 수 있다.AWS EC2
에 액세스해 배포하는 단계는 cd.yml
파일의 해당 단계를 더욱 최적화해야 할 것이다.
Dockerfile
실행 시간 최적화
Dockerfile
살펴보기
Dockerfile
은 어떻게 생겼을까?
FROM node:20-alpine AS base
FROM base AS set
ENV NODE_VERSION 20.12.2
RUN mkdir -p /app
WORKDIR /app
RUN apk update
RUN apk add npm
RUN apk add tree
RUN npm i -g pm2
RUN npm i -g pnpm
FROM set AS build
COPY . .
RUN export NODE_ENV=prod
RUN pnpm install
RUN pnpm prisma generate
RUN cd ..
EXPOSE 8080
CMD [ "pnpm", "node:prod" ]
일반 Node 이미지보다 더 가벼운 alpine
이미지를 20버전으로 가져와서 사용하며, 해당 단계는 base
라는 명칭으로 정의한다.
base
단계에서 가져와서 set
단계를 정의한다.NODE_VERSION
환경변수를 20.12.2로 정의하고 서버를 실행할 디렉토리를 mkdir app
으로 만들고, workdir
로 지정한다.
이후 alpine
에서 사용하는 설치 툴인 apk
를 활용해 npm
, tree
를 설치하고, npm
으로 pm2
와 pnpm
을 설치한다.
set
단계에서 build
단계를 정의하고, COPY
명령어를 통해 폴더 내부의 것들을 복사해온다.
이후 export
를 통해 환경변수를 정의하고, pnpm
을 이용해 패키지를 설치하고 PrismaORM
을 설정한다.
현재 디렉토리를 상위 디렉토리로 변경하고 EXPOSE
를 통해 포트를 정의한다.
마지막으로 CMD
를 활용해 pnpm node:prod
명령어를 실행한다.
불필요한 부분 제거 및 변경
우선 가장 문제인 부분은 cd.yml
파일의 cd 빌드에서 실행한 부분을 여기에서도 실행하고 있다는 점이다.
중복된 실행은 실행 시간을 증가시킬 뿐더러 패키지를 중복으로 설치하므로 메모리 낭비가 발생할 수 있다.
따라서 cd.yml
파일에서 설치했던 pnpm
, 과거 무중단 배포를 위해 설치했지만 현재는 사용하지 않는 pm2
나 구조 확인을 위해 설치했던 tree
도 설치 명령어를 삭제해준다.
또한 이미 실행했던 명령어인 pnpm install
이나 pnpm prisma generate
도 같이 삭제해준다.
그리고 마지막 줄의 CMD ["pnpm", "node:prod"]
도 변경할 것이다.
현재 package.prod.json
에 정의해둔 명령어에서 node:prod
는 ts 파일을 통째로 실행하기 때문에 굳이 ts 파일들을 js 파일로 컴파일한 이유가 없고, 실행 속도에 유의미한 차이가 있을 것으로 예상한다.
따라서 이를 node main.js
로 변경한다.
결과적으로 Dockerfile
은 다음과 같은 형태를 띄게 되었다.
FROM node:20-alpine AS base
FROM base AS set
ENV NODE_VERSION 20.12.2
RUN mkdir -p /app
WORKDIR /app
RUN apk update
FROM set AS build
COPY . .
RUN export NODE_ENV=prod
RUN cd ..
EXPOSE 8080
CMD [ "node", "main.js" ]
수정 이후 배포를 시도해보았을 때, 기존에 50s 가량 걸리던 단계가 6s로 무려 88% 가량 감소했다!!
또한 도커허브로 push하던 단계는 35s에서 9s로 74% 가량 감소했다.
전체 70s 정도를 Dockerfile 최적화로 줄인 것이다.
AWS EC2
배포
cd.yml
파일의 AWS EC2
단계 살펴보기
cd 빌드를 실행하는 yml 파일은 어떤 단계를 거칠까?
- name: EC2 Production Server Deploy
uses: appleboy/ssh-action@master
with:
key: ${{ secrets.PROD_AWS_SSH_KEY }}
host: ${{ secrets.PROD_AWS_HOST }}
port: ${{ secrets.AWS_PORT }}
username: ubuntu
script: |
groupadd docker
sudo usermod -aG docker $USER
newgrp docker
echo "${{ secrets.DOCKERHUB_PASSWORD }}" >> ~/my_password.txt
cat ~/my_password.txt | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/dauth:latest
touch .env
echo POSTGRESQL_DB_URL=${{secrets.POSTGRESQL_DB_URL}} >> .env
echo JWT_PRIVATE=${{secrets.JWT_PRIVATE}} >> .env
echo JWT_SECRET=${{secrets.JWT_SECRET}} >> .env
echo PORT=${{secrets.PORT}} >> .env
echo SALT=${{secrets.SALT}} >> .env
echo ORIGIN=${{ secrets.ORIGIN }} >> .env
echo AWS_USER=${{ secrets.PROD_AWS_USER }} >> .env
echo AWS_HOST=${{ secrets.PROD_AWS_HOST }} >> .env
docker run -d -p 8080:8080 --env-file .env iixanx/dauth:latest
y | docker rm $(docker ps -a -f status=exited -q)
y | docker image prune
해당 단계가 실행되는 과정은 다음과 같다.
도커에 권한을 우선적으로 추가해준다.
이후 DockerHub
비밀번호를 txt 파일에 저장해주고, --password-stdin
옵션을 활용해 입력해준다.
새로이 push되었던 도커 이미지를 pull받고 .env
파일을 생성해 환경변수들을 저장해준다.
그 다음으로는 docker run
명령어를 통해 도커파일을 실행시키며, 이전의 이미지와 그를 바탕으로 실행되던 컨테이너를 삭제해준다.
불필요한 부분 제거 및 변경
파일 I/O에는 생각보다 많은 시간이 걸린다.
이에 대한 원인은 다양하게 확인할 수 있는데, 하드웨어 문제나 운영체제의 오버헤드 등으로 인한 것이 가장 크다.
이러한 환경을 고려했을 때, .env
파일을 생성 및 작성하는 데에는 상당한 자원과 시간이 소모될 것으로 추측한다.
즉 중복된 환경변수 파일 생성 단계를 삭제하기만 해도 훨씬 시간 소요가 줄어들 수 있음을 의미한다.
해당 부분을 삭제해주고 다시 CD 빌드를 작동시켜보자.
빌드를 작동시키고 결과를 확인하였을 때, 47초 가량 걸리던 EC2 배포가 13초 가량으로 줄었다.
파일 I/O를 하지 않는 것 만으로도 실행 시간 중 34초가 줄어들었다.
결론
결론적으로 전체 배포 시간이 70s 정도로 줄어들었다. 기존 (183s) 대비 61% 가량을 감소시킨 것이다.
비효율적인 코드가 그동안 생산성을 얼마나 저하시켰는지 알 수 있다.
앞으로도 코드 작성에서만 끝낼 것이 아닌, 효율적인 리소스 사용 및 성능 개선을 위한 리팩토링을 지속적으로 진행하며 성능 최적화를 고려하며 코드를 작성하고 싶다.
RECOMMEND FOR YOU
[Infra] NestJS & Github Action 빌드 개선하기 (1 : CI build - ci.yml 개선) : https://iio-nff.tistory.com/4