Continuous Integration/Continous Deployment(혹은 Delivery)의 약자로, 지속적인 통합과 지속적인 배포를 의미합니다.
Continuous Integration
예시)
Continuous Deployment(Delivery)
예시)
이번에 진행하는 프로젝트에서는 CI 툴로 CircleCI를 사용하였고, CD 방법으로는 첫번째 방법을 사용하였습니다.
# .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 스크립트는 아래에서 설명하겠습니다.
#!/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-@@으로 바꾸었습니다.
CD를 시작해서, deploy.sh이 실행되면 일단 api의 디렉토리 이동하는 것 부터 시작됩니다.
git checkout main && git pull 을 통해 최신 버전의 api로 업데이트를 진행합니다.
현재 실행중이거나 정지중인 도커 컨테이너가 있는지 확인하고 실행중이거나 정지중인 컨테이너가 있으면, 삭제합니다.
그 후 Prisma를 이용하여 새롭게 Migration할 데이터가 존재 할 수 있기 때문에, Migration을 진행합니다.
Migration이 끝나면, Migration에 사용된 Docker Container를 삭제하고 새로운 api 컨테이너를 올립니다.
이렇게 CI/CD를 전부 진행하고 나면 main branch에 push하고, 총 걸리는 시간은 1분 30초 ~ 2분 정도입니다.
물론, 실시간으로 사용자가 많은 경우에는 꽤나 긴 시간이겠지만
저희가 제공할 서비스는 새벽이나 이른 아침에는 사용자가 거의 없다고 판단했기에
이러한 방식을 사용 할 수 있다고 생각하여 이렇게 구축하게 되었습니다.
그리고 사실, Docker Container는 Build하고 난 뒤 내려가기 때문에 아무리 늦어도 "현재 컨테이너 삭제 - migration 실행 - 새로운 컨테이너 실행"이 5초안에 이루어집니다.
큰 문제가 없다고 봅니다! 하지만, 여러 사람들이 동시다발적으로 불특정한 시간에 쓰는 product는 위와 같은 방법이 아닌 reverse proxy를 이용하는 방법을 사용하는게 맞는 것 같습니다.