Github action caching을 활용한 CI/CD속도 개선

Always·2024년 10월 11일
1

Backend&Devops

목록 보기
6/15
post-thumbnail

💥문제 현황

진짜 우연히 Prometheus와 연동된 Grafana 서버를 들어가서, 배포한 웹사이트가 잘 동작을 하나 확인을 했는데...

10월 9일 10시 48분 경에 cpu사용량 97퍼 가 되어있는 기이한 장면을 목격하고야 말았습니다.
또한 ram으로 데이터를 올리는 page-in 과 데이터를 ram으로 내리는 page-out 도 매우 높은 것을 발견하였습니다.

  • 이 외에도 2틀 혹은 하루에 한번 정도 cpu사용률 및 page-in, page-out을 비롯한 network-traffic, page-fault rate역시 모두 높은 시간대가 있는 것을 확인

  • 이 시간대에 자원 사용 패턴을 분석하여, 어떤 애플리케이션이나 프로세스가 높은 부하를 유발하는지 파악할 필요가 있다고 판단하였습니다.

❓왜 그랬을까

왜 위처럼 cpu사용률이 급증 했을까 궁금해서 해당 시간대의 docker log를 열어보니 다음과 같습니다.

org.springframework.dao.IncorrectResultSizeDataAccessException: Query did not return a unique result: 6 results were returned

즉 채용공고가 update되는 과정에서 기존의 CompanyRepository에서 회사이름을 통해서 company를 찾는데, 이때 unique하게 반환하도록 설정되어있는데, 6개가 반환된 것입니다.
그래서 Cpu사용률이 올라간 이유를 총 3가지 정도로 생각해보았습니다.

  1. 예외가 터지면서 트랜잭션이 정상적으로 종료되지 않고, 누수가 생겨서 자원낭비 혹은 db락이 생겼다.
  2. 사람인 api를 가지고 올 때의 과정에서 네트워크 지연이 생긴다.
  3. CI/CD과정에서 생기는 오류(pr하고 머지한 기간이랑 비슷해서 생각)
  • 먼저 1의 경우는 spring data jpa를 사용하고 있기 때문에 자동으로, 예외가 터지면 트랜잭션이 끝나므로 아닙니다.
  • 2또한 사람인 APi를 가지고 오는 시간이 그렇게 길지 않았습니다.
  • 3을 테스트 하기 위해서 pr,merge를 해보았는데, 이 때 CI/CD에서 CPU사용률이 갑자기 급증하였습니다.

그래서 저는 현재 도커내에 모든 파일을 그 때 그 때 다운받아서 빌드 및 cpu를 많이 사용하는듯해서, 이를 CI/CD과정에서 Github Caching을 이용해서 해결하고자 했습니다.

🪢현재 CI/CD Workflow

현재 github action을 활용한 CI/CD 코드는 다음과 같습니다

name: Docker Image 만들기
# Event : 실행되는 시점
on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main
# 하나의 워크플로우에 1개 이상의 잡
# 잡은 병렬 수행
jobs:
  My-Deploy-Job:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          submodules: recursive
          token: ${{secrets.PAT_TOKEN}}
      - name: JDK 17 설치
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
      - name: Spring Project 빌드하기
        run: |
          cd ./server/
          chmod +x ./gradlew
          ./gradlew clean build -x test
      - name: Docker 이미지 만들고 Dockerhub에 푸시
        run: |
          docker login -u ${{secrets.DOCKER_USERNAME}} -p ${{secrets.DOCKER_PASSWORD}}
          docker build -t ${{secrets.DOCKER_REPO}}/devroute-server:${{github.sha}} .
          docker push ${{secrets.DOCKER_REPO}}/devroute-server:${{github.sha}}
      - name: EC2에서 이미지 pull 후, 컨테이너 실행
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_SSH_USER }}
          key: ${{ secrets.PRIVATE_KEY }}
          script: |
            docker kill $(docker ps -qf expose=8080) 2> /dev/null || echo 'No container running on port 8080'
            docker run -d -p 8080:8080 ${{secrets.DOCKER_REPO}}/devroute-server:${{github.sha}}

checkout -> JDK 17 설치 -> Spring Project 빌드하기->dockerfile을 통해서 이미지를 만들고, 이를 DockerHub에 푸시-> Ec2에서 이미지 pull후, dockerhub에 만든 컨테이너를 실행

  • 그런데 이 과정에서 Dockerfile에 크롤링 관련 의존성 패키지 및 파일을 다운받는데, 이 시간이 생각보다 오래 걸리는 것 같다고 판단하였습니다.
  • 이 도커 파일이나 레이어를 캐싱하여, 캐시에 있으면 굳이 다시 다운 하지 않고, 캐시에서 찾아서 사용하는 것이 훨씬 빠르겠다고 생각하였습니다.

🧐Github Cache적용

현재 github cache를 활용한 CI/CD 코드는 다음과 같습니다

name: Docker 이미지 만들기

# 이벤트: 실행되는 시점을 정의합니다
on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  My-Deploy-Job:
    runs-on: ubuntu-latest  # Ubuntu 환경에서 실행
    steps:
      - name: 코드 체크아웃  # GitHub 리포지토리의 소스 코드를 가져옵니다
        uses: actions/checkout@v3  # GitHub의 공식 액션 사용
        with:
          submodules: recursive  # 서브모듈 포함 체크아웃
          token: ${{ secrets.PAT_TOKEN }}  # Private Access Token 사용
            
      #  Dockerfile이 변경된 경우에만 캐시 저장 여부 확인
      - name: Check for Dockerfile changes
        id: dockerfileChanged
        run: |
          git fetch origin main
          git diff --quiet origin/main -- ./dev-route/Dockerfile || echo "changed=true" >> $GITHUB_OUTPUT

      - name: Docker Buildx 설치 및 설정  # Buildx 설치 및 Docker 컨테이너 드라이버 사용 설정
        uses: docker/setup-buildx-action@v2
        with:
          install: true
          driver: docker-container  # docker-container 드라이버 설정
          buildkitd-flags: --debug  # 추가 플래그 (옵션)

      - name: JDK 17 설치  # JDK 17을 설치하여 Java 환경을 설정합니다
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'  # Temurin 배포판 사용

      - name: Spring 프로젝트 빌드  # Spring 프로젝트를 빌드합니다
        run: |
          cd ./dev-route/
          chmod +x ./gradlew  # gradlew 파일에 실행 권한 부여
          ./gradlew clean build -x test  # 테스트 제외하고 빌드 실행
      
      # 항상 캐시 복원 시도 (이전 캐시가 있고, dockerfile이 변경이 안되어있으면 복원)
      - name: Docker 레이어 캐시 복원
        if: steps.dockerfileChanged.outputs.changed != 'true'
        uses: actions/cache/restore@v4
        with:
          path: /tmp/.buildx-cache  # Docker 빌드 캐시를 복원할 경로
          key: ${{ runner.os }}-buildx-${{ hashFiles('Dockerfile') }}  # 캐시 키 설정
          restore-keys: |
            ${{ runner.os }}-buildx-
      - name: Docker 이미지 생성 및 DockerHub에 푸시  # Docker 이미지를 빌드하고 DockerHub에 푸시합니다
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}  # DockerHub 로그인
          cd ./dev-route/  # dev-route 디렉토리로 이동
          docker buildx build --cache-from=type=local,src=/tmp/.buildx-cache \
            --cache-to=type=local,dest=/tmp/.buildx-cache,mode=max \
            --push \
            -t ${{ secrets.DOCKER_REPO }}/devroute-server:${{ github.sha }} .  # Docker Buildx 사용

      - name: EC2에서 이미지 pull 후 컨테이너 실행  # EC2 서버에서 Docker 이미지 pull 후 컨테이너 실행
        uses: appleboy/ssh-action@master  # EC2 서버에 SSH 접속 후 명령 실행
        with:
          host: ${{ secrets.EC2_HOST }}  # EC2 인스턴스 호스트 정보
          username: ${{ secrets.EC2_SSH_USER }}  # SSH 접속 사용자 이름
          key: ${{ secrets.PRIVATE_KEY }}  # SSH 프라이빗 키
          script: |  # EC2에서 실행할 스크립트
            docker rm -f $(docker ps -qa)  # 실행 중인 모든 컨테이너 제거
            docker pull ${{ secrets.DOCKER_REPO }}/devroute-server:${{ github.sha }}  # 새로운 Docker 이미지 pull
            docker run -d -p 8080:8080 -e "SPRING_PROFILES_ACTIVE=prod" ${{ secrets.DOCKER_REPO }}/devroute-server:${{ github.sha }}  # 새 컨테이너 실행


      
      #  Dockerfile이 변경된 경우에만 캐시 저장
      - name: Docker 레이어 캐시 저장
        if: steps.dockerfileChanged.outputs.changed == 'true'  # Dockerfile 변경 시에만 캐시 저장
        uses: actions/cache/save@v4
        with:
          path: /tmp/.buildx-cache  # Docker 빌드 캐시를 저장할 경로
          key: ${{ runner.os }}-buildx-${{ hashFiles('Dockerfile') }}  # 캐시 키 설정

순서는 다음과 같습니다
1. checkout
2. Dockerfile이 변경된 경우 changed=true
3. docker buildx 설치 및 설정
4.jdk 17 저장
5.spring project 빌드
6.레이어 캐시 복원 시도 (이전 캐시가 있고, dockerfile이 변경이 안되어있으면 복원)
7.Docker 이미지 생성 및 DockerHub에 푸시
8. EC2에서 이미지 pull 후 컨테이너 실행->
9.Dockerfile이 변경된 경우(changed==true)에만 캐시 저장

  • 3에서 buildx를 설치하고 7에서 사용하여서 로컬 캐시를 사용하여 빌드를 가속화하는 역할을 합니다.
  • 아래에는 핵심 로직이있습니다.

    깃허브에 레이어 관련 캐시가 있고, dockerfile이 변경되지 않았으면, 캐시를 복구→ dockerfile을 통해서 의존성을 다운 받고,도커 이미지를 만들 때 의존성 파일이 캐시에 있을 때, 캐시를 이용하여 이미지를 만들고 docker hub에 push→후에 이미지를 pull후에 ec2에서 컨테이너 실행→ dockerfile이 변경된 경우에는 캐시에 저장

  • 또한 cache@v2에서는 캐시 불러오기와 저장이 한번에 되어서 현재의 로직을 짜기 쉽지않은데, cache@v4에는 cache/restore와 cache/save로 나뉘어서 쉽게 짤 수 있었습니다.
    https://github.com/actions/cache

결과

  • 캐싱을 도입하기 전 ci/cd 시간

  • 캐싱을 도입하기 후 ci/cd시간

2분 57초에서 1분 39초로 개선되면서 , 약 44.07% 향상되었습니다.

그러나 CPU사용률은 아직 그대로입니다. 이에 대해서는 다음 포스팅에 다루겠습니다.

profile
🐶개발 블로그

0개의 댓글