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.
# 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.
# 동일 출처 (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
- GET, HEAD, POST methods
- Uses only basic headers (Content-Type: application/json, etc.)
- The browser sends the request directly
2. Preflight Request
- Uses PUT, DELETE, or custom headers
- The browser first checks permission with an OPTIONS request
- DevOps note: Handling the OPTIONS method is essential!
CORS Response Headers (What to Configure in Infrastructure)
# 서버/게이트웨이에서 설정해야 할 응답 헤더
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.
레거시 인프라 (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/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
- 5 ALBs × 4 environments = 20 configuration changes
- Configuration mistakes cause service outages
- Rollbacks are complicated
3. Inconsistent configuration across environments
# 개발환경
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)
현대적 인프라 (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
# 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
# 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
# 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)
# 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
# 단일 도메인에서 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
# 모든 서비스에 적용되는 글로벌 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
# 서비스별 트래픽 정책
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
# 외부 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
- Address CORS at its root with a single domain + API Gateway pattern
- Implementation examples for Kong Gateway, AWS ALB, and Istio
- Significantly reduced infrastructure complexity: 5 ALBs → 1 Gateway
- Improved DevOps operational efficiency: Centralized management