AWS CDK로 실무에서 흔히 쓰는 ALB + Auto Scaling Group 구조를 띄웠다. 헷갈렸던 개념들을 정리한다.
만든 아키텍처
인터넷
↓
IGW (Internet Gateway)
↓
ALB (Public Subnet, AZ 2개)
↓
EC2 (Private Subnet, ASG가 관리)
- VPC, Public/Private 서브넷, NAT Gateway는 별도 NetworkStack에서 만든다
- ALB는 Public 서브넷에 둬서 인터넷에서 접근 가능
- EC2는 Private 서브넷에 두고 ALB로만 트래픽 받음 (보안)
- ASG가 인스턴스 생성/삭제 + 자동 확장 담당
CDK 코드 핵심
const alb = new elbv2.ApplicationLoadBalancer(this, "Alb", {
vpc,
internetFacing: true,
});
const asg = new autoscaling.AutoScalingGroup(this, "Asg", {
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.NANO),
machineImage: ec2.MachineImage.latestAmazonLinux2023({
cpuType: ec2.AmazonLinuxCpuType.ARM_64,
}),
minCapacity: 1,
maxCapacity: 2,
desiredCapacity: 1,
userData,
healthChecks: autoscaling.HealthChecks.withAdditionalChecks({
additionalTypes: [autoscaling.AdditionalHealthCheckType.ELB],
gracePeriod: cdk.Duration.seconds(120),
}),
});
asg.scaleOnCpuUtilization("CpuScaling", { targetUtilizationPercent: 20 });
const listener = alb.addListener("Listener", { port: 80 });
listener.addTargets("AsgTarget", { port: 80, targets: [asg] });
listener.addTargets([asg]) 한 줄이 Target Group 자동 생성, ASG 등록, 보안 그룹 규칙(ALB→EC2 80포트)까지 해준다.
이런 자동 와이어링 때문에 L2 construct를 쓴다.
부하 테스트 결과
stress 도구로 CPU 부하를 주었다.

06:15 stress 시작 → CPU 92%
06:19 Target Tracking 알람(High) ALARM 트리거
06:20 ASG가 새 인스턴스 launch 시작
06:23 새 인스턴스 healthy
부하 시작 ~ 트래픽 받기까지 약 5~7분 걸렸다. Target Tracking은 즉시 반응하지 않는다. 갑작스런 트래픽 폭주에 대응하려면 평소 capacity 여유를 둬야 한다.
Scale-in은 보수적이다. AlarmLow는 보통 15분 연속 임계값 미만이어야 발동, 그 후 Connection Draining 5분까지 합치면 부하 종료 후 약 15~20분 걸린다.
ALB ↔ ASG 관계
ALB ←참조→ Target Group ←등록→ ASG
ALB와 ASG는 직접 연결되지 않는다. 무조건 Target Group이 사이에 있다.

| 컴포넌트 | 역할 |
|---|---|
| ALB | 트래픽 받아서 라우팅 |
| Listener | ALB의 설정 — 어떤 포트 받을지 |
| Target Group | 인스턴스 명단 + 헬스체크 수행 |
| ASG | 인스턴스 launch/terminate, capacity 관리 |
헬스체크는 Target Group이 한다. 30초마다 등록된 인스턴스에 HTTP 요청 보내서 healthy/unhealthy 판정하고, ALB와 ASG가 각자의 용도로 결과를 가져다 쓴다.
- ALB: unhealthy 인스턴스로 트래픽 안 보냄
- ASG (
HealthCheckType=ELB일 때): unhealthy 인스턴스 종료 + 새거 launch
ELB 헬스체크 추가
헬스체크 기본값은 EC2 헬스체크인데, 이건 OS 살아있는지만 본다. httpd가 죽어도 OS만 살아있으면 healthy로 판정한다.
해결: HealthChecks.withAdditionalChecks로 ELB 헬스체크 추가하여 앱이 죽었는지도 판단한다.
healthChecks: autoscaling.HealthChecks.withAdditionalChecks({
additionalTypes: [autoscaling.AdditionalHealthCheckType.ELB],
gracePeriod: cdk.Duration.seconds(120),
}),
gracePeriod는 인스턴스 launch 후 헬스체크 무시 시간. userData가 httpd 설치하는 동안 false alarm을 방지한다.
리뷰노트
ALB + ASG 워크샵 리뷰 노트
2. 핵심 개념
ALB / Listener / Target Group / ASG의 관계
ALB ←참조→ Target Group ←등록→ ASG
- ALB와 ASG는 직접 연결되지 않는다. Target Group이 무조건 중간에.
- 헬스체크는 Target Group이 한다 (30초마다 HTTP 요청)
- ALB는 TG의 healthy 결과 보고 트래픽 분배
- ASG는 (ELB 헬스체크 옵션 켜면) TG의 unhealthy 결과 보고 인스턴스 교체
ASG는 트래픽 경로에 없다
트래픽 흐름: 브라우저 → IGW → ALB → EC2
ASG는 인스턴스를 만들고 지우는 백그라운드 관리자일 뿐, 트래픽이 통과하지 않음.
Listener / Target Group 위치
- Listener: ALB 안의 설정 (포트별 규칙). ALB와 종속관계.
- Target Group: 별개의 독립 리소스. ARN 따로, 콘솔 메뉴도 별도.
- 둘 다 "ELB 서비스" 우산 아래 있음
addTargets() 한 줄이 하는 일
listener.addTargets("AsgTarget", { port: 80, targets: [asg] });
자동 생성:
- Target Group (헬스체크 기본값 포함)
- Listener의 default action으로 TG 연결
- ASG의 TargetGroupARNs에 TG의 ARN 등록
- 보안 그룹 규칙 (ALB → EC2 80포트 인바운드)
3. 내가 헷갈렸던 부분 (중요)
❓ "라우팅 테이블" 함정
ALB와 EC2 사이에 뭐가 있냐는 질문에 두 번 "라우팅 테이블"이라고 답했음. 정답은 Target Group.
라우팅 테이블Target Group| 레이어 | L3 (IP/네트워크) | L7 (HTTP) |
| 역할 | "이 CIDR 패킷은 IGW로? NAT로?" | "이 HTTP 요청은 어느 EC2로?" |
이름에 둘 다 "target" 들어가서 헷갈리지만 완전 다른 개념.
❓ Listener는 hop인가 설정인가
처음엔 트래픽 흐름도에 Listener를 hop으로 그렸지만 그건 잘못. Listener/Target Group은 ALB 내부의 처리 단계이지, 별도 hop이 아님.
물리적 hop만 그리면: IGW → ALB → EC2
ALB 내부 처리: Listener 매칭 → Rule 매칭 → Target Group 조회 → EC2 선택
❓ ASG가 ELB를 직접 쳐다보나?
아니. ASG는 자기가 등록된 Target Group의 헬스 결과만 보면 됨. ALB 자체를 모름.
HealthCheck.elb()라는 옵션 이름이 헷갈리는 이유:
- 옛날에 Classic ELB만 있을 때 만들어진 이름
- 지금은 "ELB 패밀리(TG 포함) 헬스 결과 본다"라는 의미
- 실제로는 Target Group의 헬스체크 결과를 봄
❓ TG가 ELB의 일부인지 별개인지
둘 다 맞음.
- 리소스 레벨: TG ≠ ALB (별개 ARN, 독립 리소스)
- 서비스 레벨: TG ⊂ ELB (둘 다 ELB 서비스 컴포넌트)
콘솔에서도 "Elastic Load Balancing" 카테고리 아래에 "Load Balancers"와 "Target Groups"가 함께.
❓ IAM 권한 변경 후 인스턴스 새로 띄워야 한 이유
- 기존 인스턴스는 SSM 에이전트가 부팅 시점에 권한 없어서 등록 시도 실패
- 한 번 실패하면 backoff 길게 들어가서 한참 retry 안 함
- 권한 추가됐어도 에이전트가 모르고 있는 상태 → "권한은 있는데 등록은 안 됨"
- 해결: terminate-instance-in-auto-scaling-group --no-should-decrement-desired-capacity로 종료 → ASG가 새로 launch (이번엔 처음부터 권한 가짐)
❓ HealthCheckType 변경은 즉시 적용되는데 IAM은 아닌 이유
IAM Role 정책 변경ASG의 HealthCheckType 변경| 영향 위치 | 인스턴스 부팅 시점의 SSM 등록 | ASG 자체의 동작 |
| 기존 인스턴스 적용? | ❌ (에이전트가 backoff) | ✅ (즉시 반영) |
| 해결 | 인스턴스 새로 띄우기 | 그냥 cdk deploy로 충분 |
❓ Scale-out vs Scale-in 비대칭 시간
트리거 후 적용까지이유| Scale-out | 약 5~7분 | CloudWatch 1분 + AlarmHigh 3분 평가 + 인스턴스 부팅 2~3분 |
| Scale-in | 약 15~20분 | AlarmLow 15분 평가 (보수적) + Connection Draining 5분 |
의도된 비대칭 설계: 부하 들어올 땐 빠르게 대응(서비스 보호), 부하 빠질 땐 천천히 줄임(과민반응 방지).
❓ Target Tracking은 정확히 임계값에서 발동 안 함
target=20 설정해도:
- AlarmHigh: CPU > 24 정도 (target보다 위로 마진)
- AlarmLow: CPU < 15 정도 (target보다 아래로 마진)
- 안정 영역: 15~24 (아무 일도 안 일어남)
이걸 hysteresis(히스테리시스)라고 함. flapping(왔다갔다) 방지용.
❓ Target Tracking의 인스턴스 추가량
항상 1개씩 추가는 아님. 목표값에서 얼마나 벗어났냐에 따라 여러 개 한 번에 추가 가능. 단 maxCapacity 한도까지만.
❓ 알람 → ASG 직접 호출이 아님
알람 → Scaling Policy → ASG의 desired 변경 → launch/terminate
알람은 신호만 주고, 실제 동작은 정책이. 정책은 ASG의 desired를 바꾸기만 함. ASG가 그걸 보고 launch/terminate.
4. 코드 핵심 부분
두 스택 구조 (cross-stack reference)
// network-stack.ts
export class NetworkStack extends cdk.Stack {
public readonly vpc: ec2.Vpc; // ← 외부 노출
constructor(...) {
this.vpc = new ec2.Vpc(this, "TutorialVpc", {
maxAzs: 2,
subnetConfiguration: [
{ name: "Public", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24 },
{ name: "Private", subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, cidrMask: 24 },
{ name: "Isolated", subnetType: ec2.SubnetType.PRIVATE_ISOLATED, cidrMask: 24 },
],
});
}
}
// alb-asg-stack.ts — VPC를 props로 받음
interface AlbAsgStackProps extends cdk.StackProps {
vpc: ec2.Vpc;
}
// bin/cdk-vpc-workshop.ts — 두 스택 연결
const network = new NetworkStack(app, "NetworkStack", { env });
new AlbAsgStack(app, "AlbAsgStack", { env, vpc: network.vpc });
ALB + ASG + ELB 헬스체크
const alb = new elbv2.ApplicationLoadBalancer(this, "Alb", {
vpc,
internetFacing: true,
});
const asg = new autoscaling.AutoScalingGroup(this, "Asg", {
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.NANO),
machineImage: ec2.MachineImage.latestAmazonLinux2023({
cpuType: ec2.AmazonLinuxCpuType.ARM_64,
}),
minCapacity: 1,
maxCapacity: 2,
desiredCapacity: 1,
userData,
healthChecks: autoscaling.HealthChecks.withAdditionalChecks({
additionalTypes: [autoscaling.AdditionalHealthCheckType.ELB],
gracePeriod: cdk.Duration.seconds(120),
}),
});
asg.scaleOnCpuUtilization("CpuScaling", { targetUtilizationPercent: 20 });
asg.role.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"),
);
const listener = alb.addListener("Listener", { port: 80 });
listener.addTargets("AsgTarget", { port: 80, targets: [asg] });
⚠️ Deprecated 주의
// 옛날 (deprecated)
healthCheck: autoscaling.HealthCheck.elb({ grace: cdk.Duration.seconds(120) })
// 새 API
healthChecks: autoscaling.HealthChecks.withAdditionalChecks({
additionalTypes: [autoscaling.AdditionalHealthCheckType.ELB],
gracePeriod: cdk.Duration.seconds(120),
})
5. 운영 팁
NAT Gateway 비용
- 시간당 $0.045/개 + 데이터 전송료
- 2개면 시간당 ~$0.09, 24시간이면 ~$2.20
- 학습 끝나면 즉시 cdk destroy
- 비용 줄이려면 natGateways: 1 (HA는 떨어짐)
desiredCapacity 경고
- ASG에 desiredCapacity 박아두면 deploy 때마다 그 수치로 리셋됨
- 트래픽 늘어서 4대 됐다가 cdk deploy 다시 하면 다시 1대로
- 첫 배포 후엔 desiredCapacity 줄 제거하고 minCapacity만 둬야 정석
인스턴스 새로 띄우기
aws autoscaling terminate-instance-in-auto-scaling-group \
--instance-id i-xxx \
--no-should-decrement-desired-capacity
- --no-should-decrement-desired-capacity = "desired 줄이지 마" = ASG가 새로 launch
AWS CLI boolean 옵션
- --should-X true/false 안 됨
- --should-X (true) / --no-should-X (false) 형태로 사용
리전 주의
- 명령어에 --region ap-northeast-2 빠지면 default(us-east-1)로 갈 수 있음
- 또는 export AWS_DEFAULT_REGION=ap-northeast-2
AWS Profile
export AWS_PROFILE=qa
# 또는 매번 --profile qa
6. CDK 레이어 (참고)
L1L2L3L4 (비공식)| 이름 | CfnXxx | 일반 클래스 | XxxPattern | 자체 Construct |
| 자동 와이어링 | ❌ | ✅ | ✅✅ | ✅✅✅ |
| 사용 빈도 | 드물게 (escape hatch) | 가장 많이 | 자주 | 큰 조직 |
| 예 | ec2.CfnInstance | ec2.Instance | ApplicationLoadBalancedFargateService | 사내 표준 패턴 |
이번 워크샵은 L2 위주로 짰음 (ApplicationLoadBalancer, AutoScalingGroup 등). 다음 Fargate 워크샵에선 L3로 넘어감.
7. 명령어 모음 (자주 쓴 것)
export AWS_PROFILE=qa
# 합성 (배포 전 검증)
npx cdk synth AlbAsgStack
# 배포 (의존성 자동 처리)
npx cdk deploy AlbAsgStack
# 등록된 스택 목록
npx cdk list
# 정리
npx cdk destroy AlbAsgStack NetworkStack
# 또는 전부
npx cdk destroy --all
# 활성 스택 확인
aws cloudformation list-stacks \
--region ap-northeast-2 \
--stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE \
--query 'StackSummaries[].StackName'
# ALB DNS로 부하 테스트
curl http://<alb-dns>
for i in {1..10}; do curl -s http://<alb-dns>; echo; done
# SSM 접속 (private 인스턴스에)
aws ssm start-session --target i-xxxxx
# stress 도구로 CPU 부하
sudo dnf install -y stress
stress --cpu 2 --timeout 600
# CloudWatch CPU 메트릭 직접 조회
aws cloudwatch get-metric-statistics \
--namespace AWS/EC2 --metric-name CPUUtilization \
--dimensions Name=AutoScalingGroupName,Value=<asg-name> \
--start-time $(date -u -v-10M +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 60 --statistics Average \
--region ap-northeast-2
# Target Group 헬스 상태
aws elbv2 describe-target-health \
--target-group-arn <arn> \
--region ap-northeast-2
8. 다음 학습 단계
이번에 배운 것 위에 쌓을 수 있는 것:
- Fargate — EC2 대신 컨테이너로 같은 패턴
- CI/CD — git push → 자동 배포 파이프라인
- 3-tier — RDS, ElastiCache로 데이터 계층 추가
- CloudFront / WAF — 글로벌 캐싱 + 보안
- Custom Construct — 반복되는 패턴을 자체 L3로 추출
9. 빨리 다시 까먹지 않기 위해
가장 중요한 핵심 3가지:
- ALB ↔ ASG는 직접 연결 안 됨. TG가 무조건 중간에.
- 헬스체크 주체는 Target Group이고, ALB와 ASG는 그 결과를 각자의 용도로 활용.
- Scale-out 빠르게(서비스 보호), Scale-in 천천히(과민반응 방지) — 의도된 비대칭.
'Infrastructure' 카테고리의 다른 글
| 일반적인 도메인 관리 방식 (0) | 2026.07.21 |
|---|---|
| CLI와 CloudFormation으로 만들어본 기본 아키텍처를 CDK로 만들기 (0) | 2026.04.06 |
| CLI로 만든 VPC를 CloudFormation으로 다시 만들기 (3) — EC2 배포와 삭제 (0) | 2026.03.31 |
| CLI로 만든 VPC를 CloudFormation으로 다시 만들기 (2) — 전체 네트워크 (0) | 2026.03.27 |
| CLI로 만든 VPC를 CloudFormation으로 다시 만들기 (1) — 템플릿 기초 (0) | 2026.03.24 |