end
#
# ... еще 20 тысяч строчек...
#
end
Вы сразу же понимаете, что этот код странный и его лучше разбить на ряд небольших автономных функций, каждая из которых делает что-то одно:
def calculate_images_and_predictions(images_test, predicted)
x_pca = PCA(n_components=2).fit_transform(X_train)
clusters = clf.fit_predict(X_train)
ax = plt.subplots(1, 2, figsize=(4))
fig = plt.subplots(3, 4, figsize=(5))
fig.subplots_adjust(top=0.85)
predicted = svc_model.predict(X_test)
return list(zip(images_test, predicted))
end
def process_x_coords(ax)
for x in 0..xlimit
ax[0].scatter(X_pca[x], X_pca[1], c=clusters)
ax[0].set_title('Predicted Training Labels')
ax[1].scatter(X_pca[x], X_pca[1], c=y_train)
ax[1].set_title('Actual Training Labels')
ax[2].scatter(X_pca[x], X_pca[1], c=clusters)
end
return ax
end
def process_y_coords(ax)
for y in 0..ylimit
ay[0].scatter(X_pca[y], X_pca[1], c=clusters)
ay[0].set_title('Predicted Training Labels')
ay[1].scatter(X_pca[y], X_pca[1], c=y_train)
ay[1].set_title('Actual Training Labels')
ay[2].scatter(X_pca[y], X_pca[1], c=clusters)
end
return ay
end
#
# ... множество других мелких функций...
#
Ту же стратегию нужно применять и к Terraform. Представьте, что вы имеете дело с архитектурой, изображенной на рис. 6.1.
Если эта архитектура описана в едином огромном модуле Terraform длиной 20 000 строк, вы должны сразу же почувствовать, что с этим кодом что-то не так. Лучше всего разбить его на ряд небольших автономных модулей, каждый из которых выполняет одну задачу (рис. 6.2).
Модуль webserver-cluster, над которым вы работаете, начинает разрастаться. К тому же он отвечает сразу за три малосвязанные между собой задачи.
• Группа автомасштабирования (ASG). Модуль webserver-cluster развертывает группу ASG, которая умеет выполнять скользящие обновления с нулевым временем простоя.
• Балансировщик нагрузки (ALB). Модуль webserver-cluster развертывает ALB.
• Демонстрационное приложение. Модуль webserver-cluster также развертывает простое демонстрационное приложение.
Рис. 6.1. Относительно сложная архитектура AWS
Рис. 6.2. Относительно сложная архитектура AWS, разбитая на множество мелких модулей
Разделим этот код на три небольших модуля.
• modules/cluster/asg-rolling-deploy. Обобщенный универсальный автономный модуль для развертывания группы ASG, которая умеет выполнять скользящие обновления с нулевым временем простоя.
• modules/networking/alb. Обобщенный универсальный автономный модуль для развертывания ALB.
• modules/services/hello-world-app. Модуль для развертывания демонстрационного приложения.
Прежде чем начинать, не забудьте выполнить команду terraformdestroy для удаления всех копий webserver-cluster, которые могли остаться с предыдущих глав. После этого можете приступать к написанию модулей asg-rolling-deploy и alb. Создайте новую папку modules/cluster/asg-rolling-deploy и переместите (скопируйте и вставьте) следующие ресурсы из файла module/services/webserver-cluster/main.tf в modules/cluster/asg-rolling-deploy/main.tf:
•aws_launch_configuration;
• aws_autoscaling_group;
• aws_autoscaling_schedule (оба экземпляра);
• aws_security_group (для серверов, но не для ALB);
• aws_security_group_rule (оба правила для серверов, но не те, что для ALB);
•aws_cloudwatch_metric_alarm (оба экземпляра).
Далее переместите следующие переменные из файла module/services/webserver-cluster/variables.tf в modules/cluster/asg-rolling-deploy/variables.tf:
•cluster_name;
• ami;
• instance_type;
• min_size;
• max_size;
• enable_autoscaling;
• custom_tags;
•server_port.
Теперь перейдем к модулю ALB. Создайте новую папку modules/networking/alb и переместите следующие ресурсы из файла module/services/webserver-cluster/main.tf в modules/networking/alb/main.tf:
•aws_lb;
• aws_lb_listener;
• aws_security_group (тот, что для ALB, но не те, что для серверов);
•aws_security_group_rule (оба правила для ALB, но не те, что для серверов).
Создайте файл modules/networking/alb/variables.tf и объявите в нем одну переменную:
variable "alb_name" {
description = "The name to use for this ALB"
type = string
}
Используйте эту переменную в качестве аргумента name для ресурса aws_lb:
resource "aws_lb" "example" {
name = var.alb_name
load_balancer_type = "application"
subnets = data.aws_subnet_ids.default.ids
security_groups = [aws_security_group.alb.id]
}
и аргумента name для ресурса aws_security_group:
resource "aws_security_group" "alb" {
name = var.alb_name
}
Мы перетасовали много кода, поэтому можете воспользоваться примерами для данной главы на странице github.com/brikis98/terraform-up-and-running-code.
Компонуемые модули
Теперь у вас есть два небольших модуля, asg-rolling-deploy и alb, каждый из которых хорошо делает что-то одно. Как их объединить? Как создавать компонуемые модули, пригодные к повторному использованию? Этот вопрос актуален не только для Terraform — программисты размышляют о нем на протяжении десятилетий. Процитирую Дуга Макилроя52, создателя Unix-каналов и ряда других инструментов для Unix, включая diff, sort, join и tr:
Это философия Unix: пишите программы, которые делают что-то одно, и делают это хорошо. Пишите программы, которые могут работать вместе.
Дуг Макилрой
Один из способов этого добиться — использовать композицию функций. Это когда вы можете взять вывод одной функции и передать его в качестве ввода другой. Представьте, что у вас есть следующие небольшие функции на Ruby:
# Простая функция для сложения
def add(x, y)
return x + y
end
# Простая функция для вычитания
def sub(x, y)
return x - y
end
# Простая функция для умножения
def multiply(x, y)
return x * y
end
С помощью композиции функций вы можете их скомпоновать, передав вывод add и sub на вход multiply:
# Сложная функция, объединяющая несколько более простых
def do_calculation(x, y)
return multiply(add(x, y), sub(x, y))
end
Одним из основных способов сделать функции компонуемыми является минимизация побочных эффектов. Для этого по возможности следует избегать чтения состояния извне, а вместо этого передавать его через входные параметры. Кроме того, вместо записи состояния вовне лучше возвращать результаты своих вычислений через выходные параметры. Минимизация побочных эффектов — это один из основных принципов функционального программирования, который упрощает понимание, тестирование и повторное использование кода. Последнее особенно важно, так как благодаря композиции сложная функция может представлять собой комбинацию более простых.
И хотя избежать побочных эффектов при работе с инфраструктурным кодом нельзя, вы все равно можете следовать тем же основным правилам в своем коде Terraform: передавайте все в виде входных переменных, возвращайте все через выходные и создавайте сложные модули на основе более простых.
Откройте файл modules/cluster/asg-rolling-deploy/variables.tf и добавьте четыре новые входные переменные:
variable "subnet_ids" {
description = "The subnet IDs to deploy to"
type = list(string)
}
variable "target_group_arns" {
description = "The ARNs of ELB target groups in which to register Instances"
type = list(string)
default = []
}
variable "health_check_type" {
description = "The type of health check to perform. Must be one of: EC2, ELB."
type = string
default = "EC2"
}
variable "user_data" {
description = "The User Data script to run in each Instance at boot"
type = string
default = ""
}
Первая переменная, subnet_ids, говорит модулю asg-rolling-deploy о том, в каких подсетях нужно развертываться. Если в модуле webserver-cluster подсети и VPC по умолчанию были прописаны прямо в коде, то этот модуль можно использовать в любых VPC и подсетях благодаря переменной subnet_ids, которая доступна извне. Следующие две переменные, target_group_arns и health_check_type, определяют то, как ASG интегрируется с балансировщиком нагрузки. Если у модуля webserver-cluster был встроенный экземпляр ALB, asg-rolling-deploy задумывался как универсальный модуль, поэтому выбор параметров балансировщика нагрузки с помощью входных переменных позволяет применять ASG в широком спектре сценариев: например, без ALB, с одним балансировщиком, с несколькими NLB и т. д.
Возьмите эти три входные переменные и передайте их ресурсу aws_autoscaling_group в файле modules/cluster/asg-rolling-deploy/main.tf, заменив вручную прописанные параметры, которые ссылались на ресурсы (скажем, ALB) и источники данных (вроде aws_subnet_ids) и не были скопированы в наш модуль asg-rolling-deploy:
resource "aws_autoscaling_group" "example" {
# Напрямую зависит от имени конфигурации запуска, поэтому вместе
# с ним всегда нужно заменять и эту группу ASG
name = "${var.cluster_name}-${aws_launch_configuration.example.name}"
launch_configuration = aws_launch_configuration.example.name
vpc_zone_identifier = var.subnet_ids
# Настраиваем интеграцию с балансировщиком нагрузки
target_group_arns = var.target_group_arns
health_check_type = var.health_check_type
min_size = var.min_size
max_size = var.max_size