github action으로 마이그레이션하기 및 빌드 캐시로 성능 개선하기(gitlab)

junto·2024년 6월 22일
0

devops

목록 보기
7/9
post-thumbnail

프로젝트는 국비 교육 기관 저장소(Gitlab)에서 진행되었고, 프로젝트가 끝나도 외부에서 접근할 수 없었다. 결국 깃랩 공개 저장소나 깃허브 공개 저장소로 옮겨야 했는데 투표에 따라 깃허브로 옮기게 되었다.

gitlab 파이프라인 사용시간

  • 대략 3주간 gitlab pipeline을 이용했는데 총 시간이 얼마나 소요되었는지 궁금했다. 하지만, 설정을 찾아봐도 아래처럼 총 파이프라인 개수나 개별 파이프라인 소요 시간만 알 수 있었다.

  • gitlab에서 제공하는 API를 사용하면 파이프라인 총사용 시간을 알아낼 수 있다.
  • 모든 파이프라인 정보를 가져오는 API를 통해 모든 파이프라인 id를 가져온 후 id로 개별 파이프라인을 조회하는 API를 호출하여 duration 응답 필드에 사용 시간(초)를 모두 더하면 된다. https://docs.gitlab.com/ee/api/pipelines.html
  • 파이프라인을 한 번 가져올 때 페이지당 최대 100개의 데이터를 가져올 수 있다. https://docs.gitlab.com/ee/api/rest/

Gitlab 파이프라인 API

1. 모든 파이프라인 정보 가져오기

curl --header "PRIVATE-TOKEN: {privateToken}" "{domain}/api/v4/projects/{projectId}/pipelines"

2. 특정 파이프라인 가져오는 API

curl --header "PRIVATE-TOKEN: {privateToken}" "{domain}/api/v4/projects/{projectId}/pipelines/{id}"

파이썬으로 파이프라인 총사용 시간 알아내기

import os
import requests

GITLAB_TOKEN = os.getenv('GITLAB_TOKEN')
GITLAB_PROJECT_ID = os.getenv('GITLAB_PROJECT_ID')
GITLAB_URL = f'https://kdt-gitlab.elice.io/api/v4/projects/{GITLAB_PROJECT_ID}/pipelines'

headers = {
    'PRIVATE-TOKEN': GITLAB_TOKEN
}

# 모든 파이프라인을 가져옴
def get_pipelines():
    pipelines = []
    page = 1
    while True:
        response = requests.get(GITLAB_URL, headers=headers, params={'page': page, 'per_page': 100})
        response.raise_for_status()
        pipeline_response = response.json()
        if not pipeline_response:
            break
        pipelines.extend(pipeline_response)
        page += 1
    return pipelines

# 특정 파이프라인 세부 정보를 가져옴
def get_pipeline_details(pipeline_id):
    pipeline_url = f'https://kdt-gitlab.elice.io/api/v4/projects/{GITLAB_PROJECT_ID}/pipelines/{pipeline_id}'
    response = requests.get(pipeline_url, headers=headers)
    response.raise_for_status()
    return response.json()

pipelines = get_pipelines()

total_pipelines = len(pipelines)

# 모든 파이프라인 Duration 시간을 더함
total_duration = 0
for pipeline in pipelines:
    pipeline_id = pipeline['id']
    pipeline_details = get_pipeline_details(pipeline_id)
    duration = pipeline_details.get('duration')
    if duration is not None:
        total_duration += duration

total_duration_minutes = total_duration / 60

print(f'총 파이프라인 갯수: {total_pipelines}')
print(f'총 파이프라인 사용시간: {total_duration_minutes}')

대략 3주간 367개의 파이프라인이 실행되었고, 977시간이 소요되었다. 생각보다 많은 시간을 사용했다는 것에 놀랐고, 개별 파이프라인 실행속도는 사소할지 몰라도 반복적으로 실행하여 누적되면 그 차이는 사소하지 않다. 파이프라인은 중복이 없어야 하고, 빠르게 실행되어야 한다.

로컬 빌드 서버를 사용해야 할까?

  • gitlab에서 로컬 빌드 서버를 사용하게 된 가장 큰 이유는 월 사용량이 400분까지만 무료이기 때문이다. https://about.gitlab.com/pricing/
  • 하지만 github에서는 공개 저장소를 사용하는 경우 무료다. private 저장소에서 사용할 경우 2000분까지 무료로 제공한다고 한다. 그리고, 기존 빌드 서버는 AWS Freetier로 CPU 1개, 메모리 1GB였지만, github에서 제공하는 머신은 대략 4배 더 좋다.

굳이 로컬 빌드 서버를 사용할 이유는 없었고, github hosted runner를 사용하기로 했다. 테스트해 본 결과 로컬 빌드 서버에서는 매번 의존성을 설치할 필요는 없었으나 빌드하는 데 시간이 오래 걸렸고, github hosted runner는 매번 필요한 의존성을 설치해도 이전보다 더 빠르게 동작하였다.

개선 부분

1. 불필요한 파이프라인 삭제

  • 기존에는 Compile이라는 단계가 존재했다. gradle로 빌드하고 실패하면 다음 단계로 진행하지 않는 것인데, 이 부분은 Build 스테이지에서 또 한 번 빌드하므로 중복된다. 또한 Test 단계에서도 중복된다. Test 단계에서는 ./gradlew build test테스트만 진행되는데 해당 단계에서 이미 Compile 에러는 잡아준다. 따라서 불필요하게 시간을 잡아먹던 Compile 스테이지를 제거했다. 빌드 속도가 대략 20~30초 더 빨라졌다.

  • 기존에는 docker-in-docker(dind)를 이용해 빌드 환경을 구축하고 배포했는데, 토이 프로젝트에서는 dind까지 쓸 필요가 없다. 빌드할 때마다 환경에 따라 설정이 꼬일 염려가 없고, 동시에 여러 파이프를 실행시킬 상황이 없기 때문이다. dind를 내려받는 시간과 제한된 도커 리소스로 인해 속도가 조금 더 느렸지만, 이를 제거하여 빌드 속도가 대략 20~30초 더 빨라졌다.

2. next, gradle 빌드 캐시 적용

1) next build cache 적용하기 (next export -> nginx 배포)

jobs:
  Deploy:
    runs-on: ubuntu-24.04
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
       node-version: 20
       cache: 'npm'
    - run: echo ${{ secrets.ENV_LOCAL }} > .env.local
    - run: npm ci
    
    - run: npm run build
  • node 20을 설치하고 next 캐시를 적용하지 않고, 파이프라인을 2번 연속으로 실행해 시간을 측정해보았다.
  • 약간의 차이는 있지만, npm run build부터 배포 서버에 file uploading까지 대략 31~32초가 걸린다.
  • 그럼 빌드하기 전에 next build cache를 적용해보자.
- name: Next cache build
  id: next-build-cache
  uses: actions/cache@v4
  with:
	path: |
	  .next/cache
	key: ${{ runner.os }}-next-build-${{ hashFiles('package-lock.json') }}
	restore-keys: |
	  ${{ runner.os }}-next-build-
- run: npm run build

  • 빌드 캐시를 적용한 결과 필요한 종속성을 캐시하는 데 추가로 4초가 소요되었다. 걸리는 시간은 31~32초에서 17 ~ 21초로 10초 이상 줄어든 걸 확인할 수 있다. 프로젝트 규모가 클수록 빌드 캐시 효과가 더욱 클 거라 예상된다.

2) gradle build cache 적용하기

- name: Set up jdk17
uses: actions/setup-java@v4
with:
  distribution: 'corretto'
  java-version: '17'
  cache: 'gradle'
  • github hosting machine에서 gradle을 사용하기 위해선 위와 같이 매번 설치해 주어야 한다. 로그를 보면 아래와 같이 빌드 캐시를 가져오는 것을 확인할 수 있다.
# Run actions/setup-java@v4
# Installed distributions
# Creating settings.xml with server-id: github
# Writing to /home/runner/.m2/settings.xml
# Received 233806950 of 242195558 (96.5%), 222.8 MBs/sec
# Cache Size: ~231 MB (242195558 B)
# /usr/bin/tar -xf /home/runner/work/_temp/5bff7d4d-4d54-41b8-acfc-9b4ac9464ca3/cache.tzst -P -C /home/runner/work/backend/backend --use-compress-program unzstd
# Cache restored successfully
# Cache restored from key: setup-java-Linux-gradle-b5f8b219da233fd5274dc83c92b7496fae144a8b62e0031545559fdfb82e6b42
# Received 242195558 of 242195558 (100.0%), 115.4 MBs/sec

3. 자동 태그 적용

  • github action은 gitlab보다 사용자가 더 많아서 그런지 오픈 소스가 더 활성화되어 있다. 부족한 게 있다면 아래와 같이 market place에서 필요한 기능을 찾으면 된다.
  • auto labeler 적용하여 Issue와 Merge Request에 자동으로 제목과 내용을 검사해 라벨을 붙이도록 하였다.

헤맸던 부분

1. SSH_SECRET_KEY 환경변수로 전달하기

  • gitlab에서는 환경변수로 비밀 키들을 전달할 때 SSH키의 경우 파일로 만든다는 옵션이 있었다. 해당 옵션을 선택했을 경우 개행을 붙여야 한다. github에서는 파일로 제공한다는 옵션이 없어서 SSH_KEY를 PEM 형식으로 변환하여 환경 변수에 추가해야 한다. (Encrypt fail error) 또한, 깃랩과 달리 환경 변수 설정할 때 개행이 붙으면 제대로 동작하지 않는다.

2. 특정 파이프라인이 실패하면 Workflow 실행하지 않게 하기

  • gitlab에서는 rules if로 트리거 조건을 설정할 수 있었지만, github에서는 Settings-branches에서 해당 조건을 추가할 수 있었다.
  • 파이프라인 이름을 적는 게 아니라 Job이름을 적어야 한다. Job이름을 적으면 선택할 수 있는 Job이 표시된다.

CI 파이프라인과 CD 파이프라인

1. 백엔드 CI 파이프라인

name: CI Pipeline
run-name: ${{ github.actor }} is currently working on continuous integration 🚀

on:
  push:
    branches:
      - main
      - dev
  pull_request:
    branches:
      - main
      - dev
  workflow_dispatch:

jobs:
  Test:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - run: echo "${{ secrets.ENV_YML }}" > ./src/main/resources/env.yml

      - name: Set up jdk17
        uses: actions/setup-java@v4
        with:
          distribution: 'corretto'
          java-version: '17'
          cache: 'gradle'

      - run: ./gradlew build test --no-daemon

2. 백엔드 CD 파이프라인

name: CD Pipeline
run-name: ${{ github.actor }} is currently deploying 🚀

on:
  workflow_run:
    workflows: ["CI Pipeline"]
    types:
      - completed

env:
  IMAGE_NAME: jinyhehe/spacestory
  IMAGE_TAG: "1.0"
  DOCKER_REGISTRY_USER: ${{ secrets.DOCKER_REGISTRY_USER }}
  DOCKER_REGISTRY_PASS: ${{ secrets.DOCKER_REGISTRY_PASS }}

jobs:
  build:
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    runs-on: ubuntu-24.04

    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: |
          echo "${{ secrets.ENV_YML }}" > ./src/main/resources/env.yml
          echo $DOCKER_REGISTRY_PASS | docker login --username $DOCKER_REGISTRY_USER --password-stdin
          docker build -t $IMAGE_NAME:$IMAGE_TAG .
          docker push $IMAGE_NAME:$IMAGE_TAG

  deploy:
    needs: build
    runs-on: ubuntu-24.04

    steps:
      - uses: actions/checkout@v4

      - name: Setup ssh
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          ssh-keyscan -H 13.125.206.46 >> ~/.ssh/known_hosts
      
      - name: Deploy image
        run: ssh -i ~/.ssh/id_rsa ubuntu@13.125.206.46 "echo $DOCKER_REGISTRY_PASS | docker login --username $DOCKER_REGISTRY_USER --password-stdin && docker rm app -f || true && docker pull $IMAGE_NAME:$IMAGE_TAG && docker run -d -p 8080:8080 --name app --network juny $IMAGE_NAME:$IMAGE_TAG"

3. 프론트 CD 파이프라인

name: CD Pipeline
run-name: ${{ github.actor }} is currently deploying 🚀

on:
  push:
    branches:
      - main
      - dev
  pull_request:
    branches:
      - main
      - dev
  workflow_dispatch:

jobs:
  Deploy:
    runs-on: ubuntu-24.04
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
       node-version: 20
       cache: 'npm'
    - run: echo ${{ secrets.ENV_LOCAL }} > .env.local
    - run: npm ci

    - name: Next cache build
      id: next-build-cache
      uses: actions/cache@v4
      with:
        path: |
          .next/cache
        key: ${{ runner.os }}-next-build-${{ hashFiles('package-lock.json') }}
        restore-keys: |
          ${{ runner.os }}-next-build-
    - run: npm run build
    
    - name: Sending out/
      run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          ssh-keyscan -H 34.64.229.17 >> ~/.ssh/known_hosts
          cat ~/.ssh/id_rsa
          rsync -avz --delete -e "ssh -i ~/.ssh/id_rsa" out/ elice@34.64.229.17://home/elice/nginx/html/

참고 자료

profile
꾸준하게

0개의 댓글

관련 채용 정보