본문 바로가기
CICD

Gighub Action , Docker Compose, Nginx, code deploy 무중단 배포

by 순원이 2024. 1. 11.

무중단 배포

1. rolling update 배포

새로 배포되어야 하는 버전을 하나씩 순차적으로 적용시키면서 배포하는 방식입니다. 한 번에 모두 배포되는 게 아니기 때문에 배포가 되는 과정에서 옛날 버전과 새로운 버전이 공존합니다. 그렇기 때문에 잘못하면 배포하는 과정 중에 호환성 문제가 생길 수 있습니다

2. Blue,Green 배포

예시를 들자면 이전의 8080포트를 쓰고 있는 컨테이너가 있습니다. 새로운 컨테이너를 새로운 포트(8081)로 올립니다. 다 올려졌다면 nginx는 새로운 컨테이너 포트로 연결하고 이전 컨테이너는 내립니다.

3. 카나리 배포

적은 사용자에게 먼저 배포 후 A/B 테스트. 현재 단계에서는 고려하지 않겠습니다.

rolling update는 인스턴스를 두 대 이상 사용하여 비용 문제가 발생합니다. 그래서 비교적 자료가 많고 구현이 간단한 Blue,Green 배포를 선택하겠습니다!

💡알고 들어갈 지식

  • docker-compose
    • 여러개의 도커 컨테이너를 쉽게 관리하기 위한 툴이다 반복적인 작업을 docker-compose.yml 파일을 읽어 실행됩니다.
  • NGIX
    • 서버 인스턴스에는 클라이언트의 요청을 처리해주는 서버 소프트웨어가 필요합니다. 서버 소프트웨어에는 크게 웹 서버웹 애플리케이션 서버로 구성됩니다.
    • 웹 애플리케이션 서버(WAS) : 서버의 배포된 코드를 실행 시키고, 동적인 응답을 준다.
    • 웹 서버: HTTP 프로트콜로 요청을 받고 정적인 파일(HTML,CSS,Javascript)를 응답으로 전달 해준다. 또한, 웹 애플리케이션 서버로 라우팅 하는 역할을 합니다. 대표적인 product로는 nginx와 apache가 있습니다.

 

 

전체적인 플로우

1. 깃허브 액션에서 빌드한다.

2. 깃허브 액션에서 zip 파일을 생성해 AWS S3에 저장한다

3. 깃허브 액션에서 Code deploy에게 S3에 저장한 Zip을 받아 배포하라고 요청한다.

4. Code deploy는 appspec.yml 파일을 보고 배포를 진행한다.

5. appspec.yml에 주요 동작은 deploy.sh를 보고 진행한다.

6. deploy.sh 동작은 blue가 올라가 있으면  green를 올리고 blue를 내린다. green이 올라가 있으면 blue를 올리고 green을 내린다.(green이나 blue를 빌드하고 올리는 건 docker-compose.blue(or green).yml를 보고 올린다.)

 

파일구조

deploy.yml(깃허브액션을 위한)

# 실행 시기
on:
  push:
    branches: develop
  pull_request:
    branches: develop
    
# 작업 내용
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Chekcout Main Branch
        uses: actions/checkout@v3

      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: 17
          distribution: 'temurin'
          
      - name: Set application.yml  
        uses: microsoft/variable-substitution@v1
        with:
          files: src/main/resources/application.yml
        env:
          jwt.secretkey: ${{ secrets.SECRETKEY }} 
          cloud.aws.credentials.access-key: ${{ secrets.AWS_ACCESS_KEY_ID }}
          cloud.aws.credentials.secret-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          imp.secret-key: ${{ secrets.API_SECRET_KEY }}
          imp.key: ${{ secrets.API_KEY }}
        
    
      ## gradle caching/빌드 시간 단축
      - name: Gradle Caching
        uses: actions/cache@v3
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      - name: Grant Permission Gradlew
        run: chmod +x gradlew
        shell: bash

      - name: Build Gradle
        run: ./gradlew build
        shell: bash

      # 배포에 필요한 여러 설정 파일과 프로젝트 빌드파일을 zip 파일로 모아줍니다.
      - name: zip file 생성
        run: |
          mkdir deploy
          cp ./docker/docker-compose.blue.yml ./deploy/
          cp ./docker/docker-compose.green.yml ./deploy/
          cp ./appspec.yml ./deploy/
          cp ./docker/Dockerfile ./deploy/
          cp ./scripts/deploy.sh ./deploy/
          cp ./build/libs/potato-API-server-0.0.1-SNAPSHOT.jar ./deploy/
          zip -r -qq -j ./$GITHUB_SHA.zip ./deploy

      # AWS 연결
      - name: AWS 연결
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      # S3에 프로젝트를 업로드
      - name: S3에 프로젝트 업로드
        run: |
         aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.zip s3://ugly-potato-bucket/$GITHUB_SHA.zip
          
      # CodeDelploy에 배포를 요청
      - name: Code Deploy 배포 요청
        run: aws deploy create-deployment --application-name potato --deployment-config-name CodeDeployDefault.AllAtOnce --deployment-group-name potato_deploy --s3-location bucket=ugly-potato-bucket,bundleType=zip,key=$GITHUB_SHA.zip

Dockerfile

# 기본 이미지 설정
FROM openjdk:17-jdk-slim

### JAR_FILE 경로에 해당하는 파일을 Docker 이미지 내부로 복사한다.
COPY ./potato-API-server-0.0.1-SNAPSHOT.jar potato-dev.jar

# 컨테이너 실행 명령 설정
ENTRYPOINT ["java", "-jar", "/potato-dev.jar"]

docker-compose.blue.yml

#blue
version: '3'
services:
  # 서비스의 이름
  backend:
    # 현재 디렉토리에서의 Dockerfile을 사용하여 Docker 이미지를 빌드
    build: .
    # 호스트의 8081 포트와 컨테이너의 80 포트를 매핑
    ports:
      - "8081:80"
    # 컨테이너의 이름
    container_name: spring-blue

docker-compose.green.yml

#green
version: '3'
services:
  backend:
    build: .
    ports:
      - "8082:80"
    container_name: spring-green

deploy.sh

#!/bin/bash

# 작업 디렉토리를 /home/ec2-user/app으로 변경
cd /home/ec2-user/ugly-potato

# 환경변수 DOCKER_APP_NAME을 spring으로 설정
DOCKER_APP_NAME=spring

# 실행중인 blue가 있는지 확인
# 프로젝트의 실행 중인 컨테이너를 확인하고, 해당 컨테이너가 실행 중인지 여부를 EXIST_BLUE 변수에 저장
EXIST_BLUE=$(sudo docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml ps | grep Up)

# 배포 시작한 날짜와 시간을 기록
echo "배포 시작일자 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ec2-user/deploy.log

# green이 실행중이면 blue up
# EXIST_BLUE 변수가 비어있는지 확인
if [ -z "$EXIST_BLUE" ]; then

  # 로그 파일(/home/ec2-user/deploy.log)에 "blue up - blue 배포 : port:8081"이라는 내용을 추가
  echo "blue 배포 시작 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ec2-user/deploy.log

	# docker-compose.blue.yml 파일을 사용하여 spring-blue 프로젝트의 컨테이너를 빌드하고 실행
	sudo docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml up -d --build

  # 30초 동안 대기
  sleep 30

  # /home/ec2-user/deploy.log: 로그 파일에 "green 중단 시작"이라는 내용을 추가
  echo "green 중단 시작 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ec2-user/deploy.log

  # docker-compose.green.yml 파일을 사용하여 spring-green 프로젝트의 컨테이너를 중지
  sudo docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml down

   # 사용하지 않는 이미지 삭제
  sudo docker image prune -af

  echo "green 중단 완료 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ec2-user/deploy.log

# blue가 실행중이면 green up
else
	echo "green 배포 시작 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ec2-user/deploy.log
	sudo docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml up -d --build

  sleep 30

  echo "blue 중단 시작 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ec2-user/deploy.log
  sudo docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml down
  sudo docker image prune -af

  echo "blue 중단 완료 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ec2-user/deploy.log

fi
  echo "배포 종료  : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ec2-user/deploy.log

  echo "===================== 배포 완료 =====================" >> /home/ec2-user/deploy.log
  echo >> /home/ec2-user/deploy.log

appspec.yml

version: 0.0

# Deploy 대상 서버의 운영체제를 표시
os: linux

# 코드 파일 전송과 관련된 설정
files:
  # 코드 파일의 소스 경로
  - source: /
    # 코드 파일의 대상 경로 -> /home/ec2-user/app 디렉토리로 파일을 복사한다.
    destination: /home/ec2-user/ugly-potato
    # 대상 경로에 이미 파일이 존재하는 경우, 덮어쓰기를 허용할지 여부
    overwrite: yes

# 파일 및 디렉토리 권한에 관련된 설정
permissions:
  # 권한을 설정할 대상 경로
  - object: /
    # 모든 파일 및 디렉토리를 의미
    pattern: "**"
    # 파일 및 디렉토리의 소유자를 ec2-user로 설정
    owner: ec2-user
    # 파일 및 디렉토리의 그룹을 ec2-user로 설정
    group: ec2-user

# Deploy 전후에 실행할 스크립트 또는 명령에 관련된 설정
hooks:
  # 애플리케이션 시작시 실행할 스크립트 또는 명령에 관련된 설정
  ApplicationStart:
    # 실행할 스크립트 또는 명령의 위치
    - location: deploy.sh
      # 스크립트 또는 명령 실행의 제한 시간을 설정
      timeout: 60
      # CodeDeploy 중 실행되는 스크립트 또는 명령을 실행할 사용자를 지정
      runas: ec2-user

AWS S3, CodeDeploy, ec2 생성 및 설정은 아래 링크를 참고하시면 됩니다.

https://velog.io/@ch4570/Github-Actions-Nginx를-이용한-CICD-무중단-배포-자동화-구축-나머지-설정과-Github-Actions-셋팅

AWS EC2 인스턴스 도커,도커 컴포즈 설치

// 도커 설치
sudo yum install docker -y

// 도커 실행
sudo service docker start

//Docker 그룹에 sudo 추가 (인스턴스 접속 후 도커 바로 제어할 수 있도록)
sudo usermod -aG docker ec2-user

//인스턴스 재접속 후 Docker 명령어 실행해보기 
docker run hello-world

// Docker 관련 권한 추가
sudo chmod 666 /var/run/docker.sock
docker ps

// 도커 컴포즈(도커 컨테이너를 다루는 도구) 설치
sudo curl \\
-L "<https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$>(uname -s)-$(uname -m)" \\
-o /usr/local/bin/docker-compose

// 권한 추가
sudo chmod +x /usr/local/bin/docker-compose

sudo dnf install libxcrypt-compat

// 버전 확인
docker-compose --version

 

AWS S3, CodeDeploy 생성 및 설정은 아래 링크를 참고하시면 됩니다.

https://velog.io/@ch4570/Github-Actions-Nginx를-이용한-CICD-무중단-배포-자동화-구축-나머지-설정과-Github-Actions-셋팅

한 인스턴스 내의 로드 밸런서로 Blue,Grenn 무중단 배포를(로드밸런싱) 구현할 것이기 때문에 ec2에 nginx를 설치해줍니다.

ec2에 nginx 설치

//nginx 설치
sudo yum install nginx
//nginx 위치 찾기
sudo find / -name nginx.conf

//파일 수정
sudo vim /etc/nginx/nginx.conf

// nginx 시작
sudo service nginx start

// 현재 nginx 상태확인
sudo service nginx status

// nginx 서비스를 중지 후 시작
sudo service nginx restart

nginx.conf

# nginx.conf

# Nginx 이벤트 모듈을 설정합니다. 
events {}

# HTTP 요청과 관련된 설정을 하는 부분입니다.
http {
	
    # upstream 서버 그룹을 정의하는 부분입니다. 로드 밸런싱을 위해 여러개의 서버를 가리킬 수 있습니다.
    upstream spring-server {
    	# 로드 밸런싱을 위해 locahost의 8081 포트와 8082 포트에 동작하는 서버를 지정합니다.
    	server localhost:8081;
        server localhost:8082;
    }
    
    # 실제 HTTP 서버를 설정하는 부분입니다.
    server {
    	# listen 지시문은 서버가 80포트에서 들어오는 요청을 수신하도록 설정합니다.
    	listen 80;
        
        include /etc/nginx/default.d/*.conf;
        
        # 모든 경로에 대한 처리를 정의합니다. 프록시 서버로의 요청을 설정하는 부분입니다.
        location / {
        	# proxy_set_header는 프록시 서버로 전달되는 요청 헤더를 설정하는 역할을 합니다.
        	proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header HOST $http_host;
            proxy_set_header X-Nginx-Proxy true;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            
            # proxy_pass는 실제 요청을 전달할 upstream 서버 그룹을 지정합니다.
            proxy_pass <http://spring-server>;
            
            # proxy_redirect는 프록시 응답의 리다이렉션을 설정하는 부분입니다.
            proxy_redirect off;
        }
    }
    
}

 

EC2에 code deploy agent 설치

//aws-cli 설치
sudo yum install -y aws-cli

//cli 설정
//Access Key, Secret Access Key : csv 파일에 있는 내용을 입력합니다.
//region name : 서울이라면 ap-northeast-2를 입력합니다.
//output format : json
cd /home/ec2-user
sudo aws configure

// ruby 설치
sudo yum install -y ruby

//code deploy agent 다운로드
wget <https://aws-codedeploy-ap-northeast-2.s3.amazonaws.com/latest/install>

//실행권한 추가한 뒤 설치
chmod +x ./install
sudo ./install auto

/Agent 실행중인지 확인
sudo service codedeploy-agent status

 

 

 

정상적으로 작동하였지만, 두 번째 배포(green)이 올라갈 때 ec2가 뻑이 났습니다. 이를 해결하는 건 아래 링크에서 설명하겠습니다.

 

 

 

 

 

 

 


문제점1

알고보니 ec2를 생성할 때 지역을 버지니아로 생성했었습니다. 아래 링크를 참조하여 ec2를 새롭게 만들어줬습니다.

https://ndb796.tistory.com/257

 

AWS EC2 인스턴스 지역 변경하기 (이미지를 활용한 방법)

가끔 자신이 AWS EC2로 운영하고 있는 서버의 지역(Region)을 바꾸고 싶을 때가 있습니다. 저는 다음과 같이 기존의 서버를 [오하이오] 지역에서 운영하고 있는 상태였습니다. 이를 [서울] 지역으로

ndb796.tistory.com

 

 

 


문제점2

S3에 zip파일이 올라가고 ec2가 이걸 받기 까지는 했는데 도커 컨테이너가 올라가 있지 않습니다.

Dockerfile에서 jar파일의 위치를 잘못 나타내고 있었습니다.

ec2내에서 위와 같이 jar 파일이 위치해있습니다. 

 

기존에는 프로젝트 파일내에 위치한 경로로 작성하였기에 ec2에서는 알 수 없는 경로이기에 이미지가 빌드가 안되었던 것입니다. ec2 기준으로 경로를 다시 작성하여 문제를 해결하였습니다.

 

기존코드

# 기본 이미지 설정
FROM openjdk:17-jdk-slim

# GitHub Actions에서 빌드한 JAR 파일 복사
COPY build/libs/potato-API-server-0.0.1-SNAPSHOT.jar potato-dev.jar

# 컨테이너 실행 명령 설정
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/potato-dev.jar"]

새로운 코드

# 기본 이미지 설정
FROM openjdk:17-jdk-slim

### JAR_FILE 경로에 해당하는 파일을 Docker 이미지 내부로 복사한다.
COPY ./potato-API-server-0.0.1-SNAPSHOT.jar potato-dev.jar

# 컨테이너 실행 명령 설정
ENTRYPOINT ["java", "-jar", "/potato-dev.jar"]

 


TIP)

가장 최신 로그를 확인하려면 ec2에서 해당 명령어로 확인할 수 있습니다.

tail -F /var/log/aws/codedeploy-agent/codedeploy-agent.log

 

AWS Code deploy 설정을 다 안해주고 ec2에 Code Deploy Agent를 설치하면 변경사항이 적용이 안됩니다. Code Deploy 설정을 바꿔줬다면 아래 명령어로 Agent를 다시 실행하세요 ~

sudo service codedeploy-agent restart

'CICD' 카테고리의 다른 글

리눅스 Ec2 mysql 명령어 모음  (0) 2024.02.06
리눅스 Ec2에 Redis6 설치  (0) 2024.01.27
Gighub Actions,docker 배포  (1) 2024.01.11