Github Actions로 배포 자동화 파이프라인 구축하기

옹심이·2024년 12월 24일
0
post-thumbnail

CI/CD는 Jenkins 쓰는거 아니야?

주변에서 Jenkins를 많이 사용하는 것을 보고 처음에는 막연히 Jenkins를 도입하려고 했다. 하지만 Jenkins는 별도의 서버 구축이 필요해 비용이 발생한다는 단점이 있었다. 반면 Github Actions는 Github에 내장된 CI/CD 기능을 별도의 서버 없이 사용할 수 있다.

Github Actions는 추가 비용이 들지 않고 초기 설정도 간단해서 더 유리했다.

환경 세팅

application.yml

spring:
  profiles:
    active: local
    include: secret
---
spring:
  config:
    activate:
      on-profile: dev
  transaction:
    default-timeout: '${custom.transaction.default-timeout}'
  datasource:
    hikari:
      maximum-pool-size: '${custom.hikari.maximum-pool-size}'
      idle-timeout: '${custom.hikari.idle-timeout}'
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: '${custom.db.url}'
    username: '${custom.db.username}'
    password: '${custom.db.password}'
  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        default_batch_fetch_size: 1000
...

워크플로우가 시작되면 가장 먼저 application-secret.yml 파일을 생성한다. 프로필 설정에 secret이 포함되어 있어, 워크플로우에서 생성된 application-secret.yml을 통해 데이터소스와 같은 중요 변수들을 관리할 수 있다.

application-secret.yml

custom:
  db:
    url: 데이터소스 url
    username: 유저 이름
    password: 비밀 번호

  transaction:
    default-timeout: 100
  hikari:
    maximum-pool-size: 10
    idle-timeout: 600000
    max-lifetime: 1800000
    minimum-idle: 20

application-secret.yml에서 db, 트랜잭션, 커넥션풀 관련 변수를 관리한다.
워크플로우에서 application-secret.yml을 만들기 위해 github에 secret을 등록 해야한다.

시크릿은 다음과 같이 등록 되어 있다. (base64로 인코딩 후 등록했다)

워크플로우 설정

CI/CD 파이프라인을 도식화 한 것이다. 워크플로우 절차는 다음과 같다.

1. 트리거 설정

name: Java CI with Gradle

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]
  • name : 워크플로우의 이름을 Java CI with Gradele로 명명한다.
  • on : main 브랜치로 코드가 푸시되거나 PR이 생성될 때 워크플로우가 트리거 된다.

2. 빌드 작업 정의

jobs:
  build:
    runs-on: ubuntu-latest
  • runs-on : 우분투 환경에서 실행된다. 깃허브에서 제공하는 최신 우분투 가상 머신을 사용한다.

3. 코드 체크아웃 및 빌드

steps:
  - uses: actions/checkout@v4
  • uses : 깃허브 레포지토리의 코드를 로컬로 클론한다.

4. JDK 환경 설정

- name: Set up JDK 17
  uses: actions/setup-java@v4
  with:
    java-version: '17'
    distribution: 'temurin'
  • Gradle 빌드에 필요한 JDK 17을 설치한다

5. application-secret.yml 생성

- name: create application-secret.yml
  run: |
    cd ./src/main/resources
    touch ./application-secret.yml
    echo "${{ secrets.APPLICATION_SECRET }}" | base64 --decode > ./application-secret.yml
  • application-secret.yml 파일을 생성한다.
  • 파일에 포함될 내용은 GitHub Secrets에서 관리되며, Base64로 인코딩된 데이터를 디코딩하여 저장한다.

6. Gradle 빌드

- name: Grant execute permission for gradlew
  run: chmod +x gradlew

- name: Build with Gradle
  run: ./gradlew build -x test
  • gradle에 실행 권한을 부여한다.
  • ./gradlew build -x test : 테스트 단계 생략하고 프로젝트를 빌드한다.

7. 도커 이미지 생성 및 도커 허브에 푸시

- name: Docker build
  run: |
    echo "${{ secrets.DOCKER_TOKEN }}" | base64 --decode | docker login -u $(echo "${{ secrets.DOCKER_USERNAME }}" | base64 --decode) --password-stdin

    docker build -t app .
    docker tag app $(echo "${{ secrets.DOCKER_USERNAME }}" | base64 --decode)/solta:latest
    docker push $(echo "${{ secrets.DOCKER_USERNAME }}" | base64 --decode)/solta:latest
  • 도커 로그인 : 깃허브 시크릿에 등록된 도커 허브 계정 정보를 가져와 로그인한다.
  • 도커 이미지 생성한다.
  • 도커 이미지 태그와 푸시한다.

8. 배포 서버에서 컨테이너 실행

- name: Deploy
  uses: appleboy/ssh-action@master
  with:
    host: ${{ secrets.HOST }}
    username: ubuntu
    key: ${{ secrets.PRIVATE_KEY }}
    script: |
      docker pull $(echo "${{ secrets.DOCKER_USERNAME }}" | base64 --decode)/solta:latest
      docker stop $(docker ps -a -q)
      docker run -d --log-driver=syslog -p 8080:8080 $(echo "${{ secrets.DOCKER_USERNAME }}" | base64 --decode)/solta:latest
      docker rm $(docker ps --filter 'status=exited' -a -q)
      docker image prune -a -f
  • SSH 연결 : GitHub Secrets에서 호스트와 SSH 키 정보를 가져와 서버에 연결한다
  • 도커 이미지 배포:
    • docker pull : 도커 허브에서 이미지를 가져온다.
    • 기존 컨테이너 정지:
      • docker stop 으로 모든 컨테이너를 정지한다.
    • 새로운 컨테이너 실행:
      • docker run 명령으로 새 컨테이너를 실행한다.
    • 정리 작업:
      • 종료된 컨테이너와 사용하지 않는 이미지를 제거한다.

gradle.yml 전체 코드

name: Java CI with Gradle

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    ## load application
    - uses: actions/checkout@v4

    ## jdk setting
    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: '17'
        distribution: 'temurin'

    ## create application-secret.yml
    - name: create application-secret.yml
      run: |
        cd ./src/main/resources
        touch ./application-secret.yml
        echo "${{ secrets.APPLICATION_SECRET }}" | base64 --decode > ./application-secret.yml
        
    - name: Grant execute permission for gradlew
      run: chmod +x gradlew

    - name: Build with Gradle
      run: ./gradlew build -x test

    ## docker build & push
    - name: Docker build
      run: |
       echo "${{ secrets.DOCKER_TOKEN }}" | base64 --decode | docker login -u $(echo "${{ secrets.DOCKER_USERNAME }}" | base64 --decode) --password-stdin

        docker build -t app .
        docker tag app $(echo "${{ secrets.DOCKER_USERNAME }}" | base64 --decode)/solta:latest
        docker push $(echo "${{ secrets.DOCKER_USERNAME }}" | base64 --decode)/solta:latest

    ## docker deploy to EC2
    - name: Deploy
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.HOST }}
        username: ubuntu
        key: ${{ secrets.PRIVATE_KEY }}
        script: |
          docker pull $(echo "${{ secrets.DOCKER_USERNAME }}" | base64 --decode)/solta:latest
          docker stop $(docker ps -a -q)
          docker run -d --log-driver=syslog -p 8080:8080 $(echo "${{ secrets.DOCKER_USERNAME }}" | base64 --decode)/solta:latest
          docker rm $(docker ps --filter 'status=exited' -a -q)
          docker image prune -a -f

마치며

CI/CD를 구축하며 AWS, 도커 등 익숙치 않은 기술에 대한 여러 어려움이 있었는데 한 포스트에 다 적기는 어려워서 나중에 따로 정리하기로 했다.

앞으로는 액추에이터를 활용하여 스프링 부트 어플리케이션 헬스 체크를 도입할 예정이다.

0개의 댓글