CircleCI와 Docker로 CI/CD 파이프라인 구축하기

kshired·2021년 7월 21일
0
post-thumbnail

CI/CD란?

Continuous Integration/Continous Deployment(혹은 Delivery)의 약자로, 지속적인 통합과 지속적인 배포를 의미합니다.

Continuous Integration

  • 빌드 및 테스트를 자동화해줍니다

예시)

  • Github에 Push
  • CI 도구( CircleCI, Jenkins, TravisCI 등.. )에서 자동으로 빌드 및 테스트 실행
  • 실패 혹은 성공의 결과를 알려줌

Continuous Deployment(Delivery)

  • 배포를 자동화의 개념

예시)

  • CI 도구를 통해 자동으로 빌드 및 테스트가 성공적으로 끝났으면 CD 시작
  • CD는 2가지의 예시를 생각 할 수 있음
    • production 서버에서 원래 동작하던 애플리케이션을 중단시키고, 바로 새로운 애플리케이션 시작
    • production 서버에서 새로운 애플리케이션을 다른 포트로 실행하고, reverse proxy 시켜줌

이번에 진행하는 프로젝트에서는 CI 툴로 CircleCI를 사용하였고, CD 방법으로는 첫번째 방법을 사용하였습니다.

CircleCI config.yml 설정하기

# .circleci/config.yml

version: 2.1

orbs:
  node: circleci/node@4.1

jobs:
  test: # test job
    docker:
      - image: cimg/node:14.17.3 # test를 실행할 node image
    steps:
      - checkout
      - node/install-packages: # yarn을 통해 package 설치
          pkg-manager: yarn
      - run: CURRENT_PATH=$(pwd)
      - run: sudo touch ${CURRENT_PATH}/.env.test # .env.test 환경 변수 파일 생성
      - run:
          name: "Setting environment vars for test env" # 테스트 환경을 위한 .env.test 파일 구성
          command: |
            echo "DATABASE_URL=${DATABASE_URL_TEST}" | sudo tee -a ${CURRENT_PATH}/.env.test
            echo "PORT=${PORT}" | sudo tee -a ${CURRENT_PATH}/.env.test
            echo "SECRET=${SECRET}" | sudo tee -a ${CURRENT_PATH}/.env.test
      - run: yarn install # install packages
      - run: yarn test    # start test with jest
  deploy:  # deploy job
    docker:
      - image: cimg/base:2021.04
    steps:
      - add_ssh_keys:
          fingerprints:
            - ${FINGERPRINTS} # ssh public key를 사용할 fingerprints 
      - run:
          name: "Run deploy script" # start deploy, 서버에 접속하여 deploy.sh 실행
          command: |
            ssh -o StrictHostKeyChecking=no ${HOST}@${HOSTNAME} /${TARGET_DIR}/deploy.sh 
workflows:
  test-and-deploy: # workflow 이름
    jobs:
      - test  # test job
      - deploy:
          requires: # test가 끝나야 deploy 실행
              - test
          filters:
              branches:
                only: main # main 브랜치에 push 됐을 때만 실행

저는 위와 같이 설정하였습니다.

Express를 이용하기 때문에, docker node 이미지를 통해 테스트를 진행하였습니다.

test job은 Docker image를 통해 Container내에서 패키지를 다운받고, 환경변수파일을 생성하여 진행합니다.

그 후 테스트가 성공하면 deploy job이 실행되도록 설정하였습니다.

deploy job은 서버에 접속하여, 서버에 설정되어있는 shell script를 실행하도록 하였고, shell 스크립트는 아래에서 설명하겠습니다.

deploy.sh

#!/bin/bash

echo "Deploy : Move to api directory."
cd /some/where/dir/fake-api/

echo "Deploy : Update fake-api."
git checkout main
git pull

echo "Deploy : Start build fake-api container."
docker build -t fak-api:latest .

echo "Deploy : Build Done."

container=$(docker ps --format "{{.Names}}" | grep fake-api)

if [ ! -z ${container} ] ; then # 현재 실행중인 컨테이너가 있는지 체크, 있다면 stop and remove
	echo "Deploy : Found currently running fake-api container."
	echo "Deploy : Stop and remove currently running fake-api container."
	docker stop fake-api
	docker rm fake-api
else  
	echo "Deploy : There is not currently running fake-api container."
	echo "Deploy : Start checking stopped fake-api container exists."
	
	container_stopped=$(docker ps -a --format "{{.Names}}" | grep fake-api)
	
	# 현재 정지되어있는 컨테이너가 있는지 체크, 있다면 remove
	if [ ! -z ${container_stopped} ] ; then
		echo "Deploy : Found stopped fake-api container."
		echo "Deploy : Remove stopped fake-api container."
		docker rm fake-api
	else
		echo "Deploy : There is not stopped fake-api container."
	fi
fi

echo "Deploy : Start migration." # 마이그레이션 시작
docker run -v fake-db:/usr/app/db --name fake-api-migration fake-api yarn migrate # 마이그레이션 후 종료되는 컨테이너

echo "Deploy : Migration done."
echo "Deploy : Remove migration container." # 마이그레이션이 끝났으니 컨테이너 삭제
docker rm fake-api-migration 

echo "Deploy : Start new version of fake-api container." # 새로운 버전의 api 컨테이너 시작
# fake-db volume은 db 볼륨, fake-log volume은 log file 볼륨
docker run -d -v fake-db:/usr/app/db -v fake-log:/root/.pm2/logs -p 8080:8080 --name fake-api fake-api

일단, 실제 개발중이기 때문에 이름은 전부 fake-@@으로 바꾸었습니다.

  1. CD를 시작해서, deploy.sh이 실행되면 일단 api의 디렉토리 이동하는 것 부터 시작됩니다.

  2. git checkout main && git pull 을 통해 최신 버전의 api로 업데이트를 진행합니다.

  3. 현재 실행중이거나 정지중인 도커 컨테이너가 있는지 확인하고 실행중이거나 정지중인 컨테이너가 있으면, 삭제합니다.

  4. 그 후 Prisma를 이용하여 새롭게 Migration할 데이터가 존재 할 수 있기 때문에, Migration을 진행합니다.

  5. Migration이 끝나면, Migration에 사용된 Docker Container를 삭제하고 새로운 api 컨테이너를 올립니다.

이렇게 CI/CD를 전부 진행하고 나면 main branch에 push하고, 총 걸리는 시간은 1분 30초 ~ 2분 정도입니다.

물론, 실시간으로 사용자가 많은 경우에는 꽤나 긴 시간이겠지만

저희가 제공할 서비스는 새벽이나 이른 아침에는 사용자가 거의 없다고 판단했기에

이러한 방식을 사용 할 수 있다고 생각하여 이렇게 구축하게 되었습니다.

그리고 사실, Docker Container는 Build하고 난 뒤 내려가기 때문에 아무리 늦어도 "현재 컨테이너 삭제 - migration 실행 - 새로운 컨테이너 실행"이 5초안에 이루어집니다.

큰 문제가 없다고 봅니다! 하지만, 여러 사람들이 동시다발적으로 불특정한 시간에 쓰는 product는 위와 같은 방법이 아닌 reverse proxy를 이용하는 방법을 사용하는게 맞는 것 같습니다.

profile
글 쓰는 개발자

0개의 댓글