CI/CD 구축기

김영한·2021년 12월 5일
5
post-thumbnail

깃허브
배포주소


부스트캠프 멤버십 마지막 과정인 그룹 프로젝트를 진행할 때 백엔드와 데브옵스 역할을 맡아 진행했는데 그 때, 구축했던 CI/CD를 정리해볼까 합니다.

저희 팀은 6주 프로젝트로 끝나는 것이 아닌 실제 서비스 운영까지 해보자 라는 생각으로 모이게 되어서 자동 배포가 매우 중요한 이슈로 다가왔는데요 😥
프로젝트를 하는데 전날 올린 PR을 매일 아침 merge하고 서버에 올리는 이 과정을 수동으로 제어하려면 매일 반복적인 작업을 하는 고된 일이 될 것 같았습니다.

우리 팀은 지속적으로 개발 가능한 프로젝트를 만들자는 목표를 가지고 임했기 때문에 이런 반복적인 작업에 할당되는 리소스를 줄일 필요가 있었습니다.

어떤 프로그램을 사용했을까??

CI 프로그램....?

젠킨스, 트래비스, 등 여러 가지가 존재하지만 6주간에 빠르게 개발하기 위해서는 가장 익숙한 프로그램을 써서 빠르게 환경을 구축하는게게 현명하다는 생각이었고 부스트캠프를 진행하면서 사용해 본적이 있는 Github Actions를 이용하기로 결정!

또한 우리 팀은 로컬 서버, 개발(테스트) 서버, 운영 서버 총 3개의 서버를 사용하는데 로컬 서버는 그렇다치고 개발 서버와 운영 서버 이 2개의 서버에 똑같은 환경을 구축할 필요성이 있었습니다.

왜냐?? 노드 버전이나 운영체제 등이 달라서 발생하는 문제는 골치아플 것 같았거든요..😥

그런데 개발자가 직접 설치하는 방식으로는 완벽하게 같게 설정하기는 쉽지 않아 보였습니다. 그래서 가장 편리한 해결 방법이고 또 요즘 개발자라면 알아야할 Docker를 사용해서 해결해보고자 했습니다!

지금은 서버 비용을 부스트캠프에서 지원해주는 네이버 클라우드 크래딧으로 해결하고 있지만 지원이 끊긴다면.. 서버를 이전해야할 텐데 그 때도 이 Docker가 큰 힘을 발휘해줄 것 같았습니다.

  • 스펙
    • GitHub Actions
    • Docker

써놓고 보니까 초라한 스펙이지만.. 저 2개 만으로도 우리 프로젝트에선 훌륭한 CI/CD를 구축할 수 있었습니다😎

그래서 어떤 흐름으로 자동 배포가 되는 건데??

흐름을 설명하기에 앞서서 Docker를 어떤 방식으로 사용하는지 부터 알아봅시다.

밑에 사진과 같이 우리 프로젝트는 기본적으로 React, NestJS, Nginx, MySQL 총 4개의 컨테이너를 띄워서 돌아갑니다.

각 애플리케이션 Dockerfile

  • Nginx
    • FrontEnd와 BackEnd 사이에서 Proxy 역할을 해줍니다.
    • Dockerfile, Default.conf
	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가 가능합니다.

  • React
    • React 컨테이너는 앞단에 nginx를 같이 두는데 build 파일을 Serving 해주는 용도로 사용됩니다.
    • Dockerfile, default.conf
        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;
            }
        }
  • NestJS
    • Dockerfile
        FROM node:alpine
        WORKDIR /usr/src/app
        COPY ./package.json ./
        RUN yarn
        COPY . .
        CMD ["yarn", "start"]
  • MySQL
    • Mysql은 Docker에서 제공해주는 Image를 사용했고 conf만 설정해주었습니다.
    • my.cnf
        [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

Docker Compose

  • Docker Compose를 이용해 4개의 이미지를 같은 네트워크에 띄워주었습니다.
    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 비밀번호)
  • 밑에서 설명하겠지만 Dockerfile을 직접 사용하는 것이 아닌 Docker Hub에 올린 이미지를 pull 받아서 사용하는 방식입니다.

Github Actions로 Docker 이미지 만들고 Docker Hub에 push하기

  • .github/workflows 폴더 안에 수행할 yml 파일을 설정해줍니다.
    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)
  • on : workflow를 트리거하는 이벤트 이름입니다.
    • 위의 코드는 develop 브랜치에 push나 PR이 트리거되면 동작한다는 의미
    • types라는 조건을 붙여서 조건에 속하는 경우에만 workflow가 동작
  • job : workflow 실행은 하나 이상의 job으로 구성되고 기본적으로 병렬로 실행됩니다.
    • build, deploy 등으로 커스텀하여 사용
  • Steps : 가장 중요한 부분으로 command 명령을 실행할 수 있고 다른사람들이 만들어놓은 오픈 소스를 가져와 사용할 수 있습니다.
    • name은 임의로 자기가 설정
    • uses를 통해 다른 사람이 만들어 놓은 것을 가져와 사용
    • action/checkout@v2 : 현재 상태의 소스코드를 가상의 컨테이너 안으로 checkout해주는 역할
    • 만들어둔 React와 NestJS의 Dockerfile을 이용해 빌드하고 만들어진 이미지를 DockerHub에 push합니다.
    • 이후 우리 서버에 설정해놓은 Docker Compose 파일을 스크립트로 실행시켜 배포를 완료하게 됩니다.

흐름 정리

전체적인 흐름을 그림으로 정리해보자면 Develop 브랜치에 변경사항이 생길 때 Github Action이 각 애플리케이션 마다 설정해둔 Dockerfile을 읽어서 Docker Image를 만들고 Docker Hub에 push하게 됩니다.

build과정을 성공적으로 완료하게 되면 Github Action이 서버 환경에서 설정해둔 스크립트를 실행하게 되는데 Docker Hub에 올려놓은 이미지들을 pull 받고 Docker Compose로 총 4개의 컨테이너를 띄워서 배포까지 자동으로 완료하게 되는 흐름을 가지고 있습니다.

이렇게 해서 우리팀은 수동 배포에 신경쓰지 않고 자동으로 통합/배포 환경을 사용할 수 있게 되었습니다! 😎

회고

  • 젠킨스...? 사실은 Github Action보다는 젠킨스를 사용해보고 싶었습니다. 현업에서 많이들 사용하는 프로그램으로 알고 있고 한 번 경험해보는 것도 나쁘지 않을 것 같다라는 생각이었습니다. 하지만 이런 인프라 환경 구축하는데 시간을 많이 사용하면 6주안에 프로젝트를 내가 원하는 수준까지 만들기 힘들 것이라고 판단했습니다..ㅜ 이후 우리끼리 프로젝트 서버를 이전하면서 개선할 때 CI/CD 환경을 젠킨스로 변경해보고자 합니다..!
  • 개선할점
    • 빌드하는데 오래걸리는 느낌...? 처음에는 프로젝트 크기가 작아서 그런지 4분 후반~5분 초반정도면 배포가 완료되었었는데 점점 프로젝트 기능들이 추가되다보니 매 배포시마다 6분까지 걸리는 수준으로 왔습니다. 알파인같은 최대한 가벼운 베이스 이미지를 사용했지만 체감상 느껴지는 속도는 좀 답답한 느낌이었습니다. 그래서 추후 multi-stage build를 사용해서 최종 결과물만 이미지에 저장하는 방식으로 개선해볼 생각입니다! (참고)
    • mysql을 도커로 사용하니 계속 최신 이미지를 가져오게끔 구현되어있습니다. mysql이 계속 업데이트 된다면 업데이트 문제로 안되는 상황이 발생할수도..? 그와 별개의 문제로 볼륨을 사용하긴 하지만 혹시 모르게 날라갈수도 있으니까 mysql은 컨테이너보단 서버를 하나 파는게 좋을 것 같습니다.
    • package.json에서 devDependencies 잘 정의하면 좋을 것 같습니다.
      아무 생각없이 모든 패키지를 구분없이 설치하는 경우가 종종 있었는데 패키지를 dev와 prod 구분하여 설치하면 multi-stage build시에 프로덕션에 필요한 패키지만 골라서 설치할 수 있게되니 속도면에서 더 빠르게 개선할 수 있을 것 같습니다.

0개의 댓글