본문 바로가기

Server/Infra

[Docker] 도커 활용해서 Spring Boot 환경에서 CI/CD 파이프라인 구축하기

반응형
SMALL
 

[Docker] jar 파일 도커(docker) 이미지로 배포하기 (with. Java, SpringBoot)

Docker로 서버 배포하기진행하던 사이드 프로젝트에서 헥사고날 아키텍처를 도입하게 되었다. 그에 따라 멀티모듈로 싹 바꾸고 리팩토링 작업을 진행했다. 약 일주일 간의 작업 후 수정된 서버를

soso-hyeon.tistory.com

 

안녕하세요!

약 2달 전에 도커 이미지를 서버에 수동 배포하는 글을 작성했었는데요, 이번엔 배포를 자동화하는 CI/CD 파이프라인을 구축해보아서 그 과정을 기록하려고 해요.

 

해당 작업을 진행한 환경은 다음과 같아요.

  • Java, Spring Boot (jar 파일 활용)
  • Docker Hub
  • Github Action
  • AWS EC2 Ubuntu

 

기존에 진행하던 프로젝트에 CI/CD 파이프라인을 추가했는데, 해당 프로젝트가 Java, Spring Boot 기반에 AWS의 Ubuntu 환경의 EC2를 사용하고 있었어요. Docker Hub는 오픈소스 기반의 registry이기도 하고, 하나의 계정으로 하나의 private한 registry를 무료로 사용할 수 있어 선택했어요.

 

본격적으로 파이프라인을 구축한 진행 과정을 시작해볼게요.

 

지난 글에서 진행한 과정과 동일해요. 다만 지난 번에는 위 과정을 수동으로 진행했다면, 이번에는 이 과정을 자동화할 거에요.

 


Github Action

name: deploy

on:
  push:
    branches: [ develop ]

jobs:
  ci:
    runs-on: ubuntu-20.04

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: 17
          distribution: 'temurin'
          cache: gradle

      - name: Build with Gradle
        run: |
          chmod +x ./gradlew
          ./gradlew build -x test
        shell: bash

      - name: Set docker
        uses: docker/setup-buildx-action@v2.9.1

      - name: Login docker
        uses: docker/login-action@v2.2.0
        with:
          username: ${{ secrets.DOCKERHUB_LOGIN_USERNAME }}
          password: ${{ secrets.DOCKERHUB_LOGIN_ACCESSTOKEN }}

      - name: Build docker image
        run: |
          docker build --platform linux/amd64 -t smeemdev/smeem-dev:latest -f Dockerfile-dev .
          docker push smeemdev/smeem-dev:latest

  cd:
    needs: ci
    runs-on: ubuntu-20.04

    steps:
      - name: SSH로 서버 접속
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.RELEASE_SERVER_IP }}
          username: ${{ secrets.RELEASE_SERVER_USER }}
          key: ${{ secrets.RELEASE_SERVER_KEY }}
          script: |
            cd ~

            # deploy.sh 파일 다운로드
            wget https://raw.githubusercontent.com/Team-Smeme/Smeme-server-renewal/develop/script/deploy.sh -O deploy.sh
            chmod +x deploy.sh

            # .env 파일 추가
            if ! grep -q "REGISTRY_URL=" .env; then
              echo "REGISTRY_URL=${{ secrets.REGISTRY_URL }}" >> .env
            fi
            if ! grep -q "IMAGE_NAME=" .env; then
              echo "IMAGE_NAME=${{ secrets.IMAGE_NAME }}" >> .env
            fi

            # 배포 스크립트 실행
            sudo ./deploy.sh

Github Action에 대한 workflows 파일로, .github/workflows/deploy.yml 파일 위치에 저장되어 있어요.

하나씩 알아봅쉬다 ~.~

 

CI

프로젝트에 대한 도커 이미지를 빌드하고, registry로 푸시해요.

 

      - name: Set docker
        uses: docker/setup-buildx-action@v2.9.1

      - name: Login docker
        uses: docker/login-action@v2.2.0
        with:
          username: ${{ secrets.DOCKERHUB_LOGIN_USERNAME }}
          password: ${{ secrets.DOCKERHUB_LOGIN_ACCESSTOKEN }}

      - name: Build docker image
        run: |
          docker build --platform linux/amd64 -t smeemdev/smeem-dev:latest -f Dockerfile .
          docker push smeemdev/smeem-dev:latest

 

  • Set docker
    • 도커 환경을 세팅해요.
  • Login docker
    • 도커 허브에 로그인해요.
    • DOCKERHUB_LOGIN_USERNAME: Docker Hub 계정 이름
    • DOCKERHUB_LOGIN_ACCESSTOKEN: Docker Hub 계정에 접근할 수 있는 액세스 토큰
  • Build docker image
    • 도커 이미지를 빌드하고, 도커 허브에 push해요.
    • --platform linux/amd64: 활용한 우분투 환경의 서버에 맞춰주기 위해 추가가 필요한 옵션
    • -t smeemdev/smeem-dev:latest: ${도커 허브 username}/${도커 이미지 이름}:${태그}
    • -f Dockerfile: Dockerfile 명이면 옵션 없이도 default로 실행 (다른 이름일 경우 파일 파일 이름 명시 필요)

 

**도커 허브 Access Token 발급 받기

로그인할 때, 토큰 대신 비밀번호를 입력해도 가능은 해요. 하지만 보안을 높이기 위해 보통 액세스 토큰을 많이 사용하고는 하죠.

그럼 어떻게 발급 받을 수 있을까요?

 

1. 도커 허브에서 오른쪽 상단 프로필을 눌러 Account Settings 창으로 진입해주세요.

2. 그럼 아래 이미지처럼 뜰텐데, Security 항목의 Personal access tokens 창으로 진입해주세요.

 

3. 이제 아래 이미지처럼 뜰텐데, Generate new token을 클릭하고, 필요한 권한 추가해서 토큰을 생성해주세요. 생성 후 토큰 정보를 한번만 볼 수 있으니 개인 저장소에 잘 적어놔주세요.

 

 

**Dockerfile 생성하기

FROM bellsoft/liberica-openjdk-alpine:17
ARG JAR_FILE=/smeem-bootstrap/build/libs/smeem-bootstrap-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} smeem.jar
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=dev", "/smeem.jar"]

별 거 없죠? jar 파일 기반으로 도커 이미지를 생성하는 파일이에요.

Java와 같이 필요한 환경 세팅해두고, (Spring Boot 환경이니까) jar 파일 빌드해서 옵션 주고 도커 이미지로 빌드될 수 있도록 해요. 위 workflows에서 도커 이미지가 빌드될 때, 해당 파일이 사용되어요.

 

 

CD

이제 EC2와 같은 서버 내부에서 registry에 올라온 latest 도커 이미지를 pull 받고, 실행함으로써 배포하는 단계에요. SSH로 접속할 때 appleboy/ssh-action@master가 많이 사용되더라구요. 대중적인 픽으로 사용했어요.

해당 작업은 배포하려는 서버에 도커가 설치되어있다고 가정해요.

 

    steps:
      - name: SSH로 서버 접속
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.RELEASE_SERVER_IP }}
          username: ${{ secrets.RELEASE_SERVER_USER }}
          key: ${{ secrets.RELEASE_SERVER_KEY }}
          script: |
            cd ~

            # deploy.sh 파일 다운로드
            wget https://raw.githubusercontent.com/Team-Smeme/Smeme-server-renewal/develop/script/deploy.sh -O deploy.sh
            chmod +x deploy.sh

            # .env 파일 추가
            if ! grep -q "REGISTRY_URL=" .env; then
              echo "REGISTRY_URL=${{ secrets.REGISTRY_URL }}" >> .env
            fi
            if ! grep -q "IMAGE_NAME=" .env; then
              echo "IMAGE_NAME=${{ secrets.IMAGE_NAME }}" >> .env
            fi

            # 배포 스크립트 실행
            sudo ./deploy.sh
  • RELEASE_SERVER_IP: 릴리즈 할 서버 IP 주소 (탄력적 IP를 사용한다면 탄력적 IP 주소)
  • RELEASE_SERVER_USER: 환경 이름 (해당 글에서는 우분투 환경의 서버를 사용했으므로 ubuntu)
  • RELEASE_SERVER_KEY: 서버 접속에 필요한 ppk/pem 키 값 (텍스트 형태)
  • script
    • SSH로 접속했으면 스크립트를 통해 도커 이미지를 실행하여 서비스를 배포해요.
    • 배포하기 전 wget을 활용해서 deploy.sh 파일을 다운로드 받아줘요. 배포하려는 서버 내에 파일을 두어도 되지만, 초기 배포 과정에서 해당 파일을 수정할 경우가 많아 외부로 빼고 다운로드하도록 변경했어요.
    • .env 파일을 추가해줘요. deploy.sh 파일에서 시크릿용 값(변수용)이 있어서, .env 파일을 통해 값이 대입되도록 했어요.
    • 마지막으로 배포 스크립트를 실행해주면 끝!

 


Deploy.sh

배포 과정에 필요한 스크립트 파일이에요.

 

#!/bin/bash
source .env

REGISTRY_URL=${REGISTRY_URL}
IMAGE_NAME=${IMAGE_NAME}
TAG="latest"
CONTAINER_NAME="smeem"
HEALTH_CHECK_URI="/actuator/health"

echo "> Pull docker image"
sudo docker pull "${REGISTRY_URL}"/"${IMAGE_NAME}":"${TAG}"

echo "> Stop running docker container"
if [ "$(sudo docker ps -a -q -f name=${CONTAINER_NAME})" ]; then
  sudo docker stop ${CONTAINER_NAME}
  sudo docker rm ${CONTAINER_NAME}
fi

echo "> Run docker"
sudo docker run -d --name ${CONTAINER_NAME} -p 80:8080 "${REGISTRY_URL}"/"${IMAGE_NAME}":${TAG}

echo "----------------------------------------------------------------------"

sleep 15
for RETRY_COUNT in {1..15}
do
    echo "> Health check"

    RESPONSE=$(curl -s http://localhost${HEALTH_CHECK_URI})
    # shellcheck disable=SC2126
    UP_COUNT=$(echo "${RESPONSE}" | grep 'UP' | wc -l)

    if [ "${UP_COUNT}" -ge 1 ]
    then
        echo "> Success"
        break
    else
        echo "> Not run yet"
        echo "> 응답 결과: ${RESPONSE}"
    fi
    if [ "${RETRY_COUNT}" -eq 15 ]
		then
        echo "> Failed to running server"
        sudo docker rm -f ${CONTAINER_NAME}
        exit 1
    fi
    sleep 2
done
echo "----------------------------------------------------------------------"

 

Pull docker image

echo "> Pull docker image"
sudo docker pull "${REGISTRY_URL}"/"${IMAGE_NAME}":"${TAG}"

먼저 도커 이미지를 다운 받아야겠죠?

새로 배포한다는 것은 registry에 새로운 도커 이미지가 올라왔다는 것이니, latest로 pull 받아줘요. (여기서 TAG는 latest)

 

Stop running docker container

echo "> Stop running docker container"
if [ "$(sudo docker ps -a -q -f name=${CONTAINER_NAME})" ]; then
  sudo docker stop ${CONTAINER_NAME}
  sudo docker rm ${CONTAINER_NAME}
fi

이미 실행 중인 컨테이너가 있다면 중지하고 삭제해줘야 해요.

배포하려는 컨테이너 이름으로 업로드된 이미지가 있다면, 중지(stop)하고 제거(rm)해줘요.

 

Run docker

echo "> Run docker"
sudo docker run -d --name ${CONTAINER_NAME} -p 80:8080 "${REGISTRY_URL}"/"${IMAGE_NAME}":${TAG}

앞에서 pull 받았던 도커 이미지를 실행해요.

--name 옵션으로 컨테이너 이름을, -p 옵션으로 포트를 설정할 수 있어요. 

 

Health check

sleep 15
for RETRY_COUNT in {1..15}
do
    echo "> Health check"

    RESPONSE=$(curl -s http://localhost${HEALTH_CHECK_URI})
    # shellcheck disable=SC2126
    UP_COUNT=$(echo "${RESPONSE}" | grep 'UP' | wc -l)

    if [ "${UP_COUNT}" -ge 1 ]
    then
        echo "> Success"
        break
    else
        echo "> Not run yet"
        echo "> 응답 결과: ${RESPONSE}"
    fi
    if [ "${RETRY_COUNT}" -eq 15 ]
		then
        echo "> Failed to running server"
        sudo docker rm -f ${CONTAINER_NAME}
        exit 1
    fi
    sleep 2
done

마지막으로 배포되었는지 확인해요. 개인적으로 직접 AWS나 서버에 접속하지 않아도, Github Action 로그를 통해 서버가 정상적으로 실행되었는지 확인할 수 있어서 너무 편하고 좋았어요.

배포에는 시간이 좀 걸리기 때문에, 도커 이미지 실행 후 15초 뒤에, 15번동안 상태 체크 API를 호출해보면서 서버의 배포 여부를 확인해요.

만약 15번의 재시도 후에도 서버 API가 응답하지 않으면 배포는 실패한 것으로 간주하고 실행한 이미지를 제거(rm)하면서 스크립트를 실패로 마쳐요.

 

직접 해보면? 자동으로 배포 성공 ㅎㅎㅎㅎㅎ

 

 

**API 헬스 체크

이전에는 직접 TestApi를 만들어서 서버 상태를 체크했는데, 이번 과정에서 actuator를 활용해보았는데 깔끔해지고 편했어요. (Spring Boot 환경 위주로 설명)

 

implementation 'org.springframework.boot:spring-boot-starter-actuator'

(Gradle) dependencies에 actuator 의존성을 추가해요.

 

Gradle Build 후, 어플리케이션을 실행시켜 /actuator/health 경로로 API를 호출해보면 위 이미지처럼 서버 상태를 확인할 수 있어요. status가 UP으로 뜨면 서버가 정상적으로 실행했다는 의미에요. 굉장히 쉽고 간편해서 좋았어요-!

 

 

CI/CD 파이프라인을 직접 구축한 프로젝트 레포를 남기며 이만 글을 마무리하려고 해요 🙇‍♀️

 

GitHub - Team-Smeme/Smeme-server-renewal: 스밈 서버에도 봄은 온다 🍃

스밈 서버에도 봄은 온다 🍃. Contribute to Team-Smeme/Smeme-server-renewal development by creating an account on GitHub.

github.com

 


 

마무리

이렇게 도커를 활용해서 CI/CD 파이프라인을 구축해보았어요. Jar 파일을 생으로 올려 배포하는 것보다 EC2 용량도 덜 잡아먹고 너무 좋았어요. 코드 작성하는대로 원하는 시점에 자동으로 배포되어 너무너무 편했습니다,, 👍 이후로는 여유될 때 무중단 배포 CI/CD 파이프라인으로 업그레이드 해보려고 해요 :)

 


References

https://velog.io/@sysy123/Github-Actions-CICD

GPT에게 자문 구하기 :D

반응형
LIST