GitHub Actions로 CI/CD 구축하기

스르륵·2024년 3월 25일
0

개발을 하다보면 코드를 작성하는 것도 중요하지만 작성한 코드를 배포하는 것도 아주 중요하다.
원격 저장소에 코드를 푸쉬하고, 빌드하고, 컨테이너를 쓴다면 이미지로 만들고 다시 이미지 공유하고..
코드만 작성하고 끝이 아니고 그 후 작업이 산적해있다.

GitHub Actoins로 자동화하기

그래서 쉽고 간단하게 GitHub에서 공식적으로 지원하는 CI/CD 툴인 GitHub Actions를 통해 배포 파이프라인을 구성해보았다.
GitHub Actions는 YAML파일로 전체 워크플로우를 정의하고 레포지토리에 업로드하면 해당 파일을 기반으로 파이프라인을 실행시켜 준다. 레포지토리의 Actions 탭을 들어가면 GitHub Actions를 실행하거나, 실행된 내역들을 살펴볼 수 있다.

먼저 자동화 파이프라인을 구성하기 위해 필요한 단계들을 나눠보았다.

  • main 브랜치에 대한 PR이 만들어지거나, 커밋을 push하면 워크플로우 트리거 실행
  • 자바로 작성했기 때문에 어플리케이션 빌드
  • 도커 이미지 빌드
  • 이미지를 컨테이너로 배포
    실제로 작성한 워크플로우는 이것 보다 세분화 되어있지만 결국 기본적으로 자동화 해야 할 부분은 이렇게 나눌 수 있을 것 같다.

GitHub Actions는 워크플로우의 job을 실행하기 위해 runner를 실행시키고 그 안에서 작업을 하게된다. 그래서 별개의 인스턴스로 실행되기 때문에 이전에 작업한 것을 다음 runner로 옮기지 못하면 이어서 작업을 할 수 없을 수 있다. 현재 runner에서 빌드한 파일을 다음 runner로 옮기는 방법이 있는지는 모르겠지만 이번에는 간단하게 쓰면 되니까 하나의 runner에서 빌드, 도커 이미지 빌드까지 하도록 했다.
그리고 다음 runner에서 배포할 서버로 접속하여 배포하도록 하는 두 단계로 구성했다.

YAML로 파이프라인 정의하기

Workflow

자동화된 전체 프로세스로 하나 이상의 job으로 구성되며 Event에 의해 트리거됨. GitHub 레포지토리의 .github/workflows 디렉토리 아래에 YAML 파일로 저장하면 자동으로 인식한다.

Event

workflow의 트리거가 되는 것. commit push, PR 생성, merge 등 다양한 조건에 의해 파이프라인이 실행되도록 할 수 있다.

Job

여러 step으로 구성된 작업. 단일 가상 환경(runner)에서 실행되며 다른 job과 병렬적으로 실행될 수도, 의존관계를 가질 수도 있다.

Step

순차적으로 실행되는 프로세스의 단위. 실제 명령어는 여기 단계에 작성하게 된다. Job 내에서는 step이 순차적으로 실행된다.

Runner

위에서 정의된 모든 작업을 실행하는 인스턴스. airflow의 worker나 k8s의 pod 등과 같은 것

이렇게 직접 모든 작업을 job으로 정의하고 사용할 수도 있지만 marketplace에서 미리 정의된 action들을 가져다가 사용할 수도 있다.

직접 작성하기

name: CI/CD
env:
  DOCKER_REGISTRY: ghcr.io/gnlenfn/egomoya

on:
  push:
  pull_request:
    branches:
      - main

맨 위에는 이 workflow의 이름과 event가 정의된다. 그리고 workflow 내에서 사용할 환경변수도 정의할 수 있는데 이 부분은 기본 변수들도 있고 해서 따로 정리가 좀 필요한 것 같다.
우선 main 브랜치에 push, PR이 있을 경우 실행되도록 정의했다.

CI job

jobs:
  build-app:
    runs-on: ubuntu-latest
    outputs:
      docker-image-tag: ${{ steps.set-output-docker-image-tag.outputs.OUTPUT_DOCKER_IMAGE_TAG }}
    steps:
      # SOURCE 단계 - 저장소 Checkout
      - name: Checkout-source code
        uses: actions/checkout@v3

      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'adopt'

      # Gradle 실행권한 부여
      - name: Grant execute permission to gradlew
        run: chmod +x ./gradlew

      # Spring boot application 빌드
      - name: Build with gradle
        run: |
          ./gradlew clean 
          ./gradlew build -x test
        

      - name: Set Seoul timezone
        uses: szenius/set-timezone@v1.2
        with:
          timezoneLinux: "Asia/Seoul"

      # GitHub Container Registry 로그인
      - name: Login GitHub Container Registry
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{github.actor}}
          password: ${{secrets.GHCR_TOKEN}}

      - name: Create Docker Image Tag
        run: |
          echo "DOCKER_IMAGE_TAG=$(date +%Y%m%d-%H%M%S)_$GITHUB_RUN_NUMBER" >> $GITHUB_ENV

      - name: Set Docker Image Tag
        id: set-output-docker-image-tag
        run: |
          echo "OUTPUT_DOCKER_IMAGE_TAG=$DOCKER_IMAGE_TAG" >> $GITHUB_OUTPUT

      # docker image build & push
      - name: Build Docker image
        run: |
          docker build --build-arg VERSION=$DOCKER_IMAGE_TAG -t $DOCKER_REGISTRY:$DOCKER_IMAGE_TAG .      

      - name: Push to Github Container Registry
        run: |
          docker push $DOCKER_REGISTRY:$DOCKER_IMAGE_TAG

다음은 CI 과정을 정의한 부분이다. 나는 CI job과 CD job 2개로 구성된 workflow를 만들었다.
gradle 빌드와 도커 이미지 빌드 그리고 레지스트리에 push까지 하도록 하는 작업이다.
나는 docker hub 대신 GitHub Container Registry를 사용했다.

CD job

  deploy-ncp:
    runs-on: ubuntu-latest
    needs: [ build-app ]
    steps:
      - name: Connect to NCP & Execute Application
        env:
          DOCKER_IMAGE_TAG: ${{ needs.build-app.outputs.docker-image-tag }}
        uses: appleboy/ssh-action@v0.1.6
        with:
          host: ${{ secrets.NCP_HOST }}
          username: ${{ secrets.NCP_USERNAME }}
          key: ${{ secrets.NCP_SSH_KEY }}
          port: ${{ secrets.NCP_SSH_PORT }}
          script: |
            docker stop $(docker ps -a -q) 
            docker rm $(docker ps -a -q) 
            docker pull ${{ env.DOCKER_REGISTRY }}:${{ env.DOCKER_IMAGE_TAG }}
            docker run -d -p 8080:8080 --name backend ${{ env.DOCKER_REGISTRY }}:${{ env.DOCKER_IMAGE_TAG }}

마지막으로 네이버 클라우드에 생성한 서버로 접속하여 컨테이너를 실행하는 작업니다.

변수 사용

위에서 작성한 내용에는 공개 저장소에 올라가면 안되는 정보들이 있다. 서버 접속을 위한 ssh key라든지, 도커 레지스트리 접속을 위한 토큰 등 민감 정보들이 포함되어 있다. 이러한 내용들을 직접 작성하지 않고 환경 변수로 처리해서 안전하게 사용할 수 있다.

settings의 Secrets - Actions로 들어가면 변수를 추가할 수 있다. 이때 변수 이름에 접미사로 GITHUB_을 사용할 수 없는데 default variable에 해당하는 값들이기 때문에 사용할 수 없다.

그리고 YAML 파일에서는 {{ secrets.변수이름 }} 이런식으로 불러와서 사용할 수 있다.

이외에도 이전 job에서 변수를 불러오기, runner 공통 변수로 저장하기 등 다양한 방법으로 변수를 활용할 수 있다.


직접 CI/CD를 위한 솔루션들을 구축할 필요 없이 GitHub에서 제공하는 GitHub Actions로 CI/CD 파이프라인을 구성해보았다. 지금이야 혼자 개발하니 자동화가 되어있지 않더라도 크게 불편하지 않을 수 있지만 동료 개발자가 생긴다면 매번 작업하지 않아도 되기 때문에 편할 것 같다.
또한 지금 테스트 부분이 빠져 있는데, 테스트하는 job 까지 추가한다면 더 안전한 배포 과정이 될 수 있을 것이다.

마지막으로 이렇게 자동화된 프로세스가 갖춰지면 다른 작업에 시간 쓸 필요 없이 개발에만 집중하면 될 것 같아서 뿌듯했다. 시간이 부족한 해커톤을 진행중인 만큼 아직 기획 단계로 개발할 일이 많이 없을때 환경 구축으로 해놓기에 딱인 작업이었다.

profile
기록하는 블로그

0개의 댓글