[Dimelo Project] Travis CI, Code Deploy, Docker로 무중단배포 하기

Suyeon Pi·2022년 3월 11일
1

Dimelo

목록 보기
21/22
post-thumbnail

Docker와 Docker hub를 이용한 무중단 배포글을 찾는데 힘이 들어 ..
나같이 Docker와 Docker hub를 이용해서 무중단 배포를 하려는 사람들을 위해 도움을 주고자 글을 써보려고 한다..
아직 Docker초보이고..아마도 더 좋은 방법이 있을지도 모르니까! 그냥 이렇게 했구나~ 정도로만 봐주시면 좋겠다!

순서

  1. VS code에서 코드를 git main 브랜치에 푸쉬한다
  2. travis CI에서 main브랜치의 commit을 감지하고 테스트를 진행한다.
  3. travis CI에서 Dockerfile을 통해 image를 빌드 한 후 Docker hub로 image를 푸쉬한다.
  4. travis CI에서 빌드 한 파일을 S3버킷으로 보낸다
  5. travis CI는 직접 배포를 못하므로 Code Deploy에서 S3에 전달된 파일을 EC2에 배포스크립트와 함께 보낸다.
  6. EC2에 코드가 도착하면 배포스크립트가 실행된다. 배포스크립트에서는 travis CI에서 Docker hub로 push한 image를 pull해서 docker container를 띄운다.
  7. nginx를 통해 무중단으로 운영된다.

설정하기

Travis CI - Github 연동과정, AWS EC2 생성 과정은 생략한다!
(참고로 나는 ubuntu 20.4 인스턴스를 생성했다)

AWS S3 버킷 생성

빌드한 코드를 저장할 버킷을 생성하자.
AWS - S3 Bucket 에들어가서 버킷만들기 클릭
버킷 이름을 정하고 나머지 설정은 그대로 둔 채 버킷만들기를 해준다.
리전은 서울 ap-northeast-2로 설정한다

AWS IAM 생성

Travis CI가 S3, Code Deploy를 사용 할수 있도록 Travis CI에 AWS 계정을 부여해준다

AWS - IAM - 사용자 추가



발급받은 액세스키 ID 와 비밀 엑세스키를 저장한다.

IAM 역할 생성

이제는 배포에 사용할 EC2와 Code Deploy에게 역할을 부여해보자.

EC2를 위한 역할

AWS - IAM - 액세스 관리 - 역할 - 역할만들기


CodeDeploy를 위한 역할만들기

EC2가 아닌 CodeDeploy를 검색해서 나오는 CodeDeploy를 선택해야한다. (이것 때문에 한참 해맸다 ㅋ)

EC2에 역할 연결하기

이제 우리 EC2 인스턴스에 아까 첫번째로 만든 역할을 연결해준다

AWS - EC2 - 인스턴스 (서울)

CodeDeploy 역할 연결

역할을 연결해주기 전에 Code Deploy 애플리케이션을 생성 해주자

애플리케이션을 생성했다면 배포그룹을 생성해줘야 한다.


애플리케이션 - 배포그룹 생성

아까 2번째로 만든 Code Deploy역할을 연결시켜준다


EC2에 CodeDeploy Agent설치하기

EC2 인스턴스에 터미널로 접속한다.
먼저 터미널에 awscli를 설치한다.

sudo apt-get install awscli

aws cli설치 후 aws configure 설정. 아까 IAM만들때 받은.csv파일에서 aws access key, aws secret access key를 설정.

$ aws configure
AWS Access Key ID [None]: 
AWS Secret Access Key [None]:
Default region name [None]: 
Default output format [None]: json

CodeDeploy agent 설치

https://docs.aws.amazon.com/ko_kr/ko_kr/codedeploy/latest/userguide/codedeploy-agent-operations-install-ubuntu.html

sudo apt-get update
sudo apt install ruby-full
sudo apt install wget


aws s3 cp s3://aws-codedeploy-ap-northeast-2/latest/install . --region ap-northeast-2

chmod +x ./install

CodeDeploy 실행중인지 확인

sudo service codedeploy-agent status

Docker, docker-compose설치

ec2 서버에 도커를 설치 한다.

curl -fsSL https://get.docker.com/ | sudo sh

sudo 없이 Docker를 실행 하려면 다음과 같이 입력

sudo usermod -aG docker ubuntu

docker-compose를 이용해 docker container를 띄울 것이므로 docker-compose도 linux서버에 설치해준다.

sudo curl -L "https://github.com/docker/compose/releases/download/1.23.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

sudo chmod +x /usr/local/bin/docker-compose

docker-compose version

Nginx 설정

nginx를 통해 80포트로 요청을 받아 3001, 3002 포트로 리버스 프록시를 만들어 줘야한다. (나는 ssl관련은 aws elastic load balancer로 하고 있기 때문에 nginx에서는 80포트만 listen하도록 하였다)

우선 nginx설치를 하자.

https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-18-04

sudo apt-get update && sudo apt-get install nginx

80포트만 허용한다

sudo ufw allow 'Nginx HTTP'
sudo ufw status

nginx가 구동되고 있는지 확인

systemctl status nginx

서버블록 만들기 (dimelo라는 이름으로 만듬)

sudo vi /etc/nginx/sites-available/dimelo
upstream 원하는 이름 {
                least_conn;
                server 127.0.0.1:3001 weight=5 max_fails=3 fail_timeout=10s;
                server 127.0.0.1:3002 weight=10 max_fails=3 fail_timeout=10s;
}
server {
        server_name 서버의 도메인이나 IP;
        location / {
                proxy_set_header HOST $host;
                proxy_pass http://원하는 이름;
       }
}

80포트로 들어오는 요청을 3001, 3002포트로 로드밸런싱하도록 설정한다.

/etc/nginx/sites-enabled 와 방금 만든 서버블록 연결하기

sudo ln -fs /etc/nginx/sites-available/dimelo /etc/nginx/sites-enabled/

nginx 문법 확인 후 nginx 재시작

sudo nginx -t
sudo service nginx restart

배포를 위한 스크립트 만들기

-- backend/
    |-- dist/
    |-- node_modules/
    |-- src/
        ㄴ-- main.ts
    |-- package.json
    |-- Dockerfile(앞으로 만들 파일)
    |-- .dockerignore(앞으로 만들 파일)
    |-- Dockerfile.dev(앞으로 만들 파일)
    |-- .travis.yml(앞으로 만들 파일)
    |-- appspec.yml(앞으로 만들 파일)
    |-- docker-compose.blue.yml(앞으로 만들 파일)
    |-- docker-compose.green.yml(앞으로 만들 파일)
    |-- deploy.sh(앞으로 만들 파일)
    ㄴ-- execute-deploy.sh(앞으로 만들 파일)
    

VS Code로 돌아와 다음과 같이 파일을 만들 예정이다.

Dockerfile 만들기

vscode에서 각자의 프로젝트에 맞게 Dockerfile을 만들어 준다.
Dimelo프로젝트는 nest.js 프로젝트이고 PM2를 사용하여 무중단 운영한다.
그리고 alpine이미지를 사용하기 때문에 bcrypt같은 라이브러리 사용을 위해 python을 추가로 설치해주고 있다.
이미지 경량화를 위해 multi stage build를 하였다.
방법이 정확하지 않을 수 있으니 참고로만 봐주면 좋겠다 !

Dockerfile

FROM node:12-alpine3.11 As development

WORKDIR /usr/src/app

COPY package*.json ./

# pm2 설치
RUN npm install --global pm2

# bcrypt 사용을 위해 python 설치
RUN apk add --no-cache --virtual .gyp python make g++ pkgconfig pixman-dev cairo-dev pango-dev 
RUN npm install

COPY . .

RUN npm run build

FROM node:12-alpine3.11 as production

ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install --global pm2
RUN apk add --no-cache --virtual .gyp python make g++ pkgconfig pixman-dev cairo-dev pango-dev
RUN npm install --only=production

COPY . .

COPY --from=development /usr/src/app/dist ./dist

EXPOSE 3000

CMD ["pm2-runtime", "dist/main.js"]

Docker image를 build 시킬때 node_modules은 넣고 싶지 않으므로 .dockerignore 파일을 만들어서 다음과 같이 작성해준다

.dockerignore

node_modules

도커배포환경 이미지를 빌드하기 전에 테스트를 위한 운영환경을 위한 Dockerfile.dev 파일을 만들어주었다 (*필수 아님)

Dockerfile.dev

FROM node:12-alpine3.11 As development

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install --global pm2
RUN apk add --no-cache --virtual .gyp python make g++ pkgconfig pixman-dev cairo-dev pango-dev
RUN npm install

COPY . .

CMD ["npm", "run", "start:dev"]

travis CI 설정 파일 만들기

가장 중요한 부분이다! .travis.yml 파일을 만들어 어플을 테스트하고 Docker image를 빌드하고 travisCI는 스스로 배포를 하지 못하기 때문에 provider에 s3, codedeploy를 작성해줘야한다.

  • travis ci에 docker환경으로 만들 것이라고 선언
  • 구성된 도커환경에서 Dockerfile.dev를 이용해서 운영버전 이미지를 생성한다.
  • 만들어진 운영버전 이미지를 통해 test를 수행한다.
  • 테스트가 성공했다면 배포버전 이미지(Dockerfile)을 빌드
  • Docker hub에 배포 이미지를 PUSH한다.

.travis.yml

language: generic

sudo: required

services:
  - docker #docker 환경임을 명시

branches:
  - main

before_install: 
  - docker build -t {docker hub아이디}/dimelo -f Dockerfile.dev . #운영버전 이미지빌드
script:

  - docker run -e CI=true {docker hub아이디}/dimelo npm run test # 운영버전이미지로 테스트
after_success:
  - docker build -t {docker hub아이디}/dimelo . #배포환경 이미지 빌드

  - echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_ID" --password-stdin #Docker hub로그인, 추후에 travis에서 설정

  - docker push {docker hub아이디}/dimelo #Docker이미지 hub에 푸쉬

before_deploy:
  - zip -r dimelo * #dimelo라는 이름으로 zip파일 생성
  - mkdir -p deploy #deploy폴더 생성
  - mv dimelo.zip deploy/dimelo.zip #만든 zip파일을 deploy폴더로 이동

deploy:
  - provider: s3
    access_key_id: $AWS_ACCESS_KEY #추후에 travis에서 설정
    secret_access_key: $AWS_SECRET_ACCESS_KEY #추후에 travis에서 설정
    region: ap-northeast-2
    bucket: dimelo-ci-cd #s3버킷 설정
    skip_cleanup: true
    local_dir: # deploy 디렉터리에 있는 파일을 s3로 업로드 하겠다는 의미
    wait-until-deployed: true
    on:
      branch: main
      repo: dimelo-project/backend #본인의 git repository
  - provider: codedeploy
    access_key_id: $AWS_ACCESS_KEY
    secret_access_key: $AWS_SECRET_ACCESS_KEY
    bucket: dimelo-ci-cd
    key: dimelo.zip
    bundle_type: zip
    application: dimelo-deploy #codedeploy 애플리케이션
    deployment_group: dimelo #codedeploy 배포그룹
    region: ap-northeast-2
    wait-until-deployed: true
    on:
      repo: dimelo-project/backend #본인의 git repository
      branch: main
notifications:
  email:
    on_success: always

travis CI 사이트로 와서 github와 연동한 프로젝트에 Settings에 들어간다.

.travis.yml파일에서 설정한 변수에 대해서 값을 넣어 준다.

CodeDeploy 행동 정의, 배포 스크립트 작성

travis ci는 깃허브의 코드를 s3에 업로드를 하고 code Deploy에게 이벤트를 발생 시킨다. appspec.yml파일은 CodeDeploy가 이벤트가 발생했을 때 어떻게 행동할지 정의하는 파일이다.

appspec.yml

version: 0.0
os: linux
files:
  - source: /
    destination: /home/ubuntu/dimelo # S3에서 가지고 온 파일을 서버에 저장할 장소
hooks:
  AfterInstall: # 배포가 끝나면 실행
    - location: execute-deploy.sh
      timeout: 240

CodeDeploy는 s3에서 코드를 받아 EC2서버에 전달하고 직접 명령어 실행은 하지 못한다. 그렇기 때문에 루트 디렉터리에 쉘 스크립트를 만들고 appspect.yml이 해당 쉘 스크립트를 실행 하도록 해야한다.
프로젝트 루트디렉터리에 execute-deploy.sh 파일을 생성한다.

execute-deploy.sh

#!/bin/bash

cd /home/ubuntu/dimelo 
sudo chmod +x ./deploy.sh 
sudo ./deploy.sh > /dev/null 2> /dev/null < /dev/null & 

배포 스크립트가 있는 곳으로 이동 -> 배포 스크립트를 실행할 권한 부여 -> 배포 스크립트 실행

execute-deploy.sh 스크립트가 최종 배포 스크립트를 실행 하도록 했다면 이제 진짜 최종 배포 스크립트를 만들어 보자. 이 부분을 다른 블로그와 조금 다르게 설정하였다. 프로젝트 루트에 deploy.sh 파일을 만든다.

deploy.sh

#!/bin/bash

DOCKER_APP_NAME=nest-app

EXIST_BLUE=$(docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml ps | grep Up)

if [ -z "$EXIST_BLUE" ]; then
	echo "blue up"
	docker pull {이미지 이름}
	docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml up -d

	sleep 10

	docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml down
	docker image prune -af #사용 하지 않는 이미지 삭제
else
	echo "green up"
	docker pull {이미지 이름}
	docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml up -d

	sleep 10

	docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml down
	docker image prune -af
fi

blue, green 중 어떤 container가 실행 중인지 확인하고 이에 반대대는 컨테이너를 띄우고(up) 실행 중이었던 컨테이너는 다시 종료(down)하는 스크립트이다. docker-compose를 통해 컨테이너를 실행하기 전에 docker hub에서 해당 image를 pull 받고 docker-compose를 통해 container를 실행, 종료 시키고 이전 컨테이너를 종료 시키면 이전에 사용 하던 docker image를 삭제한다.

docker-compose 설정, .env파일 생성

이제 마지막이다 !! docker-compose를 통해 blue, green컨테이너를 띄울 것이기 때문에 docker-compose.blue.yml docker-compose.green.yml 파일을 만들어 준다.

#blue
version: '3'
services:
  nest-app:
    image: {이미지 이름}:latest
    restart: unless-stopped
    ports:
      - 3001:3000
    env_file:
      - /home/ubuntu/env/.env
#green
version: '3'
services:
  nest-app:
    image: {이미지 이름}:latest
    restart: unless-stopped
    ports:
      - 3002:3000
    env_file:
      - /home/ubuntu/env/.env

deploy.sh 에서 Pull 받은 이미지 중 가장 최신 버전을 사용 할 것을 image에 명시한다. 그리고 컨테이너에 환경 변수를 넣어 주기 위해 우분투 서버에 환경 변수를 위한 디렉터리를 만들고 sudo mkdir env .env 파일을 만들어 sudo vi .env vscode의 .env 환경변수와 똑같이 입력하고 저장한다.:wq!
docker-compose env_file에다 해당 환경변수 디렉터리를 전달한다.
ports는 blue는 3001, green은 3002로 실행한다. (참고로 프로젝트 기본은 3000으로 실행된다, 각자 프로젝트에 맞게 설정하면 된다)

끝 테스트 해보자 !

main 브랜치에서 git push를 하면 travis CI가 감지하고 이미지를 빌드하고 코드는 S3 -> Code Deploy -> EC2로 전달 된다.

Code Deploy에서 성공했음을 볼 수 있다.

터미널에서 docker ps를 통해 port가 성공적으로 바뀌었는지도 확인해보자!


막막했던 무중단 배포에 정말 도움을 많이 받은 글
https://velog.io/@jeff0720/Travis-CI-AWS-CodeDeploy-Docker-%EB%A1%9C-%EB%B0%B0%ED%8F%AC-%EC%9E%90%EB%8F%99%ED%99%94-%EB%B0%8F-%EB%AC%B4%EC%A4%91%EB%8B%A8-%EB%B0%B0%ED%8F%AC-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-2

profile
Stay hungry, Stay foolish!

0개의 댓글