본문 바로가기
DevOps/AWS

[DevOps] CICD 추가 과정 1-2. Swagger에서 HTTPS 요청(springdoc) & HTTP로 요청시 HTTPS로 리디렉션

by persi0815 2024. 7. 17.

상황


서비스의 api 도메인이 https 프로토콜을 사용하는데, swagger에서 이를 테스트할 수 있었으면 한다. 또한 추가로, http, local까지 이렇게 세 환경 모두를 테스트 가능하도록 하고자 한다. (사실 뒤에 가면 http 안쓰게 되긴 했다)
 

기존에 swagger를 https로 접속을 해도, http 서버로만 요청이 가능했다.

→  왜? 기본적으로 swagger는 http로 요청을 보내어, https로 요청을 보내기 위해서는 추가적인 작업이 필요하다. 

 

그리고, https로 접속한 상태에서 http로 요청을 하니, cors에러가 떴다.

→ 왜? 실서버 도메인과 요청하는 도메인이 다른 프로토콜을 쓰기 때문이다.
→ 즉, Origin이 달라서 cors에러가 발생하는 것이다.

CORS 에러에 대해 더 자세히 알고 싶다면? https://persi0815.tistory.com/30

[Spring/Java] CORS란? 해결법은?

CORS란? Cross-Origin Resource Sharing = 교차 출처 리소스 공유 정책 자세히는 추후 말하겠다. 출처(Origin)란? URL의 Protocol + Host + Port를 의미하는데, 간단히 말해선 웹 페이지나 리소스의 출처를 나타내는

persi0815.tistory.com

 

해결


추가적인 작업을 해주어 https로 요청을 보낼 수 있도록 하여, 실서버 도메인과 요청하는 도메인이 모두 같은 https 프로토콜을 사용하도록 하면 된다. 그러면 위와 같은 문제가 사라진다. 
 

방법 1) SwaggerConfig 파일에 @OpenAPIDefinition 추가

https://stackoverflow.com/questions/70843940/springdoc-openapi-ui-how-do-i-set-the-request-to-https

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.servers.Server;

@OpenAPIDefinition(
        servers = {
                @Server(url = "https://kopis.sangsin.site", description = "kopis https 서버입니다."),
                @Server(url = "http://kopis.sangsin.site", description = "kopis http 서버입니다."),
                @Server(url = "http://localhost:8080", description = "kopis local 서버입니다.")
        }
)
@Configuration
public class SwaggerConfig {

    @Bean
    public OpenAPI api() {
        Info info = new Info()
                .version("v1.0.0")
                .title("Koala's KOPIS API")
                .description("Koala's KOPIS API 명세서");
                //...
}

방법 2) SwaggerConfig 파일에 OpenAPI url 직접 설정

@Bean
public OpenAPI api() {
    Info info = new Info()
            .version("v1.0.0")
            .title("Koala's KOPIS API")
            .description("Koala's KOPIS API 명세서");

    // SecurityScheme명
    String jwtSchemeName = "AccessToken";
    // API 요청헤더에 인증정보 포함
    SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwtSchemeName);
    // SecuritySchemes 등록
    Components components = new Components()
            .addSecuritySchemes(jwtSchemeName, new SecurityScheme()
                    .name(jwtSchemeName)
                    .type(SecurityScheme.Type.HTTP) // HTTP 방식
                    .scheme("bearer")
                    .bearerFormat("JWT")); // 토큰 형식을 지정하는 임의의 문자(Optional)

    // 서버 URL 설정
    Server httpsServer = new Server();
    httpsServer.setUrl("https://kopis.sangsin.site");
    httpsServer.setDescription("kopis https 서버입니다.");

    Server httpServer = new Server();
    httpServer.setUrl("http://kopis.sangsin.site");
    httpServer.setDescription("kopis http 서버입니다.");
    
    Server localServer = new Server();
    localServer.setUrl("http://localhost:8080");
    localServer.setDescription("kopis local 서버입니다.");

    return new OpenAPI()
            .info(info)
            .addSecurityItem(securityRequirement)
            .components(components)
            .servers(List.of(httpsServer, httpServer, localServer));
}

 

결과물


 

그리고 삽질한 nginx.conf .......

결론부터 말하자면, 아래와 같이 AWS의 ALB/NLB에서 HTTPS 요청을 처리하고, Nginx는 HTTP 요청만 처리하면 된다. 
그런데.. 나는 ACM을 사용해서 HTTPS 설정을 해줬는데도 불구하고, 바보같이 Nginx에서도 HTTPS 요청을 처리해줘야하는 줄 알았다. 
 
나는 ACM을 사용해서 SSL 인증서를 제공받았는데, Nginx에서 HTTPS 요청을 처리해주려다 보니, 인증서와 키를 아래 사진과 같이 배포된 환경의 특정 디렉토리에 넣어줘야 했다. (그래서 실제로 AWS CLI를 통해 인증서와 키를 추출해서 디렉토리에 넣는 방식을 시도해보았다ㅠ.ㅠ) 그런데, CICD 환경에서는 인스턴스가 계속 바뀌기에 매번 새로운 인스턴스에 인증서와 키 파일을 배포해야 하는 문제가 발생했고.. 굉장히 비효율적인 방법이라는 생각이 들어서 다른 방법을 모색해보았다. 

 
좀 더 알아보니.. AWS ACM은 SSL/TLS 인증서를 제공하고, 이를 ALB/NLB(Application Load Balancer/Network Load Balancer)와 통합하여 HTTPS 트래픽을 처리할 수 있다는 걸 알게되었다. 그래서 ACM을 통해 SSL 인증서를 관리하고 ALB/NLB를 사용하여 SSL 종료를 처리한다면, SSL 인증서를 Nginx 설정에 포함할 필요가 없으며, 인증서 관리도 자동으로 이루어져 편리하겠다는 생각이 들었고, 곧바로 코드를 수정했다.

HTTP 요청만 처리하는 nginx.conf

user                    nginx;
error_log               /var/log/nginx/error.log warn;
pid                     /var/run/nginx.pid;
worker_processes        auto;
worker_rlimit_nofile    33282;

events {
    use epoll;
    worker_connections  1024;
    multi_accept on;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    include       conf.d/*.conf;

    map $http_upgrade $connection_upgrade {
        default     "upgrade";
    }

    upstream springboot {
        server 127.0.0.1:8080;
        keepalive 1024;
    }

    # HTTP 서버 블록
    server {
        listen        80 default_server;
        listen        [::]:80 default_server;

        location / {
            proxy_pass          http://springboot;
            if ($request_method = 'OPTIONS') {
                # OPTIONS 요청에 대한 CORS 헤더 추가
                add_header 'Access-Control-Allow-Origin' '*';
                add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
                add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type';
                add_header 'Access-Control-Allow-Credentials' 'true';
                add_header 'Content-Length' '0';
                add_header 'Content-Type' 'text/plain charset=UTF-8';
                return 204;
            }

            # CORS 관련 헤더 추가
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type';
            add_header 'Access-Control-Allow-Credentials' 'true';
            proxy_http_version  1.1;
            proxy_set_header    Connection          $connection_upgrade;
            proxy_set_header    Upgrade             $http_upgrade;

            proxy_set_header    Host                $host;
            proxy_set_header    X-Real-IP           $remote_addr;
            proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
        }

        access_log    /var/log/nginx/access.log main;

        client_header_timeout 60;
        client_body_timeout   60;
        keepalive_timeout     60;
        gzip                  off;
        gzip_comp_level       4;

        # Include the Elastic Beanstalk generated locations
        include conf.d/elasticbeanstalk/healthd.conf;
    }
}

 

이렇게 발급받은 ACM을

이렇게 ELB 리스너에 넣어주어서 HTTPS 설정을 해줬었다. 

ALB(AWS Load Balancer)
Target Group

CICD 구축하며 ALB와 Target Group을 따로 설정을 해준적이 없어서 되어있는지 몰랐는데, 이렇게 Target Group을 통한 ALB가 자동으로 설정되어 있었다.. 예전에 EC2 배포하고 HTTPS 적용할때와 방법이 같아서 신기했다
 
EC2에 HTTPS 적용할때 참고했던 블로그: https://woojin.tistory.com/93, https://woojin.tistory.com/94
 
윗 블로그 다시 봤더니, CICD 환경에서 HTTP로 요청이 들어오면 HTTPS로 리디렉션 보내고 싶어져서..ㅋㅋㅋ 지금 새벽 4시인데도..! 주저하지 않고 load balancer의 설정에 손을 댔다.

edit을 하면 rule을 수정할 수도 있는데, 나는 타깃 그룹으로 라우팅 되어있는 설정에 https로 리다이렉트하는 리스너 룰을 추가해주었다. 

왼쪽 사진처럼 target group로 라우팅되어있던 설정에다가 오른쪽 설정을 아래 과정을 통해 추가해주었다. 

 
그랬더니..!! http로 요청해도 https로 응답한다!!!

리디렉션 검증.mp4
3.61MB

 

 

아직 8080포트만 손 봐서 80포트로 접속하면 여전히 http로 요청이 가길래 80포트도 위와 같이 리디렉션 설정해주었다!

 
역시나 성공!! ><
포트 80이든 8080이든 HTTP로 요청했을때 이제 바로 HTTPS로 redirection되는거 보면 진짜 너무 뿌듯하다. 
이 쉽고 재밌는걸 왜 그동안 못했을깡...ㅋㅋㅋ 앞으론 새로움에 주저하지 않고 뛰어들어야겠다 ㅋㅋㅋ

리디렉션 검증 포트 80.mp4
1.74MB

번외) springfox가 아닌 springdoc을 사용하는 이유


Springfox는 업데이트가 중단되었기 때문에 Spring Boot 3.x와의 호환성 문제가 발생할 수 있으며, 최신 기능 지원이 부족하다. 반면, Springdoc은 지속적인 업데이트가 이루어지고 있으며, Spring Boot 3.x와 호환된다. 또한, 최신 OpenAPI 3.0 표준을 지원하고, 설정 및 사용이 간편하여 API 문서화를 쉽게 할 수 있다.