GitHub Action을 활용한 AWS ECR 자동배포

GitHub Action을 활용한 AWS ECR 자동배포

사내 IDC Jenkins에서 GitHub Action으로 갈아타기

·

6 min read

기존 Jenkins 설정을 활용한 배포 과정

Jenkins를 활용한 빌드-배포도 나쁘진 않았다

근데 GitHub Action을 쓰다보니 다신 못 돌아가겠다

Jenkins - Groovy 기반 스크립트는 뭔가 어색하다

  • Jenkins Pipeline Script는 Groovy를 사용함
  • Backend 분들은 Spring Boot 설정 파일에서 사용하는 경우도 있어서 익숙할지 모르겠으나..
  • Spring Boot와 어색한 우리 Frontend 에겐 러닝 커브가 있음
  • 그래도 한줄한줄 보다보면 어떤 느낌으로 흘러가는 지 정도는 파악할 수 있음
    • 뭔가 파이썬스럽기도 하고..
    • check parameter -> git checkout/pull -> docker build -> push to ECR -> GitLab k8s commit

Jenkins on IDC - 그냥 뭔가 불편하다

  • 버전이 너무 낮음

    • GitLab도.. Jenkins도 버전이 오래됨..
    • 플러그인들 붙이기도 어려움
      • 검색해서 뭔가 신박한걸 해보려고 하지만 할 수가 없음
    • UI도 왠지 구림
  • IDC 서버에서 작동

    • CPU 성능이 GitHub Action 서버의 40% 정도 수준임
      • => 빌드 속도에서 차이가 날 수 밖에 없음
    • 가끔 다른 프로젝트들에서 이미 여러개 실행 중인 경우, 대기 큐에 밀려서 끝날때까지 기다려야 함
    • 어쩌다 한번씩 젠킨스 서버가 터져서 배포를 못하기도 함.. 디스크 용량 초과라든지.. 마스터 계정 비번을 못찾는다든지..
Jenkins - IDCGitHub
image.pngimage.png

image.png

출처: cpubenchmark.net/compare/Intel-Xeon-E5-2673..


GitHub Actions를 활용한 ECR 배포 자동화

  • 현재 ArgoCD의 업데이트 반영 로직은, AWS ECR을 직접 지켜보는 것이 아닌, IDC GitLab k8s 레포지토리를 지켜보도록 되어 있음
    • 이것만 아니면 ECR 배포만 해서 자동으로 배포까지 이어질텐데, 이 부분 때문에 결국 사내망을 한번은 접속해서 commit을 해줘야 함
    • 어차피 QA 서버 확인하려면 사내망 접속하긴 해야 되니깐 뭐..
    • 언제될지 모르겠지만.. 방화벽 문제 해결 되면 이것도 해결 되긴 할 듯..
  • 그래서 위 Jenkins Pipeline에서 Push to ECR까지만 GitHub Action으로 옮기고, 마지막 Tag Update하는 부분만 Jenkins에 남겨둔다

QA 배포 자동화 스크립트

name: AWS EKS Auto-Deploy (QA)

on:
  pull_request:
    branches:
      - 'develop'

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node: [14]

    outputs:
      version: ${{ steps.set-version.outputs.version }}
      tag: ${{ steps.set-version.outputs.tag }}

    steps:
      - name: Git Clone, Checkout
        uses: actions/checkout@v3

      - name: Set up Docker Buildx for Docker Cache
        uses: docker/setup-buildx-action@v2

      - name: Cache Dependencies
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node }}
          cache: 'yarn'

      - name: Check Build Environments
        run: npx envinfo

      - name: Install Dependencies
        run: yarn install --no-progress --emoji=false

      - name: Build App
        run: |
          rm -rf node_modules/.cache
          yarn build

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_QA }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_QA }}
          aws-region: ap-northeast-2

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: Set ENVs
        id: set-version
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: finance/finance-front/dev
          version: ${{ steps.set-version.outputs.version }}
        run: |
          version=$(jq -r .version package.json)-$(date '+%Y%m%d-%H%M%S')
          echo "::set-output name=version::$version"
          echo "::set-output name=tag::$ECR_REGISTRY/$ECR_REPOSITORY:$version"

      - name: Build, tag, and push image to Amazon ECR
        uses: docker/build-push-action@v3
        with:
          context: .
          push: true
          tags: ${{ steps.set-version.outputs.tag }}
          file: qa.github.Dockerfile

      - name: Check Container Tag
        env:
          version: ${{ steps.set-version.outputs.version }}
        run: echo version $version
  • GitHub Action의 장점 중 하나는 이미 다른 사람들이 만들어놓은 Workflow를 플러그인처럼 갖다 붙일 수 있다는 것
    • 직접 Git Checkout 하고, Node, Docker 환경설정하고 ECR login-push 하는 스크립트를 짤 필요 없이,
    • 그냥 갖다 쓰기만 하고 parameters만 잘 넣어주면 된다
  • 또한 의존성(node_modules) 설치한 것들을 캐싱해서 쓸 수 있다는 것
    • 물론 특정 디스크 용량 초과할 경우 비용 발생할 수 있음
    • actions/setup-node@v3을 활용해 node_modules 디렉토리를 캐싱할 수 있는데, 쌩으로 yarn install 했을 때 1분~1분 30초 정도 소요된다면, 캐싱을 통해 30초 내외로 시간을 단축할 수 있다
    • yarn PnP Zero-installs 설정을 하게되면, 3초 내외로 단축할 수 있다
      • node_modules 디렉토리 전부를 git에 넣어버리는 셈이라..
      • zip 파일들이라 용량도 크게 줄어든다

변수 설정

  • 나름 제일 까다로웠던 (?) 부분
  • jq를 활용한 version tag 생성
    • container tag를 생성할 때, 기존처럼 버전을 직접 입력하는게 아니라 package.json에 있는 버전을 가져와서 자동으로 생성하도록 만들고 싶었음
    • 처음엔 어떻게 해야 될지 몰라서, shell에서 package.json 파일 내용을 출력한 다음 정규식으로 version에 해당하는 값을 가져올 수 있는 방법을 찾아봤음
    • 많은 개발 지식은 물론 쓰레기도 섞여있는 stackOverflow를 보면서 이런저런 삽질을 하다보니 jq라는 라이브러리로 json parsing이 가능하다는 걸 알게 됨
    • version=$(jq -r .version package.json)-$(date '+%Y%m%d-%H%M%S')
      • 이 스크립트로 프로젝트 최상단 package.json에 있는 version 값과 현재 datetime 값을 합쳐 tag 생성
  • outputs을 활용한 전역변수 생성
    • 각 step마다 env를 설정할 수 있는데 이건 그 step에서만 사용할 수 있는 지역변수임
    • 전역변수 env를 선언할 수도 있긴한데, 위 tag처럼 동적으로 생성하는 건 안됨. 상수만 가능한 듯
    • 그래서 동적으로 전역변수를 선언하려면 outputs를 활용해야 함
    • [전역변수명]: ${{ steps.[stepId].outputs.[변수명] }}
      • jobs.outputs에서 전역변수명들을 선언하고, 어떤 step에서 어떤 output으로 나올 건지 알려준다
    • echo "::set-output name=[변수명]::$version"
      • 해당 step에서 변수명에 해당하는 값을 대입해준다

Docker Build; Image 사이즈 최적화

  • GitHub Action에서 사용할 수 있는 OS는 Ubuntu, MacOS, Windows 3가지임

  • GitHub Action에서 빌드하고, 빌드 결과물, 최소한의 필요한 의존성만 Docker로 Copy해서 넘김

    • Docker base OS가 alpine이긴한데, Ubuntu에서 빌드해도 문제 안됨
    • 문제되는 경우는 Window - Unix 계열 end of line 문자가 달라서 빌드 파일에 영향을 미치거나,
    • CPU architecture가 intel - arm 달라서 Esbuild, Parcel (SWC), node-sass 등 일부 빌드 관련 패키지가 아예 달라지는 경우 등
      • Rust는 하루 빨리 M1을 지원하라..!!
    • 그거 말고는 아직 못봤음, 암튼 동일한 intel CPU면 Ubuntu - Alpine 차이 정도는 문제 안됨
  • GitHub Action도 Container 환경(아마도 99% 확률로 Docker?)에서 실행되는 거라, 기존처럼 Docker-in-Docker 방식으로 빌드하면 속도가 더 느려질 듯

  • 이때 Docker Cache는 굳이 쓸 필요가 없음

Container Tag

  • 여기서 만든 Docker Container Tag를 출력하고,
  • 이 tag를 복사해서, 사내 Jenkins에서 tag만 업데이트하는 pipeline을 실행시킨다

조심해야 할 것

GitHub Action 월간 사용량

image.png

출처: docs.github.com/en/billing/managing-billing..

  • Pro/Team Plan 월 3,000분까지 무료, 그 이후부터는 분당 과금됨
  • ChartIQ 패키지 추가하면서 빌드 한번에 10분씩 걸리는데..
    • 월 20일 출근 X 하루 15번 빌드 X 빌드 1회당 10분 => 3000분
    • 모바일 파트쪽 빌드/테스트하는 것까지 포함하면 월간 사용량이 꽤 될 것 같아서 조금 걱정됨..
    • 사실 걱정 보다는 나중에 시간 줄이라고(돈 아끼라고) 얘기 나올 거 같아서 귀찮아질 것 같음
    • => PR 만들어놓고 1 commit - 1 push 보다는 commit 어느정도 쌓인 후 push 하는게 좋을듯..

AWS ECR 비용 문제

  • 다행히도 ECR inbound 네트워크 비용은 발생하지 않는 듯 image.png

    출처: aws.amazon.com/ko/ecr/pricing

    • 자동배포를 하면서 QA 서버 빌드는 PR / push 할 때마다 Docker 이미지가 GitHub -> AWS ECR로 날아감
    • 이때 프로젝트에 따라 Docker image 크기는 적게는 300mb 많게는 500 mb 내외
    • 당장은 데이터 수신 비용이 $0 여서 이건 문제가 안됨
  • 만약 나중에 AWS에서 돈을 받겠다고 한다면?

    • AWS CodeBuild, CodeDeploy 도입을 고려해볼 수 있을 듯
      • AWS 동일 region 내 네트워크 비용은 발생하지 않으므로..
      • 근데 CodeBuild는 월간 무료 사용량 100분 밖에 안되기 때문에, 비용 비교를 잘 해봐야 할 듯

좀더 개선해 볼 것

Cron으로 정기 배포 (Xvezda님 의견 👍)

  • Jenkins에서 어쨌든 k8s 레포지토리 태깅 커밋은 한번 해줘야 함
  • QA 서버 정도는 5분~10분 단위로 Cron 걸어서 자동 배포해줘도 크게 문제 없을 듯
  • Cron은 Jenkins에서 해도 되고, GitLab CI에서 해도 됨
    • 개인적으로는 이것도 GitLab CI가 좀더 편함.. (서버시간 엉망인것만 빼고..)
  • Prod 서버는 그래도 QA 서버에서 충분히 확인하고 수동으로 배포하는게 좋지 않을까..

페이지 마다 모노레포 패키지로 나눠서 빌드 시간을 줄여보자 (somedaycode님 의견 👍)

  • turborepo 적용해 패키지 병렬 빌드 가능하도록 함
  • domain / BE 패키지는 5~15초 내외 소요되고 있어서, 병렬 빌드하는 이점이 크게 없음
  • 문제는 FE 빌드시간인데, Chart 빌드 => FE:CSR 빌드 => FE:SSR 빌드 순서가 반드시(;;) 지켜져야하는 상황
    • ChartIQ 라이브러리 사이즈가 꽤 되다보니, Chart~FE 빌드만 7~8분 정도 소요됨
  • Chart 패키지 쓰는 페이지(상세페이지) - 나머지 페이지 나눠서 병렬 빌드하면 이 문제가 좀 해결될 수도?
    • 좀 큰 공사가 될수도 있어서 겁남..
    • Next/Nuxt 쓰면 알아서 해결될 문제긴 한데.. 아무튼..

references

resources