[Infra] VPC Endpoint로 ECR·S3 통신하기

고수가 되고 싶은 감자·2026년 9월 16일
post-thumbnail

VPC Endpoint로 외부 인터넷 없이 안전하게 ECR·S3 통신하기

AWS에서 프라이빗 서브넷(Private Subnet)을 구성하고 나면 가장 먼저 마주치는 고민이 있다.

"외부 인터넷과 완전히 단절된 프라이빗 서버(EKS 노드, EC2)에서 도커 이미지는 어떻게 pull 받고, S3 파일은 어떻게 다운받지?"

처음에는 단순히 "NAT 게이트웨이를 달아서 인터넷으로 나가서 받아오면 되지 않나?"라고 생각했다.
하지만 이 방식에는 몇가지 단점이 있었다.


1. NAT 게이트웨이 경유 방식의 문제점

프라이빗 서브넷의 서버가 ECR이나 S3에 접근할 때 NAT 게이트웨이를 거치게 되면 다음과 같은 문제가 발생한다.

  1. 데이터 전송 비용 발생: NAT 게이트웨이는 시간당 인스턴스 비용뿐만 아니라 처리되는 데이터 GB당 비용($0.059/GB 수준)이 청구된다. 수백 MB에서 수 GB에 달하는 도커 이미지를 지속적으로 배포할 경우 비용이 기하급수적으로 늘어난다. (NAT Instance를 되긴하나, 추가적으로 관리할게 생김)
  2. 보안 리스크: AWS 내부 서비스와 통신하는데 굳이 외부 공용 인터넷망을 경유해서 나갔다 들어오는 비효율적인 경로를 타게 된다.
  3. 가용성 의존성: NAT 인스턴스나 NAT 게이트웨이에 문제가 생기면 내부 도커 배포와 이미지 조회가 전면 중단된다.

이 문제를 해결하기 위해 AWS 내부 전용 사설 통로인 VPC Endpoint를 도입하였다.


2. VPC Endpoint란 무엇인가?

VPC Endpoint는 외부 인터넷이나 NAT 게이트웨이를 거치지 않고, AWS 내부 전용 백본 네트워크를 통해 AWS 서비스(S3, ECR 등)에 직접 연결해 주는 출입문이다.

VPC Endpoint에는 두 가지 타입이 있다.

① Gateway 엔드포인트 (S3, DynamoDB)

  • 라우팅 테이블에 S3/DynamoDB로 가는 전용 경로만 등록하는 방식이다.
  • 서브넷에 별도의 IP나 가상 랜카드(ENI)가 생성되지 않는다.
  • 비용이 무료이다.
  • ECR, SQS, CloudWatch 등 대부분의 AWS 서비스를 지원한다.
  • 서브넷 내부에 실제 사설 IP를 갖는 가상 랜카드(ENI, Elastic Network Interface)가 생성된다.
  • 실제 네트워크 인터페이스가 생기므로 보안그룹 설정이 필수이며, 소정의 시간당/데이터 처리 비용이 발생한다.

3. ECR 통신을 위해 필요한 3가지 엔드포인트

처음에는 "ECR 엔드포인트 하나만 만들면 되겠지"라고 생각했다.
하지만 ECR에서 도커 이미지를 정상적으로 pull 받으려면 총 3개의 엔드포인트가 세트로 필요했다.

  1. ecr.api (Interface 엔드포인트):
    aws ecr get-login-password 같은 로그인 인증 및 레포지토리 메타데이터를 제어하는 창구.
  2. ecr.dkr (Interface 엔드포인트):
    실제 도커 데몬이 docker pull / push 명령으로 이미지 매니페스트를 통신하는 창구.
  3. s3 (Gateway 엔드포인트):
    도커 이미지의 실제 무거운 바이너리 레이어 파일들이 백그라운드의 S3에 저장되어 있으므로, 이 대용량 레이어를 무료 내부망으로 내려받기 위한 필수 통로.

이 3개 중 하나라도 빠지면 프라이빗 환경에서 도커 이미지 다운로드가 실패한다.


4. 테라폼 코드 구현

모듈의 재사용성과 결합도 분리를 위해 modules/vpc-endpoints 모듈을 분리하여 구현했다.

① Interface 엔드포인트 전용 보안그룹 (vpce-sg)

Interface 엔드포인트는 서브넷에 가상 랜카드가 생기므로 방화벽이 필요하다.
우리 VPC 내부 IP 대역(var.vpc_cidr)에서 오는 HTTPS(443 포트) 인바운드 트래픽만 허용하도록 격리했다.

resource "aws_security_group" "vpce" {
  name        = "${var.project_name}-${var.environment}-vpce-sg"
  description = "Security group for VPC Interface Endpoints (ECR API, ECR DKR)"
  vpc_id      = var.vpc_id

  ingress {
    description = "Allow HTTPS from VPC internal CIDR"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = [var.vpc_cidr]
  }

  egress {
    description = "Allow all outbound traffic"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = merge(var.tags, {
    Name = "${var.project_name}-${var.environment}-vpce-sg"
  })
}

② S3 Gateway 엔드포인트

무료이며, 프라이빗 라우팅 테이블에 연결한다.

resource "aws_vpc_endpoint" "s3" {
  vpc_id            = var.vpc_id
  service_name      = "com.amazonaws.${data.aws_region.current.name}.s3"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = var.private_route_table_ids

  tags = merge(var.tags, {
    Name = "${var.project_name}-${var.environment}-s3-vpce"
  })
}

③ ECR API / DKR Interface 엔드포인트

ECR 도메인을 호출했을 때 공인 IP 대신 우리 서브넷 엔드포인트로 연결해야하기 때문에
private_dns_enabled = true 설정을 반드시 켜주어야 한다.
이 옵션을 끈다면 요청이 여전히 외부 공인 IP를 바라고보고 있어, 프라이빗 환경에서는 도커 이미지 다운로드가 실패한다.

resource "aws_vpc_endpoint" "ecr_api" {
  vpc_id              = var.vpc_id
  service_name        = "com.amazonaws.${data.aws_region.current.name}.ecr.api"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = var.private_subnet_ids
  security_group_ids  = [aws_security_group.vpce.id]
  private_dns_enabled = true

  tags = merge(var.tags, {
    Name = "${var.project_name}-${var.environment}-ecr-api-vpce"
  })
}

resource "aws_vpc_endpoint" "ecr_dkr" {
  vpc_id              = var.vpc_id
  service_name        = "com.amazonaws.${data.aws_region.current.name}.ecr.dkr"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = var.private_subnet_ids
  security_group_ids  = [aws_security_group.vpce.id]
  private_dns_enabled = true

  tags = merge(var.tags, {
    Name = "${var.project_name}-${var.environment}-ecr-dkr-vpce"
  })
}

5. 실제 동작 방식: 개발자는 무엇을 바꿔야 할까?

결론부터 말하면 개발자나 애플리케이션 코드는 아무것도 바꿀 필요가 없다.

테라폼에서 private_dns_enabled = true 옵션을 켜두었기 때문에, 서버가 평소처럼 *.dkr.ecr.ap-northeast-2.amazonaws.com 도메인으로 요청을 보내면:

  1. VPC 내부 Route 53 전용 DNS가 이를 가로채서 공인 IP 대신 서브넷 안에 박힌 엔드포인트의 사설 IP(10.0.x.x)를 반환한다.
  2. 서버는 자연스럽게 사설 IP로 통신하여 ECR 로그인 및 이미지 pull을 수행한다.
  3. 실제 이미지 바이너리 레이어는 S3 게이트웨이 엔드포인트를 통해 AWS 내부망으로 고속 다운로드된다.

모든 과정이 NAT 게이트웨이를 1바이트도 거치지 않고 완벽하게 투명(Transparent)하게 동작한다.


6. 마치며

정리하자면:

  1. VPC Endpoint는 외부 인터넷(NAT) 없이 AWS 내부망으로 S3와 ECR에 직통 연결하는 사설 터널
  2. S3는 완전 무료인 Gateway 방식 사용
  3. ECR은 인증(ecr.api)과 이미지 다운로드(ecr.dkr) 2개의 Interface 엔드포인트가 모두 필요하며, 대용량 레이어 다운로드를 위해 S3 Gateway 엔드포인트도 함께 필수
  4. Interface 엔드포인트는 실제 ENI가 생기므로 VPC 내부 HTTPS(443) 허용 보안그룹이 필요
  5. Private DNS를 활성화하면 애플리케이션이나 도커 설정 변경 없이 기존 도메인 그대로 사설 통신 가능

이상.

profile
DevOps 엔지니어 지망생

0개의 댓글