Terraform: инфраструктура на уровне кода — страница 39 из 65

  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