실제로 한 팀원이 EC2 t2g.micro에 젠킨스 CI/CD를 적용했다가, 메모리가 부족해 서버가 죽는 과거의 경험을 공유해주었습니다.
때문에 적어도 CI 만큼은 GitHub Actions로 분리해 테스트 실행을 외부 서비스에 위임하기로 했습니다.
하지만 그렇다면, 간결한 파이프라인을 위해 CI/CD를 모두 GitHub Actions로 통합하는 것이 좋다고 판단했습니다.
EC2에서 ./gradlew build를 수행해 직접 빌드 및 테스트를 할 경우 아래와 같이 총 26.2%의 사용량이 확인되지만, ec2-build-usage.png
그 대신 Actions Runner가 가동하는 상황을 확인해본 결과, 전체 메모리의 최대 11.9%만을 사용함을 알 수 있었습니다. actions-runner-usage.png
쉽고 빠른 적용
GitHub 레포지토리에서 바로 설정이 가능해 기존 개발 환경과 통합해 사용 가능합니다.
YAML 파일 작성만으로도 쉽고 빠른 적용이 가능합니다.
GitHub가 Runner를 관리해주기 때문에, 서버 관리 부담이 덜합니다.
CI/CD 파이프라인
CI/CD 파이프라인은 다음과 같습니다.
Pull Request의 생성 또는 업데이트, Pull Request에 대한 Merge가 발생합니다.
이벤트에 트리거된 GitHub Actions workflow가 실행됩니다.
workflow에 작성한 내용에 의해, GitHub hosting Runner가 빌드 및 테스트를 수행합니다.
이 때, 테스트 결과를 PR 코멘트로 등록해줍니다.
Pull Request의 생성 또는 업데이트에 대한 workflow의 경우 이 단계까지만 수행합니다.
Pull request Merge에 대한 workflow의 경우, 빌드한 결과물을 GitHub의 자체 저장소인 Artifact Storage에 업로드합니다.
우리의 EC2에 설치된 Self Hosted Runner가 4번의 결과물을 다운로드 받습니다.
Self Hosted Runner가 배포 스크립트를 실행하게 하여, 필요한 어플리케이션을 실행하고 정적 파일을 적절한 위치에 배치합니다.
2번부터 6번까지, 모두 하나의 workflow 내에서 벌어지는 일입니다.
적용 방법
GitHub Actions workflow
workflow란 하나 이상의 작업을 실행하는, 자동화된 프로세스입니다.
레포지토리의 .github/workflows 디렉토리에 YAML 파일을 저장해 workflow 를 정의할 수 있습니다.
하나의 레포지토리는 여러 개의 workflow를 가질 수 있습니다.
괜찮을지도 서비스에서는
pull request에 대한 빌드 및 테스트만 수행하는 상황 vs pull request merge 시 배포까지 수행하는 상황
대상이 main 브랜치 vs dev 브랜치
대상이 프론트엔드 / 백엔드
세 가지 상황을 고려하여 여러 개의 workflow를 작성했습니다.
workflow 만들기
workflow는 기본적으로 아래와 같은 요소들을 가져야 합니다.
workflow를 유발시킬 하나 이상의 이벤트 : on
workflow에서 수행할 하나 이상의 작업: job
job 블록 내에 단계 별 스크립트 또는 익스텐션 실행을 정의: step
여기서 주의할 점은, 여러개의 job은 기본적으로 병렬적으로 처리된다는 것입니다.
순차적으로 실행하고 싶다면, needs 블록을 사용해야 합니다.
아래는 현재 적용된 workflow입니다. 이해를 돕기 위해 주석을 추가하였습니다.
백엔드 CI workflow
name: Backend CI For Test Validation
# 트리거 설정on:workflow_dispatch:pull_request:branches:[ main, develop ]paths: backend/**# 해당 디렉토리 내 파일의 변경이 있을 때만 트리거됨# 테스트 결과 등록을 위한 권한 설정permissions:pull-requests: write
checks: write
contents: write
# 수행할 작업들jobs:build-and-comment:# 해당 job을 실행할 환경 설정runs-on: ubuntu-22.04steps:-uses: actions/checkout@v3
-name: Set up JDK 17
uses: actions/setup-java@v3
with:java-version:'17'distribution:'temurin'-name: gradlew 실행 권한 부여
run: chmod +x gradlew
working-directory: backend
-name: Gradle build 시작
run: ./gradlew clean build
working-directory: backend
-name: 테스트 결과를 PR에 코멘트로 등록합니다
uses: EnricoMi/publish-unit-test-result-action@v1
if: always()
with:files:'backend/build/test-results/test/TEST-*.xml'-name: 테스트 실패 시, 실패한 코드 라인에 Check 코멘트를 등록합니다
uses: mikepenz/action-junit-report@v3
if: always()
with:report_paths:'**/build/test-results/test/TEST-*.xml'token: ${{ github.token }}
백엔드 CD workflow
name: Backend develop CI/CD
on:workflow_dispatch:pull_request:# 배포 설정은 main, develop 브랜치 별로 다르게 트리거branches:[ develop ]# 브랜치가 닫힐 때만 트리거types:[closed]paths: backend/**permissions:contents: read
jobs:build-and-upload:# Pull Request를 그냥 닫은 게 아니라, merge해서 닫았을 때만 실행if: github.event.pull_request.merged
runs-on: ubuntu-22.04steps:-uses: actions/checkout@v3
-name: Set up JDK 17
uses: actions/setup-java@v3
with:java-version:'17'distribution:'temurin'-name: gradlew 실행 권한 부여
run: chmod +x gradlew
working-directory: backend
-name: Gradle build 시작
run: ./gradlew clean build
working-directory: backend
-name: jar 파일 artifact에 업로드
uses: actions/upload-artifact@v3
with:name: BackendApplication
path: backend/build/libs/mapbefine.jar
deploy:# Pull Request를 그냥 닫은 게 아니라, merge해서 닫았을 때만 실행if: github.event.pull_request.merged
# deploy는 build-and-upload 작업과 달리 self-hosted runner를 사용# main, develop 브랜치마다 각각 운영 서버, 개발 서버의 runner만을 실행하도록 해야 함runs-on:[ self-hosted, dev ]를
# build-and-upload 작업이 성공적으로 완료될 경우 실행needs: build-and-upload
steps:-name: 구버전 jar 파일 삭제
run: rm -rf /home/ubuntu/backend/build/*.jar-name: jar파일 artifact에서 다운로드
uses: actions/download-artifact@v3
with:name: BackendApplication
path: /home/ubuntu/backend/build/
-name: 배포하기
run: /home/ubuntu/backend/deploy.sh
-name: 슬랙 메시지 보내기
uses: 8398a7/action-slack@v3
with:mention:'here'if_mention: always
status: ${{ job.status }}fields: workflow,job,commit,message,ref,author,took
env:SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}if: always()
프론트엔드 CI workflow
name: Frontend CI For Test Validation
# 어떤 이벤트가 발생하면 실행할지 결정on:#pull request open과 reopen 시 실행한다.pull_request:branches:[main, develop]paths: frontend/**defaults:run:working-directory: ./frontend
jobs:jest:runs-on: ubuntu-22.04steps:-name: Checkout repository
uses: actions/checkout@v2
-name: Setup Node.js
uses: actions/setup-node@v2
with:node-version:"18"-name: Install node modules
run: npm install
-name: Run Jest test
run: npm run test
앞서 말씀드린 파이프라인 대로 배포를 하기 위해서는, 배포를 할 서버에 GitHub Actions의 Self-hosted Runner를 설치해야 합니다.
앞서 설명드린 workflow는 GitHub에서 호스팅하는 Runner가 수행할 수도 있지만,
원하는 서버에 자체적으로 Runner를 호스팅하여 사용할 수도 있습니다.
workflow의 runs-on 블록에서 어떤 Runner를 통해 작업을 수행할지 정의합니다.
EC2에 Self-hosted Runner 띄우기
1. 다운로드
레포지토리의 Settings - Actions - Runners에 들어가면 New self-hosted runner 버튼이 있습니다. create-runner.png
버튼을 누르면 아래와 같은 설정 페이지로 접속하는데, Runner를 띄울 서버 환경에 맞는 설정을 선택해줍니다. architecture-runner.png
해당 환경의 터미널에서 runner package를 다운로드합니다. 선택한 설정에 맞게 제공된 커맨드를 복사해 실행하면 됩니다. download-runner.png
아래는 Linux, ARM64 환경의 runner를 다운로드받기 위한 스크립트입니다.
# Create a folder
$ mkdir actions-runner &&cd actions-runner
# Download the latest runner package
$ curl-o actions-runner-linux-arm64-2.307.1.tar.gz -L https://github.com/actions/runner/releases/download/v2.307.1/actions-runner-linux-arm64-2.307.1.tar.gz
# Optional: Validate the hash
$ echo"01edc84342ef4128a8f19bc2f33709b40f2f1c40e250e2a4c5e0adf620044ab3 actions-runner-linux-arm64-2.307.1.tar.gz"| shasum -a256-c# Extract the installer
$ tar xzf ./actions-runner-linux-arm64-2.307.1.tar.gz
2. Runner 생성 및 설정
마찬가지로 제공된 스크립트에 따라, runner를 생성하고 설정합니다. configure-runner.png
먼저 아래 커맨드를 실행하면, 아래와 같이 Runner 환경 설정을 위해 몇 가지를 입력해야 합니다.
$ ./config.sh --url${repository URL}--token${runner 생성 페이지에서 제공된 토큰}
runner-registration.png
위 이미지처럼 name, 추가 label을 지정하고 설치한 runner를 확인하면, 다음과 같이 dev 라벨이 추가된 것을 확인할 수 있습니다.
runner-label.png
⚠️ 개인 로컬에서 테스트로 만든 예시 이미지로, 실제 서비스의 운영 환경과는 상이합니다.
더 빠른 설정을 원한다면, 아래와 같이 ./config.sh 실행 시 원하는 값들을 바로 전달해줘도 됩니다.
$ ./config.sh --url${repository URL}--token${runner 생성 페이지에서 제공된 토큰}--nametest--labels dev
label을 왜 추가했을까요?
각 Runner를 구분할 수 있는 label을 지정해서 runs-on 블록에서 어떤 Runner에게 일을 시킬지 명시하기 위함입니다.
괜찮을지도 팀은 한 리포지토리에서 운영 서버, 개발 서버에 각각 띄울 두 개의 Runner를 사용하기 때문에
이를 구분하기 위한 dev, prod label을 지정하였습니다.
아래는 dev, self-hosted 라벨을 가진 runner를 지정하는 예시입니다.
runs-on:[ self-hosted, dev ]
이미 생성한 Runner에 label을 추가하려면?
GitHub 공식문서에 따르면, 2023년 2월부터 GUI로 label을 추가하는 기능을 제공하지 않는다고 합니다.
config.sh 파일을 수정해 설정할 수도 없습니다.
대신 GitHub에서 제공하는 "Self-hosted runners" REST API를 사용해 추가해주어야 합니다.
3. Runner 실행
Runner 설정이 완료되었다면, 아래 명령어로 Runner를 실행합니다.
레포지토리의 Settings - Actions - Runners 페이지에서 Status가 idle로 바뀌었다면 정상적으로 실행된 것입니다.