ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • GitHub-Actions로 CI/CD 구축하기 (2)
    Lesson-Learned/tech 2023. 12. 17. 17:51
    GitHub-Actions로 CI/CD 구축하기 (2) (AWS S3 + AWS CodeDeploy + Spring Boot)

     

    안녕하세요, MADII의 Server 개발자 하노입니다 🍀

    오늘은 지난 포스팅에 이어 마디의 서버 아키텍처 CI/CD 과정을 마저 소개해 보겠습니다!

    유중단 배포까지 성공했으니, 이어서 Nginx를 활용하여 무중단 배포 과정을 진행해 보겠습니다.


    CI/CD 구축 과정 2) Nginx로 무중단 배포하기

    [레퍼런스 2], [레퍼런스 3]을 참고하기도 하고 구글링도 많이 하면서 진행했습니다.

    전체적인 무중단 배포 과정을 먼저 설명드리겠습니다.

    1. appspec.yml 에 정의된 대로 Code Deploy가 새 애플리케이션 파일을 EC2 인스턴스에 배포합니다.
    2. run_new.sh 스크립트가 실행되어 새로운 WAS 인스턴스를 시작합니다.
    3. health_check.sh 스크립트가 새 WAS 인스턴스의 정상 작동 여부를 확인합니다.
    4. 모든 것이 정상이면, switch.sh 스크립트가 실행되어 Nginx가 새 WAS 인스턴스로 트래픽을 전환합니다.
    5. 이 과정을 통해 기존 WAS 인스턴스에서 새로운 WAS 인스턴스로의 전환이 서비스 중단 없이 이루어집니다.

    먼저 Ubuntu 환경에서 Nginx를 설치하기 위해서는 apt 패키지 관리 도구를 사용해야 합니다.

    # 패키지 리스트 업데이트
    sudo apt update
    
    # nginx 설치
    sudo apt install -y nginx
    
    # nginx 실행 확인
    sudo systemctl status nginx
    

    Nginx 실행 확인

     

    http(80) 로 접속하면 Nginx 에서 요청을 다른 포트로 전달할 수 있도록 아래 설정을 진행합니다.

    1. service-url.inc 파일 생성

    sudo vim /etc/nginx/conf.d/service-url.inc
    
    # service-url.inc
    # 아래 파일로 nginx가 바라볼 포트를 지정한다.
    # 내용은 배포시 마다 실행되는 scripts로 동적으로 변경된다.
    
    set $service_url <http://127.0.0.1:8081>;
    

     

    2. nginx.conf 로 설정 파일 수정

    *sudo vim /etc/nginx/nginx.conf*
    
    server {
                    listen 80;
                    listen [::]:80;
                    server_name _;
    
                    include /etc/nginx/conf.d/service-url.inc;
    
                    location / {
                            # service_url로 요청 전달!
                            proxy_pass $service_url;
                            proxy_set_header X-Real-IP $remote_addr;
                            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                            proxy_set_header Host $http_host;
                    }
            }

    * nginx.conf 일부

     

    3. appspec.yml 추가

    AWS CodeDeploy에 의해 사용되며, 배포할 파일의 위치, 권한 설정, 그리고 배포 중 실행할 스크립트에 대한 정보를 담고 있습니다.

    • files 섹션에서는 소스 파일을 EC2 인스턴스의 /home/ubuntu/seesaw-app/ 경로에 배포하도록 지정합니다.
    • permissions 섹션은 배포된 파일에 대한 사용자(ubuntu) 및 그룹(ubuntu) 권한을 설정합니다.
    • hooks 섹션은 배포 생명주기의 특정 단계에서 실행할 스크립트를 지정합니다. 여기서는 ApplicationStart 단계에서 run_new_was.sh, health_check.sh, switch.sh 스크립트를 순서대로 실행합니다.
    version: 0.0
    os: linux
    files:
      - source: /
        destination: /home/ubuntu/seesaw-app/ # 파일들을 EC2 인스턴스에서 어디에 배포할지를 나타내는 경로
        overwrite: yes
    
    permissions:
      - object: /
        pattern: "**"
        owner: ubuntu
        group: ubuntu
    
    # 새로 추가한 부분
    hooks:
      ApplicationStart:
        - location: scripts/run_new_was.sh
          timeout: 180
          runas: ubuntu
        - location: scripts/health_check.sh
          timeout: 180
          runas: ubuntu
        - location: scripts/switch.sh
          timeout: 180
          runas: ubuntu
    

     

    4. scripts 디렉토리 생성

    4-1. run_new_was.sh

    새로운 웹 애플리케이션 서버(WAS) 인스턴스를 시작하며, 스크립트는 다음과 같은 순서로 동작합니다.

    1. 현재 WAS가 실행 중인 포트 번호를 가져옵니다.
    2. 타겟 포트 번호를 설정합니다.
    3. 타겟 포트에서 실행 중인 WAS의 PID를 가져옵니다.
    4. 해당 PID가 존재하면 프로세스를 종료합니다.
    5. 새 WAS 인스턴스를 타겟 포트에서 실행합니다.
    #!/bin/bash
    
    CURRENT_PORT=$(cat /etc/nginx/conf.d/service-url.inc | grep -Po '[0-9]+' | tail -1)
    TARGET_PORT=0
    
    echo "> Current port of running WAS is ${CURRENT_PORT}."
    
    if [ ${CURRENT_PORT} -eq 8081 ]; then
      TARGET_PORT=8082
    elif [ ${CURRENT_PORT} -eq 8082 ]; then
      TARGET_PORT=8081
    else
      echo "> No WAS is connected to nginx"
    fi
    
    TARGET_PID=$(lsof -Fp -i TCP:${TARGET_PORT} | grep -Po 'p[0-9]+' | grep -Po '[0-9]+')
    
    if [ ! -z ${TARGET_PID} ]; then
      echo "> Kill WAS running at ${TARGET_PORT}."
      sudo kill ${TARGET_PID}
    fi
    
    nohup java -jar -Dserver.port=${TARGET_PORT} /home/ubuntu/seesaw-app/build/libs/SeeSaw-0.0.1-SNAPSHOT.jar > /home/ubuntu/nohup.out 2>&1 &
    echo "> Now new WAS runs at ${TARGET_PORT}."
    exit 0
    

     

    스크립트 실행 전에 다음 사항들을 확인한 후 동작하도록 합니다.

    /etc/nginx/conf.d/service-url.inc &nbsp;파일이 실제로 존재하는지 확인합니다.
    service-url.inc &nbsp;파일 내에 원하는 포트 정보(예: 8081 또는 8082)가 포함되어 있는지 확인합니다.
    /home/ubuntu/seesaw-app/build/libs/ &nbsp;디렉토리에&nbsp; .jar &nbsp;파일이 실제로 있는지 확인합니다.
    lsof &nbsp;명령어가 시스템에 설치되어 있어야 합니다. 없다면 **sudo apt install lsof**로 설치할 수 있습니다.

     

    4-2. health_check.sh

    새로 시작된 WAS 인스턴스가 정상적으로 작동하는지 확인하는 스크립트입니다.

    • **curl**을 사용하여 새 WAS 인스턴스의 건강 상태를 확인합니다. 여기서 HTTP 상태 코드 200이 응답되면 정상으로 간주합니다.
    • 여러 번 시도 후에도 정상적인 응답이 없으면 스크립트는 실패로 종료됩니다.
    @RestController
    public class HealthController {
    
        @GetMapping("/health")
        public String checkHealth() {
            return "healthy";
        }
    }
    
    #!/bin/bash
    
    # Crawl current connected port of WAS
    CURRENT_PORT=$(cat /etc/nginx/conf.d/service-url.inc | grep -Po '[0-9]+' | tail -1)
    TARGET_PORT=0
    
    # Toggle port Number
    if [ ${CURRENT_PORT} -eq 8081 ]; then
        TARGET_PORT=8082
    elif [ ${CURRENT_PORT} -eq 8082 ]; then
        TARGET_PORT=8081
    else
        echo "> No WAS is connected to nginx"
        exit 1
    fi
    
    echo "> Start health check of WAS at '<http://127.0.0.1>:${TARGET_PORT}' ..."
    
    for RETRY_COUNT in 1 2 3 4 5 6 7 8 9 10
    do
        echo "> #${RETRY_COUNT} trying..."
        RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}"  <http://127.0.0.1>:${TARGET_PORT}/health)
    
        if [ ${RESPONSE_CODE} -eq 200 ]; then
            echo "> New WAS successfully running"
            exit 0
        elif [ ${RETRY_COUNT} -eq 10 ]; then
            echo "> Health check failed."
            exit 1
        fi
        sleep 10
    done
    

     

    스크립트 실행 전에 curl 명령어가 시스템에 설치되어 있는지 확인합니다. 만약 설치되어 있지 않다면 **sudo apt install curl**로 설치할 수 있습니다.

     

    4-3. switch.sh

    Nginx 설정을 변경하여 새로운 WAS 인스턴스로 트래픽을 전환하는 스크립트입니다.

    • Nginx 의 service-url.inc 파일을 수정하여 프록시가 새 WAS 인스턴스로 트래픽을 전달하도록 설정합니다.
    • Nginx 를 리로드하여 변경 사항을 적용합니다.
    #!/bin/bash
    
    # Crawl current connected port of WAS
    CURRENT_PORT=$(cat /etc/nginx/conf.d/service-url.inc  | grep -Po '[0-9]+' | tail -1)
    TARGET_PORT=0
    
    echo "> Nginx currently proxies to ${CURRENT_PORT}."
    
    # Toggle port number
    if [ ${CURRENT_PORT} -eq 8081 ]; then
        TARGET_PORT=8082
    elif [ ${CURRENT_PORT} -eq 8082 ]; then
        TARGET_PORT=8081
    else
        echo "> No WAS is connected to nginx"
        exit 1
    fi
    
    # Change proxying port into target port
    echo "set \\$service_url <http://127.0.0.1>:${TARGET_PORT};" | sudo tee /etc/nginx/conf.d/service-url.inc
    
    echo "> Now Nginx proxies to ${TARGET_PORT}."
    
    # Reload nginx
    sudo systemctl reload nginx
    
    echo "> Nginx reloaded."
    

     

    5. 무중단 배포 전 WAS 띄우기

    netstat -tuln | grep 8081
    

    처음에는 어떤 WAS도 실행되지 않고 있기 때문에 초기에 WAS를 하나 실행합니다. EC2에 접속해서 해당 프로젝트 디렉터리로 이동한 후 JAR 파일을 Java로 실행하여 애플리케이션을 시작합니다.

    보통 Spring Boot 프로젝트를 빌드할 때 *.jar*-plain.jar 두 가지 버전의 JAR 파일이 생성될 수 있습니다. 이 두 JAR 파일의 차이점은 다음과 같습니다:

    1. -plain.jar: 이것은 일반적인 JAR 파일로서, 내장 Tomcat과 같은 내장 웹 서버 없이 생성됩니다.
    2. .jar: 이것은 실행 가능한 JAR 파일로, 내장 웹 서버(Tomcat, Jetty 등)를 포함하고 있어 java -jar 명령어로 직접 실행할 수 있습니다.

    무중단 배포나 다른 용도로 Spring Boot 애플리케이션을 실행하려면 내장 웹 서버를 포함하는 실행 가능한 *.jar 파일을 사용해야 합니다.

    java -jar -Dserver.port=8081 /home/ubuntu/seesaw-app/build/libs/SeeSaw-0.0.1-SNAPSHOT.jar &
    

    -> &로 백그라운드 실행!

     

    8081로 띄우기

     

    무중단 배포 성공

    성공!

     


    CI/CD 구축하면서 만난 Trouble Shooting은 크게 2가지가 있습니다.

    Trouble Shooting 1. 유중단 배포 확인 중 Code Deploy 배포 실패

    이렇게 실패가 떠서 View events를 눌렀더니 다음 화면이 나왔습니다.
    UnknownError를 눌러보니 &lsquo;The specified key does not exist&rsquo;라고 떴습니다.

     

    원인

    • AWS S3에서 특정 객체를 찾을 수 없을 때 발생하는 오류입니다. AWS Code Deploy가 배포 프로젝트를 찾을 수 없을 때 발생한다고 합니다.
    • AWS S3에 압축 파일의 형태로 업로드는 잘 되고 있지만, 원인은 S3에 업로드 되어 있는 위치와 Code Deploy에서 참조하는 위치가 달랐기 때문이었습니다.

     

    해결

    - name: Upload to S3
      run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.zip s3://$S3_BUCKET_NAME/$PROJECT_NAME/$GITHUB_SHA.zip
    
      # run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.zip s3://$S3_BUCKET_NAME/$GITHUB_SHA.zip
    ​
    • 경로 수정하니 배포 성공하였고, Code Deploy가 참조하길 기대하는 위치에 압축 파일의 형태로 프로젝트가 올라가 있는 것을 확인할 수 있었습니다.

     

    Trouble Shooting 2. CI/CD는 성공했는데, Spring Boot Application 연결 오류

    기대하는 바로는 퍼블릭 ip 접속 시 Nginx 화면이 뜨고 Spring Boot Application의 Whitelabel이 뜨지 않았습니다. 프로젝트 연결이 잘 안 되었나 생각이 들어 Nginx 설정 파일을 살펴 보았습니다.

     

    원인

    • 80번 포트로 들어온 요청을 처리하는 설정 파일이 충돌하는 문제였습니다.
      • 무중단 블루-그린 배포를 위한 Nginx 설정의 기본 개념은 ‘Nginx가 들어오는 모든 요청을 현재 활성화된 서버(블루 혹은 그린)로 프록시하는 것’입니다.
      • 기본적으로 Nginx 설정 파일은 /etc/nginx/nginx.conf 또는 /etc/nginx/sites-available/default 등의 경로에 위치하게 됩니다.
      user www-data;
      worker_processes auto;
      pid /run/nginx.pid;
      include /etc/nginx/modules-enabled/*.conf;
      
      events {
              worker_connections 768;
              # multi_accept on;
      }
      
      http {
              server {
                      listen 80;
                      listen [::]:80;
                      server_name _;
      
                      include /etc/nginx/conf.d/service-url.inc;
      
                      location / {
                              # service_url로 요청 전달!
                              proxy_pass $service_url;
                              proxy_set_header X-Real-IP $remote_addr;
                              proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                              proxy_set_header Host $http_host;
                      }
              }
      
      # Default server configuration
      #
      server {
              listen 80 default_server;
              listen [::]:80 default_server;
      
              root /var/www/html;
      
              # Add index.php to the list if you are using PHP
              index index.html index.htm index.nginx-debian.html;
      
              server_name _;
      
              location / {
                      # First attempt to serve request as file, then
                      # as directory, then fall back to displaying a 404.
                      try_files $uri $uri/ =404;
              }
      
      • default 설정은 여러 가지 용도로 사용할 수 있지만, 현재 상황에서는 **nginx.conf**의 설정과 충돌이 발생할 수 있습니다. 그 이유는 두 설정 모두 80 포트에서 동작하고 있기 때문입니다.
      • 현재 제시된 설정에 따르면 **nginx.conf**에서는 80 포트로 들어온 요청을 service-url.inc에 정의된 서비스 URL 즉, http://127.0.0.1:8081로 프록시하도록 설정되어 있습니다. 반면 default 설정에서는 80 포트로 들어온 요청을 기본 웹 루트 (/var/www/html)에서 처리하도록 설정되어 있습니다
      • 두 설정이 동시에 활성화된 상태에서는 80 포트로 들어온 요청이 어느 설정에 따라 처리될지 명확하지 않습니다. 따라서 default 설정을 비활성화하여 이 충돌을 해결할 필요가 있습니다.

     

    해결

    • default 설정 비활성화: sites-enabled 디렉터리의 default 심볼릭 링크를 제거하여 default 설정을 비활성화합니다.이렇게 하면 **/etc/nginx/sites-available/default**의 설정은 더 이상 활성화되지 않습니다.
    sudo rm /etc/nginx/sites-enabled/default
    
    • Nginx 재시작: 설정 변경 사항을 적용하기 위해 Nginx를 재시작합니다.
    sudo systemctl restart nginx
    

     

    원하는 Whitelabel 화면이 보이고 해결 완료했습니다!

     


    이렇게 마디의 CI/CD 구축 과정을 적어 보았는데요, 아직 공부하면서 배우고 있는 중이라 부족한 점이 많을 수 있으니 참고해 주세요. 처음 도전한 무중단 블루-그린 배포 방식, 처음 사용해본 Code Deploy로 새로운 인프라 구축을 해볼 수 있어 유의미한 시간이었습니다.

     

    그럼 이만 포스팅을 마치도록 하겠습니다. 마디와 구떠리에 많은 관심 가져 주시고 다음 포스팅에서 만나요 :)

    긴 글 읽어 주셔서 감사합니다 ❤️

Designed by Tistory.