학습 목표

시나리오

컨테이너 이미지로 배포된 API 서비스를 올립니다.

세 번째와 네 번째 요구가 서로 다른 역할을 필요로 합니다. 애플리케이션 코드가 S3를 호출하는 것과, ECS 에이전트가 ECR에서 이미지를 받아오는 것은 주체가 다릅니다.

아키텍처 개요

태스크 역할과 태스크 실행 역할
항목 태스크 역할(taskRoleArn) 태스크 실행 역할(executionRoleArn)
누가 쓰는가 컨테이너 안의 애플리케이션 코드 ECS 에이전트(플랫폼)
무엇에 쓰는가 Amazon S3·DynamoDB 등 AWS 서비스 호출 ECR에서 이미지 가져오기, 로그 전송, 비밀 주입
누락되면 애플리케이션이 실행 중에 권한 오류를 낸다 태스크가 시작조차 되지 않는다
신뢰 주체 ecs-tasks.amazonaws.com ecs-tasks.amazonaws.com
EC2 시작 유형과 Fargate의 책임 경계
항목 EC2 시작 유형 Fargate
컨테이너 인스턴스 고객이 관리(패치·용량·확장) 없다
자원 지정 인스턴스 타입 선택 태스크 단위 CPU·메모리 조합
네트워크 모드 bridge·host·awsvpc·none awsvpc만
보안 그룹 인스턴스 단위(또는 awsvpc면 태스크 단위) 태스크마다 네트워크 인터페이스와 보안 그룹
고르는 신호 GPU·특수 인스턴스·비용 최적화가 중요할 때 LEAST operational overhead

구성 단계

1 · 역할 두 개

ecs-tasks-trust.json — 두 역할이 공통으로 쓰는 신뢰 정책
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "ecs-tasks.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}
create-roles.sh — 실행 역할과 태스크 역할 (버킷 이름은 예시입니다)
# 실행 역할 — ECR 이미지 가져오기와 로그 전송
aws iam create-role \
  --role-name Lab09TaskExecutionRole \
  --assume-role-policy-document file://ecs-tasks-trust.json

aws iam attach-role-policy \
  --role-name Lab09TaskExecutionRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

# 태스크 역할 — 애플리케이션이 쓰는 권한만 최소로
aws iam create-role \
  --role-name Lab09TaskRole \
  --assume-role-policy-document file://ecs-tasks-trust.json

aws iam put-role-policy \
  --role-name Lab09TaskRole \
  --policy-name WriteUploads \
  --policy-document '{
      "Version": "2012-10-17",
      "Statement": [{
        "Effect": "Allow",
        "Action": ["s3:PutObject"],
        "Resource": "arn:aws:s3:::example-lab09-uploads/*"
      }]
    }'

2 · 태스크 정의

task-definition.json — Fargate용 태스크 정의 (이미지와 ARN은 예시입니다)
{
  "family": "lab09-api",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "executionRoleArn": "arn:aws:iam::123456789012:role/Lab09TaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::123456789012:role/Lab09TaskRole",
  "containerDefinitions": [
    {
      "name": "api",
      "image": "123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/lab09-api:1.0.0",
      "essential": true,
      "portMappings": [
        { "containerPort": 8080, "protocol": "tcp" }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/lab09-api",
          "awslogs-region": "ap-northeast-2",
          "awslogs-stream-prefix": "api"
        }
      }
    }
  ]
}
register-task.sh — 로그 그룹을 먼저 만들고 태스크 정의 등록
aws logs create-log-group --log-group-name /ecs/lab09-api
aws logs put-retention-policy --log-group-name /ecs/lab09-api --retention-in-days 14

aws ecs register-task-definition \
  --cli-input-json file://task-definition.json \
  --query 'taskDefinition.[family,revision,cpu,memory]' --output table

3 · 서비스 만들기

create-service.sh — 프라이빗 서브넷에 배치하고 대상 그룹에 연결 (ID는 예시입니다)
aws ecs create-cluster --cluster-name lab09-cluster

aws ecs create-service \
  --cluster lab09-cluster \
  --service-name lab09-api-svc \
  --task-definition lab09-api \
  --desired-count 2 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={
      subnets=[subnet-0prv00000000000a,subnet-0prv00000000000b],
      securityGroups=[sg-0task00000000000],
      assignPublicIp=DISABLED
    }" \
  --load-balancers "targetGroupArn=$TG_ARN,containerName=api,containerPort=8080" \
  --health-check-grace-period-seconds 60

대상 그룹은 대상 유형이 ip여야 합니다. awsvpc 모드에서는 태스크마다 네트워크 인터페이스와 사설 IP가 생기고, 로드 밸런서는 인스턴스가 아니라 그 IP를 대상으로 등록하기 때문입니다.

4 · 서비스 오토스케일링

service-scaling.sh — 확장 대상 등록과 대상 추적 정책
aws application-autoscaling register-scalable-target \
  --service-namespace ecs \
  --resource-id service/lab09-cluster/lab09-api-svc \
  --scalable-dimension ecs:service:DesiredCount \
  --min-capacity 2 \
  --max-capacity 10

aws application-autoscaling put-scaling-policy \
  --service-namespace ecs \
  --resource-id service/lab09-cluster/lab09-api-svc \
  --scalable-dimension ecs:service:DesiredCount \
  --policy-name lab09-cpu-target \
  --policy-type TargetTrackingScaling \
  --target-tracking-scaling-policy-configuration '{
      "TargetValue": 60.0,
      "PredefinedMetricSpecification": {
        "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
      },
      "ScaleInCooldown": 300,
      "ScaleOutCooldown": 60
    }'

ECS 서비스에 쓸 수 있는 사전 정의 지표에는 ECSServiceAverageCPUUtilization과 ECSServiceAverageMemoryUtilization이 있습니다. 로드 밸런서 뒤라면 대상당 요청 수를 사용자 지정 지표로 쓸 수도 있습니다.

검증 방법

verify.sh — 서비스·태스크 상태와 실패 원인 확인
aws ecs describe-services \
  --cluster lab09-cluster \
  --services lab09-api-svc \
  --query 'services[0].[status,desiredCount,runningCount,pendingCount,events[0:3].message]' \
  --output json

aws ecs list-tasks --cluster lab09-cluster --service-name lab09-api-svc \
  --query 'taskArns' --output text

aws ecs describe-tasks \
  --cluster lab09-cluster \
  --tasks $(aws ecs list-tasks --cluster lab09-cluster --service-name lab09-api-svc --query 'taskArns[0]' --output text) \
  --query 'tasks[0].[lastStatus,healthStatus,stoppedReason,containers[].[name,lastStatus,reason]]' \
  --output json
증상으로 원인을 좁히는 표
증상 가장 흔한 원인
태스크가 PENDING에서 멈춘다 이미지 가져오기 경로 없음(NAT나 엔드포인트 부재) 또는 실행 역할 권한 부족
태스크가 시작되고 곧 종료된다 컨테이너 프로세스가 즉시 종료됨. 로그를 확인한다
대상 그룹에 등록되지 않는다 대상 유형이 ip가 아니거나 컨테이너 포트 불일치
상태 확인이 계속 실패한다 보안 그룹이 로드 밸런서에서 컨테이너 포트를 허용하지 않음
애플리케이션이 권한 오류를 낸다 태스크 역할에 해당 권한이 없음
로그가 보이지 않는다 로그 그룹이 없거나 실행 역할에 로그 권한 없음
check-logs.sh — 컨테이너 로그 확인
aws logs describe-log-streams \
  --log-group-name /ecs/lab09-api \
  --order-by LastEventTime --descending --max-items 3 \
  --query 'logStreams[].logStreamName' --output text

비용 고려

요금이 발생하는 항목
항목 성격
Fargate 태스크 요청한 CPU와 메모리에 대해 실행 시간만큼
최소 태스크 수 가용성을 위한 하한이 곧 상시 비용의 바닥이다
로드 밸런서 가동 시간과 처리 용량 단위
NAT 게이트웨이 이미지 가져오기와 외부 호출이 지나면 처리량 항목이 커진다
VPC 인터페이스 엔드포인트 엔드포인트 항목이 생기지만 NAT 처리량을 줄인다
CloudWatch Logs 수집량과 보존 기간
ECR 저장 이미지 저장 용량. 오래된 태그는 수명 주기 정책으로 정리한다

Fargate는 요청한 자원에 대해 과금되므로 실제 사용량보다 크게 잡으면 그대로 낭비입니다. 지표를 보고 CPU와 메모리를 조정하는 것이 가장 직접적인 절감입니다.

프로덕션 고려사항

정리 — 리소스 삭제

cleanup.sh — 서비스를 0으로 줄이고 순서대로 삭제
aws application-autoscaling deregister-scalable-target \
  --service-namespace ecs \
  --resource-id service/lab09-cluster/lab09-api-svc \
  --scalable-dimension ecs:service:DesiredCount

aws ecs update-service \
  --cluster lab09-cluster \
  --service lab09-api-svc \
  --desired-count 0

aws ecs delete-service \
  --cluster lab09-cluster \
  --service lab09-api-svc \
  --force

aws ecs delete-cluster --cluster lab09-cluster

aws logs delete-log-group --log-group-name /ecs/lab09-api

aws iam delete-role-policy --role-name Lab09TaskRole --policy-name WriteUploads
aws iam delete-role --role-name Lab09TaskRole
aws iam detach-role-policy \
  --role-name Lab09TaskExecutionRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
aws iam delete-role --role-name Lab09TaskExecutionRole

태스크 정의는 등록을 취소해도 목록에는 남습니다. 요금이 발생하지는 않지만, 필요하면 개별 리비전을 등록 취소해 정리합니다.

자주 하는 실수

자주 나오는 실수와 증상
실수 증상 고치는 법
두 역할을 하나로 씀 권한이 과하거나 태스크가 시작되지 않는다 실행 역할과 태스크 역할을 분리한다
프라이빗 서브넷에 이미지 경로 없음 태스크가 PENDING에서 실패한다 NAT 또는 ECR·로그·S3 엔드포인트를 추가한다
대상 그룹 유형이 instance 태스크가 등록되지 않는다 awsvpc 모드에서는 ip 유형을 쓴다
CPU·메모리 조합이 유효하지 않음 태스크 정의 등록이 거부된다 Fargate가 허용하는 조합을 확인한다
로그 그룹을 만들지 않음 로그가 남지 않거나 태스크가 실패한다 로그 그룹을 미리 만들고 보존 기간을 설정한다
이미지 태그가 latest 어떤 버전이 돌고 있는지 알 수 없다 불변 태그를 쓰고 배포마다 갱신한다
비밀을 환경 변수 평문으로 태스크 정의를 조회하면 노출된다 secrets로 주입하고 실행 역할에 읽기 권한을 준다

이어서 볼 곳

공식 문서 출처