콘텐츠로 이동

2026-05-28 — ECS 클러스터를 삭제하지 않고 사용 차단하기 (3단계)

한 줄 요약

ECS 클러스터를 물리적으로 삭제하지 않은 채 누구도 다시 사용할 수 없도록 잠그는 절차. 워크로드 중단(Level 1) → 인프라 잠금(Level 2) → IAM 정책 차단 (Level 3)의 3단으로 강도 조절.

  • 대상 클러스터: compositet3-prod-240531-cluster
  • 계정: 790590917886 (reco-kyle)
  • 리전: us-east-1
  • 작업일: 2026-05-28

1. 배경/목적

해당 클러스터는 워크로드가 거의 종료되었으나, 운영 기록·태그·태스크 정의 등을 유지하기 위해 삭제는 하지 않기로 결정되었다. 동시에 누가 실수로(또는 의도적으로) 다시 태스크를 띄우는 일도 막아야 했다. → "사용 안 됨"을 강제하기 위한 다층 방어.

클러스터의 구성 요소

이 클러스터가 "사용"될 수 있는 모든 경로를 막아야 한다.

flowchart LR
  user[사람/CI] -->|UpdateService| svc[Service]
  user -->|RunTask| task[Task]
  svc --> task
  task --> cp[Capacity Provider]
  cp --> asg[Auto Scaling Group]
  asg --> ec2[EC2 instances]

→ 막아야 할 layer: Service → Task / Capacity Provider → ASG / IAM 호출 자체.


2. 절차

Level 1 — 워크로드 즉시 중단 (되돌리기 쉬움)

모든 서비스를 desiredCount=0으로 만들고, 진행 중인 태스크를 강제 종료.

PROFILE=reco-kyle; REGION=us-east-1
CLUSTER=compositet3-prod-240531-cluster

# 1) 활성 서비스 desiredCount=0
aws ecs update-service --profile $PROFILE --region $REGION \
  --cluster $CLUSTER --service compositet3-prod-240531-ec2-spot \
  --desired-count 0
# (desired=0이 아닌 서비스가 더 있으면 모두 같은 방식으로 0으로 만들 것)

# 2) 현재 PROVISIONING/RUNNING 중인 태스크 식별
aws ecs list-tasks --profile $PROFILE --region $REGION --cluster $CLUSTER

# 3) 태스크 강제 stop (위 명령으로 얻은 taskId)
aws ecs stop-task --profile $PROFILE --region $REGION \
  --cluster $CLUSTER --task <TASK_ID> \
  --reason "Manual lockdown: cluster being decommissioned (no-delete)"

→ 약점: 누가 desiredCount를 다시 올리면 다시 동작. ASG도 그대로라 EC2가 다시 떠 버린다. 그래서 Level 2가 필요.

Level 2 — 인프라 잠금

ASG의 min/max/desired모두 0으로 고정해 EC2 인스턴스 자체가 못 뜨도록 하고, Capacity Provider의 managed scaling도 비활성화한다.

PROFILE=reco-kyle; REGION=us-east-1

# ASG min=max=desired=0
for ASG in compositet3-prod-240531-ondemand-autoscaling-group \
           compositet3-prod-240531-spot-autoscaling-group; do
  aws autoscaling update-auto-scaling-group --profile $PROFILE --region $REGION \
    --auto-scaling-group-name "$ASG" \
    --min-size 0 --max-size 0 --desired-capacity 0
done

# Capacity Provider의 managed scaling/termination 둘 다 DISABLED
# (둘 중 하나만 DISABLED 시도하면 ClientException: "Managed scaling can't be
#  disabled if managed termination is enabled." 발생 — 반드시 함께 꺼야 함)
for CP in compositet3-prod-240531-ondemand-asg-capacity-provider \
          compositet3-prod-240531-spot-asg-capacity-provider; do
  aws ecs update-capacity-provider --profile $PROFILE --region $REGION \
    --name "$CP" \
    --auto-scaling-group-provider '{"managedScaling":{"status":"DISABLED"},"managedTerminationProtection":"DISABLED"}'
done

update-capacity-provider는 비동기

updateStatusUPDATE_IN_PROGRESS로 바뀐 뒤 잠시(보통 10–30초) 후 UPDATE_COMPLETE가 된다. 같은 CP에 연속 호출하면 두 번째 호출이 UPDATE_FAILED가 되니, 한 호출의 UPDATE_COMPLETE를 확인한 뒤 다음 호출을 해야 한다.

Level 3 — IAM 정책으로 차단 (가장 강함)

해당 클러스터를 대상으로 한 호출 자체를 Deny로 막는다.

PROFILE=oldplicar-kyle   # ← IAM은 보통 별도 권한이 필요
ACCOUNT_ID=790590917886
CLUSTER_ARN="arn:aws:ecs:us-east-1:${ACCOUNT_ID}:cluster/compositet3-prod-240531-cluster"

cat > /tmp/deny-ecs-cluster.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyRunTaskOnCluster",
      "Effect": "Deny",
      "Action": ["ecs:RunTask", "ecs:StartTask"],
      "Resource": "*",
      "Condition": {"ArnEquals": {"ecs:cluster": "${CLUSTER_ARN}"}}
    },
    {
      "Sid": "DenyServiceMutationsOnCluster",
      "Effect": "Deny",
      "Action": [
        "ecs:CreateService", "ecs:UpdateService",
        "ecs:CreateTaskSet", "ecs:UpdateTaskSet",
        "ecs:UpdateServicePrimaryTaskSet"
      ],
      "Resource": "*",
      "Condition": {"ArnEquals": {"ecs:cluster": "${CLUSTER_ARN}"}}
    },
    {
      "Sid": "DenyClusterCapacityChanges",
      "Effect": "Deny",
      "Action": [
        "ecs:PutClusterCapacityProviders",
        "ecs:UpdateCluster",
        "ecs:UpdateCapacityProvider"
      ],
      "Resource": [
        "${CLUSTER_ARN}",
        "arn:aws:ecs:us-east-1:${ACCOUNT_ID}:capacity-provider/compositet3-prod-240531-ondemand-asg-capacity-provider",
        "arn:aws:ecs:us-east-1:${ACCOUNT_ID}:capacity-provider/compositet3-prod-240531-spot-asg-capacity-provider"
      ]
    }
  ]
}
EOF

aws iam create-policy --profile $PROFILE \
  --policy-name DenyEcsCluster-compositet3-prod-240531 \
  --description "Deny RunTask/StartTask/CreateService/UpdateService and capacity changes on the decommissioned cluster." \
  --policy-document file:///tmp/deny-ecs-cluster.json

생성 후 적용 대상에 attach:

# 사용자/역할/그룹에 attach
aws iam attach-user-policy   --user-name <user>  --policy-arn <POLICY_ARN>
aws iam attach-role-policy   --role-name <role>  --policy-arn <POLICY_ARN>
aws iam attach-group-policy  --group-name <grp>  --policy-arn <POLICY_ARN>

본인 권한에 영향을 줄 수 있다

Deny 정책은 명시적 Allow를 이기므로, 자기 자신에게 attach하면 본인도 잠긴다. Organization SCP로 적용할 때는 루트/관리자 계정/긴급 복구 역할은 반드시 제외되도록 조건을 걸 것.


3. 검증

PROFILE=reco-kyle; REGION=us-east-1
CLUSTER=compositet3-prod-240531-cluster

# 클러스터: 태스크 0, 인스턴스 0
aws ecs describe-clusters --profile $PROFILE --region $REGION --clusters $CLUSTER \
  --query 'clusters[0].{runningTasks:runningTasksCount,pendingTasks:pendingTasksCount,activeServices:activeServicesCount,containerInstances:registeredContainerInstancesCount}'

# 모든 서비스 desired=0
for svc in compositet3-prod-240531-ec2-spot compositet3-prod-240531-external compositet3-prod-240531-ec2-ondemand; do
  aws ecs describe-services --profile $PROFILE --region $REGION --cluster $CLUSTER --services "$svc" \
    --query 'services[0].{name:serviceName,desired:desiredCount,running:runningCount}'
done

# ASG: min=max=desired=0
aws autoscaling describe-auto-scaling-groups --profile $PROFILE --region $REGION \
  --auto-scaling-group-names \
    compositet3-prod-240531-ondemand-autoscaling-group \
    compositet3-prod-240531-spot-autoscaling-group \
  --query 'AutoScalingGroups[].{name:AutoScalingGroupName,min:MinSize,max:MaxSize,desired:DesiredCapacity,instances:length(Instances)}'

# Capacity Provider: managed scaling/termination DISABLED, updateStatus=UPDATE_COMPLETE
aws ecs describe-capacity-providers --profile $PROFILE --region $REGION \
  --capacity-providers \
    compositet3-prod-240531-ondemand-asg-capacity-provider \
    compositet3-prod-240531-spot-asg-capacity-provider \
  --query 'capacityProviders[].{name:name,managedScaling:autoScalingGroupProvider.managedScaling.status,managedTermination:autoScalingGroupProvider.managedTerminationProtection,updateStatus:updateStatus}'

성공 기준:

  • runningTasks, pendingTasks, containerInstances 모두 0
  • 모든 서비스의 desiredCount/runningCount 모두 0
  • 두 ASG 모두 min=max=desired=0, instances=0
  • 두 Capacity Provider 모두 managedScaling=DISABLED, managedTerminationProtection=DISABLED, updateStatus=UPDATE_COMPLETE
  • IAM 정책 attach 후, 차단된 사용자/역할로 RunTask/UpdateService 시도 시 AccessDeniedException 발생

4. 함정과 노하우

  • update-capacity-provider 응답에 표시되는 값은 갱신 전 값일 수 있다. 실제 적용 여부는 describe-capacity-providersupdateStatus로 확인할 것.
  • managed scaling만 끄려고 하면 실패한다. managed termination이 ENABLED인 상태에서는 managed scaling을 DISABLED로 못 바꾼다. 함께 DISABLED로 변경할 것.
  • ASG가 min=0,max=0이어도 누가 update-service로 desired를 올리면 ECS는 태스크 placement를 시도한다. 인스턴스가 없으니 결국 실패하지만, 에러 이벤트가 쌓이고 알람을 만들 수 있다. 그래서 IAM Deny까지 가는 게 가장 안전.
  • CodePipeline/EventBridge로 트리거되는 RunTask가 있는지 별도 확인 필요. 이번 케이스에는 없었지만 일반적으로 체크할 것.

5. 롤백

각 레벨의 역순으로 풀면 된다.

# Level 3: IAM Deny 정책 detach + 삭제
aws iam detach-user-policy --user-name <user> --policy-arn <POLICY_ARN>
aws iam delete-policy --policy-arn <POLICY_ARN>

# Level 2: Capacity Provider 재활성화
for CP in compositet3-prod-240531-ondemand-asg-capacity-provider \
          compositet3-prod-240531-spot-asg-capacity-provider; do
  aws ecs update-capacity-provider --profile reco-kyle --region us-east-1 \
    --name "$CP" \
    --auto-scaling-group-provider '{"managedScaling":{"status":"ENABLED","targetCapacity":100},"managedTerminationProtection":"ENABLED"}'
done

# ASG 용량 복구 (원래 값으로 — 사전에 메모해둘 것)
for ASG in compositet3-prod-240531-ondemand-autoscaling-group \
           compositet3-prod-240531-spot-autoscaling-group; do
  aws autoscaling update-auto-scaling-group --profile reco-kyle --region us-east-1 \
    --auto-scaling-group-name "$ASG" \
    --min-size <ORIG_MIN> --max-size <ORIG_MAX> --desired-capacity <ORIG_DESIRED>
done

# Level 1: 서비스 desiredCount 복구
aws ecs update-service --profile reco-kyle --region us-east-1 \
  --cluster compositet3-prod-240531-cluster \
  --service compositet3-prod-240531-ec2-spot \
  --desired-count <ORIG_DESIRED>

원래 값 기록

Level 2를 적용하기 전에 ASG/CP의 원래 값을 describe-auto-scaling-groups, describe-capacity-providers로 떠서 별도 위치(예: 이 글의 코멘트, Linear 티켓 등)에 저장해두면 롤백이 쉽다.