프로젝트를 진행하면서 여러 가지 어려운 문제들을 겪었습니다. 특히, Vue.js, Spring Boot, 그리고 쿠키 문제와 관련된 이슈들이 가장 힘들었습니다. 하지만 이러한 문제들을 해결하면서 많은 것을 배울 수 있었습니다. 이제 각각의 이슈를 소개하고 해결 과정을 공유하겠습니다
파일 구조
nginx.conf
server {
listen 80; # 웹 서버가 HTTP 요청을 수신할 포트 번호를 지정합니다. 여기서는 포트 80을 사용합니다.
server_name localhost; # 서버 이름을 지정합니다. 여기서는 localhost로 설정되어 있습니다.
location / {
root /usr/share/nginx/html; # 요청된 파일의 기본 디렉토리를 설정합니다. 여기서는 /usr/share/nginx/html 디렉토리를 사용합니다.
try_files $uri $uri/ /index.html; # 요청된 URI를 실제 파일이나 디렉토리와 일치시키려 시도합니다. 만약 요청된 파일이나 디렉토리가 없으면 /index.html을 반환합니다.
}
error_page 404 /index.html; # 404 오류 페이지를 설정합니다. 404 오류가 발생하면 /index.html 파일을 반환합니다.
location = /40x.html {
internal; # 이 위치 블록은 내부 요청에만 사용됩니다. 직접 외부에서 접근할 수 없습니다.
}
}
이 설정 파일은 기본적으로 Nginx가 포트 80에서 수신하는 HTTP 요청을 처리하고, 요청된 URI가 실제 파일이나 디렉토리와 일치하지 않는 경우 index.html 파일을 반환하는 구성을 정의합니다. 이는 일반적으로 SPA에서 클라이언트 측 라우팅을 지원하기 위해 사용됩니다. 404 오류가 발생할 때도 메인 페이지인 index.html을 반환하여 사용자가 항상 메인 페이지를 볼 수 있도록 합니다.
Dockerfile
# 빌드 스테이지: Node.js를 사용하여 빌드
FROM node:latest AS build # 최신 Node.js 이미지를 사용하여 빌드 스테이지를 정의합니다.
WORKDIR /app # 컨테이너 내에서 /app 디렉토리를 작업 디렉토리로 설정합니다.
# package.json 및 package-lock.json(있는 경우) 파일 복사 및 의존성 설치
COPY package*.json ./ # package.json 및 package-lock.json 파일을 컨테이너의 작업 디렉토리로 복사합니다.
RUN npm install # 의존성 패키지를 설치합니다.
# 소스 코드 복사 및 빌드 실행
COPY . . # 모든 소스 코드를 컨테이너의 작업 디렉토리로 복사합니다.
RUN npm run build # 애플리케이션을 빌드합니다.
# 프로덕션 스테이지: nginx를 사용하여 정적 파일 서빙
FROM nginx:latest # 최신 nginx 이미지를 사용하여 프로덕션 스테이지를 정의합니다.
# 빌드된 파일을 nginx의 기본 웹 디렉토리로 복사
COPY --from=build /app/dist /usr/share/nginx/html # 빌드 스테이지에서 생성된 파일을 nginx의 기본 웹 디렉토리로 복사합니다.
# nginx 설정 파일 복사
COPY nginx.conf /etc/nginx/conf.d/default.conf # 사용자 정의 nginx 설정 파일을 복사하여 nginx의 기본 설정을 덮어씁니다.
EXPOSE 80 # 컨테이너가 80번 포트를 수신 대기하도록 설정합니다.
CMD ["nginx", "-g", "daemon off;"] # nginx를 실행합니다. 'daemon off;' 옵션은 nginx가 포어그라운드에서 실행되도록 합니다.
이 Dockerfile은 Node.js를 사용하여 애플리케이션을 빌드하고, nginx를 사용하여 정적 파일을 서빙하는 환경을 설정합니다. 빌드 스테이지에서 애플리케이션을 빌드한 후, 프로덕션 스테이지에서 nginx를 사용하여 정적 파일을 제공하게 됩니다.
chan1117/book-tory-front ( docker Repositories )
ec2에 서버에 접속해서 가져오기 위해서는 repository를 생성해서 build를 해줘야한다.
Docker build 명령어
docker build --platform linux/amd64 -t chan1117/book-tory-front .
or
docker build -t chan1117/book-tory-front .
차이점 요약
- 플랫폼 지정 (--platform):
- 첫 번째 명령어는 --platform linux/amd64 옵션을 사용하여 이미지를 특정 플랫폼(linux/amd64)에서 빌드합니다.
- 두 번째 명령어는 플랫폼을 지정하지 않고 기본 플랫폼에서 이미지를 빌드합니다. 이 경우 Docker가 실행되고 있는 시스템의 플랫폼을 사용합니다.
- 사용 시나리오:
- 첫 번째 명령어는 여러 플랫폼을 지원하는 시스템에서 이미지의 플랫폼을 명시적으로 지정하고자 할 때 유용합니다. 예를 들어, Apple M1 (ARM) 칩에서 x86_64 아키텍처용 이미지를 빌드하려는 경우에 사용합니다.
- 두 번째 명령어는 플랫폼을 지정할 필요가 없을 때, 즉 Docker가 실행되고 있는 시스템의 기본 플랫폼에 맞게 이미지를 빌드하고자 할 때 사용합니다.
dist 폴더가 생기면 성공
빌드된 이미지를 Docker Hub에 푸시합니다:
docker push chan1117/book-tory-front
ec2 Ubuntu 환경에 접속
Ununtu에서 Nginx 설치
1. 패키지 목록 업데이트
sudo apt update
2. Nginx 설치:
sudo apt install nginx
3. Nginx 시작:
sudo systemctl start nginx
4. Nginx 상태 확인:
sudo systemctl status nginx
5. Nginx가 부팅 시 자동 시작되도록 설정:
sudo systemctl enable nginx
집중!!! 어렵기 시작한다... ㅠㅠㅠ
그동안 일어났넌 트러블 이슈들.... 문제점을 찾아가보자...
첫번째 이슈
해당 오류는 CORS(Cross-Origin Resource Sharing) 정책과 관련된 문제입니다. 메시지에서 알 수 있듯이 Access-Control-Allow-Origin 헤더가 동일한 값이 중복되어 설정되었습니다. 이 문제는 Nginx 또는 Spring Boot 설정에서 중복된 CORS 설정으로 인해 발생합니다.
문제 되는 코드
Nginx 설정
server {
listen 80;
server_name 52.78.9.158;
location / {
proxy_pass http://52.78.9.158:5173;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
try_files $uri $uri/ /index.html;
# CORS 헤더 설정
add_header Access-Control-Allow-Origin "http://52.78.9.158:5173" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, access, refresh" always;
add_header Access-Control-Expose-Headers "Authorization, access, refresh" always;
add_header Access-Control-Allow-Credentials true always;
}
location /api {
proxy_pass http://52.78.9.158:8082;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# CORS 헤더 설정
add_header Access-Control-Allow-Origin "http://52.78.9.158:5173" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, access, refresh" always;
add_header Access-Control-Expose-Headers "Authorization, access, refresh" always;
add_header Access-Control-Allow-Credentials true always;
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "http://52.78.9.158:5173";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Authorization, Content-Type, access, refresh";
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Max-Age 3600;
return 204;
}
}
}
WebConfig
package com.booktory.booktoryserver.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("access", "refresh")
.allowCredentials(true)
.maxAge(3600);
}
}
security config
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://52.78.9.158:5173", "http://localhost:5173"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "access", "refresh"));
configuration.setExposedHeaders(Arrays.asList("Authorization", "access", "refresh"));
configuration.setMaxAge(3600L); // 1시간
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
Cors 관련만 3개를 설정했던 것이다...
이 문제를 해결하려면 Nginx와 Spring Boot 설정 중 하나에서 CORS 설정을 제거해야 합니다. 아래는 Nginx 설정과 Spring Boot 설정을 수정하는 방법입니다.
해결 방법
그럼 어디에 설정을 해야할지가 많은 고민에 빠지게 되었다. Spring Boot에서 관리하는 것이 더 간편할거 같아 코드를 수정해 보겠다.
WebConfig vs Spring Security ( Cors )
같은 점
- 둘 다 CORS 설정을 정의하여, 특정 도메인에서의 요청을 허용합니다.
- allowedOrigins, allowedMethods, allowedHeaders, exposedHeaders, allowCredentials, maxAge 등의 설정을 통해 CORS 정책을 설정합니다.
다른 점
- 적용 시점:
- SecurityConfig의 CORS 설정은 Spring Security 필터 체인에서 처리되며, 보안과 관련된 요청에 대해 적용됩니다.
- WebConfig의 CORS 설정은 Spring MVC에서 처리되며, 주로 컨트롤러 요청에 대해 적용됩니다.
기본적으로 두 설정이 동일한 역할을 하지만, 둘 중 하나만 사용하는 것이 좋습니다. Spring Security를 사용하는 경우, SecurityConfig에서 CORS 설정을 관리하는 것이 더 일반적입니다.
Spring Security 전체 코드
package com.booktory.booktoryserver.config;
import com.booktory.booktoryserver.Users.filter.CustomLogoutFilter;
import com.booktory.booktoryserver.Users.filter.JWTFilter;
import com.booktory.booktoryserver.Users.filter.JWTUtil;
import com.booktory.booktoryserver.Users.filter.LoginFilter;
import com.booktory.booktoryserver.Users.mapper.RefreshMapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.Arrays;
import java.util.Collections;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final AuthenticationConfiguration authenticationConfiguration;
private final JWTUtil jwtUtil;
private final RefreshMapper refreshMapper;
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.csrf((csrf) -> csrf.disable());
http
.cors((cors) -> cors.configurationSource(corsConfigurationSource()));
http
.sessionManagement((sessionManagement) -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/login", "/", "/api/users/auth/register", "/reissue").permitAll()
.requestMatchers("/test").hasRole("USER")
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated());
http
.formLogin((formLogin) -> formLogin.disable());
http
.httpBasic((httpBasic) -> httpBasic.disable());
http
.addFilterBefore(new CorsFilter(corsConfigurationSource()), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class)
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, refreshMapper), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new CustomLogoutFilter(jwtUtil, refreshMapper), LogoutFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://52.78.9.158:5173", "http://localhost:5173"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "access", "refresh"));
configuration.setExposedHeaders(Arrays.asList("Authorization", "access", "refresh"));
configuration.setMaxAge(3600L); // 1시간
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Nginx 설정
sudo vim /etc/nginx/sites-available/default
Nginx 설정 코드
server {
listen 80;
server_name 52.78.9.158;
# 프론트엔드 애플리케이션 서빙
location / {
proxy_pass http://52.78.9.158:5173; # Docker 컨테이너의 프론트엔드 서비스로 프록시
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
try_files $uri $uri/ /index.html; # Vue.js 라우팅을 위해 추가
}
error_page 404 /index.html;
# API 요청 프록시
location /api {
proxy_pass http://52.78.9.158:8082; # Docker 컨테이너의 백엔드 서비스로 프록시
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
이렇게 하면 설정이 중복되어 발생할 수 있는 충돌을 피할 수 있습니다.
두번째 이슈
문제 원인
프리플라이트 요청은 브라우저가 실제 요청을 보내기 전에 서버의 CORS 정책을 확인하기 위해 보내는 OPTIONS 요청입니다. 이 요청에 대한 응답에 Access-Control-Allow-Origin 헤더가 포함되어 있지 않으면 브라우저가 CORS 오류를 발생시킵니다.
해결 방법
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
위에 코드를 추가 ( 지금 하는 작업은 Nginx가 아닌 Spring Security에서 작업 첫번째 이슈에서 해결방법 코드가 알맞는 코드다.)
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://52.78.9.158:5173", "http://localhost:5173"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "access", "refresh"));
configuration.setExposedHeaders(Arrays.asList("Authorization", "access", "refresh"));
configuration.setMaxAge(3600L); // 1시간
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
http
.addFilterBefore(new CorsFilter(corsConfigurationSource()), UsernamePasswordAuthenticationFilter.class)
추가적으로 이 부분을 놓치는 분들이 많다.
configuration.setAllowedOrigins(Arrays.asList("http://52.78.9.158:5173", "http://localhost:5173"));
configuration.setAllowedOrigins(): 이 메서드는 CORS 설정에서 허용할 출처(origin)를 지정합니다. CORS는 웹 애플리케이션이 도메인 간 요청을 할 때 보안 정책을 관리하는 방법 중 하나로, 특정 출처에서의 요청만 허용하도록 설정할 수 있습니다. 이를 통해 개발 환경과 프로덕션 환경에서의 도메인 간 요청을 안전하게 처리할 수 있습니다.
설정이 끝났으면 이제 배포해 보자!!!
Vue.js & Docker 서버 배포
docker pull chan1117/book-tory-front
docker pull: 이 명령어는 Docker CLI(Command Line Interface)의 일부로, 지정된 Docker 이미지를 Docker 이미지 레지스트리에서 다운로드하여 로컬 시스템에 저장합니다.
백그라운드에서 Docker 컨테이너를 실행
docker run -d -p 5173:80 chan1117/book-tory-front
chan1117/book-tory-front 이미지를 기반으로 컨테이너를 실행하고, 로컬 머신의 포트 5173을 컨테이너의 포트 80에 매핑
- docker run: 새로운 Docker 컨테이너를 생성하고 실행합니다.
- -d: 컨테이너를 데몬 모드(백그라운드)에서 실행합니다.
- -p 5173:80: 로컬 머신의 포트 5173을 컨테이너의 포트 80에 매핑합니다. 이를 통해 로컬 머신의 http://<EC2 IP>:8080을 통해 컨테이너에 접근할 수 있습니다.
'AWS' 카테고리의 다른 글
[AWS] IAM 정책 (0) | 2024.06.28 |
---|---|
[AWS] IAM - ID 및 액세스 관리 (0) | 2024.06.28 |
[Cloud] 클라우드 컴퓨팅이란? (1) | 2024.06.28 |
[Cloud] Spring Boot & Docker EC2 & Git Ation (CI/CD) - 2탄 (1) | 2024.06.15 |