Деплой контейнера от начала до конца
Собери образ, запушь его в ECR, потом запусти — либо сервис ECS/Fargate за ALB, либо управляемый сервис App Runner. Сеньорские детали — две роли, health check и ловушка arm64/amd64.
Образ собрался на ноутбуке, запустился локально, ты его запушил. Через пару минут сервис ECS застрял: desired count 1, running count 0, задачи падают с exec format error. С образом всё в порядке — просто не тот CPU. Ты собрал его на Mac с Apple Silicon (arm64), а задача Fargate — amd64. Ничто в docker push не предупреждает; несовпадение всплывает только когда контейнер пытается запуститься. Деплой от начала до конца — это ровно про такое: каждый переход — сборка, push, запуск, маршрутизация трафика — это место, где счастливый путь тихо ломается.
Собери образ и запушь в ECR
Почему это важно знать до того, как ты касаешься ECS или App Runner? Потому что каждый режим отказа в пайплайне деплоя молчит до рантайма — инструментарий не остановит тебя от отгрузки неправильной архитектуры, push’а в несуществующий репозиторий или аутентификации с просроченным токеном. Знать тихий сбой каждого перехода — это то, что отличает первый деплой от надёжного CI-пайплайна.
ECR (Elastic Container Registry) — приватный Docker-реестр AWS. Перед docker push ты аутентифицируешься: aws ecr get-login-password выдаёт короткоживущий токен, который ты передаёшь в docker login. Затем тегируешь локальный образ полным именем реестра и пушишь.
# 1. Аутентифицируем Docker в приватном реестре ECR (токен живёт ~12ч)
aws ecr get-login-password --region eu-central-1 \
| docker login --username AWS \
--password-stdin 123456789012.dkr.ecr.eu-central-1.amazonaws.com
# 2. Собираем под ту архитектуру, которую ждёт runtime — это и есть ловушка.
# На Mac с Apple Silicon --platform обязателен, иначе отгрузишь arm64 в amd64-Fargate.
docker build --platform linux/amd64 -t myapp:1.0 .
# 3. Тегируем полным URI ECR-репозитория, потом пушим
docker tag myapp:1.0 123456789012.dkr.ecr.eu-central-1.amazonaws.com/myapp:1.0
docker push 123456789012.dkr.ecr.eu-central-1.amazonaws.com/myapp:1.0Здесь кусают две вещи. Первая — репозиторий уже должен существовать: aws ecr create-repository --repository-name myapp, потому что push его не создаёт. Вторая — архитектура: образ собирается под конкретный CPU, и docker push с радостью загрузит arm64-образ в репозиторий, который amd64-задача потом будет тянуть. Падение откладывается до рантайма в виде exec format error. Собирай с явным --platform или используй docker buildx для мульти-арх манифеста и выбери у Fargate runtimePlatform (X86_64 или ARM64), который совпадает.
| Переход | Команда / объект | Тихий сбой, если пропустить |
|---|---|---|
| Аутентификация | ecr get-login-password | docker login | no basic auth credentials при push |
| Сборка | docker build —platform linux/amd64 | arm64-образ → exec format error при запуске |
| Репо существует | ecr create-repository | name unknown / репозиторий не существует |
| Push | docker tag && docker push <uri> | пушишь голый тег, а не URI реестра |
Вместе эти четыре перехода образуют единую цепочку отказов: пропусти любой — и ошибка молчит вплоть до момента, когда контейнер пытается запуститься. Несовпадение архитектуры — самое коварное, потому что проходит все шаги валидации до того, как ядро отвергает бинарник.
Маршрут A — ECS на Fargate за ALB
Fargate запускает твой контейнер, не заставляя тебя управлять серверами, но несколько объектов ты собираешь сам. Определение задачи (task definition) — это чертёж: какой образ, сколько CPU/памяти, какой порт, какие роли, куда идут логи.
{
"family": "myapp",
"requiresCompatibilities": ["FARGATE"],
"networkMode": "awsvpc",
"cpu": "512",
"memory": "1024",
"runtimePlatform": { "cpuArchitecture": "X86_64", "operatingSystemFamily": "LINUX" },
"executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::123456789012:role/myappTaskRole",
"containerDefinitions": [
{
"name": "myapp",
"image": "123456789012.dkr.ecr.eu-central-1.amazonaws.com/myapp:1.0",
"portMappings": [ { "containerPort": 8080, "protocol": "tcp" } ],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/myapp",
"awslogs-region": "eu-central-1",
"awslogs-stream-prefix": "myapp"
}
}
}
]
}Две роли — самая путаная часть ECS, и они не взаимозаменяемы:
- Execution role (
executionRoleArn) — используется агентом ECS при запуске, ещё до того как заработает твой код, чтобы вытянуть образ из ECR и писать логи в CloudWatch. AWS-managed политикаAmazonECSTaskExecutionRolePolicyпокрывает ровно это. Если её нет, задача не стартует: ты видишьCannotPullContainerErrorили отсутствие лог-группы. - Task role (
taskRoleArn) — личность, которую принимает на себя код твоего приложения в рантайме. Именно её используют вызовы твоего SDK, чтобы читать бакет S3, таблицу DynamoDB или секрет. Давай ей минимум привилегий — лишь те действия, что приложению реально нужны; к вытягиванию образа она отношения не имеет.
Затем сервис держит N копий запущенными и связывает их с балансировщиком:
aws ecs create-service \
--cluster prod \
--service-name myapp \
--task-definition myapp \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration 'awsvpcConfiguration={
subnets=[subnet-aaa,subnet-bbb],
securityGroups=[sg-app],
assignPublicIp=DISABLED }' \
--load-balancers 'targetGroupArn=arn:...:targetgroup/myapp/abc,
containerName=myapp,containerPort=8080'Разнеси подсети по двум зонам доступности (AZ), чтобы отказ одной AZ не уронил тебя целиком. Спереди стоит Application Load Balancer (ALB) с target group, чья проверка здоровья (health check) опрашивает путь (например, GET /healthz). ALB шлёт трафик только тем целям, что проходят проверку, и на это опираются rolling-деплои: ECS поднимает задачи новой версии, ждёт, пока они пройдут health check, регистрирует их, потом сливает (drain) и останавливает старые. Слишком строгая, слишком быстрая или указывающая на несуществующий путь проверка заставит каждый деплой откатываться — новые задачи «падают», ECS держит старые, а ты гадаешь, почему твоё изменение так и не выехало.
Маршрут B — App Runner: управляемый короткий путь
Если тебе не нужен контроль ECS, App Runner сворачивает почти весь маршрут A в один сервис. Ты указываешь ему образ в ECR, а он сам разворачивает балансировщик, TLS-сертификат, автомасштабирование и публичный HTTPS-URL — без JSON определения задачи, без target group, без подсетей, которые надо проводить.
aws apprunner create-service \
--service-name myapp \
--source-configuration '{
"ImageRepository": {
"ImageIdentifier": "123456789012.dkr.ecr.eu-central-1.amazonaws.com/myapp:1.0",
"ImageRepositoryType": "ECR",
"ImageConfiguration": { "Port": "8080" }
},
"AutoDeploymentsEnabled": true,
"AuthenticationConfiguration": {
"AccessRoleArn": "arn:aws:iam::123456789012:role/AppRunnerECRAccessRole"
}
}' \
--health-check-configuration '{ "Protocol": "HTTP", "Path": "/healthz" }'У App Runner та же идея двух личностей: access role, чтобы тянуть из ECR (аналог execution role), и опциональная instance role для собственных AWS-вызовов приложения (аналог task role). Он по-прежнему гоняет health check. И по-прежнему заботится об архитектуре — App Runner запускает образ как есть, поэтому arm64-образ не стартует. Размен — контроль на удобство: ты отдаёшь тонкую настройку сети, кастомные правила балансировщика и точное размещение задач, а получаешь рабочий HTTPS-сервис одним вызовом. Тянись к ECS/Fargate, когда нужна интеграция с VPC, сайдкары или точное масштабирование; тянись к App Runner, когда просто хочешь контейнер в интернете.
▸Почему это работает
Почему образ тянет execution role, а не task role? Потому что pull происходит до того, как твой контейнер существует — агенту ECS (инфраструктуре AWS) нужны права на ECR и CloudWatch, чтобы вообще запустить задачу. Task role принимается внутри запущенного контейнера твоим кодом через SDK. Смешать их — классический баг ECS: люди вешают права на S3 на execution role (так агент мог бы читать S3, но приложение — нет) и недоумевают, почему их код получает AccessDenied, хотя задача стартует нормально.
Задача ECS/Fargate сразу падает с 'exec format error' и ни разу не обслуживает запрос. Самая вероятная причина?
Контейнер стартует нормально, но его код получает AccessDenied на вызове s3:GetObject. В определении задачи есть обе роли. Куда вешать права на S3?
- 01Пройди весь путь от локального Docker-образа до живого трафика на ECS/Fargate за ALB.
- 02Объясни execution role против task role и ловушку arm64/amd64 — почему каждая тихо ломается до самого рантайма.
Деплой контейнера в AWS — это четыре перехода, и каждый прячет тихий сбой. Ты собираешь образ — под правильный CPU, потому что docker push отгрузит arm64-образ в amd64-задачу, и узнаешь об этом только в рантайме через exec format error. Пушишь в ECR, предварительно создав репозиторий и аутентифицировавшись через ecr get-login-password в docker login, тегируя полным URI реестра. Потом запускаешь. На ECS/Fargate ты собираешь определение задачи (образ, cpu/память, порт, runtimePlatform, конфигурация логов в CloudWatch через драйвер awslogs) с двумя разными ролями — execution role, которой агент ECS тянет образ и пишет логи, и task role с минимумом привилегий, которую твой код принимает для собственных вызовов AWS, — затем сервис с desired count, подсетями по двум AZ, security group и target group у ALB, чья проверка здоровья пропускает трафик и ведёт rolling-деплои. App Runner сворачивает балансировщик, TLS, автомасштабирование и публичный URL в один вызов create-service, нацеленный на образ в ECR; он сохраняет тот же раздел access-role/instance-role и по-прежнему заботится об архитектуре. Повторяющийся урок: собирай под правильную архитектуру, разделяй две роли и правильно настраивай health check — именно на этих трёх вещах деплои от начала до конца ломаются. Теперь, когда увидишь деплой, застрявший при desired count 1 / running count 0, ты знаешь: сначала проверяешь exec format error, потом execution role, потом путь health check — именно в таком порядке, прежде чем тянуться к любому другому инструменту.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.