포스트

[Spring Boot] 프로젝트 배포 + CI/CD (github action, docker)

더 많은 기능을 구현하기 전에 배포를 하기로 결정하였다. 예전에 플젝을 진행하면서 배포를 시도해보았는데, 성공은 커녕 아직 역량이 부족하다고만 느낄 수 밖에 없었다. 해당 프로젝트는 나도 모르게 그동안 부족했던 부분을 중점적으로 진행하게 되는 것 같다. 역량이 부족하여 실패했던 경험을 토대로 다시 도전하여 차근차근 쌓아나가는 것을 보면 못할건 없다는 생각...

[Spring Boot] 프로젝트 배포 + CI/CD (github action, docker)

더 많은 기능을 구현하기 전에 배포를 하기로 결정하였다. 예전에 플젝을 진행하면서 배포를 시도해보았는데, 성공은 커녕 아직 역량이 부족하다고만 느낄 수 밖에 없었다.

해당 프로젝트는 나도 모르게 그동안 부족했던 부분을 중점적으로 진행하게 되는 것 같다. 역량이 부족하여 실패했던 경험을 토대로 다시 도전하여 차근차근 쌓아나가는 것을 보면 못할건 없다는 생각이 든다.

배포 (Deployment)

개발자가 작성한 프로젝트를 빌드하여 다른 사람들로 하여금 사용할 수 있도록 하는 작업이다. 즉, 본인의 컴퓨터인 localhost가 아니라 도메인 혹은 IP로 접속하여 사용한다는 것이다. 이는 개발자 본인 뿐만 아니라 다른 컴퓨터에서도 접근할 수 있게 한다. 이를 배포(Depolyment)라고 부른다.

Spring Boot를 사용한다고 했을 때, 이를 빌드하여 *.jar파일을 만들고, 이를 인터넷에 연결된 컴퓨터에서 실행한다. 이에 다른 컴퓨터는 빌드된 프로젝트를 실행한 컴퓨터에 해당하는 도메인을 입력하여 8080포트에서 실행된 Spring Boot 애플리케이션을 확인할 수 있다.

이 과정에서 8080포트를 http인 80포트나, https인 443포트로 Web Server를 사용해 포트포워딩을 할 수도 있지만 일단 넘어가도록 하겠다.

우리는 일반적으로 로컬 컴퓨터에 도메인을 연결하고 빌드된 파일을 실행하여 서버로 사용하지 않는다. 컴퓨터를 끄면 동시에 배포된 서비스도 종료되는 것이 가장 큰 이유다.

이에 우리는 EC2라는 AWS의 클라우드 컴퓨팅 서비스를 사용한다. 사용자의 필요에 따라 리소스를 쉽게 확장할 수 있는 것이 큰 특징이다. 터미널을 통해 SSH 22포트로 접속이 가능하다. 이때 EC2의 운영체제는 서버만을 목적으로 하는 리눅스가 적절하다. 리눅스 명령어를 자주 접할 수 있는 좋은 기회가 된다고 생각한다.

CI/CD

CI/CD는 지속적 통합 (Continuous Integration), 지속적 배포 (Continuous Deployment)의 약자이다. 배포 과정에 있어 복잡한 과정을 자동화하여 소프트웨어를 개발하는데 있어 효율성을 높이도록 도와준다.

예를 들면 프로젝트 빌드와 빌드된 파일을 가상 컴퓨팅 서버로 옮기며, 이를 적절한 환경변수와 함께 실행함을 자동으로 해주도록 설정한다는 것이다.

Spring Boot 프로젝트를 배포하는 과정을 CI/CD로 구축할 계획이다.

배포 과정

  1. deploy브랜치로 push한 commit을 인식하여 github actions을 실행한다.
    • github actions이란, 개발과정에서 workflow를 자동화하도록 도와주는 도구이다.
    • github actions는 Spring Boot 프로젝트의 .github/workflows/gradle.yml파일에서 설정한다.
  2. github actions에서 docker를 사용해 image를 push한다.
    • 단순히 ec2로 jar파일을 복사하는 방법도 있겠지만 도커를 실행했을 때 다양한 실행 옵션이 있기 때문에 도커를 사용할 예정이다.
    • Docker Hub를 이용하여 ec2에서 push한 image를 pull받을 수 있다.
  3. ec2에서 image를 pull받고 Dockerfile에 따라 Spring Boot 프로젝트 빌드 파일을 실행한다.
    • Dockerfile은 프로젝트의 루프 폴더에 생성한다.

gradle.yml

github actions를 사용하기 위해 작성하는 스크립트 파일이다. 어떤 조건에서 어떠한 작업을 자동으로 진행할 지 작성할 수 있다. 아래는 예시이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
name: Build and Deploy Spring Boot Application

on:
  push:
    branches:
      - deploy  # main 브랜치에 푸시될 때만 실행

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      # GitHub Actions 기본 제공 체크아웃 액션
      - name: Checkout code
        uses: actions/checkout@v2

      # JDK 17 설정
      - name: Set up JDK 17
        uses: actions/setup-java@v1
        with:
          java-version: '17'

      # Gradle 빌드
      - name: Build with Gradle
        run: |
          chmod +x ./gradlew
          ./gradlew build

      # Docker 로그인을 통해 DockerHub로 인증
      - name: Log in to Docker Hub
        run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin

      # Docker 이미지를 빌드하고 DockerHub로 푸시
      - name: Build and push Docker image
        run: |
          docker build -t ${{ secrets.DOCKER_USERNAME }}/practice:latest .
          docker push ${{ secrets.DOCKER_USERNAME }}/practice:latest

  deploy:
    runs-on: ubuntu-latest
    needs: build

    steps:
      # SSH를 통해 원격 서버에 접속하고 Docker 이미지를 pull한 후 컨테이너 실행
      - name: SSH into server and deploy
        uses: appleboy/ssh-action@v0.1.6
        with:
          host: ${{ secrets.REMOTE_HOST }}
          username: ${{ secrets.REMOTE_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: 22
          script: |
            # 기존 실행 중인 컨테이너가 있으면 중지 및 삭제
            if [ "$(docker ps -q -f name=practice)" ]; then
              docker stop practice
              docker rm practice
            fi
            
            # 동일한 이름의 중지된 컨테이너가 있으면 삭제
            if [ "$(docker ps -a -q -f status=exited -f name=practice)" ]; then
              docker rm practice
            fi
            
            # 최신 이미지로 업데이트
            docker pull ${{ secrets.DOCKER_USERNAME }}/practice:latest
            
            # 새로운 컨테이너 실행
            docker run -d \
              --name practice \
              -p 8080:8080 \
              ${{ secrets.DOCKER_USERNAME }}/practice:latest
  • name : 해당 workflow의 이름
  • on : gradle.yml이 실행될 조건 작성
    • push : 푸시할 때
    • pull_request : pr할 때
    • fork : fork될 때
      • branch : 어떤 branch에서 발생하는 이벤트인지

어떤 branch에서 어떤 이벤트(push, pull_request, fork)가 발생했을 때 실행하겠다

  • jobs : 실행할 작업 나열
    • “ tmp ” : 사용자 정의 작업 이름
      • runs-on : 작업을 실행할 환경 (ubuntu, windows 등)
      • steps : 해당 작업의 단계적으로 실행할 작업 단위
        • name : 작업 단위의 이름
        • uses : 실행할 라이브러리 이름
        • run : 사용자 정의 스크립트 작성

위의 작성 가이드를 확인했을 때, 주어진 gradle.ymlbuild, deploy 두 개의 단계로 진행됨을 알 수 있다.

  • build는,
    1. Checkout code : 소스코드 가져오기
    2. Set up JDK 17 : 자바 17 버전 환경으로 설정
    3. Build with Gradle : gradle을 이용하여 해당 프로젝트 빌드
    4. Log in to Docker Hub : DockerHub에 접속하기 위해 Docker 로그인
      • 이때 ${{ secrets.DOCKER_USERNAME }}과 같은 환경변수는 github의 secret action 변수를 설정하여야 한다.
    5. Build and push Docker image : docker image를 생성하고 DockerHub로 push
  • deploy는,
    1. SSH into server and deploy : ssh 프로토콜을 통해 ec2에 접속 후 image를 pull → 컨테이너 실행
      • appleboy/ssh-action@0.1.6라는 서드파티 액션 사용 (ssh 접속용)
      • 배포를 두 번 이상 진행할 시 이미 존재하는 이미지가 있게 되므로 이를 삭제하는 과정도 필요하다.

DockerHub에 접속하기 위해서는 Docker 아이디, 비밀번호

EC2에 접속하기 위해서는 remote_host, remote_user, private_key, port번호가 필요하다.

이들은 남들한테 알려지면 안되는 중요한 환경변수이므로 그 자체를 push하지 않도록 조심해야 한다.

github레벨에서 환경변수를 설정하는 방법은 github secret을 사용하는 방법이다.

github repository에서 Settings → Security - Secrets and Variables → Actions → New repository secret을 통해 환경 변수를 추가할 수 있다.

Dockerfile

Dockerfile은 이미지를 생성하기 위한 명령어의 모음이다. 애플리케이션이 실행되는 환경과 해당 애플리케이션을 실행하는 명령어를 넣게 된다.

이를 통해 서로 다른 환경에서도 일관성 있는 애플리케이션의 실행이 가능해진다.

1
2
3
4
5
6
7
8
9
10
11
# 이미지를 실행할 환경 설정
FROM openjdk:17-jdk

# 프로젝트의 빌드 파일의 경로를 JAR_FILE이라는 변수로 초기화
ARG JAR_FILE=build/libs/practice-0.0.1-SNAPSHOT.jar

# 해당 빌드 파일을 컨테이너 내부의 경로로 복사
COPY ${JAR_FILE} /practice.jar

# 복사한 빌드 파일을 실행하는 명령어
ENTRYPOINT ["java -jar -Dspring.profiles.active=prod /practice.jar"]
  • 해당 파일은 프로젝트 빌드 파일을 컨테이너 내부로 복사하는 과정을 거친다. 자바 17 버전을 사용해 실행됨을 확인할 수 있다.

이제 deploy브랜치로 push하였다.

다음과 같이 github의 actions 단락에 우리가 지정한 gradle.yml에 따라 발동 조건을 자동으로 인식하여 작업이 자동으로 실행된다.

빠진 것 없이 잘 완료했으면 해당 EC2의 도메인이나 IP주소의 8080포트로 프로젝트를 빌드한 애플리케이션이 실행됨을 확인할 수 있다.


주의

해당 과정은 환경 변수를 처리하는데 있어 주의를 기울여야 한다. 리포지토리가 public이고 application.properties를 push하게 되면 공개해선 안되는 키가 공개되어 AWS에서 연락이 오게 된다.. 그 순간부터 다양한 환경에서 사용하던 변수들이 꼬여 손도 못대게 될 수 있으니 처음부터 잘 정리를 해가며 환경변수를 다룰 수 있어야 한다고 생각한다.


앞으로

여기까지 많은 실패를 맛봤다. 가끔씩 포기하고싶었지만 지금 그래도 어찌 하는걸 보니 포기하기는 아직 멀었다고 생각한다. 하지만 이것으로는 부족하다. 웹 서버인 Nginx를 사용해 도메인을 http 프로토콜에서 https로 바꾸는 과정이 남아있다. 또한 애플리케이션을 실행할 때 도커 컨테이너 안에서 실행하기 때문에 로그를 볼 수 없어 오류가 생겨도 알 수가 없는 문제가 발생한다.

이를 해결하기 위해 따로 로그 파일을 나눠놓는 과정을 거칠 수 있다. 복잡해지지 않도록 천천히 나아갈 수 있도록 노력하고싶다.


원문: Velog

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.