- Published on
AWS 동적 인프라 완전 가이드: Auto Scaling, Spot 인스턴스, IaC로 비용 90% 절감하는 실전 전략
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 1. 왜 동적 인프라인가? (정적 vs 동적)
- 2. EC2 Auto Scaling 완전 정복
- 3. Spot 인스턴스 마스터 클래스
- 4. 서버리스 동적 인프라
- 5. Terraform으로 동적 인프라 구축
- 6. AWS CDK로 동적 인프라 구축
- 7. 비용 최적화 실전 전략
- 8. 고가용성 아키텍처
- 9. 필요할 때 EC2 생성하는 패턴 3가지
- 10. 모니터링과 알람
- 11. 퀴즈
- 12. 참고 자료
1. 왜 동적 인프라인가? (정적 vs 동적)
고정 서버의 근본적 문제
많은 기업이 여전히 "피크 트래픽 기준"으로 서버를 프로비저닝합니다. 블랙프라이데이에 트래픽이 10배 증가한다면, 1년 내내 10배 규모의 서버를 운영하는 방식입니다. 이는 80% 이상의 시간 동안 비용을 낭비하는 것과 같습니다.
실제 사례를 살펴보면:
- 전자상거래 A사: 피크 시간(저녁 8-10시)에만 트래픽 5배 증가 → 나머지 22시간은 서버 유휴 상태
- 미디어 B사: 주말 트래픽이 평일의 3배 → 평일에는 서버 66% 유휴
- SaaS C사: 월말 정산 시 배치 처리 폭주 → 월 27일은 과잉 프로비저닝
이런 패턴에서 정적 인프라는 다음과 같은 문제를 야기합니다:
| 문제 | 영향 | 비용 낭비율 |
|---|---|---|
| 피크 기준 프로비저닝 | 평소 과잉 리소스 | 60-80% |
| 수동 스케일링 | 트래픽 급증 시 대응 지연 | 다운타임 발생 |
| 인프라 변경 어려움 | 새 기능 배포 지연 | 기회 비용 |
| 단일 장애점 | 서버 장애 시 서비스 중단 | 매출 손실 |
동적 인프라의 핵심 가치
동적 인프라란 워크로드에 따라 리소스가 자동으로 확장되고 축소되는 아키텍처를 말합니다.
핵심 3가지 원칙:
- 탄력성(Elasticity): 트래픽 증가 → 자동 확장, 트래픽 감소 → 자동 축소
- 비용 최적화(Cost Optimization): 사용한 만큼만 비용 지불
- 고가용성(High Availability): 장애 발생 시 자동 복구
AWS 비용 최적화의 핵심 3축
비용 최적화 = Right-sizing + Auto Scaling + Spot Instances
(적정 크기) (자동 확장) (할인 인스턴스)
- Right-sizing: 워크로드에 맞는 적절한 인스턴스 타입 선택 (m5.xlarge가 아닌 t3.medium으로 충분할 수도)
- Auto Scaling: 트래픽에 따라 인스턴스 수를 자동 조절
- Spot Instances: 온디맨드 대비 최대 90% 할인된 가격으로 여유 용량 활용
이 세 가지를 조합하면 월 클라우드 비용을 60-90%까지 절감할 수 있습니다.
2. EC2 Auto Scaling 완전 정복
ASG의 핵심 구성요소
Auto Scaling Group(ASG)은 세 가지 핵심 컴포넌트로 구성됩니다:
- Launch Template: 어떤 인스턴스를 생성할지 정의 (AMI, 인스턴스 유형, 보안 그룹, 키 페어)
- Scaling Policy: 언제, 어떻게 스케일링할지 규칙 정의
- Health Check: 인스턴스 상태를 모니터링하고 비정상 인스턴스 교체
Launch Template 작성
먼저 AWS CLI로 Launch Template을 생성하는 예제입니다:
{
"LaunchTemplateName": "web-server-template",
"LaunchTemplateData": {
"ImageId": "ami-0abcdef1234567890",
"InstanceType": "t3.medium",
"KeyName": "my-key-pair",
"SecurityGroupIds": ["sg-0123456789abcdef0"],
"UserData": "IyEvYmluL2Jhc2gKeXVtIHVwZGF0ZSAteQp5dW0gaW5zdGFsbCAteSBodHRwZA==",
"TagSpecifications": [
{
"ResourceType": "instance",
"Tags": [
{
"Key": "Environment",
"Value": "production"
},
{
"Key": "Project",
"Value": "web-app"
}
]
}
],
"BlockDeviceMappings": [
{
"DeviceName": "/dev/xvda",
"Ebs": {
"VolumeSize": 30,
"VolumeType": "gp3",
"Encrypted": true
}
}
]
}
}
Terraform으로 동일한 Launch Template을 정의하면:
resource "aws_launch_template" "web_server" {
name_prefix = "web-server-"
image_id = data.aws_ami.amazon_linux_2.id
instance_type = "t3.medium"
key_name = "my-key-pair"
vpc_security_group_ids = [aws_security_group.web.id]
user_data = base64encode(<<-EOF
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "Hello from $(hostname)" > /var/www/html/index.html
EOF
)
block_device_mappings {
device_name = "/dev/xvda"
ebs {
volume_size = 30
volume_type = "gp3"
encrypted = true
}
}
tag_specifications {
resource_type = "instance"
tags = {
Environment = "production"
Project = "web-app"
}
}
lifecycle {
create_before_destroy = true
}
}
Scaling Policy 4가지 전략
2-1. Simple Scaling (단순 스케일링)
CloudWatch 알람 하나에 하나의 조정 동작을 연결하는 가장 기본적인 방식입니다.
resource "aws_autoscaling_policy" "scale_up" {
name = "scale-up"
autoscaling_group_name = aws_autoscaling_group.web.name
adjustment_type = "ChangeInCapacity"
scaling_adjustment = 2
cooldown = 300
}
resource "aws_cloudwatch_metric_alarm" "high_cpu" {
alarm_name = "high-cpu-alarm"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = 120
statistic = "Average"
threshold = 80
dimensions = {
AutoScalingGroupName = aws_autoscaling_group.web.name
}
alarm_actions = [aws_autoscaling_policy.scale_up.arn]
}
한계점: 스케일링 동작 후 쿨다운 기간 동안 추가 스케일링이 불가하여 급격한 트래픽 증가에 대응이 느림.
2-2. Step Scaling (단계별 스케일링)
알람 범위에 따라 다른 크기의 조정을 수행합니다.
resource "aws_autoscaling_policy" "step_scaling" {
name = "step-scaling-policy"
autoscaling_group_name = aws_autoscaling_group.web.name
policy_type = "StepScaling"
adjustment_type = "ChangeInCapacity"
step_adjustment {
scaling_adjustment = 1
metric_interval_lower_bound = 0
metric_interval_upper_bound = 20
}
step_adjustment {
scaling_adjustment = 3
metric_interval_lower_bound = 20
metric_interval_upper_bound = 40
}
step_adjustment {
scaling_adjustment = 5
metric_interval_lower_bound = 40
}
}
이 정책은 CPU가 임계값을 초과하는 정도에 따라 1개, 3개, 5개의 인스턴스를 추가합니다.
2-3. Target Tracking Scaling (목표 추적 스케일링)
가장 권장되는 방식입니다. 특정 메트릭이 목표값을 유지하도록 ASG가 자동으로 인스턴스를 조절합니다.
resource "aws_autoscaling_policy" "target_tracking" {
name = "target-tracking-cpu"
autoscaling_group_name = aws_autoscaling_group.web.name
policy_type = "TargetTrackingScaling"
target_tracking_configuration {
predefined_metric_specification {
predefined_metric_type = "ASGAverageCPUUtilization"
}
target_value = 70.0
}
}
# ALB 요청 수 기반 Target Tracking
resource "aws_autoscaling_policy" "target_tracking_alb" {
name = "target-tracking-alb"
autoscaling_group_name = aws_autoscaling_group.web.name
policy_type = "TargetTrackingScaling"
target_tracking_configuration {
predefined_metric_specification {
predefined_metric_type = "ALBRequestCountPerTarget"
resource_label = "${aws_lb_target_group.web.arn_suffix}/${aws_lb.web.arn_suffix}"
}
target_value = 1000.0
}
}
Target Tracking이 권장되는 이유:
- 수동으로 알람과 정책을 관리할 필요 없음
- Scale-in과 Scale-out을 자동으로 균형 있게 조절
- 여러 Target Tracking 정책을 동시에 적용 가능
2-4. Predictive Scaling (예측 스케일링)
ML 모델이 과거 14일간의 트래픽 패턴을 분석하여 미래 트래픽을 예측하고, 사전에 인스턴스를 프로비저닝합니다.
resource "aws_autoscaling_policy" "predictive" {
name = "predictive-scaling"
autoscaling_group_name = aws_autoscaling_group.web.name
policy_type = "PredictiveScaling"
predictive_scaling_configuration {
metric_specification {
target_value = 70
predefined_scaling_metric_specification {
predefined_metric_type = "ASGAverageCPUUtilization"
resource_label = ""
}
predefined_load_metric_specification {
predefined_metric_type = "ASGTotalCPUUtilization"
resource_label = ""
}
}
mode = "ForecastAndScale"
scheduling_buffer_time = 300
max_capacity_breach_behavior = "HonorMaxCapacity"
}
}
Predictive Scaling 사용 시나리오:
- 매일 같은 시간대에 트래픽 급증 (출퇴근 시간, 점심시간)
- 주간/월간 반복 패턴이 명확한 서비스
- Target Tracking과 함께 사용하면 시너지 효과
Cooldown Period와 Warm-up 설정
resource "aws_autoscaling_group" "web" {
name = "web-asg"
desired_capacity = 2
max_size = 20
min_size = 1
vpc_zone_identifier = [aws_subnet.private_a.id, aws_subnet.private_c.id]
launch_template {
id = aws_launch_template.web_server.id
version = "$Latest"
}
# 기본 쿨다운: 스케일링 액션 후 대기 시간
default_cooldown = 300
# 인스턴스 워밍업: 새 인스턴스가 완전히 준비되기까지의 시간
default_instance_warmup = 120
health_check_type = "ELB"
health_check_grace_period = 300
tag {
key = "Name"
value = "web-server"
propagate_at_launch = true
}
}
쿨다운과 워밍업의 차이:
- Cooldown: 스케일링 동작 후 다음 스케일링까지 대기하는 시간 (과도한 스케일링 방지)
- Warm-up: 새로 시작된 인스턴스가 트래픽을 받을 준비가 될 때까지의 시간 (ASG 메트릭에서 제외)
Mixed Instances Policy (온디맨드 + 스팟 혼합)
비용을 극적으로 절감하면서도 안정성을 유지하는 핵심 전략입니다:
resource "aws_autoscaling_group" "web_mixed" {
name = "web-mixed-asg"
desired_capacity = 6
max_size = 30
min_size = 2
vpc_zone_identifier = [
aws_subnet.private_a.id,
aws_subnet.private_b.id,
aws_subnet.private_c.id
]
mixed_instances_policy {
launch_template {
launch_template_specification {
launch_template_id = aws_launch_template.web_server.id
version = "$Latest"
}
override {
instance_type = "t3.medium"
}
override {
instance_type = "t3a.medium"
}
override {
instance_type = "m5.large"
}
override {
instance_type = "m5a.large"
}
}
instances_distribution {
on_demand_base_capacity = 2
on_demand_percentage_above_base_capacity = 20
spot_allocation_strategy = "capacity-optimized"
spot_max_price = "" # 온디맨드 가격까지 허용
}
}
}
이 설정의 의미:
- 기본 2대: 항상 온디맨드로 유지 (안정성 보장)
- 추가 인스턴스의 80%: 스팟 인스턴스 (비용 절감)
- 추가 인스턴스의 20%: 온디맨드 (안정성 보완)
- 다중 인스턴스 타입: 특정 타입의 스팟 용량 부족 시 대체 타입 자동 선택
Lifecycle Hooks (인스턴스 수명주기 후크)
인스턴스 시작 또는 종료 시 커스텀 작업을 수행할 수 있습니다:
resource "aws_autoscaling_lifecycle_hook" "launch_hook" {
name = "launch-setup-hook"
autoscaling_group_name = aws_autoscaling_group.web.name
lifecycle_transition = "autoscaling:EC2_INSTANCE_LAUNCHING"
heartbeat_timeout = 600
default_result = "CONTINUE"
notification_target_arn = aws_sns_topic.asg_notifications.arn
role_arn = aws_iam_role.asg_hook_role.arn
}
resource "aws_autoscaling_lifecycle_hook" "terminate_hook" {
name = "terminate-cleanup-hook"
autoscaling_group_name = aws_autoscaling_group.web.name
lifecycle_transition = "autoscaling:EC2_INSTANCE_TERMINATING"
heartbeat_timeout = 300
default_result = "CONTINUE"
notification_target_arn = aws_sns_topic.asg_notifications.arn
role_arn = aws_iam_role.asg_hook_role.arn
}
Lifecycle Hook 활용 사례:
- Launch Hook: 인스턴스 시작 시 설정 관리 도구(Ansible, Chef)로 초기 구성 완료 확인
- Terminate Hook: 인스턴스 종료 전 로그 백업, 커넥션 드레이닝, 서비스 디스커버리 등록 해제
3. Spot 인스턴스 마스터 클래스
Spot 인스턴스란?
Spot 인스턴스는 AWS의 미사용 EC2 용량을 온디맨드 가격 대비 최대 90%까지 할인된 가격으로 제공하는 인스턴스입니다.
가격 비교 (m5.xlarge 기준, us-east-1):
| 구매 옵션 | 시간당 가격 | 월간 비용(730시간) | 할인율 |
|---|---|---|---|
| 온디맨드 | $0.192 | $140.16 | - |
| Reserved (1년, 전액선결제) | $0.120 | $87.60 | 37% |
| Savings Plan (1년) | $0.125 | $91.25 | 35% |
| Spot (평균) | $0.058 | $42.34 | 70% |
| Spot (최저) | $0.019 | $13.87 | 90% |
Spot 가격 히스토리 분석
AWS CLI로 스팟 가격 히스토리를 조회할 수 있습니다:
aws ec2 describe-spot-price-history \
--instance-types m5.xlarge m5a.xlarge m5d.xlarge \
--product-descriptions "Linux/UNIX" \
--start-time "2026-03-16T00:00:00" \
--end-time "2026-03-23T00:00:00" \
--query 'SpotPriceHistory[*].[InstanceType,AvailabilityZone,SpotPrice,Timestamp]' \
--output table
스팟 가격 안정성을 높이는 전략:
- 여러 인스턴스 타입 지정 (m5.xlarge, m5a.xlarge, m5d.xlarge, m5n.xlarge)
- 여러 가용 영역(AZ) 활용
- capacity-optimized 할당 전략 사용 (가장 여유 용량이 많은 풀에서 할당)
Spot Interruption 핸들링
스팟 인스턴스는 AWS가 해당 용량을 필요로 할 때 2분 경고와 함께 회수될 수 있습니다.
메타데이터 폴링으로 인터럽션 감지
#!/bin/bash
# spot-interruption-handler.sh
METADATA_TOKEN=$(curl -s -X PUT \
"http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
while true; do
INTERRUPTION=$(curl -s -H "X-aws-ec2-metadata-token: $METADATA_TOKEN" \
http://169.254.169.254/latest/meta-data/spot/instance-action 2>/dev/null)
if [ "$INTERRUPTION" != "" ] && echo "$INTERRUPTION" | grep -q "action"; then
echo "Spot interruption detected! Starting graceful shutdown..."
# 1. ALB에서 인스턴스 제거 (새 요청 수신 중지)
INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $METADATA_TOKEN" \
http://169.254.169.254/latest/meta-data/instance-id)
aws elbv2 deregister-targets \
--target-group-arn "$TARGET_GROUP_ARN" \
--targets "Id=$INSTANCE_ID"
# 2. 진행 중인 요청 완료 대기 (Connection Draining)
sleep 30
# 3. 로그 백업
aws s3 sync /var/log/app/ "s3://my-logs-bucket/spot-terminated/$INSTANCE_ID/"
# 4. 애플리케이션 정상 종료
systemctl stop my-app
echo "Graceful shutdown completed."
break
fi
sleep 5
done
Lambda 기반 Interruption Handler
EventBridge 규칙을 통해 스팟 인터럽션 이벤트를 Lambda로 처리합니다:
import json
import boto3
ec2 = boto3.client('ec2')
elbv2 = boto3.client('elbv2')
sns = boto3.client('sns')
asg = boto3.client('autoscaling')
def lambda_handler(event, context):
"""
EventBridge에서 Spot Interruption Warning 이벤트를 수신하여 처리
"""
detail = event.get('detail', {})
instance_id = detail.get('instance-id')
action = detail.get('instance-action')
print(f"Spot interruption: instance={instance_id}, action={action}")
# 1. ASG에서 인스턴스를 비정상으로 마킹 (대체 인스턴스 즉시 시작)
try:
asg.set_instance_health(
InstanceId=instance_id,
HealthStatus='Unhealthy',
ShouldRespectGracePeriod=False
)
print(f"Marked {instance_id} as unhealthy in ASG")
except Exception as e:
print(f"ASG health update failed: {e}")
# 2. SNS 알림 발송
sns.publish(
TopicArn='arn:aws:sns:ap-northeast-2:123456789012:spot-alerts',
Subject=f'Spot Interruption: {instance_id}',
Message=json.dumps({
'instance_id': instance_id,
'action': action,
'region': event.get('region'),
'time': event.get('time')
}, indent=2)
)
return {
'statusCode': 200,
'body': f'Handled interruption for {instance_id}'
}
EventBridge 규칙 설정 (Terraform):
resource "aws_cloudwatch_event_rule" "spot_interruption" {
name = "spot-interruption-rule"
description = "Capture EC2 Spot Instance Interruption Warning"
event_pattern = jsonencode({
source = ["aws.ec2"]
detail-type = ["EC2 Spot Instance Interruption Warning"]
})
}
resource "aws_cloudwatch_event_target" "spot_handler_lambda" {
rule = aws_cloudwatch_event_rule.spot_interruption.name
target_id = "spot-interruption-handler"
arn = aws_lambda_function.spot_handler.arn
}
Spot Fleet: 다중 인스턴스 타입 + 다중 AZ
resource "aws_spot_fleet_request" "batch_processing" {
iam_fleet_role = aws_iam_role.spot_fleet_role.arn
target_capacity = 10
terminate_instances_with_expiration = true
allocation_strategy = "capacityOptimized"
fleet_type = "maintain"
launch_template_config {
launch_template_specification {
id = aws_launch_template.batch.id
version = "$Latest"
}
overrides {
instance_type = "c5.xlarge"
availability_zone = "ap-northeast-2a"
}
overrides {
instance_type = "c5a.xlarge"
availability_zone = "ap-northeast-2a"
}
overrides {
instance_type = "c5.xlarge"
availability_zone = "ap-northeast-2c"
}
overrides {
instance_type = "c5a.xlarge"
availability_zone = "ap-northeast-2c"
}
overrides {
instance_type = "c6i.xlarge"
availability_zone = "ap-northeast-2a"
}
overrides {
instance_type = "c6i.xlarge"
availability_zone = "ap-northeast-2c"
}
}
}
Spot 사용 적합/부적합 워크로드
적합한 워크로드:
- CI/CD 파이프라인 (빌드, 테스트 러너)
- 배치 데이터 처리 (ETL, 로그 분석)
- ML 모델 학습 (체크포인트 지원)
- 부하 테스트 / 성능 테스트
- 빅데이터 처리 (EMR, Spark)
- 이미지/비디오 인코딩
- 웹 서버 (ASG Mixed Instances Policy 사용 시)
부적합한 워크로드:
- 단일 인스턴스 데이터베이스 (RDS는 Multi-AZ 사용)
- 실시간 결제 시스템 (중단 불가)
- 장시간 상태 유지가 필요한 워크로드
- SLA가 99.99% 이상 요구되는 핵심 서비스 (온디맨드 또는 Reserved 사용)
4. 서버리스 동적 인프라
Lambda: 이벤트 기반 자동 스케일링
AWS Lambda는 동적 인프라의 극단적 형태입니다. 요청이 없으면 비용 0원, 요청이 오면 밀리초 단위로 리소스 할당.
import json
import time
def lambda_handler(event, context):
"""
API Gateway에서 호출되는 Lambda 함수
동시 실행: 0 ~ 수천 개까지 자동 스케일링
"""
start = time.time()
# 비즈니스 로직
body = event.get('body', '{}')
data = json.loads(body) if body else {}
result = process_request(data)
duration = (time.time() - start) * 1000
print(f"Processing took {duration:.2f}ms")
return {
'statusCode': 200,
'headers': {
'Content-Type': 'application/json',
'X-Processing-Time': f'{duration:.2f}ms'
},
'body': json.dumps(result)
}
def process_request(data):
# 실제 비즈니스 로직
return {'status': 'success', 'data': data}
Lambda 동시성 관리
# 예약 동시성: 이 함수가 사용할 최대 동시 실행 수 확보
resource "aws_lambda_function_event_invoke_config" "example" {
function_name = aws_lambda_function.api.function_name
maximum_event_age_in_seconds = 60
maximum_retry_attempts = 0
}
# 프로비저닝 동시성: 미리 인스턴스를 따뜻하게 유지 (Cold Start 방지)
resource "aws_lambda_provisioned_concurrency_config" "api" {
function_name = aws_lambda_function.api.function_name
provisioned_concurrent_executions = 50
qualifier = aws_lambda_alias.live.name
}
동시성 유형 비교:
- Reserved Concurrency: 다른 함수가 이 용량을 사용하지 못하도록 확보. 비용 추가 없음
- Provisioned Concurrency: 미리 실행 환경을 따뜻하게 유지. Cold Start 제거. 비용 발생
Cold Start 최소화 전략
- 프로비저닝 동시성 사용 (가장 확실한 방법)
- 패키지 크기 최소화 (의존성 최적화, Layer 활용)
- 런타임 선택: Python/Node.js는 Java보다 Cold Start가 빠름
- 초기화 코드 최적화: 핸들러 외부에서 DB 연결 등 초기화
- SnapStart 활용 (Java 런타임 한정, Cold Start 90% 감소)
Fargate: 서버리스 컨테이너
resource "aws_ecs_service" "api" {
name = "api-service"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.api.arn
desired_count = 2
launch_type = "FARGATE"
network_configuration {
subnets = [aws_subnet.private_a.id, aws_subnet.private_c.id]
security_groups = [aws_security_group.ecs.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.api.arn
container_name = "api"
container_port = 8080
}
capacity_provider_strategy {
capacity_provider = "FARGATE"
weight = 1
base = 2 # 최소 2개는 Fargate 온디맨드
}
capacity_provider_strategy {
capacity_provider = "FARGATE_SPOT"
weight = 4 # 추가분의 80%는 Fargate Spot
}
}
# Fargate Auto Scaling
resource "aws_appautoscaling_target" "ecs" {
max_capacity = 20
min_capacity = 2
resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.api.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}
resource "aws_appautoscaling_policy" "ecs_cpu" {
name = "ecs-cpu-scaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.ecs.resource_id
scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
service_namespace = aws_appautoscaling_target.ecs.service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
target_value = 70.0
scale_in_cooldown = 300
scale_out_cooldown = 60
}
}
Lambda vs Fargate vs EC2 비교표
| 항목 | Lambda | Fargate | EC2 (ASG) |
|---|---|---|---|
| 스케일링 속도 | 밀리초 | 30초-2분 | 2-5분 |
| 최대 실행 시간 | 15분 | 무제한 | 무제한 |
| 메모리 | 128MB-10GB | 512MB-120GB | 인스턴스 타입 따라 |
| vCPU | 최대 6 | 최대 16 | 인스턴스 타입 따라 |
| 비용 모델 | 요청 수 + 실행 시간 | vCPU + 메모리 시간 | 인스턴스 시간 |
| Cold Start | 있음 | 있음 (더 긴편) | 없음 (이미 실행 중) |
| 관리 부담 | 최소 | 중간 | 높음 |
| 컨테이너 지원 | 이미지 배포 가능 | 네이티브 | Docker 직접 관리 |
| Spot 지원 | 해당 없음 | Fargate Spot (70%) | Spot Instance (90%) |
| 적합 워크로드 | 이벤트 처리, API | 마이크로서비스, 웹앱 | 고성능, 상태 유지 |
5. Terraform으로 동적 인프라 구축
전체 ASG + ALB 인프라 예제
# provider.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "web-app/terraform.tfstate"
region = "ap-northeast-2"
dynamodb_table = "terraform-lock"
encrypt = true
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
ManagedBy = "terraform"
Project = var.project_name
}
}
}
# variables.tf
variable "aws_region" {
default = "ap-northeast-2"
}
variable "environment" {
default = "production"
}
variable "project_name" {
default = "web-app"
}
variable "vpc_cidr" {
default = "10.0.0.0/16"
}
# vpc.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-vpc"
}
}
resource "aws_subnet" "public_a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "${var.aws_region}a"
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-public-a"
Tier = "public"
}
}
resource "aws_subnet" "public_c" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "${var.aws_region}c"
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-public-c"
Tier = "public"
}
}
resource "aws_subnet" "private_a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.10.0/24"
availability_zone = "${var.aws_region}a"
tags = {
Name = "${var.project_name}-private-a"
Tier = "private"
}
}
resource "aws_subnet" "private_c" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.11.0/24"
availability_zone = "${var.aws_region}c"
tags = {
Name = "${var.project_name}-private-c"
Tier = "private"
}
}
# alb.tf
resource "aws_lb" "web" {
name = "${var.project_name}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = [aws_subnet.public_a.id, aws_subnet.public_c.id]
enable_deletion_protection = true
access_logs {
bucket = aws_s3_bucket.alb_logs.bucket
prefix = "alb-logs"
enabled = true
}
}
resource "aws_lb_target_group" "web" {
name = "${var.project_name}-tg"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.main.id
health_check {
enabled = true
healthy_threshold = 2
interval = 15
matcher = "200"
path = "/health"
port = "traffic-port"
protocol = "HTTP"
timeout = 5
unhealthy_threshold = 3
}
deregistration_delay = 30
stickiness {
type = "lb_cookie"
enabled = false
}
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.web.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = aws_acm_certificate.main.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.web.arn
}
}
# asg.tf - 완전한 Mixed Instances ASG
resource "aws_autoscaling_group" "web" {
name = "${var.project_name}-asg"
desired_capacity = 4
max_size = 30
min_size = 2
vpc_zone_identifier = [aws_subnet.private_a.id, aws_subnet.private_c.id]
target_group_arns = [aws_lb_target_group.web.arn]
health_check_type = "ELB"
health_check_grace_period = 300
default_instance_warmup = 120
mixed_instances_policy {
launch_template {
launch_template_specification {
launch_template_id = aws_launch_template.web_server.id
version = "$Latest"
}
override {
instance_type = "t3.medium"
weighted_capacity = "1"
}
override {
instance_type = "t3a.medium"
weighted_capacity = "1"
}
override {
instance_type = "m5.large"
weighted_capacity = "2"
}
override {
instance_type = "m5a.large"
weighted_capacity = "2"
}
}
instances_distribution {
on_demand_base_capacity = 2
on_demand_percentage_above_base_capacity = 20
spot_allocation_strategy = "capacity-optimized"
}
}
instance_refresh {
strategy = "Rolling"
preferences {
min_healthy_percentage = 80
instance_warmup = 120
}
}
tag {
key = "Name"
value = "${var.project_name}-web"
propagate_at_launch = true
}
}
# Target Tracking 스케일링
resource "aws_autoscaling_policy" "cpu_target" {
name = "cpu-target-tracking"
autoscaling_group_name = aws_autoscaling_group.web.name
policy_type = "TargetTrackingScaling"
target_tracking_configuration {
predefined_metric_specification {
predefined_metric_type = "ASGAverageCPUUtilization"
}
target_value = 70.0
}
}
# Predictive Scaling 추가
resource "aws_autoscaling_policy" "predictive" {
name = "predictive-scaling"
autoscaling_group_name = aws_autoscaling_group.web.name
policy_type = "PredictiveScaling"
predictive_scaling_configuration {
metric_specification {
target_value = 70
predefined_scaling_metric_specification {
predefined_metric_type = "ASGAverageCPUUtilization"
resource_label = ""
}
predefined_load_metric_specification {
predefined_metric_type = "ASGTotalCPUUtilization"
resource_label = ""
}
}
mode = "ForecastAndScale"
scheduling_buffer_time = 300
}
}
Terraform Modules로 재사용 가능한 인프라
# modules/asg/main.tf
module "web_asg" {
source = "./modules/asg"
project_name = "my-web-app"
environment = "production"
vpc_id = module.vpc.vpc_id
private_subnet_ids = module.vpc.private_subnet_ids
alb_target_group = module.alb.target_group_arn
instance_types = ["t3.medium", "t3a.medium", "m5.large"]
min_size = 2
max_size = 30
desired_capacity = 4
spot_percentage = 80
on_demand_base = 2
target_cpu = 70
tags = local.common_tags
}
State Management (S3 + DynamoDB)
# state-backend/main.tf (먼저 이것을 로컬에서 apply)
resource "aws_s3_bucket" "terraform_state" {
bucket = "my-company-terraform-state"
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
resource "aws_dynamodb_table" "terraform_lock" {
name = "terraform-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
Terraform 워크플로우
# 1. 초기화
terraform init
# 2. 코드 검증
terraform validate
# 3. 변경 계획 확인
terraform plan -out=tfplan
# 4. 변경 적용
terraform apply tfplan
# 5. 상태 확인
terraform state list
terraform state show aws_autoscaling_group.web
# 6. 인프라 삭제 (개발 환경)
terraform destroy
6. AWS CDK로 동적 인프라 구축
CDK TypeScript 예제: ASG + ALB
import * as cdk from 'aws-cdk-lib'
import * as ec2 from 'aws-cdk-lib/aws-ec2'
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'
import * as autoscaling from 'aws-cdk-lib/aws-autoscaling'
import * as rds from 'aws-cdk-lib/aws-rds'
import { Construct } from 'constructs'
export class WebAppStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props)
// VPC
const vpc = new ec2.Vpc(this, 'WebVpc', {
maxAzs: 3,
natGateways: 2,
subnetConfiguration: [
{
cidrMask: 24,
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,
},
],
})
// ALB
const alb = new elbv2.ApplicationLoadBalancer(this, 'WebAlb', {
vpc,
internetFacing: true,
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
})
// ASG with Mixed Instances
const asg = new autoscaling.AutoScalingGroup(this, 'WebAsg', {
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
mixedInstancesPolicy: {
instancesDistribution: {
onDemandBaseCapacity: 2,
onDemandPercentageAboveBaseCapacity: 20,
spotAllocationStrategy: autoscaling.SpotAllocationStrategy.CAPACITY_OPTIMIZED,
},
launchTemplate: new ec2.LaunchTemplate(this, 'LaunchTemplate', {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
machineImage: ec2.MachineImage.latestAmazonLinux2023(),
userData: ec2.UserData.custom(`
#!/bin/bash
yum update -y
yum install -y docker
systemctl start docker
docker run -d -p 80:8080 my-web-app:latest
`),
}),
launchTemplateOverrides: [
{ instanceType: new ec2.InstanceType('t3.medium') },
{ instanceType: new ec2.InstanceType('t3a.medium') },
{ instanceType: new ec2.InstanceType('m5.large') },
{ instanceType: new ec2.InstanceType('m5a.large') },
],
},
minCapacity: 2,
maxCapacity: 30,
healthCheck: autoscaling.HealthCheck.elb({
grace: cdk.Duration.seconds(300),
}),
})
// Target Tracking Scaling
asg.scaleOnCpuUtilization('CpuScaling', {
targetUtilizationPercent: 70,
cooldown: cdk.Duration.seconds(300),
})
asg.scaleOnRequestCount('RequestScaling', {
targetRequestsPerMinute: 1000,
})
// ALB Listener
const listener = alb.addListener('HttpsListener', {
port: 443,
certificates: [
elbv2.ListenerCertificate.fromArn(
'arn:aws:acm:ap-northeast-2:123456789012:certificate/abc-123'
),
],
})
listener.addTargets('WebTarget', {
port: 80,
targets: [asg],
healthCheck: {
path: '/health',
interval: cdk.Duration.seconds(15),
healthyThresholdCount: 2,
unhealthyThresholdCount: 3,
},
deregistrationDelay: cdk.Duration.seconds(30),
})
// RDS Multi-AZ
const database = new rds.DatabaseCluster(this, 'Database', {
engine: rds.DatabaseClusterEngine.auroraPostgres({
version: rds.AuroraPostgresEngineVersion.VER_15_4,
}),
writer: rds.ClusterInstance.provisioned('Writer', {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.R6G, ec2.InstanceSize.LARGE),
}),
readers: [
rds.ClusterInstance.provisioned('Reader1', {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.R6G, ec2.InstanceSize.LARGE),
}),
],
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
storageEncrypted: true,
deletionProtection: true,
})
// ASG에서 RDS 접근 허용
database.connections.allowDefaultPortFrom(asg)
// Outputs
new cdk.CfnOutput(this, 'AlbDnsName', {
value: alb.loadBalancerDnsName,
description: 'ALB DNS Name',
})
}
}
CDK Constructs: L1 vs L2 vs L3
| 레벨 | 이름 | 설명 | 예시 |
|---|---|---|---|
| L1 | Cfn 리소스 | CloudFormation 리소스 1:1 매핑 | CfnInstance, CfnVPC |
| L2 | 큐레이티드 | 합리적 기본값 + 헬퍼 메서드 | ec2.Vpc, lambda.Function |
| L3 | 패턴 | 여러 리소스를 조합한 아키텍처 패턴 | ecs_patterns.ApplicationLoadBalancedFargateService |
L3 패턴을 활용하면 복잡한 인프라를 몇 줄로 정의할 수 있습니다:
import * as ecs_patterns from 'aws-cdk-lib/aws-ecs-patterns'
// L3 패턴: ALB + Fargate 서비스를 한 번에
const fargateService = new ecs_patterns.ApplicationLoadBalancedFargateService(
this,
'FargateService',
{
cluster,
desiredCount: 2,
taskImageOptions: {
image: ecs.ContainerImage.fromRegistry('my-app:latest'),
containerPort: 8080,
},
publicLoadBalancer: true,
capacityProviderStrategies: [
{ capacityProvider: 'FARGATE', weight: 1, base: 2 },
{ capacityProvider: 'FARGATE_SPOT', weight: 4 },
],
}
)
fargateService.targetGroup.configureHealthCheck({
path: '/health',
})
CDK vs Terraform vs CloudFormation 비교
| 항목 | CDK | Terraform | CloudFormation |
|---|---|---|---|
| 언어 | TypeScript, Python, Java, Go | HCL | YAML/JSON |
| 학습 곡선 | 중간 (프로그래밍 언어 활용) | 중간 (HCL 학습) | 낮음 (선언적 YAML) |
| 추상화 수준 | 높음 (L3 패턴) | 중간 (모듈) | 낮음 (리소스 단위) |
| 멀티 클라우드 | AWS 전용 | 멀티 클라우드 | AWS 전용 |
| State 관리 | CloudFormation 스택 | S3 + DynamoDB | 자동 관리 |
| 테스트 | 유닛 테스트 가능 | 테라테스트 | 제한적 |
| 드리프트 감지 | CloudFormation 통해 지원 | terraform plan | 지원 |
| 생태계 | Construct Hub | Terraform Registry | 제한적 |
CDK Pipeline: CI/CD로 인프라 배포
import { CodePipeline, CodePipelineSource, ShellStep } from 'aws-cdk-lib/pipelines'
const pipeline = new CodePipeline(this, 'Pipeline', {
pipelineName: 'WebAppPipeline',
synth: new ShellStep('Synth', {
input: CodePipelineSource.gitHub('my-org/my-repo', 'main'),
commands: ['npm ci', 'npm run build', 'npx cdk synth'],
}),
})
// 스테이징 환경 배포
pipeline.addStage(
new WebAppStage(this, 'Staging', {
env: { account: '123456789012', region: 'ap-northeast-2' },
})
)
// 프로덕션 환경 배포 (수동 승인 포함)
pipeline.addStage(
new WebAppStage(this, 'Production', {
env: { account: '987654321098', region: 'ap-northeast-2' },
}),
{
pre: [new pipelines.ManualApprovalStep('PromoteToProduction')],
}
)
7. 비용 최적화 실전 전략
Reserved Instances vs Savings Plans vs Spot 비교
| 항목 | Reserved Instances | Savings Plans | Spot Instances |
|---|---|---|---|
| 할인율 | 최대 72% | 최대 72% | 최대 90% |
| 약정 기간 | 1년 또는 3년 | 1년 또는 3년 | 없음 |
| 유연성 | 인스턴스 타입/리전 고정 | 컴퓨팅 유형 유연 | 중단 가능 |
| 선결제 옵션 | 전액/부분/없음 | 전액/부분/없음 | 없음 |
| 적합 대상 | 예측 가능한 기본 워크로드 | 다양한 컴퓨팅 사용 | 중단 가능 워크로드 |
| Lambda 적용 | 불가 | Compute SP 적용 가능 | 해당 없음 |
| Fargate 적용 | 불가 | Compute SP 적용 가능 | Fargate Spot |
AWS Compute Optimizer 활용
Compute Optimizer는 ML을 사용하여 워크로드를 분석하고 최적 인스턴스 타입을 추천합니다.
# Compute Optimizer 권장 사항 조회
aws compute-optimizer get-ec2-instance-recommendations \
--instance-arns "arn:aws:ec2:ap-northeast-2:123456789012:instance/i-0123456789abcdef0" \
--query 'instanceRecommendations[*].{
InstanceArn: instanceArn,
CurrentType: currentInstanceType,
Finding: finding,
Recommendations: recommendationOptions[0].instanceType,
EstimatedSavings: recommendationOptions[0].estimatedMonthlySavings.value
}' \
--output table
Schedule-based Scaling
개발/스테이징 환경이나 업무 시간 외 트래픽이 적은 서비스에 적용합니다:
# 업무 시간 (월-금 09:00-18:00): 인스턴스 4개
resource "aws_autoscaling_schedule" "scale_up_business_hours" {
scheduled_action_name = "scale-up-business"
autoscaling_group_name = aws_autoscaling_group.web.name
min_size = 4
max_size = 30
desired_capacity = 4
recurrence = "0 0 * * 1-5" # UTC 기준 (KST 09:00)
time_zone = "Asia/Seoul"
}
# 야간 (월-금 18:00 이후): 인스턴스 2개
resource "aws_autoscaling_schedule" "scale_down_night" {
scheduled_action_name = "scale-down-night"
autoscaling_group_name = aws_autoscaling_group.web.name
min_size = 2
max_size = 10
desired_capacity = 2
recurrence = "0 9 * * 1-5" # UTC 기준 (KST 18:00)
time_zone = "Asia/Seoul"
}
# 주말: 인스턴스 1개
resource "aws_autoscaling_schedule" "scale_down_weekend" {
scheduled_action_name = "scale-down-weekend"
autoscaling_group_name = aws_autoscaling_group.web.name
min_size = 1
max_size = 5
desired_capacity = 1
recurrence = "0 0 * * 6" # 토요일 00:00 KST
time_zone = "Asia/Seoul"
}
태그 기반 비용 추적
# 모든 리소스에 비용 추적 태그 적용
locals {
cost_tags = {
CostCenter = "engineering"
Team = "platform"
Project = "web-app"
Environment = var.environment
ManagedBy = "terraform"
}
}
# AWS Cost Explorer에서 태그별 비용 분석 활성화
resource "aws_ce_cost_category" "team_costs" {
name = "TeamCosts"
rule {
value = "Platform"
rule {
tags {
key = "Team"
values = ["platform"]
match_options = ["EQUALS"]
}
}
}
rule {
value = "Backend"
rule {
tags {
key = "Team"
values = ["backend"]
match_options = ["EQUALS"]
}
}
}
}
실전 사례: 월 10,000달러에서 2,500달러로 절감
Before (정적 인프라):
- EC2 m5.xlarge x 10대 (온디맨드, 24/7) = $1,401/월
- EC2 m5.2xlarge x 5대 (배치 서버, 24/7) = $1,401/월
- RDS db.r5.xlarge Multi-AZ = $1,020/월
- NAT Gateway x 2 = $130/월
- ALB = $50/월
- 기타 (EBS, S3, CloudWatch) = $500/월
- 총 월 비용: 약 $4,502
After (동적 인프라):
| 변경 사항 | Before | After | 절감 |
|---|---|---|---|
| 웹 서버 10대 → ASG (2 온디맨드 + 스팟) | $1,401 | $420 | 70% |
| 배치 서버 → Spot Fleet (필요시만) | $1,401 | $140 | 90% |
| RDS → Savings Plan 적용 | $1,020 | $663 | 35% |
| NAT Gateway 최적화 | $130 | $65 | 50% |
| Schedule Scaling (야간/주말 축소) | - | 추가 -30% | - |
| 합계 | $4,502 | 약 $1,288 | 71% |
8. 고가용성 아키텍처
Multi-AZ 배포 아키텍처
Route 53 (DNS Failover)
|
CloudFront (CDN)
|
ALB (Multi-AZ)
/ \
AZ-a (ap-northeast-2a) AZ-c (ap-northeast-2c)
+------------------+ +------------------+
| EC2 (ASG) | | EC2 (ASG) |
| - Web Server x2 | | - Web Server x2 |
| | | |
| RDS (Primary) | | RDS (Standby) |
| ElastiCache | | ElastiCache |
+------------------+ +------------------+
Route 53 Health Check + Failover
resource "aws_route53_health_check" "primary" {
fqdn = "app.example.com"
port = 443
type = "HTTPS"
resource_path = "/health"
failure_threshold = 3
request_interval = 10
regions = ["us-east-1", "eu-west-1", "ap-southeast-1"]
tags = {
Name = "primary-health-check"
}
}
resource "aws_route53_record" "primary" {
zone_id = aws_route53_zone.main.zone_id
name = "app.example.com"
type = "A"
alias {
name = aws_lb.web.dns_name
zone_id = aws_lb.web.zone_id
evaluate_target_health = true
}
failover_routing_policy {
type = "PRIMARY"
}
health_check_id = aws_route53_health_check.primary.id
set_identifier = "primary"
}
Chaos Engineering: AWS Fault Injection Simulator
resource "aws_fis_experiment_template" "spot_interruption" {
description = "Simulate Spot Instance interruptions"
role_arn = aws_iam_role.fis.arn
stop_condition {
source = "aws:cloudwatch:alarm"
value = aws_cloudwatch_metric_alarm.error_rate.arn
}
action {
name = "interrupt-spot-instances"
action_id = "aws:ec2:send-spot-instance-interruptions"
parameter {
key = "durationBeforeInterruption"
value = "PT2M"
}
target {
key = "SpotInstances"
value = "spot-instances-target"
}
}
target {
name = "spot-instances-target"
resource_type = "aws:ec2:spot-instance"
selection_mode = "COUNT(2)"
resource_tag {
key = "Environment"
value = "staging"
}
}
}
9. 필요할 때 EC2 생성하는 패턴 3가지
9-1. EventBridge + Lambda에서 EC2 생성 (이벤트 기반)
S3에 파일이 업로드되면 EC2를 시작하여 처리하고 자동 종료하는 패턴입니다:
import boto3
import json
import time
ec2 = boto3.client('ec2')
ssm = boto3.client('ssm')
def lambda_handler(event, context):
"""
S3 업로드 이벤트 -> EC2 인스턴스 생성 -> 처리 -> 자동 종료
"""
# S3 이벤트에서 파일 정보 추출
bucket = event['detail']['bucket']['name']
key = event['detail']['object']['key']
print(f"Processing file: s3://{bucket}/{key}")
# EC2 인스턴스 시작
user_data = f"""#!/bin/bash
set -e
# 작업 수행
aws s3 cp s3://{bucket}/{key} /tmp/input
python3 /opt/process.py /tmp/input /tmp/output
aws s3 cp /tmp/output s3://{bucket}-processed/{key}
# 작업 완료 후 자기 자신 종료
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
aws ec2 terminate-instances --instance-ids $INSTANCE_ID
"""
response = ec2.run_instances(
ImageId='ami-0abcdef1234567890',
InstanceType='c5.xlarge',
MinCount=1,
MaxCount=1,
IamInstanceProfile={
'Name': 'ec2-processing-role'
},
UserData=user_data,
TagSpecifications=[
{
'ResourceType': 'instance',
'Tags': [
{'Key': 'Name', 'Value': f'processor-{key[:20]}'},
{'Key': 'Purpose', 'Value': 'batch-processing'},
{'Key': 'AutoTerminate', 'Value': 'true'}
]
}
],
InstanceMarketOptions={
'MarketType': 'spot',
'SpotOptions': {
'SpotInstanceType': 'one-time',
'InstanceInterruptionBehavior': 'terminate'
}
}
)
instance_id = response['Instances'][0]['InstanceId']
print(f"Started processing instance: {instance_id}")
return {
'statusCode': 200,
'body': json.dumps({
'instance_id': instance_id,
'file': f's3://{bucket}/{key}'
})
}
9-2. Step Functions 오케스트레이션
복잡한 워크플로우를 Step Functions로 관리합니다:
{
"Comment": "EC2 기반 배치 처리 워크플로우",
"StartAt": "CreateInstance",
"States": {
"CreateInstance": {
"Type": "Task",
"Resource": "arn:aws:states:::ec2:runInstances",
"Parameters": {
"ImageId": "ami-0abcdef1234567890",
"InstanceType": "c5.2xlarge",
"MinCount": 1,
"MaxCount": 1,
"IamInstanceProfile": {
"Name": "batch-processing-role"
},
"TagSpecifications": [
{
"ResourceType": "instance",
"Tags": [
{
"Key": "Purpose",
"Value": "step-function-batch"
}
]
}
]
},
"ResultPath": "$.instanceInfo",
"Next": "WaitForInstance"
},
"WaitForInstance": {
"Type": "Wait",
"Seconds": 60,
"Next": "CheckInstanceStatus"
},
"CheckInstanceStatus": {
"Type": "Task",
"Resource": "arn:aws:states:::aws-sdk:ec2:describeInstanceStatus",
"Parameters": {
"InstanceIds.$": "States.Array($.instanceInfo.Instances[0].InstanceId)"
},
"ResultPath": "$.status",
"Next": "IsInstanceReady"
},
"IsInstanceReady": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.status.InstanceStatuses[0].InstanceState.Name",
"StringEquals": "running",
"Next": "RunProcessing"
}
],
"Default": "WaitForInstance"
},
"RunProcessing": {
"Type": "Task",
"Resource": "arn:aws:states:::ssm:sendCommand.sync",
"Parameters": {
"DocumentName": "AWS-RunShellScript",
"InstanceIds.$": "States.Array($.instanceInfo.Instances[0].InstanceId)",
"Parameters": {
"commands": ["cd /opt/app && python3 process.py"]
}
},
"ResultPath": "$.processingResult",
"Next": "TerminateInstance",
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"Next": "TerminateInstance",
"ResultPath": "$.error"
}
]
},
"TerminateInstance": {
"Type": "Task",
"Resource": "arn:aws:states:::aws-sdk:ec2:terminateInstances",
"Parameters": {
"InstanceIds.$": "States.Array($.instanceInfo.Instances[0].InstanceId)"
},
"End": true
}
}
}
9-3. Kubernetes Jobs + Karpenter
Karpenter는 AWS에 최적화된 Kubernetes 노드 오토스케일러입니다:
# karpenter-nodepool.yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: batch-processing
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ['spot', 'on-demand']
- key: node.kubernetes.io/instance-type
operator: In
values:
- c5.xlarge
- c5a.xlarge
- c5.2xlarge
- c6i.xlarge
- c6i.2xlarge
- m5.xlarge
- m5a.xlarge
- key: topology.kubernetes.io/zone
operator: In
values:
- ap-northeast-2a
- ap-northeast-2c
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
limits:
cpu: '100'
memory: 400Gi
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 30s
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
amiSelectorTerms:
- alias: al2023@latest
subnetSelectorTerms:
- tags:
Tier: private
securityGroupSelectorTerms:
- tags:
kubernetes.io/cluster/my-cluster: owned
instanceProfile: KarpenterNodeInstanceProfile
# batch-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: data-processing-job
spec:
parallelism: 10
completions: 100
backoffLimit: 3
template:
metadata:
labels:
app: data-processor
spec:
containers:
- name: processor
image: my-registry/data-processor:v1.2
resources:
requests:
cpu: '2'
memory: '4Gi'
limits:
cpu: '4'
memory: '8Gi'
env:
- name: BATCH_SIZE
value: '1000'
restartPolicy: OnFailure
nodeSelector:
karpenter.sh/capacity-type: spot
tolerations:
- key: 'karpenter.sh/disruption'
operator: 'Exists'
Karpenter vs Cluster Autoscaler 비교:
| 항목 | Karpenter | Cluster Autoscaler |
|---|---|---|
| 노드 프로비저닝 속도 | 수초 (EC2 직접 호출) | 수분 (ASG 경유) |
| 인스턴스 타입 선택 | 워크로드 기반 자동 선택 | ASG에 정의된 타입만 |
| 빈 팩킹 | 자동 최적화 | 제한적 |
| Spot 통합 | 네이티브 지원 | ASG Mixed Instances |
| 스케일 다운 | 즉시 (미사용 노드 30초 후 제거) | 10분 기본 대기 |
| AWS 종속 | AWS 전용 | 멀티 클라우드 |
10. 모니터링과 알람
CloudWatch Alarms + SNS
# ASG 관련 알람
resource "aws_cloudwatch_metric_alarm" "asg_high_cpu" {
alarm_name = "asg-high-cpu"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 3
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = 60
statistic = "Average"
threshold = 85
alarm_description = "ASG CPU 사용률이 85%를 초과했습니다"
dimensions = {
AutoScalingGroupName = aws_autoscaling_group.web.name
}
alarm_actions = [aws_sns_topic.alerts.arn]
ok_actions = [aws_sns_topic.alerts.arn]
}
# Spot Interruption 카운트 알람
resource "aws_cloudwatch_metric_alarm" "spot_interruptions" {
alarm_name = "spot-interruption-count"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "SpotInterruptionCount"
namespace = "Custom/SpotMetrics"
period = 300
statistic = "Sum"
threshold = 3
alarm_description = "5분 내 스팟 인터럽션이 3회 이상 발생했습니다"
alarm_actions = [aws_sns_topic.critical_alerts.arn]
}
# ALB 5xx 에러율 알람
resource "aws_cloudwatch_metric_alarm" "alb_5xx" {
alarm_name = "alb-5xx-error-rate"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
threshold = 5
metric_query {
id = "error_rate"
expression = "(errors / requests) * 100"
label = "5xx Error Rate"
return_data = true
}
metric_query {
id = "errors"
metric {
metric_name = "HTTPCode_Target_5XX_Count"
namespace = "AWS/ApplicationELB"
period = 60
stat = "Sum"
dimensions = {
LoadBalancer = aws_lb.web.arn_suffix
}
}
}
metric_query {
id = "requests"
metric {
metric_name = "RequestCount"
namespace = "AWS/ApplicationELB"
period = 60
stat = "Sum"
dimensions = {
LoadBalancer = aws_lb.web.arn_suffix
}
}
}
alarm_actions = [aws_sns_topic.critical_alerts.arn]
}
CloudWatch Dashboard
resource "aws_cloudwatch_dashboard" "main" {
dashboard_name = "web-app-dashboard"
dashboard_body = jsonencode({
widgets = [
{
type = "metric"
x = 0
y = 0
width = 12
height = 6
properties = {
metrics = [
["AWS/EC2", "CPUUtilization", "AutoScalingGroupName", aws_autoscaling_group.web.name],
["AWS/EC2", "NetworkIn", "AutoScalingGroupName", aws_autoscaling_group.web.name],
["AWS/EC2", "NetworkOut", "AutoScalingGroupName", aws_autoscaling_group.web.name]
]
period = 300
stat = "Average"
title = "ASG Metrics"
}
},
{
type = "metric"
x = 12
y = 0
width = 12
height = 6
properties = {
metrics = [
["AWS/AutoScaling", "GroupDesiredCapacity", "AutoScalingGroupName", aws_autoscaling_group.web.name],
["AWS/AutoScaling", "GroupInServiceInstances", "AutoScalingGroupName", aws_autoscaling_group.web.name],
["AWS/AutoScaling", "GroupTotalInstances", "AutoScalingGroupName", aws_autoscaling_group.web.name]
]
period = 60
stat = "Average"
title = "ASG Capacity"
}
}
]
})
}
Grafana + Prometheus on EKS
EKS 환경에서 Prometheus와 Grafana를 사용한 모니터링 스택:
# prometheus-values.yaml (Helm)
prometheus:
prometheusSpec:
retention: 15d
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: gp3
accessModes: ['ReadWriteOnce']
resources:
requests:
storage: 50Gi
additionalScrapeConfigs:
- job_name: karpenter
kubernetes_sd_configs:
- role: endpoints
namespaces:
names:
- karpenter
relabel_configs:
- source_labels: [__meta_kubernetes_endpoint_port_name]
regex: http-metrics
action: keep
grafana:
adminPassword: 'secure-password'
persistence:
enabled: true
size: 10Gi
dashboardProviders:
dashboardproviders.yaml:
apiVersion: 1
providers:
- name: default
orgId: 1
folder: ''
type: file
options:
path: /var/lib/grafana/dashboards/default
11. 퀴즈
Q1: Auto Scaling Group에서 Target Tracking Scaling이 Simple Scaling보다 권장되는 주된 이유는?
정답: Target Tracking은 목표 메트릭 값을 자동으로 유지하며, Scale-in과 Scale-out을 균형 있게 조절하고, 별도의 CloudWatch 알람 관리가 불필요합니다.
Simple Scaling은 쿨다운 기간 동안 추가 스케일링이 불가하고, 스케일링 양을 수동으로 정의해야 합니다. Target Tracking은 AWS가 자동으로 최적의 스케일링을 수행하므로 운영 부담이 적고 더 정확한 스케일링이 가능합니다.
Q2: Mixed Instances Policy에서 on_demand_base_capacity를 2로, on_demand_percentage_above_base_capacity를 20으로 설정했습니다. ASG에서 총 12대의 인스턴스가 필요하다면 온디맨드와 스팟의 비율은?
정답: 온디맨드 4대, 스팟 8대
계산 과정:
- 기본 온디맨드: 2대
- 추가 필요: 12 - 2 = 10대
- 추가분 중 온디맨드 (20%): 10 x 0.2 = 2대
- 추가분 중 스팟 (80%): 10 x 0.8 = 8대
- 총 온디맨드: 2 + 2 = 4대, 총 스팟: 8대
Q3: Spot 인스턴스가 중단(interruption)될 때 AWS는 몇 분 전에 경고를 보내나요? 그리고 이 경고를 감지하는 방법 2가지는?
정답: 2분 전에 경고를 보냅니다.
경고 감지 방법:
- EC2 메타데이터 폴링: 인스턴스 내부에서
http://169.254.169.254/latest/meta-data/spot/instance-action엔드포인트를 주기적으로 확인 - EventBridge 규칙: EC2 Spot Instance Interruption Warning 이벤트를 EventBridge로 수신하여 Lambda 등으로 처리
Q4: Karpenter가 Cluster Autoscaler보다 노드 프로비저닝이 빠른 근본적 이유는?
정답: Karpenter는 EC2 API를 직접 호출하여 노드를 생성하지만, Cluster Autoscaler는 ASG(Auto Scaling Group)를 경유하여 노드를 생성하기 때문입니다.
Cluster Autoscaler는 Pending Pod를 감지하고 ASG의 desired count를 변경한 후, ASG가 Launch Template에 따라 인스턴스를 생성하는 간접적 경로를 거칩니다. Karpenter는 워크로드 요구사항을 분석하고 최적의 인스턴스 타입을 선택하여 EC2 Fleet API로 직접 생성하므로 수초 내에 노드가 준비됩니다.
Q5: Terraform의 State를 S3 + DynamoDB로 관리할 때, DynamoDB의 역할은 무엇인가요?
정답: DynamoDB는 State Locking(상태 잠금)을 담당합니다.
여러 팀원이 동시에 terraform apply를 실행하면 State 파일이 충돌할 수 있습니다. DynamoDB 테이블에 Lock 레코드를 생성하여 한 번에 한 명만 State를 수정할 수 있도록 합니다. 이는 경쟁 조건(Race Condition)을 방지하고 인프라의 일관성을 보장합니다.
12. 참고 자료
- AWS Auto Scaling 공식 문서
- EC2 Spot Instances Best Practices
- AWS Well-Architected Framework - Cost Optimization Pillar
- Terraform AWS Provider Documentation
- AWS CDK Developer Guide
- Karpenter Documentation
- AWS Compute Optimizer User Guide
- AWS Savings Plans User Guide
- AWS Fault Injection Simulator User Guide
- AWS Step Functions Developer Guide
- Amazon ECS on Fargate Best Practices
- AWS Lambda Pricing
- Spot Instance Advisor
- AWS CloudWatch User Guide
- Terraform Best Practices
- AWS re:Invent - Cost Optimization at Scale
- Karpenter vs Cluster Autoscaler Deep Dive