Skip to content
Published on

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

Authors

1. 왜 동적 인프라인가? (정적 vs 동적)

고정 서버의 근본적 문제

많은 기업이 여전히 "피크 트래픽 기준"으로 서버를 프로비저닝합니다. 블랙프라이데이에 트래픽이 10배 증가한다면, 1년 내내 10배 규모의 서버를 운영하는 방식입니다. 이는 80% 이상의 시간 동안 비용을 낭비하는 것과 같습니다.

실제 사례를 살펴보면:

  • 전자상거래 A사: 피크 시간(저녁 8-10시)에만 트래픽 5배 증가 → 나머지 22시간은 서버 유휴 상태
  • 미디어 B사: 주말 트래픽이 평일의 3배 → 평일에는 서버 66% 유휴
  • SaaS C사: 월말 정산 시 배치 처리 폭주 → 월 27일은 과잉 프로비저닝

이런 패턴에서 정적 인프라는 다음과 같은 문제를 야기합니다:

문제영향비용 낭비율
피크 기준 프로비저닝평소 과잉 리소스60-80%
수동 스케일링트래픽 급증 시 대응 지연다운타임 발생
인프라 변경 어려움새 기능 배포 지연기회 비용
단일 장애점서버 장애 시 서비스 중단매출 손실

동적 인프라의 핵심 가치

동적 인프라란 워크로드에 따라 리소스가 자동으로 확장되고 축소되는 아키텍처를 말합니다.

핵심 3가지 원칙:

  1. 탄력성(Elasticity): 트래픽 증가 → 자동 확장, 트래픽 감소 → 자동 축소
  2. 비용 최적화(Cost Optimization): 사용한 만큼만 비용 지불
  3. 고가용성(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)은 세 가지 핵심 컴포넌트로 구성됩니다:

  1. Launch Template: 어떤 인스턴스를 생성할지 정의 (AMI, 인스턴스 유형, 보안 그룹, 키 페어)
  2. Scaling Policy: 언제, 어떻게 스케일링할지 규칙 정의
  3. 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.6037%
Savings Plan (1년)$0.125$91.2535%
Spot (평균)$0.058$42.3470%
Spot (최저)$0.019$13.8790%

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 최소화 전략

  1. 프로비저닝 동시성 사용 (가장 확실한 방법)
  2. 패키지 크기 최소화 (의존성 최적화, Layer 활용)
  3. 런타임 선택: Python/Node.js는 Java보다 Cold Start가 빠름
  4. 초기화 코드 최적화: 핸들러 외부에서 DB 연결 등 초기화
  5. 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 비교표

항목LambdaFargateEC2 (ASG)
스케일링 속도밀리초30초-2분2-5분
최대 실행 시간15분무제한무제한
메모리128MB-10GB512MB-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

레벨이름설명예시
L1Cfn 리소스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 비교

항목CDKTerraformCloudFormation
언어TypeScript, Python, Java, GoHCLYAML/JSON
학습 곡선중간 (프로그래밍 언어 활용)중간 (HCL 학습)낮음 (선언적 YAML)
추상화 수준높음 (L3 패턴)중간 (모듈)낮음 (리소스 단위)
멀티 클라우드AWS 전용멀티 클라우드AWS 전용
State 관리CloudFormation 스택S3 + DynamoDB자동 관리
테스트유닛 테스트 가능테라테스트제한적
드리프트 감지CloudFormation 통해 지원terraform plan지원
생태계Construct HubTerraform 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 InstancesSavings PlansSpot 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 (동적 인프라):

변경 사항BeforeAfter절감
웹 서버 10대 → ASG (2 온디맨드 + 스팟)$1,401$42070%
배치 서버 → Spot Fleet (필요시만)$1,401$14090%
RDS → Savings Plan 적용$1,020$66335%
NAT Gateway 최적화$130$6550%
Schedule Scaling (야간/주말 축소)-추가 -30%-
합계$4,502약 $1,28871%

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 비교:

항목KarpenterCluster 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분 전에 경고를 보냅니다.

경고 감지 방법:

  1. EC2 메타데이터 폴링: 인스턴스 내부에서 http://169.254.169.254/latest/meta-data/spot/instance-action 엔드포인트를 주기적으로 확인
  2. 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. 참고 자료

  1. AWS Auto Scaling 공식 문서
  2. EC2 Spot Instances Best Practices
  3. AWS Well-Architected Framework - Cost Optimization Pillar
  4. Terraform AWS Provider Documentation
  5. AWS CDK Developer Guide
  6. Karpenter Documentation
  7. AWS Compute Optimizer User Guide
  8. AWS Savings Plans User Guide
  9. AWS Fault Injection Simulator User Guide
  10. AWS Step Functions Developer Guide
  11. Amazon ECS on Fargate Best Practices
  12. AWS Lambda Pricing
  13. Spot Instance Advisor
  14. AWS CloudWatch User Guide
  15. Terraform Best Practices
  16. AWS re:Invent - Cost Optimization at Scale
  17. Karpenter vs Cluster Autoscaler Deep Dive