관리 메뉴

Optimal Solution

CI/CD - 빌드 자동화 기초 본문

CICD

CI/CD - 빌드 자동화 기초

ICE_MAN 2024. 9. 1. 02:42
728x90

빌드 자동화 관련 환경 설명

나는 빌드 자동화를 구축해보기 위해 아래와 같은 환경으로 작업했다.

- 언어 : Java

- 버전 관리 : Github

- 빌드 도구 : Gradle

- 운영체제 : Windows

 

Github에서 빌드 자동화를 구축하는 방법 - Github Action

Github에서 CI/CD를 구축하려면 Github Action을 사용한다.

Github Action은 Github에서 제공하는 CI/CD 툴이다.

완전 순수 100% 내가 다 CI/CD 환경을 구축해보면 좋은 경험이겠으나, 이를 위해선 CI/CD를 위한 서버를 구성해야 한다는 어려움이 있다.

Github Action은 CI/CD를 위해 필요한 서버를 제공해주기 때문에 부담이 적고 간편하게 설정할 수 있다는 장점이 있다.

 

Github Action의 핵심 컴포넌트

Workflow

Workflow는 Github 레파지토리에서 정의하는 자동화된 프로세스를 의미한다.

이는 특정 이벤트(ex - PUSH, PR, etc)에 대한 응답으로 실행되는 하나 이상의 작업으로 구성된다.

 

레파지토리의 .github/workflows 디렉토리 하위에 있는 YAML 파일(.yaml or .yml)을 사용해 Workflow를 정의할 수 있다.

Workflow를 트리거하는 이벤트로는 아래와 같은 것들이 있다.

- push : 브랜치로 PUSH가 발생하는 경우

- pull_request : PR이 생성, 업데이트 또는 종료되는 경우

- schedule : 특정 시간이 도래하는 경우(cron 구문 사용)

- workflow_dispatch : 사용자가 수동으로 트리거

- repository_dispatch : API를 통한 외부 이벤트에 의해 트리거

 

Job

Job은 동일한 Runner에서 실행되는 일련의 Step를 의미한다.

Job은 Workflow의 핵심 구성 요소이며, 기본적으로 병렬로 실행된다.

 

Job은 다른 Job에 종속될 수 있다. 즉, 한 Job이 시작되기 전에 다른 Job이 완료될 때까지 기다릴 수 있다는 것이다.

각각의 Job은 Runner라고 하는, Job 단계가 실행되는 가상 머신 또는 컨테이너에서 실행된다.

Runner는 Workflow에 정의된 Job을 실행하는 서버라고 볼 수 있다.

Github는 호스팅된 Runner를 제공하거나, 자체적으로 호스팅한 Runner를 사용할 수 있다.

 

Step

Step은 Job 내의 개별 작업을 의미한다.

각 Step은 단일 명령 또는 동작을 수행한다.

 

Step은 Job 내에서 순차적으로 실행된다.

쉘에서 직접 명령을 실행하거나, Github 마켓플레이스에서 미리 정의된 Job을 사용할 수 있다.

레파지토리 체크아웃, 빌드 스크립트 실행 또는 테스트 명령 실행 등은 일반적으로 사용하는 Step에 해당한다.

 

Action

Action은 Workflow 내에서 특정 작업을 수행하는 재사용 가능한 모듈식 코드 단위를 의미한다.

이는 Github Action의 기본 구성 요소이며, 커뮤니티에서 만들어지거나 Github 마켓플레이스에서 사용할 수 있다.

 

Action의 유형으로는 아래와 같은 것들이 있다.

- Docker 컨테이너 Action

  - Docker 컨테이너에서 실행되며 'Dockerfile'로 정의된다.

- JavaScript Action

  - JavaScript로 작성되며 Runner 환경에서 직접 실행된다.

- Composite Action

  - 여러 Step과 Action을 하나의 작업으로 그룹화하여 재사용 가능하고 모듈식으로 만들 수 있다.

 

일반적인 Action에는 코드 체크아웃(actions/checkout), 프로그래밍 언어 환경 설정(ex - Node.js의 경우 actions/setup-node) 또는 클라우드 서비스에 코드 배포 등이 있다.

 

Github Action 사용하기

빌드 자동화 목표

나의 경우, 빌드 자동화의 목표를 아래와 같이 설정했다.

 

1. 특정 브랜치(ex - feature 브랜치, develop 브랜치, hotfix 브랜치 등)에
2. 소스코드(src 디렉토리 하위 리소스들)에 대해서
3. PUSH가 발생하면

빌드한다.

 

Workflow를 정의할 yaml 파일 생성하기

Github Action은 Workflow를 정의하기 위해 yaml 문법을 사용한다고 했다.

빌드 자동화에 대한 yaml 파일의 파일명은 'java-builder'로 하려고 했는데, 확장자의 종류가 yaml, yml 두 가지로 혼용되어 쓰이는 것 같아서 확인해봤다.

 

과거에는 3글자의 확장자명을 쓰는 것이 하나의 유행이었다고 한다.

마치 지금 회사의 오래된 시스템에 접속할 때 .html이 아닌 .htm으로 접속 URL에 표시되는 경우처럼, .yaml이나 .yml이나 큰 차이는 없다고 한다.

테스트삼아서 yml 파일 확장자명을 yaml로 변경했을 때에도 똑같이 Workflow는 수행된다.

.yml이나, .yaml이나 매한가지로 Workflow는 돌아간다.

 

각설하고, yaml 파일의 위치는 레파지토리의 root 디렉토리 기준으로, '.github/workflow/' 하위에 추가되어야 한다.

IntelliJ 상에서 (친절히) 캡처해 보여주자면, 아래와 같다.

.github/workflows/ 라는 2개의 폴더를 생성한 후 그 아래에 .yml 파일을 생성한다.

 

빌드를 자동화하는 yaml 파일 작성하기

단계별로 yaml 코드를 보여주면서 설명하도록 하겠다.

나는 yaml 문법 강사가 아니기 때문에, 내 java-builder.yml에서만 사용한 문법만 설명할 것이다.

 

1 - Workflow의 이름을 정의한다.

name: Java Builder

 

지금은 Java를 빌드하는 Workflow 하나만을 만들 것이지만, Workflow를 여러 개를 만들어서 사용하는 경우도 있을 것이다.

그렇다면 Github의 Action 탭 상에서, 이 Workflow들이 각각 어떤 Workflow인지를 어떻게 구분할 것인지를 정해줄 '이름'이 필요하다.

 

이렇게 정한 name은, Action 탭 상에서 실행된/실행 중인 Workflow가 무슨 작업을 하는 Workflow인지 구분짓는 것을 쉽게 만들어준다.

Java Builder라는 Workflow 3개가 정상적으로 수행완료되었음을 보여준다.

 

2 - Workflow가 트리거되는 조건을 정의한다.

# Trigger the workflow on push to specific branches and folders
on:
  push:
    branches:
      - 'feature/*'  # Any branch starting with 'feature'
      - 'develop'    # Develop branch
      - 'release/*'  # Any branch starting with 'release'
      - 'hotfix/*'   # Any branch starting with 'hotfix'
      - 'main'       # Main branch
    paths:
      - 'src/**'     # Trigger only when changes are made in the 'src' folder

 

나는 우선은 학습하는 입장이기 때문에, git flow 전략에서 사용되는 브랜치명로, src 디렉토리 하위에 일어난 변경에 대한, PUSH가 발생하면 Java 빌드가 실행되도록 정의하기로 했다.

 

yaml의 문법을 딱히 설명하고 싶지는 않은데, yaml에서는 들여쓰기가 매우 중요하다.

들여쓰기를 통해 계층 구조를 파악하기 때문에, 2칸의 들여쓰기를 반드시 지켜야 한다.

 

yaml의 장점인데, 위의 코드를 보면 너무나도 직관적이다.

그렇지만 설명해보겠다.

 

'on' : 이 Workflow를 실행할 조건을 정의한다.

'push' : PUSH가 발생하면 이 Workflow를 실행한다.

'branches' : feature, develop, release, hotfix, main 브랜치에 PUSH가 발생하면 이 Workflow를 실행한다.

'pathes' : src 디렉토리 하위에 변경이 일어났고 이에 대한 PUSH가 발생하면 이 Workflow를 실행한다.

 

여기서, branches와 pathes는 같은 계층에 있는 조건이기 때문에 AND 조건으로 취급된다.

 

내가 java-builder.yml을 구현해보면서 배운 한 가지 사실이 있는데, 궁금하면 읽어봐도 된다.

짧다.

더보기

주석에 보면 'Any branch starting with '~'' 라고 되어있는데, 이는 사실이 아니다.

저 주석대로라면 feature1 브랜치를 PUSH해도 Workflow가 실행되어야 한다.

 

이건 내 브랜치 네이밍 컨벤션과도 관련이 있다.

나는 브랜치명을 (브랜치 타입)/#(이슈 번호)-(이슈 설명) 형태로 작성한다.

 

그런데 처음에 branch 조건에 'feature*' 라고 작성한 후 feature/#1-~~~ 브랜치를 PUSH했는데 Workflow가 실행되지 않는 것이었다.

 

'feature*'라고 branch 이름 조건을 건다면, '/'를 사용했을 때 인식하지 못하게 된다.

인터넷을 뒤져봤는데 아무도 안 알려주는 것 같아 좀 화가 났는데, 이게 원인일거라 생각하고 추측해 수정했더니 잘 됐다.

 

3 - Workflow 내에서 처리될 Job의 이름, Job의 실행 환경을 정의한다.

# Define the jobs to run
jobs:
  build:
    runs-on: ubuntu-latest

 

지금까지 Workflow의 이름과 동작 조건을 정의했다면, 이제는 이 Workflow에서 수행해야 할 Job, 즉 작업을 정의할 차례이다.

그렇지만 작업을 정의하기에 앞서 정의해야 할 몇 가지가 있다.

 

Workflow와 Job은 1:1 대응 관계가 아니다.

하나의 Workflow 안에 여러 Job들이 정의되어 있을 수도 있다는 것이다.

 

그렇다면 Workflow의 이름을 정의한 것과 마찬가지로 각각의 Job의 이름을 정의해야 할 것이다.

Job의 이름을 정의하는 것은 매우 간단하다.

 

jobs 태그 바로 하위에 작성하는 것이 바로 Job의 이름이 된다.

이 경우에는 'build'가 Job의 이름이 되는 것이다.

 

그 후, 이 'build'라는 Job은 어떤 환경에서 실행되어야 하는지, 즉 어떤 Runner에 의해 실행될 것인지 정해야 한다.

이를 정의해주는 부분이 바로 'runs-on'이다.

'ubuntu-latest'를 Runner로 정의해 우리는 이 'build'라는 작업을 가장 최신의 우분투 가상 환경에서 수행하게 될 것이다.

 

가장 최근의 우분투 버전이 아닌 특정 우분투 버전을 지정할 수도 있고, Windows나 macOS와 같은 환경도 지정해줄 수 있다.

 

4 - Job의 상세한 Step을 정의한다.

이 부분은 어떠한 작업을 수행하는 Step인지, 각 파라미터의 키와 값이 의미하는 바가 무엇인지 하나하나 설명하고 실행 결과를 Github Action 탭에서 확인해보며 설명하기 위해서 Step별로 명시하겠다.

큰 틀은, 결국 Java를 빌드하는 단계에 대한 기술이다.


    steps:
      # Checkout the latest code from the repo
      - name: Checkout code
        uses: actions/checkout@v3

 

첫째로, Job 내의 Step들에 대해 정의하는 'steps' 태그이다.

가장 먼저 이뤄지는 Step은 코드에 대한 체크아웃이다.

 

영어로 체크아웃이라고 하면 조금 생소할 수 있지만, 이를 한국어로 번역하면 '반출'이다.

반출이라 함은 어딘가에 저장된 무언가를 꺼내온다는 뜻이다.

즉, 레파지토리의 코드(뿐 아니라 모든 리소스들)를 꺼내오는 과정이다.

 

첫 번째 Step의 이름은 'Checkout code'이며, 이 Step에서는 'actions/checkout@v3'를 사용한다고 정의했다.

'uses' 태그는 Github Actions 마켓플레이스에 정의된 Action을 사용하고자 할 때 사용한다.

즉, 'actions/checkout@v3'는 Github Actions 마켓플레이스에 정의된 Action이다.

 

'actions/checkout@v3'는 현재의 Workflow를 트리거한 현재 레퍼런스(브랜치, 태그 혹은 커밋 SHA)에서 레파지토리의 콘텐츠를 반출하는 작업을 수행한다.

 

체크아웃하는 작업은 필수적이다.

Runner는 가상 환경이다.

당신의 코드가 Runner에 있는가 ? 당연히 없다.

그렇다면 당신의 코드를 Runner에 가져다 놓아야 하지 않겠는가 ?

'actions/checkout@v3'는 당신의 코드를 Runner에 가져다 두는 작업을 수행해주는 것이다.

'actions/checkout@v3'라는 Github Action 마켓플레이스에 등록된 Action을 사용해 레파지토리의 코드 등 콘텐츠를 반출한다.


      # Set up JDK 11
      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'  # You can choose different distributions like 'zulu'

 

그 다음은 Runner 환경에서 사용될 JDK를 세팅하는 Step이다.

 

'name'과 'uses' 태그는 위에서 설명했으니 이해할 수 있을 것이다.

Step의 이름은 'Set up JDK 11'이고, 'actions/setup-java@v3'라는 Github Actions 마켓플레이스에 등록된 Action을 가져다 쓸 것이다.

 

'with' 태그는 Action에 입력 매개변수를 전달하는 데 사용된다.

'actions/setup-java@v3'라는 Action에서는 Java의 버전과, 어느 배포 버전을 사용할 것인지를 매개변수로 전달받는다.

 

우리가 프로젝트에서 사용할 JDK를 구성할 때를 생각해보자.

우리는 어느 버전의 Java를 사용할 것인지(Java 1.8, 11, 17 등등)와 어디서 제공해주는 Java를 사용할 것인지(Oracle, Zulu, AdoptOpenJDK 등등)를 결정하고 이를 적용한다.

 

Runner 환경에서 Java를 빌드하기 위해서, 우리가 프로젝트에서 하던 것들과 마찬가지로 우리는 JDK 환경을 구성해 주는 것이다.

'actions/setup-java@v3'라는 Action을 사용해 Runner에서 사용할 JDK를 구성한다.


      # Ensure Gradle wrapper has execute permission
      - name: Make Gradle wrapper executable
        run: chmod +x ./gradlew

 

그 다음은 Gradle 빌드를 위해 gradlew에 실행 권한을 부여하는 Step이다.

 

Gradle을 사용해 빌드하는 경우, gradlew라는 Gradle Wrapper를 실행하게 된다.

그런데 만일 gradlew에 대한 실행 권한이 없다면 ? 예상 가능하겠지만 Permission denied라는 메시지를 출력하며 실행되지 않을 것이다.

 

나 또한 gradlew에 대한 Execution Permission이 없어 테스트 중 실패한 경험이 있다.

디버깅을 위해 'ls -al' 명령어를 실행하는 Step을 추가해 확인해 본 결과, 실제로 gradlew에 대한 실행 권한이 없었다.

Checkout 직후 gradlew는 rw-r--r--, 즉 권한이 644이다.

 

위 Step을 추가한 후, chmod 명령어 이후 'ls -al'을 실행시켜보면 아래와 같은 결과로 변경된다.

chmod +x는 실행 권한을 추가한다는 의미이고, chmod 직후 gradlew는 rwxr-xr-x로 권한이 변경되었다.


      # Build with Gradle
      - name: Build with Gradle
        run: ./gradlew clean build

 

마지막으로 build를 실행하는 Step이다.

 

명령어에 포함되어 있는 clean은 빌드 디렉터리(build/)를 삭제해 이전 빌드의 output을 모두 제거하는 작업이다.

이렇게 하면 이전 빌드에서 남은 파일 없이 새로 빌드를 시작할 수 있다.

혹여 모를 오래된 파일 또는 충돌하는 파일로 인한 문제를 방지하는 데 주로 사용된다.


아래는 전체 yml 파일이다.

name: Java Builder

# Trigger the workflow on push to specific branches and folders
on:
  push:
    branches:
      - 'feature/*'  # Any branch starting with 'feature'
      - 'develop'    # Develop branch
      - 'release/*'  # Any branch starting with 'release'
      - 'hotfix/*'   # Any branch starting with 'hotfix'
      - 'main'       # Main branch
    paths:
      - 'src/**'     # Trigger only when changes are made in the 'src' folder

# Define the jobs to run
jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      # Checkout the latest code from the repo
      - name: Checkout code
        uses: actions/checkout@v3

      # Set up JDK 11
      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'  # You can choose different distributions like 'zulu'

      # Ensure Gradle wrapper has execute permission
      - name: Make Gradle wrapper executable
        run: chmod +x ./gradlew

      # Build with Gradle
      - name: Build with Gradle
        run: ./gradlew clean build

 

 

 

Workflow 트리거하기

위에서 만든 java-builder.yml로 정의되는 Workflow는,

1. feature/develop/release/hotfix/main 브랜치에

2. src 디렉토리 하위에 코드 변경이 일어나

3. PUSH가 발생하면

트리거된다.

 

'feature/#1-build-automation'이라는 브랜치를 생성하고,

'Hello world!'를 출력하는 아주 간단한 Java 프로젝트를 레파지토리에 추가하여

PUSH를 하면 Workflow가 트리거되어 빌드가 수행된다는 의미이다.

 

실제로 그렇게 수행했더니, Actions 탭에서 내 Workflow가 잘 실행된 것을 확인할 수 있었다.

Actions 탭에서 나의 Workflow가 잘 돌아갔다.

 

매우 간단한 Workflow지만 이를 잘 활용하면 효율이 크게 올라갈 것 같긴 하다고 느꼈다.

물론 지금 처음 CI/CD를 구성하는 것이기 때문에, CI/CD를 사용하지 않을 때보다 이것 저것 공부하고 설정할 것이 많아 번거롭게 느껴질 법도 하다.

하지만 한 번 공부해둬서 앞으로 쭉 편할 수 있다면 공부하는 게 맞다고 생각한다.

 

다음에는 간단한 계산기 프로그램을 만들고, 이를 테스트하는 테스트 코드를 작성해 테스트와 관련한 CI 작업을 마친 후 포스트로 찾아오겠다.

728x90

'CICD' 카테고리의 다른 글

CI/CD - 빌드 자동화 개념  (0) 2024.08.31
CI/CD - 기본 개념  (0) 2024.08.31