[AWS] 프리티어로 EC2 인스턴스 생성 및 SSH로 접속하기 / EC2와 RDS 연결하기
지난 글에서 AWS EC2 서버를 만들고, RDS 와 연결하는 과정까지 진행했다.
개발 중인 Spring boot 프로젝트를 EC2 서버에서 실행시켜보자.
서버에서 프로젝트를 직접 배포하는 것은 크게 두 가지가 있다.
나는 이미 FileZila를 쓰고 있었기 때문에, jar 파일을 직접 전달하는 방법을 선택했다.
FileZila
사용자의 PC와 호스팅 서버 간 파일 송수신을 위한 위한 FTP(File Transfer Protocol) 소프트웨어
FileZila Client 는 여기서 다운받을 수 있다.
https://filezilla-project.org/download.php?type=client

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

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

서버에 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 를 구축하기로 했다.
도커를 활용한 배포 흐름은 아래와 같다.
이 과정을 자동화 할 것이고, CI/CD 파이프라인 구축에는 Github Actions를 사용할 것이다.
Github Actions
Github 저장소를 기반으로 소프트웨어 개발 Workflow를 자동화할 수 있는 도구. 특정한 브랜치에 대한 이벤트(트리거)를 통해 설정된 Workflow를 따라, 빌드, 테스트, 배포 등의 다양한 동작을 자동으로 실행되게 할 수 있다.
프로젝트 레포의 Settings-Security-Secrets and Variables-Actions 경로에서 보안상의 이유로 Github에 올릴 수 없는 환경 변수들을 작성해준다.

등록한 환경변수들은 값을 볼 수 없고, 수정 또는 삭제만 가능하다.
Docker Hub 로그인 시 구글 로그인으로 가입하면 username과 password 만으로는 로그인이 불가능하다. 토큰 방식을 사용해야만 로그인이 가능한 듯 하다.
프로젝트 레포의 Actions-New Workflow를 통해 workflow 파일 작성을 위한 양식을 선택할 수 있다.



Java with Gradle을 선택하면 Java-Gradle 환경에서 사용할 수 있는 Workflow 파일의 기본 양식을 보여준다. 이걸로 생성해도 되고, 직접 .github > workflows > 폴더 안에 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
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
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
윈도우 10 홈 에디션 기준
제어판 > 프로그램 > 프로그램 및 기능 > Windows 기능 켜기/끄기 버튼을 클릭한다.
Linux 용 Windows 하위 시스템, 가상 머신 플랫폼에 체크하고 확인 버튼을 클릭한다. -> 체크한 기능을 활성화하려면 컴퓨터를 다시 시작해야한다.
https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi
에 접속하여 리눅스 커널을 다운받는다. 다운로드 받은 파일을 실행하여 리눅스 커널을 업데이트 할 수 있다.
https://docs.docker.com/desktop/install/windows-install/
윈도우용 도커 데스크톱을 다운받는다.
설치 중, Configuration 화면에서 모든 항목에 체크한 다음 OK 버튼을 진행한다.
설치가 완료되면 Close and log out 버튼을 클릭해 윈도우에 다시 로그인한다. (컴퓨터가 재부팅된다.)
바탕화면에 Docker Desktop이 추가되어있다.
# 도커 설치
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 image를 push할 레포지토리를 생성한다. public으로 생성하자. (private의 경우 계정 당 1개까지만 무료로 생성이 가능하다.)
프로젝트 최상위 경로 아래에 Dockerfile을 생성한다. (※ src 아래에 생성하면 안 된다!)

| 지시어 | 설명 |
|---|---|
| FROM | 베이스 이미지 지정 |
| RUN | 이미지를 지정하면서 실행할 명령 지정 |
| ENTRYPOINT | 컨테이너의 어플 지정 (컨테이너 시작할 때 실행할 명령어) |
| EXPOSE | 컨테이너의 포트 지정 |
| ADD | 이미지 생성 시 파일 추가 |
| COPY | 이미지 생성시 파일 복사 |
| WORKDIR | 컨테이너 작업 디렉토리 지정 |
| MAINTAINER | 이미지 작성자 명시 |
| CMD | 컨테이너의 어플 지정 (컨테이너 시작할 때 실행할 명령어) |
| LABEL | 이미지의 라벨 지정 |
| ENV | 컨테이너의 환경 변수 지정 |
| VOLUME | 컨테이너의 볼륨 지정 |
| USER | 컨테이너의 사용자 지정 |
| ARG | 인자 설정 |
# 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을 현재 디렉토리에서 찾을 수 있게 된다.

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 설정 및 프로젝트 배포