Published on

Terraform EKS 삭제 시 ResourceInUseException 해결

Authors

서론

Terraform로 AWS EKS 클러스터를 만들고 나서 terraform destroy를 실행하면, 의외로 가장 자주 마주치는 실패가 ResourceInUseException입니다. 메시지는 대개 “리소스가 아직 사용 중이라 삭제할 수 없다”인데, 문제는 무엇이 사용 중인지가 한 번에 드러나지 않는다는 점입니다. EKS는 VPC, 보안 그룹, ENI, 로드밸런서, 타깃 그룹, IAM/OIDC, CloudWatch 로그 그룹 등 여러 AWS 리소스와 쿠버네티스 오브젝트가 얽혀 있고, 그중 일부는 컨트롤 플레인/노드/애드온이 비정상 상태일 때 정리(garbage collection)가 멈춰서 “삭제 불가” 상태를 오래 유지합니다.

이 글은 Terraform EKS 삭제 실패를 원인별로 빠르게 좁히는 진단법, 안전한 삭제 순서, Terraform 설정으로 재발 방지하는 방법을 실전 관점에서 정리합니다.

ResourceInUseException이 나는 대표 지점

삭제 단계에서 자주 터지는 리소스는 다음과 같습니다.

  1. EKS Cluster 자체 삭제 실패: aws_eks_cluster 삭제 중 ResourceInUseException (대개 노드/ENI/애드온/연결된 네트워크가 남아있음)
  2. 보안 그룹(Security Group) 삭제 실패: “has a dependent object” 혹은 사용 중인 ENI가 남아있는 상태
  3. 서브넷/라우트테이블/IGW/NAT 삭제 실패: ENI 또는 LB가 서브넷을 붙잡고 있음
  4. 로드밸런서/타깃 그룹 삭제 실패: Kubernetes Service(LoadBalancer), Ingress(ALB/NLB), AWS Load Balancer Controller가 만든 리소스 잔존
  5. OIDC Provider/IRSA 관련 리소스: IAM 역할/정책, OIDC provider가 다른 리소스에 의해 참조됨

핵심은 “EKS를 지우기 전에 쿠버네티스가 만든 AWS 리소스를 먼저 지워야 한다”는 점입니다.

1) 가장 먼저: 쿠버네티스 쪽 LoadBalancer/Ingress 잔존 확인

EKS에서 가장 흔한 잠금(lock)은 Service type=LoadBalancer 또는 **Ingress(ALB/NLB)**가 만든 AWS 리소스입니다. 컨트롤러가 정상이라면 Service/Ingress 삭제 시 AWS 리소스도 같이 삭제되지만, 컨트롤러가 죽었거나 권한(IRSA)이 꼬이면 AWS 리소스가 남습니다.

확인 명령

kubectl get svc -A | egrep -i "LoadBalancer|EXTERNAL-IP"
kubectl get ingress -A
kubectl get pods -n kube-system

AWS Load Balancer Controller를 쓰는 환경이라면 다음도 확인합니다.

kubectl -n kube-system get deploy aws-load-balancer-controller
kubectl -n kube-system logs deploy/aws-load-balancer-controller --tail=200

만약 컨트롤러 로그에 AccessDenied가 보이면 IRSA 문제로 리소스 정리가 안 되었을 가능성이 큽니다. 이 경우 IRSA 점검은 아래 글이 바로 도움이 됩니다.

우선 조치

  1. Service/Ingress부터 삭제
kubectl delete ingress -A --all
kubectl delete svc -A --field-selector spec.type=LoadBalancer
  1. 삭제 후에도 AWS 리소스가 남는다면 AWS에서 직접 확인
aws elbv2 describe-load-balancers
aws elbv2 describe-target-groups
aws elbv2 describe-listeners

남아있는 LB가 특정 보안 그룹/서브넷을 계속 물고 있으면, 그 보안 그룹/서브넷은 Terraform이 삭제하지 못합니다.

2) ENI(Elastic Network Interface)가 보안그룹/서브넷을 붙잡는 경우

EKS 삭제 실패의 2대 원인은 ENI가 남아있는 상태입니다. 특히 다음 상황에서 ENI 정리가 늦거나 멈춥니다.

  • 노드 그룹 삭제가 제대로 끝나지 않았음
  • VPC CNI가 비정상(예: ipamd 문제)이라 ENI detach가 지연
  • Private endpoint/보안 설정 문제로 컨트롤 플레인과 통신이 불안정

ENI 확인

aws ec2 describe-network-interfaces \
  --filters Name=description,Values='*Amazon EKS*' \
  --query 'NetworkInterfaces[].{Id:NetworkInterfaceId,Status:Status,Desc:Description,Subnet:SubnetId,SG:Groups[].GroupId,Att:Attachment.InstanceId}' \
  --output table

또는 특정 보안 그룹이 왜 삭제가 안 되는지 역으로 찾습니다.

SG_ID=sg-xxxxxxxx
aws ec2 describe-network-interfaces \
  --filters Name=group-id,Values=$SG_ID \
  --query 'NetworkInterfaces[].{Id:NetworkInterfaceId,Desc:Description,Status:Status,Att:Attachment.InstanceId}' \
  --output table

정리 전략

  • 정석: 노드 그룹/ASG가 완전히 삭제되도록 기다린 뒤 ENI가 자동으로 사라지는지 확인
  • 수동: 더 이상 붙어있지 않은(attachment 없는) ENI는 삭제
ENI_ID=eni-xxxxxxxx
aws ec2 delete-network-interface --network-interface-id $ENI_ID

주의: attachment가 있는 ENI를 억지 삭제하려 하면 실패합니다. 먼저 인스턴스/노드/ASG를 제거해야 합니다.

3) EKS Managed Node Group/ASG가 남아서 클러스터 삭제가 막히는 경우

Terraform에서 EKS 모듈을 사용하면 노드 그룹이 별도 리소스로 생성됩니다. terraform destroy가 클러스터부터 지우려고 시도하면, 노드/ENI가 남아 ResourceInUseException이 납니다.

권장 삭제 순서(개념)

  1. Ingress/Service(LB) 제거 → LB/TargetGroup 정리
  2. 애드온(특히 AWS LB Controller, ExternalDNS 등) 제거
  3. Node Group/ASG 제거
  4. EKS Cluster 제거
  5. VPC/보안그룹/서브넷 제거

Terraform에서 의존성 보강(예시)

아래는 “클러스터보다 노드 그룹을 먼저 없애도록” 유도하는 패턴입니다. (환경마다 리소스명이 다르므로 개념적으로 참고하세요.)

resource "aws_eks_cluster" "this" {
  name     = var.cluster_name
  role_arn = aws_iam_role.eks_cluster.arn

  vpc_config {
    subnet_ids = var.subnet_ids
  }

  depends_on = [
    aws_iam_role_policy_attachment.eks_cluster_AmazonEKSClusterPolicy,
  ]
}

resource "aws_eks_node_group" "this" {
  cluster_name    = aws_eks_cluster.this.name
  node_group_name = "default"
  node_role_arn   = aws_iam_role.eks_node.arn
  subnet_ids      = var.subnet_ids

  scaling_config {
    desired_size = 2
    max_size     = 3
    min_size     = 0
  }

  # 노드 그룹이 먼저 삭제되도록 추가 의존성은 보통 필요 없지만,
  # 모듈 조합에 따라 lifecycle/depends_on을 명시해 destroy 순서를 안정화할 수 있습니다.
}

추가로, 노드 그룹에서 min_size=0을 허용해두면 “노드가 남아있어서 drain이 안 됨” 같은 상황에서 복구가 쉬워집니다.

4) aws-auth, IRSA, 애드온이 꼬여서 컨트롤러가 리소스 정리를 못하는 경우

ResourceInUseException 자체는 AWS API의 결과지만, 그 원인이 쿠버네티스 컨트롤러(예: AWS Load Balancer Controller, ExternalDNS, Cluster Autoscaler)가 권한 문제로 AWS 리소스를 못 지우는 경우가 많습니다.

  • IRSA(OIDC) 설정 불일치
  • 서비스어카운트 annotation의 role arn 오타
  • OIDC provider가 삭제되었거나 thumbprint/issuer mismatch

이 경우 “쿠버네티스 오브젝트는 지웠는데 AWS 리소스가 남는” 현상이 발생합니다. ExternalDNS 사례지만 IRSA 진단 관점은 동일합니다.

5) 삭제 중 kubectl이 안 먹어서 정리 자체가 막히는 경우(네트워크/권한)

클러스터가 이미 불안정하거나 API 서버 접근이 깨지면, kubectl delete로 Ingress/Service를 지우는 단계부터 막힙니다. 특히 사설 엔드포인트, 보안 그룹 변경, NACL/MTU 문제 등으로 kube-apiserver 연결이 타임아웃 나면 정리 작업이 진행되지 않습니다.

이런 상황에서는 우회적으로 AWS 리소스를 먼저 정리(ELBv2, TargetGroup, ENI 등)한 뒤, 마지막에 EKS를 삭제하는 접근이 필요합니다.

6) 실전 진단 플로우(체크리스트)

아래 순서로 보면 “무엇이 사용 중인지”를 빠르게 특정할 수 있습니다.

6.1 Terraform 에러에서 어떤 리소스가 막혔는지 확인

terraform destroy -auto-approve
# 또는
terraform apply -destroy

에러가 aws_security_group, aws_subnet, aws_eks_cluster, aws_iam_openid_connect_provider 중 어디에서 발생하는지 먼저 봅니다.

6.2 로드밸런서 잔존 여부 확인

kubectl get svc -A | grep LoadBalancer || true
kubectl get ingress -A || true
aws elbv2 describe-load-balancers --output table

6.3 ENI가 어떤 SG/Subnet을 물고 있는지 확인

aws ec2 describe-network-interfaces \
  --filters Name=description,Values='*Amazon EKS*' \
  --output table

6.4 노드 그룹/ASG 잔존 확인

aws eks list-nodegroups --cluster-name <CLUSTER>
aws autoscaling describe-auto-scaling-groups --output table

6.5 CloudFormation으로 만든 리소스가 남았는지(컨트롤러가 CFN을 쓰는 경우)

일부 애드온/헬름 차트가 내부적으로 CFN 스택을 만들기도 합니다.

aws cloudformation list-stacks \
  --stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE ROLLBACK_COMPLETE \
  --output table

7) “삭제가 항상 잘 되게” 만드는 Terraform 운영 팁

7.1 쿠버네티스 리소스도 Terraform으로 관리하기

Service/Ingress를 kubectl로만 만들면, 삭제 시점에 사람이 놓치기 쉽습니다. 가능하면 kubernetes provider나 Helm provider로 Ingress/Service/Controller를 Terraform 상태에 포함해, destroy 시 함께 제거되도록 만듭니다.

provider "helm" {
  kubernetes {
    host                   = data.aws_eks_cluster.this.endpoint
    cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data)
    token                  = data.aws_eks_cluster_auth.this.token
  }
}

resource "helm_release" "aws_lb_controller" {
  name       = "aws-load-balancer-controller"
  repository = "https://aws.github.io/eks-charts"
  chart      = "aws-load-balancer-controller"
  namespace  = "kube-system"

  set {
    name  = "clusterName"
    value = var.cluster_name
  }
}

이렇게 해두면 컨트롤러가 먼저 내려가고, 그 다음 Ingress/Service를 내리는 순서를 설계하기 쉬워집니다(물론 의존성은 별도로 조정 필요).

7.2 destroy 전에 “LB 제거 훅”을 두기

조직 정책상 쿠버네티스 리소스를 Terraform에 넣기 어렵다면, null_resource + local-exec로 사전 정리 단계를 넣는 방법도 있습니다.

resource "null_resource" "pre_destroy_cleanup" {
  triggers = {
    cluster = var.cluster_name
  }

  provisioner "local-exec" {
    when    = destroy
    command = <<EOT
set -e
kubectl delete ingress -A --all || true
kubectl delete svc -A --field-selector spec.type=LoadBalancer || true
sleep 30
EOT
  }
}

주의: 이 방식은 로컬 실행 환경에 kubectl/kubeconfig가 준비되어 있어야 하고, 파이프라인 환경에서는 인증/네트워크 이슈로 실패할 수 있습니다.

7.3 삭제 타임아웃을 현실적으로 늘리기

EKS/노드 그룹 삭제는 생각보다 오래 걸립니다. Terraform 리소스별 timeouts를 늘리면 “사실은 진행 중인데 타임아웃으로 실패”를 줄일 수 있습니다.

resource "aws_eks_node_group" "this" {
  # ...
  timeouts {
    delete = "60m"
  }
}

resource "aws_eks_cluster" "this" {
  # ...
  timeouts {
    delete = "60m"
  }
}

8) 최후의 수단: 남은 AWS 리소스를 강제 정리한 뒤 Terraform state 정리

이미 일부 리소스를 콘솔/CLI로 수동 삭제했다면 Terraform state와 실제가 어긋납니다. 이 경우 다음 중 하나가 필요합니다.

  • terraform state rm <address>로 상태에서 제거
  • 또는 import로 다시 맞춘 뒤 destroy

예:

terraform state list | grep elb
terraform state rm module.eks.aws_security_group_rule.some_rule

이 단계는 실수하면 다른 환경 리소스까지 상태에서 빠질 수 있으므로, 워크스페이스/백엔드 분리 상태를 재확인하고 진행해야 합니다.

결론

Terraform EKS 삭제의 ResourceInUseException은 “EKS가 안 지워진다”가 아니라, 대부분 쿠버네티스가 만든 AWS 리소스(LB/TargetGroup/ENI/SG)가 남아서 VPC 계층 삭제가 막히는 문제입니다. 따라서 해결의 핵심은

  1. Ingress/Service(LoadBalancer)부터 제거해 LB 계층을 먼저 정리하고,
  2. ENI가 남아있는지 추적해 SG/Subnet 잠금을 풀고,
  3. 노드 그룹/ASG 삭제 완료를 확인한 뒤,
  4. 마지막에 클러스터와 VPC를 삭제하는 순서를 지키는 것입니다.

추가로 IRSA/컨트롤러 권한 문제나 apiserver 접근 장애가 있으면 “자동 정리”가 멈추므로, 원인 로그(AccessDenied, i/o timeout)를 먼저 해결하는 것이 전체 삭제 시간을 크게 줄입니다.