본문 바로가기
DevOps

Terraform 으로 만든 비공개 인프라: 외부는 막고 내부는 연다

by kkodecaffeine 2025. 7. 27.

시작하며;

미국에서 먼저 서비스를 시작하기로 결정됐다. 하지만 단순히 기존 한국 리전의 AI 추론 서버를 옮기는 수준이 아니었다. 아예 처음부터, 미국 리전에 완전히 새로운 추론 인프라를 구축해야 했다.
 
한국 리전은 이미 갖춰진 네트워크, 보안, ECS 환경 위에 서비스를 얹는 일이었다. 기반 인프라가 있었기 때문에 상대적으로 수월하게 진행할 수 있었다. 하지만 미국 리전은 달랐다.
 
VPC 부터 시작해 서브넷, 라우팅 테이블, 보안 그룹, ALB, ECS, 도메인 구성까지 — 모든 걸 처음부터 직접 설계해야 했다. 완전한 제로베이스에서 시작했다. 이 모든 과정을 Terraform으로 코드화하기로 했다. 목표는 단순했다.

"외부에 노출하지 않고, 내부 서비스 간 통신만 가능하게 하자."

 
AWS 의 제약, Terraform 의 설계 한계, 그리고 GPU 환경이라는 특수 조건까지 예상치 못한 문제들이 연이어 터져 나를 괴롭혔다. 그럼에도 불구하고, 하나씩 문제를 해결해 나갔다. 단순한 서비스 배포가 아니라, 리전 하나를 인프라 설계부터 운영까지 온전히 책임지는 프로젝트가 되었다. 그리고 그 삽질 끝에, 미국 리전에 완전히 독립된 내부 추론 인프라를 구축하는 데 성공했다. 그 과정을 글로 남겨본다.


인프라 어떻게 설계했나;

설계 방향은 명확했다. 특히 "외부에 노출하지 않는다"는 기준이 있었기 때문에 ALB 도 내부용 (ALB scheme: internal) 으로 설정해야 했고, 도메인도 Private Hosted Zone 기반으로 구성해야 했다. 그러다 보니 자연스럽게 DNS 설정, VPC 와의 연결 관계, health check 경로와 포트 구성까지 모든 요소를 하나씩 직접 검토해야 했다.
 
사실 Private Hosted Zone 을 선택하게 된 배경은 조금 현실적이다. Internal ALB 의 DNS 주소를 그대로 사용하는 건 너무 길어서 다른 방법이 없을까 고민했다. 그래서 자연스럽게 "내부 도메인 만들어서 깔끔하게 호출하면 안 되나?" 하는 생각이 들었다. 그러다 문득, 예전 직장에서 internal 도메인으로 서버 간 통신하던 기억이 어렴풋이 떠올랐다. 그 흐릿한 기억을 더듬어가며 이것저것 찾아봤고, 결국 Route 53 의 Private Hosted Zone 기능을 통해 내부 도메인을 구성하게 되었다. 내부 서비스 간 통신 구조가 훨씬 직관적으로 정리되었고, 보안 측면에서도 외부 노출 없이 도메인 기반 접근을 적용할 수 있었다.


순서도;

서비스 토폴로지
서비스 토폴로지

전체 흐름은 아래와 같다:

  1. 외부 클라이언트에서 보낸 요청은 Public ALB 을(를) 통해 수신되고, 내부의 guard ECS 서비스로 전달된다.
  2. guard 는 내부 API 처리를 위해 내부 ALB 을(를) 통해 face-generator 서비스로 요청을 보낸다.
    1. Private Hosted Zone 을 이용해 내부에서만 접근 가능한 도메인을 설정
  3. face-generator 서비스는 결과 처리를 완료하면, 이를 다시 guard 서비스로 웹훅 형태로 전송한다.
  4. face-generator 서비스는 결과를 외부로 전달하지 않고, 내부 DNS 를 통해 guard 의 웹훅 엔드포인트로 직접 POST 요청을 보낸다.

삽질 과정;

1. GPU 는 Fargate 로 안 된다;

처음에는 ECS Fargate 를 활용하려고 했다. 운영 부담도 적고, 자동으로 리소스를 관리해주기 때문이다. 하지만 곧 현실을 마주했다. Fargate 는 GPU 를 지원하지 않는다. 따라서 추론 서버는 ECS EC2 Launch Type 으로 구성할 수밖에 없었다. AWS 에서 공식 제공하는 GPU AMI 중 ecs_gpu_optimized 를 선택했다.

2. Terraform 은 조건 분기가 쉽지 않다;

GPU 와 CPU 환경을 동시에 고려해 조건 분기를 하고 싶었다. 변수 하나로 CPU 용/ GPU 용을 다르게 처리하려고 했다. 예를 들어 use_gpu = true 일 경우에는 g4dn.xlarge, 아니면 t3.medium 같은 식으로 구성하고자 했다. 하지만 Terraform 은 단일 리소스 내에서 동적으로 속성을 바꿀 수 없었다. 결국 Launch Template 도 GPU 용과 CPU 용을 각각 따로 만들고, Auto Scaling Group 도 나눠서 구성해야 했다. 코드가 중복되더라도 분리하는 것이 유지보수상 더 낫겠지.

3. internal host is unavailable

오류 메시지는 아래와 같았다. 처음엔 ALB 설정 문제라고 생각했다. 보안 그룹이 잘못됐나? 타겟 그룹이 비어 있나? 여러 가지를 점검했지만, Route 53 의 Private Hosted Zone 에서 생성한 도메인이, VPC 에 연결되어 있지 않았던 것이 원인이었다.

{   "detail": "internal host is unavailable: HTTPConnectionPool(host='api.dev.infer.xxx.internal', port=80): Max retries exceeded with url: /health (Caused by NameResolutionError(...))" }

 
단순히 Private Hosted Zone 을 만들고 레코드를 등록하는 것만으로는 내부 DNS가 작동하지 않는다. 반드시 해당 Hosted Zone 을 VPC 에 명시적으로 연결해야만 한다. 여기서 한 가지 더 중요한 설정이 있다. VPC 에서 enableDnsSupport 와 enableDnsHostnames 옵션이 모두 true로 설정되어 있어야 한다.

  • enableDnsSupport 는 VPC 내부에서 DNS 해석 기능 자체를 활성화하는 옵션이고,
  • enableDnsHostnames 는 EC2 인스턴스 등 리소스에 대해 DNS 이름을 할당할 수 있게 해주는 설정이다.

이 중 하나라도 꺼져 있으면, 내부 DNS 는 제대로 작동하지 않는다. 해당 설정들을 다시 확인하고, Private Hosted Zone 을 명시적으로 VPC 에 연결한 뒤에야 내부 도메인 (api.dev.infer.xxx.internal) 이 정상적으로 resolve 되었고, 서비스도 정상 호출되었다.


마무리하며;

이번 삽질을 통해 확실히 배운 점:

  • Private Hosted Zone 설정은 VPC 와 연결되어야만 동작한다.
  • 내부 ALB 는 HTTPS 를 열려면 ACM 인증서가 필수이고, 내부 도메인용 인증서는 별도 관리가 필요하다.
  • 헬스체크 에러는 단순히 "안됨"이 아닌, “어디까지 됐는지”의 시그널일 수 있다.

return;