https://20240228.tistory.com/442
[EasyMarket] 프로젝트 시작, CORS 해결하기
프로젝트 환경 설명 spring boot(html, css, javascript, thymeleaf) 프론트spring boot 백엔드 나는 백엔드 개발자여서 vue, react 같은 프론트 프레임워크를 전혀 모르지만 단순 SSR을 하는 프로젝트 보다는 이렇
20240228.tistory.com
이전 내용
순서
EC2 인스턴스 생성 → 탄력적 IP → CICD 이해 → CICD 구축 → 타임리프 트러블 슈팅
EC2 인스턴스 생성하기
AWS EC2 서비스로 들어가서 리전(Region) 선택
꼭 아시아 태평양(서울) ap-northeast-2 로 선택 (한국에 산다면)
리전이란?
EC2란 우리가 컴퓨터를 원격으로 접속해 사용하는 서비스이다. EC2를 통해 빌려서 쓸 수 있는 컴퓨터들이 전 세계적으로 다양하게 분포해있다. 이렇게 컴퓨터가 위치한 위치를 AWS에서는 리전이라고 한다.
사용자의 위치와 애플리케이션의 실행시키고 있는 컴퓨터의 위치가 멀면 멀수록 느려진다. 따라서 애플리케이션의 주된 사용자들의 위치와 지리적으로 가까운 리전을 선택하는 것이 좋다.
참고로 리전마다 EC2가 따로따로 관리된다.
애플리케이션 및 OS 이미지 Ubuntu 선택
인스턴스 유형 선택
실제 서비스 운영이 아닌 프로젝트용 배포여서 t2.micro 프리티어를 선택했다.
키 페어 (로그인)
나는 이미 생성된 .pem 키가 있어서 재사용
만약 처음 생성한다면 생성하고 잘 보관해야한다. 추후에 인스턴스에 SSH로 접근할 때 필요(인증 수단)
물론 .pem 키를 분실해도 AWS에 로그인하고 관리 콘솔을 통해 EC2 접근은 가능하지만 GithubActions에서 필요
네트워크 설정
방화벽(보안 그룹)을 보면 보안 그룹 생성과 기존 보안 그룹 선택이 있는데
보안 그룹을 생성하면 기본적으로 22(SSH) 80(HTTP) 443(HTTPS) 포트가 인바운드 규칙으로 설정된다.
즉, 외부에서 접근할 때 접근 가능한 포트가 3개 열린 것이다.
나는 이미 보안 그룹이 있어서 재사용하고 없으면 생성하면 된다.
스토리지 구성
우리가 쓰고 있는 노트북이나, 컴퓨터는 전부 하드디스크를 가진다. EC2 또한 하나의 컴퓨터이다보니 파일을 저장할 공간이 필요하다.
이 저장 공간을 EBS(Elastic Block Storage)라고 부른다. 또는 스토리지(Storage), 볼륨(Volume)이라고 부른다.
모든 EC2 즉, 내가 생성하고 실행중인 모든 EC2의 총합 최대 무료 스토리지는 30GiB이다.
스토리지 종류를 보면 gp3 이외에도 여러가지 종류의 스토리지가 있는데 gp3가 가장 가성비가 좋다.
인스턴스 시작
탄력적 IP 연결하기
프라이빗 IP, 퍼블릭 IP
프라이빗 IP는 내부 네트워크에서만 유효한 IP 주소이다.
즉, 외부 인터넷과의 직접적인 통신은 불가능하고, 내부 시스템 간의 통신을 위해 사용된다.
퍼블릭 IP는 인터넷 상에서 고유한 주소로, 외부 인터넷과의 통신을 위해 사용된다. 해당 IP는 이 인터넷 상의 다른 시스템과 직접 통신할 수 있다.
탄력적 IP?
EC2 인스턴스를 생성하면 퍼블릭 IP(43.201.10.182)를 할당받는다. 하지만 이렇게 할당받은 퍼블릭 IP는 임시적인 IP이다. EC2 인스턴스를 잠깐 중지시켰다가 다시 실행시켜보면 퍼블릭 IP가 바뀌어있다. EC2 인스턴스를 중지시켰다가 다시 실행시킬 때마다 IP가 바뀌면 굉장히 불편하다. 그래서 중지시켰다가 다시 실행시켜도 바뀌지 않는 고정 퍼블릭 IP(탄력적 IP, Elastic IP)를 할당받아야 한다.
참고로 이렇게 인스턴스를 중단하고 다시 실행해서 IP가 바뀌는 이유는 퍼블릭 IP 주소는 전 세계적으로 제한된 자원이다.
AWS는 동적 할당 방식을 통해 필요할 때만 IP를 할당하고, 사용이 끝난 인스턴스에 대해서는 퍼블릭 IP를 반납한다.
이 때문에 인스턴스를 중단한 후 다시 시작하면, 기존의 퍼블릭 IP는 반납되고 새로운 IP가 할당된다.
탄력적 IP 할당, 연결
나는 현재 이미 탄력적 IP 주소를 할당받았는데 할당받지 않았다면 탄력적 IP 주소 할당 버튼을 눌러서 할당받으면된다.
할당된 탄력적 IP이다. 이제 탄력적 IP 주소 연결을 누른다.
방금 생성한 EC2 인스턴스를 선택하고 EC2 프라이빗 IP 주소를 넣어준다.
연결을 하고 인스턴스로 돌아와서 퍼블릭 IP 주소를 확인해 보면 탄력적 IP로 바뀐 것을 확인할 수 있다.
CI/CD란?
CI/CD란 Continuous Integration, Continuous Deployment라는 의미를 가지고 있다. 즉, 테스트(Test), 통합(Merge), 배포(Deploy)의 과정을 자동화하는 걸 의미한다.
서비스를 운영하다보면 새로운 기능을 추가하는 일이 많아진다. 새로운 기능에 대한 코드를 작성하고 배포하려면 EC2에 직접 접속해서 새로운 코드를 clone/pull 받고 실행시켜주어야 한다.
이 과정을 코드의 수정이 일어날 때마다 반복하기란 너무 귀찮은 일이다. 그래서 이런 반복적인 과정을 자동화시키기 위해 CI/CD를 배우는 것이다.
일반적인 CI/CD 흐름
CI/CD 구축
내가 구축하려는 CI/CD 흐름
AWS EC2에서 프로젝트를 빌드하고 배포하는 게 아닌 Github Actions에서 테스트/빌드 하고 빌드 파일을 EC2에 전달한다.
빌드 작업을 Github Actions에서 하기 때문에 운영하고 있는 서버의 성능에 영향을 거의 주지 않는다.
현재 프론트/백엔드 application.yml은 gitignore된 상태
즉, application.yml이 버전관리가 되지 않게 세팅한다.
나는 application.yml 자체를 깃허브 레포지토리 시크릿 변수로 적용하려고 한다.
프론트 application.yml
spring:
profiles:
active: prod
---
spring:
config:
activate:
on-profile: local
backend:
url: http://localhost:8081
server:
port: 8080
---
spring:
config:
activate:
on-profile: prod
backend:
url: http://54.180.214.174:8081
server:
port: 80
프론트엔드 서버 80포트로 설정
EC2 인스턴스에서 프론트엔드 서버를 80번 포트로 설정하면, 퍼블릭 IP 주소만으로도 접근이 가능해진다. 이는 HTTP 프로토콜의 기본 포트인 80번 포트를 사용하기 때문에, URL에 포트를 명시할 필요 없이 단순히 IP 주소나 도메인만 입력해도 접근할 수 있다.
ex) naver.com
백엔드 application.yml
spring:
profiles:
active: local
---
spring:
config:
activate:
on-profile: local
front:
url: http://localhost:8080
server:
port: 8081
---
spring:
config:
activate:
on-profile: prod
front:
url: http://54.180.214.174
server:
port: 8081
액션 시크릿 변수 넣어주기
Repository → Settings → Security → Secrets and Variables → Actions → new Repository secret
프론트/백엔드 각 레포지토리에 아래 변수들 추가
APPLICATION_PROPERTIES: application.yml(activate profile: prod)
EC2_HOST: 탄력적 IP
EC2_USERNAME: ubuntu
EC2_PRIVATE_KEY: .pem key
-----BEGIN RSA PRIVATE KEY-----
~~~~~~~~
-----END RSA PRIVATE KEY-----
주의 맨 마지막 % 제외하기
EC2 자바 설치
연결 클릭
연결 클릭
JDK 설치, 확인
sudo apt update && /
sudo apt install openjdk-17-jdk -y
java -version
깃허브 액션 작성
트러블 슈팅
일반적으로 포트 번호가 1024보다 작은 포트는 운영 체제에서 시스템 포트로 간주되어, 보통 root 권한이 필요하다.
즉 내가 프론트 서버에서 사용하려는 80포트는 sudo 관리자 권한으로 서버를 실행해야 한다.
프로세스 조회도 마찬가지 관리자 권한으로 확인
프로젝트 루트 경로에 깃허브 액션 코드 작성
.github/workflows/deploy.yml
프론트엔드
name: Deploy to EC2(Front)
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Github Repository 올린 파일들 불러오기
uses: actions/checkout@v4
- name: JDK 17버전 설치
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: application.yml 파일 만들기
run: echo "${{ secrets.APPLICATION_PROPERTIES }}" > ./src/main/resources/application.yml
- name: 테스트 및 빌드하기 (테스트 실패 -> 배포 실패)
run: ./gradlew clean build
- name: 빌드된 파일 이름 변경하기
run: mv ./build/libs/*SNAPSHOT.jar ./front.jar
- name: SCP 사용 EC2에 빌드된 파일 전송
uses: appleboy/scp-action@v0.1.7
with:
# EC2 퍼블릭 주소
host: ${{ secrets.EC2_HOST }}
# EC2 username
username: ${{ secrets.EC2_USERNAME }}
# EC2 .pem
key: ${{ secrets.EC2_PRIVATE_KEY }}
source: front.jar
target: /home/ubuntu/front-server/tobe
- name: SSH 사용하여 EC2 접속 및 배포
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
script_stop: true # 실패하면 전체 실패 처리
script: |
# 이전 폴더 삭제
rm -rf /home/ubuntu/front-server/current
# 폴더 다시 생성
mkdir /home/ubuntu/front-server/current
# tobe -> current 빌드파일 이동
mv /home/ubuntu/front-server/tobe/front.jar /home/ubuntu/front-server/current/front.jar
cd /home/ubuntu/front-server/current
# 실행 중인 애플리케이션 종료 (서버가 실행 중이 아니어도 에러 없이 통과)
sudo fuser -k -n tcp 80 || true
# 종료 대기 (2초)
sleep 2
# 애플리케이션 실행 output.log 로그 기록 80 포트는 sudo 필요
sudo nohup java -jar front.jar > ./output.log 2>&1 &
# 폴더 삭제
rm -rf /home/ubuntu/front-server/tobe
# 배포 완료
백엔드
name: Deploy to EC2(Back)
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Github Repository 올린 파일들 불러오기
uses: actions/checkout@v4
- name: JDK 17버전 설치
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: application.yml 파일 만들기
run: echo "${{ secrets.APPLICATION_PROPERTIES }}" > ./src/main/resources/application.yml
- name: 테스트 및 빌드하기 (테스트 실패 -> 배포 실패)
run: ./gradlew clean build
- name: 빌드된 파일 이름 변경하기
run: mv ./build/libs/*SNAPSHOT.jar ./back.jar
- name: SCP 사용 EC2에 빌드된 파일 전송
uses: appleboy/scp-action@v0.1.7
with:
# EC2 퍼블릭 주소
host: ${{ secrets.EC2_HOST }}
# EC2 username
username: ${{ secrets.EC2_USERNAME }}
# EC2 .pem
key: ${{ secrets.EC2_PRIVATE_KEY }}
source: back.jar
target: /home/ubuntu/backend-server/tobe
- name: SSH 사용하여 EC2 접속 및 배포
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
script_stop: true # 실패하면 전체 실패 처리
script: |
# 이전 폴더 삭제
rm -rf /home/ubuntu/backend-server/current
# 폴더 다시 생성
mkdir /home/ubuntu/backend-server/current
# tobe -> current 빌드파일 이동
mv /home/ubuntu/backend-server/tobe/back.jar /home/ubuntu/backend-server/current/back.jar
cd /home/ubuntu/backend-server/current
# 실행 중인 애플리케이션 종료 (서버가 실행 중이 아니어도 에러 없이 통과)
sudo fuser -k -n tcp 8081 || true
# 종료 대기 (2초)
sleep 2
# 애플리케이션 실행 output.log 로그 기록
nohup java -jar back.jar > ./output.log 2>&1 &
# 폴더 삭제
rm -rf /home/ubuntu/backend-server/tobe
# 배포 완료
타임리프 트러블 슈팅
@Controller
public class HomeController {
@GetMapping("/")
public String home(){
return "/home/home";
}
}
로컬 환경에서는 이렇게 해도 정상 동작했지만 배포 환경에서는 타임리프 에러가 발생한다.
org.thymeleaf.exceptions.TemplateInputException: Error resolving template [/home/home]
기본적으로 타임리프 템플릿은 src/main/resources/templates/에 위치하는데
배포환경에서 경로가src/main/resources/templates//home/home이렇게 되어서 실패했던 것이다.
그런데 왜 로컬에서는 되는거지..?
해결 완료
@Controller
public class HomeController {
@GetMapping("/")
public String home(){
return "home/home";
}
}
백엔드 프론트 통신 테스트
http://54.180.214.174(front) → http:54.180.214.174:8081(back)
CORS 문제 없이 성공!
다음 목표
깃허브 액션을 통한 CI/CD 구축을 완료하여 로컬 환경과 배포 환경 모두 편하게 개발하게 되었다.
다음은 회원가입을 위한 이메일 OTP 인증을 구현해 보겠다.
참고 자료
비전공자도 이해할 수 있는 CI/CD 입문·실전 강의 | JSCODE 박재성 - 인프런
JSCODE 박재성 | , 🤬 에라이, 못 해먹겠네!비전공자로 개발을 시작해 여러 회사에서 CTO로 활동하다가, 현재는 교육자로 활동하고 있는 박재성이라고 합니다. 저도 비전공자로 개발을 시작해 서버
www.inflearn.com
'사이드 프로젝트 기록' 카테고리의 다른 글
[EasyMarket] 스프링 이메일 인증 구현 (Redis/OTP) (0) | 2025.03.27 |
---|---|
[EasyMarket] 프로젝트 시작, CORS 해결하기 (0) | 2025.03.26 |
[EasyMarket] 트랜잭션, 꼭 서비스에서 시작해야 할까? (0) | 2025.02.26 |
[EasyMarket] 세션 VS JWT 토큰 방식 (0) | 2025.02.07 |