TroubleShooting & Study/Infra

[GitHub Actions] 스프링 서버 ci/cd 구축하기 (feat. Docker Hub)

DH_0518 2024. 1. 12. 01:45

이번 글은 다음 링크의 연장선이라 볼 수 있다

https://kdh0518.tistory.com/entry/cicd-develop%EC%84%9C%EB%B2%84%EC%99%80-release%EC%84%9C%EB%B2%84-%EB%B6%84%EB%A6%AC

 

[ci/cd] develop서버와 release서버 분리

현재 상황 ec2에 배포서버 1개, 배포db 1개, redis 1개, jenkins 1개 + etc = 총 6개 정도의 컨테이너가 동작중 ci/cd 과정 local에서 개발 진행 개발 사항을 PR, 이후 develop으로 merge develop이 업데이트되면 jenkin

kdh0518.tistory.com

 

 

 

 

 이전 글에서 내가 한 가지 크게 착각하고 있었던 부분이 있었다.
 분명 내 맥북은 메모리가 16GB이고, EC2 t3.medium은 4GB라서 로컬에서 Jenkins를 돌리면 넉넉하게 돌아갈 줄 알았으나...

로컬에서 Jenkins CPU 사용량 무려 500%!!!!

ㅋㅋ.. 이게 무슨;;

...

 크게 착각하고 있었던 게, 내 로컬에서는 EC2처럼 순수하게 서버를 운용하기 위한 프로세스들만 돌아가는 것이 아니라, 수많은 응용 프로그램들이 함께 돌아가고 있었던 것이다.

 

30개에 근접하는 크롬 창 + Intellij만 6개 + VScode 2개 + 알파 = 그냥 죽여줘..

 

 단순 무식하게 "EC2 메모리 x 4 = 내 맥북 ㅎㅎ 짱이당", 이러고 넘어갈 문제가 아니었는데.. 결국 젠킨스랑 이것저것 셋팅을 다 해보고 나서야 docker stats로 cpu 사용량을 확인하고 문제를 깨닫고 만 것이었다.

 

 

 

그래서 이제 어떻게 할건데!?

 

 

이제 나에게는 두 가지 방법이 남았다

  1. Jenkins만을 따로 돌리는 EC2를 운용한다
  2. Github Actions를 사용한다

 두 가지 방법을 고민하던 중, 내가 2주일 동안 Jenkins를 붙잡고 있었기에 당연히 Jenkins에 애정(증오)이 생길 수밖에 없었고, 나는 Jenkins를 돌릴 ec2를 만들기 시작했다.

 

 여기저기 사람들의 후기를 찾아보니까, 프리티어로 사용 가능한 t2.micro에 swap메모리를 사용해서 Jenkins를 돌렸다는 분들이 많아서 그대로 사용해 보기로 했다.

 

 만드는 과정에 Jenkins 설정을 그대로 옮기는 법도 공부했고

https://kdh0518.tistory.com/entry/Jenkins-Jenkins-jobs%EB%A5%BC-%EB%8B%A4%EB%A5%B8-Jenkins%EB%A1%9C-%EC%9D%B4%EB%8F%99%EC%8B%9C%ED%82%A4%EB%8A%94-%EB%B0%A9%EB%B2%95

 

[Jenkins] Jenkins jobs를 다른 Jenkins로 이동시키는 방법

최근 서버 비용을 줄이기 위해서 local과 ec2를 넘나들며 jenkins를 테스트 하다보니, 기존에 jenkins를 설정하고 다른곳에 넘어가서 새로 연결하는게 너무 귀찮았다. 그래서 어떻게 방법이 없을까 하

kdh0518.tistory.com

 

 swap 메모리 설정까지 완료한 후 Jenkins가 제대로 돌아가는 것을 확인할 때까지만 하더라도, "ec2 비용을 아꼈으니까 보스한테 칭찬받겠지 ㅎㅎ?" 라고 생각했지만..

 현실은 elastic ip비용 + ebs 비용을 합치면 결국 추가 금액이 발생하기에, 그냥 옆팀에서 사용하는 Github Actions를  배우라는 피드백을 받고 혼자 속으로 울었다..

 

 어쨌든, 그렇게 눈물을 머금고 PM님께 배운 Github Actions 사용법을 알아보도록 하자

 

 

GitHub Actions

(해당 방법은 Github Actions + DockerHub를 사용하는 방식이다.)

 

전체 흐름은 다음과 같다

Github -> Docker Image Build -> DockerHub Repository push -> SSH ec2 access -> ec2 pull DockerHub Image -> Docker Run Container

 

  1. workflows/yml 생성
    1. 변경이 일어날 시 ci/cd가 진행될 브랜치를 선택한다
    2. 해당 브랜치의 root directory에. github/workflows를 생성한다(둘 다 폴더)
    3. workflows 폴더 안에 gradle.yml 파일을 생성한다 (예시 코드는 아래에)
  2. github secrets 생성 (개발 브랜치에 적용되는 게 아니라, 리포지토리 하나에 통틀어서 사용된다)
    1. repository -> settings -> Secrets and variables -> actions에 접근
    2. new repository secret을 클릭하여, gradle.yml에서 환경변수로 다루고자 했던 값들을 생성한다
    3. Name이 Key값, Secret을 Value라고 생각하고 작성하면 된다
      • ex) ${{ APPLICATION_YML }}을 환경변수로 넣고 싶다면, Name에 APPLICATION_YML을, Secret에 작성한 application.yml을 통째로 넣어주면 된다
  3. Dockerfile 생성
    1. 배포할 스프링 서버 root directory에 Dockerfile을 생성한다
    2. 이후 변경 사항을 push 하거나 pull request 해서 자신이 설정한 행동을 진행하면 ci/cd가 진행될 것이다!
# server를 위한 dockerfile

FROM openjdk:17-alpine AS builder
COPY gradlew .
COPY gradle gradle
COPY build.gradle .
COPY settings.gradle .
COPY src src
RUN chmod +x ./gradlew
RUN ./gradlew bootJAR

FROM openjdk:17-alpine
COPY --from=builder build/libs/*.jar app.jar
ENTRYPOINT ["java","-jar", "-Duser.timezone=Asia/Seoul", "-Dspring.profiles.active=prod", "/app.jar"]

 

 

 

 

 

 이제 설정은 끝났다

 아래쪽의 workflows 설정 파일만 자기 상황에 맞춰서 잘 작성하면 된다

 젠킨스처럼 web hook을 걸어주고 pipe line을 만들어주고 할 것 없이, 이 정도만 설정하면 모든 게 끝이다

 너무 간단해서 진작 왜 안 썼을까.. 하는 허탈함이 조금 있긴 하지만, 지금이라도 배워서 다행이라 생각한다.

 

 그럼 모두 파이팅!

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle

# 워크플로우 이름
name: CI/CD

# 어떤 브랜치에서, 어떤 행동이 일어날 때 ci/cd가 일어날 것인지를 설정
on:
  pull_request: # push로 하고 싶으면 push로 설정하면 된다
    types: [closed] # pr이 닫혔을 경우
    branches: [ "develop" ]

# 워크플로우가 깃 레포에 대한 권한을 읽기 만 가능하게 설정
permissions:
  contents: read

# 워크플로우에서 할 작업 정의
jobs:
  CI-CD:
    # 작업 환경 = 우분투 최신 버전
    runs-on: ubuntu-latest

    # 깃허브에서 제공하는 checkout 액션 사용
    steps:
      - uses: actions/checkout@v3

      # jdk 17 설정
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          # temurin = Adoptium에서 제공하는 JDK
          distribution: 'temurin'

      # gradle caching - 캐싱을 통한 빌드 시간 향상 (젠킨스에서는 기본 동작하지만, 깃액션은 직접 설정해줘야함)
      - name: Gradle Caching
        uses: actions/cache@v3
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-  

      # application-prod.yml 생성
      # touch : 파일을 생성
      # echo + > : '>'는 기존에 동일한 이름의 파일이 없다면 생성하고, 동일한 파일이 있다면 새로운 파일로 덮어쓰기 한다
      # echo + >> : '>>'를 사용하면 기존에 파일이 없다면 생성하고, 동일한 파일이 있다면 파일 내부에 새로운 내용을 추가하는 방식으로 동작
      - name: make application-prod.yml
        run: |
          mkdir -p ./src/main/resources
          cd ./src/main/resources
          touch ./application.yml
          echo '${{ secrets.DEVELOP_YML }}' > ./application.yml
        shell: bash

        # gradle wrapper 파일에 실행 권한을 부여
        # gradle wrapper = 개발자가 특정 버전의 Gradle을 미리 설치하지 않고도 Gradle 빌드를 실행할 수 있게 해주는 편리한 도구
      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      # Gradle 빌드 액션을 이용해서 프로젝트 빌드
      - name: Build with Gradle
        # -x test 옵션을 사용하면, 테스트 없이 빌드
        run: ./gradlew build -x test

      # 도커 이미지 빌드 & 이미지를 도커허브로 push
      - name: Docker build & push to dockerhub
        # -f 뒤에는 도커파일 명을, /뒤에는 도커허브 저장소 명을 적는다
        # 이메일 회원가입을 진행했다면, DOCKER_PASSWORD에는 token값을 적으면 된다
        # 도커 유저네임에는 도커허브 이름을 적는다(이메일이 아님)
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker build -f Dockerfile -t ${{ secrets.DOCKER_REPO }}/[리포 이름] .
          docker push ${{ secrets.DOCKER_REPO }}/[리포 이름]

      # ec2에 ssh로 접속하여 도커 이미지 실행
      - name: Deploy to prod
        uses: appleboy/ssh-action@master
        id: deploy-prod
        with:
          host: ${{ secrets.HOST_EC2 }} # EC2 퍼블릭 IPv4 DNS
          username: ubuntu
          key: ${{ secrets.PRIVATE_KEY }}
          envs: GITHUB_SHA
          script: |
            sudo docker ps
            sudo docker stop [컨테이너 이름]
            sudo docker rm [컨테이너 이름]
            sudo docker pull "${{ secrets.DOCKER_USERNAME }}"/[리포 이름]
            sudo docker run -d --name [컨테이너 이름] -p "${{ secrets.해당 컨테이너 포트 }}":"${{ secrets.해당 서비스 포트 }}" --network [네트워크이름] "${{ secrets.DOCKER_USERNAME }}"/[리포 이름]
            sudo docker image prune -f