Hyun's Wonderwall

[Spring Boot] 백엔드 서버 배포하기 - AWS EC2, RDS + Docker + GitHub Actions로 CI/CD🌿 본문

Study/Java, Spring

[Spring Boot] 백엔드 서버 배포하기 - AWS EC2, RDS + Docker + GitHub Actions로 CI/CD🌿

Hyun_! 2024. 9. 22. 23:30

[Spring Boot] 백엔드 서버 배포하기 - AWS EC2, RDS + Docker + GitHub Actions로 CI/CD🌿

0. 시작하기 전에

이 글은 AWS EC2, RDS + GitHub Actions + Docker 기술을 사용해 Spring Boot 프로젝트를 배포하는 방법을 차근차근 안내한다. 필자가 야매로 터득한 방법으로, 백엔드 서버를 빠르게 배포하는 방법을 설명하는 데 초점을 맞추고 있다.
 

HTTP 배포하는 법 중심으로 다루며, HTTPS를 적용하는 방법 또한 글 후반부에 소개한다.

  • 백엔드 개발자가 혼자 배포를 연습하거나, 프론트엔드 로컬과 통신할 때나, 프론트엔드와 백엔드를 같은 서버로 배포할 때는 HTTP여도 문제없다. 다만 프론트엔드와 백엔드를 따로 배포하는 경우(=3티어 아키텍쳐), Vercel, Netlify 등으로 배포된 프론트엔드 서버와 통신하기 위해서는 백엔드 서버에 HTTPS를 적용해야 한다. (이것이 바로 악명이 자자한 CORS이다.)
  • 이 글은 학습에 도움을 주고자 탄력적 IP 주소, DB 엔드포인트와 비밀번호 등 모든 리소스 정보를 노출하였지만 이는 글 업로드 시점에 모든 리소스의 삭제를 완료했기 때문이다. 실제로 개발할 때는 이러한 정보들을 노출시키면 안 된다.

사전에 필요한 준비물은 다음과 같다.

  1. AWS 계정 회원가입 및 로그인
  2. GitHub에 올린 Spring Boot 레포지토리
  3. - API 개발이 끝나지 않은 상황이어도 된다. 다만, 로컬 빌드 에러가 나지 않아야 한다.
  4. Docker Hub 회원가입
  5. + Postman, MySQL Workbench

사용한 기술 스택은 다음과 같다. 자바 버전이나 EC2 아키텍처를 다른 것을 쓰는 경우에 텍스트를 변경해야 하는 부분이 있는제 해당 파트에서 첨언하도록 하겠다.

Java 17
Springboot 3.2.4
AWS EC2 (OS: Ubuntu 24.04 / 아키텍처: 64비트(x86))
RDS (MySQL Community)
Github Actions
Docker

1. AWS - EC2, VPC 생성

스팟 인스턴스가 아닌 온디맨드 인스턴스로 생성한다. (스팟 인스턴스는 저렴한 옵션으로 언제든 꺼질 수 있는 것.) 모든 AWS 리소스들은 사용이 끝나면 꼭 잘 삭제해야 한다.

1. (1) EC2 인스턴스 생성 1/2

AWS 서비스에서 오른쪽 위에 표시되는 지역명, "리전(Region)"을 선택한다. (리전은 국적과 무관하니 무작정 서울을 고르지 말고 시간당 요금이 저렴한 곳을 선택하자. 나는 버지니아 북부로 선택했다.)
 
EC2 서비스에 들어가서 [인스턴스 시작] 버튼을 누르고, 인스턴스 이름을 작성한다. (ex. my-instance)
 
[애플리케이션 및 OS 이미지]에서 사용할 운영체제를 고른다.
나는 Ubuntu를 선택했다. 아키텍처는 기본으로 설정되는 64비트(x86)으로 했다.
인스턴스 유형은 이 리전에서 프리 티어로 사용 가능한 t2.micro를 골랐다. (전문적으로 배포할 게 아니라면 무조건 요금이 저렴한 프리 티어 인스턴스 유형을 고르자. 일부 리전은 t2가 없고 t3.micro가 프리 티어이다.)

 
[키 페어(로그인)]에서 [새 키 페어 생성]을 클릭하고, 키 페어 이름을 입력한 뒤 [키 페어 생성]을 클릭해 내려받는다. 이 키 페어가 추후 CI/CD에 쓰인다.

 
이제 스크롤을 내려 [네트워크 설정]을 보면 VPC, 서브넷이라는 항목이 있다.
디폴트 VPC를 사용해도 되지만, 나는 VPC를 새로 만들어보겠다. 이미 생성해놓은 VPC가 있는 경우 1. (2)는 패스해도 된다.

1. (2) VPC 생성

새 탭을 열고 AWS VPC 서비스에 들어가 [VPC 생성]을 클릭한다.
생성할 리소스에 [VPC 등]을 클릭하고, 목적에 맞게 커스텀할 수 있다.
나는 가용 영역 수를 2개, 퍼블릭 서브넷 수를 2개로 해주었다. (프라이빗 서브넷 수는 상관없다. 요금도 더 나가지 않는다.)
NAT 게이트웨이와 VPC 엔드포인트는 사용하지 않을 것이므로 [없음]으로 설정했다.

VPC 생성을 클릭하면 VPC가 생성된다.
이제 VPC 탭은 닫아도 된다.

EC2를 만들고 있었던 탭으로 돌아가서, 네트워크 설정을 보면 VPC와 서브넷 오른쪽에 새로고침 버튼이 있다. (아래 사진 참고) 이 버튼을 눌러서 1. (2)에서 만든 VPC, 서브넷 정보로 세팅한다.

 

1. (3) EC2 인스턴스 생성 2/2

[네트워크 설정]에서, 자동으로 세팅된 VPC, 서브넷 이외에 다른 VPC나 서브넷을 고르고 싶다면 클릭해서 변경할 수 있다.
우리는 인스턴스를 배포용으로 사용할 것이므로 인스턴스가 퍼블릭이어야 한다. 퍼블릭 서브넷에 잘 연결된 것을 확인해준다.

[퍼블릭 IP 자동 할당] 설정은, 우리는 탄력적 IP를 퍼블릭 IP로 할당해 줄 것이기 때문에 비활성화했다. 
 
EC2가 사용할 [방화벽(보안 그룹)]을 만들어줘야 한다. [보안 그룹 생성]을 클릭해서 새로 만들어준다.

인바운드 보안 그룹 규칙은 아래에서 다시 설정해 줄 것이다.

 
[스토리지 구성]에서 볼륨 용량을 선택한다. 프리 티어로 30GiB까지 지원해주므로 30을 입력했다.

 
모든 과정을 완료하면 인스턴스가 잘 생성된 것을 볼 수 있다.

 

1. (4) 탄력적 IP 주소 연결

EC2 서비스 왼쪽 사이드바의 [네트워크 및 보안] 토글 하위의 [탄력적 IP] 메뉴를 선택한다.
[탄력적 IP 주소 할당] 버튼을 클릭해 퍼블릭 IPv4 주소를 얻는다.
[작업] > [탄력적 IP 주소 연결]을 클릭해, 위에서 생성한 인스턴스에 연결한다.

 
인스턴스 메뉴로 돌아가 인스턴스의 세부 정보를 보면, 인스턴스 퍼블릭 IPv4 주소로 탄력적 IP 주소가 잘 연결되어있다.

 

1. (5) 보안 그룹 편집

EC2 인스턴스 정보 화면에서 [세부 정보] 옆 [보안]을 클릭하면 인스턴스에 연결된 보안 그룹이 표시된다. 이 보안 그룹의 인바운드 규칙을 편집해야 한다.
보안 그룹을 클릭하고, [인바운드 규칙] > [인바운드 규칙 편집]을 클릭한다.

아래와 같이 SSH, 사용자 지정 TCP 8080, HTTP, HTTPS 를 Anywhere-IPv4 소스로부터 열어준다.

1. (6) EC2 인스턴스에 연결

EC2 탭으로 돌아와 인스턴스를 선택한 후 [연결]을 클릭한다.
사용자 이름이 뜨는데 ubuntu OS라서 디폴트로 ubuntu이다.
바꿔주지 않는 편이 편하다. [연결] 버튼을 누른다.

 
와~ EC2 인스턴스 접속에 성공해 콘솔 화면을 볼 수 있다.

아래 회색 공간에 인스턴스 정보가 표시된다.

+) Mac을 쓰고 있다면 터미널에서 바로 ec2에 접속할 수 있으며, 윈도우도 MobaXterm, Putty 등을 쓰면 [SSH 클라이언트]로 접속할 수 있다. (다른 블로그를 참고하시길) 나는 지금처럼 웹 페이지로 띄운 상태로 진행하겠다.
 
인스턴스에 Nginx와 Docker 설치하는 것을 2번에서 먼저 다루고 RDS 생성은 3번에서 다루겠다.


2. Nginx, Docker 세팅

이제부터 EC2 콘솔에서 ubuntu OS의 명령어를 사용해 이것저것 작업들을 수행하게 된다. 우분투, 리눅스, 쉘 명령어가 많이 쓰인다. 자주 쓰이는 명령어 sudo, ls, cd는 외워두는 것이 좋다.
sudo : 최고 관리자 권한으로 실행
ls
: list, 즉 목록 보기 (-l 옵션: 자세히 정보 보기, -a 옵션: 숨김 파일까지 보기)
cd : change directory, 즉 볼 폴더 이동

2. (1) EC2 인스턴스에 Nginx, Docker 설치

EC2 인스턴스 콘솔에 접속하여 아래의 명령어들을 입력한다.
 
서버 timezone을 서울로 세팅

timedatectl set-timezone Asia/Seoul

 
Nginx 설치

sudo apt update
sudo apt install nginx

설치 완료 후 sudo systemctl status nginx를 입력하면 nginx가 실행 중인 것을 확인할 수 있다.
 
Docker 설치

curl -fsSL https://get.docker.com/ | sudo sh

설치 완료 후 sudo docker --version으로 버전을 확인할 수 있다.
 

2. (2) Docker Hub에서 도커 레포지토리 생성

https://hub.docker.com/
도커 허브에 로그인하고, [Repository] 메뉴에서 [Create Repository]를 클릭한다.
내 계정에 만들 것이므로 namespace에 내 도커 계정 아이디가 들어가 있다.
레포지토리 이름을 지어주고, Visibility는 Public으로 한다.


3. AWS - RDS 생성 및 연결

RDS를 따로 만들어야 하는 이유는 다음과 같다.
  • Spring Boot 프로젝트를 MySQL DB와 연결하기 위해 당신은 MySQL을 설치했었고, 로컬호스트에 3306 포트로 사용하고 있었을 것이다(localhost:3306). EC2 인스턴스, 우리가 빌린 AWS 서버실의 어느 컴퓨터는, MySQL에 대해 아무것도 모르고 있는 상태다.
  • 어딘가에 DB를 두어야 한다. EC2 인스턴스의 로컬 DB를 쓸까? 그렇게 하면 인스턴스에 문제가 생겼을 때 DB도 함께 날아갈 것이다. RDS를 생성해 DB를 따로 두는 것이 안전하다.
  • EC2 인스턴스에 MySQL을 설치하면 콘솔에서 RDS와 연결해 DB를 확인할 수 있는데 여기서는 진행하지 않는다.
이 섹션은 RDS를 퍼블릭 서브넷에 위치시키고 내 컴퓨터의 MySQL Workbench에서 접속하는 방법을 안내한다.
  • RDS 퍼블릭 엑세스를 여는 방법으로 설명한다.
  • RDS를 퍼블릭 엑세스를 비허용하고 프라이빗 서브넷에 위치시켜 EC2 인스턴스를 통해서만 접근하도록 하는 것이 보안상으로 좋다고 알고 있지만, 해당 방법은 필자가 아직 잘 알지 못하여... 추후 공부를 보충하도록 하겠다.

3. (1) RDS 데이터베이스 생성

RDS 서비스 > 데이터베이스 탭에서 [데이터베이스 생성]을 클릭한다.
[데이터베이스 생성 방식 선택]에서 [표준 생성] 클릭 > 사용할 데이터베이스 엔진을 선택한다. (저의 경우엔 MySQL)

 
[템플릿]을 [프리 티어]로 선택한다.

 
[설정]에서 (이미지 첨부x)
"DB 인스턴스 식별자"가 DB 이름이다. 데이터베이스 이름을 입력해준다. (ex. mydatabase)
"마스터 사용자 이름"을 설정한다. (username) 디폴트는 admin이고 굳이 수정하지 않아도 된다.
"마스터 암호"를 설정한다. (password)
 
[스토리지]에 [스토리지 자동 조정 활성화] 체크를 해제하여 비활성화한다.

 
[연결]에서 RDS가 생성될 VPC를 위에서 생성한 VPC로 지정한다.
[퍼블릭 액세스]를 "예"로 설정한다.
RDS용의 보안 그룹을 새로 생성해야 하며, 나는 "my-rds-sg"라고 이름 붙였다.


스크롤을 내려서 [추가 구성] 토글을 열고 [자동 백업을 활성화합니다.] 체크를 해제하여 비활성화한다.

[데이터베이스 생성]을 클릭해 DB가 생성되는 동안 3.(2)를 진행한다.

3. (2) RDS 데이터베이스 보안 그룹 편집

데이터베이스 상세 정보의 [연결 및 보안] 탭 하위에서 보안 그룹을 클릭한다.
인바운드 규칙을 편집해 3306 포트로 들어오는 모든 IP 출처(IPv4, IPv6)의 트래픽을 허용하도록 규칙을 추가하고 저장한다.

모든 출처의 3306 포트 트래픽을 허용하는 이유: 막 생성한 RDS는 자기 자신 IP의 접속만 허용하고 있는데, 다른 모든 IP에서도 접속할 수 있도록 해야 여러 인터넷 위치에서 접속하는 개발 팀원들이 MySQL Workbench로 연결해서 DB를 볼 수 있다. 혼자 개발하더라도 여러 와이파이에서 접속할 것이므로 설정해줘야 한다.

3. (3) 배포용 application.yml 작성

Spring Boot 프로젝트를 인텔리제이로 연다.
GitHub 레포지토리에 올라가있으며 application.yml가 잘 깃이그노어 처리가 되어있다는 하에 진행한다.

.gitignore 파일에 application.yml이 적혀있어야 한다. application.yml을 이미 깃허브에 올린 적이 있어 깃이그노어가 되지 않는 경우, application.yml 파일을 삭제하는 커밋을 한번 푸시한 후 새로 생성하면 추적되지 않는다. yml 파일을 프로필로 로컬용/배포용 따로 관리하는 방법도 유용하나 여기서는 설명하지 않겠다.

 
본인이 로컬에서 사용하던 application.yml의 datasource 부분을 편집해 RDS 정보를 작성한다. 예시는 아래와 같다.
(url 부분에서 {RDS 엔드포인트}로 하든 {RDS 엔드포인트}:3306 으로 하든 둘 다 된다)

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://{RDS 엔드포인트}/{데이터베이스이름}?createDatabaseIfNotExist=true&characterEncoding=UTF-8&characterSetResults=UTF-8
    username: # 마스터 사용자 이름 (admin)
    password: # 마스터 암호

  jpa:
    hibernate:
      ddl-auto: update
    generate-ddl: true
    show-sql: true
예시

Run을 클릭해 이 파일로 Spring Boot 애플리케이션이 잘 실행되는 것을 확인하자.
Run 시키면 이 application.yml 파일을 통해 RDS에 연결하는데, 아직 생성된 데이터베이스가 없었으므로 새 데이터베이스가 생성된다. 콘솔에서 테이블들을 생성하는 DDL문들이 찍히는 것을 확인할 수 있다. (이미지 생략)
application.yml의 수정사항이 깃에 추적되지 않는 것을 다시 한번 확인해주자. 깃에 실수로 올리지 않도록 항상 주의해야 한다.
 

3. (4) MySQL Workbench에서 RDS 데이터베이스 연결 테스트

connection 추가에서 application.yml에 적었던 것처럼 엔드포인트, 사용자 이름, 암호를 넣어 RDS 데이터베이스에 연결할 수 있다. 현재 시점에 꼭 해보진 않아도 되므로 생략한다.


4. Docker, GitHub Actions로 CI/CD 구축

4. (1) Dockerfile 생성

배포용 브랜치에 Dockerfile을 아래와 같이 build.gradle 옆에 생성한다.

파일엔 아래 내용을 작성한다.
 

# 사용할 base 이미지 선택
FROM openjdk:17-slim

# build/libs/ 에 있는 jar 파일을 JAR_FILE 변수에 저장
ARG JAR_FILE=build/libs/*.jar

# JAR_FILE을 app.jar로 복사
COPY ${JAR_FILE} app.jar

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "-Duser.timezone=Asia/Seoul", "app.jar"]
Dockerfile 내용에 대한 간단 설명:
Docker에서 '이미지'란 애플리케이션 실행 환경을 포함하는 파일 시스템의 스냅샷이다. 
'base 이미지'는 컨테이너를 생성하는 데 사용되는 기본 이미지이다.
Spring Boot 애플리케이션을 빌드하고 실행하는 데 JDK가 필요하므로, 적절한 오픈소스 JDK 이미지를 베이스 이미지로 선택해야 한다.
이때 본인의 EC2 아키텍처를 고려해서 이미지를 선택해야 한다. 나는 EC2를 x86 아키텍처로 만들었었으므로 openjdk:17-slim 이미지를 선택했다. (다른 아키텍처라면 구글에 검색)

 
작성을 완료했다면 푸시한다.

4. (2) Repository Secrets 설정

깃허브에 배포용 RDS 정보나 EC2 키 등을 바로 올리는 것은 위험하다. 악의적인 공격을 받으면 요금 폭탄을 받게 될 수 있다.
안전을 위해 깃허브의 Secrets 기능을 사용하자.
깃허브 레포지토리 페이지의 Settings > Security > Secrets and variables > Actions에서
Repository secrets > New repository secret을 클릭해 아래의 변수들을 만들어준다. (변수명 정확히 같게 하기)

  • APPLICATION : 배포용 application.yml 파일 내용 복붙하기
  • DOCKER_USERNAME : Docker 아이디 (namespace)
  • DOCKER_PASSWORD:  Docker 비밀번호
  • DOCKER_REPOnamespace/레포지토리명 형식 // 2.(2)에서 만든 것!
  • HOST : EC2의 퍼블릭 IP
  • KEY : EC2 pem key // EC2를 만들 때 다운받아진 키 파일을 열어 전체 내용을 복붙
  • USERNAME : EC2 username // EC2 콘솔에서 입력할 때 왼쪽에 보이는 이름, 우리의 경우엔 ubuntu

4. (3) GitHub Actions 워크플로우 생성

웹 브라우저에서 배포할 깃허브 레포지토리를 열고 Actions 탭에서 [set up a workflow yourself]를 클릭한다.

 
워크플로우 파일명(ex. github-actions.yml)을 짓고, 아래의 내용을 붙여넣는다.
파일에서 ${{ secrets.변수명 }}이 적혀 있는 위치들에 위에서 만든 환경 변수의 값들이 들어간다.
본인의 세팅에 맞게 몇몇 정보를 수정해야 할 수 있다. # 주석을 참고.

name: Deploy to Amazon EC2

on:
  push:
    branches: [ "main", "deploy" ] # push가 될 때 워크플로우를 실행할 브랜치 리스트

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest

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

      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17' # 스프링부트 애플리케이션 자바 버전에 맞게
          distribution: 'zulu'

      - name: make application.yml
        run: |
          mkdir ./src/main/resources # 원격에 resources 폴더가 올라가 있다면 이 줄 삭제
          cd ./src/main/resources
          touch ./application.yml
          echo "${{ secrets.APPLICATION }}" > ./application.yml

      - 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-

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

      - name: Login to DockerHub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}


      # Docker 이미지 빌드 및 푸시
      - name: Docker build & push
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./Dockerfile
          platforms: linux/amd64 # EC2 아키텍쳐에 맞게 (x86 = amd64, arm = arm64)
          push: true
          tags: ${{ secrets.DOCKER_REPO }}:latest

      # 서버에 배포
      - name: Deploy to Server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.KEY }}
          envs: GITHUB_SHA
          script: |
            sudo docker ps -qa | xargs -r sudo docker rm -f
            sudo docker pull ${{ secrets.DOCKER_REPO }}:latest
            sudo docker run -d -p 8080:8080 ${{ secrets.DOCKER_REPO }}:latest
            sudo docker image prune -f

 
이제 위에 지정한 브랜치에 푸시가 발생할 때마다 워크플로우가 실행될 것이다.
 
워크플로우 파일을 main에 푸시하고 Actions 탭에서 실행 중인 워크플로우를 클릭하여 완료되길 기다리자.

✅️ : 에러가 나지 않았다. 축하드립니다!
 
❌️ : 에러가 발생했다. 에러 로그를 읽고 문제를 해결하자.
내 경우 아래 사진처럼 현재의 깃허브 레포지토리 안에 community라는 폴더가 한 겹 더 있어서 리눅스 명령어 부분에서 파일을 찾지 못하는 문제가 발생했다. 아래처럼 워크플로우 파일을 수정했다.
(아래의 워크플로우 코드는 디렉터리 구조가 아래 사진 처럼 생긴 사람만 쓰세요. 위에서 문제가 발생하지 않았다면 수정 필요 x)

name: Deploy to Amazon EC2

on:
  push:
    branches: [ "main", "deploy" ] # push가 될 때 워크플로우를 실행할 브랜치 리스트

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest

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

      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17' # 스프링부트 애플리케이션 자바 버전에 맞게
          distribution: 'zulu'

      - name: make application.yml
        run: |
          cd ./community/src/main/resources
          touch ./application.yml
          echo "${{ secrets.APPLICATION }}" > ./application.yml

      - 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-

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

      - name: Login to DockerHub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}


      # Docker 이미지 빌드 및 푸시
      - name: Docker build & push
        uses: docker/build-push-action@v6
        with:
          context: ./community
          file: ./community/Dockerfile
          platforms: linux/amd64 # EC2 아키텍쳐에 맞게 (x86 = amd64, arm = arm64)
          push: true
          tags: ${{ secrets.DOCKER_REPO }}:latest

      # 서버에 배포
      - name: Deploy to Server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.KEY }}
          envs: GITHUB_SHA
          script: |
            sudo docker ps -qa | xargs -r sudo docker rm -f
            sudo docker pull ${{ secrets.DOCKER_REPO }}:latest
            sudo docker run -d -p 8080:8080 ${{ secrets.DOCKER_REPO }}:latest
            sudo docker image prune -f

 
워크플로우 진행상황을 보며 기다리니 배포에 성공했다. 


4. (4) 도커 컨테이너 상태 확인

정말 배포가 성공했는지 보자.
EC2 콘솔에 sudo docker ps를 입력해서 실행 중인 컨테이너가 있는지 확인한다.
컨테이너가 나오고, STATUS가 Up이라면 컨테이너가 잘 떠 있는 것이다. 성공🥳

 
깃허브 액션에서는 워크플로우가 성공했다고 떴는데 실제로 컨테이너가 떠 있지 않다면, 내부에서 티 나지 않는 문제가 발생한 것이다.
워크플로우 로그를 훑어보자. 검은 화면에서 각 단계 토글을 클릭하면 로그를 볼 수 있다. err 같은 로그가 있을 수 있다.

sudo docker images는 올라와있는 도커 이미지들을 보여주는 명령어로, 이미지가 잘 올라왔는지 확인할 수 있다. 이미지는 잘 올라왔는지 한번 확인해보자.
 
컨테이너가 실행되지 않았다면 다음과 같은 문제를 짐작해볼 수 있다.

  1. GitHub Actions 워크플로우 파일에서 환경 변수 이름 오타
  2. Repository Secrets에 환경 변수 이름을 잘못 만들었거나 값을 잘못 넣음
  3. Deploy to Server에서 도커 명령어 관련해 sudo 권한을 주지 않음
  4. RDS 연결이 되지 않음

sudo docker ps -a를 입력해서 보니 컨테이너가 올라왔었는데 Exit됐다면, 스프링부트 애플리케이션 실행 에러로 인해 종료된 것이다. application.yml 파일이나 RDS 문제일 수 있다. (혹시 redis를 사용한다면 추가적인 설치와 세팅이 필요하다. 이 글에서 설명하진 않는다.)
 

4. (5) 스프링부트 애플리케이션 로그 보기

도커 컨테이너 ID로 애플리케이션 로그를 볼 수 있다.
sudo docker ps 또는 sudo docker ps -a의 출력 결과에서 CONTAINER ID를 복사하여 다음과 같이 입력한다.

sudo docker logs {컨테이너명}

애플리케이션이 잘 실행되었다는 로그가 보인다.
+) sudo docker logs {컨테이너명} --tail 200 와 같이 tail 옵션을 주면 아래서부터 200줄만 출력할 수 있다.
 
'url' ~~ 하는 익숙한 데이터베이스 연결 에러가 뜬다면 RDS와 연결을 하지 못하는 것이다. 지금까지의 절차를 잘 따라왔다면 이 문제가 발생하지 않아야 한다. 그래도 발생했다면: RDS의 퍼블릭 엑세스 권한이 허용인지, RDS의 보안 그룹 인바운드 규칙에서 해당 데이터베이스가 쓰는 포트가 Anywhere:IPv4에 잘 열려있는지, application.yml 파일에 오타가 없는지 등을 확인해보기 바란다.

4. (6) 웹 브라우저에서 배포 확인

IP 주소를 웹 브라우저에 입력하면 Nginx가 반겨준다.

주의 요함이라고 뜨는 이유는 HTTP 배포라서 그렇다.

5. 배포된 API 테스트 해보기

개발 완료된 API가 배포된 스프링부트 애플리케이션에 들어있다면 Postman으로 테스트를 할 수 있다. 
Environments > Globals에서 환경 변수를 만들자. 이름은 host로 하고 퍼블릭 IP를 담은 후 저장한다. 

 
요청을 보낼 url을 적고 API 요청을 보내보자. {{host}} 이렇게 중괄호 두 개로 환경 변수를 쓸 수 있다.
Nginx에 별다른 설정을 하지 않아, IP 뒤에 8080 포트 번호까지 붙여야 Spring Boot 애플리케이션과 통신할 수 있다.
나는 회원가입 API 요청을 보내보겠다.

성공하여 회원이 생성되었다.
+) MySQL Workbench에서도 데이터가 생성된 것을 확인해보시길 바란다.


6. HTTPS

현재까지 한 것만으로, 프론트엔드 로컬과 통신을 할 수 있다. (http://{퍼블릭IP주소}:8080/{uri}으로 요청 보내도록)
이제 도메인 연결, Nginx 설정과 HTTPS 적용이 남았다.
프론트엔드 배포 서버와 통신을 하려면 HTTPS 적용이 필수이기 때문에 꼭 해줘야 한다. 조금만 더 힘내서 다 해버리자!
HTTPS 적용 방법을 잘 설명해주신 선배님의 글을 첨부하였다. 글의 중반부터 읽으면 된다.
https://velog.io/@xyzw/Springboot-EC2-Github-Actions-Docker-%EC%9D%B4%EC%9A%A9%ED%95%9C-CICD-Nginx-Certbot-%EC%9D%B4%EC%9A%A9%ED%95%9C-HTTPS-%EB%AC%B4%EB%A3%8C-%EB%B0%B0%ED%8F%AC


7. AWS 리소스 지우기

AWS 리소스 사용이 모두 끝났다면 잘 삭제해줘야 과금을 막을 수 있다.

EC2 관련

EC2 인스턴스 종료(삭제)
탄력적 IP 주소 연결 해제 및 릴리스
스냅샷 생성된 것 있는지 확인하고, 있으면 삭제
시작 템플릿이랑 AMI 생성된 것 있는지 확인하고, 있으면 삭제

RDS 관련

데이터베이스 인스턴스 삭제
최종 스냅샷 만들지 않도록 하기

스냅샷 탭에 생성된 것 있는지 확인하고, 있으면 삭제

VPC

vpc도 삭제한다. 또는 퍼블릭 서브넷들의 퍼블릭 IP 자동 할당을 비허용으로 한다.


 
스프링 부트 공부를 한 지가 어느덧 1년 반, 개발 프로젝트에서 백엔드 개발자로써 CI/CD 구축 및 배포를 담당해본 것이 4회가 넘었네요. 이제 배포 루틴이 꽤 익숙해졌지만 아직도 작업의 우선 순위나 몇몇 설정들을 헷갈리게 되어서 한번 쭉 정리해 보았습니다.
오류가 있다면 언제든 지적 부탁드리며, 궁금한 부분이 있으실 경우 물어보시면 아는 대로 답변 드리겠습니다! 
부족함 많은 글이지만 읽어주셔서 감사드립니다. 도움이 되었다면 하트 한 번 부탁드립니다❤