AWS

Docker та контейнеризація в AWS — ECR, ECS та Fargate

Від локального Dockerfile до повноцінного задеплоєного .NET Web API на AWS ECS Fargate. Розбираємо ECR, ECS Task Definitions, Clusters, Services, Auto Scaling та порівнюємо ECS, Fargate і EKS.

Docker та контейнеризація в AWS

Передумови

Для виконання практичних завдань у цьому розділі необхідне таке програмне забезпечення та конфігурація:

ІнструментВерсіяПризначення
AWS CLI v22.xКерування ресурсами AWS з терміналу
Docker Desktop4.x+Збірка та локальний запуск контейнерів
.NET SDK 88.0+Компіляція ASP.NET Core проєктів
AWS-акаунтДоступ до ECR, ECS, IAM, CloudWatch

Перевірте налаштування AWS CLI перед початком:

aws configure list      # переглянути поточний профіль
aws sts get-caller-identity   # перевірити підключення до акаунту

Користувач або роль, від імені якої ви працюєте, повинна мати права на: ecr:*, ecs:*, iam:CreateRole, iam:AttachRolePolicy, ec2:DescribeVpcs, ec2:DescribeSubnets, ec2:CreateSecurityGroup, elasticloadbalancing:*, logs:CreateLogGroup.


Проблема, яку вирішують контейнери

Уявіть класичну ситуацію: ваш .NET Web API ідеально працює на вашому MacBook. Ви передаєте код колезі — і він не запускається, бо у нього інша версія .NET Runtime. Ви деплоїте на сервер з Ubuntu — і знову проблеми, бо там встановлені інші версії бібліотек. DevOps-інженер каже: «У мене все працює» — але ж середовища різні!

Це фундаментальна проблема розробки програмного забезпечення, відома як «works on my machine» — «у мене на машині працює». Десятиліттями це вирішувалось через детальну документацію налаштування середовища, складні інструкції для DevOps, або просто через «ну, запустіть ще раз».

Docker вирішує цю проблему радикально: замість того, щоб передавати код і сподіватись, що середовище збігається — ви передаєте весь застосунок разом із середовищем. Код, .NET Runtime, бібліотеки, системні залежності — все упаковане в один незмінний артефакт, який однаково запускається на будь-якій машині.

Цей артефакт називається Docker-образом (image), а запущений екземпляр образу — контейнером (container).

Docker та контейнеризація — окрема велика тема, яку ви, можливо, вже вивчали або будете вивчати детально. У цьому модулі ми зосередимось на тому, як використовувати контейнери в AWS: де зберігати образи, як запускати їх у хмарі, як масштабувати та оновлювати без простоїв.

Контейнер vs Віртуальна машина

Важливо відразу розуміти різницю між контейнером і віртуальною машиною (VM), яку ми вивчали у попередньому модулі (EC2).

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

package "Віртуальна машина (VM)" #fef3c7 {
    node "EC2 Instance" {
        rectangle "Guest OS (Linux/Windows)" as GuestOS1 #e5e7eb
        rectangle "App + Runtime" as App1 #d1fae5
        GuestOS1 -down-> App1
    }
    rectangle "Hypervisor (Xen/KVM)" as HV #f3f4f6
    rectangle "Host OS" as HostOS1 #e5e7eb
    rectangle "Фізичне залізо" as HW1 #d1d5db
    HV -down-> HostOS1
    HostOS1 -down-> HW1
}

package "Контейнер (Docker)" #d1fae5 {
    node "Docker Host" {
        rectangle "Container A\n(App + Runtime)" as CA #bbf7d0
        rectangle "Container B\n(App + Runtime)" as CB #bbf7d0
        rectangle "Container C\n(App + Runtime)" as CC #bbf7d0
    }
    rectangle "Docker Engine" as DE #f3f4f6
    rectangle "Host OS (Linux)" as HostOS2 #e5e7eb
    rectangle "Фізичне залізо" as HW2 #d1d5db
    DE -down-> HostOS2
    HostOS2 -down-> HW2
}
@enduml

VM — повна ізоляція через апаратну віртуалізацію. Кожна VM містить власну гостьову ОС (~1–20 GB). Запуск займає хвилини.

Контейнер — ізоляція на рівні ОС через namespaces та cgroups Linux. Контейнер ділить ядро з хост-ОС. Розмір образу — десятки–сотні MB. Запуск займає секунди або мілісекунди.

Для AWS це означає: на одному EC2 інстансі можна запустити десятки контейнерів, де могло б бути лише кілька VM.


Amazon ECR — Elastic Container Registry

Перш ніж запустити контейнер в AWS, образ потрібно десь зберегти. Amazon Elastic Container Registry (ECR) — це керований сервіс для зберігання, версіонування та поширення Docker-образів, аналогічний Docker Hub, але повністю інтегрований з AWS IAM та іншими сервісами AWS.

Чому ECR, а не Docker Hub?

Приватність та безпека: ECR-репозиторії за замовчуванням приватні і захищені IAM-правами. Жоден зовнішній користувач не зможе завантажити ваші образи без явного дозволу.

Інтеграція з AWS: ECS, EKS та Lambda автоматично аутентифікуються в ECR через IAM Role — без додаткових секретів і паролів. Порівняйте з Docker Hub, де потрібно зберігати credentials у Secrets Manager.

Швидкість: ECR розміщений в тому ж регіоні, що і ваші ECS/EKS кластери. Завантаження образу відбувається через внутрішню мережу AWS — без витрат на transfer та з максимальною швидкістю.

Lifecycle Policies: ECR автоматично видаляє старі версії образів за правилами, які ви визначаєте (наприклад, зберігати лише 10 останніх версій). Це суттєво знижує вартість зберігання.

Структура ECR

ECR організований через репозиторії (repositories). Один репозиторій — один Docker-образ у різних версіях (тегах).

123456789012.dkr.ecr.eu-central-1.amazonaws.com/
├── my-api/
│   ├── :latest
│   ├── :v1.2.3
│   └── :sha256-abc123...
├── my-worker/
│   ├── :latest
│   └── :v2.0.0
└── my-frontend/
    └── :latest

Адреса ECR-репозиторію завжди має формат:

{account-id}.dkr.ecr.{region}.amazonaws.com/{repository-name}:{tag}

Практика: Створення ASP.NET Minimal API проєкту

Перш ніж писати Dockerfile, підготуємо сам застосунок. Пройдемо по кроках — від порожнього каталогу до готового проєкту, який потім контейнеризуємо та задеплоїмо в AWS.

Крок 1 — Створення проєкту:

dotnet new webapi — ініціалізація
$ dotnet new webapi \
        --name MyApi \
        --output ./MyApi
The template "ASP.NET Core Web API" was created successfully.
Restore succeeded.

Крок 2 — Структура проєкту:

MyApi/
├── MyApi.csproj
├── Program.cs
├── appsettings.json
└── appsettings.Development.json

Крок 3 — Замініть вміст Program.cs на мінімальний API з health check endpoint:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHealthChecks();

var app = builder.Build();

app.MapGet("/", () => new { status = "ok", version = "1.0.0" });

app.MapGet("/weatherforecast", () =>
{
    var summaries = new[] { "Freezing", "Cool", "Warm", "Hot" };
    return Enumerable.Range(1, 5).Select(i => new
    {
        Date = DateOnly.FromDateTime(DateTime.Now.AddDays(i)),
        TemperatureC = Random.Shared.Next(-20, 55),
        Summary = summaries[Random.Shared.Next(summaries.Length)]
    });
});

// Health check endpoint — потрібен для ECS health check
app.MapHealthChecks("/health");

app.Run();

Крок 4 — Локальний запуск і перевірка:

dotnet run — локальна перевірка
$ cd MyApi && dotnet run
info: Microsoft.Hosting.Lifetime[14]
    Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
    Application started. Press Ctrl+C to shut down.
curl — перевірка health endpoint
$ curl http://localhost:5000/health
Healthy

Проєкт готовий до контейнеризації. Далі — пишемо Dockerfile.


Dockerfile для .NET 8 — multi-stage build

Multi-stage build — це техніка написання Dockerfile, яка дозволяє значно зменшити розмір фінального образу. Ідея: використовуємо один образ для збірки (з усіма інструментами розробника) і окремий — мінімальний — для запуску в production.

Розглянемо детально кожен рядок оптимального Dockerfile для ASP.NET Core 8:

# ─── STAGE 1: BUILD ────────────────────────────────────────────────────────────
# Базовий образ із SDK для збірки (~800 MB)
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build

# Встановлюємо робочу директорію всередині контейнера
WORKDIR /src

# Спочатку копіюємо ЛИШЕ .csproj файли і відновлюємо залежності.
# Це використовує Docker layer caching: якщо .csproj не змінились —
# команда restore береться з кешу, і збірка прискорюється суттєво.
COPY ["MyApi/MyApi.csproj", "MyApi/"]
RUN dotnet restore "MyApi/MyApi.csproj"

# Тепер копіюємо весь вихідний код
COPY . .

# Перейдіть у директорію проєкту
WORKDIR "/src/MyApi"

# Публікуємо Release-версію у директорію /app/publish
# -c Release      — конфігурація Release
# -o /app/publish — директорія виводу
# --no-restore    — restore вже виконали вище
RUN dotnet publish "MyApi.csproj" -c Release -o /app/publish --no-restore

# ─── STAGE 2: RUNTIME ──────────────────────────────────────────────────────────
# Мінімальний runtime-образ (~200 MB) — набагато менший за SDK
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final

# Задаємо робочу директорію
WORKDIR /app

# Копіюємо ЛИШЕ опублікований артефакт зі stage build
# Весь SDK, вихідний код, проміжні файли — залишаються в stage build
# і НЕ потрапляють у фінальний образ
COPY --from=build /app/publish .

# Вказуємо точку входу
ENTRYPOINT ["dotnet", "MyApi.dll"]

Завдяки multi-stage build:

  • Build image: ~800 MB (SDK + вихідний код)
  • Final image: ~200 MB (лише Runtime + скомпільований застосунок)
  • Вихідний код не потрапляє у production образ — важливо для безпеки

.dockerignore — що не копіювати в образ

Файл .dockerignore (аналог .gitignore) вказує Docker, які файли ігнорувати при копіюванні:

# Директорії збірки — вони будуть перезбудовані всередині контейнера
**/bin/
**/obj/

# Git-директорія та файли
.git
.gitignore

# Налаштування IDE
.vs/
.rider/
*.user

# Docker-файли самі по собі
Dockerfile
.dockerignore

# Локальні секрети та конфігурації
**/appsettings.Development.json
**/appsettings.Local.json
**/*.pfx
**/.env

Практика: Push образу в Amazon ECR

Після написання Dockerfile — виконуємо п'ять послідовних операцій для відправки образу в ECR.

Крок 1 — Аутентифікація у ECR. AWS ECR не зберігає постійних паролів Docker Hub. Натомість використовуються тимчасові токени, дійсні 12 годин. Команда get-login-password отримує токен і через pipe передає його стандартному docker login:

aws ecr get-login-password — аутентифікація
$ aws ecr get-login-password \
        --region eu-central-1 \
        | docker login --username AWS \
        --password-stdin 123456789012.dkr.ecr.eu-central-1.amazonaws.com
Login Succeeded

Крок 2 — Створення репозиторію у ECR (виконується один раз; якщо репозиторій вже існує — пропустіть):

aws ecr create-repository — відповідь
$ aws ecr create-repository \
        --repository-name my-api \
        --region eu-central-1 \
        --image-scanning-configuration scanOnPush=true
{
  "repository": {
    "repositoryArn": "arn:aws:ecr:eu-central-1:123456789012:repository/my-api",
    "registryId": "123456789012",
    "repositoryName": "my-api",
    "repositoryUri": "123456789012.dkr.ecr.eu-central-1.amazonaws.com/my-api",
    "imageScanningConfiguration": { "scanOnPush": true }
  }
}

Значення repositoryUri — адреса вашого репозиторію в ECR. Саме цей URI використовується у наступних двох кроках.

Крок 3 — Збірка та теґування образу:

docker build
$ docker build -t my-api:latest .
[+] Building 38.5s (12/12) FINISHED
=> [build 3/5] RUN dotnet restore "MyApi.csproj"          10.2s
=> [build 5/5] RUN dotnet publish "MyApi.csproj" -c Release -o /app/publish  18.1s
=> => writing image sha256:a1b2c3d4e5f6...  0.1s
# Теґуємо образ адресою ECR репозиторію
docker tag my-api:latest \
    123456789012.dkr.ecr.eu-central-1.amazonaws.com/my-api:latest

Крок 4 — Завантаження образу в ECR:

docker push — завантаження в ECR
$ docker push 123456789012.dkr.ecr.eu-central-1.amazonaws.com/my-api:latest
The push refers to repository [123456789012.dkr.ecr.eu-central-1.amazonaws.com/my-api]
8c2d1e5f9a3b: Pushing  45.23 MB/120.5 MB
3f4a8b9c1d2e: Pushed
1a4f7e2c8b6d: Pushed
latest: digest: sha256:abc123def456... size: 1847
Зверніть увагу на --image-scanning-configuration scanOnPush=true. ECR автоматично сканує кожен завантажений образ на відомі CVE (Common Vulnerabilities and Exposures) за допомогою Amazon Inspector. Це безкоштовна перша лінія захисту від вразливостей у ваших залежностях.

Amazon ECS — Elastic Container Service

Amazon Elastic Container Service (ECS) — це повністю керований оркестратор контейнерів від AWS. Якщо ECR — це «склад» для образів, то ECS — це «виробничий цех», де ці образи запускаються, масштабуються та оновлюються.

Оркестратор контейнерів — це система, яка відповідає за:

  • Запуск контейнерів на доступних серверах
  • Перезапуск контейнерів у разі збою
  • Масштабування (запуск більше копій при зростанні навантаження)
  • Rolling updates — оновлення без зупинки сервісу
  • Балансування трафіку між копіями контейнера

Три ключові сутності ECS

ECS оперує трьома основними концепціями. Розберемо їх через аналогію з рестораном.

Task Definition — це рецепт страви. Він описує: який образ використовувати, скільки CPU і RAM виділити, які змінні середовища передати, де зберігати логи, які порти відкрити. Task Definition є незмінним шаблоном — кожна зміна створює нову версію (revision).

Task — це конкретна приготована страва. Один запущений екземпляр Task Definition. Контейнер, що реально виконується прямо зараз.

Service — це менеджер, який гарантує, що завжди є рівно N порцій страви готових до подачі. Якщо одна «страва» зіпсувалась (контейнер впав) — Service автоматично замовить нову. Він також керує rolling updates і підключає Tasks до Load Balancer.

Cluster — це сам ресторан: фізичне місце, де все це відбувається. Логічна група, яка об'єднує всі Tasks і Services.

Lifecycle Task: від запиту до зупинки

Розуміння станів Task є практично важливим: якщо Task «застряг» у певному стані, правильна діагностика можлива лише тоді, коли ви розумієте, що в ньому відбувається.

СтанЩо відбувається
PROVISIONINGFargate виділяє ізольоване обчислювальне середовище, ENI
PENDINGЗавантажується Docker-образ з ECR у виділене середовище
ACTIVATINGЗапускаються контейнери, виконується ENTRYPOINT
RUNNINGКонтейнер працює; ECS виконує health check-и
DEACTIVATINGПочинається graceful shutdown: Task отримує сигнал SIGTERM
STOPPINGОчікування завершення застосунку (до 30 сек за замовчуванням)
STOPPEDTask завершено; причина зупинки зафіксована в stoppedReason

Типові причини застрягання в PENDING: образ не знайдено в ECR (невірний URI або тег), Task Execution Role не має прав на ecr:GetDownloadUrlForLayer, Task у приватній підмережі без NAT Gateway.

Ревізії Task Definition. Кожна зміна Task Definition (навіть однієї змінної середовища) створює нову revision — наприклад, my-api:1, my-api:2, my-api:3. Попередні ревізії зберігаються назавжди і не видаляються автоматично. ECS Service завжди посилається на конкретну ревізію, що гарантує точну відтворюваність середовища при кожному запуску Task.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

package "ECS Cluster: production" #e9ecef {

    rectangle "Task Definition: my-api:v3" as TD #dbeafe

    note right of TD
      Image: 123456789012.dkr.ecr.../my-api:v1.2.3
      CPU: 256 (.25 vCPU)
      Memory: 512 MB
      Port: 8080
      Env: ASPNETCORE_ENVIRONMENT=Production
    end note

    package "ECS Service: my-api-service" as Service #d1fae5 {

        rectangle "Task 1" as T1 #bbf7d0
        rectangle "Task 2" as T2 #bbf7d0
        rectangle "Task 3" as T3 #bbf7d0
    }

    note top of Service
      Desired count: 3 Tasks
      Rolling update: max 200%, min 50%
      ALB target group
    end note

    TD -down-> Service : "запускає Tasks за шаблоном"
}

rectangle "Application Load Balancer" as ALB #fef3c7
ALB -right-> T1
ALB -right-> T2
ALB -right-> T3
@enduml

Task Definition: детальна конфігурація

Task Definition — це JSON-документ, але AWS Console надає зручний конструктор. Розберемо ключові поля:

{
    "family": "my-api",
    "networkMode": "awsvpc",
    "requiresCompatibilities": ["FARGATE"],
    "cpu": "256",
    "memory": "512",
    "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
    "taskRoleArn": "arn:aws:iam::123456789012:role/my-api-task-role",
    "containerDefinitions": [
        {
            "name": "my-api",
            "image": "123456789012.dkr.ecr.eu-central-1.amazonaws.com/my-api:v1.2.3",
            "portMappings": [
                {
                    "containerPort": 8080,
                    "protocol": "tcp"
                }
            ],
            "environment": [
                { "name": "ASPNETCORE_ENVIRONMENT", "value": "Production" },
                { "name": "ASPNETCORE_URLS", "value": "http://+:8080" }
            ],
            "secrets": [
                {
                    "name": "ConnectionStrings__DefaultConnection",
                    "valueFrom": "arn:aws:secretsmanager:eu-central-1:123456789012:secret:prod/db-connection"
                }
            ],
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/my-api",
                    "awslogs-region": "eu-central-1",
                    "awslogs-stream-prefix": "ecs"
                }
            },
            "healthCheck": {
                "command": ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"],
                "interval": 30,
                "timeout": 5,
                "retries": 3,
                "startPeriod": 60
            }
        }
    ]
}

Розберемо важливі поля:

executionRoleArn — IAM Role для ECS агента (не вашого застосунку). Потрібна для завантаження образу з ECR та запису логів у CloudWatch. Стандартна роль: AmazonECSTaskExecutionRolePolicy.

taskRoleArn — IAM Role для вашого застосунку всередині контейнера. Якщо API потребує доступу до S3 або DynamoDB — саме сюди додаються ці права.

secrets — замість передачі паролів через environment (небезпечно!) — отримання секретів із AWS Secrets Manager або SSM Parameter Store. ECS агент завантажує секрет і підставляє як змінну середовища під час запуску контейнера.

healthCheck — ECS регулярно перевіряє стан контейнера. Якщо health check провалюється тричі — Task вважається нездоровим і Service замінює його новим.

Допустимі комбінації CPU та пам'яті для Fargate

Параметри cpu і memory у Task Definition — це рядки (не числа). Fargate не приймає довільні значення — допустимі лише певні комбінації, перелік яких наведено нижче. Спроба вказати неіснуючу комбінацію призведе до помилки валідації Task Definition.

CPU (JSON рядок)Реальне значенняДопустима пам'ять (memory, MB)
"256"0.25 vCPU512, 1024, 2048
"512"0.5 vCPU1024, 2048, 3072, 4096
"1024"1 vCPU2048–8192 (крок 1024)
"2048"2 vCPU4096–16384 (крок 1024)
"4096"4 vCPU8192–30720 (крок 1024)
"8192"8 vCPU16384–61440 (крок 4096)
"16384"16 vCPU32768–122880 (крок 8192)

Для більшості .NET Web API-сервісів достатньо "256" (0.25 vCPU) і "512" MB RAM — це найменш вартісна конфігурація Fargate. При високому RPS або інтенсивних обчисленнях рекомендовано стартувати з "512" / 1024 MB і коригувати на основі метрик CloudWatch.

Health checks у .NET потребують окремого endpoint. Додайте у Program.cs:
app.MapHealthChecks("/health");
І переконайтесь, що endpoint доступний до підключення до бази даних. ECS починає перевірку одразу після старту контейнера, а DB може ще не бути доступна.

AWS Fargate vs EC2 Launch Types

Один із ключових вибір при налаштуванні ECS: як запускати Tasks? ECS підтримує два «launch types» — режими запуску, які принципово відрізняються підходом до управління інфраструктурою.

EC2 Launch Type

При використанні EC2 launch type ви самостійно управляєте EC2 інстансами, на яких запускаються контейнери. ECS розміщує Tasks на ваших інстансах, але відповідальність за самі інстанси — на вас: встановлення ECS Agent, оновлення ОС, вибір типу і розміру інстансу, масштабування парку машин.

Переваги EC2 launch type:

  • Нижча вартість при стабільному, передбачуваному навантаженні (Reserved Instances, Savings Plans)
  • Більш детальний контроль: GPU інстанси для ML, специфічні типи інстансів, Spot Instances для batch-задач
  • Можливість запускати privileged контейнери та монтувати локальні томи

Недоліки: Ви платите за інстанс, навіть якщо він простоює. Потрібно управляти capacity, оновленнями ОС, патчами безпеки.

AWS Fargate — Serverless Containers

AWS Fargate — це serverless платформа для запуску контейнерів. При використанні Fargate launch type AWS повністю бере на себе управління інфраструктурою: ви не вибираєте, не запускаєте і не обслуговуєте жодного EC2 інстансу. Ви просто описуєте Task Definition (образ, CPU, RAM) — і Fargate запускає контейнер.

Принцип роботи Fargate: для кожного Task AWS виділяє ізольоване мікро-середовище з запитаними ресурсами. Task не знає про інші Tasks. Після зупинки Task — ресурси звільняються. Ви платите лише за час виконання Task (vCPU-годину та GB-годину RAM).

Переваги Fargate:

  • Нуль операційного overhead: немає кластеру EC2 для управління
  • Ізоляція на рівні завдання — кожен Task у власному мікро-VM
  • Автоматичне масштабування від 0 до тисяч Tasks без попереднього планування capacity
  • Ідеально для нерівномірного навантаження, мікросервісів та стартапів

Недоліки: Дорожче за EC2 для стабільного, постійного навантаження. Немає GPU-підтримки. Холодний старт повільніший (кілька секунд для підняття ізольованого середовища).

Fargate — коли обирати

  • Мікросервісна архітектура
  • Нерівномірне навантаження (пікові навантаження вдень, майже нуль вночі)
  • Команда без DevOps-досвіду або маленька команда
  • Batch-задачі, фонові воркери
  • Стартапи та MVP

EC2 Launch Type — коли обирати

  • Стабільне, передбачуване навантаження 24/7
  • Потреба у GPU (ML моделі)
  • Специфічні вимоги до типу інстансу
  • Spot Instances для batch-обробки даних
  • Велика компанія з виділеним DevOps-відділом

ECS Task Networking: awsvpc режим

Перш ніж розглядати мережевий режим awsvpc, необхідно сформувати чітке розуміння фундаментальної концепції AWS-інфраструктури — Amazon VPC, — без якої неможливо пояснити, яким чином ECS Tasks отримують мережеву ідентичність і як вони стають доступними ззовні.

Що таке Amazon VPC

Amazon Virtual Private Cloud (VPC) — це логічно ізольована віртуальна мережа всередині AWS, яку користувач визначає і якою повністю керує. Концептуально VPC відтворює традиційну корпоративну мережу, розміщену у власному дата-центрі: ви самостійно обираєте діапазон IP-адрес, розбиваєте мережу на сегменти, встановлюєте правила фільтрації трафіку — проте замість фізичного обладнання весь цей контроль здійснюється програмно.

Кожен AWS-акаунт при створенні отримує Default VPC у кожному регіоні — попередньо налаштовану мережу з адресним простором 172.31.0.0/16, готову до використання без жодних додаткових налаштувань. Саме цей Default VPC застосовуватиметься у практичному прикладі далі в розділі.

VPC охоплює весь регіон AWS (наприклад, eu-central-1), однак ресурси всередині VPC фізично розміщуються у конкретних Availability Zones (зонах доступності) — незалежних дата-центрах у межах регіону. Для розподілу ресурсів між зонами VPC ділиться на підмережі (Subnets).

AWS Region: eu-central-1 (Frankfurt)
└── VPC: 10.0.0.0/16  ← адресний простір: 65 536 IP-адрес
    ├── Subnet: 10.0.1.0/24  → Availability Zone eu-central-1a  (256 адрес)
    ├── Subnet: 10.0.2.0/24  → Availability Zone eu-central-1b  (256 адрес)
    └── Subnet: 10.0.3.0/24  → Availability Zone eu-central-1c  (256 адрес)

Публічні та приватні підмережі

Підмережі поділяються на два принципово різних типи залежно від наявності маршруту до Інтернету.

Публічна підмережа (Public Subnet) — підмережа, маршрутна таблиця якої містить запис, що направляє зовнішній трафік (0.0.0.0/0) до Internet Gateway (IGW). Internet Gateway — керований AWS компонент, що забезпечує двосторонній зв'язок між ресурсами VPC та публічним Інтернетом. Ресурс, розміщений у публічній підмережі і наділений публічною IP-адресою, є досяжним з Інтернету і сам здатний ініціювати вихідні з'єднання назовні.

Приватна підмережа (Private Subnet) — підмережа, маршрутна таблиця якої не містить маршруту до Internet Gateway. Ресурси у ній недоступні з Інтернету за будь-яких умов. Для вихідного трафіку (наприклад, завантаження Docker-образу з Amazon ECR або звернення до зовнішнього API) використовується NAT Gateway — керований AWS-сервіс, розміщений у публічній підмережі. NAT Gateway транслює приватну IP-адресу вихідного пакета у власну публічну Elastic IP, пропускаючи трафік назовні, проте залишаючи ресурси закритими для будь-яких вхідних з'єднань ззовні.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

rectangle "Internet" as NET #e5e7eb

package "VPC: 10.0.0.0/16" #f0f4ff {

    rectangle "Internet Gateway (IGW)" as IGW #fef3c7

    package "Public Subnet: 10.0.1.0/24\n(AZ: eu-central-1a)" #d1fae5 {
        rectangle "ECS Task\nPublic IP: 3.64.x.x\nPrivate IP: 10.0.1.15" as TASK_PUB #bbf7d0
        rectangle "NAT Gateway\n(Elastic IP)" as NAT #fde68a
    }

    package "Private Subnet: 10.0.2.0/24\n(AZ: eu-central-1b)" #dbeafe {
        rectangle "ECS Task\nPrivate IP: 10.0.2.8\n(без публічного IP)" as TASK_PRV #bfdbfe
        rectangle "RDS Database\nPrivate IP: 10.0.2.50" as RDS #e0e7ff
    }
}

NET <--> IGW
IGW <--> TASK_PUB : вхідний та вихідний трафік
TASK_PRV -down-> NAT : вихідний трафік (наприклад, pull образу з ECR)
NAT --> IGW : передає назовні з Elastic IP
note right of TASK_PRV
  Недоступний з Інтернету напряму.
  Вихідний трафік — через NAT Gateway.
end note
@enduml

ENI — Elastic Network Interface

Elastic Network Interface (ENI) — це віртуальний мережевий адаптер у VPC. Фізичною аналогією слугує мережева карта (NIC) комп'ютера: ENI є тим інтерфейсом, через який ресурс AWS підключається до мережі, отримує приватну IP-адресу, і через який проходить увесь його мережевий трафік.

Кожен ENI характеризується такими атрибутами:

  • Приватна IP-адреса — постійна адреса всередині VPC, що призначається з діапазону підмережі
  • Публічна IP-адреса — необов'язкова; призначається лише для ресурсів у публічних підмережах при явному запиті
  • Security Groups — набори правил фільтрації трафіку, прикріплені безпосередньо до ENI

Режим awsvpc та його значення для ECS

awsvpc — це мережевий режим Task Definition, при якому кожен запущений Task отримує власний виділений ENI і, відповідно, власну приватну IP-адресу всередині VPC. Цей режим є обов'язковим для Fargate і рекомендованим для EC2 launch type.

Дана модель принципово відрізняється від традиційного підходу, де всі контейнери на одному хості розділяють мережевий простір хостової машини і відрізняються лише номерами портів. У режимі awsvpc Task є повноправним мережевим ресурсом VPC — таким самим, як EC2-інстанс або RDS-база даних.

Практичні наслідки цього рішення:

  • Власна IP-адреса кожного Task. ALB реєструє IP кожного Task як окремий target і направляє трафік безпосередньо, без додаткових рівнів трансляції.
  • Security Groups на рівні Task. Правила фільтрації трафіку застосовуються окремо до кожного Task, що забезпечує точний контроль: наприклад, дозволити трафік на порт 8080 лише з конкретного ALB.
  • Підтримка VPC Flow Logs. Весь мережевий трафік Tasks можна логувати та аналізувати на рівні мережі.

Публічний доступ до Fargate Task: assignPublicIp

Найпростіший спосіб надати Task доступ з Інтернету — розмістити його у публічній підмережі та встановити параметр assignPublicIp: ENABLED при запуску Service або окремого Task. У такому випадку AWS автоматично призначає ENI Task тимчасову публічну IP-адресу.

Internet
    ↓
Public IP: 3.64.x.x  (тимчасова, змінюється при рестарті Task)
    ↓
ENI Task (awsvpc)
    ↓
.NET контейнер: порт 8080
Тимчасова публічна IP-адреса є прийнятною для навчальних цілей і прямого тестування без балансувальника. У production-середовищі Tasks розміщуються у приватних підмережах, а єдиною точкою входу ззовні слугує Application Load Balancer — ресурс зі статичним DNS-name, розміщений у публічних підмережах. Такий підхід забезпечує стабільну адресацію і виключає пряме звернення до Tasks з Інтернету.

Приватне розміщення Tasks і роль NAT Gateway

Якщо ECS Task розташований у приватній підмережі (рекомендована production-конфігурація), він не має публічного IP і залишається ізольованим від зовнішнього трафіку. Проте для коректного запуску Task вихідне з'єднання усе одно необхідне — щонайменше для завантаження Docker-образу з Amazon ECR в момент старту контейнера.

Для забезпечення вихідного трафіку з приватної підмережі в цій же VPC розміщується NAT Gateway — у публічній підмережі, де він має доступ до Internet Gateway. NAT Gateway транслює адресу Task у власну Elastic IP при проходженні трафіку назовні, не відкриваючи Task для вхідних з'єднань ззовні.

NAT Gateway є платним сервісом: ~$0.045/год за роботу плюс ~$0.045/GB переданих даних. У навчальній лабораторній роботі далі Tasks розміщуватимуться у публічній підмережі з assignPublicIp: ENABLED — в такому разі NAT Gateway не потрібен, а трафік іде напряму через Internet Gateway. Врахуйте цей факт при проектуванні production-архітектури.

Підсумкова схема адресації Tasks

VPC: 10.0.0.0/16
│
├── Public Subnet: 10.0.1.0/24  (AZ: eu-central-1a)
│   ├── Task 1  ENI → Private: 10.0.1.15 │ Public: 3.64.12.5   (my-api)
│   ├── Task 2  ENI → Private: 10.0.1.23 │ Public: 3.64.18.7   (my-api)
│   └── NAT Gateway (Elastic IP: 52.29.x.x)
│
└── Private Subnet: 10.0.2.0/24  (AZ: eu-central-1b)
    ├── Task 3  ENI → Private: 10.0.2.8  │ Public: —           (my-api)
    └── RDS     ENI → Private: 10.0.2.50 │ Public: —           (postgres)

Tasks у публічній підмережі доступні з Інтернету безпосередньо за публічним IP (прийнятно для тестування). Tasks у приватній підмережі досяжні лише через ALB або інші ресурси всередині VPC — що є стандартною production-моделлю.


ECS Service Auto Scaling

Auto Scaling — це автоматична зміна кількості запущених Tasks у відповідь на зміну навантаження. ECS Service Auto Scaling інтегрований з Amazon CloudWatch та Application Auto Scaling і підтримує три стратегії масштабування:

Target Tracking Scaling

Найпростіший і найрекомендованіший підхід для початку. Ви вказуєте цільове значення метрики — і AWS автоматично додає або видаляє Tasks для підтримання цього значення.

{
    "TargetValue": 70.0,
    "PredefinedMetricSpecification": {
        "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
    },
    "ScaleInCooldown": 300,
    "ScaleOutCooldown": 60
}

Ця конфігурація каже: «Підтримуй середнє CPU-завантаження на рівні 70%. Якщо CPU зростає вище — додай Tasks (cooldown 60 сек). Якщо CPU падає нижче — видали Tasks (cooldown 300 сек)».

ScaleOutCooldown (60 сек) — пауза після масштабування вгору. Невеликий, бо при зростанні навантаження важливо швидко реагувати.

ScaleInCooldown (300 сек) — пауза після маштабування вниз. Більший, бо передчасне видалення Tasks при тимчасовому зниженні навантаження є контрпродуктивним.

Налаштування Target Tracking через AWS CLI:

# Крок 1: Зареєструйте ECS Service як scalable target.
# resource-id має формат: service/<cluster-name>/<service-name>
aws application-autoscaling register-scalable-target \
    --service-namespace ecs \
    --scalable-dimension ecs:service:DesiredCount \
    --resource-id service/production/my-api \
    --min-capacity 2 \
    --max-capacity 20 \
    --region eu-central-1

# Крок 2: Додайте Target Tracking Policy
aws application-autoscaling put-scaling-policy \
    --service-namespace ecs \
    --scalable-dimension ecs:service:DesiredCount \
    --resource-id service/production/my-api \
    --policy-name cpu-target-70 \
    --policy-type TargetTrackingScaling \
    --target-tracking-scaling-policy-configuration '{
        "TargetValue": 70.0,
        "PredefinedMetricSpecification": {
            "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
        },
        "ScaleInCooldown": 300,
        "ScaleOutCooldown": 60
    }' \
    --region eu-central-1
--min-capacity 2 — для production-сервісу рекомендовано завжди підтримувати щонайменше 2 Tasks: це захищає від повного простою при падінні одного Task або під час rolling update. --max-capacity 20 — верхня межа захищає від неочікуваних витрат (наприклад, хиба у логіці політики, що змушує CPU зростати безмежно).

Step Scaling та Scheduled Scaling

Step Scaling дозволяє задати різні кроки масштабування залежно від величини відхилення. Наприклад: CPU 70–80% → +1 Task, CPU 80–90% → +2 Tasks, CPU >90% → +4 Tasks.

Scheduled Scaling дозволяє масштабувати за розкладом — корисно, якщо навантаження передбачуване (наприклад, кожен день о 8:00 збільшувати мінімум до 5 Tasks).

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

actor "User Traffic" as User
rectangle "Application Load Balancer" as ALB #fef3c7
package "ECS Service (Auto Scaling)" #d1fae5 {
    rectangle "Task 1" as T1 #bbf7d0
    rectangle "Task 2" as T2 #bbf7d0
    rectangle "Task N..." as TN #bbf7d0 {
        note right
          Додається автоматично
          при CPU > 70%
        end note
    }
}
rectangle "CloudWatch<br/>Metrics" as CW #e0e7ff
rectangle "Auto Scaling<br/>Policy" as ASP #fce7f3

User -down-> ALB
ALB -down-> T1
ALB -down-> T2
ALB -down-> TN
T1 -right-> CW : CPU, Memory metrics
T2 -right-> CW
CW -down-> ASP : Alarm triggers
ASP -right-> "ECS Service (Auto Scaling)" : Scale Out/In
@enduml

Application Load Balancer для ECS

Application Load Balancer (ALB) — це Layer 7 (HTTP/HTTPS) балансувальник навантаження, який розподіляє вхідні запити між Tasks у ECS Service. Без ALB ваш застосунок буде доступний лише за приватною IP-адресою Task — що змінюється при кожному рестарті.

Як ALB інтегрується з ECS

Коли ECS Service запускає новий Task — він автоматично реєструє IP Task у Target Group ALB. Коли Task зупиняється — видаляє його. ALB завжди знає актуальний список здорових Tasks і направляє на них трафік.

Ланцюжок запиту:

Internet → Route 53 (DNS) → ALB (HTTPS:443) → Target Group → Tasks (HTTP:8080)

ALB виконує SSL termination: приймає HTTPS від клієнта, розшифровує і передає у Tasks по звичайному HTTP — Tasks не потрібно конфігурувати SSL.

Налаштування ALB для ECS через AWS CLI

# 0. Створіть сам ALB (публічний, розміщується в публічних підмережах)
ALB_ARN=$(aws elbv2 create-load-balancer \
    --name my-api-alb \
    --subnets subnet-aaa subnet-bbb \
    --security-groups sg-alb-id \
    --scheme internet-facing \
    --type application \
    --query "LoadBalancers[0].LoadBalancerArn" --output text \
    --region eu-central-1)
echo "ALB ARN: $ALB_ARN"

# 1. Створіть Target Group (куди ALB направлятиме запити)
TG_ARN=$(aws elbv2 create-target-group \
    --name my-api-tg \
    --protocol HTTP \
    --port 8080 \
    --vpc-id vpc-0a1b2c3d4e5f \
    --target-type ip \
    --health-check-path /health \
    --health-check-interval-seconds 30 \
    --healthy-threshold-count 2 \
    --unhealthy-threshold-count 3 \
    --query "TargetGroups[0].TargetGroupArn" --output text \
    --region eu-central-1)
echo "Target Group ARN: $TG_ARN"

# 1b. Додайте HTTP Listener до ALB (порт 80 → Target Group)
aws elbv2 create-listener \
    --load-balancer-arn $ALB_ARN \
    --protocol HTTP --port 80 \
    --default-actions Type=forward,TargetGroupArn=$TG_ARN \
    --region eu-central-1

# 2. Під час створення ECS Service — прив'яжіть Target Group
aws ecs create-service \
    --cluster production \
    --service-name my-api \
    --task-definition my-api:3 \
    --desired-count 3 \
    --launch-type FARGATE \
    --load-balancers "targetGroupArn=$TG_ARN,containerName=my-api,containerPort=8080" \
    --network-configuration "awsvpcConfiguration={subnets=[subnet-xxx,subnet-yyy],securityGroups=[sg-zzz],assignPublicIp=DISABLED}"

Зверніть увагу: --target-type ip — це специфічно для Fargate та awsvpc режиму, де кожен Task має власний IP. ALB звертається до Task безпосередньо по IP, а не через порт на хості. Security Group ALB повинна дозволяти вхід на порт 80/443 з 0.0.0.0/0, а Security Group Tasks — вхід на порт 8080 лише з Security Group ALB.


Rolling Updates — оновлення без зупинки сервісу

Rolling update — це стратегія оновлення, яка дозволяє замінити стару версію застосунку новою без жодного часу простою (downtime). ECS Service виконує rolling update автоматично щоразу, коли ви змінюєте Task Definition.

Як це працює покроково:

Припустимо, у вас 4 Tasks з версією v1.2.3, і ви хочете оновитись до v1.3.0. Конфігурація: minimumHealthyPercent=50, maximumPercent=200.

  1. ALB продовжує направляти трафік на всі 4 Tasks (v1.2.3)
  2. ECS запускає 4 нових Tasks (v1.3.0) — тепер запущено 8 Tasks (200% від бажаних 4)
  3. ECS чекає, поки нові Tasks пройдуть health check і зареєструються в ALB
  4. ECS зупиняє 4 старих Tasks (v1.2.3) — залишається 4 Tasks (v1.3.0), 100%
  5. Оновлення завершено, ALB направляє трафік на v1.3.0

У будь-який момент процесу minimumHealthyPercent=50 гарантує, що щонайменше 2 Tasks (50% від 4) залишаються здоровими.

Нова версія образу в ECR не запускається автоматично. ECS запам'ятовує конкретний digest образу в Task Definition. Щоб запустити оновлений образ — оновіть Task Definition (вкажіть новий тег або sha256 digest) та оновіть Service. Це захищає від ненавмисних оновлень через зміну тегу :latest.

Amazon EKS — огляд Kubernetes на AWS

Amazon Elastic Kubernetes Service (EKS) — це керований сервіс Kubernetes від AWS.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

package "Amazon EKS Cluster" #e9ecef {
    rectangle "Control Plane" as CP #dbeafe {
        rectangle "API Server" as API #bfdbfe
        rectangle "etcd" as ETCD #bfdbfe
        rectangle "Scheduler" as SCH #bfdbfe
        rectangle "Controller\nManager" as CM #bfdbfe
    }

    rectangle "Worker Nodes (EC2)" as WN #d1fae5 {
        rectangle "Pod: UserService" as P1 #bbf7d0
        rectangle "Pod: OrderService" as P2 #bbf7d0
        rectangle "Pod: Notification" as P3 #bbf7d0
    }

    rectangle "Fargate Profile" as FP #fef3c7 {
        rectangle "Pod on Fargate" as P4 #fde68a
    }
}

rectangle "AWS Managed" as AWS #f3f4f6
CP -down--> WN : manages
CP -down--> FP : manages
AWS --> CP : manages control plane
@enduml

Він надає повноцінний Kubernetes кластер, де AWS бере на себе управління control plane (API Server, etcd, Controller Manager, Scheduler) — найскладнішою і найкритичнішою частиною Kubernetes.

Kubernetes і ECS — в чому різниця?

Якщо ECS — це «AWS-рідний» оркестратор, розроблений Amazon, то Kubernetes — це відкритий галузевий стандарт оркестрації контейнерів, розроблений Google і переданий у CNCF (Cloud Native Computing Foundation).

Amazon ECS

  • Розроблений Amazon, нативно інтегрований з AWS
  • Значно простіший в налаштуванні та управлінні
  • Менше концепцій для вивчення
  • Менша гнучкість і екосистема
  • Ідеальний вибір, якщо ви не плануєте мігрувати між провайдерами

Amazon EKS (Kubernetes)

  • Відкритий стандарт, мультихмарний (AWS, Azure, GCP, on-premises)
  • Значно потужніша екосистема (Helm, Istio, ArgoCD, Prometheus)
  • Складніший: потрібно розуміти Pods, Deployments, Services, Ingress, RBAC тощо
  • Вища гнучкість та контроль
  • Краще для великих команд із K8s-досвідом

Коли обирати EKS?

  • Команда вже має Kubernetes-досвід
  • Потреба у мультихмарній або гібридній архітектурі
  • Складні networking вимоги (service mesh, mutual TLS)
  • Розширена екосистема: Helm charts, GitOps (ArgoCD/Flux), observability стек (Prometheus + Grafana)
  • Потреба у більшому контролі над scheduling (node affinity, tolerations, pod disruption budgets)
У цьому курсі ми зосереджуємось на ECS Fargate як оптимальному варіанті для .NET розробників без глибокого DevOps-досвіду. Kubernetes і EKS є окремою великою темою, яка заслуговує власного курсу. Базовий огляд K8s ви вже маєте з розділу про інструменти DevOps.

ECS vs EKS vs Fargate: підсумкова таблиця

ХарактеристикаECS + EC2ECS + FargateEKS + EC2EKS + Fargate
Управління серверамиВиAWSВиAWS
Управління K8s control planeAWSAWS
Складність налаштуванняСередняНизькаВисокаВисока
ГнучкістьСередняНизькаВисокаСередня
ВартістьНижчаВищаНижчаВища
Мультихмарність
Рекомендовано дляДосвідчені DevOpsМалі команди, MVPВеликі команди з K8sСкладні K8s, без серверів

Environment Variables у ECS Task Definitions — специфіка .NET

Проблема: конфігурація у контейнерному середовищі

.NET традиційно зберігає конфігурацію в appsettings.json. У Docker-контейнері це проблема: конфіг «запечений» у образ і не може відрізнятись між dev та production без перезбірки образу.

Правильний підхід: дотримання принципу 12-Factor App — конфігурація зберігається у змінних середовища (environment variables), які передаються контейнеру при запуску.

ASP.NET Core автоматично читає змінні середовища через IConfiguration — жодних додаткових налаштувань не потрібно. Ієрархічні ключі з appsettings.json відповідають змінним з подвійним підкресленням __:

// appsettings.json
{
    "ConnectionStrings": {
        "DefaultConnection": "будь-яке значення"
    },
    "Feature": {
        "MaxItems": 100
    }
}

Відповідні змінні середовища:

ConnectionStrings__DefaultConnection=Host=prod-db;Database=mydb;...
Feature__MaxItems=50

Де зберігати секрети в ECS

У Task Definition є два способи передачі конфігурації:

environment — для незасекреченої конфігурації:

"environment": [
    {"name": "ASPNETCORE_ENVIRONMENT", "value": "Production"},
    {"name": "Feature__MaxItems", "value": "100"},
    {"name": "Logging__LogLevel__Default", "value": "Warning"}
]

secrets — для секретів (паролі, API ключі, connection strings). Значення не зберігається у Task Definition у відкритому вигляді — ECS агент завантажує секрет з AWS Secrets Manager або SSM Parameter Store під час запуску:

"secrets": [
    {
        "name": "ConnectionStrings__DefaultConnection",
        "valueFrom": "arn:aws:secretsmanager:eu-central-1:123456789012:secret:prod/db-conn"
    },
    {
        "name": "ExternalApi__ApiKey",
        "valueFrom": "/prod/external-api/key"
    }
]

У .NET-коді все виглядає однаково через IConfiguration:

// Program.cs — нічого особливого, стандартна конфігурація
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
var apiKey = builder.Configuration["ExternalApi:ApiKey"];

Довідник: Amazon ECR — повна документація функцій

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

rectangle "ECR Repository: my-api" as REPO #dbeafe
rectangle "Images (25 versions)" as IMGS #e5e7eb

rectangle "Lifecycle Policy Rules" as RULES #fef3c7 {
    rectangle "Rule 1: Untagged > 1 day" as R1 #fde68a
    rectangle "Rule 2: Keep last 10 tagged" as R2 #fde68a
}

rectangle "AWS EventBridge\n(daily trigger)" as EB #e0e7ff
rectangle "Expired Images" as EXP #fecaca

IMGS --> RULES : evaluated
EB -down-> RULES : triggers daily
RULES -right-> EXP : deletes
note right of EXP
  Images #11-25 deleted,
  images #1-10 preserved
end note
@enduml

Lifecycle Policies — автоматичне управління версіями образів

Lifecycle Policy — це правило, яке ECR виконує автоматично для видалення старих або непотрібних образів. Без lifecycle policy репозиторій безмежно росте, і ви платите за зберігання тисяч застарілих образів.

Навіщо потрібні: кожен CI/CD pipeline при кожному commit'і завантажує новий образ. За місяць це 30+ образів на репозиторій. Lifecycle policy забезпечує автоматичне очищення.

Lifecycle policy складається з правил (rules) з пріоритетами. Кожне правило описує: які образи шукати (за тегом або без тегу) та що з ними робити (видалити після N днів або зберегти лише N останніх).

Типовий сценарій: зберігати лише 10 tagged версій і видаляти untagged після 1 дня:

{
    "rules": [
        {
            "rulePriority": 1,
            "description": "Видаляти untagged образи через 1 день",
            "selection": {
                "tagStatus": "untagged",
                "countType": "sinceImagePushed",
                "countUnit": "days",
                "countNumber": 1
            },
            "action": { "type": "expire" }
        },
        {
            "rulePriority": 2,
            "description": "Зберігати лише 10 останніх tagged образів",
            "selection": {
                "tagStatus": "tagged",
                "tagPrefixList": ["v"],
                "countType": "imageCountMoreThan",
                "countNumber": 10
            },
            "action": { "type": "expire" }
        }
    ]
}

Застосування через AWS CLI:

aws ecr put-lifecycle-policy \
    --repository-name my-api \
    --lifecycle-policy-text file://lifecycle-policy.json \
    --region eu-central-1

Через AWS Console: ECR → Repositories → my-api → вкладка «Lifecycle policy»Create rule → задайте умови у формі → Save.

Перед застосуванням lifecycle policy перевірте, які образи будуть видалені: ECR → Repositories → my-apiLifecycle policyTest rules → ECR покаже список образів, які підпадають під правила, без реального видалення.

Image Scanning — автоматичний аудит безпеки

ECR інтегрований з Amazon Inspector і може автоматично сканувати кожен завантажений образ на відомі CVE (Common Vulnerabilities and Exposures) — бази відомих вразливостей у ПЗ.

Два режими сканування:

Basic Scanning — безкоштовне сканування на основі відкритої бази даних CVE. Виконується один раз при push (якщо scanOnPush=true) або вручну.

Enhanced Scanning (Amazon Inspector) — платне безперервне сканування. Повторно аналізує образи при появі нових CVE у базі — навіть якщо образ давно завантажений.

Перегляд результатів через CLI:

# Переглянути результати останнього сканування
aws ecr describe-image-scan-findings \
    --repository-name my-api \
    --image-id imageTag=v1.2.3 \
    --region eu-central-1

# Приклад відповіді:
# {
#   "imageScanFindings": {
#     "findings": [
#       {
#         "name": "CVE-2023-12345",
#         "severity": "HIGH",
#         "description": "Buffer overflow in libssl...",
#         "uri": "https://nvd.nist.gov/vuln/detail/CVE-2023-12345"
#       }
#     ],
#     "findingSeverityCounts": {"HIGH": 1, "MEDIUM": 3, "LOW": 12}
#   }
# }

Запустити сканування вручну:

aws ecr start-image-scan \
    --repository-name my-api \
    --image-id imageTag=v1.2.3 \
    --region eu-central-1

Image Tag Mutability — захист від перезапису тегів

Tag Mutability визначає, чи можна перезаписати тег образу, який вже існує.

  • MUTABLE (за замовчуванням) — тег :latest або :v1.0.0 можна перезаписати новим образом. Зручно, але небезпечно: CI/CD міг завантажити інший образ під тим самим тегом.
  • IMMUTABLE — після першого завантаження тег не можна перезаписати. Забезпечує повну відтворюваність: образ :v1.0.0 завжди залишається тим самим.

Рекомендація: використовуйте IMMUTABLE для production репозиторіїв. Для тегування використовуйте git commit SHA або семантичне версіонування, а не :latest.

# Змінити mutability для існуючого репозиторію
aws ecr put-image-tag-mutability \
    --repository-name my-api \
    --image-tag-mutability IMMUTABLE \
    --region eu-central-1

Cross-Account Access — доступ між AWS акаунтами

У multi-account архітектурі (окремі акаунти для dev/staging/production) ECR може бути в одному акаунті, а ECS кластери — в інших. Для цього налаштовується Repository Policy — IAM Policy на рівні репозиторію, яка дозволяє зовнішнім акаунтам завантажувати образи.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowPullFromProductionAccount",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::PRODUCTION_ACCOUNT_ID:root"
            },
            "Action": ["ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "ecr:BatchCheckLayerAvailability"]
        }
    ]
}
# Застосувати Repository Policy
aws ecr set-repository-policy \
    --repository-name my-api \
    --policy-text file://repo-policy.json \
    --region eu-central-1

ECR Replication — реплікація між регіонами

Якщо ваші ECS кластери розташовані у кількох регіонах (наприклад, EU та US), образи варто реплікувати в кожен регіон — для швидкого завантаження без cross-region трафіку.

# Налаштувати реплікацію образів у інший регіон
aws ecr put-replication-configuration \
    --replication-configuration '{
        "rules": [{
            "destinations": [{"region": "us-east-1", "registryId": "ACCOUNT_ID"}],
            "repositoryFilters": [{"filter": "my-api", "filterType": "PREFIX_MATCH"}]
        }]
    }' \
    --region eu-central-1

Довідник: Amazon ECS — повна документація функцій

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

actor "Developer" as Dev
rectangle "AWS CLI\naws ecs execute-command" as CLI #e0e7ff
rectangle "AWS Systems Manager\nSession Manager" as SSM #fef3c7
rectangle "ECS Task\n(Container)" as Task #bbf7d0 {
    rectangle ".NET API" as API #d1fae5
    rectangle "/bin/bash" as BASH #f3f4f6
}

Dev -right-> CLI : runs command
CLI -right-> SSM : creates session
SSM -right-> Task : opens channel
note right of Task
  Interactive bash session
  inside running container
end note
@enduml

ECS Exec — підключення до контейнера для дебагінгу

ECS Exec — це функція, яка дозволяє запустити інтерактивну сесію (bash або будь-яку команду) всередині запущеного Task. Аналог kubectl exec у Kubernetes або docker exec для локальних контейнерів.

Навіщо потрібно: коли контейнер поводиться дивно у production — перевірити змінні середовища, пінгувати БД зсередини контейнера, переглянути файли.

Підготовка — IAM права для ECS Exec:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ssmmessages:CreateControlChannel",
                "ssmmessages:CreateDataChannel",
                "ssmmessages:OpenControlChannel",
                "ssmmessages:OpenDataChannel"
            ],
            "Resource": "*"
        }
    ]
}

Додайте цю Policy до Task Role (не Execution Role).

Увімкнення ECS Exec для Service:

# Увімкнути execute-command при оновленні сервісу
aws ecs update-service \
    --cluster my-cluster \
    --service my-api-service \
    --enable-execute-command \
    --region eu-central-1

# Підключитися до контейнера у запущеному Task
TASK_ARN="arn:aws:ecs:eu-central-1:123456789012:task/my-cluster/abc123"

aws ecs execute-command \
    --cluster my-cluster \
    --task $TASK_ARN \
    --container my-api \
    --interactive \
    --command "/bin/bash" \
    --region eu-central-1
ECS Exec session
The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.
Starting session with SessionId: ecs-execute-command-abc123
root@3f4a8b9c1d2e:/app# env | grep ASPNETCORE
ASPNETCORE_ENVIRONMENT=Production
ASPNETCORE_URLS=http://+:8080
root@3f4a8b9c1d2e:/app# curl localhost:8080/health
Healthy
ECS Exec використовує AWS Systems Manager Session Manager під капотом. Для роботи потрібно: встановлений AWS CLI Session Manager plugin локально (session-manager-plugin), Task Role з SSM правами (вище), enableExecuteCommand: true у Service.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

rectangle "Service Update (v1→v2)" as UPDATE #dbeafe
rectangle "New Tasks starting..." as NEW #bbf7d0
rectangle "Health Check Failed ❌" as FAIL #fecaca
rectangle "Count: 10 failures" as COUNT #fde68a
rectangle "Circuit Breaker 🔴" as CB #fca5a5
rectangle "Rollback to v1 ✅" as ROLLBACK #bbf7d0
rectangle "Service stable" as OK #d1fae5

UPDATE -down-> NEW
NEW -down-> FAIL : health check fails
FAIL -down-> COUNT : increment counter
COUNT -down-> CB : threshold reached
CB -down-> ROLLBACK : automatic
ROLLBACK -down-> OK
@enduml

Deployment Circuit Breaker — автоматичний відкат при збоях

Circuit Breaker — це механізм, який автоматично зупиняє rolling deployment і відкочується до попередньої версії, якщо нові Tasks не проходять health check.

Без Circuit Breaker: якщо новий образ має баг і Tasks не запускаються — ECS нескінченно намагається їх запустити. Deployment «зависає», стара версія поступово видаляється — сервіс деградує.

З Circuit Breaker: ECS підраховує, скільки Tasks підряд не вдалося запустити. Якщо досягнуто порогу (за замовчуванням 10 провальних Tasks) — deployment зупиняється і автоматично відкочується до попередньої Task Definition revision.

# Увімкнути Circuit Breaker при оновленні Service
aws ecs update-service \
    --cluster my-cluster \
    --service my-api-service \
    --deployment-configuration '{
        "deploymentCircuitBreaker": {
            "enable": true,
            "rollback": true
        },
        "maximumPercent": 200,
        "minimumHealthyPercent": 50
    }' \
    --region eu-central-1

Через Console: ECS → Services → Update → розділ «Deployment options» → ✅ Circuit breaker → ✅ Rollback on failure.

Завжди вмикайте Circuit Breaker з rollback: true у production. Це забезпечує автоматичний захист від деплою зламаного коду.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

package "ECS Cluster" #e9ecef {
    package "Service Connect Namespace: my-app" #dbeafe {
        rectangle "OrderService\n(order-service:8080)" as OS #bbf7d0
        rectangle "UserService\n(user-service:8080)" as US #bbf7d0
        rectangle "NotificationService\n(notify-service:8080)" as NS #bbf7d0
    }
}

OS -down-> US : http://user-service:8080/api/users
OS -down-> NS : http://notify-service:8080/send
note right of OS
  Service Connect proxy (Envoy)
  automatically routes traffic
end note
@enduml

Service Connect — внутрішній service discovery

ECS Service Connect — це вбудований механізм service discovery та load balancing для комунікації між мікросервісами всередині одного кластера. Замість того, щоб налаштовувати AWS Cloud Map або ALB для internal трафіку, Service Connect робить це автоматично.

Сценарій: OrderService потребує звертатись до UserService. З Service Connect — UserService доступний просто за назвою user-service:8080 без жодного DNS-налаштування.

Налаштування Service Connect у Task Definition:

"serviceConnectConfiguration": {
    "enabled": true,
    "namespace": "my-app",
    "services": [{
        "portName": "http",
        "discoveryName": "user-service",
        "clientAliases": [{
            "port": 8080,
            "dnsName": "user-service"
        }]
    }]
}

Після цього OrderService може звертатись до http://user-service:8080/api/users — ECS автоматично проксіює запит на відповідний Task через Envoy proxy, вбудований у кожен Task.


Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

rectangle "EventBridge Scheduler\ncron(0 2 * * ? *)" as EB #e0e7ff
rectangle "IAM Role" as IAM #fef3c7
rectangle "ECS Cluster" as ECS #dbeafe {
    rectangle "Daily Cleanup Task\n(Fargate)" as TASK #bbf7d0
}
rectangle "CloudWatch Logs" as LOGS #d1fae5

EB -right-> IAM : assumes role
IAM -right-> ECS : ecs:RunTask
ECS -right-> TASK : runs container
TASK -right-> LOGS : writes logs
note right of EB
  Trigger: every day at 02:00 UTC
  One-time RunTask (not Service)
end note
@enduml

Scheduled Tasks — запуск контейнерів за розкладом

ECS підтримує запуск одноразових Tasks за розкладом через інтеграцію з Amazon EventBridge Scheduler. Це ідеально для batch-задач, щоденних звітів, очищення даних.

Приклад: запускати контейнер щодня о 02:00 UTC:

# Крок 1: Створіть Trust Policy файл для IAM Role
cat > /tmp/eb-ecs-trust.json << 'EOF'
{
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Principal": { "Service": "scheduler.amazonaws.com" },
        "Action": "sts:AssumeRole"
    }]
}
EOF

# Крок 2: Створіть IAM Role
aws iam create-role \
    --role-name EventBridgeECSRole \
    --assume-role-policy-document file:///tmp/eb-ecs-trust.json

# Крок 3: Додайте інлайн-політику з правами ecs:RunTask та iam:PassRole
aws iam put-role-policy \
    --role-name EventBridgeECSRole \
    --policy-name ECSRunTaskPolicy \
    --policy-document '{
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": "ecs:RunTask",
                "Resource": "arn:aws:ecs:eu-central-1:123456789012:task-definition/cleanup-task:*"
            },
            {
                "Effect": "Allow",
                "Action": "iam:PassRole",
                "Resource": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole"
            }
        ]
    }'

# Крок 4: Створіть scheduled task
aws scheduler create-schedule \
    --name "daily-cleanup" \
    --schedule-expression "cron(0 2 * * ? *)" \
    --flexible-time-window '{"Mode": "OFF"}' \
    --target '{
        "Arn": "arn:aws:ecs:eu-central-1:123456789012:cluster/my-cluster",
        "RoleArn": "arn:aws:iam::123456789012:role/EventBridgeECSRole",
        "EcsParameters": {
            "TaskDefinitionArn": "arn:aws:ecs:eu-central-1:123456789012:task-definition/cleanup-task:1",
            "LaunchType": "FARGATE",
            "NetworkConfiguration": {
                "AwsvpcConfiguration": {
                    "Subnets": ["subnet-xxx"],
                    "SecurityGroups": ["sg-xxx"],
                    "AssignPublicIp": "DISABLED"
                }
            }
        }
    }' \
    --region eu-central-1

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

rectangle "ECS Cluster" as CLUSTER #e9ecef {
    rectangle "Capacity Provider Strategy" as STRAT #dbeafe {
        rectangle "FARGATE (25%)\nbase=1, weight=1" as FG #bbf7d0
        rectangle "FARGATE_SPOT (75%)\nweight=3" as SPOT #fef3c7
    }
    rectangle "Task 1 (base)" as T1 #bbf7d0
    rectangle "Task 2 (Spot)" as T2 #fde68a
    rectangle "Task 3 (Spot)" as T3 #fde68a
    rectangle "Task 4 (Spot)" as T4 #fde68a
}

note right of SPOT
  ~50-60% cost savings
  Tasks can be interrupted
  with 2-min warning
end note
@enduml

Capacity Providers — управління ресурсами кластера

Capacity Provider визначає, де і як ECS виділяє ресурси для запуску Tasks. Для Fargate доступні два built-in провайдери:

  • FARGATE — стандартний Fargate. Підходить для більшості задач.
  • FARGATE_SPOT — Tasks запускаються на AWS Spot Capacity. Знижка до 70%, але Tasks можуть бути примусово зупинені з 2-хвилинним попередженням. Ідеально для batch-задач і stateless воркерів.

Налаштування Capacity Provider Strategy:

# Налаштувати кластер на використання Fargate Spot
aws ecs put-cluster-capacity-providers \
    --cluster my-cluster \
    --capacity-providers FARGATE FARGATE_SPOT \
    --default-capacity-provider-strategy \
        "capacityProvider=FARGATE_SPOT,weight=3,base=0" \
        "capacityProvider=FARGATE,weight=1,base=1" \
    --region eu-central-1

Ця конфігурація: 1 Task завжди на FARGATE (base=1), потім 75% нових Tasks — FARGATE_SPOT, 25% — FARGATE. Економія 50–60% при прийнятному рівні надійності.


Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

rectangle "ECS Cluster" as CLUSTER #e9ecef {
    rectangle "Service: my-api\nTask 1" as T1 #bbf7d0
    rectangle "Service: my-api\nTask 2" as T2 #bbf7d0
}

rectangle "CloudWatch" as CW #dbeafe {
    rectangle "Container Insights" as CI #bfdbfe
    rectangle "Metrics: CPU, Memory\nNetwork, Storage" as MET #e0e7ff
    rectangle "Logs: /ecs/my-api" as LOGS #d1fae5
    rectangle "Dashboard" as DASH #fef3c7
}

CLUSTER -right-> CW : emits metrics & logs
CW --> CI : aggregates
CI --> MET : collects
CI --> LOGS : streams
LOGS --> DASH : visualizes
@enduml

Container Insights — розширений моніторинг

CloudWatch Container Insights — це dashboard з деталізованими метриками для ECS: CPU та Memory по кожному Task, Service та Cluster; кількість Task failures; network I/O; storage I/O.

Увімкнення для кластера:

aws ecs update-cluster-settings \
    --cluster my-cluster \
    --settings name=containerInsights,value=enabled \
    --region eu-central-1

Через Console: ECS → Clusters → my-clusterUpdate cluster → ✅ Use Container Insights.

Після увімкнення: CloudWatchContainer Insights → оберіть ECS Clusters — ви побачите автоматично згенеровані дашборди з графіками CPU, Memory, Task count по кожному сервісу.


Практичний приклад: .NET Web API на ECS Fargate від А до Я

Тепер застосуємо всю теорію та пройдемо повний шлях: від порожньої папки до працюючого API у хмарі.

Крок 1: Створення .NET проєкту

Відкрийте термінал і створіть новий проєкт:

dotnet new webapi
$ dotnet new webapi -n EcsLabApi --no-openapi
The template "ASP.NET Core Web API" was created successfully.
$ cd EcsLabApi

Відредагуйте Program.cs — повний вміст файлу:

var builder = WebApplication.CreateBuilder(args);

// Реєструємо Health Check сервіс
builder.Services.AddHealthChecks();

var app = builder.Build();

// Головний ендпоінт — версія відповіді береться зі змінної середовища
var version = Environment.GetEnvironmentVariable("APP_VERSION") ?? "1.0.0";

app.MapGet("/", () => new
{
    message = "Hello from ECS Fargate!",
    version = version,
    environment = app.Environment.EnvironmentName,
    timestamp = DateTime.UtcNow
});

// Health check ендпоінт — ECS використовує його для перевірки стану
app.MapHealthChecks("/health");

app.Run();

Перевірте, що проєкт збирається локально:

dotnet run
$ dotnet run
info: Microsoft.Hosting.Lifetime[14]
     Now listening on: http://localhost:5000
$ curl http://localhost:5000/health
Healthy

Крок 2: Dockerfile та .dockerignore

Створіть файл Dockerfile у корені проєкту EcsLabApi/:

# ─── STAGE 1: BUILD ───────────────────────────────────────────
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# Копіюємо .csproj і відновлюємо залежності (layer cache!)
COPY ["EcsLabApi.csproj", "."]
RUN dotnet restore "EcsLabApi.csproj"

# Копіюємо решту коду і публікуємо Release-версію
COPY . .
RUN dotnet publish "EcsLabApi.csproj" -c Release -o /app/publish --no-restore

# ─── STAGE 2: RUNTIME ─────────────────────────────────────────
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app

# ASP.NET Core слухає на порту 8080 (не 5000!)
ENV ASPNETCORE_URLS=http://+:8080

# Встановлюємо curl — він потрібен для health check команди в ECS Task Definition.
# Базовий aspnet-образ мінімальний і curl не містить за замовчуванням.
# Без цього рядка ECS health check завжди повертає "unhealthy".
RUN apt-get update \
    && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

# Копіюємо лише опублікований артефакт — SDK і вихідний код не потрапляють у образ
COPY --from=build /app/publish .

ENTRYPOINT ["dotnet", "EcsLabApi.dll"]
curl відсутній у базовому aspnet образі. Якщо не встановити його явно — ECS health check (curl -f http://localhost:8080/health || exit 1) завжди завершуватиметься з кодом 127 («command not found»), і Task буде позначений як UNHEALTHY та циклічно перезапускатиметься.

Створіть файл .dockerignore:

**/bin/
**/obj/
.git
.gitignore
.vs/
.rider/
Dockerfile
.dockerignore
**/appsettings.Development.json

Зберіть образ і перевірте локально:

docker build & run
$ docker build -t ecs-lab-api:v1.0.0 .
[+] Building 42.3s (12/12) FINISHED
=> [build 1/5] FROM mcr.microsoft.com/dotnet/sdk:8.0
=> [build 4/5] RUN dotnet restore "EcsLabApi.csproj"
=> [build 5/5] RUN dotnet publish "EcsLabApi.csproj" -c Release -o /app/publish
=> [final 2/2] COPY --from=build /app/publish .
=> => writing image sha256:a1b2c3d4...
$ docker run -p 8080:8080 ecs-lab-api:v1.0.0
info: Microsoft.Hosting.Lifetime[14]
     Now listening on: http://[::]:8080
$ curl http://localhost:8080/health
Healthy
$ curl http://localhost:8080/
{"message":"Hello from ECS Fargate!","version":"1.0.0","environment":"Production",...}

Крок 3: Створення ECR репозиторію

# Спочатку дізнайтесь ваш Account ID — він знадобиться в наступних кроках
aws sts get-caller-identity --query Account --output text
# Виведе: 123456789012  ← запам'ятайте це число, це ВАШ реальний AWS Account ID
# Створіть репозиторій у ECR
# eu-central-1 — змініть на ваш регіон, якщо потрібно
aws ecr create-repository \
    --repository-name ecs-lab-api \
    --region eu-central-1 \
    --image-scanning-configuration scanOnPush=true

Успішна відповідь матиме вигляд:

{
    "repository": {
        "repositoryArn": "arn:aws:ecr:eu-central-1:123456789012:repository/ecs-lab-api",
        "registryId": "123456789012",
        "repositoryName": "ecs-lab-api",
        "repositoryUri": "123456789012.dkr.ecr.eu-central-1.amazonaws.com/ecs-lab-api"
    }
}

Значення repositoryUri — це адреса вашого репозиторію. Скопіюйте її, вона знадобиться у наступному кроці.

Крок 4: Push образу в ECR

Виконайте команди послідовно. Замініть 123456789012 на ваш реальний Account ID (отриманий на попередньому кроці):

# Змінні — підставте ваші реальні значення
ACCOUNT_ID="123456789012"   # ← ЗАМІНІТЬ на ваш Account ID
REGION="eu-central-1"        # ← змініть на ваш регіон
REPO_URI="${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/ecs-lab-api"

echo "Буде використано репозиторій: $REPO_URI"
# Аутентифікуємось у ECR — отримуємо тимчасовий токен і передаємо docker login
aws ecr get-login-password --region $REGION | \
    docker login --username AWS --password-stdin "${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com"
docker login output
Login Succeeded
# Тегуємо локальний образ адресою ECR репозиторію
docker tag ecs-lab-api:v1.0.0 $REPO_URI:v1.0.0

# Завантажуємо образ у ECR
docker push $REPO_URI:v1.0.0
docker push output
$ docker push 123456789012.dkr.ecr.eu-central-1.amazonaws.com/ecs-lab-api:v1.0.0
The push refers to repository [123456789012.dkr.ecr.eu-central-1.amazonaws.com/ecs-lab-api]
a1b2c3d4e5f6: Pushed
b2c3d4e5f6a7: Pushed
v1.0.0: digest: sha256:abc123... size: 1234

Перевірте результат — образ має з'явитись у консолі:

  1. ECR → Repositories → ecs-lab-api → вкладка «Images»
  2. Ви побачите образ з тегом v1.0.0 та статус сканування

Крок 5: Створення ECS Cluster

  1. Відкрийте ECS (Elastic Container Service) у AWS Console
  2. У лівому меню → ClustersCreate cluster
  3. Заповніть:
    • Cluster name: ecs-lab-cluster
    • Infrastructure:AWS Fargate (зніміть галочку з Amazon EC2 instances)
    • Моніторинг: ✅ Use Container Insights (рекомендовано — дасть метрики CPU/Memory)
  4. Натисніть «Create» — кластер створюється за ~30 секунд
aws ecs create-cluster — відповідь
$ aws ecs create-cluster --cluster-name ecs-lab-cluster --capacity-providers FARGATE --settings name=containerInsights,value=enabled --region eu-central-1
{
  "cluster": {
    "clusterArn": "arn:aws:ecs:eu-central-1:123456789012:cluster/ecs-lab-cluster",
    "clusterName": "ecs-lab-cluster",
    "status": "ACTIVE",
    "registeredContainerInstancesCount": 0,
    "settings": [{ "name": "containerInsights", "value": "enabled" }],
    "capacityProviders": ["FARGATE"]
  }
}

Крок 6: Створення IAM Role для ECS Task Execution

ECS потребує IAM Role, щоб завантажити образ з ECR та записувати логи. Це стандартна роль — її потрібно створити один раз.

  1. Відкрийте IAMRolesCreate role
  2. Trusted entity type: AWS service
  3. Use case: знайдіть і оберіть Elastic Container Service Task
  4. Натисніть Next
  5. У пошуку политик введіть AmazonECSTaskExecutionRolePolicy → поставте ✅
  6. Натисніть NextRole name: ecsTaskExecutionRoleCreate role

Крок 7: Створення Task Definition

Важливо: у полі image та executionRoleArn підставте ваші реальні значення.

  1. ECS → Task definitionsCreate new task definition
  2. Task definition family: ecs-lab-api
  3. Infrastructure requirements:
    • Launch type: ✅ AWS Fargate
    • OS/Architecture: Linux/X86_64
    • CPU: 0.25 vCPU (.25)
    • Memory: 0.5 GB (512 MB)
  4. Task roles:
    • Task role: залиште пустим (наш API не звертається до інших AWS сервісів)
    • Task execution role: ecsTaskExecutionRole
  5. Container — 1:
    • Name: ecs-lab-api
    • Image URI: ВАШ_ACCOUNT_ID.dkr.ecr.eu-central-1.amazonaws.com/ecs-lab-api:v1.0.0(скопіюйте URI з ECR → Repositories → ecs-lab-api → Images → тег v1.0.0)
    • Container port: 8080, Protocol: TCP
    • Environment variables → Add:
      • Key: ASPNETCORE_ENVIRONMENT, Value: Production
      • Key: APP_VERSION, Value: 1.0.0
    • Health check command: CMD-SHELL, curl -f http://localhost:8080/health || exit 1
    • Health check intervals: Interval: 30s, Timeout: 5s, Start period: 10s, Retries: 3
  6. Logging: ✅ Use log collection → awslogs, log group /ecs/ecs-lab-api
  7. Натисніть «Create»

Крок 8: Визначення Security Group та підмережі

Перед запуском Service потрібно знати ID вашої VPC та підмереж. Fargate Tasks будуть запускатись у них.

# Знайдіть ID вашого default VPC
aws ec2 describe-vpcs \
    --filters "Name=isDefault,Values=true" \
    --query "Vpcs[0].VpcId" \
    --output text --region eu-central-1
# Виведе щось на кшталт: vpc-0a1b2c3d4e5f67890
# ← ЗАПИШІТЬ це значення, знадобиться далі

# Знайдіть підмережі у цьому VPC (потрібні мінімум 2 у різних AZ)
aws ec2 describe-subnets \
    --filters "Name=defaultForAz,Values=true" \
    --query "Subnets[*].{ID:SubnetId,AZ:AvailabilityZone}" \
    --output table --region eu-central-1
# Виведе таблицю підмереж — запишіть 2 ID, наприклад:
# subnet-0a1b2c3d  eu-central-1a
# subnet-1b2c3d4e  eu-central-1b

Створіть Security Group для Tasks:

VPC_ID="vpc-0a1b2c3d4e5f67890"   # ← ЗАМІНІТЬ на ваш реальний VPC ID

# Створіть Security Group
SG_ID=$(aws ec2 create-security-group \
    --group-name "ecs-lab-sg" \
    --description "Security group for ECS lab tasks" \
    --vpc-id $VPC_ID \
    --region eu-central-1 \
    --query GroupId --output text)

echo "Security Group ID: $SG_ID"   # ← ЗАПИШІТЬ цей ID

# Дозвольте вхідний трафік на порт 8080
aws ec2 authorize-security-group-ingress \
    --group-id $SG_ID \
    --protocol tcp \
    --port 8080 \
    --cidr 0.0.0.0/0 \
    --region eu-central-1
Правило 0.0.0.0/0 (доступ з усього Інтернету) прийнятне лише для цьої навчальної лабораторної роботи, де Tasks мають публічний IP і немає ALB. У production-середовищі Security Group Tasks повинна дозволяти вхідний трафік на порт 8080 виключно з Security Group вашого ALB, а не з усього Інтернету:
# Правильне production-правило для Security Group Tasks:
# Source: sg-alb-id  (а не 0.0.0.0/0)
aws ec2 authorize-security-group-ingress \
    --group-id $TASKS_SG_ID \
    --protocol tcp \
    --port 8080 \
    --source-group $ALB_SG_ID \
    --region eu-central-1

Крок 9: Запуск ECS Service

  1. ECS → Clustersecs-lab-cluster → вкладка ServicesCreate
  2. Environment:
    • Compute options: Launch type → FARGATE
    • Platform version: LATEST
  3. Deployment configuration:
    • Application type: Service
    • Task definition Family: ecs-lab-api, Revision: LATEST
    • Service name: ecs-lab-service
    • Desired tasks: 2
  4. Networking:
    • VPC: ваш default VPC
    • Subnets: оберіть 2 підмережі у різних AZ
    • Security group: ecs-lab-sg (створений вище)
    • Public IP: Turn on (для цієї лабораторної роботи)
  5. Натисніть «Create»

Після запуску зачекайте 1–2 хвилини поки Tasks перейдуть у стан RUNNING.

aws ecs list-tasks
$ aws ecs list-tasks --cluster ecs-lab-cluster --region eu-central-1
{
  "taskArns": [
    "arn:aws:ecs:eu-central-1:123456789012:task/ecs-lab-cluster/abc123",
    "arn:aws:ecs:eu-central-1:123456789012:task/ecs-lab-cluster/def456"
  ]
}

Крок 10: Перевірка роботи застосунку

Знайдіть публічну IP-адресу одного з Tasks:

  1. ECS → Clusters → ecs-lab-cluster → вкладка Tasks
  2. Натисніть на будь-який Task у стані RUNNING
  3. У розділі Network → скопіюйте Public IP
  4. Відкрийте у браузері або через curl

Перевірте, що API відповідає:

curl перевірка
$ curl http://<PUBLIC_IP>:8080/health
Healthy
$ curl http://<PUBLIC_IP>:8080/
{"message":"Hello from ECS Fargate!","version":"1.0.0","environment":"Production","timestamp":"2024-..."}

Крок 11: Rolling Update — деплой нової версії

Змінимо версію API і задеплоїмо оновлення без зупинки сервісу.

Оновіть Program.cs — змініть значення за замовчуванням:

var version = Environment.GetEnvironmentVariable("APP_VERSION") ?? "1.1.0";

Зберіть і завантажте новий образ:

ACCOUNT_ID="123456789012"   # ← ваш Account ID
REGION="eu-central-1"
REPO_URI="${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/ecs-lab-api"

docker build -t ecs-lab-api:v1.1.0 .
docker tag ecs-lab-api:v1.1.0 $REPO_URI:v1.1.0
docker push $REPO_URI:v1.1.0

Оновіть Task Definition та запустіть Rolling Update:

  1. ECS → Task definitionsecs-lab-apiCreate new revision
  2. Знайдіть поле Image URI у розділі Container → змініть тег :v1.0.0 на :v1.1.0
  3. Також змініть Environment variable APP_VERSION на 1.1.0
  4. Натисніть «Create» → з'явиться revision 2
  5. ECS → Clusters → ecs-lab-cluster → Services → ecs-lab-serviceUpdate
  6. Task definition: оберіть ecs-lab-api:2 (нова revision)
  7. Натисніть «Update»
  8. Перейдіть на вкладку Deployments — спостерігайте як нові Tasks запускаються (Running: 2→4→2)

Поки йде оновлення, виконуйте запити до API — жодного збою не буде:

Перевірка під час Rolling Update
$ while true; do curl -s http://<PUBLIC_IP>:8080/ | grep version; sleep 2; done
"version":"1.0.0"
"version":"1.0.0"
"version":"1.1.0"
"version":"1.1.0"

Ви побачите, як відповіді поступово переходять з 1.0.0 на 1.1.0 — без жодної помилки.

Крок 12: Перегляд логів у CloudWatch

  1. Відкрийте CloudWatchLog groups/ecs/ecs-lab-api
  2. Оберіть один із Log Streams — ви побачите логи запитів у реальному часі
  3. Натисніть «Start tailing» для live-режиму
aws logs tail — логи .NET API з CloudWatch
$ aws logs tail /ecs/ecs-lab-api --follow --format short --region eu-central-1
2024-01-15T10:23:45 ecs/ecs-lab-api/abc123 info: Microsoft.Hosting.Lifetime[14]
2024-01-15T10:23:45 ecs/ecs-lab-api/abc123     Now listening on: http://[::]:8080
2024-01-15T10:23:46 ecs/ecs-lab-api/abc123 info: Microsoft.Hosting.Lifetime[0] Application started.
2024-01-15T10:24:01 ecs/ecs-lab-api/abc123 info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
2024-01-15T10:24:01 ecs/ecs-lab-api/abc123     Request starting HTTP/1.1 GET http://10.0.1.47:8080/health - - -
2024-01-15T10:24:01 ecs/ecs-lab-api/abc123 info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
2024-01-15T10:24:01 ecs/ecs-lab-api/abc123     Request finished HTTP/1.1 GET /health - 200 - text/plain 1.2ms
Адреса 10.0.1.47 у логах — це IP ALB, який виконує health check. Кожен Task пише логи у окремий Log Stream (ім'я стріму — це ecs/<родина>/<task-id>). При рестарті Task створюється новий стрім, тому історія логів попередньої інстанції завжди доступна в CloudWatch.

Крок 13: ОБОВ'ЯЗКОВО — Очищення ресурсів

Не пропускайте цей крок! Fargate Tasks тарифікуються за кожну хвилину роботи. ECS Service з 2 Tasks на Fargate коштує ~$0.015/год — це ~$11/місяць. Видаліть усе після завершення лабораторної роботи.

Порядок видалення важливий — ресурси мають залежності між собою:

  1. ECS → Clusters → ecs-lab-cluster → Services → ecs-lab-serviceDelete service
    • ✅ Force delete service → Delete
  2. ECS → Clusters → ecs-lab-clusterDelete cluster → підтвердіть
  3. ECR → Repositories → ecs-lab-apiDelete repository
    • ✅ Delete all images in this repository → підтвердіть назву → Delete
  4. CloudWatch → Log groups → /ecs/ecs-lab-apiActions → Delete log group(s)
  5. VPC → Security groups → ecs-lab-sgActions → Delete security group
  6. IAM → Roles → ecsTaskExecutionRoleDelete (лише якщо ви більше не будете використовувати ECS)

Крок 14 (бонус): Підключення безкоштовного домену pp.ua до ALB

Ваш ECS Service доступний через ALB DNS name — щось схоже на ecs-lab-alb-123456789.eu-central-1.elb.amazonaws.com. Підключимо читабельний домен pp.ua.

Загальна схема:

myecs.pp.ua
    ↓ CNAME record (DNS)
ecs-lab-alb-123456789.eu-central-1.elb.amazonaws.com
    ↓ ALB (порт 80/443)
ECS Fargate Task → .NET API (порт 8080)
На відміну від EC2 де ми використовували A record (IP-адресу), тут ALB не має сталої IP — він використовує DNS name. Тому для ALB завжди використовують CNAME record (псевдонім), що вказує на DNS name ALB.

Крок 14a: Отримання DNS name вашого ALB

# Знайдіть DNS name ALB (якщо не записали раніше)
aws elbv2 describe-load-balancers \
    --names ecs-lab-alb \
    --query "LoadBalancers[0].DNSName" \
    --output text --region eu-central-1
# Виведе: ecs-lab-alb-123456789.eu-central-1.elb.amazonaws.com

Крок 14b: Реєстрація субдомену на pp.ua

  1. Перейдіть на https://pp.ua та зареєструйте субдомен (наприклад myecs.pp.ua)
  2. Підтвердіть email і увійдіть у панель управління

Крок 14c: Додавання CNAME record у pp.ua

Панель pp.ua → DNS ManagementAdd Record:

ПолеЗначення
TypeCNAME
Name/Hostmyecs
Value/Targetecs-lab-alb-123456789.eu-central-1.elb.amazonaws.com (ваш ALB DNS)
TTL300

Різниця A vs CNAME: A record вказує на IP-адресу (3.64.185.42). CNAME record вказує на інший DNS name (ecs-lab-alb-xxx.amazonaws.com) — браузер потім сам вирішує IP цього name. CNAME не можна використовувати для кореневого домену (pp.ua), лише для субдоменів.

Зачекайте 1–10 хвилин і перевірте:

Перевірка CNAME та HTTP запиту
$ nslookup -type=CNAME myecs.pp.ua
Non-authoritative answer:
myecs.pp.ua canonical name = ecs-lab-alb-123456789.eu-central-1.elb.amazonaws.com.
$ curl http://myecs.pp.ua/
{"message":"Hello from ECS Fargate!","container":"abc123def456",...}

Крок 14d: HTTPS через ACM + ALB (опціонально)

ALB підтримує ACM сертифікати — на відміну від EC2, де потрібен Certbot. Сертифікат ACM безкоштовний.

ACM сертифікат для ALB (на відміну від CloudFront) створюється у тому ж регіоні що й ALB — наприклад eu-central-1. Не us-east-1!
# 1. Запросити сертифікат (eu-central-1, той самий регіон що ALB)
CERT_ARN=$(aws acm request-certificate \
    --domain-name "myecs.pp.ua" \
    --validation-method DNS \
    --region eu-central-1 \
    --query CertificateArn --output text)
echo "Certificate ARN: $CERT_ARN"

# 2. Отримати CNAME для DNS валідації
aws acm describe-certificate \
    --certificate-arn $CERT_ARN \
    --region eu-central-1 \
    --query "Certificate.DomainValidationOptions[0].ResourceRecord"
# Виведе Name і Value для CNAME — додайте у pp.ua (Type: CNAME)

Додайте у pp.ua другий CNAME для валідації ACM (Name і Value з попередньої команди). Дочекайтесь статусу Issued.

# 3. Знайдіть ARN вашого ALB
ALB_ARN=$(aws elbv2 describe-load-balancers \
    --names ecs-lab-alb \
    --query "LoadBalancers[0].LoadBalancerArn" \
    --output text --region eu-central-1)

# 3b. Знайдіть ARN Target Group (створеної при налаштуванні Service)
TG_ARN=$(aws elbv2 describe-target-groups \
    --names ecs-lab-tg \
    --query "TargetGroups[0].TargetGroupArn" \
    --output text --region eu-central-1)

# 4. Додати HTTPS Listener на ALB
aws elbv2 create-listener \
    --load-balancer-arn $ALB_ARN \
    --protocol HTTPS --port 443 \
    --certificates CertificateArn=$CERT_ARN \
    --default-actions Type=forward,TargetGroupArn=$TG_ARN \
    --region eu-central-1

# 5. HTTP → HTTPS redirect (змінити HTTP listener)
HTTP_LISTENER=$(aws elbv2 describe-listeners \
    --load-balancer-arn $ALB_ARN \
    --query "Listeners[?Port==\`80\`].ListenerArn" \
    --output text --region eu-central-1)

aws elbv2 modify-listener \
    --listener-arn $HTTP_LISTENER \
    --default-actions 'Type=redirect,RedirectConfig={Protocol=HTTPS,Port=443,StatusCode=HTTP_301}' \
    --region eu-central-1

Тепер https://myecs.pp.ua — ваш ECS Fargate .NET API з офіційним SSL сертифікатом!


Резюме

  • Docker вирішує проблему «works on my machine»: застосунок пакується разом із середовищем у незмінний образ.
  • Multi-stage Dockerfile: build stage (~800 MB SDK) → runtime stage (~200 MB). Вихідний код не потрапляє у фінальний образ.
  • Amazon ECR — приватний реєстр образів, інтегрований з IAM. Автоматичне сканування вразливостей при push.
  • ECS: Task Definition (шаблон) → Task (запущений контейнер) → Service (підтримує N Tasks, rolling updates).
  • Fargate — serverless запуск контейнерів без управління EC2. Оплата за vCPU/RAM × час роботи Task.
  • awsvpc — кожен Task отримує власний ENI та IP. Security Groups на рівні Task.
  • Rolling updates — нові Tasks запускаються паралельно зі старими, трафік переключається поступово.
  • Environment variables у Task Definition: environment для конфігурації, secrets для паролів зі Secrets Manager.

Практичні завдання

Рівень 1 (Базовий)

Завдання 1. Поясніть різницю між Docker image та Docker container. Що таке multi-stage Dockerfile і яку проблему він вирішує для .NET?

Завдання 2. У чому принципова відмінність між Fargate та EC2 launch type? Для якого сценарію ви б обрали кожен?

Завдання 3. Поясніть, що відбувається під час кожного з станів Task: PROVISIONING, PENDING, ACTIVATING. Чому Task може застрягти у стані PENDING і як це діагностувати?

Рівень 2 (Практичний)

Завдання 4. Напишіть Dockerfile для ASP.NET Core 8, який використовує multi-stage build, запускається як non-root user (USER app перед ENTRYPOINT) і налаштований для production (Release конфігурація).

Завдання 5. Напишіть повний Task Definition JSON для ECS Fargate де: .NET API слухає порт 8080, ASPNETCORE_ENVIRONMENT=Production, connection string PostgreSQL отримується з Secrets Manager, логи пишуться у CloudWatch /ecs/my-api, health check кожні 30 секунд за /health.

Завдання 6. Сервіс отримує всплески після ролінгу оновлення до нової версії. Що змінити в Service, щоб деплой завжди зупинявся при 10+ послідовних помилкових health check? Які два параметри деплою відповідають за мінімальну кількість здорових Tasks під час оновлення?

Рівень 3 (Архітектура)

Завдання 7. Спроектуйте ECS-архітектуру для трьох мікросервісів: UserService (.NET), OrderService (.NET), NotificationService (background worker). Вимоги: усі у приватних підмережах, зовнішній трафік лише через ALB, UserService/OrderService масштабуються за CPU (60%), NotificationService — за кількістю повідомлень у SQS. Паролі в Secrets Manager. Намалюйте PlantUML схему.

Завдання 8. Ваш .NET API пише цитабельні дані безпосередньо у файл (appsettings.Production.json), який потрапляє в Docker image. Опишіть послідовність дій для перенесення секретів у Secrets Manager (які AWS пермішення потрібні, які зміни в Task Definition, які зміни в коді .NET для зчитування з перемінних середовища).

Copyright © 2026