[Terraform] 운영 서버 무중단 인프라 리팩토링: 프로젝트 간 참조 제거
공유 NAT 인스턴스를 homepage와 ssuport에서 분리하고, Terraform state만 옮겨 운영 환경 다운타임 없이 무중단 마이그레이션한 과정을 기록합니다.
초기 IT지원위원회의 인프라는 homepage라는 단일 서비스만 운영했기에, VPC, 서브넷, NAT 인스턴스 같은 모든 인프라 자원이 homepage/prod 폴더 하나에 집중되어 있었습니다.
문제 상황
새로운 서비스인 ssuport도 프라이빗 서브넷에서 외부 인터넷과 통신하려면 NAT 인스턴스가 필요했습니다. 새로 NAT를 띄우자니 비용이 낭비되고, 기존 NAT를 같이 쓰자니 공유 자원(NAT)이 특정 서비스(homepage) 코드 내부에 종속되는 아키텍처 결함이 발생했습니다.

폴더 구조는 다음과 같고, 이러한 상황에서 다음과 같은 이슈를 생성한 뒤 진행했습니다.
Overview
The NAT instance is currently managed in the homepage Terraform state, causing unnecessary dependency for other services. It should be separated and managed within the global.
Proposed Solution
- Add NAT instance resource definition to global and output for NAT ENI in global
- Move NAT instance state from homepage to global using
terraform state mv - Update ssuport to reference NAT ENI from global remote state
- Remove NAT instance resource from homepage code
Acceptance Criteria
- The NAT instance is managed solely via the global Terraform state
- ssuport routes external traffic through the global NAT ENI successfully
- Dependency from ssuport → homepage is completely eliminated
요약하자면,
운영 서버의 NAT instance를 homepage와 ssuport에서 공유하고 있는데 해당 자원이 homepage/prod 폴더에 존재하는 문제점이 있었습니다.
프로젝트 간 참조 관계를 제거하고 공유 자원을 global 폴더로 이관해야 했습니다.
해결 방법
가장 쉬운 방법은 global 폴더에 NAT 코드를 새로 짜고, 기존 homepage 코드를 지운 뒤 terraform apply를 하는 것이었습니다.
하지만 이 방식은 현재 운영 서버에서 절대 해선 안 될 위험한 방식이었습니다. 코드를 지우고 apply를 하는 순간, AWS 상의 기존 NAT 인스턴스가 Destroy 되고 새 NAT 인스턴스가 뜰 때까지 모든 프라이빗 서버의 인터넷 연결이 끊기기 때문입니다.
따라서 실제 클라우드 리소스는 그대로 살려둔 채, Terraform의 State만 homepage에서 global로 옮기는 무중단 마이그레이션 전략을 선택했습니다.
1. global에 NAT instance 코드 준비
먼저 global/ec2.tf에 옮겨갈 NAT 인스턴스 코드를 작성합니다. 이때 다른 프로젝트들이 NAT의 ENI(Elastic Network Interface)를 참조할 수 있도록 output도 함께 정의합니다.
resource "aws_instance" "itsupport_prod_nat_instance" {
ami = data.aws_ami.itsupport_nat_instance_ami.id
instance_type = "t4g.micro"
subnet_id = aws_subnet.itsupport_prod_public_subnet_az1.id
vpc_security_group_ids = [module.public_security_group.security_group_id]
associate_public_ip_address = true
disable_api_termination = true
source_dest_check = false
hibernation = false
metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
http_put_response_hop_limit = 2
}
root_block_device {
encrypted = false
}
tags = merge(local.common_tags, {
Name = "itsupport-prod-nat-instance"
Environment = "production"
})
}output "itsupport_prod_nat_instance_eni_id" {
description = "The Primary Network Interface ID of the NAT Instance"
value = aws_instance.itsupport_prod_nat_instance.primary_network_interface_id
sensitive = true
}
output "itsupport_prod_nat_instance_id" {
description = "The Instance ID of the NAT Instance"
value = aws_instance.itsupport_prod_nat_instance.id
sensitive = true
}2. homepage 상태 백업
terraform state pull > ~/homepage_prod_state_backup.json
terraform state list3. homepage/prod state에서 NAT 인스턴스 관리 제거
homepage/prod 환경에서 기존 NAT 인스턴스와 EIP를 Terraform 관리 대상에서 제거합니다. terraform state rm 명령은 실제 AWS 리소스를 삭제하지 않고, Terraform state에서만 해당 리소스 정보를 제거합니다.
terraform state rm aws_eip.nat_eip
terraform state rm aws_instance.itsupport_prod_nat_instance4. global로 state import
이제 global 폴더에서 terraform import를 사용해 기존 AWS 리소스를 현재 Terraform state에 등록합니다.
terraform import aws_instance.itsupport_prod_nat_instance <instance id>
terraform import aws_eip.itsupport_prod_nat_eip <eip id>이후 global에서 terraform apply를 실행하여 State를 최신화하고 output 값을 생성합니다.
5. ssuport 프로젝트의 route table 업데이트
global remote state 참조로 변경합니다.
resource "aws_route" "ssuport_prod_default_route" {
route_table_id = aws_route_table.ssuport_prod_route_table.id
destination_cidr_block = "0.0.0.0/0"
network_interface_id = data.terraform_remote_state.itsupport_global.outputs.itsupport_prod_nat_instance_eni_id
}6. homepage의 NAT 참조 정리
homepage/prod/route_table.tf에서 로컬 NAT 참조를 삭제하고 global remote state 참조로 변경합니다.
resource "aws_route" "backend_application_default_route" {
route_table_id = aws_route_table.backend_application.id
destination_cidr_block = "0.0.0.0/0"
network_interface_id = data.terraform_remote_state.itsupport_global.outputs.itsupport_prod_nat_instance_eni_id
}변경 후 apply를 수행합니다. 실제 라우팅 목적지(ENI ID)는 이전과 동일한 물리적 자원을 가리키므로 네트워크 단절은 전혀 발생하지 않았습니다.
결과
기존 구조: ssuport → homepage → NAT
개선 후 구조:
homepage ─┐
-----------├──→ global NAT
ssuport ───┘최종적으로,
- 공유 인프라가 global로 분리되었고
- 서비스 간 Terraform 의존성이 제거되었으며
- 운영 환경 다운타임 없이 마이그레이션을 완료할 수 있었습니다.
마치며
이 경험을 통해 Terraform state의 이동이 단순히 코드 리팩토링이 아니라, 운영 인프라의 안정성과 서비스 연속성을 동시에 보장해야 하는 작업이라는 점을 배웠습니다. 특히 무중단 배포와는 다른 차원에서, 상태 이전 과정 자체도 운영 리스크를 관리해야 한다는 점이 인상 깊었습니다.