부스트캠프 멤버십 마지막 과정인 그룹 프로젝트를 진행할 때 백엔드와 데브옵스 역할을 맡아 진행했는데 그 때, 구축했던 CI/CD를 정리해볼까 합니다.
저희 팀은 6주 프로젝트로 끝나는 것이 아닌 실제 서비스 운영까지 해보자 라는 생각으로 모이게 되어서 자동 배포가 매우 중요한 이슈로 다가왔는데요 😥
프로젝트를 하는데 전날 올린 PR을 매일 아침 merge하고 서버에 올리는 이 과정을 수동으로 제어하려면 매일 반복적인 작업을 하는 고된 일이 될 것 같았습니다.
우리 팀은 지속적으로 개발 가능한 프로젝트를 만들자는 목표를 가지고 임했기 때문에 이런 반복적인 작업에 할당되는 리소스를 줄일 필요가 있었습니다.
젠킨스, 트래비스, 등 여러 가지가 존재하지만 6주간에 빠르게 개발하기 위해서는 가장 익숙한 프로그램을 써서 빠르게 환경을 구축하는게게 현명하다는 생각이었고 부스트캠프를 진행하면서 사용해 본적이 있는 Github Actions를 이용하기로 결정!
또한 우리 팀은 로컬 서버, 개발(테스트) 서버, 운영 서버 총 3개의 서버를 사용하는데 로컬 서버는 그렇다치고 개발 서버와 운영 서버 이 2개의 서버에 똑같은 환경을 구축할 필요성이 있었습니다.
왜냐?? 노드 버전이나 운영체제 등이 달라서 발생하는 문제는 골치아플 것 같았거든요..😥
그런데 개발자가 직접 설치하는 방식으로는 완벽하게 같게 설정하기는 쉽지 않아 보였습니다. 그래서 가장 편리한 해결 방법이고 또 요즘 개발자라면 알아야할 Docker를 사용해서 해결해보고자 했습니다!
지금은 서버 비용을 부스트캠프에서 지원해주는 네이버 클라우드 크래딧으로 해결하고 있지만 지원이 끊긴다면.. 서버를 이전해야할 텐데 그 때도 이 Docker가 큰 힘을 발휘해줄 것 같았습니다.
써놓고 보니까 초라한 스펙이지만.. 저 2개 만으로도 우리 프로젝트에선 훌륭한 CI/CD를 구축할 수 있었습니다😎
흐름을 설명하기에 앞서서 Docker를 어떤 방식으로 사용하는지 부터 알아봅시다.
밑에 사진과 같이 우리 프로젝트는 기본적으로 React, NestJS, Nginx, MySQL 총 4개의 컨테이너를 띄워서 돌아갑니다.
FROM nginx
RUN apt update && apt install -y net-tools
COPY ./default.conf /etc/nginx/conf.d/default.conf
upstream front {
server front:3000;
}
upstream back {
server back:5001;
}
server {
listen 80;
return 301 https://justus.kr$request_uri;
}
server {
listen 443 ssl;
server_name justus.kr;
client_max_body_size 0;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
location / {
proxy_pass http://front;
}
location /api {
proxy_pass http://back;
}
location /sockjs-node {
proxy_pass http://front;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header connection "Upgrade";
}
ssl_certificate /etc/letsencrypt/live/justus.kr/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/justus.kr/privkey.pem;
ssl on;
ssl_prefer_server_ciphers on;
}
Docker Compose에 설정한 Services 이름을 통해 proxy가 가능합니다.
FROM node:16.0.0-alpine as builder
WORKDIR /usr/src/app
COPY ./package.json ./
RUN yarn
COPY . .
ARG REACT_APP_NCP_CLOUD_ID
ARG REACT_APP_SERVER_URL
ENV REACT_APP_NCP_CLOUD_ID=$REACT_APP_NCP_CLOUD_ID
ENV REACT_APP_SERVER_URL=$REACT_APP_SERVER_URL
RUN yarn build
FROM nginx
EXPOSE 3000
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /usr/src/app/build /usr/share/nginx/html
server {
listen 3000;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}
FROM node:alpine
WORKDIR /usr/src/app
COPY ./package.json ./
RUN yarn
COPY . .
CMD ["yarn", "start"]
[client]
default-character-set = utf8mb4
[mysql]
default-character-set = utf8mb4
[mysqld]
skip-character-set-client-handshake
init_connect = SET collation_connection = utf8mb4_general_ci
init_connect = SET NAMES utf8mb4
character-set-server = utf8mb4
collation-server = utf8mb4_general_ci
version: "3"
volumes:
mysql_data: {}
services:
front:
image: soosungp33/dev_react:latest
stdin_open: true
tty: true
nginx:
image: soosungp33/prod_nginx:latest
volumes:
- ../etc/letsencrypt:/etc/letsencrypt
restart: always
ports:
- "80:80"
- "443:443"
back:
image: soosungp33/dev_node:latest
environment:
(사용하는 환경 변수 설정)
mysql:
image: mysql:5.7
restart: unless-stopped
ports:
- "3306:3306"
volumes:
- ./mysql/conf.d:/etc/mysql/conf.d
- mysql_data:/var/lib/mysql
- ./mysql/sqls/:/docker-entrypoint-initdb.d/
environment:
MYSQL_ROOT_PASSWORD: (DB 비밀번호)
MYSQL_DATABASE: (DB 비밀번호)
name: dev
on:
push:
branches: [develop]
env:
DOCKER_IMAGE_NAME: soosungp33
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Node Build and push
uses: docker/build-push-action@v2
with:
context: ./backend
push: true
tags: ${{ env.DOCKER_IMAGE_NAME }}/dev_node:latest
- name: React Build and push
uses: docker/build-push-action@v2
with:
context: ./frontend
push: true
build-args: REACT_APP_NCP_CLOUD_ID=${{ secrets.REACT_APP_NCP_CLOUD_ID }}
tags: ${{ env.DOCKER_IMAGE_NAME }}/dev_react:latest
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: ssh connect & production
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.NCLOUD_DEV_HOST_IP }}
username: ${{ secrets.NCLOUD_DEV_USERNAME }}
password: ${{ secrets.NCLOUD_DEV_PASSWORD }}
port: ${{ secrets.NCLOUD_DEV_PORT }}
script: |
cd ~
docker-compose pull
docker-compose up --force-recreate --build -d
docker rmi $(docker images -f "dangling=true" -q)
전체적인 흐름을 그림으로 정리해보자면 Develop 브랜치에 변경사항이 생길 때 Github Action이 각 애플리케이션 마다 설정해둔 Dockerfile을 읽어서 Docker Image를 만들고 Docker Hub에 push하게 됩니다.
build과정을 성공적으로 완료하게 되면 Github Action이 서버 환경에서 설정해둔 스크립트를 실행하게 되는데 Docker Hub에 올려놓은 이미지들을 pull 받고 Docker Compose로 총 4개의 컨테이너를 띄워서 배포까지 자동으로 완료하게 되는 흐름을 가지고 있습니다.