🧩 ECR에 이미지 푸시하기
ECR(Elastic Container Registry)는 AWS의 Docker Hub다.
# 0. 환경 변수 설정 (재사용 편의)
export AWS_REGION=ap-northeast-2
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
export ECR_REPOSITORY=spring-boot-ecs-demo
# 1. ECR 로그인
aws ecr get-login-password --region $AWS_REGION | \
docker login --username AWS --password-stdin \
$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com
# 2. ECR 레포지토리 생성
aws ecr create-repository \
--repository-name $ECR_REPOSITORY \
--region $AWS_REGION \
--image-scanning-configuration scanOnPush=true \
--encryption-configuration encryptionType=AES256
# 3. 이미지 태그
docker tag spring-boot-ecs-demo:latest \
$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPOSITORY:latest
docker tag spring-boot-ecs-demo:latest \
$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPOSITORY:v1.0.0
# 4. 푸시
docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPOSITORY:latest
docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPOSITORY:v1.0.0
# 5. 이미지 확인
aws ecr list-images \
--repository-name $ECR_REPOSITORY \
--region $AWS_REGION
- ECR 라이프사이클 정책 (선택): 오래된 이미지를 자동으로 삭제해 비용 절감
{
"rules": [
{
"rulePriority": 1,
"description": "Keep last 10 images",
"selection": {
"tagStatus": "any",
"countType": "imageCountMoreThan",
"countNumber": 10
},
"action": {
"type": "expire"
}
}
]
}
s ecr put-lifecycle-policy \
--repository-name $ECR_REPOSITORY \
--lifecycle-policy-text file://lifecycle-policy.json
🧩 ECS Fargate 배포 실습
IAM Role 생성
# 1. Trust Policy 파일 생성
cat > ecs-task-execution-trust-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
# 2. Role 생성
aws iam create-role \
--role-name ecsTaskExecutionRole \
--assume-role-policy-document file://ecs-task-execution-trust-policy.json
# 3. AWS 관리형 정책 연결
aws iam attach-role-policy \
--role-name ecsTaskExecutionRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
# 4. Role ARN 확인 (나중에 사용)
aws iam get-role --role-name ecsTaskExecutionRole --query 'Role.Arn'
- Task Role 생성 (애플리케이션용, 선택)
# 1. Trust Policy
cat > ecs-task-trust-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
# 2. Role 생성
aws iam create-role \
--role-name ecsTaskRole \
--assume-role-policy-document file://ecs-task-trust-policy.json
# 3. 커스텀 정책 생성 (예: S3 접근)
cat > ecs-task-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-app-bucket/*"
}
]
}
EOF
aws iam put-role-policy \
--role-name ecsTaskRole \
--policy-name S3AccessPolicy \
--policy-document file://ecs-task-policy.json
VPC 및 보안그룹 준비
- 필요한 네트워크 구성
- VPC (기본 VPC 사용 가능)
- Public 서브넷 최소 2개 (Multi-AZ)
- 보안 그룹 2개 (ALB용, ECS용)
- ALB용 보안 그룹
# VPC ID 확인
export VPC_ID=$(aws ec2 describe-vpcs \
--filters "Name=isDefault,Values=true" \
--query 'Vpcs[0].VpcId' \
--output text)
# ALB 보안 그룹 생성
export ALB_SG=$(aws ec2 create-security-group \
--group-name ecs-alb-sg \
--description "Security group for ALB" \
--vpc-id $VPC_ID \
--query 'GroupId' \
--output text)
# Inbound 규칙: HTTP
aws ec2 authorize-security-group-ingress \
--group-id $ALB_SG \
--protocol tcp \
--port 80 \
--cidr 0.0.0.0/0
# Inbound 규칙: HTTPS (선택)
aws ec2 authorize-security-group-ingress \
--group-id $ALB_SG \
--protocol tcp \
--port 443 \
--cidr 0.0.0.0/0
# ECS Task 보안 그룹 생성
export ECS_SG=$(aws ec2 create-security-group \
--group-name ecs-task-sg \
--description "Security group for ECS tasks" \
--vpc-id $VPC_ID \
--query 'GroupId' \
--output text)
# Inbound 규칙: ALB에서 8080 포트 통신 허용
aws ec2 authorize-security-group-ingress \
--group-id $ECS_SG \
--protocol tcp \
--port 8080 \
--source-group $ALB_SG
# Outbound 규칙: HTTPS (ECR, Secrets Manager 등 접근)
aws ec2 authorize-security-group-egress \
--group-id $ECS_SG \
--protocol tcp \
--port 443 \
--cidr 0.0.0.0/0
# Outbound 규칙: HTTP (Health Check, 외부 API)
aws ec2 authorize-security-group-egress \
--group-id $ECS_SG \
--protocol tcp \
--port 80 \
--cidr 0.0.0.0/0
- VPC Endpoints (비용 절감, 선택): NAT Gateway 없이 AWS 서비스 접근
# ECR용 VPC Endpoints
aws ec2 create-vpc-endpoint \
--vpc-id $VPC_ID \
--service-name com.amazonaws.ap-northeast-2.ecr.api \
--route-table-ids rtb-xxxxx
aws ec2 create-vpc-endpoint \
--vpc-id $VPC_ID \
--service-name com.amazonaws.ap-northeast-2.ecr.dkr \
--route-table-ids rtb-xxxxx
# S3용 Gateway Endpoint (무료!)
aws ec2 create-vpc-endpoint \
--vpc-id $VPC_ID \
--service-name com.amazonaws.ap-northeast-2.s3 \
--route-table-ids rtb-xxxxx
ECS Cluster 생성
# Cluster 생성
aws ecs create-cluster \
--cluster-name spring-boot-cluster \
--region ap-northeast-2 \
--settings name=containerInsights,value=enabled
# Cluster 상태 확인
aws ecs describe-clusters \
--clusters spring-boot-cluster \
--query 'clusters[0].status'
Task Definition 생성
{
"family": "spring-boot-app",
"networkMode": "awsvpc",
"requiresCompatibilities": [
"FARGATE"
],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "arn:aws:iam::123456789:role/ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::123456789:role/ecsTaskRole",
"containerDefinitions": [
{
"name": "spring-app",
"image": "123456789.dkr.ecr.ap-northeast-2.amazonaws.com/spring-boot-ecs-demo:latest",
"cpu": 512,
"memory": 1024,
"essential": true,
"portMappings": [
{
"containerPort": 8080,
"protocol": "tcp",
"name": "spring-app-8080-tcp"
}
],
"environment": [
{
"name": "SPRING_PROFILES_ACTIVE",
"value": "prod"
},
{
"name": "SERVER_PORT",
"value": "8080"
},
{
"name": "JAVA_OPTS",
"value": "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-create-group": "true",
"awslogs-group": "/ecs/spring-boot-app",
"awslogs-region": "ap-northeast-2",
"awslogs-stream-prefix": "ecs"
}
},
"healthCheck": {
"command": [
"CMD-SHELL",
"curl -f http://localhost:8080/api/health || exit 1"
],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 60
},
"stopTimeout": 120
}
]
}
# Task Definition 등록
aws ecs register-task-definition \
--cli-input-json file://task-definition.json
# 최신 버전 확인
aws ecs describe-task-definition \
--task-definition spring-boot-app \
--query 'taskDefinition.revision'
| 항목 |
값 |
설명 |
| cpu |
512 |
0.5 vCPU |
| memory |
1024 |
1GB RAM |
| newtowkMode |
awsvpc |
Task마다 독립 IP |
| executionRoleArn |
IAM Role |
ECR 이미지 풀링, 로그 전송 |
| taskRoleArn |
IAM Role |
애플리케이션 AWS API 접근 |
| stopTimeout |
120 |
Graceful shutdown 대기 시간 |
| startPeriod |
60 |
Health check 유예 기간 |
- stopTimeout
- Task 종료 시 SIGTERM 신호 전송
- 애플리케이션이 정리 작업 수행
- stopTimeout 초과 시 SIGKILL (강제 종료)
- 스프링 부트 Graceful Shutdown과 연동
Application Load Balancer 생성
- 왜 필요한가?
- Task IP는 동적으로 바뀜
- 여러 Task에 트래픽 분산
- Health check로 비정상 Task 제외
- 서브넷 확인
# Public 서브넷 확인 (최소 2개 AZ)
aws ec2 describe-subnets \
--filters "Name=vpc-id,Values=$VPC_ID" \
--query 'Subnets[?MapPublicIpOnLaunch==`true`].[SubnetId,AvailabilityZ
one]' \
--output table
export SUBNET1=subnet-xxxxx
export SUBNET2=subnet-yyyyy
# Target Group 생성
export TG_ARN=$(aws elbv2 create-target-group \
--name spring-boot-tg \
--protocol HTTP \
--port 8080 \
--vpc-id $VPC_ID \
--target-type ip \
--health-check-enabled \
--health-check-path /api/health \
--health-check-interval-seconds 30 \
--health-check-timeout-seconds 5 \
--healthy-threshold-count 2 \
--unhealthy-threshold-count 2 \
--deregistration-delay 30 \
--query 'TargetGroups[0].TargetGroupArn' \
--output text)
echo "Target Group ARN: $TG_ARN"
# ALB 생성
export ALB_ARN=$(aws elbv2 create-load-balancer \
--name spring-boot-alb \
--subnets $SUBNET1 $SUBNET2 \
--security-groups $ALB_SG \
--scheme internet-facing \
--type application \
--ip-address-type ipv4 \
--query 'LoadBalancers[0].LoadBalancerArn' \
--output text)
# ALB DNS 이름 확인
export ALB_DNS=$(aws elbv2 describe-load-balancers \
--load-balancer-arns $ALB_ARN \
--query 'LoadBalancers[0].DNSName' \
--output text)
echo "ALB DNS: $ALB_DNS"
# HTTP Listener 생성
aws elbv2 create-listener \
--load-balancer-arn $ALB_ARN \
--protocol HTTP \
--port 80 \
--default-actions Type=forward,TargetGroupArn=$TG_ARN
- HTTPS 설정 (ACM 인증서 필요, 선택)
# ACM 인증서가 있다면
aws elbv2 create-listener \
--load-balancer-arn $ALB_ARN \
--protocol HTTPS \
--port 443 \
--certificates CertificateArn=arn:aws:acm:... \
--default-actions Type=forward,TargetGroupArn=$TG_ARN
ECS Service 생성
- Service가 하는 일
- Task 3개를 항상 유지
- 장애 시 자동 복구
- ALB에 자동 등록
- 롤링 업데이트
# Service 생성
aws ecs create-service \
--cluster spring-boot-cluster \
--service-name spring-boot-service \
--task-definition spring-boot-app:1 \
--desired-count 3 \
--launch-type FARGATE \
--platform-version LATEST \
--network-configuration "awsvpcConfiguration={
subnets=[$SUBNET1,$SUBNET2],
securityGroups=[$ECS_SG],
assignPublicIp=ENABLED
}" \
--load-balancers "targetGroupArn=$TG_ARN,
containerName=spring-app,
containerPort=8080" \
--health-check-grace-period-seconds 60 \
--deployment-configuration "maximumPercent=200,
minimumHealthyPercent=100,
deploymentCircuitBreaker={enable=true,rollback=true}" \
--enable-execute-com
| 항목 |
값 |
설명 |
| desired-count |
3 |
유지할 Task 수 |
| platform-version |
LATEST |
Fargate 플랫폼 버전 |
| assignPublicIp |
ENABLED |
Public IP 할당 (필수) |
| maximumPercent |
200 |
배포 시 최대 200% 허용 |
| minimumHealthyPercent |
100 |
최소 100% 유지 |
| deploymentCircuitBreaker |
true |
배포 실패 시 자동 롤백 |
| enable-execute-command |
- |
ECS Exec 활성화 (디버깅) |
- Deployment Circuit Breaker
- 새 버전 배포 실패 시 자동 감지
- 자동으로 이전 버전으로 롤백
- Task가 계속 실패하면 배포 중단
- ECS Exec
- Task 컨테이너에 직접 접속 가능
- 디버깅 및 트러블슈팅용
- SSM Session Manager 기반
# ECS Exec로 Task 접속
aws ecs execute-command \
--cluster spring-boot-cluster \
--task task-id \
--container spring-app \
--interactive \
--command "/bin/sh"
배포 확인
# Service 상세 정보
aws ecs describe-services \
--cluster spring-boot-cluster \
--services spring-boot-service \
--query 'services[0].[status,runningCount,desiredCount,deployments]'
| 항목 |
기대 결과 |
| status |
ACTIVE |
| runningCount |
3 |
| desiredCount |
3 |
| deployments |
PRIMARY (rolloutState: COMPLETED) |
# 실행 중인 Task 목록
aws ecs list-tasks \
--cluster spring-boot-cluster \
--service-name spring-boot-service \
--desired-status RUNNING
# Task 상세 정보
aws ecs describe-tasks \
--cluster spring-boot-cluster \
--tasks $(aws ecs list-tasks --cluster spring-boot-cluster --service-name
spring-boot-service --query 'taskArns[0]' --output text) \
--query 'tasks[0].[lastStatus,healthStatus,containers[0].healthStatus]'
| 항목 |
기대 결과 |
| lastStatus |
RUNNING |
| healthStatus |
HEAlTHY |
# Target Health 확인
aws elbv2 describe-target-health \
--target-group-arn $TG_ARN
// 기대 결과
{
"TargetHealthDescriptions": [
{
"Target": {
"Id": "10.0.1.100",
"Port": 8080,
"AvailabilityZone": "ap-northeast-2a"
},
"HealthCheckPort": "8080",
"TargetHealth": {
"State": "healthy"
}
},
{
"Target": {
"Id": "10.0.2.100",
"Port": 8080,
"AvailabilityZone": "ap-northeast-2b"
},
"HealthCheckPort": "8080",
"TargetHealth": {
"State": "healthy"
}
},
{
"Target": {
"Id": "10.0.3.100",
"Port": 8080,
"AvailabilityZone": "ap-northeast-2c"
},
"HealthCheckPort": "8080",
"TargetHealth": {
"State": "healthy"
}
}
]
}
// http://spring-boot-alb-123456.ap-northeast-2.elb.amazonaws.com/api/hello
{
"message": "Hello from ECS!",
"timestamp": "2024-01-15T15:30:00",
"hostname": "ip-10-0-1-100.ap-northeast-2.compute.internal",
"version": "1.0.0"
}
- 여러번 새로고침 → hostname이 바뀌는 것 확인 → 3개 Task에 로드밸런싱됨
- CloudWatch Logs 확인
# 최신 로그 스트림 확인
aws logs tail /ecs/spring-boot-app --follow
# 또는 특정 Task 로그
aws logs tail /ecs/spring-boot-app --follow --filter-pattern "ip-10-0-1-100"
# CPU 사용률
aws cloudwatch get-metric-statistics \
--namespace AWS/ECS \
--metric-name CPUUtilization \
--dimensions Name=ServiceName,Value=spring-boot-service \
Name=ClusterName,Value=spring-boot-cluster \
--start-time $(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 300 \
--statistics Average
🧩 오토 스케일링 설정 실습
⛳ 목표
트래픽 증가 시 자동으로 Task가 늘어나고, 감소 시 줄어들도록 설정
Auto Scaling 전략
| 언제 Scale Out할까? |
권장 설정 |
| CPU 사용률 70% 이상 |
Min Capacity: 2 (비용 vs 가용성 균형) |
| 메모리 사용률 80% 이상 |
Max Capacity: 10 (폭주 방지) |
| ALB Target 당 요청 수 1000개 이상 |
Target Value: 70% (CPU 사용률) |
Scalable Target 등록
# Scalable Target 등록
aws application-autoscaling register-scalable-target \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/spring-boot-cluster/spring-boot-service \
--min-capacity 2 \
--max-capacity 10 \
--region ap-northeast-2
# 확인
aws application-autoscaling describe-scalable-targets \
--service-namespace ecs \
--resource-ids service/spring-boot-cluster/spring-boot-service
Target Tracking Scaling 정책
aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/spring-boot-cluster/spring-boot-service \
--policy-name cpu-scaling-policy \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 70.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ECSServiceAverageCPUUtilization"
},
"ScaleOutCooldown": 60,
"ScaleInCooldown": 300
}'
aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/spring-boot-cluster/spring-boot-service \
--policy-name memory-scaling-policy \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 80.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ECSServiceAverageMemoryUtilization"
},
"ScaleOutCooldown": 60,
"ScaleInCooldown": 300
}'
| 설정 의미 |
왜 Scale In이 더 긴가? |
| TargetValue: 목표 사용률 (%) |
안정성 우선 |
| ScaleOutCooldown: Scale Out 후 대기 시간 (60초) |
너무 빨리 줄이면 트래픽 급증 시 대응 불가 |
| ScaleInCooldown: Scale In 후 대기 시간 (300초) |
Task 시작에는 시간이 걸림 (이미지 Pull, 초기화 등) |
ALB 요청 기반 Scaling (권장!)
- 왜 이게 더 나을까?
- CPU/메모리는 지연 지표 (이미 늦었을 수 있음)
- 요청 수는 선행 지표 (미리 대응 가능)
- 웹 서비스에 가장 적합
# ALB 요청 수 기반 Scaling
aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/spring-boot-cluster/spring-boot-service \
--policy-name request-count-scaling-policy \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration "{
\"TargetValue\": 1000.0,
\"PredefinedMetricSpecification\": {
\"PredefinedMetricType\": \"ALBRequestCountPerTarget\",
\"ResourceLabel\": \"$(aws elbv2 describe-load-balancers --load-balancer-arns $ALB_ARN --query 'LoadBalancers[0].LoadBalancerArn' --output text | cut -d: -f6)/$(aws elbv2 describe-target-groups --target-group-arns $TG_ARN --query 'TargetGroups[0].TargetGroupArn' --output text | cut -d: -f6)\"
},
\"ScaleOutCooldown\": 60,
\"ScaleInCooldown\": 180
}"
- Target 당 요청 수 1000개 의미
- Task 1개가 초당 1000개 요청 처리
- 초과 시 Task 추가
- 애플리케이션 성능에 따라 조정
Scheduled Scaling (선택)
# 평일 오전 9시 Scale Out (사전 준비)
aws application-autoscaling put-scheduled-action \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/spring-boot-cluster/spring-boot-service \
--scheduled-action-name weekday-morning-scale-out \
--schedule "cron(0 0 * * MON-FRI *)" \
--scalable-target-action MinCapacity=5,MaxCapacity=15
# 평일 저녁 6시 Scale In
aws application-autoscaling put-scheduled-action \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/spring-boot-cluster/spring-boot-service \
--scheduled-action-name weekday-evening-scale-in \
--schedule "cron(0 9 * * MON-FRI *)" \
--scalable-target-action MinCapacity=2,MaxCapacity=10
- Cron 표현식 (UTC 기준)]
- 한국 시간 -9시간
- cron(0 0 * * MON-FRI *) : 매주 월~금 00:00 UTC (한국 09:00)
Scaling 테스트
# 실시간 Task 수 확인
watch -n 5 'aws ecs describe-services \
--cluster spring-boot-cluster \
--services spring-boot-service \
--query "services[0].[desiredCount,runningCount,pendingCount]" \
--output table'
# Scaling 활동 이력
aws application-autoscaling describe-scaling-activities \
--service-namespace ecs \
--resource-id service/spring-boot-cluster/spring-boot-service \
--max-results 10
# CloudWatch 알람 상태
aws cloudwatch describe-alarms \
--alarm-name-prefix TargetTracking
# CPU 사용률 실시간 조회
aws cloudwatch get-metric-statistics \
--namespace AWS/ECS \
--metric-name CPUUtilization \
--dimensions Name=ServiceName,Value=spring-boot-service Name=Clu
sterName,Value=spring-boot-cluster \
--start-time $(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 60 \
--statistics Average
CloudWatch 알람 설정
# CPU 80% 이상 알람
aws cloudwatch put-metric-alarm \
--alarm-name ecs-cpu-high \
--alarm-description "ECS CPU utilization high" \
--metric-name CPUUtilization \
--namespace AWS/ECS \
--statistic Average \
--period 300 \
--threshold 80 \
--comparison-operator GreaterThanThreshold \
--evaluation-periods 2 \
--dimensions Name=ServiceName,Value=spring-boot-service Name=Clu
sterName,Value=spring-boot-cluster \
--alarm-actions arn:aws:sns:ap-northeast-2:123456789:admin-alerts
# Task 2개 이하 알람 (가용성 위험)
aws cloudwatch put-metric-alarm \
--alarm-name ecs-task-count-low \
--alarm-description "ECS task count too low" \
--metric-name RunningTaskCount \
--namespace ECS/ContainerInsights \
--statistic Average \
--period 60 \
--threshold 2 \
--comparison-operator LessThanThreshold \
--evaluation-periods 1 \
--dimensions Name=ServiceName,Value=spring-boot-service Name=Clu
sterName,Value=spring-boot-cluster \
--alarm-actions arn:aws:sns:ap-northeast-2:123456789:admin-alerts
# 응답 시간 1초 이상 알람
aws cloudwatch put-metric-alarm \
--alarm-name alb-response-time-high \
--alarm-description "ALB response time too high" \
--metric-name TargetResponseTime \
--namespace AWS/ApplicationELB \
--statistic Average \
--period 60 \
--threshold 1.0 \
--comparison-operator GreaterThanThreshold \
--evaluation-periods 2 \
--dimensions Name=LoadBalancer,Value=app/spring-boot-alb/... \
--alarm-actions arn:aws:sns:ap-northeast-2:123456789:admin-alerts