TL;DR
레거시 도메인 기반 통신에서 발생하는 CORS 문제를 인프라 레벨에서 근본적으로 해결하는 DevOps 가이드입니다. API Gateway와 Istio 설정을 통해 애플리케이션 코드 수정 없이 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 (차단 - 포트 다름)
1. Simple Request (단순 요청)
2. Preflight Request (사전 요청)
# 서버/게이트웨이에서 설정해야 할 응답 헤더
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 캐시 시간
많은 기업들이 다음과 같은 도메인 기반 분리 구조를 가지고 있습니다:
레거시 인프라 (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)
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 재설정
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,..."
현대적 인프라 (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개 ALB | 1개 ALB |
CORS 설정 지점 | 20곳 (5×4환경) | 4곳 (1×4환경) |
SSL 인증서 | 5개 도메인 인증서 | 1개 와일드카드 |
DNS 관리 | 5개 도메인 관리 | 1개 도메인 관리 |
장애점 | 분산된 5개 지점 | 중앙집중 1개 지점 |
# 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
# 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
}
}
# 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
MSA 환경에서 Istio를 사용한다면 가장 강력하고 세밀한 CORS 제어가 가능합니다.
# 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 # 단일 도메인!
# 단일 도메인에서 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
# 모든 서비스에 적용되는 글로벌 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"
# 서비스별 트래픽 정책
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
# 외부 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 문제를 완전히 해결할 수 있습니다.