AWS NAT Gateway에서 NAT instance로 전환하기

2015년 12월 NAT Gateway가 출시 된 이후로 AWS에서 매니지드 NAT Gateway를 사용할 수 있게 되었다. (서울 리전은 2016년 6월에 출시)

NAT Gateway가 출시되기 전에는 NAT instance (NAT용 AMI 를 통해 생성한 EC2 instance)를 사용했는데 instance 유형에 따라 대역폭이 제한되고 HA를 수동으로 구성해야하는 등 어려움이 있었다.

NAT 인스턴스 및 NAT 게이트웨이 비교 페이지에서 좀 더 자세한 내용을 확인할 수 있다.

회사에서도 NAT Gateway를 사용했었고 AWS VPC Network with Terraform 등의 글에서 VPC를 설계할 때도 당연히 NAT Gateway를 사용했다. 이 때는 블로그 글을 쓰고나면 terraform destroy로 VPC를 내리곤 해서 NAT Gateway의 비용에 대해서는 크게 생각하지 못했었다.

올 여름 해커톤에 참가할 기회가 있어, 프로젝트의 서버를 배포하기 위해 신규 AWS 계정을 생성했고 블로그에 공유했던 Terraform Module을 사용해서 VPC를 구성했었다.

시간이 지나 다음과 같은 Billing Information을 확인할 수 있었다.

Billing Information 크게보기

실제 NAT Gateway를 통해 전송된 데이터의 크기가 무척 작음에도 NAT Gateway의 비용이 상당했다.

EC2 사용시간 대비 NAT 사용시간이 2배로 측정된 것은 Multi AZ 구성을 위해 ap-northeast-2a, ap-northeast-2c 리전에 각 1개씩 NAT Gateway를 올렸기 때문이다. 해당 수치는 약 17일간 서비스를 운영한 결과로 NAT Gateway 2개를 한달간 운영했을 때, 서울리전을 기준으로 계산해보면

$0.059(시간당 사용 가격) * 24 * 31 * 2 = $87.792

최소 사용량을 사용 시에도 한달에 한화로 약 9만 8천원을 사용하게 된다.
ref: Amazon VPC Pricing

NAT 인스턴스 및 NAT 게이트웨이 비교 페이지에서 설명된 NAT Gateway의 고가용성, 스케일링 등의 장점은 NAT 인스턴스를 직접 운영하는 것에 비해 상당히 큰 장점이라고 생각되지만 개인 서비스를 운영하는데 NAT Gateway만 매달10만원(A zone에만 운영할 경우 5만원)에 가까운 비용을 지불하는 것은 쉽지 않다고 생각한다.

NAT instance로 전환을 결정

NAT Gateway의 출시 이전 방식인 NAT instance는 프러덕션 환경에서는 부족함이 있을 수 있지만, 개인 서버를 운영하는 데에는 크게 지장이 없을 것이라 판단하고 기존 생성되어 있는 NAT Gateway를 NAT instance로 전환하기로 결정했다.

다행히 AWS VPC with Terraform Modules 에서 썼던 것처럼 VPC 설계 시 Terraform Module을 사용하여 적용하였으므로 기존 구성되어 있는 VPC의 Terraform코드를 일부 수정하여 NAT instance로 전환하고자 한다.

Terraform의 장점은 여러가지가 있겠지만, 이렇게 이미 생성이 완료된 AWS Resource에 변경이 필요할 때도 유용한 것 같다. 여차하면 기존 형태로 다시 돌릴 수도 있고, 모듈로 구성한 경우 리소스의 전체 구조는 유지하면서 variables의 값만 바꿔서 테스트 후 적용해 볼 수도 있다.

기존 VPC 설계 확인

기존 VPC의 Terraform 코드는 다음과 같다.

  • 2dal-infrastructure/terraform/modules/vpc/dev_vpc.tf
module "vpc" {
  source = "github.com/asbubam/2dal-infrastructure/terraform/modules/vpc"

  name = "dev"
  cidr = "172.16.0.0/16"

  azs              = ["ap-northeast-1a", "ap-northeast-1c"]
  public_subnets   = ["172.16.1.0/24", "172.16.2.0/24"]
  private_subnets  = ["172.16.101.0/24", "172.16.102.0/24"]
  database_subnets = ["172.16.201.0/24", "172.16.202.0/24"]

  tags = {
    "TerraformManaged" = "true"
  }
}

module "bastion" {
  source = "github.com/asbubam/2dal-infrastructure/terraform/modules/bastion"

  name   = "dev"
  vpc_id = "${module.vpc.vpc_id}"

  ami                 = "${data.aws_ami.amazon_linux.id}"
  availability_zone   = "ap-northeast-1a"
  subnet_id           = "${module.vpc.public_subnets_ids[0]}"
  ingress_cidr_blocks = "${var.office_cidr_blocks}"
  keypair_name        = "${var.keypair_name}"

  tags = {
    "TerraformManaged" = "true"
  }
}

AWS VPC with Terraform Modules에서 작성한 vpc, bastion 모듈을 사용했으며, 위와 같이 Terraform 코드를 작성하고 terraform apply 하면 다음과 같은 형태의 dev VPC가 생성된다.

VPC architecture 크게보기

bastion instance는 default type을 t2.nano로 설정했다. 이 bastion instance를 NAT겸 bastion으로 사용하면 비용을 절약할 수 있을 것 같다. NAT를 Multi-AZ 으로 구성하면 좋겠지만 비용에 최적화된 개인 용도의 VPC이므로 우선 A zone에만 NAT를 셋업하고, 이후 필요하면 C zone에도 NAT를 추가하기로 했다.

변경 후의 VPC는 다음과 같은 형태가 된다. 루시드차트 도큐먼트 남겨놔서 천만다행… VPC architecture2 크게보기

vpc, bastion Terraform 모듈 병합

  • cheap_vpc 모듈 생성

vpc 모듈은 다른 상황에서 NAT Gateway를 사용하는 구성으로 사용할 수도 있으니, vpc 모듈을 카피해서 cheap_vpc 라는 이름으로 카피해서 구성해보기로 한다. (이후에 두 모듈을 하나의 모듈로 합쳐서 NAT gateway or NAT instance를 선택할 수 있게도 구성할 수 있을 것 같다.)

$ cd 2dal-infastructure/terraform/modules
$ cp -R vpc cheap_vpc
  • bastion 모듈 내용 병합

bastion 모듈의 main.tf, output.tf, variable.tf의 내용을 cheap_vpc에 병합했다.

$ cd 2dal-infastructure/terraform/modules/cheap_vpc
$ cat ../../modules/bastion/main.tf >> main.tf
$ cat ../../modules/bastion/variables.tf >> variables.tf
$ cat ../../modules/bastion/outputs.tf >> outputs.tf

# 2개의 모듈에서 겹치는 variables(name, tags)를 정리하고...
# bastion 모듈의 ${var.vpc_id} 를 ${aws_vpc.this.id} 로 변경
  • 모듈이 잘 병합되었는지 확인
$ cd 2dal-infrastructure/terraform/common/vpc/
$ vi dev_vpc.tf
module "vpc" {
  source = "../../modules/cheap_vpc"

  name = "dev"
  cidr = "172.16.0.0/16"

  azs              = ["ap-northeast-1a", "ap-northeast-1c"]
  public_subnets   = ["172.16.1.0/24", "172.16.2.0/24"]
  private_subnets  = ["172.16.101.0/24", "172.16.102.0/24"]
  database_subnets = ["172.16.201.0/24", "172.16.202.0/24"]

  ami                 = "${data.aws_ami.amazon_linux.id}"
  availability_zone   = "ap-northeast-1a"
  subnet_id           = "${module.vpc.public_subnets_ids[0]}"
  ingress_cidr_blocks = "${var.office_cidr_blocks}"
  keypair_name        = "${var.keypair_name}"

  tags = {
    "TerraformManaged" = "true"
  }
}

$ terraform init
$ terraform plan
...
Plan: 28 to add, 0 to change, 0 to destroy.

NAT Gateway를 NAT instance로 전환

NAT Gateway 관련 Terraform 코드를 제거하고, VPC NAT 인스턴스 의 가이드를 참고해서 Bastion instance를 NAT instance 용도로도 사용할 수 있도록 변경한다.

$ cd 2dal-infrastructure/terraform/modules/cheap_vpc/

기존 NAT Gateway 정의 삭제

Terraform 코드에서 기존 NAT Gateway 관련 내용을 삭제한다.

  • 2dal-infrastructure/terraform/modules/cheap_vpc/main.tf
# main.tf
# EIP for NAT gateway
resource "aws_eip" "nat" {
  count = "${length(var.azs)}"

  vpc = true
}

# NAT gateway
resource "aws_nat_gateway" "this" {
  count = "${length(var.azs)}"

  allocation_id = "${aws_eip.nat.*.id[count.index]}"
  subnet_id     = "${aws_subnet.public.*.id[count.index]}"
}
  • 2dal-infrastructure/terraform/modules/cheap_vpc/outputs.tf
# NAT gateway
output "nat_ids" {
  description = "NAT Gateway에 할당된 EIP ID 리스트"
  value       = ["${aws_eip.nat.*.id}"]
}

output "nat_public_ips" {
  description = "NAT Gateway에 할당된 EIP 리스트"
  value       = ["${aws_eip.nat.*.public_ip}"]
}

output "natgw_ids" {
  description = "NAT Gateway ID 리스트"
  value       = ["${aws_nat_gateway.this.*.id}"]
}

Bastion instance를 NAT instance로 세팅

  • 2dal-infrastructure/terraform/common/vpc/variables.tf AMI image를 NAT 전용 이미지로 변경
data "aws_ami" "amazon_linux" {
  most_recent = true
  ...

  filter {
    name   = "name"
    # 변경 전:
    # values = ["amzn-ami-hvm-*"]

    # 변경 후:
    values = ["amzn-ami-vpc-nat-hvm-*"]
  }

  ...
}

AWS VPC NAT instance 가이드를 참고해서 AMI 이미지 검색 필터를 수정한다.

  • 2dal-infrastructure/terraform/modules/cheap_vpc/main.tf – private route table 수정
# private route table
resource "aws_route_table" "private" {
  count = "${length(var.azs)}"

  vpc_id = "${aws_vpc.this.id}"

  route {
    cidr_block     = "0.0.0.0/0"
    # 변경 전:
    # nat_gateway_id = "${aws_nat_gateway.this.*.id[count.index]}"

    # 변경 후:
    instance_id    = "${aws_instance.bastion.id}"
  }

  tags = "${merge(var.tags, map("Name", format("%s-private-%s", var.name, var.azs[count.index])))}"
}
  • 2dal-infrastructure/terraform/modules/cheap_vpc/main.tf – Bastion instance의 소스/대상 확인 비 활성화, default SG 추가
# bastion EC2
resource "aws_instance" "bastion" {
  ami                    = "${var.ami}"
  instance_type          = "${var.instance_type}"
  availability_zone      = "${var.availability_zone}"
  subnet_id              = "${var.subnet_id}"
  key_name               = "${var.keypair_name}"

  # 변경 전:
  # vpc_security_group_ids = ["${aws_security_group.bastion.id}"]
   
  # 변경 후:
  vpc_security_group_ids = [
    "${aws_security_group.bastion.id}",
    "${aws_default_security_group.dev_default.id}"
  ]

  associate_public_ip_address = true
  # 다음 항목을 추가
  source_dest_check = false

  tags = "${merge(var.tags, map("Name", format("%s-bastion", var.name)))}"
}

NAT, VPN 등의 용도로 EC2 인스턴스를 사용할 경우에는 **소스/대상 확인기능을 비 활성화** 해야한다. 이 기능이 활성화 되면, EC2로 들어오는 트래픽의 Network Target이 해당 인스턴스일 경우만 허용하게 되기 때문에 NAT, VPN 등의 용도로 사용할 수 없게 된다.

EC2 콘솔 인스턴스에서 인스턴스를 마우스 오른쪽 버튼으로 클릭하고 네트워킹 아래에서 소스/대상 확인 변경을 선택한 후 Disabled(비활성화)를 선택합니다.

ref: AWS NAT instance 가이드

source_dest_check – (Optional) Controls if traffic is routed to the instance when the destination address does not match the instance. Used for NAT or VPNs. Defaults true.

ref: AWS: aws_instance – Terraform by HashiCorp

그리고, 기존에는 Bastion instance는 22번 SSH만 허용했으나 dev vpc 의 default SG 를 허용해서 NAT 요청을 동일 VPC 안에서 허용할 수 있게 한다.

Terraform plan

Terraform 코드가 올바르게 수정되었는지, 변경될 내용을 terraform plan 명령으로 미리 확인해 본다.

$ cd 2dal-infrastructure/terraform/common/vpc/
$ terraform plan
  ...
  ~ module.vpc.aws_eip.bastion
  - module.vpc.aws_eip.nat[0]
  - module.vpc.aws_eip.nat[1]
-/+ module.vpc.aws_instance.bastion (new resource required)
      id:                                          "i-0cd58b1a6b11418b0" => <computed> (forces new resource)
      ami:                                         "ami-00a5245b4816c38e6" => "ami-03cf3903" (forces new resource)
      ...
      source_dest_check:                           "true" => "false"
      ...
  - module.vpc.aws_nat_gateway.this[0]
  - module.vpc.aws_nat_gateway.this[1]
  ~ module.vpc.aws_route_table.private[0]
      route.1778422370.cidr_block:                 "0.0.0.0/0" => ""
      route.1778422370.nat_gateway_id:             "nat-006f4f66e99d66ba6" => ""
      route.~3744414067.cidr_block:                "" => "0.0.0.0/0"
      route.~3744414067.instance_id:               "" => "${aws_instance.bastion.id}"
      ...
  ~ module.vpc.aws_route_table.private[1]
      route.909752400.cidr_block:                  "0.0.0.0/0" => ""
      route.909752400.nat_gateway_id:              "nat-0fb8e2c90ba15bf36" => ""
      route.~3744414067.cidr_block:                "" => "0.0.0.0/0"
      route.~3744414067.instance_id:               "" => "${aws_instance.bastion.id}"
      ...

Plan: 1 to add, 3 to change, 5 to destroy.

Terraform plan 의 결과를 보면

  • bastion 인스턴스를 NAT 전용 instance로 재 생성하고 (source_dest_check 값은 false)
  • NAT Gateway 관련 리소스를 삭제하고
  • private subnet 에 연결된 0.0.0.0/0 으로의 route table을 NAT gateway에서 bastion instance의 id로 변경

하는 것을 확인할 수 있다.

Terraform apply

변경사항을 Terraform apply 명령으로 적용한다.

$ terraform apply
...
Plan: 1 to add, 3 to change, 5 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

Apply complete! Resources: 1 added, 3 changed, 5 destroyed.

야호!

private EC2 instance에서 아웃바운드 요청 확인

# private subnet 에 생성된 EC2 instance에 ssh 접속
$ sudo curl ifconfig.co
52.xxx.xxx.xxx

private instance에서 bastion(NAT) instance를 통해 아웃바운드 인터넷 요청을 성공했다.

NAT Gateway에서 NAT instance로 전환 끝!


본인의 AWS 계정에도 동일하게 VPC를 만들고 싶다면…

GitHub 에 위에서 설명한 cheap_vpc모듈을 조금 더 정리해서 올려놓았다. 다음과 같이 새로운 VPC를 생성할 수 있다.

  • Terraform 설치 및, Access key ID, Secret access key 세팅

AWS VPC Network with Terraform 글을 참고해 Terraform을 설치하고, direnv 등을 이용해 Access Key ID, Secret Access Key를 환경변수에 세팅한다.

나는 tokyo 리전을 사용하기 때문에 ap-northeast-1 을 기준으로 작성했다.

seoul 리전의 경우 region에 ap-northeast-2 을 azs(Availability Zones)에는 ap-northeast-2a, ap-northeast-2c를 입력하면 된다.

  • provider.tf 파일 생성
provider "aws" {
  region = "ap-northeast-1"
}
  • variables.tf 파일 생성
data "aws_ami" "amazon_linux_nat" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "architecture"
    values = ["x86_64"]
  }

  filter {
    name   = "root-device-type"
    values = ["ebs"]
  }

  filter {
    name   = "name"
    values = ["amzn-ami-vpc-nat-hvm-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

variable "office_cidr_blocks" {
  type = "list"

  default = [
    "0.0.0.0/0",
  ] # 이 값은 bastion에 접속을 허용할 IP를 넣어야 한다.
}

variable "keypair_name" {
  default = "2dal-dev" # 이 값은 Bastion 및 private 인스턴스 접속에 사용할 aws key pair 이름을 입력한다.
}

data.aws_ami.amaon_linux_nat 는 가장 최근의 AWS 공식 NAT 이미지를 가져와서 사용할 수 있게 해준다. 이 값을 사용하지 않고 Amazon VPC NAT AMI 리스트에서 특정 AMI id를 확인해서 아래 vpc.tf 파일에 입력해도 된다.

  • vpc.tf 파일 생성
module "vpc" {
  # GitHub으로 부터 cheap_vpc 모듈을 가져온다.
  source = "github.com/asbubam/2dal-infrastructure/terraform/modules/cheap_vpc"

  # vpc name 을 입력한다.
  name = "dev"

  # 사용할 VPC 대역을 입력한다.
  cidr = "172.16.0.0/16" 

  azs              = ["ap-northeast-1a", "ap-northeast-1c"]
  public_subnets   = ["172.16.1.0/24", "172.16.2.0/24"]
  private_subnets  = ["172.16.101.0/24", "172.16.102.0/24"]
  database_subnets = ["172.16.201.0/24", "172.16.202.0/24"]

  bastion_ami                 = "${data.aws_ami.amazon_linux_nat.id}"
  bastion_availability_zone   = "${module.vpc.azs[0]}"
  bastion_subnet_id           = "${module.vpc.public_subnets_ids[0]}"
  bastion_ingress_cidr_blocks = "${var.office_cidr_blocks}"
  bastion_keypair_name        = "${var.keypair_name}"

  tags = {
    "TerraformManaged" = "true"
  }
}

사용하는 IP CIDR 을 입력해서 파일을 완성한다.

  • terraform init 명령으로 provider와 module을 다운로드하고 테라폼을 사용할 준비를 한다.
$ terraform init
Initializing modules...
- module.vpc

Initializing provider plugins...
...
Terraform has been successfully initialized!
  • terraform plan 명령으로 생성 예정인 AWS 리소스를 확인한다.
$ terraform plan
...
Plan: 24 to add, 0 to change, 0 to destroy.
  • terraform apply 명령으로 AWS 리소스를 생성한다.
$ terraform apply
...
Plan: 24 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes
...
Apply complete! Resources: 24 added, 0 changed, 0 destroyed.

VPC 생성 끝!

다음과 같이 (cheap) VPC가 생성되었다. VPC architecture2 크게보기

  • bastion에 접속해본다.
$ ssh -i {key-pair 경로/key-pair 이름}.pem ec2-user@{AWS 콘솔에서 확인한 bastion EC2 instance ip}

정리

기존에 Terraform 을 통해 생성한 VPC의 NAT Gateway를 NAT instance로 전환해 봤다.

지난 여름부터 생각했던 일인데, 미루고 미루다 올해의 마지막 날 적용하게 됐다. 개인 계정으로 테스트를 하다보면 회사에서 사용할 때와 다르게 내 돈이 빠져 나가기 때문에 저렴한 비용으로 필요한 리소스를 운영하는 방법에 대해 고민해 볼 수 있어 좋은 것 같다.

블로그 주제가 많이 밀렸는데 이제 네트워크도 정리되었으니 이 블로그를 시작으로 다시 꾸준히 블로그를 작성할 수 있으면 좋겠다. 올해 두번의 입사를 하게 되는 등, 개인사가 복잡했다. 항상 마음 한켠에 블로그가 있었는데 쓰고 있던 글을 완성하는 것이 쉽지 않았다.

꾸준히 Patreon 을 통해 꾸준히 기부해 주셨던 기부자님들께 사과의 말씀을 전하고 싶다. 죄송합니다! 그리고 감사합니다! 밀린 글들은 다시 파이팅해서 올리겠다고 약속드린다.

기부금을 환불받고자 하시는 분이 계시다면 patreon 메시지 혹은 asbubam at gmail dot com 으로 연락해 주세요!

설명에 사용된 파일은 GitHub – asbubam/2dal-infrastructure에서 확인할 수 있다.

참고자료