Handling CORS in MSA Environments: An Infrastructure-Focused Guide for DevOps Engineers

Contents

TL;DR
A DevOps guide to addressing CORS problems in legacy domain-based communication at the infrastructure level. It covers resolving CORS issues through API Gateway and Istio configuration without changing application code.

What Is CORS? (The Basics DevOps Engineers Need)

What CORS Is and How It Works

CORS (Cross-Origin Resource Sharing) is a security policy applied when a web browser accesses resources from a different origin.

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

Under the Same-Origin Policy, browsers block requests to different origins by default.

bash
# 동일 출처 (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 (차단 - 포트 다름)

Two Types of CORS Requests

1. Simple Request

2. Preflight Request

CORS Response Headers (What to Configure in Infrastructure)

http
# 서버/게이트웨이에서 설정해야 할 응답 헤더
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 캐시 시간

The Problem: CORS Hell in Legacy Infrastructure

Typical Legacy Architecture Problems

Many companies use an architecture with separate domains like the following.

Code
레거시 인프라 (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)

Practical Problems for DevOps Engineers

1. Complex CORS configuration management

nginx
# 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. Reconfiguring CORS everywhere whenever a new service is added

3. Inconsistent configuration across environments

bash
# 개발환경
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,..."

The Solution: A Single Domain + API Gateway Pattern

Modern Infrastructure Architecture (TO-BE)

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

Key Benefits from an Infrastructure Perspective

Category AS-IS (Domain-Based) TO-BE (Path-Based)
ALB count 5 ALBs 1 ALB
CORS configuration points 20 (5×4 environments) 4 (1×4 environments)
SSL certificates Certificates for 5 domains 1 wildcard
DNS management Manage 5 domains Manage 1 domain
Failure points 5 distributed points 1 centralized point

Implementing Kong Gateway (Kubernetes Environment)

Kong Ingress Configuration

yaml
# 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

Implementing AWS ALB + API Gateway

ALB Target Group Configuration

yaml
# 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 Configuration

yaml
# 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

Using Istio Service Mesh

If you use Istio in an MSA environment, it provides the most fine-grained CORS control.

1. Istio Gateway Configuration (Unified Entry Point)

yaml
# 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 Routing Configuration

yaml
# 단일 도메인에서 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. Global CORS Control with EnvoyFilter

yaml
# 모든 서비스에 적용되는 글로벌 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. Traffic Policy Configuration with DestinationRule

yaml
# 서비스별 트래픽 정책
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. External Service Access Configuration

yaml
# 외부 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

Conclusion

The approaches covered in this guide let you resolve CORS issues at the infrastructure level without changing application code.

Key Points

Diagram

100%

Drag to pan; pinch or use the wheel to zoom. Keyboard: arrow keys pan, + − zoom, 0 fits, Esc closes. The same content is in “Read the diagram as text” below the figure.