최근에는 젠킨스 대신 GitHub Action을 이용해 CI/CD 파이프라인을 구성하는 사례가 많아졌습니다. GitHub에 코드가 이미 호스팅되어 있는 환경에서는 설정이 간단하고 GitHub와의 연동이 원활하기 때문에 효과적입니다. 이번 글에서는 이전에 작성한 Git Hook 기반 작업을 따라하셨다면 확장하여 간단하게 적용하는 방법(예제 1)과 PM2를 사용하여 무중단 배포를 고려한 CI/CD 설정 예제(예제 2)를 공유하고, 각 요소에 대한 상세 설명과 함께 보완할 부분에 대해 설명해 보겠습니다.

위의 링크가 걸린 글을 따라하신 적이 없다면 예제 1은 보지 않으셔도 됩니다!

GitHub Action 주요 구성 요소

먼저 GitHub Action 워크플로우 파일은 YAML 형식으로 작성되며, 각 요소는 다음과 같은 역할을 합니다.

  • name
    워크플로우의 이름을 지정합니다. 예제에서는 name: CI로 설정하여 CI 파이프라인임을 명시합니다.

  • on
    워크플로우가 실행될 이벤트를 정의합니다.
    예를 들어,

on:
  push:
    branches: [ "master" ]

는 master 브랜치에 push 이벤트가 발생할 때 워크플로우가 실행되도록 합니다.
on: [push, pull_request]
혹은

on:
  push:
    branches: [master]
  pull_request:
    branches: [master]

이런 방식도 가능합니다.

  • jobs
    하나의 워크플로우 내에서 실행할 여러 작업(job)을 정의하는 부분입니다. 각 job은 독립적으로 실행되며, 필요에 따라 다른 job에 의존성을 가질 수 있습니다.

  • runs-on
    각 job이 실행될 환경(예: ubuntu-latest)을 지정합니다.

  • steps
    각 job 내에서 순차적으로 실행될 단계(step)를 정의합니다. 각 step은 다음과 같은 방식으로 구성할 수 있습니다:

  • uses
    이미 만들어진 GitHub Action(예: actions/checkout@v3, actions/setup-node@v3)을 재사용할 때 사용합니다. 이 방식은 코드의 중복을 줄이고, 신뢰성 있는 작업을 빠르게 적용할 수 있는 장점이 있습니다.

  • run
    직접 실행할 셸 명령어를 지정할 때 사용합니다. 스크립트 명령어를 여러 줄로 작성할 수 있으며, 환경설정, 테스트 실행, 배포 작업 등 다양한 작업에 활용됩니다.

  • needs
    하나의 job이 실행되기 전에 다른 job의 성공 여부를 확인할 때 사용합니다. 예제에서는 deploy job이 실행되기 전에 test job이 성공해야 하도록 needs: [ test ]로 설정되어 있습니다.

레포지토리 별 secrets 설정 방법

.yml 파일의 위치

${Project Root}/.github/workflows/CI.yml 와 같은 경로에 해당 파일을 만들면 자동으로 인식합니다.

기본 구성 개요

제가 작성한 CI/CD 워크플로우는 크게 두 가지 job으로 구성되어 있습니다.

  1. test job
  • 목적: 코드를 checkout하고, 의존성을 설치한 후 테스트를 실행하여 문제를 사전에 발견합니다.
  1. deploy job
  • 목적: test job이 성공한 경우 원격 서버로 배포 작업을 진행합니다.
  • 특징: 배포 전에 SSH 키 설정과 known_hosts 등록을 통해 보안적으로 안전한 연결을 유지합니다.

예제 1: 원격 post-receive hook 호출 방식

아래 예제는 원격 서버에 미리 구성되어 있는 post-receive hook 스크립트를 호출하여 배포하는 방식입니다.

name: CI
 
on:
  push:
    branches: [ "master" ]
 
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18.17.0'
      - name: Install dependencies
        run: npm ci
      - name: Create env file
        run: |
          echo "NOTION_API_KEY=${{ secrets.NOTION_API_KEY }}" > .env
          echo "NOTION_DATABASE_ID=${{ secrets.NOTION_DATABASE_ID }}" >> .env
          echo "DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }}" >> .env
          echo "CLIENT_ID=${{ secrets.CLIENT_ID }}" >> .env
          echo "GUILD_ID=${{ secrets.GUILD_ID }}" >> .env
      - name: Run tests
        run: npm test
 
  deploy:
    runs-on: ubuntu-latest
    needs: [ test ]
    steps:
      - name: Setup SSH key
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
      - name: Setup known_hosts
        run: |
          mkdir -p ~/.ssh
          ssh-keyscan -H ${{ secrets.SSH_IP }} >> ~/.ssh/known_hosts
          chmod 644 ~/.ssh/known_hosts
      - name: Deploy to remote server
        run: ssh -i ~/.ssh/id_rsa ${{ secrets.SSH_ID }}@${{ secrets.SSH_IP }} "sh /your/path/myapp.git/hooks/post-receive"

장단점 및 주의사항

  • 장점:

    • 이미 구성된 post-receive hook을 활용하여 배포 과정이 단순합니다.
  • 단점:

    • 원격 스크립트 내부에서 발생하는 오류를 GitHub Action의 로그로 확인하기 어려워 디버깅이 어렵고, github action 입장에서는 이전의 과정들이 성공했기 때문에 post-receive shell에서 오류가 나더라도 알 수 없습니다.

그래서 저는 예제 2와 같이 전면 수정하였습니다.

예제 2: Heredoc을 사용하여 직접 명령어 실행 방식

문제 발생 시 바로 로그를 확인하고 디버깅하기 위해 배포 커맨드를 직접 명시하는 방식입니다.

name: CI
 
on:
  push:
    branches: [ "master" ]
 
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18.17.0'
      - name: Install dependencies
        run: npm ci
      - name: Create env file
        run: |
          echo "NOTION_API_KEY=${{ secrets.NOTION_API_KEY }}" > .env
          echo "NOTION_DATABASE_ID=${{ secrets.NOTION_DATABASE_ID }}" >> .env
          echo "DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }}" >> .env
          echo "CLIENT_ID=${{ secrets.CLIENT_ID }}" >> .env
          echo "GUILD_ID=${{ secrets.GUILD_ID }}" >> .env
      - name: Run tests
        run: npm test
 
  deploy:
    runs-on: ubuntu-latest
    needs: [ test ]
    steps:
      - name: Setup SSH key
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
      - name: Setup known_hosts
        run: |
          mkdir -p ~/.ssh
          ssh-keyscan -H ${{ secrets.SSH_IP }} >> ~/.ssh/known_hosts
          chmod 644 ~/.ssh/known_hosts
      - name: Deploy to remote server
        run: |
          ssh -i ~/.ssh/id_rsa ${{ secrets.SSH_ID }}@${{ secrets.SSH_IP }} << 'EOF'
            cd ${{ secrets.PROJECT_HOME }}
            git pull origin master
            npm install
            echo "NOTION_API_KEY=${{ secrets.NOTION_API_KEY }}" > /your/path/.env
            echo "NOTION_DATABASE_ID=${{ secrets.NOTION_DATABASE_ID }}" >> /your/path/.env
            echo "DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }}" >> /your/path/.env
            echo "CLIENT_ID=${{ secrets.CLIENT_ID }}" >> /your/path/.env
            echo "GUILD_ID=${{ secrets.GUILD_ID }}" >> /your/path/.env
            pm2 reload your_process_name
          EOF

장점 및 주의할 점

  • 장점:

    • 각 단계(예: git pull, npm install, 환경변수 재설정, pm2 reload)를 직접 확인할 수 있어, 실행 중 발생하는 로그를 바로 GitHub Action에서 확인할 수 있습니다.
    • 문제가 발생했을 때 빠르게 디버깅하고 원인을 파악할 수 있습니다.
  • 주의사항:

    • 여기문(Heredoc) 내 변수 치환:
      여기문에 단일 인용부호(‘EOF’)를 사용하면 내부 내용이 리터럴로 전달됩니다. 다만, GitHub Action에서는 템플릿 변수( ${{ secrets.XXX }})가 미리 치환되므로 큰 문제는 없지만, 원격 서버에서 추가적인 셸 변수 치환을 원하신다면 이 부분은 추가적으로 확인이 필요할 수 있습니다.
    • 환경변수 파일 경로:
      보안상 민감한 정보가 포함되어 있으므로 환경변수 파일 경로는 독자의 서버 환경에 맞게 /your/path/.env와 같이 변경하여 관리해야 합니다.
    • 배포 대상 경로:
      실제 서버 환경에 맞는 올바른 경로를 사용하도록 수정하세요.

GitHub Action vs 젠킨스

  • GitHub Action 장점:

    • GitHub에 코드가 호스팅되어 있으면 연동이 간편합니다.
    • 설정 파일(YAML)만으로 파이프라인을 구성할 수 있어 유지보수가 용이합니다.
    • 최신 트렌드에 맞춰 커뮤니티와 자료가 풍부합니다.
  • 젠킨스와의 차이점:

    • 젠킨스는 보다 복잡한 빌드 환경과 플러그인을 활용할 수 있으나, 설정과 유지 관리가 더 복잡할 수 있습니다.
    • GitHub Action은 GitHub 생태계와 자연스럽게 통합되어 있어 단순한 CI/CD 구축에 유리합니다.

마무리하며

이번 글에서는 GitHub Action을 이용한 CI/CD 파이프라인의 예제와 함께 각 구성 요소에 대해 자세히 설명해 보았습니다.

  • name, on, jobs, steps, needs 등
    각 요소는 워크플로우의 실행 시점, 실행 환경, 작업 순서를 결정하며, 이를 통해 효율적인 자동화가 가능합니다.

  • 배포 스크립트 내부 로깅 및 에러 처리
    직접 명령어를 실행하는 방식은 발생하는 에러를 GitHub Action 로그로 바로 확인할 수 있어 디버깅에 유리합니다.

  • 환경변수 파일의 보안
    민감한 정보가 노출되지 않도록 배포 시 환경변수 파일 경로를 독자의 환경에 맞게 /your/path와 같이 변경하여 관리하는 것이 좋습니다.

  • 여기문 내 변수 치환
    의도한 대로 변수 치환이 이루어지고 있는지 확인이 필요합니다. 여러분의 프로젝트에 맞게 위 예제를 수정하고 각 단계별 로깅 및 에러 처리를 꼼꼼히 적용해 보시길 바랍니다.