Внутренности ECS: жизненный цикл задачи, сеть awsvpc, capacity providers
Task definition — неизменный чертёж, task — работающий экземпляр, service сводит N к желаемому числу. Задачи проходят жизненный цикл; awsvpc даёт каждой свой ENI (конечный ресурс); capacity providers выбирают Fargate или EC2; а stoppedReason говорит, почему задача умерла.
Трафик Чёрной пятницы удваивается, ты поднимаешь desired count сервиса с 20 до 60 и смотришь. Сорок новых задач застревают в PROVISIONING и не двигаются. ALB всё ещё обслуживает 20 здоровых задач, но они захлёбываются, p99 растёт, и часть запросов начинает отваливаться по таймауту. Ты открываешь вкладку событий, и вот оно, снова и снова: RESOURCE:ENI. С образом, ролями и кодом всё в порядке — ты исчерпал свободные IP-адреса в подсети, и ECS не может прицепить сетевой интерфейс к задаче, которую некуда воткнуть. Тот самый scale-out, которым ты тянулся спасти запуск, и роняет трафик. Вот что живёт под счастливым путём деплоя: планировщик, сводящий желаемое к работающему, против VPC с жёсткими счётными лимитами.
Task definition, task, service — и жизненный цикл планировщика
Три слова используют как синонимы, а это не одно и то же. Task definition — неизменный версионированный чертёж: какие контейнеры, их образ, CPU/память, две IAM-роли, сетевой режим, маппинг портов, конфиг логов. Регистрация даёт нумерованную ревизию (myapp:7), которую ты не правишь — ты регистрируешь новую ревизию. Task — один работающий экземпляр ревизии: реальные контейнеры на реальной ёмкости с реальным сетевым интерфейсом. Service — контроллер, который держит N задач заданного определения, регистрирует их в балансировщике и ведёт автомасштабирование. Сервис крутит непрерывный цикл сведения (reconciliation): сравнивает desired count с running count, и если они различаются, запускает или останавливает задачи, пока не сойдутся, — та же модель схождения к желаемому состоянию, что и у контроллера Kubernetes.
Задача не возникает мгновенно. Она проходит жизненный цикл, и знание состояний говорит, где застрявшая задача застряла:
PROVISIONING → PENDING → ACTIVATING → RUNNING
→ DEACTIVATING → STOPPING → DEPROVISIONING → STOPPEDВ PROVISIONING ECS готовит то, что задаче нужно до запуска — для awsvpc-задач здесь выделяется и прицепляется ENI. PENDING — состояние ожидания ёмкости: планировщик ждёт ресурсов или агента. ACTIVATING — здесь агент тянет образы, создаёт контейнеры, настраивает сеть и регистрирует таргеты балансировщика. RUNNING — установившийся режим. На спуске DEACTIVATING сливает задачу из балансировщика, STOPPING шлёт SIGTERM (затем SIGKILL по таймауту остановки), DEPROVISIONING отцепляет и удаляет ENI, а STOPPED — терминальное. Выигрыш по разбору сбоев: задача, застрявшая в PROVISIONING, — это проблема ресурса (нет ENI, нет IP), а задача, циклящая RUNNING → STOPPED, — проблема рантайма (краш, проваленный health check). Состояние называет слой.
{
"family": "checkout",
"requiresCompatibilities": ["FARGATE"],
"networkMode": "awsvpc",
"cpu": "1024",
"memory": "2048",
"executionRoleArn": "arn:aws:iam::111122223333:role/ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::111122223333:role/checkoutTaskRole",
"containerDefinitions": [
{
"name": "checkout",
"image": "111122223333.dkr.ecr.eu-central-1.amazonaws.com/checkout:7",
"secrets": [
{ "name": "DB_PASSWORD", "valueFrom": "arn:aws:secretsmanager:eu-central-1:111122223333:secret:checkout/db-AbCdEf" }
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/checkout",
"awslogs-region": "eu-central-1",
"awslogs-stream-prefix": "checkout"
}
}
}
]
}Обрати внимание на две роли — их путаница это самый частый баг ECS. executionRoleArn принимается агентом ECS — инфраструктурой AWS — при запуске, до того как заработает твой код, чтобы вытянуть образ из ECR, писать в CloudWatch Logs и расшифровать значения secrets из Secrets Manager / SSM. taskRoleArn принимается внутри контейнера кодом твоего приложения для его собственных вызовов AWS (читать S3, запрашивать DynamoDB). Они не взаимозаменяемы: нехватка прав у execution role означает, что задача вообще не стартует (CannotPullContainerError или ResourceInitializationError при расшифровке секрета); нехватка прав у task role означает, что приложение работает, но получает AccessDenied на вызове API.
▸Почему это работает
Почему образ тянет execution role, а не task role? Потому что вытягивание происходит до того, как существует твой контейнер. Агенту ECS (инфраструктуре под управлением AWS) нужны права ECR + CloudWatch + Secrets Manager, чтобы вообще собрать задачу — притянуть слои, создать лог-стрим, подставить расшифрованный секрет как переменную окружения. И только когда контейнер уже работает, твой код принимает task role через цепочку учётных данных SDK (эндпоинт container-credentials на 169.254.170.2). Классический сбой: инженер ловит AccessDenied на s3:GetObject, «чинит» это, навесив S3-политику на execution role, ошибка не сдвигается, и он в недоумении — теперь агент может читать S3, но не агент его вызывает. Права на S3 принадлежат task role. Обратный симптом: задача, застрявшая в PENDING с CannotPullContainerError, — это проблема execution role (или сети), но никогда task role.
Сеть awsvpc: один ENI на задачу, и ENI конечны
Зачем отдельный раздел про сетевой режим? Потому что awsvpc даёт тебе полноправного гражданина VPC на каждую задачу — свою security group, свой IP, свои flow-логи, — но эта элегантность имеет жёсткую счётную цену, которая приводит к инцидентам ровно в худший момент: когда ты масштабируешься.
В режиме awsvpc — обязательном для Fargate, современном дефолте для EC2 — каждая задача получает собственный сетевой интерфейс (ENI, Elastic Network Interface — виртуальный сетевой адаптер в VPC) с приватным IP в твоей подсети. В этом и фича: задача — полноправный гражданин VPC со своей security group, своим IP и прямыми flow-логами, вместо того чтобы делить интерфейс хоста и жонглировать портами (режимы bridge/host). Цена в том, что ENI и IP — реальные счётные ресурсы, и оба можно исчерпать.
Кусают два лимита. Первый — IP подсети: подсеть /24 имеет 256 адресов, AWS резервирует 5, остаётся 251 пригодных — то есть максимум ~251 awsvpc-задача на всё, что делит эту подсеть, и именно тут умер scale-out из вступления (RESOURCE:ENI поднимается и при исчерпании IP). Второй — на ёмкости EC2 число ENI на инстанс ограничено типом: c5.large допускает 3 ENI всего, первичный считается одним, так что на нём можно запустить лишь 2 awsvpc-задачи — плотная упаковка крошечных задач упирается в стену ENI задолго до CPU или памяти. ENI trunking (настройка аккаунта awsvpcTrunking) цепляет общий транковый интерфейс, и тот же c5.large прыгает до эффективных 10 задач, но только на инстансах, запущенных после включения. Fargate обходит лимит ENI на инстанс целиком — каждая задача получает выделенный ENI, сколько бы ты ни запустил — но каждая всё равно потребляет IP подсети, так что Fargate не спасает от исчерпания подсети. Сеньорский рефлекс: размеряй подсети под пиковое число задач плюс запас на деплой (rolling-деплои кратко держат новые + старые) и бери /22 или /21 для всего, что масштабируется, а не /26.
# Почему scale-out застрял? Читай события сервиса — они называют причину.
aws ecs describe-services --cluster prod --services checkout \
--query "services[0].events[0:5].message"
# => "... unable to place a task because no container instance met all of
# its requirements ... has insufficient ENI capacity. RESOURCE:ENI"
# Сколько свободных IP в подсети задачи прямо сейчас?
aws ec2 describe-subnets --subnet-ids subnet-aaa \
--query "Subnets[0].{Cidr:CidrBlock,Free:AvailableIpAddressCount}"
# => { "Cidr": "10.0.1.0/24", "Free": 0 } <-- вот твой инцидентCapacity providers: Fargate против EC2 и как выбрать
Capacity provider отвечает на вопрос «где задачи на самом деле работают?». Есть два семейства. Fargate — serverless: нет хостов для патчинга, масштабирования и упаковки; ты платишь посекундно за vCPU/память задачи (минимум 1 минута); это быстрее всего в эксплуатации и правильный дефолт. Цена — наценка за vCPU поверх голого EC2 и меньше контроля: нет daemon-контейнеров, нет GPU, ограничены типы инстансов и тюнинг ядра. Capacity providers на EC2 запускают твои инстансы в Auto Scaling-группе перед провайдером, который ведёт managed scaling (растит/сжимает ASG под ожидающие задачи) и managed termination protection (не сожмёт инстанс, на котором ещё крутятся задачи). EC2 дешевле при постоянной высокой утилизации, поддерживает Spot для экономии ~70% на прерываемой работе, GPU и daemonset-ы — но патчинг, AMI, ту самую математику плотности ENI и упаковку кластера ты держишь сам. Fargate Spot даёт ту же скидку за прерываемость без управления хостами, для задач, терпящих 2-минутное уведомление о завершении.
{
"capacityProviders": ["FARGATE", "FARGATE_SPOT"],
"defaultCapacityProviderStrategy": [
{ "capacityProvider": "FARGATE", "base": 4, "weight": 1 },
{ "capacityProvider": "FARGATE_SPOT", "base": 0, "weight": 4 }
]
}Эта стратегия читается так: держи гарантированную базу из 4 задач на on-demand Fargate (твой пол, никогда не прерывается), а всё сверх базы дели 1:4 — на каждую 1 on-demand-задачу запускай 4 на Spot. Это канонический паттерн «базовый on-demand держит, дешёвый Spot бёрстит», и та же форма работает для микса EC2 + EC2-Spot.
Платформенная команда держит ~120 маленьких контейнерных сервисов. Большинство всплесковые и низконагруженные, горстка постоянно высоконагружена 24/7, и есть ночной батч-слой, полностью прерываемый. Команда маленькая и явно хочет минимизировать операционную рутину. Выбери основную стратегию ёмкости.
Почему задача не стартует: чтение stoppedReason
Когда задача умирает, ECS записывает на остановленной задаче почему: грубый stopCode и человекочитаемый stoppedReason. Это сеньорская точка входа в отладку — ты читаешь их до того, как гадать.
aws ecs describe-tasks --cluster prod --tasks <task-arn> \
--query "tasks[0].{Code:stopCode,Reason:stoppedReason,
Containers:containers[].{N:name,Reason:reason,Exit:exitCode}}"Чек-лист, привязанный к тому, что увидишь:
CannotPullContainerError— провал вытягивания образа. Причины: отсутствует/недостаточна execution role, приватная подсеть без NAT/VPC-эндпоинтов для доступа к ECR (очень частое), неверный тег образа или несовпадениеarm64/amd64. Задача умирает вPENDING/ACTIVATING.RESOURCE:ENI/RESOURCE:MEMORY/RESOURCE:CPU(stopCode: TaskFailedToStart/ событие размещения) — в кластере нет места: исчерпаны IP подсети (ENI), либо нет EC2-инстанса с достаточным CPU/памятью. Застревает вPROVISIONING/PENDING.ResourceInitializationError— обычно расшифровка значенияsecrets: у execution role нетsecretsmanager:GetSecretValue/ssm:GetParameters(илиkms:Decryptна ключе).Essential container ... exitedс ненулевымexitCode— приложение упало или провалило container/ALB health check, и ECS его убил. В сервисе это становится петлёй деплоя: новые задачи проваливают проверку, ECS не повышает их, старые остаются. Deployment circuit breaker (enable: true, rollback: true) ловит петлю и откатывает на последнюю хорошую ревизию вместо вечного перемалывания.
Числа, которые держать в голове: задача Fargate обычно в RUNNING за десятки секунд (прицеп ENI + вытягивание образа); само вытягивание — секунды-минуты по размеру образа, поэтому слим-образы и pull-through cache важны. Таймаут остановки (SIGTERM → SIGKILL) по умолчанию 30 секунд. Тут нет гаданий — stoppedReason указывает на слой; ты чинишь этот слой.
Контейнер стартует и работает, но его код ловит AccessDenied на s3:GetObject. В task definition есть и execution role, и task role. Куда вешать права на S3, и что сделало бы навешивание их на execution role?
- 01Различи task definition, task и service, затем пройди жизненный цикл и скажи, о чём говорит застревание в PROVISIONING против петли RUNNING→STOPPED.
- 02Объясни execution role против task role, почему ENI в awsvpc конечны и как stoppedReason ведёт отладку задачи, которая не стартует.
В ECS три слова, которые путают: task definition — неизменный версионированный чертёж (контейнеры, образ, CPU/память, две роли, сетевой режим, порты, логи); task — один работающий экземпляр ревизии; service — контроллер, сводящий desired count из N задач, привязывающий их к балансировщику и автомасштабирующий. Задача проходит жизненный цикл — PROVISIONING (прицеп ENI/IP) → PENDING (ждёт ёмкость) → ACTIVATING (тянет образ, создаёт контейнеры) → RUNNING → DEACTIVATING (слив) → STOPPING (SIGTERM, затем SIGKILL через ~30с) → DEPROVISIONING (отцеп ENI) → STOPPED — и где она застряла, называет слой: PROVISIONING/PENDING — проблема ресурса, петля RUNNING→STOPPED — проблема рантайма. Две роли — классическая ловушка: execution role это личность агента (тянуть образ, писать логи, расшифровать секреты), используемая до кода, а task role — личность приложения в рантайме для его собственных вызовов AWS — права на S3 принадлежат task role, и навешивание их на execution role даёт читать S3 агенту, но никогда коду. Режим awsvpc делает каждую задачу полноправным гражданином VPC со своим ENI и security group, но ENI и IP конечны: /24 даёт 251 пригодный IP, c5.large без trunking вмещает лишь 2 awsvpc-задачи (~10 с awsvpcTrunking), а Fargate обходит лимит ENI на инстанс, но всё равно жжёт IP подсети — так что scale-out в маленькую подсеть застревает в PROVISIONING с RESOURCE:ENI. Capacity providers выбирают, где задачи работают: Fargate (нет хостов, посекундная тарификация, наценка, меньше контроля) против EC2 (дешевле при постоянном масштабе, Spot для ~70% экономии, GPU и daemonset-ы, но патчинг и упаковку держишь сам), часто смешанные стратегией «базовый on-demand плюс Spot». А когда задача умирает, stopCode и stoppedReason называют слой сбоя — CannotPullContainerError, RESOURCE:ENI, ResourceInitializationError или essential-контейнер, вышедший ненулевым — так что ты чинишь этот слой вместо гадания и даёшь deployment circuit breaker откатить проваленный деплой. Теперь, когда увидишь задачи, застрявшие в PROVISIONING, первая команда — aws ecs describe-services --query "events", а не перезапуск деплоя или пересборка образа. Сообщение события называет слой; ты чинишь этот слой.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.