CI/CD 구축하기(Github Actions, Vercel)

문도연·2023년 12월 4일
6
post-thumbnail

같은글 노션링크 | 코드 변경 전후를 명확하게 보실수있습니다.

1.CI/CD

1.1 CI, CD, CDep
1.2 CI/CD 구축의 필요성
1.3 도입 목표

2. 구현 전에 고민할 것

2.1 현재 브랜치 전략
2.2 CI/CD 흐름 생각해보기

3. CI/CD 구축하기

3.1 워크플로우 트리거 시점 정하기
3.2 CI
3.3 CDep
3.4 CI 속도 개선하기 (의존성 캐싱적용)

4. 마치며


1.CI/CD

CI/CD는 애플리케이션 개발(빌드, 테스트, 배포) 단계를 자동화하여 보다 짧은 주기로 앱을 고객에게 제공하는 방법을 말한다.

1.1 CI, CD, CDep

CI(지속적인통합)는 소스코드의 새로운 변경사항을 빌드하고 테스트한뒤 코드를 병합(통합)하는 프로세스를 자동화하는 작업이다.

CD와 CDep는 CI 의 연장선인데, CI 를 성공하고나서 팀 사정에 따라 CD 까지 혹은 CDep까지 자동화 수준을 결정하면 되는 것으로 이해했다.

Code - Build - Test - Staging - Deploy

←————————→ CI

←—————————————→ CD

←———————————————————>CDep

  • CD(지속적인제공)는 CI를 완료한 뒤, 개발 브랜치 코드를 main 브랜치로 자동 병합하는 작업이다. 자동 병합 후 병합된 코드를 스테이징 환경에 자동배포 해서 추가적인 테스트를 진행할 수 있다. 모든 추가 테스트가 완료되면 프로덕션 레벨로 배포를 진행하게 된다.
  • CDep(지속적인배포) CI를 완료한 뒤, main 브랜치로 자동병합 후 곧바로 프로덕션 레벨까지 자동배포하는 프로세스를 자동화하는 작업이다.

1.2 CI/CD 구축의 필요성

머지, 빌드, 테스트, 배포는 매번 수동으로 하면 리소스 낭비가 큰 작업이다. 즉, 휴먼에러가 발생하기 좋은 작업들이어서 수동으로 하다가 에러가 터지면 해결하기까지 시간이 오래걸리고, 앱의 개선은 느려지게 되고, 고객은 그 사이에 이탈할 것이며 서비스는 망하게 된다.

새로운 코드 변경사항에 버그가 있진않은지 검사하고, 안전하고 빠르게 앱을 업데이트 할 수 있는 방법이 CI/CD 인 것이다.

1.3 도입목표

현재 vercel에 배포 중이고, 브랜치는 dev(개발브랜치)와 main이 존재한다. 또한, 현재 main브랜치에 대해 PR을 생성하면 CI 없이 곧바로 vercel을 통해 스테이징 환경에 알아서 자동 배포되고 있다.

이에 나는 CI가 성공해야 Vercel이 프로덕션 환경에 자동배포를 하도록 CDep 프로세스를 제어하고 싶다.

즉, 코드 변경 사항이 CI 프로세스의 모든 테스트를 통과하자마자 main브랜치 코드가 자동으로 프로덕션에 배포되게 만들고 싶다.

2. 구현 전에 고민할 것

2.1 현재 브랜치 전략

본 프로젝트의 브랜치 전략을 논하기 전에,

보다 일반적인 팀의 브랜치 전략과 그에 따른 CI/CD 구축 시나리오를 생각해보자

💡 <————branch———>

feat —— dev —— main —— production

  • feat에서 dev로 PR 요청하면 CI(build, test, merge)를 수행한다. (merge 승인전에 코드리뷰?)
    - CI가 성공하고 나서 CD로 갈것이냐 CDep로 갈것이냐
    1. CD
    - dev코드를 main 브랜치에 자동병합까지만 하는 것
    - 이 단계에서는 main 코드를 스테이징 환경까지는 자동배포해서 QA 등 추가 테스트를 진행할듯. 테스트를 완료하고나면 프로덕션 레벨로 드디어 배포를 할듯
    2. CDep
    - dev코드를 main으로 자동병합 하고 곧바로 프로덕션 레벨까지 자동배포를 진행하는게 CDep

본프로젝트는 dev, main 두개의 브랜치가 존재한다.

<—branch—>

dev —— main —— production

  • dev에서 main으로 PR 요청시, CI(빌드, 테스트)를 수행한다.
    • CI가 실패하면 PR Close
    • CI가 성공해 PR이 승인되어 dev 코드가 main으로 병합되면 CDep를 진행되게 한다.

2.2 CI/CD 흐름 생각해보기

  1. 로컬dev에서 새로운 코드 변경사항을 원격dev로 푸쉬한다.

  2. 개발자가 PR(dev to main)을 생성한다.

  3. PR 생성시, CI(빌드, 테스트)가 이뤄진다.

    3-1. 빌드-테스트에 성공하면, 자동으로 PR Merge

    3-2. 빌드 또는 테스트에 실패하면 어떤 단계에서 실패한건지 코멘트가 달리고, PR Close

  4. PR Merge(main으로 push) 되면 vercel을 통해 앱이 자동배포된다.

3. CI/CD 구축하기

3.1 워크플로우 트리거 시점 정하기

1) PR 생성시 CI 워크플로우가 진행돼야한다.

# .github/workflow/CI.YML
on: 
  pull_request:
    branches: [main]

2) PR Merge(main으로 push)시 CDep 워크플로우가 진행돼야한다.

# .github/workflow/deploy CI.YML
on:
  push:
    branches: [main]

3.2 CI (build, test, 자동 레포지토리 병합)

어떤 단계에서 에러가 발생한 것인지 파악하기 위해서 build, test job을 따로 작성했다.

  1. build job
    • 실패시, 빌드가 실패했다는 코멘트가 PR에 달리고, PR이 닫혀야 한다. test job은 진행하지 않는다.
    • 성공시 test job을 진행한다.
  2. test job
    • 실패시, 테스트가 실패했다는 코멘트가 PR에 달리고, PR이 닫혀야 한다.
    • 성공하면, 해당 PR은 자동머지 한다.

[workflow] CI.YML

name: CI

on:
  pull_request:
    branches: ['main']

jobs:
	# 1. 빌드
  build:
    runs-on: ubuntu-latest # 실행환경

    steps:
      - name: Checkout code # 플젝코드 소스코드 내려받기 
        uses: actions/checkout@v3

      - name: Install dependencies # 종속성 설치
        run: yarn install

      - name: Build # 빌드
        run: yarn build

      - name: Close PR, if build fails # 빌드 실패시 pr 닫기
        if: ${{ failure() }} # 이전 step이 실패한 경우에만 이 step을 실행시키는 syntax
        uses: actions/github-script@v6
        with: # actions(uses)의 파라미터 역할
          github-token: ${{ github.TOKEN }}
					# octokit 문법 참고
          script: | 
            const pull_number = ${{ github.event.pull_request.number }}
            const updated_title = `[BUILD FAIL] ${{ github.event.pull_request.title }}`
            await github.rest.pulls.createReview({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: pull_number,
              body: '빌드에 실패했습니다.',
              event: 'REQUEST_CHANGES'
            })
            await github.rest.pulls.update({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: pull_number,
              title: updated_title,
              state: 'closed'
            })

	# 2. 테스트
  test:
    runs-on: ubuntu-latest
    needs: build  # build 테스트 이후에 test job 이 이뤄지도록 순서 제어
    env:
      REACT_APP_BASE_URL: ${{ secrets.REACT_APP_BASE_URL }}
      REACT_APP_API_KEY: ${{ secrets.REACT_APP_API_KEY }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Install dependencies
        run: yarn install

      - name: Run test
        run: yarn test

      - name: Close PR, If test fail
        if: ${{ failure() }}
        uses: actions/github-script@v6
        with:
          github-token: ${{ github.TOKEN }}
          script: |
            const pull_number = ${{ github.event.pull_request.number }}
            const updated_title = `[TEST FAIL] ${{ github.event.pull_request.title }}`
            await github.rest.pulls.createReview({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: pull_number,
              body: '테스트에 실패했습니다.',
              event: 'REQUEST_CHANGES'
            })
            await github.rest.pulls.update({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: pull_number,
              title: updated_title,
              state: 'closed'
            })

      - name: Approve PR, If test passes # 테스트 통과시, PR 머지
        uses: actions/github-script@v6
        with:
          github-token: ${{ github.TOKEN }}
          script: |
            const pull_number = ${{ github.event.pull_request.number }}
            await github.rest.pulls.createReview({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: pull_number,
              body: '테스트에 통과해 main 브랜치로 자동 머지합니다.',
              event: 'REQUEST_CHANGES'
            })
            await github.rest.pulls.merge({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: pull_number,
            })

* 테스트 실패시 PR 닫기 (actions/github-script@v6)

  1. 깃허브 토큰 만들기

    해당 액션이 github api를 이용하는 액션이므로, github token을 생성해서 제공해줘야 깃 리포지토리의 정보에 대해 http 요청을 날릴 수 있다.

  2. 에러 : Input required and not supplied: github-token

  • 토큰이름을 secret.GIHUB_ACTIONS 에서 secret.TOKEN 으로 바꿨더니 해결됐다. 오타가 있었던것같다.
    - 변경전 토큰이름

    - 변경후 토큰이름

  1. 에러 : RequestError [HttpError]: Resource not accessible by integration
    - 2의 에러를 해결했지만, 권한이 여전히 부족한 것 같다. 상태코드가 403 이다.

구글링을 하니까, 워크플로우 퍼미션을 write 도 할 수 있게 해주라는 솔루션이 있었다.

  • 즉, ‘워크플로우 동작으로 레포지토리에 영향(내 경우 PR Close)을 줘도 된다’는 옵션으로 변경하라는 말

  • 레포지토리 - Setting - 사이드바의 action - general

  • 해결됐다.

  1. Jest 6개의 테스트 중 2개가 실패했고, yarn test 중 실패했기 때문에 PR이 정상적으로 클로즈 됐다.

* Jest가 ReactDom.Portal 을 인식하지 못해 test 가 실패하는 경우

에러모달 테스트가 실패한다.
에러 모달이 제대로 뜨지 않는 것 같다. createPortal 과 연관 돼있는듯하다.

  • 코드 수정 후 해결했다.
    • test 환경에서는 컴포넌트를 단순하게 리턴, 렌더하고
    • 테스트 환경이 아닌 경우에만 portal로 컴포넌트를 감싸는 분기 로직을 추가했다.

* CI.Yml 정상 동작 확인

가짜 커밋을 푸쉬, main에 PR 생성했다. 아래와 같이 YAML 파일이 정상적으로 동작하고, 테스트도 통과가 잘됐다.
PR을 그대로 둔채, 만들었던 가짜 컴포넌트를 제거하고 다시 커밋, 푸쉬 했다. 다시 YAML 파일이 동작했다.

3.3 CDep

main 브랜치로 머지가 완료되고나서(push to main), 배포가 이뤄져야 한다.

  1. 문제: vercel 자동배포가 CI 프로세스보다 먼저 이뤄지고있다.

    아래 사진에서 빨강네모는 배포가 먼저 이뤄진 것을 보여주고, 파랑네모는 github action 의 CI.YAML이 build job 을 진행중임을 보여준다. 즉, 순서가 안맞는다.

  1. 원인: 현재 vercel은 github actions 흐름에 종속돼있지 않고. 그저 연동된 깃 레포지토리의 모든 브랜치에 push 이벤트가 발생하면 바로 자동배포를 한다. (이것이 vercel 배포시 디폴트 동작임)
  2. 목표: vercel 배포동작을 YAML로 제어하자. CI 가 성공하고나서 자동배포가 되게 하자.
    • 우선 vercel에서 main 브랜치에 푸시되는 경우에만 빌드-배포 동작을 하도록 설정했다
  • 다음 블로그글을 참고해서 vercel 배포를 쉽게 할 수 있는 action을 설정했다.

    [workflow] deploy CI.YML

    name: deploy CI
    
    on:
      push:
        branches: ['main'] 
    
    jobs:
      deploy:
        runs-on: ubuntu-latest
    
        steps:
          - name: Checkout code
            uses: actions/checkout@v3
    
          - name: Deploy to Vercel Action
            uses: BetaHuhn/deploy-to-vercel-action@latest
            with:
              GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN_DEPLOY }}
              VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
              VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
              VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
  1. 결과
  • CI.yml 동작으로 인해 메인 브랜치로 자동머지
  • 자동 머지 후 Vercel 대시보드 - 머지된 PR에 대해 깃허브액션에 의해 deploy CI.YML 이 동작해 Vercel에서 배포를 수행한다.

3.4 CI 워크플로우 속도 개선하기 - 의존성 캐싱적용하기

  1. 이슈: CI가 너무 느리다.

  2. 원인: 워크플로우가 매번 새로운 가상환경에서 실행되기 때문에, 의존성을 job (build, test) 마다 설치하기 때문이다. 비슷한 의존성을 반복적으로 설치하는 것은 네트워크 사용량을 증가시키고 런타임을 늘린다. → 시간과 자원의 낭비

    GitHub Actions에서 제공하는 의존성 캐싱을 적용하면 워크플로우의 실행 시간을 단축할 수 있다.

  3. 목표: 의존성들을 캐시하고 워크플로우 속도를 개선하자.

  4. 과정

    의존성 캐싱 방법(2)

    1) actions/cache를 사용해서 직접 설정로직을 작성하거나

    2) actions/cache를 내장하고있는 actions/setup-node을 이용하는 방법이 있다.

    처음에 전자를 시도해보다가 CLI 가 어렵게 느껴져서 후자 방법으로 전환했다. (전자는 나중에 해보기)

    **actions/setup-node을 이용해 의존성 캐싱적용하기 (Built-in 의존성 캐싱)**

💡 워크플로우 셋업이 용이하도록 GitHub에서 제공하는 [action]은 대부분 [actions/cache]를 내장하고 있어 복잡한 설정 없이 의존성 캐싱을 적용할 수 있습니다.

아래 코드를 추가하기만 하면 된다.

steps:
  - uses: actions/checkout@v3
  - uses: actions/setup-node@v3
    with:
      node-version: 16
      cache: npm# or yarn, pnpm

Built in 의존성 캐싱 - ci.yml

name: CI

on:
  pull_request:
    branches: ['main']

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

			# 추가
      - uses: actions/setup-node@v3
        with:
          node-version: 18.x
          cache: yarn

      - name: Install dependencies
        run: yarn install

      - name: Build
        run: yarn build

            ...

  test:
    runs-on: ubuntu-latest
    needs: build

    env:
      REACT_APP_BASE_URL: ${{ secrets.REACT_APP_BASE_URL }}
      REACT_APP_API_KEY: ${{ secrets.REACT_APP_API_KEY }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - uses: actions/setup-node@v3
        with:
          node-version: 18.x
          cache: yarn

      - name: Install dependencies
        run: yarn install

      - name: Run test
        run: yarn test

     ...
  1. 결과

build job, test job 직렬 순서로 워크플로우가 동작하는데 build job은 항상 의존성을 순정으로 설치한다. build job에서 저장된 캐싱은 test job에서 재사용한다.

아래사진을 통해 test job에서 캐시 hit하고 의존성 설치에 19초가 소요됨을 알 수 있다. (build job은 의존성 설치에 32초정도 소요됨)

캐싱 로직을 적용하기전에 test job의 기존 러닝타임은 55초였고, 캐싱 힛트로 인해 36초로 19초가 단축됐다. 전체 러닝타임은 캐싱적용전 1분49초, 후가 1분 38초이다.

  • 캐싱로직 적용 전
    - ci 전체 시간: 1분 49초
    - 캐싱 적용되지않은 test job 소요 시간: 55초

  • 캐싱로직 적용 후
    - ci 전체 시간: 1분 38초
    - 캐싱 적용한 test job 소요 시간: 36초

      

전체 시간이 극적으로 단축된 것은 아니어서 처음엔 실망할뻔 했다가, 캐싱파일을 사용한 Test job의 수행시간이 크게 19초나 줄어든 것을 보고, test Job 이외에 다른 Job들이 추가될 큰 프로젝트일수록 캐싱의 효용이 더욱 드러날 것으로 생각됐다.

actions/cache 를 이용하면 위의 UI 에서 사이드바 하단의 Run details 메뉴에 caches 라는 메뉴가 나타난다. 해당 메뉴에는 캐시된 해시값키가 보여지는데. 아마 그 캐시를 이용하면 build job도 캐싱이 적용돼서 시간을 더 단축할 수 있을것같다는 추측이다. 마음의 여유가 생기면 도전 해봐야겠다.

4. 마치며

깃액션을 다룰때마다 떨리는 것 같다. GUI가 없고, 주로 명령어를 작성하거나, 명령어를 많이 읽어야한다거나,, 해서 인 것 같다. 또 깃 사용 경험이 적어서인 것도 있다. 한번 기간을 잡고 명령어, 깃을 공부해야할 것같다. 다양한 에러상황을 테스트 해보면서 깃에 대한 항마력(?)을 키워야겠다.


학습

참고

[Github Actions] CI - 빌드 실패 시 Pull Request 닫고 코멘트 등록하기
s3, cloudfront + github actons
Github actions를 이용한 빌드 및 배포 자동화 - netlify
github-actions, Elastic beanstalk
if문으로 실패 제어
카카오엔터프라이즈가 GitHub Actions를 사용하는 이유
[CI/CD] Husky, GitHub Actions 로 팀 프로젝트 코드를 지속적으로 통합/배포하기 (ESLint, Prettier, Jest, gh-pages)
PR에서 테스팅 후 성공시에만 자동 Merge하기
**PR Github actions의 결과에 따라 자동 comment 남기기**
**[Github Actions] CI - 빌드 실패 시 Pull Request 닫고 코멘트 등록하기**
Github Actions 를 이용한 CI 테스트 자동화
**[Github Actions] Github API를 사용하는 스크립트 모음**
https://octokit.github.io/rest.js/v18#pulls-request-reviewers
https://velog.io/@couchcoding/CICD-Github-Actions으로-내-포트폴리오에-CICD를-적용하기-1
내가 사용하는 GitHub Actions - Vercel.👏🏻
marketplace - actions | deploy-to-vercel-action
vercel | how-can-i-use-github-actions-with-vercel

더빠른 워크플로우
더 빠른 워크플로우를 향해
[Github Action] 데이터 좀 맡길게! 나중에 그대로 다시줄래? - action cache
https://velog.io/@lingodingo/배포-속도를-올려보자-CI
깃허브 닥스 | caching-dependencies-to-speed-up-workflows
Github Actions를 이용한 CI 구축하기(Prettier, ESLint, TSC)

profile
중요한건 꺾이지 않는 마음이 맞는 것 같습니다

0개의 댓글