Как развернуть 2-уровневую архитектуру с AWS и Terraform Cloud

В этом проекте с помощью Terraform Cloud, AWS и Github создадим конфигурационные файлы Terraform и легко масштабируемую, высокодоступную двухуровневую архитектуру для конвейера CI/CD с публичной и закрытой сетями.

Первая ориентирована на пользователя  —  здесь он получает доступ к веб-приложению, вторая  —  это ЦОД, из которого веб-приложению предоставляется бизнес-логика. С такой архитектурой данные ЦОД остаются скрытыми, отделяются от клиента. Приступим к ее созданию.


План проекта

I. Создаем высокодоступную двухуровневую архитектуру AWS.

1. Пользовательское виртуальное частное облако с:

  • двумя публичными и двумя закрытыми подсетями для уровня веб-сервера и RDS служб реляционных баз данных соответственно;
  • двумя маршрутными таблицами: публичной и закрытой.

2. В каждой публичной подсети веб-уровня создаем группу автомасштабирования для запуска экземпляра EC2 с веб-сервером NGINX. Конфигурируем необходимые группы безопасности.

3. Создаем один экземпляр RDS MySQL (micro) в закрытых подсетях RDS с соответствующими группами безопасности.

II. Чтобы проверить сборку, с помощью Terraform Cloud развертываем как инструмент CI/CD.

Потребуется:

  • учетная запись администратора AWS с ключом доступа и секретным ключом доступа;
  • среда AWS Cloud9;
  • бесплатная учетная запись на Terraform Cloud;
  • учетная запись на Github;
  • знание команд Linux и Git.

Часть A. Создание конфигурационных файлов

  • Начиная со среды AWS Cloud9 Terraform, клонируем или дублируем репозиторий Github для конфигурационных файлов инфраструктуры.
  • Для создания конфигурационных файлов понадобится документация Terraform и AWS. Открываем консоль AWS в отдельной вкладке браузера и посматриваем туда при создании архитектуры. Теги в конфигурационных файлах необязательны, но с ними легко идентифицировать ресурсы в консоли управления AWS.
  • Создаем новую ветку через CLI или Cloud9, введя в строке поиска слева вверху create branch, нажимаем create branch («Создать ветку») > выбираем клонированный репозиторий > называем ветку > публикуем ее:
  • Настроив репозиторий и ветку, займемся конфигурационными файлами инфраструктуры. Сначала делаем рабочий каталог проекта, переходим в него.
  • Создаем файл terraform.tf, копируем и вставляем в него следующий блок кода:
terraform {
required_version = "~> 1.4.4"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.62.0"
}
}
}

Ограничением ~> задаем поставщику AWS нижнюю и верхнюю границы версий.

  • Сохраняем этот и все создаваемые файлы. Корректное форматирование обеспечиваем командой terraform fmt.
  • Каталог инициализируем командой terraform init.

Установочный файл NGINX

  • В текущем рабочем каталоге создаем файл nginx.sh: в нем будет скрипт BASH для установки веб-сервера NGINX в экземпляре Amazon Linux 2 EC2. Не забываем .sh, с помощью текстового редактора копируем и вставляем следующий скрипт:
#!/bin/bash  
sudo yum update -y
sudo amazon-linux-extras install nginx1 -y
sudo amazon-linux-extras enable nginx1
sudo systemctl start nginx

Файл переменных

  • Чтобы чаще использовать конфигурацию повторно и легко разворачивать ресурсы, создадим файл variables.tf для нескольких переменных, копируем и вставляем такой код:
variable "aws_region" {
type = string
default = "us-east-1"
}

variable "instance_type" {
type = string
default = "t2.micro"
}

variable "ami_id" {
description = "AMI ID for Amazon Linux 2"
type = string
default = "ami-069aabeee6f53e7bf"
}

variable "vpc_name" {
description = "Name for Custom VPC"
type = string
default = "tt-vpc"
}

variable "vpc_cidr" {
type = string
default = "10.10.0.0/16"
}

variable "us-aze1a" {
description = "First AZ for public and private subnets"
type = string
default = "us-east-1a"
}

variable "us-aze1b" {
description = "Second AZ for public and private subnets"
type = string
default = "us-east-1b"
}

variable "db_username" {
description = "Database administrator username"
type = string
default = "dbadmin"
sensitive = true
}

variable "db_password" {
description = "Database administrator password"
type = string
default = "yourdbpassword"
sensitive = true
}
  • Подведем итог: в коде регион по умолчанию  —  us-east-1, применяемый образ машины Amazon  —  Amazon Linux 2 с запуском экземпляров EC2 t2.micro в настраиваемом виртуальном частном облаке VPC.

В целях высокой доступности создадим две публичные подсети, каждую в своей зоне доступности: us-east-1a и us-east-1b. В тех же зонах доступности будут и две закрытые подсети.

Для учетных данных БД имеются переменные, обозначенные как конфиденциальные. Так что добавляем свои имя пользователя и пароль БД: в Terraform значения отредактируются в любых лог-файлах или командах вывода.

Файл «main»

Создаем файл main.tf и идем по блокам кода сверху вниз до ссылки на репозиторий Github с финальным файлом в конце.

  • В этой части создадим пользовательское виртуальное частное облако с применением блока CIDR из файла переменных. Хотя аргумент hostnames службы доменных имен необязателен, включаем его и задаем булеву флагу значение true, потому что значение по умолчанию  —  false.
  • Включением vpc_id пользовательскому виртуальному частному облаку  —  через прикручиваемый к нему шлюз интернета  —  обеспечивается интернет-доступ:
provider "aws" {
region = var.aws_region
}

#создаем пользовательское виртуальное частное облако VPC
resource "aws_vpc" "tt-vpc" {
cidr_block = var.vpc_cidr

tags = {
Name = var.vpc_name
Environment = "your_env_name"
Terraform = "true"
}

enable_dns_hostnames = true
}

#создаем интернет-шлюз для подключения к пользовательскому VPC
resource "aws_internet_gateway" "tt-igw" {
vpc_id = aws_vpc.tt-vpc.id

tags = {
Name = "tt-igw"
}
}
  • В следующем блоке кода создаются две публичные и две закрытые подсети. Созданный в начале файла main.tf идентификатор пользовательского виртуального частного облака используется во всем коде, поэтому в Terraform не задействуется VPC по умолчанию. Если при этом не сослаться на пользовательский VPC, получим ошибки при тестировании конфигурационных файлов:
##Развертываем две публичных подсети для уровня веб-сервера
##Публичная подсеть запустится в зоне доступности us-east-1a

resource "aws_subnet" "tt-pubsubnet-1" {
vpc_id = aws_vpc.tt-vpc.id
cidr_block = "10.10.1.0/24"
availability_zone = var.us-aze1a
map_public_ip_on_launch = true

tags = {
Name = "tt-pubsubnet-1"
}
}

##Публичная подсеть запустится в зоне доступности us-east-1b
resource "aws_subnet" "tt-pubsubnet-2" {
vpc_id = aws_vpc.tt-vpc.id
cidr_block = "10.10.2.0/24"
availability_zone = var.us-aze1b
map_public_ip_on_launch = true

tags = {
Name = "tt-pubsubnet-2"
}
}

#Развертываем две закрытые подсети для уровня RDS
##Закрытая подсеть запустится в зоне доступности us-east-1a
resource "aws_subnet" "tt-privsubnet1" {
vpc_id = aws_vpc.tt-vpc.id
cidr_block = "10.10.3.0/24"
availability_zone = var.us-aze1a

tags = {
Name = "tt-privsubnet1"
}
}
##Закрытая подсеть запустится в зоне доступности us-east-1b
resource "aws_subnet" "tt-privsubnet2" {
vpc_id = aws_vpc.tt-vpc.id
cidr_block = "10.10.4.0/24"
availability_zone = var.us-aze1b

tags = {
Name = "tt-privsubnet2"
}
}
  • В этом блоке кода создаются публичные и закрытые маршрутные таблицы вместе со связями подсетей. В публичную маршрутную таблицу добавляется маршрут для интернет-шлюза. Затем обе публичные подсети увязываются с публичной маршрутной таблицей, как показано ниже. В закрытую маршрутную таблицу добавляется маршрут для шлюза трансляции сетевых адресов NAT  —  так закрытым подсетям предоставляется доступ к внешним ресурсам. Затем закрытые подсети увязываются с закрытой маршрутной таблицей:
#создаем публичную маршрутную таблицу с маршрутом для интернет-шлюза 
resource "aws_route_table" "public-tt-rt" {
vpc_id = aws_vpc.tt-vpc.id

route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.tt-igw.id
}

tags = {
Name = "public-tt-rt"
}
}

#создаем закрытую маршрутную таблицу с маршрутом для NAT-шлюза
resource "aws_route_table" "private-tt-rt" {
vpc_id = aws_vpc.tt-vpc.id

route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.tt-nat-gw.id
}

tags = {
Name = "private-tt-rt"
}
}

#публичная маршрутная таблица со связями публичных подсетей
resource "aws_route_table_association" "publicsub1" {
route_table_id = aws_route_table.public-tt-rt.id
subnet_id = aws_subnet.tt-pubsubnet-1.id
}

resource "aws_route_table_association" "publicsub2" {
route_table_id = aws_route_table.public-tt-rt.id
subnet_id = aws_subnet.tt-pubsubnet-2.id
}

#закрытая маршрутная таблица со связями закрытых подсетей
resource "aws_route_table_association" "privatesub1" {
route_table_id = aws_route_table.private-tt-rt.id
subnet_id = aws_subnet.tt-privsubnet1.id
}

resource "aws_route_table_association" "privatesub2" {
route_table_id = aws_route_table.private-tt-rt.id
subnet_id = aws_subnet.tt-privsubnet2.id
}
  • Следующий блок кода предназначен для эластичного IP-адреса и шлюза NAT, так как они тесно связаны. В маршрутной таблице шлюз NAT упоминается перед блоком ресурсов, которым шлюз NAT создается, но этот порядок на маршрутной таблице не сказывается. Чтобы в Terraform сначала создавался шлюз NAT, ниже добавлен метааргумент depends_on, он необязательный:
#создаем эластичный IP-адрес для присвоения шлюзу NAT
resource "aws_eip" "tt-nat-eip" {
vpc = true #подтверждается, находится эластичный IP-адрес в VPC или нет
depends_on = [aws_internet_gateway.tt-igw]
tags = {
Name = "tt-nat-eip"
}
}

#создаем шлюз NAT для закрытых подсетей
resource "aws_nat_gateway" "tt-nat-gw" {
depends_on = [aws_eip.tt-nat-eip]
allocation_id = aws_eip.tt-nat-eip.id
subnet_id = aws_subnet.tt-pubsubnet-1.id
tags = {
Name = "tt-nat-gw"
}
}
  • Чтобы создать высокодоступную, масштабируемую архитектуру, делаем шаблон запуска. Нужно автоматизировать процесс запуска экземпляров EC2 на случай внезапного всплеска трафика, при котором требуется больше ресурсов. Шаблон запуска затем используется группой автомасштабирования для запуска экземпляров EC2 с установленным веб-сервером NGINX. Чтобы обеспечить доступ к экземплярам из интернета, у них должны быть определенные правила групп безопасности. Для блоков с входящим трафиком добавляем порты 22, 80, 8080, с исходящим  —  «–1» для протокола, что эквивалентно значениям протокола «все». Порт 22 добавлен, чтобы подключиться по SSH к одному из экземпляров EC2 группы автомасштабирования и протестировать подключение к экземпляру RDS.
  • Вы увидите, что созданный ранее файл nginx.sh наконец-то включен в значение аргумента user_data, так как экземпляры EC2 загрузятся. Когда завершится инициализация экземпляров, появится возможность протестировать общедоступный IP-адрес и увидеть веб-страницу NGINX по умолчанию.
  • Для экземпляра RDS создана и отдельная группа безопасности, так как ему понадобится порт по умолчанию для MySQL. Это порт 3306, по которому клиенты MySQL подключаются к серверу MySQL, и весь обмен данными по умолчанию зашифрован:
#группы безопасности, которыми разрешается входной трафик из интернета
resource "aws_security_group" "tf-tt-sg" {
name = "tf-tt-sg"
vpc_id = aws_vpc.tt-vpc.id

ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

ingress {
from_port = 8080
to_port = 8080
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}

#создаем шаблон запуска экземпляра EC2 для группы автомасштабирования
resource "aws_launch_template" "tf-tt-launch" {
name = "tf-tt-launch"
image_id = var.ami_id
instance_type = var.instance_type
key_name = "keypair_name"
vpc_security_group_ids = [aws_security_group.tf-tt-sg.id]

tag_specifications {
resource_type = "instance"

tags = {
Name = "NGINX-app"
}
}
user_data = filebase64("nginx.sh")
}

#группа автомасштабирования для запуска минимум двух — максимум трех экземпляров
resource "aws_autoscaling_group" "tf-tt-asg" {
desired_capacity = 2
max_size = 3
min_size = 2
vpc_zone_identifier = [aws_subnet.tt-pubsubnet-1.id, aws_subnet.tt-pubsubnet-2.id]

launch_template {
id = aws_launch_template.tf-tt-launch.id
}

tag {
key = "Name"
value = "tf-tt-asg"
propagate_at_launch = true
}
}

resource "aws_security_group" "mysql-sg" {
name = "mysql-sg"
vpc_id = aws_vpc.tt-vpc.id
ingress {
from_port = 3306
to_port = 3306
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
  • Последняя часть файла main.tf  —  блок ресурсов для создания экземпляра RDS MySQL и группы подсетей для экземпляра RDS. Минимальное значение allocated_storage  —  20, оно относится к бесплатному уровню. По правилам именования, имя экземпляра начинается с буквы, иначе получается ошибка, как у меня. Экземпляру требовался идентификатор группы безопасности, его включили из mysql-sg в предыдущем блоке ресурсов aws_security_group. Учетные данные  —  они ссылаются на созданный ранее для ввода файл переменных  —  обязательно сохраняем в Notepad для последующего тестирования. И в конце включаем идентификаторы подсетей для закрытых подсетей:
#создаем экземпляр RDS MySQL
resource "aws_db_instance" "dbinstance" {
allocated_storage = 20
db_name = "dbinstance"
engine = "mysql"
engine_version = "5.7"
instance_class = "db.t2.micro"

#учетные данные добавляются в Terraform Cloud как конфиденциальные переменные
username = var.db_username
password = var.db_password
vpc_security_group_ids = [aws_security_group.mysql-sg.id]
db_subnet_group_name = "db-subnet-grp"
skip_final_snapshot = true
}

#группа подсетей для экземпляра RDS
resource "aws_db_subnet_group" "db-subnet-grp" {
name = "db-subnet-grp"
subnet_ids = [aws_subnet.tt-privsubnet1.id, aws_subnet.tt-privsubnet2.id]

tags = {
Name = "My DB private subnet group"
}
}

Выходной файл

  • Этот файл необязателен. Создаем, чтобы попрактиковаться с файлами outputs.tf. Статус экземпляра БД проверяем по значению его конечной точки.
  • Создаем файл outputs.tf, копируем и вставляем следующее:
output "db_instance_endpoint" {
description = "The DB instance endpoint"
value = aws_db_instance.dbinstance.endpoint
}

Не забываем сохранять каждый созданный файл, быстро запускаем terraform fmt -recursive, конфигурационные файлы проверяем командой terraform validate. Отправляем файлы обратно в репозиторий. Чтобы в Terraform распознавались конфигурации, создаем запрос на включение изменений и объединяем ветку с главной. Переходим в Terraform Cloud.

Часть Б. Развертывание инфраструктуры в Terraform Cloud

  • При первом входе в бесплатную учетную запись Terraform Cloud настраиваем организацию: start from scratch («Начать с нуля») > name your organization («Ввести название организации») > add email address («Добавить электронную почту») > create organization («Создать организацию»).
  • При повторном входе начинаем с создания новой рабочей области:
  • Выбираем version control workflow («Рабочий процесс контроля версий») > в Connect to VCS («Подключиться к системе контроля версий») подключаемся к Github поставщику системы контроля версий > Choose a repository («Выбрать репозиторий») > Workspace name («Название рабочей области») > default project («Проект по умолчанию») > Create workspace («Создать рабочую область»):
  • Создав рабочую область, настроим переменные. Переходим в Workspace overview («Обзор рабочей области») > Configure variables («Настроить переменные») > Add variable («Добавить переменную») > Environment variable («Переменная среды»). Понадобятся ключ доступа AWS и секретный ключ доступа. В итоге получатся две переменные рабочей области. Посмотрите , что вводится в Key («Ключ»):

Переменная № 1

Key: AWS_ACCESS_KEY_ID

Value: PASTE_YOUR_ACCESS_KEY_ID

  • Нажимаем Add variable («Добавить переменную») и повторяем.

Переменная № 2

Key: AWS_SECRET_ACCESS_KEY

Value: PASTE_YOUR_SECRET_ACCESS_KEY

  • Устанавливаем флажок Sensitive («Конфиденциально»), нажимаем Add variable («Добавить переменную»). Это отличный функционал для защиты конфиденциальных переменных.

Новый запуск

  • Настроив переменные, вернемся в Workspace overview («Обзор рабочей области») > Actions («Действия») > Start new run («Новый запуск»):
  • Появится terraform plan для создаваемых ресурсов с запросом подтверждения и применения плана:
  • У меня получилась ошибка, пришлось корректировать идентификаторы подсетей для группы автомасштабирования. На всякий случай покажу, как устранить ошибку (хотя у вас ее быть не должно, так как код этой статьи исправлен и обновлен):
  • Вот как исправлена ошибка: возвращаемся в среду Cloud9, добавляем идентификаторы подсетей, делаем новый коммит и отправляем изменения обратно в репозиторий Github. В Terraform изменение конфигурационного файла распозналось автоматически, создан новый план:
  • Вот результат, в выходном файле корректно отобразилась конечная точка RDS:

Часть В. Тестирование

  • Протестируем все созданные ресурсы.
  • В дашборде EC2 консоли AWS экземпляры EC2 запустились, инициализация завершена:
  • К одному из экземпляров EC2 подключаемся по SSH:
sudo yum update -y
sudo yum install mariadb
mysql -h <insert.your.RDS.endpoint> -P 3306 -u <insert.db.username> -p
#запрашивается пароль БД.
  • Вывод примерно такой:
  • Протестируем и общедоступный IP-адрес экземпляра EC2 группы автомасштабирования. Веб-сервер NGINX установлен правильно, если появилась эта страница:

Часть Г. Очистка

  • Во избежание лишних затрат ресурсы уничтожаются в Terraform Cloud: на левой панели переходим в > Settings («Настройки») > Destruction and deletion («Уничтожение и удаление») > Queue destroy plan («План уничтожения очереди»).
  • В Workspace overview («Обзоре рабочей области») подтверждаем и применяем план уничтожения ресурсов terraform destroy, получаем подтверждение с указанием уничтоженных ресурсов. Если и при уничтожении группы подсетей БД получается ошибка, снова запускаем план уничтожения. Сначала уничтожается экземпляр БД, потом ее подсеть:

Вот код из репозитория Github.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Avyana Baker: Deploy A Two-Tier Architecture with AWS and Terraform Cloud

Предыдущая статьяКак создать локальное средство генерации кода с open source моделями и библиотекой Guidance от Microsoft
Следующая статьяКак работает JavaScript Proxy