Цель этой статьи предоставить вам наглядные примеры и инструкции по разработке динамического модуля ECS (Elastic Container Service) с помощью Terraform. При этом предполагается наличие у вас базового представления о данном инструменте.
“Динамический” в данном случае означает, что Terraform может легко масштабироваться для обработки большего числа сервисов и задач.
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.
Читайте также:
- Как создать бессерверное приложение с помощью AWS Chalice
- LocalStack: запуск AWS на локальном компьютере
- Автоматизация скриптов на Python при помощи AWS Lightsail
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Liam Hartley: How To Create a Dynamic ECS Cluster With Terraform