AWS ECS 환경에서 도메인별 로드밸런서 통합하기 (Pulumi + TypeScript)
🎯 프로젝트 배경
기존에는 각 마이크로서비스마다 개별 ALB(Application Load Balancer)를 생성하여 운영하고 있었습니다. 예를 들어:
이러한 구조는 다음과 같은 문제가 있었습니다:
1. 비용 증가: ALB는 시간당 과금되므로, 앱이 늘어날수록 비용이 선형적으로 증가
2. 관리 복잡도: 각 ALB마다 별도의 보안 그룹, 리스너, 인증서 관리 필요
3. IP 리소스 낭비: 각 ALB마다 Public IP 할당
목표:
도메인별로 하나의 ALB를 공유하도록 통합
같은 도메인(laivdata.com, mathmore.ai, tripora.ai)을 사용하는 앱들끼리 ALB 공유
HOST 헤더 기반 라우팅으로 각 앱의 타겟 그룹으로 트래픽 분산
🏗️ 아키텍처 설계
Before (앱별 ALB)
laivdata.com 도메인:
├─ kms-auth-api-dev.laivdata.com → ALB #1 → Target Group #1
├─ kms-api-dev.laivdata.com → ALB #2 → Target Group #2
└─ kk-be-dev.laivdata.com → ALB #3 → Target Group #3
mathmore.ai 도메인:
└─ api-dev.mathmore.ai → ALB #4 → Target Group #4
tripora.ai 도메인:
├─ api-dev.tripora.ai → ALB #5 → Target Group #5
└─ tourvis-planwith-dev.laivdata.com → ALB #6 → Target Group #6
After (도메인별 ALB 통합)
laivdata.com 도메인:
└─ dev-gluon-laivdata-com-alb (공통 ALB)
├─ kms-auth-api-dev.laivdata.com → Target Group #1
├─ kms-api-dev.laivdata.com → Target Group #2
└─ kk-be-dev.laivdata.com → Target Group #3
mathmore.ai 도메인:
└─ dev-gluon-mathmore-ai-alb (공통 ALB)
└─ api-dev.mathmore.ai → Target Group #4
tripora.ai 도메인:
└─ dev-gluon-tripora-ai-alb (공통 ALB)
└─ api-dev.tripora.ai → Target Group #5
📝 구현 단계
1단계: 인터페이스 및 타입 정의
기존에는 단일 로드밸런서 리소스였지만, 이제는 도메인별 맵 구조로 변경했습니다.
infra/interface.ts
// 단일 로드밸런서 리소스
export interface ISingleLoadBalancerResource {
sharedAlb: LoadBalancer;
albSecurityGroup: SecurityGroup;
httpListener: Listener;
httpsListener: Listener;
}
// 도메인별 맵
export interface ILoadBalancerResource {
[domain: string]: ISingleLoadBalancerResource;
}
// 도메인 설정
export interface IDomainConfig {
hostedZoneId: string;
domain: string;
}
2단계: 도메인별 로드밸런서 생성
3단계: HOST 기반 라우팅 규칙 추가
각 앱 배포 시, 해당 도메인의 HTTPS 리스너에 HOST 헤더 기반 라우팅 규칙을 추가합니다.
4단계: Route53 수정
각 앱의 도메인이 해당 도메인의 ALB를 바라보도록 수정합니다.
app/default-app-fargate/
route53.ts
export function createRoute53(
appDeployInfo: IAppDeployInfo,
lbResource?: ILbResource
) {
if (!appDeployInfo.domainInfo) return {};
// 도메인에 해당하는 로드밸런서 가져오기
const baseDomain = appDeployInfo.domainInfo.baseDomain.replace("*.", "");
const lbResourceForDomain = appDeployInfo.infraResource.loadBalancerResource[baseDomain];
if (!lbResourceForDomain) {
console.error(`Load balancer not found for domain: ${baseDomain}`);
return {};
}
const sharedAlb = lbResourceForDomain.sharedAlb;
// Route 53 A 레코드 생성
new aws.route53.Record(
`${ENV}-${appDeployInfo.infraName}-${appDeployInfo.appName}-record`,
{
zoneId: appDeployInfo.domainInfo.hostedZoneId,
name: `${appDeployInfo.domainInfo.domain}${
ENV === "prod" ? "" : `-${ENV}`
}.${baseDomain}`,
type: "A",
aliases: [{
name: sharedAlb.dnsName,
zoneId: sharedAlb.zoneId,
evaluateTargetHealth: true,
}],
}
);
return {};
}
5단계: Security Group 수정
ECS 보안 그룹이 해당 도메인의 ALB 보안 그룹만 허용하도록 수정합니다.
🚀 배포 및 마이그레이션
타입 에러 해결
문제: HTTPS 리스너 생성 시 인증서가 필수
error: A certificate must be specified for HTTPS listeners
해결: ACM에서 도메인별 인증서를 가져와서 설정
const certificateArn = pulumi.output(
aws.acm.getCertificate({
domain: domainConfig.domain,
mostRecent: true,
})
).arn;
타겟 그룹 충돌 해결
문제: 기존 ALB에 연결된 타겟 그룹을 새 ALB에 연결 시도
error: TargetGroupAssociationLimit: The following target groups cannot be
associated with more than one load balancer
해결 방법:
AWS 콘솔에서 기존 앱별 ALB 수동 삭제
또는 Pulumi state에서 제거:
pulumi state delete 'aws:lb/loadBalancer:LoadBalancer::dev-gluon-kms-auth-api-alb' --yes
Pulumi refresh 후 재배포:
pulumi refresh --yes
pulumi up
📊 결과 및 효과
비용 절감
Before: 8개 앱 = 8개 ALB
After: 3개 도메인 = 3개 ALB
절감률: ~62.5% (8개 → 3개)
관리 간소화
✅ 도메인별로 하나의 ALB만 관리
✅ 보안 그룹, 인증서 관리 포인트 감소
✅ HOST 헤더 기반으로 자동 라우팅
확장성
✅ 새로운 앱 추가 시 리스너 룰만 추가하면 됨
✅ 동일 도메인의 앱은 추가 비용 없음
💡 핵심 포인트
도메인별 그룹핑: 같은 도메인을 사용하는 앱들을 하나의 ALB로 묶음
HOST 헤더 라우팅: ALB 리스너 룰의 HOST 조건으로 각 앱 분기
Pulumi Output: 비동기 리소스는 pulumi.output() 사용
마이그레이션 전략: 기존 리소스 정리 후 새 구조로 전환
🔗 참고 자료