MSA 환경에서 CORS 문제 완전 정복: DevOps 엔지니어를 위한 인프라 중심 가이드

dongdorrong·2025년 6월 28일
0

DevOps

목록 보기
6/7

TL;DR
레거시 도메인 기반 통신에서 발생하는 CORS 문제를 인프라 레벨에서 근본적으로 해결하는 DevOps 가이드입니다. API Gateway와 Istio 설정을 통해 애플리케이션 코드 수정 없이 CORS 문제를 해결하는 방법을 다룹니다.

CORS란 무엇인가? (DevOps가 알아야 할 기본 개념)

CORS의 정의와 동작 원리

CORS(Cross-Origin Resource Sharing)는 웹 브라우저가 다른 출처(origin)의 리소스에 접근할 때 적용되는 보안 정책입니다.

# Origin의 구성 요소
# https://app.company.com:443/dashboard
# └─────┘ └──────────────┘└┘└─────────┘
#  scheme      host      port  path
#
# Origin = scheme + host + port
# "https://app.company.com:443" ← 이것이 Origin

Same-Origin Policy(동일 출처 정책)에 의해 브라우저는 기본적으로 다른 출처로의 요청을 차단합니다:

# 동일 출처 (OK)
https://app.company.com → https://app.company.com/api (허용)

# 다른 출처 (CORS 필요)
https://app.company.com → https://api.company.com (차단)
https://app.company.com → http://app.company.com (차단 - 스키마 다름)
https://app.company.com → https://app.company.com:8080 (차단 - 포트 다름)

CORS 요청의 두 가지 유형

1. Simple Request (단순 요청)

  • GET, HEAD, POST 메서드
  • 기본 헤더만 사용 (Content-Type: application/json 등)
  • 브라우저가 바로 요청 전송

2. Preflight Request (사전 요청)

  • PUT, DELETE, 커스텀 헤더 사용
  • 브라우저가 먼저 OPTIONS 요청으로 허가 확인
  • DevOps 주의사항: OPTIONS 메서드 처리 필수!

CORS 응답 헤더 (인프라에서 설정해야 할 항목들)

# 서버/게이트웨이에서 설정해야 할 응답 헤더
Access-Control-Allow-Origin: https://app.company.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 3600  # Preflight 캐시 시간

문제 상황: 레거시 인프라의 CORS 지옥

전형적인 레거시 아키텍처 문제

많은 기업들이 다음과 같은 도메인 기반 분리 구조를 가지고 있습니다:

레거시 인프라 (AS-IS)
├── 사용자 앱: app1.company.com (ALB-1)
├── 관리자 앱: admin.company.com (ALB-2)
├── 대시보드: dashboard.company.com (ALB-3)
├── 쇼핑몰: shop.company.com (ALB-4)
└── API 서버: api.company.com (ALB-5)

DevOps가 겪는 실제 문제들

1. 복잡한 CORS 설정 관리

# nginx/ALB에서 각각 설정해야 함
location /api {
    add_header Access-Control-Allow-Origin 
        "https://app1.company.com https://admin.company.com https://dashboard.company.com";
    add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
    add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With";
    
    # OPTIONS 요청 처리 (필수!)
    if ($request_method = 'OPTIONS') {
        add_header Access-Control-Max-Age 86400;
        return 204;
    }
    proxy_pass http://backend;
}

2. 새로운 서비스 추가 시마다 모든 곳에 CORS 재설정

  • 5개 ALB × 4개 환경 = 20군데 설정 변경
  • 설정 실수 시 서비스 장애 발생
  • 롤백이 복잡함

3. 환경별 설정 불일치

# 개발환경
CORS_ORIGIN="http://localhost:3000,http://dev.company.com"

# 스테이징환경  
CORS_ORIGIN="https://staging.company.com"

# 운영환경
CORS_ORIGIN="https://app1.company.com,https://admin.company.com,..."

해결책: 단일 도메인 + API Gateway 패턴

현대적 인프라 아키텍처 (TO-BE)

현대적 인프라 (TO-BE)
                                    
┌─────────────────────────────────────┐
│     gateway.company.com (단일 ALB)  │
├─────────────────────────────────────┤
│  /user/*     → User App             │
│  /admin/*    → Admin Panel          │  
│  /dashboard/* → Dashboard           │
│  /shop/*     → Shopping Mall        │
│  /api/*      → API Server           │
└─────────────────────────────────────┘
         ↓ 단일 도메인이므로
      CORS 문제 완전 해결!

인프라 관점에서의 핵심 이점

구분AS-IS (도메인 기반)TO-BE (Path 기반)
ALB 개수5개 ALB1개 ALB
CORS 설정 지점20곳 (5×4환경)4곳 (1×4환경)
SSL 인증서5개 도메인 인증서1개 와일드카드
DNS 관리5개 도메인 관리1개 도메인 관리
장애점분산된 5개 지점중앙집중 1개 지점

Kong Gateway 구현 (Kubernetes 환경)

Kong Ingress 설정

# Kong Gateway 라우팅 설정
apiVersion: configuration.konghq.com/v1
kind: KongIngress
metadata:
  name: single-domain-routing
  namespace: default
routing:
  # 사용자 앱 라우팅
  - name: user-app
    paths: ["/user"]
    service: user-service
    strip_path: true
    
  # 관리자 앱 라우팅  
  - name: admin-app
    paths: ["/admin"]
    service: admin-service
    strip_path: true
    
  # API 라우팅
  - name: api-service
    paths: ["/api"]
    service: backend-api
    strip_path: true

---
# 글로벌 CORS 플러그인 (한 번만 설정!)
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: global-cors
  namespace: default
config:
  # 같은 도메인이므로 CORS 문제 없음
  # 외부 파트너 API만 별도 설정
  origins: 
    - "https://partner.external.com"  # 외부 파트너만 명시
  methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
  headers: ["Accept", "Authorization", "Content-Type", "X-Requested-With"]
  credentials: false
  max_age: 3600

---
# Ingress 설정
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: gateway-ingress
  annotations:
    kubernetes.io/ingress.class: kong
    konghq.com/plugins: global-cors
spec:
  tls:
  - hosts:
    - gateway.company.com
    secretName: gateway-tls-secret
  rules:
  - host: gateway.company.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: kong-proxy
            port:
              number: 80

AWS ALB + API Gateway 구현

ALB 타겟 그룹 설정

# ALB 설정 (Terraform)
resource "aws_lb" "gateway" {
  name               = "gateway-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets           = var.public_subnet_ids

  tags = {
    Name = "gateway-company-com"
  }
}

# 단일 리스너로 모든 트래픽 처리
resource "aws_lb_listener" "gateway" {
  load_balancer_arn = aws_lb.gateway.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS-1-2-2017-01"
  certificate_arn   = aws_acm_certificate.gateway.arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.api_gateway.arn
  }
}

# API Gateway 타겟 그룹
resource "aws_lb_target_group" "api_gateway" {
  name     = "api-gateway-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = var.vpc_id

  health_check {
    enabled             = true
    healthy_threshold   = 2
    interval            = 30
    matcher             = "200"
    path                = "/health"
    port                = "traffic-port"
    protocol            = "HTTP"
    timeout             = 5
    unhealthy_threshold = 2
  }
}

API Gateway CORS 설정

# AWS API Gateway (CloudFormation/CDK)
Resources:
  ApiGateway:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: SingleDomainGateway
      EndpointConfiguration:
        Types:
          - REGIONAL
      
  # OPTIONS 메서드 (CORS Preflight 처리)
  OptionsMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref ApiGateway
      ResourceId: !GetAtt ApiGateway.RootResourceId
      HttpMethod: OPTIONS
      AuthorizationType: NONE
      Integration:
        Type: MOCK
        IntegrationResponses:
          - StatusCode: 200
            ResponseParameters:
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
              method.response.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS'"
              method.response.header.Access-Control-Allow-Origin: "'*'"
              method.response.header.Access-Control-Max-Age: "'3600'"
        RequestTemplates:
          application/json: '{"statusCode": 200}'
      MethodResponses:
        - StatusCode: 200
          ResponseParameters:
            method.response.header.Access-Control-Allow-Headers: false
            method.response.header.Access-Control-Allow-Methods: false
            method.response.header.Access-Control-Allow-Origin: false
            method.response.header.Access-Control-Max-Age: false

Istio Service Mesh 완전 정복

MSA 환경에서 Istio를 사용한다면 가장 강력하고 세밀한 CORS 제어가 가능합니다.

1. Istio Gateway 설정 (진입점 통합)

# Istio Ingress Gateway - 단일 진입점
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  name: company-gateway
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - gateway.company.com
    # HTTP → HTTPS 리디렉션
    tls:
      httpsRedirect: true
  - port:
      number: 443
      name: https
      protocol: HTTPS
    tls:
      mode: SIMPLE
      credentialName: gateway-tls-secret  # K8s Secret
    hosts:
    - gateway.company.com  # 단일 도메인!

2. VirtualService 라우팅 설정

# 단일 도메인에서 Path 기반 라우팅
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: unified-routing
  namespace: default
spec:
  hosts:
  - gateway.company.com
  gateways:
  - istio-system/company-gateway
  
  http:
  # 사용자 앱 라우팅
  - match:
    - uri:
        prefix: "/user"
    route:
    - destination:
        host: user-frontend-service
        port:
          number: 80
    # 같은 도메인이므로 CORS 설정 불필요!
    
  # 관리자 앱 라우팅
  - match:
    - uri:
        prefix: "/admin"
    route:
    - destination:
        host: admin-frontend-service
        port:
          number: 80
          
  # 대시보드 라우팅
  - match:
    - uri:
        prefix: "/dashboard"
    route:
    - destination:
        host: dashboard-service
        port:
          number: 80
          
  # API 라우팅 (내부 서비스간 통신)
  - match:
    - uri:
        prefix: "/api/v1/users"
    route:
    - destination:
        host: user-service
        port:
          number: 8080
          
  - match:
    - uri:
        prefix: "/api/v1/orders"
    route:
    - destination:
        host: order-service
        port:
          number: 8080
          
  # 외부 API 접근용 (CORS 필요한 경우만)
  - match:
    - uri:
        prefix: "/external-api"
    # 여기서만 CORS 정책 설정
    corsPolicy:
      allowOrigins:
      - prefix: "https://partner.company.com"
      - exact: "https://trusted-client.com"
      allowMethods:
      - GET
      - POST
      allowHeaders:
      - Authorization
      - Content-Type
      - X-Requested-With
      allowCredentials: true
      maxAge: "24h"  # Preflight 캐시 시간
    route:
    - destination:
        host: external-api-service
        port:
          number: 8080

3. EnvoyFilter로 글로벌 CORS 제어

# 모든 서비스에 적용되는 글로벌 CORS 정책
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: global-cors-filter
  namespace: istio-system  # 전역 적용
spec:
  configPatches:
  - applyTo: HTTP_FILTER
    match:
      context: SIDECAR_INBOUND
      listener:
        filterChain:
          filter:
            name: "envoy.filters.network.http_connection_manager"
    patch:
      operation: INSERT_BEFORE
      value:
        name: envoy.filters.http.cors
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
          # 전역 CORS 정책
          allow_origin_string_match:
          - safe_regex:
              google_re2: {}
              regex: "https://gateway\\.company\\.com.*"  # 같은 도메인 허용
          - exact: "https://partner.trusted.com"  # 신뢰된 외부 도메인
          allow_methods: "GET, POST, PUT, DELETE, OPTIONS"
          allow_headers: "authorization, content-type, x-requested-with, x-user-id"
          max_age: "86400"  # 24시간 캐시
          allow_credentials: true
          # 동적 헤더 추가
          expose_headers: "x-request-id, x-trace-id"

4. DestinationRule로 트래픽 정책 설정

# 서비스별 트래픽 정책
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: service-policies
  namespace: default
spec:
  host: "*.default.svc.cluster.local"  # 모든 서비스에 적용
  trafficPolicy:
    # mTLS 활성화 (서비스간 보안 통신)
    tls:
      mode: ISTIO_MUTUAL
    # 연결 풀 설정
    connectionPool:
      tcp:
        maxConnections: 100
      http:
        http1MaxPendingRequests: 50
        maxRequestsPerConnection: 10
    # 로드밸런싱
    loadBalancer:
      simple: LEAST_CONN

5. 외부 서비스 접근 설정

# 외부 API 호출 시 ServiceEntry 설정
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
  name: external-partner-api
  namespace: default
spec:
  hosts:
  - partner-api.external.com
  ports:
  - number: 443
    name: https
    protocol: HTTPS
  location: MESH_EXTERNAL
  resolution: DNS

---
# 외부 서비스에 대한 가상 서비스
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: external-partner-routing
  namespace: default
spec:
  hosts:
  - partner-api.external.com
  http:
  - timeout: 30s
    retries:
      attempts: 3
      perTryTimeout: 10s
    route:
    - destination:
        host: partner-api.external.com

결론

이 가이드를 통해 애플리케이션 코드 수정 없이 인프라 레벨에서 CORS 문제를 완전히 해결할 수 있습니다.

핵심 포인트

  • 단일 도메인 + API Gateway 패턴으로 CORS 문제 근본 해결
  • Kong Gateway, AWS ALB, Istio 등 다양한 솔루션 제공
  • 인프라 복잡도 대폭 감소: 5개 ALB → 1개 Gateway
  • DevOps 운영 효율성 향상: 중앙집중 관리
profile
DevOps 엔지니어 / 열심히 해서 잘하자

0개의 댓글