Git Actions를 활용한 CI/CD 구축

Kim Hyen Su·2024년 7월 25일

대장장이

목록 보기
1/6
post-thumbnail

# GGB # CI/CD # Git Actions


개요

개발 전 배포까지 염두하고 개발하기 위해서 CI/CD 기능 구현을 진행하였으며, 해당 기능은 Jenkins를 활용하여 연동하였습니다. 하지만, AWS 프리티어 수준의 CPU는 성능이 매우 낮아 별다른 구현이 없음에도 배포 시간이 2분 정도 소요되는 것을 확인했습니다. 이로 인해, 서버 외부에서 CI/CD 구축할 것이 나을 것이라고 판단하여 GitHub Actions를 활용한 CI/CD 환경 구축을 진행했습니다.

GitHub Actions를 활용하여 CI/CD 서버를 구축하는 과정에 대해서 정리한 포스팅입니다.

CI/CD란?

CI/CD 란, "지속적인 통합(Continous Integration)"과 "지속적인 배포(Continous Deplyment)"를 의미합니다.

지속적인 통합은 코드 변경사항이 발생 시마다 자동으로 빌드 → 테스트 → 통합(병합)을 수행합니다. 테스트 과정에서 conflict 처리 및 compile error를 처리할 수 있습니다.

지속적인 배포는 통합된 코드를 자동으로 운영 서버에 배포하는 프로세스를 말합니다. 이로 인해, 새로운 기능과 버그 수정 사항이 실제 서비스에 빠르게 반영되므로 사용자 경험 향상이 됩니다. 또한, 사용자 피드백을 수집하고 제품을 개선하는 속도를 향상시킬 수 있다는 장점이 있습니다.

GitHub Actions란?

GitHub 플랫폼에서 제공하는 지속 통합/배포 서비스를 의미합니다. 이를 사용하면 코드의 통합과 배포 프로세스를 자동화해줌으로써 개발 생산성을 높여줍니다.

또한, github actions에서는 build용 가상 머신인 github-hosted runners를 제공해줍니다. 가상 머신의 동작은 다음과 같습니다.
1. 레포지토리를 로컬에 복사합니다.
2. 테스트를 위한 프로그램을 설치합니다.
3. 코드를 검사하는 명령을 실행해줍니다.

가상 머신을 사용하기 위해서는 workflow에 대한 script를 작성하고 runs-on 기능을 사용하여 가상 머신의 유형(Unbuntu, Windows, MacOS)을 지정해줘야 합니다.

구성 요소

  • workflow

    • 하나 이상의 작업을 구성 가능한 자동화된 프로세스
    • 레포지토리 내 YAML 파일에 의해 정의됩니다.
    • YAML 파일은 root directory에 위치하며, .github/workflows 내에 정의됩니다.
    • 하나의 레포지토리 내 여러 개의 workflow가 존재할 수 있고, 각각 다른 방식으로 작업 수행이 가능합니다.
  • Event

    • workflow를 실행하기 위한 Trigger의 역할을 합니다.
    • 예를 들어, PR, Push, Merge 등의 event 발생 시 작업이 수행되도록 해줍니다.
  • Jobs

    • 동일한 Runner에서 실행되는 workflow의 Step 집합을 의미합니다.
    • workflow에서 특정 event에 따라서 처리하는 프로세스를 구분하고 정의할 수 있습니다.
    • 각 step은 shell에서 명령하는 동작과 동일한 단위로 실행됩니다.
    • 각각의 step들을 정의한 순서대로 실행되며, step 별로 동일한 환경변수를 지정할 수 있어 데이터 공유가 가능합니다. 의존 관계에 따른 실행 또는 병렬 처리가 가능합니다.
  • Actions

    • 반복되는 코드를 모듈이나 함수로 관리하는 것과 같이 복잡하고 자주 사용되는 작업을 정의할 수 있는 단위를 말합니다.
    • workflow 내에서 자주 반복되는 스크립트를 미리 정의하여 효율적으로 관리가 가능합니다.
  • Runner

    • action 시 workflow를 실행하는 서버를 말합니다.
    • 각 Runner는 한 번에 하나의 Job 실행이 가능합니다.
    • Github 는 workflow 실행을 위해서 Ubuntu Linux, Microsoft Windows 및 macOS Runner를 제공합니다.
  • env

    • 특정 값을 환경 변수로 설정하기 위해 사용됩니다.
    • 등록된 환경변수 사용 방법 : ${{env.등록한_환경변수명}}

CI/CD 설계

AS-IS

  • production(운영) : develop → PR open → main → PR close → CI/CD
  • development(개발) : 기능 구현 → PR open → develop → PR close → CI/CD

TO-BE

  • production(운영) : develop → PR open → main → PR close → CI & 수동 배포
  • development(개발) : feature → PR open → develop → PR close → CI/CD
    ci script 작성
  • feature(기능 구현) : 기능 구현 → push → build + test

기존의 방식(AS-IS)의 문제점은 PR을 통해 코드 리뷰를 거친다고는 하지만, 분명히 놓치는 문제가 발생할 수 있습니다. 이로 인해 문제가 발생한 코드가 development 서버에 자동 통합 & 배포되면, 다시 PR을 작성해야 하므로 비용이 큽니다.

반면에, 개선 방식(TO-BE)으로 진행하게 되면 feature(기능구현) branch를 push할 때, build 및 테스트 코드를 진행하기 때문에 PR을 close 하기 전에 build 확인 및 테스트 코드 정상 동작 여부를 확이할 수 있기 때문에 좀 더 개발 비용이 적게 들어 빠르게 배포가 가능합니다.

따라서, TO-BE 방식의 흐름으로 workflow 스크립트를 작성하겠습니다.

workflow script 작성

root directory에 .github/workflows 를 생성 후 YAML 파일을 통해 script를 작성해줍니다.

우선, 기능 구현 → build & test 단계의 script부터 작성하겠습니다.

ci.cd.feat.yml

# workflow 이름 지정
name: CI/CD

# workflow trigger
on:
  push:
    branches:
      - feature/*

env:
  DOCKER_IMAGE_NAME: projectggb/back-end-dev:0.0.1
  DOCKER_CONTAINER_NAME: ggb-back

jobs:
  ci:
    runs-on: ubuntu-latest
    environment: feature
    steps:
      # Runner 에 repository 에 저장된 코드 복사
      - name: Checkout code
        uses: actions/checkout@v2

        # SET UP JDK 17
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'

		 # APPLICATION-SECRET.YML 등록
      - name: Create application-secret.yml
        run: |
          cat << EOF > src/main/resources/application-secret.yml
          spring:
            servlet:
              multipart:
                enabled: true
                max-file-size: 20MB
                max-request-size: 20MB
            cloud:
              aws:
                credentials:
                  access-key: ${{ secrets.AWS_ACCESS_KEY }}
                  secret-key: ${{ secrets.AWS_SECRET_KEY }}
                region:
                  static: ap-northeast-2
                s3:
                  bucket: ${{ secrets.AWS_S3_BUCKET }}
          EOF

        # BUILD WITH GRADLE
      - name: Build with gradle
        run: ./gradlew clean build
  • event

    • feature/* 형태의 브렌치가 github에 push 시 정의한 jobs가 동작하게 됩니다.
  • jobs

    • ci : job 단위 명칭을 의미.
      • runs-on : 필수 속성, 가상 머신의 유형을 지정
      • environment : github에 등록한 환경변수 사용을 위해 environment명을 지정
      • steps : 진행할 작업 목록
        • name : step(작업) 하나의 단위명
        • users : 사용할 actions(기존에 사용할 기능을 정의해놓은 외부 스크립트) 지정
        • with : actions 실행 시 사용할 변수 지정
        • run : 실행할 스크립트를 직접 지정
          • | : 여러 줄의 스크립트(명령어)를 작성 시 사용.
  • steps

    1. 가상 머신에 repository에 저장된 코드를 복사합니다.
    2. 가상 머신 내에서 빌드 시 사용할 JDK를 설정해줍니다.(JDK 17 SETUP)
    3. Build 시 필요한 민담정보를 가진 application-secret.yml 파일을 생성해줍니다.
    4. Build 및 Test를 수행합니다.

작성 후 테스트 결과, 다음과 같이 feature/* 브렌치 github에 push 시 build & test가 정상적으로 완료되었습니다.

ci.cd.dev.yml

그 다음으로 개발(development) 서버에 대한 배포 workflow 입니다.

# workflow 이름 지정
name: CI/CD

# workflow trigger
on:
  pull_request:
    branches:
      - develop
    types:
      - closed

env:
  DOCKER_IMAGE_NAME: projectggb/back-end-dev:0.0.1
  DOCKER_CONTAINER_NAME: ggb-back

jobs:
  ci:
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    environment: development
    steps:
        # Runner 에 repository 에 저장된 코드 복사
      - name: Checkout code
        uses: actions/checkout@v2

        # SET UP JDK 17
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'

		 # APPLICATION-SECRET.YML 등록
      - name: Create application-secret.yml
        run: |
          cat << EOF > src/main/resources/application-secret.yml
          spring:
            servlet:
              multipart:
                enabled: true
                max-file-size: 20MB
                max-request-size: 20MB
            cloud:
              aws:
                credentials:
                  access-key: ${{ secrets.AWS_ACCESS_KEY }}
                  secret-key: ${{ secrets.AWS_SECRET_KEY }}
                region:
                  static: ap-northeast-2
                s3:
                  bucket: ${{ secrets.AWS_S3_BUCKET }}
          EOF

        # BUILD WITH GRADLE
      - name: Build with gradle
        run: ./gradlew clean build

        # DOCKER BUILDX 설정 - Docker 빌드 기능을 확장한 도구
      - name: Set up docker buildx
        uses: docker/setup-buildx-action@v1

        # DOCKERHUB LOGIN
      - name : Login to dockerhub
        uses: docker/login-action@v1
        with:
          username: ${{secrets.DOCKERHUB_USERNAME}}
          password: ${{secrets.DOCKERHUB_TOKEN}}

        # DOCKERHUB IMAGE BUILD AND PUSH
      - name: Build and push docker image
        uses: docker/build-push-action@v2
        with:
          context: .
          push: true
          tags: ${{env.DOCKER_IMAGE_NAME}}
  cd:
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    environment: development
    needs: ci
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Copy files via SSH
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST}}
          username: ${{ secrets.EC2_USERNAME}}
          key: ${{secrets.SSH_PRIVATE_KEY}}
          script: |
            docker pull ${{env.DOCKER_IMAGE_NAME}}
            docker stop ${{env.DOCKER_CONTAINER_NAME}}
            docker rm ${{env.DOCKER_CONTAINER_NAME}}
            docker run -d --name ${{env.DOCKER_CONTAINER_NAME}} -p 8081:8080 ${{env.DOCKER_IMAGE_NAME}}
            docker image prune -af
  • develop branch에 Pull Request가 close 된 경우, 실제 CI/CD 배포가 동작합니다.

  • ci

    1. 가상 머신에 repository에 저장된 코드를 복사합니다.
    2. 가상 머신 내에서 빌드 시 사용할 JDK를 설정해줍니다.(JDK 17 SETUP)
    3. Build 시 필요한 민담정보를 가진 application-secret.yml 파일을 생성해줍니다.
    4. Build 및 Test를 수행합니다.
    5. Docker BuildX 확장 도구를 설정해줍니다.
    6. DockerHub 로그인을 해줍니다.
    7. Docker Image 빌드 및 DockerHub에 Push 해줍니다.
  • cd

    1. 가상 머신에 repository에 저장된 코드를 복사합니다.
    2. SSH에 접속하여 실행할 코드를 입력해줍니다.

작성 후 테스트 결과, 다음과 같이 develop 브렌치에 pull request 시 CI/CD가 정상적으로 완료되었습니다.

ci.cd.prod.yml

마지막으로, 운영(Production) 서버에 대한 배포 workflow 입니다.

# workflow 이름 지정
name: CI/CD

# workflow trigger
on:
  pull_request:
    branches:
      - main
    types:
      - closed

env:
  DOCKER_IMAGE_NAME: projectggb/back-end-prod:0.0.1

jobs:
  ci:
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    environment: production
    steps:
        # Runner 에 repository 에 저장된 코드 복사
      - name: Checkout code
        uses: actions/checkout@v2

        # SET UP JDK 17
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'

        # APPLICATION-SECRET.YML 등록
      - name: Create application-secret.yml
        run: |
          mkdir -p src/main/resources
          echo "${{secrets.SECRET_YML}}" | base64 --decode > src/main/resources/application-secret.yml

        # BUILD WITH GRADLE
      - name: Build with gradle
        run: ./gradlew clean build

        # DOCKER BUILDX 설정 - Docker 빌드 기능을 확장한 도구
      - name: Set up docker buildx
        uses: docker/setup-buildx-action@v1

        # DOCKERHUB LOGIN
      - name : Login to dockerhub
        uses: docker/login-action@v1
        with:
          username: ${{secrets.DOCKERHUB_USERNAME}}
          password: ${{secrets.DOCKERHUB_TOKEN}}

        # DOCKERHUB IMAGE BUILD AND PUSH
      - name: Build and push docker image
        uses: docker/build-push-action@v2
        with:
          context: .
          push: true
          tags: ${{env.DOCKER_IMAGE_NAME}}
  • main branch에 Pull Request가 close된 다음 ci가 수행됩니다.
  • ci
    1. 가상 머신에 repository에 저장된 코드를 복사합니다.
    2. 가상 머신 내에서 빌드 시 사용할 JDK를 설정해줍니다.(JDK 17 SETUP)
    3. Build 시 필요한 민담정보를 가진 application-secret.yml 파일을 생성해줍니다.
    4. Build 및 Test를 수행합니다.
    5. Docker BuildX 확장 도구를 설정해줍니다.
    6. DockerHub 로그인을 해줍니다.
    7. Docker Image 빌드 및 DockerHub에 Push 해줍니다.

작성 후 테스트 결과, 다음과 같이 main 브렌치에 pull request 시 CI가 정상적으로 완료되었습니다.

CI : Trouble-Shooting

⚠️ Java SET UP 중 오류 발생

  • 문제 상황 : Input required and not supplied: distribution 이라는 에러 발생.
  • 원인 : actions/setup-java@v3 부터는 distribution 속성값을 필수로 지정해줘야 하지만, 하지않아서 발생한 오류.
  • 해결 : distribution: temurin 속성값 지정.
    • temurin : 배포된 actions/setup-java 중 안정적이고, 널리 사용되는 옵션입니다.

⚠️ Build 중 오류 발생

  • 문제상황 : Build Fail
  • 원인 : aws 관련 설정이 필요하지만, 민감 정보로 인해 application-secret.yml을 별도로 생성하여 관리하며, 해당 파일은 .gitignore에 등록해놓았기 때문에 gradle build 시 해당 설정값을 읽어오지 못해서 발생한 오류.
  • 해결 : application-secret.yml 파일을 등록해주는 코드 추가.
        # APPLICATION-SECRET.YML 등록
      - name: Create application-secret.yml
        run: |
          cat << EOF > src/main/resources/application-secret.yml
          spring:
            servlet:
              multipart:
                enabled: true
                max-file-size: 20MB
                max-request-size: 20MB
            cloud:
              aws:
                credentials:
                  access-key: ${{ secrets.AWS_ACCESS_KEY }}
                  secret-key: ${{ secrets.AWS_SECRET_KEY }}
                region:
                  static: ap-northeast-2
                s3:
                  bucket: ${{ secrets.AWS_S3_BUCKET }}
          EOF

⚠️ Test 중 오류 발생

  • 문제 상황 : Test 시 aws 관련 오류가 발생하면서, Build Fail

  • 원인 : gradle build 및 test 실행 시 Spring Context 내 bean들을 전부 등록하는데, test 코드의 경우, ActiveProfiles="test" 설정으로 인해서 application-test.yml 파일을 참조하고 있기 때문에 TestService와 같은 클래스 bean 등록 시 필요한 속성값들을 읽어오지 못해 발생하는 오류였습니다.

  • 해결 : application-test.yml 파일에도 관련 설정들을 추가해줍니다. 만약, 민감 정보가 포함되어 있는 경우, 해당 부분에는 임시값을 넣어줍니다. - 실제 테스트 코드를 작성한 상황이 아니기 때문에 bean만 등록시켜주도록만 해주면, 오류가 발생하지 않게 됩니다.

CD : Trouble-Shooting

⚠️ 배포 중 secrets 값을 읽어오지 못하는 문제 발생

  • 문제 상황 : cd 관련 script 내 정의한 secrets 값을 가져오는 부분에서 값을 읽어오지 못하는 오류가 발생.
  • 원인 : environment를 설정해주지 않아서, 값을 읽어오지 못함.
  • 해결 : environment를 설정해주어 secrets 값을 읽어와 cd script 진행 시 읽어오도록 수정함.
    	  cd:
      runs-on: ubuntu-latest
      environment: development

⚠️ Docker 권한 오류

  • 문제 상황 : SSH 접속 후 docker script 실행 시 권한 문제로 오류 발생.
  • 원인 : EC2 내 docker demon socket(/var/run/docker.sock)에 접근 권한이 없기 때문에 발생함.
  • 해결 :
    1. 사용자를 docker 그룹에 추가
    	sudo usermod -aG docker $USER
    1. 현재 세션에서 임시로 Docker 그룹 적용
    	 newgrp docker
    1. Docker 서비스 재시작
    	 sudo systemctl restart docker

마무리

위와 같이 GitHub Actions workflow 스크립트를 작성하여 실제 EC2 인스턴스에 서버를 CI/CD 방식으로 배포하였습니다. 이를 진행하면서 배포 wokflow에 대해서 다시 한번 고민하며 script를 작성할 수 있었습니다. 추가로 개선해야할 내용과 학습해야할 내용들을 정리해가며 포스팅해 나가겠습니다.

profile
백엔드 서버 엔지니어

0개의 댓글