cloudwithbass

[Terraform] 테라폼 연습하기3: NAT gateway 본문

Terraform

[Terraform] 테라폼 연습하기3: NAT gateway

여영클 2024. 8. 27. 21:18

지난 포스팅에서 public subnet을 인터넷과 통신하도록 구성했습니다. (https://cloudwithbass.tistory.com/44)

이번 포스팅에선 private subnet이 인터넷에 연결할 수 있도록 구성할 것입니다.

전체 코드는 https://github.com/Dminus251/practice-terraform/tree/main/demo03-nat에서 확인하실 수 있습니다.

 

목차


    1. NAT에 관해서

    왜 private subnet이 인터넷에 연결해야 할까?

    • private subnet은 보안을 위해 인터넷에서 온 트래픽을 허용해선 안 됩니다. 
    • 하지만 private subnet에서 인터넷으로 나가는 트래픽을 허용해야 하는 경우가 있습니다.
      • 패키지 설치/업데이트 (apt update, apt upgrade 등), api 호출 등
    • 이런 상황에서 NAT gateway가 사용됩니다.

     

    NAT gateway란?

    • 프라이빗 서브넷에서 인터넷으로의 연결을 지원하는 네트워크 주소 변환 서비스입니다.
    • 이를 통해 프라이빗 서브넷의 인스턴스는 인터넷에 접속할 수 있지만, 외부 인터넷에서 프라이빗 서브넷으로의 직접적인 연결은 차단됩니다.

    NAT gateway 종류

    • public NAT gateway: 프라이빗 서브넷의 인스턴스를 인터넷에 접속할 수 있도록 합니다.
    • private NAT gateway: 프라이빗 서브넷의 인스턴스를 다른 VPC나 온프레미스에 접속할 수 있도록 합니다.

     

    따라서 다음과 같은 리소스들이 필요합니다.

    1. public NAT Gateway: 인터넷과 연결해야 하므로 public subnet에 배치합니다.
    2. elastic IP: NAT gateway가 인터넷과 통신할 주소가 필요합니다.
    3. route tableroute table association: 인터넷에서 온 트래픽을 nat로 라우팅하는 라우팅 테이블을 생성하고, 이 라우팅 테이블을 private subnet에 연결해야 합니다.

    1. Elastic IP 구성하기

    테라폼 문서를 참고해 Elastic IP 리소스를 생성합니다.

     

    modules/t-aws-eip/main.tf

    resource "aws_eip" "eip" {
      count = var.private_subnet-length
      tags = {
        Name = var.eip-name
      }
    }

    private subnet의 수만큼 eip를 생성할 것이기 때문에 반복문을 사용했습니다.


     

    modules/t-aws-eip/output.tf

    output "eip-id" {
      value = aws_eip.eip.id
    }

    NAT gateway를 생성할 때 사용할 eip의 id입니다.


    modules/t-aws-eip/output.tf

    variable "eip-name" {
      type = string
      default = "practice-eip"
    }

    모듈의 재사용성을 위해 변수를 사용해 이름을 생성합니다.


    2. NAT gateway 구성하기

    코드가 간단하므로 바로 넘어가겠습니다.


    modules/t-aws-nat/main.tf

    resource "aws_nat_gateway" "nat" {
    
      allocation_id = var.eip-id #eip의 id
      subnet_id     = var.subnet-id #subnet id
    
      tags = {
        Name = var.nat-name
      }
    }

    modules/t-aws-nat/vars.tf

    variable "eip-id" {
      type = string
    }
    
    variable "subnet-id" {
      type = string
    }
    
    variable "nat-name" {
      type = string
      default = "practice-nat"
    }

    modules/t-aws-nat/outputs.tf

    output "nat-id" {
      value =  aws_nat_gateway.nat.id
    }

    3. Route Table 구성하기

    인터넷에서 들어오는 트래픽을 NAT gateway로 라우팅합니다. (0.0.0.0/0 → NAT)

    코드는 이전 포스팅과 유사하지만, 가독성을 위해 변수 이름을 수정했고(igw-id → gateway-id), Name tag를 용도에 따라 설정하도록 수정했습니다.


    modules/t-aws-rt/main.tf

    resource "aws_route_table" "internet" {
      vpc_id = var.vpc-id
    
      route { 
        cidr_block = "0.0.0.0/0" #from
        gateway_id = var.gateway-id   #to
      }
    
      tags = {
        Name = "prctice-rt-${var.rt-usage}"
      }
    }

    modules/t-aws-rt/vars.tf

    variable "vpc-id" {
      type = string
    }
    
    variable "gateway-id" {
      type = string
    }
    
    variable "rt-usage" {
      type = string
    }

    modules/t-aws-rt/outputs.tf

    output "rt-id"{
      value = aws_route_table.internet.id
    }

    route table association에서 사용하기 위해 output으로 id를 사용합니다. 


    4. RTA(Route Table Association) 구성하기

    이전 포스팅에선 모듈 내에서 count를 사용했지만, 코드의 일관성과 확장성을 높이기 위해 count를 루트 모듈에서만 사용하도록 수정했습니다. 따라서 rta 모듈 내에서 count를 사용하지 않습니다.


    modules/t-aws-rta/main.tf

    resource "aws_route_table_association" "rta" {
      subnet_id = var.subnet-id
      route_table_id = var.rt-id
    }
    variable "subnet-id" {
      type = string
    }

    modules/t-aws-rta/vars.tf

    variable "subnet-id" {
      type = string
    }
    
    variable "rt-id" {
      type = string
    }

    5. 루트 모듈 구성하기

    이번 포스팅에서 추가된 부분만 다루겠습니다.

    전체 코드는 깃허브 코를 참고 바랍니다.

    https://github.com/Dminus251/practice-terraform/blob/main/demo03-nat/main.tf


    5-1. Eastic IP Module

    module "eip" { #Elastic IP
      source = "./modules/t-aws-eip"
      count = module.private_subnet.private_subnet-length
    }

     

    line 3 count = module.private_subnet.private_subnet-length

    • private_subnet-length output은 프라이빗 서브넷 리스트의 길이를 반환합니다.
    • 현재 default로 두 개의 프라이빗 서브넷이 존재하므로 길이는 2가 될 것이고, 따라서 eip도 2개 생성합니다.
    • NAT gateway는 eip가 필요한데, 가용성을 위해 프라이빗 서브넷 당 NAT를 하나씩 구성할 것입니다.

    5-2. NAT Gateway Module

    module "nat" { #NAT Gateway
      source = "./modules/t-aws-nat"
      count = module.private_subnet.private_subnet-length
      eip-id = module.eip[count.index].eip-id
      subnet-id = module.public_subnet.public_subnet-id[count.index]  #nat는 public subnet에 위치해야 함
    }

    line 3 count = module.private_subnet.private_subnet-length

    • 5-1과 같은 이유로 private subnet의 개수만큼 NAT Gateway를 생성합니다. 

     

    line 4  eip-id = module.eip[count.index].eip-id

    • module.eip[count.index].eip-id를 사용했습니다. 
    • 이건 루트 모듈에서 count를 사용해 eip를 생성했기 때문입니다. 
    • terraform apply 후 terraform.tfstate 파일에서, 테라폼이 관리하는 리소스들의 내용을 확인할 수 있습니다.

    위 사진처럼 module.eip[index] 꼴로 리소스 목록에 접근할 수 있기 때문에 module.eip[count.index].eip-id를 사용한 것입니다. 마지막에 참조하는 eip-id는 eip 모듈의 output입니다.

     

    line 5 subnet-id = module.public_subnet.public_subnet-id[count.index]

    • NAT Gateway는 인터넷 트래픽을 받아야 하므로 public subnet에 위치해야 합니다. 
    • NAT Gateway 리소스에서 subnet-id attribute는 NAT가 위치할 서브넷을 지정할 때 사용하므로 public subnet을 사용해야 합니다.

     

    Q. line 4에선 module.eip[count.index].eip-id 처럼 리소스에 인덱스로 접근했는데, 왜 line 5에서는 module.public_subnet.public_subnet-id[count.index]처럼 output에 인덱스로 접근하나요?

    A.

    public subnet은 모듈 내에서 반복문을 사용했으며, output이 string type입니다.
    반면 eip는 루트 모듈에서 반복문을 사용했으며, output은 string type입니다.
    두 방법 모두 정상적으로 작동하나, 후자의 방식이 유연성, 확장성, 재사용성 측면에서 더 유용하다고 생각하여 이번 포스팅부터는 루트 모듈 내에서 count를 사용합니다.

    5-3. Route Table

    module "route_table-internet_to_nat" { #Route Internet Traffic to NAT
      source = "./modules/t-aws-rt"
      count = module.private_subnet.private_subnet-length
      vpc-id = module.vpc.vpc-id
      gateway-id = module.nat[count.index].nat-id
      rt-usage = "nat"
    }

    line 3 count = module.private_subnet.private_subnet-length

    • 5-1과 같은 이유로 private subnet 수만큼 라우트 테이블을 생성합니다.

    line 4 vpc-id = module.vpc.vpc-id

    • Route Table을 생성할 vpc의 id입니다.

    line 5 gateway-id = module.nat[count.index].nat-id

    • NAT gateway를 루트 모듈에서 반복문을 사용해 생성했으므로 리소스에 인덱스로 접근해 nat-id output을 사용합니다.

    line 6 rt-usage = "nat"

    • 라우트 테이블의 NAme tag에 사용될 변수입니다.

    5-4. Route Table Association

    module "rta-internet_to_nat" {
      source = "./modules/t-aws-rta"
      count = module.private_subnet.private_subnet-length #이만큼 반복해서 생성
      subnet-id = module.private_subnet.private_subnet-id[count.index]
      rt-id = module.route_table-internet_to_nat[count.index].rt-id
    }

    line 3 count = module.private_subnet.private_subnet-length

    • 5-1과 같은 이유로 private subnet 개수만큼 route table association을 생성합니다.
    • 그런데.. 지금 당장은 문제가 없지만, 지금 생각해보니 항상 private subnet 수만큼 rta를 생성하는 것은 아닐 테니까 추후 인프라 확장 시 문제가 생길 것 같기도 합니다.

     

    line 4, line 5

    • 각 index의 subnet에 route table을 연결합니다.

    6. 테스트 하기

    테스트를 위해 다음과 같이 두 인스턴스를 구성했습니다.

     

    ec2-bastion

    • public_subnet-1 서브넷에 생성했습니다.
    • public subnet에 위치하므로 인터넷의 ssh를 허용합니다.

     

    ec2-private

    • private_subnet-2 서브넷에 생성했습니다.
    • ec2-bastion이 속한 보안 그룹의 인바운드만을 허용합니다.

    아래 사진은 localhost에서 ec2-bastion에 ssh 연결한 후, ec2-bastion에서 ec2-private으로 ssh 연결한 모습입니다.

     

    ping 명령이 정상적으로 작동하는 걸 보아 private subnet에 있음에도 불구하고 인터넷에 트래픽이 도달한 것을 확인할 수 있습니다.