TIL/개념정리

Github Action + PM2로 CI/CD 구축하기

초집중 2023. 7. 29. 22:39

개요

웹 서비스를 운영하면서 필수적인 배포 전략을 많이 조사해보고 고민해보았습니다.

 

배포 환경을 실제 구현하기 위한 방법은 많기 때문에, 우선 가장 좋은 배포 전략을 고민해보고자 했고 이를 간단히 정리해보았습니다.

 

배포전략

  • 롤링 배포
    • 하나씩 새 버전으로 업데이트한다 → 로드밸런서로 트래픽을 제어하거나 새로운 서버를 올려 트래픽을 하나씩 제어한다
    • 여러 오케스트레이션 도구에서 지원하고 신버전 장애 발생시 격리될 수 있다는 점, 신버전이 빠르게 릴리즈될 수 있다는 장점이 있다
    • 하나씩 배포되기 때문에 대규모 배포작업이 일어나게되면 상당히 시간이 오래걸리게 된다. 뿐만 아니라 오류가 격리될 순 있지만 복구하는데 시간이 많이 발생할 수 있다.
  • 블루/그린
    • 서버를 두 그룹으로 나누게 된다 블루/그린. 실제 운영되는 그룹을 블루라고 했을 때 새 버전이 배포될 때 그린 그룹에 모두 넣고 이후 완성되게 된다면 블루를 그린으로 교체한 뒤 블루는 유지하거나 조절한다
    • 아예 독립적으로 관리하기 때문에 환경 당 관리가 용이하고 안정성이 높다.
    • 적절하게 관리할 수 없다면 서버를 유지하기 어려워 비용이 매우 많이 발생할 수도 있다.
  • 카나리
    • 서버를 롤링이나 블루/그린방식으로 운영할 수 있다. 이때 트래픽을 가중치에 따라서 특정 서버로 트래픽을 조절할 수 있다.
    • 특정 유저에게 제공하다가 문제 발생시 롤백 혹은 문제 해결 방식이 쉽다.
    • 블루/그린 방식과 마찬가지로 정상적인 서비스 운영을 위한 관리가 더 필요하고 비용이 많이 발생할 수 있다.

무중단 배포 아키텍처와 배포 전략 (Rolling, Blue/Green, Canary)

 

무중단 배포 아키텍처와 배포 전략 (Rolling, Blue/Green, Canary)

중단 배포 방식과 다운타임 다운타임 서버 한대로 서비스를 운영한다고 가정해보자. 현재 서버에는 V1 버전이 실행되고 있는 상황이다. 그리고 우리는 이번에 여러 기능을 추가한 V2 버전을 새로

hudi.blog

 

배포 방식

위는 어디까지나 전략일 뿐 나의 서비스에 실제 적용시키기 위해서 어떻게 활용할지 조사해보았습니다.

구현이 간단했고, 아직은 개발 단계라 트래픽이 많이 발생하지 않기 때문에 블루/그린 배포 전략이 간단하고 빠르게 적용가능할 것이라고 생각되어 그쪽으로 조사해보았습니다.

 

구현

전체 코드를 Github에서 관리하기 때문에 Github Action

그리고 운영하기 전 테스트를 위해 잠시동안 AWS 프리티어를 활용하였습니다.

 

이때 S3 CodeDeploy, Beanstalk, 단순하게 스크립트 자동화하는 방식 등이 있었습니다.

 

차후 배포단계에 들어가게 되면 확장과 비용을 고려하여 AWS 뿐만 아니라 다른 클라우드 서비스를 사용할 수 있기 때문에 조금 귀찮더라도 기업의 클라우드 서비스에 덜 의존적이고, 버전이나 다른 소스 관리 등 추가적인 리소스를 제어하지 않기 위해 SSH 접속을 통한 스크립트 자동화하는 방식을 선택하게 되었습니다.

 

Github Action(CI)

코드를 통합하고 테스트 해보기 위해 우선 github action을 사용했습니다.

 

무료 계정 또한 Action을 지원하고 있어 크게 무리없이 Main 브랜치에 통합되어 동작하는 것을 확인해볼 수 있었습니다.

 

이 외에도 preview, 단위 테스트 등 여러 테스트나 환경을 확인해볼 수 있다는 것도 알게 되었고 차후에 적용해볼 예정입니다.

 

추가적인 문제로는 npm start를 사용하게 되면 실제 코드가 포그라운드로 동작하게되어 완료지않는 문제가 발생하게 되었고 이후 wait-on을 통해 확인하는 커맨드를 넣어 종료하게 되었습니다.

name: strapi-ci

on:
  workflow_dispatch:
    inputs:
      logLevel:
        description: 'Log level'     
        required: true
        default: 'warning'
      tags:
        description: 'Test scenario tags'
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout 코드
        uses: actions/checkout@v2

      - name: Node.js 버전 설정
        uses: actions/setup-node@v2
        with:
          node-version: 18

      - name: 종속성 설치
        run: npm install

      - name: 빌드
        run: npm run build

      - name: 프로젝트 실행
        run: npm start
        env:
          ADMIN_JWT_SECRET: ${{ secrets.ADMIN_JWT_SECRET }}
          API_TOKEN_SALT: ${{ secrets.API_TOKEN_SALT }}
          APP_KEYS: ${{ secrets.APP_KEYS }}
          DATABASE_CLIENT: ${{ secrets.DATABASE_CLIENT }}
          DATABASE_FILENAME: ${{ secrets.DATABASE_FILENAME }}
          JWT_SECRET: ${{ secrets.JWT_SECRET }}
          TRANSFER_TOKEN_SALT: ${{ secrets.TRANSFER_TOKEN_SALT }}

 

무중단 배포

무중단 배포를 위해 인스턴스에서 추가적인 작업이 필요했고 가장 간단한 방법은 pm2를 통해 프로세스를 관리하는 것이었습니다.

이를 위해 pm2 추가 설정이 필요했고 공식문서를 참조한 결과는 다음과 같습니다.

module.exports = {
  apps: [
    {
      name: "app",
      script: "npm",
      args: "start",
      instances: "max",
      exec_mode: "cluster",
    },
  ],
};

프로세스가 교체될 때 다운타임이 발생한다는 포스트를 읽게 되었지만 현재 단계에선 크게 신경쓸일이 없었고 배포시 최신 코드를 반영할 수 있도록, 작업이 반복되는 일이 없도록 만드는게 가장 큰 목표였습니다.

 

만약 프로세스가 교체될 때 문제가 발생하게 된다면 딜레이 등이 필요할 수도 있다는 것을 알게 되었습니다.

PM2를 활용한 Node.js 무중단 서비스하기

Process Manager | Strapi Documentation

아래와 같이 스크립트를 작성하였습니다.

 

  • jobs → 작업의 단위 여러 개의 job이 있다면 서로 격리된 환경에서 돌아간다. job끼리 순차적인 실행이 가능하며 하위에 실제 실행되는 명령어를 작성하는 steps를 가질 수 있다.
  • steps → 실행되는 단계로 같은 환경 단위에서 실행되며 steps끼리 순차적으로 실행될 수 있다.

 

이후 작업을 분리하여 더 구조화할 예정입니다.

# Workflow 이름
name: strapi-ci

# Workflow를 트리거하는 이벤트들을 정의합니다.
on:
  # 수동으로 workflow를 실행하기 위한 설정으로, 로그 레벨과 테스트 시나리오 태그 입력 가능
  workflow_dispatch:
    inputs:
      logLevel:
        description: '로그 레벨'
        required: true
        default: 'warning'
      tags:
        description: '테스트 시나리오 태그'
  
  # main 브랜치에 push 이벤트가 발생할 경우 자동으로 workflow를 실행합니다.
  push:
    branches:
      - main
  
  # main 브랜치에 대한 pull_request 이벤트가 발생할 경우 자동으로 workflow를 실행합니다.
  pull_request:
    branches:
      - main

jobs:
  # 프로젝트 빌드와 테스트를 위한 Job 정의
  build-and-test:
    # 빌드를 실행할 실행 환경을 정의합니다.
    runs-on: ubuntu-latest

    steps:
      # Step: 코드 저장소를 체크아웃합니다.
      - name: Checkout 코드
        uses: actions/checkout@v3

      # Step: Node.js 환경 설정
      - name: Node.js 버전 설정
        uses: actions/setup-node@v3
        with:
          node-version: 18

      # Step: 프로젝트 종속성 설치
      - name: 종속성 설치
        run: npm install

      # Step: 프로젝트 빌드
      - name: 빌드
        run: npm run build

      # Step: 프로젝트 실행 테스트
      - name: 프로젝트 실행 테스트
        run: npm start & npx wait-on ${{ secrets.CHECK_URL }}
        env:
          ADMIN_JWT_SECRET: ${{ secrets.ADMIN_JWT_SECRET }}
          API_TOKEN_SALT: ${{ secrets.API_TOKEN_SALT }}
          APP_KEYS: ${{ secrets.APP_KEYS }}
          DATABASE_CLIENT: ${{ secrets.DATABASE_CLIENT }}
          DATABASE_FILENAME: ${{ secrets.DATABASE_FILENAME }}
          JWT_SECRET: ${{ secrets.JWT_SECRET }}
          TRANSFER_TOKEN_SALT: ${{ secrets.TRANSFER_TOKEN_SALT }}

      # Step: 프로젝트를 원격 서버에 SSH로 배포
      - name: 원격 접속 배포
        uses: appleboy/ssh-action@v0.1.6
        with:
          host: ${{ secrets.AWS_SSH_HOST }}        # 원격 서버의 호스트명 또는 IP 주소 
          username: ${{ secrets.AWS_SSH_USERNAME }}    # SSH 인증을 위한 사용자 이름
          password: ${{ secrets.AWS_SSH_PASSWORD }}    # SSH 인증을 위한 비밀번호
          port: 22                              # SSH 포트 
          script: |
            cd strapi-demo                      # 원격 서버에서 프로젝트 디렉토리로 이동
            git pull                            # Git 저장소에서 최신 변경사항 가져오기
            npm ci                              # CI를 위한 의존성 고려
            npm build                           # 프로젝트 빌드
            pm2 reload ecosystem.config.js      # pm2를 사용하여 애플리케이션 재시작 (Node.js 애플리케이션용 프로세스 관리자)

 

환경 문제

strapi를 사용하며 배포관련 문서를 읽어봤을 때 권장되는 메모리크기는 2GB였습니다. 하지만 t2.micro를 사용하면서 1GB뿐인 메모리를 사용하며 pm2까지 작동시키기엔 가끔식 메모리 공간이 부족한 문제가 발생하였습니다.

따라서 디스크를 사용하여 속도가 조금 부족할 수 있으나 정상적인 동작을 위해 디스크 공간과 실제 메모리 공간을 스왑시켰고 정상적으로 작동하는 것을 확인하였습니다.

https://docs.strapi.io/dev-docs/deployment/amazon-aws

  • 2GB의 렘이 필요하기 때문에 1GB + 2GB로 확장

 

회고

정말 간단하게 배포하였지만, CodeDeploy, CodePipeline 혹은 Beanstalk같은 PaaS를 사용할 때 필요한 세부적인 설정과 해당 설정을 했을 때 어떤 전략을 사용하여 배포되고 어떤 문제가 발생할 수 있는지, 어떤 전략을 사용하는게 서비스에 더 적합한지 확인해보고 이해할 수 있게 되었습니다.

Difference between rolling, rolling with additional batch and immutable deployments in AWS?

 

Difference between rolling, rolling with additional batch and immutable deployments in AWS?

I am using Elastic Beanstalk to handle my deployments. I read the explanation for these deployment options but it was not quite clear to me. Could someone explain it in simpler words?

stackoverflow.com