[Docker] Docker와 Github Actions로 CI/CD 적용하기 (Springboot/EC2)

winluck·2024년 2월 17일
0

Docker

목록 보기
2/3


Docker를 이용한 EC2 프로젝트 배포에 이어 Github Actions를 기반으로 한 CI/CD를 적용해보았다.

뻘짓을 하도 많이 해서 반복하기 싫어 깔끔하게 정리해두려고 한다.

CI/CD란?

지속적 통합, 지속적 배포의 줄임말이며, 협업 프로젝트에서 점점 필수적인 요소로 떠오르고 있는 옵션이다.

지속적 통합이란, 구성원들이 구현한 여러 기능과 관련된 코드를 빌드 및 테스트 과정을 거친 후 문제가 없다면 자동으로 통합하는 과정이다.

지속적 배포란, 이렇게 통합되어 새로 반영된 프로젝트 내 변동사항을 적용한 버전의 프로젝트의 배포를 자동화하는 과정이다.

Github Actions, Jenkins 등의 관련 Tool이 존재하지만, 일단 Github Actions를 기반으로 하여 적용을 시작해보자.

환경

  • 프로젝트 개발 환경: Macbook M1 Pro
  • 서버 배포 환경: AWS EC2 Ubuntu Server 20.04 LTS (HVM) x86
  • Springboot 환경: Gradle + Java 17 + Springboot 3.2.2
  • 데이터베이스: postgreSQL

Dockerfile

# Docker file for building the image

# jdk 17
FROM openjdk:17

# Copy the jar file to the container
ARG JAR_FILE=build/libs/*.jar
# jar file Copy
COPY ${JAR_FILE} app.jar

ENTRYPOINT ["java","-Dspring.profiles.active=docker", "-jar","app.jar"]

이전 게시물과 동일하다.

Github Actions

Github Actions에 들어가 [New workflow]를 누른다.

내 프로젝트는 Java/Gradle이기에 오른쪽 위 Configure를 클릭한다.

gradle.yml이 작성되기 시작한다.
원하는 Action을 등록하면 특정 상황마다 우리가 원하는 동작을 자동화할 수 있다.

그러나 특정 동작 처리를 위해서는 환경변수나 계정 정보들을 알려주어야 하는데, 이를 yml 코드 상에서 직접적으로 노출시키는 것은 위험한 일이다.

Github에서 다행히 환경변수를 은닉하는 기능을 제공한다.

현재 Github Repository의 Setting에서 위 메뉴를 클릭한다.

[New repository secret] 버튼을 눌러 환경변수를 새롭게 추가할 수 있다.

내 프로젝트를 예시로 들면, 은닉한 변수는 다음과 같다.

  • DB 정보(url/username/password 등)
  • jwt token 관련 정보
  • dockerhub 계정 정보
  • firebaseAuth 기능 사용을 위한 serviceAccountKey.json 관련 정보

특정 파일이나 키 등을 Docker 컨테이너화 시 포함할 수 있는 볼륨/마운트 등의 기능이 존재하는 것으로 확인하였으나, 일단 기초적인 현재 방식으로 처리해보자.

참고로 secret 변수는 수정 및 삭제만 가능하고 조회는 불가능하다.

Github Actions workflow를 처리하는 yml 파일에서 이러한 secret 변수는
${{ secrets.변수명 }} 형식으로 접근할 수 있다.

참고로 환경변수 FIREBASE_JSON_KEY의 경우 firebase 사용 시 내려받는 serviceAccountKey.json 파일을 base64로 인코딩한 값을 Github Secret에 추가하였고, 실제 코드에선 아래와 같이 디코딩 과정을 거쳐 JSON으로 복원하여 사용하도록 했다.

FirebaseConfig

@Configuration
public class FirebaseConfig {

    @Bean
    public FirebaseAuth firebaseAuth() throws IOException {
        // Base64 인코딩된 JSON 키 파일 읽기
        String base64EncodedKey = System.getenv("FIREBASE_JSON_KEY");

        // Base64 디코딩
        byte[] decodedKey = Base64.getDecoder().decode(base64EncodedKey);

        // 바이트 배열에서 InputStream 생성
        ByteArrayInputStream serviceAccountStream = new ByteArrayInputStream(decodedKey);

        FirebaseOptions options = FirebaseOptions.builder()
                .setCredentials(GoogleCredentials.fromStream(serviceAccountStream))
                .build();
        FirebaseApp.initializeApp(options);
        return FirebaseAuth.getInstance();
    }
}

CI

name: CI/CD # yml 파일 이름

on: # develop 브랜치 push/pr 시 가동
  push:
    branches: [ "develop" ]
  pull_request:
    branches: [ "develop" ]

permissions: # 이 workflow에 Repository 읽기 권한 부여
  contents: read
  
jobs:
  CI: # CI 과정이므로 CI라고 명명하며, ubuntu 최신환경에서 실행
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3 # 체크아웃
    
    # jdk 17 설치
    - name: Setup JDK 17 
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'

	# Gradle 설치
    - name: Setup Gradle
      uses: gradle/actions/setup-gradle@ec92e829475ac0c2315ea8f9eced72db85bb337a # v3.0.0

	# Gradle로 프로젝트 빌드 (-x test 유닛테스트 배제)
    - name: Build with Gradle
      run: ./gradlew build -x test

	# 은닉한 환경변수로 Dockerhub 로그인
    - name: Docker Login
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_PASSWORD }}

    # Docker 이미지 빌드
    - name: Docker Image Build
      run: docker build --platform linux/amd64 -t ${{ secrets.DOCKERHUB_USERNAME }}/alert .

    # Dockerhub에 통합된 내용을 Push
    - name: DockerHub Push
      run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/alert

실제 develop 브랜치에 해당 내용을 푸시 및 PR 요청하면 workflow가 가동되어 빌드 및 테스트 및 자동화 과정을 진행하고,
문제 발생 시 fail 처리되며 해당 workflow는 롤백된다.

workflow yml 파일을 다루는데 익숙하지 않으면 위와 같은 온갖 뻘짓이 실시간으로 드러나니 꼭 관련 형식을 숙지하고 시작하도록 하자..

CD

CD:
    runs-on: self-hosted # self-hosted 방식
    needs: CI # CI가 성공한 후 진행할 수 있음
    steps:
    
    - name: Docker Container Remove # 현재 돌고 있는 도커 컨테이너 삭제
      run: sudo docker rm -f alert 2>/dev/null || true

    - name: Docker Old Image Remove # 기존 도커 이미지 삭제
      run: sudo docker rmi ${{ secrets.DOCKERHUB_USERNAME }}/alert

    - name: Docker Run New
      run: sudo docker run -d --name alert
        -e DB_URL=${{secrets.DB_URL}}
        -e DB_USERNAME=${{secrets.DB_USERNAME}}
        -e DB_PASSWORD=${{secrets.DB_PASSWORD}}
        -e JWT_SECRET=${{secrets.JWT_SECRET}}
        -e FIREBASE_JSON_KEY=${{secrets.FIREBASE_ACCOUNT_KEY}}
        -p 8080:8080 ${{secrets.DOCKERHUB_USERNAME}}/alert

명령어는 runs-on에 self-hosted를 제외하면 이전 게시물과 큰 차이는 없다.

secret에 등록해둔 주요 환경변수를 Docker run 명령어 시 포함하도록 하여 해당 환경변수에 프로젝트에 적절하게 삽입되도록 구성하였다.

self-hosted

self-hosted runner란, 유저가 직접 hosting한 서버(EC2)에서 Github Action Application을 띄우는 과정을 의미한다. 자세한 설명은 이 게시물을 참고하면 좋다.

Actions에 [Management] - [Runners] - [Selt-hosted runner] 에서 New runner 버튼을 누르자.

Download 아래에 있는 잡다한 명령어를 배포한 서버 상에서 실행하고, yml 파일에서 CD 부분의 runs-on을 self-hosted라고 설정하자. 내가 배포한 EC2 서버는 Linux x64이므로 해당 환경에 맞춰서 명령어를 실행해야 한다.

명령어를 모두 실행했다면 해당 배포 환경에서
ls
cd actions/runner
./run.sh

명령어를 실행하여 Github Action의 job 요청을 배포한 서버에서 처리할 수 있도록 준비하자.
배포한 서버는 yml 파일에서 알 수 있듯이 크게 3가지 작업을 처리해주어야 한다.

1. 현재 실행중인 Springboot Docker Container 삭제 (docker rm)
2. 기존 Docker 이미지 삭제 (docker rmi)
3. 최신화된 Docker 이미지 다운로드 및 실행 (docker run)

docker logs 명령어를 통해 해당 Container의 상황을 확인한 결과 새로운 내용이 잘 반영되어 배포까지 모두 완료된 것을 확인할 수 있다.

이렇게 간단한 Docker 기반 CI/CD를 알아보았다.

CI와 CD 분리하기

때때로 Pull request가 Merge되었을 때만 배포되는 등 제약을 걸어야 하는 순간이 있다.

그런 경우엔 CI와 CD 파일을 별도로 분리하는 것이 유용할 수 있다.

name: CI

on:
  push:
    branches: [ "dev" ]
  pull_request:
    branches: [ "dev" ]
  workflow_dispatch:

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@ec92e829475ac0c2315ea8f9eced72db85bb337a # v3.0.0

      - name: Build with Gradle
        run: ./gradlew build -x test # 테스트코드 검사 없이 빌드

      - name: Docker Login
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_PASSWORD }}

      - name: Build Docker Image
        run: docker build --platform linux/amd64 -t ${{ secrets.DOCKERHUB_USERNAME }}/프로젝트명 .

      - name: Push Docker Image
        run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/프로젝트명
name: CD

on:
  push:
    branches: [ "dev" ]
  pull_request:
    branches: [ "dev" ]
    types: [closed]
  workflow_dispatch:

permissions:
  contents: read

jobs:
  deploy:
    if: github.event.pull_request.merged == true # dev 브랜치로 향하는 PR이 Merge되었을 때만 실행
    runs-on: self-hosted

    steps:
      - uses: actions/checkout@v4

      - name: Docker Login
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_PASSWORD }}

      - name: Pull Docker Image
        run: sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/프로젝트명

      - name: Remove Old Docker Container
        run: sudo docker rm -f 프로젝트명 || true

      - name: Run Updated Docker Container
        run: sudo docker run -t --env-file ~/.env -d --name 프로젝트명 -p 8080:8080 ${{ secrets.DOCKERHUB_USERNAME }}/프로젝트명
profile
Discover Tomorrow

0개의 댓글