Создание динамического кластера ECS с помощью Terraform



Книга Создание динамического кластера ECS с помощью Terraform

Цель этой статьи предоставить вам наглядные примеры и инструкции по разработке динамического модуля ECS (Elastic Container Service) с помощью Terraform. При этом предполагается наличие у вас базового представления о данном инструменте.


“Динамический” в данном случае означает, что Terraform может легко масштабироваться для обработки большего числа сервисов и задач.


Архитектура ECS (источник)

0. Сеть


Определение самого кластера ECS не содержит сетевых требований.


resource "aws_ecs_cluster" "cluster" {
name = "${var.environment}-cluster"
tags = var.tags
}

Дело в том, что сетевые функции определены на уровне сервиса, так как относятся к эластичным компонентам, в которых размещаются задачи. https://nuancesprog.ru/media/f42f8dce5d069d978a2140db80cc6a34Обзор ECS (англ.) 


Сетевые требования указаны в определении сервиса:


resource "aws_ecs_service" "fargate-microservices" {
for_each = var.create_microservices == true ? var.fargate_microservices : {}
name = each.value["name"]
cluster = aws_ecs_cluster.cluster.id
desired_count = each.value["desired_count"]
launch_type = each.value["launch_type"]
depends_on = [aws_ecs_cluster.cluster,
aws_ecs_task_definition.ecs_tasks]
task_definition = each.value["task_definition"]

network_configuration {
subnets = var.ecs_service_subnets
security_groups = [aws_security_group.ecs_security_groups[each.value["security_group_mapping"]].id]
}

lifecycle {
ignore_changes = [
task_definition
]
}
}

Здесь нужно многое объяснить, и я начну с конфигурации сети.


network_configuration {
subnets = var.ecs_service_subnets
security_groups = [aws_security_group.ecs_security_groups[each.value["security_group_mapping"]].id]
}

В этом инстансе (закрытые) подсети наследуются от модуля, определяющего наш VPC. При создании динамического кластера мы просто ссылаемся на список подсетей в модуле VPC, чтобы создать сервисы в собственном предпочтительном VPC.


Группы безопасности задействуют функцию for_each в Terraform, лежащую в основе многих механик этого модуля.


for_each


Данная функциональность позволяет создавать несколько ресурсов, использующих общие аргументы. Модулю требуется только, чтобы for_each был определен внутри ресурса, и переменная карты передавалась в указанный аргумент.


for_each      = var.create_microservices == true ? var.fargate_microservices : {}

В этом случае мы указали, что для создания сервисов create_microservice должен быть true. Далее переменная fargate_microservices выступает в качестве карты, содержащей все необходимые сервису аргументы (пример приводится в разделе “Динамические сервисы”).


Группы безопасности (security groups)


Теперь давайте рассмотрим код модуля, позволяющий определять необходимое число групп безопасности:


resource "aws_security_group" "ecs_security_groups" {
vpc_id = var.vpc_id

for_each = var.security_groups
name = "${var.environment}-${each.value["ingress_port"]}"

ingress {
from_port = each.value["ingress_port"]
to_port = each.value["ingress_port"]
protocol = each.value["ingress_protocol"]
cidr_blocks = each.value["ingress_cidr_blocks"]
}

egress {
from_port = each.value["egress_port"]
to_port = each.value["egress_port"]
protocol = each.value["egress_protocol"]
cidr_blocks = each.value["egress_cidr_blocks"]
}

tags = var.tags
}

Этот блок ресурса будет перебирать определенный вне модуля объект var.security_groups и выбирать переменную для каждой переменной с приставкой each.value.


Вот как определяется одна группа безопасности внутри модуля:


"ecs_security_groups": {
"prod-ecs-sg": {
"ingress_port": "redacted",
"ingress_protocol": "redacted",
"ingress_cidr_blocks": [
"redacted"
],
"egress_port": "redacted",
"egress_protocol": "redacted",
"egress_cidr_blocks": [
"redacted"
]
}
}

Затем эти инструкции отображаются в соответствующие им сервисы через переменную security_group_mapping внутри каждого сервиса. Эта переменная сопоставляет id группы безопасности (pro-ecs-sg) с указанным сервисом. Таким образом можно легко добавить еще одну группу безопасности, просто присоединив этот объект карты.


Теперь мы еще раз вернемся к определению сервиса, чтобы посмотреть, как переменная security_group_mapping используется совместно с другими динамическими переменными.


1. Динамические сервисы


Разобравшись с настройками сети, давайте еще раз взглянем на определение сервиса:


resource "aws_ecs_service" "fargate-microservices" {
for_each = var.create_microservices == true ? var.fargate_microservices : {}
name = each.value["name"]
cluster = aws_ecs_cluster.cluster.id
desired_count = each.value["desired_count"]
launch_type = each.value["launch_type"]
depends_on = [aws_ecs_cluster.cluster,
aws_ecs_task_definition.ecs_tasks]
task_definition = each.value["task_definition"]

network_configuration {
subnets = var.ecs_service_subnets
security_groups = [aws_security_group.ecs_security_groups[each.value["security_group_mapping"]].id]
}}

Обратили внимание на переменную depends_on? Этот список переменных обеспечивает создание задач и кластера до создания сервиса. В отсутствие любого из этих ресурсов собрать сервисы не удастся.


Как уже говорилось, для создания сервисов необходимо, чтобы переменная create_microsrvices была установлена как true. Если так и есть, то далее мы передаем карту переменных для определения наших сервисов:


"fargate_microservices": {
"prod-service-one": {
"name": "prod-service-one",
"task_definition": "prod-task-one",
"desired_count": "1",
"launch_type": "FARGATE",
"security_group_mapping": "prod-ecs-sg"
},
"prod-service-two": {
"name": "prod-service-two",
"task_definition": "prod-task-two",
"desired_count": "1",
"launch_type": "FARGATE",
"security_group_mapping": "prod-ecs-sg"
}
}

Это похоже на магию, потому что теперь для создания нового сервиса потребуется лишь добавить в эту карту новый элемент. 


Переменные определяются следующим образом:


variable "fargate_microservices" {
description = "Map of variables to define a Fargate microservice."
type = map(object({
name = string
task_definition = string
desired_count = string
launch_type = string
security_group_mapping = string
}))

В launch_type вы определяете, как ваш кластер должен запускать контейнеры: используя AWS Fargate или EC2. Чтобы понять, какой тип запуска лучше подходит под ваши требования, ознакомьтесь с руководством по этой ссылке (англ.)


Так как же мы создаем динамические задачи?


Общая схема задач, сервисов и кластеров (источник)

2. Динамические задачи


Вот полное определение динамических задач (англ.):


resource "aws_ecs_task_definition" "ecs_tasks" {
for_each = var.create_tasks == true ? var.ecs_tasks : {}
family = each.value["family"]
container_definitions = templatefile(each.value["container_definition"], "${merge("${var.extra_template_variables}",
{
container_name = each.value["family"],
docker_image = "${var.docker_image}:${var.docker_tag}",
aws_logs_group = "/aws/fargate/${aws_ecs_cluster.cluster.name}/${each.value["family"]}/${var.environment}",
aws_log_stream_prefix = each.value["family"],
aws_region = var.region,
container_port = each.value["container_port"]
})}")

task_role_arn = aws_iam_role.ecs_task_role.arn
network_mode = var.task_definition_network_mode
cpu = each.value["cpu"]
memory = each.value["memory"]
requires_compatibilities = [var.ecs_launch_type == "FARGATE" ? var.ecs_launch_type : null]
execution_role_arn = aws_iam_role.ecs_execution_role.arn

tags = merge({
"Name" = "${each.value["family"]}-${var.environment}"
"Description" = "Task definition for ${each.value["family"]}"
}, var.tags
)
}

В самом верху снова задействуется for_each, для чего var.create_tasks должна быть установлена как true, чтобы прочитать объект карты var.ecs_tasks.


Сложнейшая часть этого проекта в создании динамических container_definitions. Эти переменные определяют выполняющие ваши задачи образы. 


container_definitions = templatefile(each.value["container_definition"], "${merge("${var.extra_template_variables}",
{
container_name = each.value["family"],
docker_image = "${var.docker_image}:${var.docker_tag}",
aws_logs_group = "/aws/fargate/${aws_ecs_cluster.cluster.name}/${each.value["family"]}/${var.environment}",
aws_log_stream_prefix = each.value["family"],
aws_region = var.region,
container_port = each.value["container_port"]
})}")

Аргумент container_defintions получает объект JSON, где определено, какой образ docker запускать, а также дополнительные необходимые переменные среды.


Все это помещается в тот же динамический цикл for_each, что и остальная часть ресурса, а затем совмещается с extra_template_variables в объекте JSON, позволяя динамическое обнаружение переменных среды. 


Передаваемый в модуль объект карты для задач очень похож на объект сервиса:


"azure_ecs_tasks": {
"prod-task-one": {
"family": "prod-task-one",
"container_definition": "./templates/task-definition-one.json",
"cpu": "1024",
"memory": "4096",
"container_port": "redacted"
},
"prod-task-two": {
"family": "prod-task-two",
"container_definition": "./templates/task-definition-two.json",
"cpu": "1024",
"memory": "4096",
"container_port": "redacted"
}
}

Опять же здесь легко внедрить новую задачу и отобразить ее обратно на соответствующий сервис (имя семейства), просто добавив дополнительный элемент. 


Объект карты для задач определяется в переменных схожим с динамическими сервисами образом:


variable "ecs_tasks" {
description = "Map of variables to define an ECS task."
type = map(object({
family = string
container_definition = string
cpu = string
memory = string
container_port = string
}))
}

3. Динамические логи


Вы могли заметить, что блок ресурса, определяющий задачи, содержит аргумент log_group.


aws_logs_group        = "/aws/fargate/${aws_ecs_cluster.cluster.name}/${each.value["family"]}/${var.environment}",

Здесь указывается, куда должны отправляться логи для соответствующей задачи. Чтобы гарантировать правильную настройку этих групп логов, нужно определить динамический ресурс Cloudwatch:


resource "aws_cloudwatch_log_group" "cw" {
name = "/aws/fargate/${aws_ecs_cluster.cluster.name}/${var.environment}"
retention_in_days = var.cw_logs_retention
tags = merge({
"name" = "${aws_ecs_cluster.cluster.name}-${var.environment}"
"description" = "Task definition for ${aws_ecs_cluster.cluster.name}"
}, var.tags
)
}

Пока имя этих ресурсов будет соответствовать значению ключа aws_log_group внутри определения задач, мы будем получать логи в Cloudwatch.


4. IAM


Если для вас это полностью новый этап настройки, то в реализуемом нами решении понадобится определить две новых роли IAM. Ими являются task role и execution role динамических задач. 


Task role определяет, с какими ресурсами AWS ваша задача может взаимодействовать. Ниже приведены соответствующие блоки данных и ресурса.


Данные (policy):


data "aws_iam_policy_document" "ecs_task_policy" {
statement {
effect = "Allow"

principals {
type = "Service"
identifiers = ["ecs-tasks.amazonaws.com"]
}

actions = ["sts:AssumeRole"]
}
}

Ресурс (role):


resource "aws_iam_role" "ecs_task_role" {
name = "${var.environment}-ecs-task-role"

assume_role_policy = data.aws_iam_policy_document.ecs_task_policy.json
permissions_boundary = "arn:aws:iam::<account>:policy/<policy>"
tags = merge({
"name" = "${var.environment}"
}, var.tags
)
}

Мы позволили задаче также вызывать AssumeRole через Security Token Service, чтобы она могла задействовать временные учетные данные для доступа к другим сервисам. 


Execution role устанавливает доступ для агента контейнера ECS и демона Docker:


resource "aws_iam_role" "ecs_execution_role" {
name = "${var.environment}-exec-task-role"
assume_role_policy = data.aws_iam_policy_document.ecs_task_policy.json
permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/ccoe/developer"
tags = merge({
"name" = "${var.environment}"
}, var.tags
)
}

Этой роли мы дали те же разрешения, что и предыдущей, с помощью того же объекта данных (ecs_task_policy).


Заключение


Рассмотренный модуль позволил моей команде ускоренно развертывать новые задачи и сервисы ECS, не требуя ручного изменения всех настроек через GUI.


Сам модуль активно задействует аргумент for_each внутри Terraform для масштабирования по мере необходимости. Такая структура оказывается очень полезна при реализации широкомасштабных решений Terraform.




1021   0  

Comments

    Ничего не найдено.