본문 바로가기

AWS

[Cloud] Vue.js & Nginx & Dockerfile ( + Cors 해결 ) - 1탄

 

 

 

프로젝트를 진행하면서 여러 가지 어려운 문제들을 겪었습니다. 특히, 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