Развертывание большинства сервисов в AWS начинается с настройки VPC. В большинстве своем это достаточно простая операция, но все же встречаются некоторые нюансы, о которых и поговорим в этой и последующих статьях. Конечная цель – сконфигурировать работающую сеть с публичными и частными подсетями и выходом в интернет через NAT Instance. Кроме того, не помешает лишняя отказоустойчивость на основе Auto Scaling group. Описывать инфраструктуру планируется из кода, используя Terraform.
UPD 22.09.2021: А вот и вторая статья – NAT Instance. Часть 2 — Auto Scaling group
Содержание
NAT Instance. Часть 1 – Subnets
Сначала как всегда немного теории. Дело в том, что у AWS есть как минимум три сущности, так или иначе связанные с маршрутизацией трафика между сетями VPC и внешним миром. Речь идет про NAT Instance 1, NAT Gateway 2 и Internet Gateway 3.
С последним все просто – этот объект используется для маршрутизации интернет-трафика и трансляции адресов для тех EC2-инстансов, к которым прикреплен публичный адрес.
Между NAT Instance и NAT Gateway есть существенные различия 4, хоть и выполняют они одну и ту же функцию. NAT Gateway представляет из себя отказоустойчивый шлюз, управляемый полностью со стороны AWS. А NAT Instance по сути является его дешевой заменой – это обычный EC2-инстанс, для которого Амазон готовит специальный AMI 5.
Подготовка окружения
Окружение представляет из себя обычную линукс-машину с предустановленным Terraform. Проект имеет следующую структуру каталогов:
Далее по очереди рассмотрим содержимое каждого файла, но конкретно в этой статье коснемся лишь базовых моментов.
Переменные
Для начала рассмотрим файл variables.tf, который содержит набор переменных. Обратите внимание, что везде присутствует значение по умолчанию. Вы можете изменить нужные вам параметры, а можете оставить все как есть, для базовой функциональности этого достаточно.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
variable "region" { default = "eu-west-1" } variable "vpc_cidr_block" { type = string default = "192.168.0.0/16" } variable "vpc_azs" { type = list(string) default = ["eu-west-1a", "eu-west-1b", "eu-west-1c"] } variable "vpc_public_subnet" { type = list(string) default = ["192.168.0.0/19", "192.168.32.0/19", "192.168.64.0/19"] } variable "vpc_private_subnet" { type = list(string) default = ["192.168.96.0/19", "192.168.128.0/19", "192.168.160.0/19"] } variable "nat_instance" { default = { name = "nat-instance" instance_type = "t2.micro" image_id = "ami-0f2ef6eaad306deab" } } |
Небольшие пояснения, если вдруг решите, что значения по умолчанию вам не подходят:
1. Вам нужно выбрать сеть и нарезать её на подсети в любой корректной конфигурации.
2. Имена и количество зон доступности – Availability zones – будут отличаться в зависимости от региона.
3. Как я упоминал выше, для Nat Instance нужен свой специальный образ AMI. Найти их можно по префиксу amzn-ami-vpc-nat в имени. Не забудьте подобрать наиболее подходящий под ваши нужды тип EC2-инстанса, ведь пропускная способность сетевого интерфейса зависит от этого напрямую.
Переходим к следующему пункту.
Terraform
Теперь пришло время позаботиться об окружении и настройках провайдера, а это файл terraform.tf:
1 2 3 4 5 6 7 8 |
provider aws { region = var.region version = "3.4.0" } terraform { required_version = "~>0.12.0" } |
Конфигурация актуальна на момент написания статьи и в будущем устареет, обратите на это внимание.
Настройка подсетей
Переходим к описанию непосредственно самой инфраструктуры. Содержимое файла vpc.tf включает в себя ресурсы для создания подсетей, маршрутов и других объектов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
resource "aws_vpc" "main" { cidr_block = var.vpc_cidr_block enable_dns_support = true enable_dns_hostnames = true } # Public subnets configuration resource "aws_subnet" "subnet_public" { count = length(var.vpc_public_subnet) vpc_id = aws_vpc.main.id cidr_block = var.vpc_public_subnet[count.index] availability_zone = var.vpc_azs[count.index] map_public_ip_on_launch = true } resource "aws_internet_gateway" "igw" { vpc_id = aws_vpc.main.id } resource "aws_route_table" "route_table_public_to_igw" { vpc_id = aws_vpc.main.id route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.igw.id } } resource "aws_route_table_association" "aws_route_table_association_public" { count = length(var.vpc_public_subnet) route_table_id = aws_route_table.route_table_public_to_igw.id subnet_id = aws_subnet.subnet_public[count.index].id } # Private subnets configuration resource "aws_subnet" "subnet_private" { count = length(var.vpc_private_subnet) vpc_id = aws_vpc.main.id cidr_block = var.vpc_private_subnet[count.index] availability_zone = var.vpc_azs[count.index] } resource "aws_route_table" "route_table_private_to_nat_instance" { vpc_id = aws_vpc.main.id route { cidr_block = "0.0.0.0/0" network_interface_id = aws_network_interface.nat_instance_interface.id } // do not recreate nat instance already created // https://www.terraform.io/docs/language/meta-arguments/lifecycle.html#ignore_changes lifecycle { ignore_changes = [route] } } resource "aws_route_table_association" "aws_route_table_association_private" { count = length(var.vpc_private_subnet) route_table_id = aws_route_table.route_table_private_to_nat_instance.id subnet_id = aws_subnet.subnet_private[count.index].id } |
Дополнительные опции ресурса aws_vpc активируют разрешение имен со стороны Route 53 и автоматическое назначение ДНС-имен внешним и внутренним адресам интерфейсов соответственно 6 7.
Как я и упоминал ранее, EC2-инстансам из публичных сетей для выхода в интернет нужен маршрут до Internet Gateway. Ресурс aws_internet_gateway создает этот объект, а aws_route_table маршрут наружу по умолчанию.
С частными подсетями все интереснее, а потому для них отдельная глава ниже.
Частные подсети
Отличаются от публичных они тем, что из них напрямую в интернет выйти не получится, только с использованием сетевой трансляции адресов – NAT. Эту функцию у нас будет выполнять отдельный EC2-инстанс. Это в некотором смысле “велосипед”, хоть и официально поддерживаемый со стороны AWS.
Основной параметр здесь это network_interface_id, который указывает на идентификатор сетевого интерфейса NAT-инстанса. Хитрость в том, что ASG будет пересоздавать сам EC2-инстанс, а вот интерфейс, заранее созданный нами (в следующей статье), будет оставаться всегда одним и тем же. Тем самым у нас не будет необходимости переделывать маршрут каждый раз при пересоздании EC2-инстанса.
Секция lifecycle с параметром ignore_changes у ресурса aws_route_table служит здесь для того, чтобы не пересоздавать EC2-инстанс, если вдруг он ранее уже был автоматически пересоздан autoscaling-группой (далее ASG).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
resource "aws_route_table" "route_table_private_to_nat_instance" { vpc_id = aws_vpc.main.id route { cidr_block = "0.0.0.0/0" network_interface_id = aws_network_interface.nat_instance_interface.id } // do not recreate nat instance already created // https://www.terraform.io/docs/language/meta-arguments/lifecycle.html#ignore_changes lifecycle { ignore_changes = [route] } } |
Происходит это потому, что к объекту aws_route_table, помимо индекса интерфейса, привязывается также и идентификатор инстанса – instance_id. Если инстанс в будущем будет пересоздан, а произойти это может например по причине его неисправности, то и instance_id будет другой. В следующий раз при выполнении команды terraform plan это повлечет за собой изменение объекта (обратите внимание, что объект route пересоздается с одним изменением):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# aws_route_table.route_table_private_to_nat_instance will be updated in-place ~ resource "aws_route_table" "route_table_private_to_nat_instance" { id = "rtb-047c5377b3e3d2a9c" owner_id = "754745079939" propagating_vgws = [] ~ route = [ + { + cidr_block = "0.0.0.0/0" + egress_only_gateway_id = "" + gateway_id = "" + instance_id = "" + ipv6_cidr_block = "" + nat_gateway_id = "" + network_interface_id = "eni-054557ada4d867947" + transit_gateway_id = "" + vpc_peering_connection_id = "" }, - { - cidr_block = "0.0.0.0/0" - egress_only_gateway_id = "" - gateway_id = "" - instance_id = "i-0422aaf54c49f540b" - ipv6_cidr_block = "" - nat_gateway_id = "" - network_interface_id = "eni-054557ada4d867947" - transit_gateway_id = "" - vpc_peering_connection_id = "" }, ] tags = {} vpc_id = "vpc-09bdc72277572d963" } |
Эта проблема красиво решается игнорированием секции route у ресурса aws_route_table.
На это сегодня все. Ну а самым интересным – конфигурацией ASG – займемся уже в следующей статье.