Github Actions 을 활용한 CI/CD 구축기

Lily·2024년 8월 4일
10
post-thumbnail

레벨 3 프로젝트를 시작하고 팀 내에서 CI / CD 구축을 담당했습니다.
그 중에서도 저는 CI 에 집중해서, 해당 부분에 대해 기록을 남깁니다.

◦ CI (Continuous Integration)

CI는 개발자들이 자주 코드 변경 사항을 통합하고, 자동화된 빌드와 테스트를 통해 이러한 변경 사항이 문제 없이 작동하는지 확인하는 프로세스입니다.
CI 실행을 지원하기 위한 툴은 Github Actions 이외에도 Jenkins, CircleCI 등 많습니다.

저희 팀은 Github Actions 를 사용하기로 했는데요!
저는 처음이지만 이미 사용법을 알고 있는 팀원이 있어 러닝커브를 줄일 수 있겠다는 이유였습니다.

⍥ Github Actions 구성 요소

Github Actions 는 이름에서 알 수 있듯이 github 에서 제공하는 자동화 툴 입니다.
구성 요소 중 제가 사용한 구성 요소와 속성을 위주로 설명합니다.

워크플로우(Workflow)

이벤트가 트리거될 때 실행되는 자동화된 프로세스 == 실행할 스크립트 파일

  • 파일 위치: .github/workflows 디렉토리의 YAML 파일로 위치해야 합니다.
  • 속성
    • name: 워크플로우의 이름

      설정한 이름이 Actions 내에 위와 같이 구분이 가능하도록 명시됩니다.
    • on: 워크플로우를 트리거하는 이벤트 == 언제 실행되게 할 건지 설정합니다.

      • 예: push, pull_request, schedule
    • jobs: 워크플로우 내에서 실행되는 잡들의 정의 == 어떤 작업이 실행되게 할 것인지 설정합니다.

      • 아래에서 자세히 설명합니다.
    name: BACKEND_CI
    
    on:
      pull_request:
        types: [ opened, reopened, synchronize ]
        branches: [ 'main' ]
    
    # 현재 main 브랜치를 대상으로 PR이 열릴 때, 다시 열릴 때, PR의 소스 브랜치에 새로운 커밋이 푸시될 때 수행됨

잡(Job)

잡은 여러 스텝(step)들로 구성됩니다.
ex) BE_CI 라는 jobs 이 있고, java 버전 확인, gradle 빌드 확인 등의 step(개별 작업)이 있는 형태

아래 이미지는 현재 총 3개의 jobs 로 이루어진 CI 형태

  • 속성
    • name: 잡의 이름
    • runs-on: 잡이 실행될 환경 == job 을 실행하는 컴퓨터(github runner)
      • 예: ubuntu-latest, windows-latest, macos-latest
    • needs: 다른 잡이 완료된 후에 실행되도록 설정 == 설정해주지 않으면 각각의 잡은 기본적으로 병렬 실행됩니다.
    • if: 조건부 실행
    • steps: 잡 내에서 순차적으로 실행되는 스텝들
  BE_CI: # name
    runs-on: ubuntu-latest # ubuntu 환경에서 잡 실행
    needs: PATH_CHANGES # PATH_CHANGES 라는 job 이 실행된 후에 해당 잡을 실행
    if: ${{ needs.PATH_CHANGES.outputs.backend == 'true' }}
    # PATH_CHANGES 의 outputs 이 true 일때만 실행

    steps:
      ...

스텝(Step)

잡 내에서 순차적으로 실행되는 개별 작업

  • 속성:
    • name: 스텝의 이름
    • uses: 재사용 가능한 액션을 호출할 때 사용 == 특정 작업을 수행하기 위해 미리 정의된 기능이나 도구를 가져다 사용합니다. 다른 사람이 작성한 액션을 가져와서 사용할 수 있습니다.
      • Github Marketplace 에서 찾을 수 있습니다.
    • with: 액션에 전달할 인자들
    • run: 쉘 명령어 또는 스크립트를 실행할 때 사용
    steps:
      - uses: actions/checkout@v4 # 레포지토리의 코드를 체크아웃하는 작업을 수행
        with: # 현재 PR의 소스 브랜치 이름을 가져옴
          ref: ${{ github.event.pull_request.head.ref }}

      - name: Setup Java # 자바 버전 확인
        uses: actions/setup-java@v4
        with:
          distribution: 'corretto'
          java-version: '21'

      - name: Setup Gradle # gradle 세팅
        uses: gradle/actions/setup-gradle@v3

      - name: Test with Gradle # gradle test
        continue-on-error: true
        id: gradle_test
        run: |
          cd backend
          ./gradlew test

⍢ CI Scripts 변천사

CI 스크립트 개선 작업을 틈틈이 진행해 온 과정을 공유합니다.

초기 스크립트

초기 작성했던 스크립트의 문제점은 다음과 같습니다.

  • 해당 스크립트는 백엔드를 위한 CI 이나, name 이 Gradle Package 이므로 어떤 작업을 하는 스크립트인지 알기 어렵다.
  • 불필요한 permissions 가 삽입되어있다.
  • steps 의 Early exit 에서 PR 의 라벨이 ‘백엔드’ 가 아닐 경우 cancel
    • 프론트엔드 일 경우 해당 CI 가 cancel → fail 판정 → merge 불가능
  • 자바 버전을 확인하는 steps 에서 불필요한 작업 수행 (아래 작업은 maven 시 필요)
    • server-id: github
      settings-path: ${{ github.workspace }}
  • gradle 를 매번 setup → CI 속도 저하
  • CI 결과를 슬랙 메시지로 전달하는 작업도 job 하나에 포함되어있음 (job 구분 X)
  • Get teamMember List 를 수행하는 step 의 script 문이 복잡함
  • job name 설정 X
name: Gradle Package

on:
  pull_request:
    types: [ opened, reopened, synchronize ]
    branches: [ 'main' ]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      actions: write

    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.ref }}
      - name: Early exit
        if: ${{ !contains(github.event.pull_request.labels.*.name, '백엔드') }}
        run: |
          gh run cancel ${{ github.run_id }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - uses: actions/setup-java@v4
        with:
          distribution: 'corretto'
          java-version: '21'
          server-id: github
          settings-path: ${{ github.workspace }}

      - name: Setup Gradle
        run: chmod +x ./backend/gradlew

      - name: Build with Gradle
        continue-on-error: true
        id: gradle_build
        run: |
          cd backend
          ./gradlew build

      - name: Get teamMember ListGet teamMember List
        id: teamMembers
        uses: actions/github-script@v6
        with:
          script: |
            const fs = require('fs');
            const workers = JSON.parse(fs.readFileSync('.github/workflows/teamMember.json'));
            const mention = context.payload.pull_request.assignees.map((user) => {
            const login = user.login;
            const mappedValue = workers[login];
            return mappedValue ? `<@${mappedValue}>` : `No mapping found for ${login}`;
            })
            return mention.join(', ');

      - name: slack mention
        uses: slackapi/slack-github-action@v1.24.0
        with:
          channel-id: ${{ secrets.ISSUE_CHANNEL }}
          payload: |
            {
                "text": "pr 테스트 결과",
                "blocks": [
                  {
                      "type": "section",
                      "text": {
                        "type": "mrkdwn",
                        "text": "pr 테스트 ${{ steps.gradle_build.outcome}} \n • 링크: <${{ github.event.pull_request.html_url }}|${{ github.event.pull_request.title }}> \n • pr 담당자: \${{ steps.teamMembers.outputs.result }}
                      }
                  }
                ]
            }
        env:
          SLACK_BOT_TOKEN: ${{ secrets.BOT_TOKEN }}

개선된 스크립트

개선 작업은 며칠에 걸쳐 조금씩 수행했습니다.

개선된 사항은 다음과 같습니다.

  • 워크플로우 name 수정 (Gradle Package → BACKEND_CI)
  • jobs 분리
    • PATH_CHANGES, BE_CI, BE_SLACK_MESSAGE
  • 트리거(if) 변경 - 쉽게 변경 가능한 PR 의 라벨이 아닌, 변경된 파일의 경로를 확인하여 수행하게 함
    • PATH_CHANGES 를 두어 경로 확인
    • BE_CI 의 if: ${{ needs.PATH_CHANGES.outputs.backend == 'true' }} 조건으로 cancel 이 아닌 Success 판정 (아래 이미지는 프론트엔드 작업의 actions 수행 결과입니다.)
  • 위 초기 스크립트의 Get teamMember List 를 수행하는 step 의 script 문이 복잡함 를 해결하기 위한 작업
    • 초기 스크립트에선 PR 의 assignee 를 가져왔으나 현재는 PR 을 발행한 사람(sender) 를 가져오도록 변경
    • 깃헙 ID 와 슬랙 ID 를 json 대신 env 를 두어 가져오도록 수정
  • Gradle 을 Caching 하여 사용하도록 수정 (속도 개선)
    • 개선된 스크립트의 캐싱을 사용하기 이전엔 수동으로 캐싱하는 작업을 수행했습니다.
        - name: Gradle Caching
                uses: actions/cache@v3
                with:
                  path: |
                    ~/.gradle/caches
                    ~/.gradle/wrapper
                  key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
                  restore-keys: |
                    ${{ runner.os }}-gradle-
    • 추후 팀원이 제안해준 방법으로 수정했습니다. → 공식문서에서 제안, 수동이 아닌 uses (gradle/actions/setup-gradle@v3) 를 사용하는 방법
      • 속도 측정 (측정 시 마다 개선 속도 차이가 조금씩 달라지긴 했지만 캐싱 후에 빨라졌다는 사실은 변함이 없었습니다.)
  • 불필요한 작업 제거 (permission 수정, maven 을 위한 작업 등)
name: BACKEND_CI

on:
  pull_request:
    types: [ opened, reopened, synchronize ]
    branches: [ 'main' ]

env:
  ...

jobs:
  PATH_CHANGES:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: read
    outputs:
      backend: ${{ steps.changes.outputs.backend }}

    steps:
      - uses: dorny/paths-filter@v3
        id: changes
        with:
          filters: |
            backend:
              - 'backend/**'

  BE_CI:
    runs-on: ubuntu-latest
    needs: PATH_CHANGES
    if: ${{ needs.PATH_CHANGES.outputs.backend == 'true' }}

    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.ref }}

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          distribution: 'corretto'
          java-version: '21'

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v3

      - name: Test with Gradle
        continue-on-error: true
        id: gradle_test
        run: |
          cd backend
          ./gradlew test

  BE_SLACK_MESSAGE:
    runs-on: ubuntu-latest
    needs: BE_CI
    if: ${{needs.BE_CI.result != 'skipped'}}
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.ref }}

      - name: Get teamMember
        id: teamMember
        run: |
          echo "SENDER_SLACK_ID=${{ env[github.event.sender.login] }}" >> $GITHUB_ENV

      - name: Slack mention
        uses: slackapi/slack-github-action@v1.24.0
        with:
          channel-id: ${{ secrets.ISSUE_CHANNEL }}
          payload: |
            {
                "text": "pr 테스트 결과",
                "blocks": [
                  {
                      "type": "section",
                      "text": {
                        "type": "mrkdwn",
                        "text": "pr 테스트 ${{ needs.BE_CI.result }} \n • 링크: <${{ github.event.pull_request.html_url }}|${{ github.event.pull_request.title }}> \n • pr 담당자: <@${{ env.SENDER_SLACK_ID }}>"
                      }
                  }
                ]
            }
        env:
          SLACK_BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
  • 참고로 Slack mention 의 결과는 다음과 같습니다.
profile
내가 하고 싶은 거

2개의 댓글

comment-user-thumbnail
2024년 8월 17일

릴리 멋있다~~~

1개의 답글

관련 채용 정보