AWS에서 프라이빗 서브넷(Private Subnet)을 구성하고 나면 가장 먼저 마주치는 고민이 있다.
"외부 인터넷과 완전히 단절된 프라이빗 서버(EKS 노드, EC2)에서 도커 이미지는 어떻게 pull 받고, S3 파일은 어떻게 다운받지?"
처음에는 단순히 "NAT 게이트웨이를 달아서 인터넷으로 나가서 받아오면 되지 않나?"라고 생각했다.
하지만 이 방식에는 몇가지 단점이 있었다.
프라이빗 서브넷의 서버가 ECR이나 S3에 접근할 때 NAT 게이트웨이를 거치게 되면 다음과 같은 문제가 발생한다.
이 문제를 해결하기 위해 AWS 내부 전용 사설 통로인 VPC Endpoint를 도입하였다.
VPC Endpoint는 외부 인터넷이나 NAT 게이트웨이를 거치지 않고, AWS 내부 전용 백본 네트워크를 통해 AWS 서비스(S3, ECR 등)에 직접 연결해 주는 출입문이다.
VPC Endpoint에는 두 가지 타입이 있다.
처음에는 "ECR 엔드포인트 하나만 만들면 되겠지"라고 생각했다.
하지만 ECR에서 도커 이미지를 정상적으로 pull 받으려면 총 3개의 엔드포인트가 세트로 필요했다.
ecr.api (Interface 엔드포인트):aws ecr get-login-password 같은 로그인 인증 및 레포지토리 메타데이터를 제어하는 창구.ecr.dkr (Interface 엔드포인트):docker pull / push 명령으로 이미지 매니페스트를 통신하는 창구.s3 (Gateway 엔드포인트):이 3개 중 하나라도 빠지면 프라이빗 환경에서 도커 이미지 다운로드가 실패한다.
모듈의 재사용성과 결합도 분리를 위해 modules/vpc-endpoints 모듈을 분리하여 구현했다.
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"
})
}
무료이며, 프라이빗 라우팅 테이블에 연결한다.
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 도메인을 호출했을 때 공인 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"
})
}
결론부터 말하면 개발자나 애플리케이션 코드는 아무것도 바꿀 필요가 없다.
테라폼에서 private_dns_enabled = true 옵션을 켜두었기 때문에, 서버가 평소처럼 *.dkr.ecr.ap-northeast-2.amazonaws.com 도메인으로 요청을 보내면:
10.0.x.x)를 반환한다.모든 과정이 NAT 게이트웨이를 1바이트도 거치지 않고 완벽하게 투명(Transparent)하게 동작한다.
정리하자면:
ecr.api)과 이미지 다운로드(ecr.dkr) 2개의 Interface 엔드포인트가 모두 필요하며, 대용량 레이어 다운로드를 위해 S3 Gateway 엔드포인트도 함께 필수이상.