[AWS] EC2 서버에 Spring boot 파일 배포하기 + Github Actions와 Docker로 자동화하기

peace w·2024년 12월 18일

[AWS] 프리티어로 EC2 인스턴스 생성 및 SSH로 접속하기 / EC2와 RDS 연결하기

지난 글에서 AWS EC2 서버를 만들고, RDS 와 연결하는 과정까지 진행했다.
개발 중인 Spring boot 프로젝트를 EC2 서버에서 실행시켜보자.

FileZila로 .jar 파일 직접 전달하기

서버에서 프로젝트를 직접 배포하는 것은 크게 두 가지가 있다.

  1. 서버에서 git 레포지토리를 clone 하여 build 하는 방법
  2. 서버에 jar파일을 직접 전달하는 방법

나는 이미 FileZila를 쓰고 있었기 때문에, jar 파일을 직접 전달하는 방법을 선택했다.

FileZila
사용자의 PC와 호스팅 서버 간 파일 송수신을 위한 위한 FTP(File Transfer Protocol) 소프트웨어

FileZila Client 는 여기서 다운받을 수 있다.
https://filezilla-project.org/download.php?type=client

다운받은 FileZila를 실행하고 버튼을 클릭해서 EC2 서버와 연결한다.

  1. 프로토콜 : SFTP - SSH File Transfer Protocol 선택
  2. 호스트 : EC2의 퍼블릭 IPv4 주소 입력
  3. 포트 : 비워도 된다
  4. 로그온 유형 : 키 파일 선택
  5. 사용자 : 사용자 이름 입력 (linux 서버의 경우 : ec2-user, ubuntu 서버의 경우 : ubuntu)
  6. 키파일 : 앞서 저장한 .pem 키 파일이 저장된 경로를 선택
  7. 연결 선택

정상적으로 연결이 되었다면 왼쪽엔 로컬PC 화면이, 오른쪽에는 FTP 서버가 보일 것이다.

  1. Spring boot에서 파일을 빌드 한 후, libs 폴더의 빌드된 jar 파일을 더블클릭한다. (서버의 저장경로로 전송)
  2. 전송이 완료되면 오른쪽 FTP 서버 화면에서도 jar 파일이 보인다.

서버에 jar 파일을 제대로 전송해주었다면, 아래처럼 입력해서 jar 파일을 실행한다.

nohup java -jar <프로젝트 이름>-0.0.1-SNAPSHOT.jar &

nohup 은 백그라운드에서 무중단으로 실행하기 위해 사용한다. 마지막에 & 도 붙여주어야 한다.
nohup을 사용하지 않으면 bash 창을 닫고 종료할때마다 어플리케이션도 같이 종료된다.

이미 실행중인 어플리케이션을 중지시켜야한다면 아래처럼 입력한다.

ps -ef | grep .jar # 실행중인 .jar 파일을 조회
kill <pid> # 조회한 pid를 입력

이 방법으로 계속 사용했는데, 변경 사항이 생길때마다 종료하고 새로 실행시키는 과정이 매우 번거로웠다. 그리고 github push는 별개로 해주어야하기에 불편했다.

그래서 Docker와 Github Actions를 활용해서 CI/CD 를 구축하기로 했다.

Docker와 Github Actions로 배포하기

도커를 활용한 배포 흐름은 아래와 같다.

  1. Dockerfile을 작성하여 build 시 docker image가 생성된다.
  2. 생성된 docker image를 docker hub에 push 한다.
  3. 서버(ec2)에 docker hub의 docker image를 pull 한다.
  4. docker image를 실행시켜 docker container를 생성한다.(어플리케이션은 container 상에서 실행된다.)

이 과정을 자동화 할 것이고, CI/CD 파이프라인 구축에는 Github Actions를 사용할 것이다.

Github Actions
Github 저장소를 기반으로 소프트웨어 개발 Workflow를 자동화할 수 있는 도구. 특정한 브랜치에 대한 이벤트(트리거)를 통해 설정된 Workflow를 따라, 빌드, 테스트, 배포 등의 다양한 동작을 자동으로 실행되게 할 수 있다.

Github Actions의 Secrets 관리

프로젝트 레포의 Settings-Security-Secrets and Variables-Actions 경로에서 보안상의 이유로 Github에 올릴 수 없는 환경 변수들을 작성해준다.

등록한 환경변수들은 값을 볼 수 없고, 수정 또는 삭제만 가능하다.

  1. APPLICATION : github에 올라가지 않은 application.properties 의 내용
  2. AWS_EC2_HOST : EC2 인스턴스의 PUBLIC IP
  3. AWS_EC2_KEY : 발급받은 .pem 키 파일
  4. DOCKER_USERNAME : Docker Hub 로그인 시 사용하는 username
  5. DOCKER_PASSWORD : Docker Hub 로그인 시 사용하는 password

Docker Hub 로그인 시 구글 로그인으로 가입하면 username과 password 만으로는 로그인이 불가능하다. 토큰 방식을 사용해야만 로그인이 가능한 듯 하다.

Workflow 파일 생성

프로젝트 레포의 Actions-New Workflow를 통해 workflow 파일 작성을 위한 양식을 선택할 수 있다.

Java with Gradle을 선택하면 Java-Gradle 환경에서 사용할 수 있는 Workflow 파일의 기본 양식을 보여준다. 이걸로 생성해도 되고, 직접 .github > workflows > 폴더 안에 workflow를 작성해도 된다.

Workflow 파일 작성

아래처럼 작성했다.

# Github Actions 에 표시되는 Workflow 이름
name: NNZZ CI/CD

# master 브랜치로 push, pull request 가 발생하면 workflow 를 실행
on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

permissions:
  contents: read

# workflow 에서 실행할 동작
jobs:
  CI:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

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

      # gradle caching - 빌드 시간 향상
      - name: Gradle Caching
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      # application.properties 파일 생성
      - name: Make application.properties
        run: mkdir ./src/main/resources |
          touch ./src/main/resources/application.properties

      - name: Deliver application.properties
        run: echo "${{ secrets.APPLICATION }}" > ./src/main/resources/application.properties

      # Gradle 설정 및 Build
      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v3

      - name: Change gradlew permissions
        run: chmod +x ./gradlew

      - name: Build with Gradle Wrapper
        run: ./gradlew clean build

      - name: List build/libs contents
        run: ls build/libs


      # Docker 로그인
      - name: Docker Login
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      # Docker 이미지 생성 및 Push
      - name: Docker build & Push
        run: |
          docker build -t ${{ secrets.DOCKER_USERNAME }}/nnzz .
          docker push ${{ secrets.DOCKER_USERNAME }}/nnzz


  CD:
    needs: CI
    runs-on: ubuntu-latest

    steps:
      # EC2 접근 후 docker 이미지 pull & run
      - name: Deploy to EC2 Server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.AWS_EC2_HOST }}
          username: ec2-user
          key: ${{ secrets.AWS_EC2_KEY }}
          port: 22
          script: |
            sudo docker pull ${{ secrets.DOCKER_USERNAME }}/nnzz
            sudo docker stop $(sudo docker ps -qa)
            sudo docker rm $(sudo docker ps -qa)
            sudo docker run -d -p 8080:8080 \
            -v /etc/localtime:/etc/localtime:ro \
            -e TZ=Asia/Seoul \
            ${{ secrets.DOCKER_USERNAME }}/nnzz
            sudo docker system prune -f

하나씩 살펴보자.

# Github Actions 에 표시되는 Workflow 이름
name: NNZZ CI/CD

# master 브랜치로 push, pull request 가 발생하면 workflow 를 실행
on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

permissions:
  contents: read
  • name : Github Actions에 표시되는 Workflow 이름이다. 자유롭게 설정 가능하다.
  • on : Workflow 의 트리거가 될 이벤트를 설정한다. master 브랜치로 push, pull request가 발생하면 workflow를 실행하도록 작성했다.
  • permissions: Workflow 작업에 적용되는 권한을 설정한다. 권한에 대한 자세한 사항은 공식문서 참조
jobs:
  CI:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

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

      # gradle caching - 빌드 시간 향상
      - name: Gradle Caching
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      # application.properties 파일 생성
      - name: Make application.properties
        run: mkdir ./src/main/resources |
          touch ./src/main/resources/application.properties

      - name: Deliver application.properties
        run: echo "${{ secrets.APPLICATION }}" > ./src/main/resources/application.properties
  • jobs : job은 Workflow의 실행 단위이다. 각 job은 다시 step으로 순차적인 실행 단계가 구분된다. CI와 CD로 job을 나누어 작성했다. (job 과 step의 이름은 자유롭게 설정 가능)

  • runs-on : Workflow를 실제로 실행하는 서버를 설정한다. GitHub-hosted runner(Github에서 자체적으로 제공)/ Self-hosted runner(사용자가 직접 설정하여 사용) 으로 나뉜다. 여기서는 GitHub-hosted runner를 사용하였다.

  • Set up JDK 17 : 프로젝트에서 사용하는 것과 동일한 JDK 17을 사용하도록 설정하였다.

  • Gradle Caching : 빌드 시간 향상을 위해 Gradle을 캐싱하도록 했다. 작성하지 않아도 Workflow를 실행하는데 문제는 없다.

  • Make application.properties : 프로젝트 레포에서 제외된 application.properties 을 생성한다.

  • Deliver application.properties : 생성된 application.properties에 Secrets에 등록한 APPLICATION의 값을 덮어씌운다.

      # Gradle 설정 및 Build
      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v3

      - name: Change gradlew permissions
        run: chmod +x ./gradlew

      - name: Build with Gradle Wrapper
        run: ./gradlew clean build

      - name: List build/libs contents
        run: ls build/libs


      # Docker 로그인
      - name: Docker Login
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      # Docker 이미지 생성 및 Push
      - name: Docker build & Push
        run: |
          docker build -t ${{ secrets.DOCKER_USERNAME }}/nnzz .
          docker push ${{ secrets.DOCKER_USERNAME }}/nnzz
  • Setup Gradle ~ : Gradle을 설정하고 권한 변경 후 프로젝트를 clean build 한다.
  • Docker Login : Secrets에 등록된 DOCKER_USERNAME과 DOCKER_PASSWORD로 Docker에 로그인한다.
  • Docker build & Push : Docker Image를 build 하여 생성 후 Docker Hub에 프로젝트를 push한다.
  CD:
    needs: CI
    runs-on: ubuntu-latest

    steps:
      # EC2 접근 후 docker 이미지 pull & run
      - name: Deploy to EC2 Server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.AWS_EC2_HOST }}
          username: ec2-user
          key: ${{ secrets.AWS_EC2_KEY }}
          port: 22
          script: |
            sudo docker pull ${{ secrets.DOCKER_USERNAME }}/nnzz
            sudo docker stop $(sudo docker ps -qa)
            sudo docker rm $(sudo docker ps -qa)
            sudo docker run -d -p 8080:8080 \
            -v /etc/localtime:/etc/localtime:ro \
            -e TZ=Asia/Seoul \
            ${{ secrets.DOCKER_USERNAME }}/nnzz
            sudo docker system prune -f
  • needs : 적어둔 작업(CI)이 완료된 후 실행된다.
  • Deploy to EC2 Server : EC2 서버에서 Docker Image를 pull하여 실행하는 작업을 수행한다.
  • uses : 원격 접속을 위해 appleboy 를 사용한다.
  • script : Docker Hub에서 최신 Image를 pull 한 후 실행 중인 컨테이너를 중지, 삭제한다.
    타임존을 한국시간대로 설정하여 image를 run하여 컨테이너를 실행시킨다.
    마지막으로 불필요한 오래된 image는 삭제한다.

Docker 설치

윈도우 10 홈 에디션 기준

  1. 제어판 > 프로그램 > 프로그램 및 기능 > Windows 기능 켜기/끄기 버튼을 클릭한다.

  2. Linux 용 Windows 하위 시스템, 가상 머신 플랫폼에 체크하고 확인 버튼을 클릭한다. -> 체크한 기능을 활성화하려면 컴퓨터를 다시 시작해야한다.

  3. https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi
    에 접속하여 리눅스 커널을 다운받는다. 다운로드 받은 파일을 실행하여 리눅스 커널을 업데이트 할 수 있다.

  4. https://docs.docker.com/desktop/install/windows-install/
    윈도우용 도커 데스크톱을 다운받는다.

  5. 설치 중, Configuration 화면에서 모든 항목에 체크한 다음 OK 버튼을 진행한다.

  6. 설치가 완료되면 Close and log out 버튼을 클릭해 윈도우에 다시 로그인한다. (컴퓨터가 재부팅된다.)

  7. 바탕화면에 Docker Desktop이 추가되어있다.

EC2에서 Docker 설치

# 도커 설치
sudo yum install docker -y

# 도커 서비스 실행
sudo service docker start

# /var/run/docker.sock 파일 권한 변경
sudo chmod 666 /var/run/docker.sock

# ec2-user를 docker 그룹에 추가, sudo 명령어 없이 docker 사용가능
sudo usermod -a -G docker ec2-user

Docker Hub 레포지토리 생성

Docker Hub에 docker image를 push할 레포지토리를 생성한다. public으로 생성하자. (private의 경우 계정 당 1개까지만 무료로 생성이 가능하다.)

Dockerfile 생성

프로젝트 최상위 경로 아래에 Dockerfile을 생성한다. (※ src 아래에 생성하면 안 된다!)

지시어설명
FROM베이스 이미지 지정
RUN이미지를 지정하면서 실행할 명령 지정
ENTRYPOINT컨테이너의 어플 지정 (컨테이너 시작할 때 실행할 명령어)
EXPOSE컨테이너의 포트 지정
ADD이미지 생성 시 파일 추가
COPY이미지 생성시 파일 복사
WORKDIR컨테이너 작업 디렉토리 지정
MAINTAINER이미지 작성자 명시
CMD컨테이너의 어플 지정 (컨테이너 시작할 때 실행할 명령어)
LABEL이미지의 라벨 지정
ENV컨테이너의 환경 변수 지정
VOLUME컨테이너의 볼륨 지정
USER컨테이너의 사용자 지정
ARG인자 설정

Dockerfile

# jdk 17(amazoncorretto:17) 환경으로 구성
FROM amazoncorretto:17

# 인자 설정 :: 변수명 JAR_FILE
# build/libs(빌드 시 jar파일 생성 경로) 하위의 모든 jar파일
ARG JAR_FILE=build/libs/*.jar

# Docker Image 생성 시 JAR_FILE을 app.jar로 복사
COPY ${JAR_FILE} app.jar

# 실행 명령어
ENTRYPOINT ["java", "-jar", "app.jar"]

결과 확인하기

master 브랜치에 push 했을 때, 배포가 잘 되는지 확인해보았다.

트러블 슈팅


1. Docker build & Push 실패

      - name: Docker build & Push
      # 레포 명 뒤에 . 이 빠짐
        run: |
          docker build -t ${{ secrets.DOCKER_USERNAME }}/nnzz
          docker push ${{ secrets.DOCKER_USERNAME }}/nnzz


      - name: Docker build & Push
      # 변경 후
        run: |
          docker build -t ${{ secrets.DOCKER_USERNAME }}/nnzz .
          docker push ${{ secrets.DOCKER_USERNAME }}/nnzz

명령어 끝에 . 을 추가해준다. Dockerfile을 현재 디렉토리에서 찾을 수 있게 된다.

  1. Github Actions은 성공하나 테스트를 위해 api를 추가하여 푸시했는데 404 No static resource api/test. 라고 뜸
    (문제를 해결하는 데 도움을 주신 승등님께 감사의 인사를ㅎㅎ)

CI/CD job 전부 error 하나 없이 잘 실행된 터라 원인을 찾는데 오래 걸렸다.

파일을 빌드하는 부분을 Workflow에 넣었기 때문에,
FileZila에서 조회할 때 갱신된 .jar 파일이 로컬이든 서버든 있어야 한다고 생각을 했는데
build 파일은 수동 배포할 때 것 그대로였다.
-> ./gradlew clean build가 실행이 안 됐다고 생각했고 gradle build 부분에 문제가 있다고 여겨 그 부분을 계속 수정해봤는데 해결이 안 됐다.

사실 로컬과 서버 둘다 빌드 파일이 업데이트 안 되는게 맞다.
Github Actions은 가상머신 위에서 돌아가기 때문에 로컬에선 빌드 안되는게 맞고,
EC2 서버에는 docker image를 docker hub 통해서 받아오는거니까 빌드 파일이 없는게 맞다.
파일이 있다면 이전에 파일질라로 직접 넘겼던 것뿐이어야 한다.

그래서

  • push 했을 때 docker hub에 image 가 올라갔는지 확인하기
    -> image는 잘 있었다.

  • push 전후로 ec2 서버에서 docker 컨테이너 id를 확인하기.

sudo docker ps -a


컨테이너 자체가 없었다..

이미 8080을 써서 어플리케이션이 실행되고 있는 와중에 별개의 도커 컨테이너를 같은 8080으로 실행시키려고 하니
포트 충돌 때문에 컨테이너 자체가 실행되지 않은 것이다.

별 생각 없이 도커 설정만 해주면 되겠거니라고 생각했다.. 반성

실행중이던 8080포트를 종료하고 push 하니 정상적으로 실행되었다.
혹시 나와 같은 오류를 겪는 분들이 있다면 도움이 되길 바란다.

레퍼런스

[프로젝트 배포] RDS, S3 설정 및 프로젝트 배포

[프로젝트 배포] Docker를 활용한 배포 및 CI/CD 구축

Docker 설치 및 기본 명령어

profile
더 성장하자.

0개의 댓글