AMAZON ECS → STACKIT KUBERNETES ENGINE · TUTORIAL
Amazon ECS to STACKIT Kubernetes: a worked example
ECS has no drop-in counterpart on STACKIT: the containers stay, the task definition and the service become Kubernetes objects on STACKIT Kubernetes Engine (SKE). This tutorial runs StackToIT on a small Fargate estate and walks through what it writes, what it holds back and what you decide before the first pod runs.
The input: one task definition, one service
A demo estate written for this page: a Fargate service with three tasks of 0.5 vCPU and 1 GiB behind an Application Load Balancer, one secret, one plain environment value and an awslogs log group. Left: the task definition and the service. Right: the Deployment StackToIT drafted for it (the long comment header of the file is left out here; everything shown is unchanged).
resource "aws_ecs_task_definition" "web" {
family = "shop-web"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = "512"
memory = "1024"
execution_role_arn = aws_iam_role.task_execution.arn
container_definitions = jsonencode([
{
name = "web"
image = "registry.example.com/shop/web:1.8.2"
essential = true
portMappings = [{ containerPort = 8080, protocol = "tcp" }]
environment = [{ name = "APP_ENV", value = "production" }]
secrets = [{ name = "DATABASE_URL", valueFrom = aws_secretsmanager_secret.db_url.arn }]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = "/ecs/shop-web"
"awslogs-region" = "eu-central-1"
"awslogs-stream-prefix" = "web"
}
}
}
])
}resource "aws_ecs_service" "web" {
name = "web"
cluster = aws_ecs_cluster.shop.id
task_definition = aws_ecs_task_definition.web.arn
desired_count = 3
launch_type = "FARGATE"
network_configuration {
subnets = [aws_subnet.app_a.id, aws_subnet.app_b.id]
security_groups = [aws_security_group.web.id]
}
load_balancer {
target_group_arn = aws_lb_target_group.web.arn
container_name = "web"
container_port = 8080
}
}apiVersion: apps/v1
kind: Deployment
metadata:
name: web
labels:
app.kubernetes.io/name: web
spec:
replicas: 0 # held back until the start step of the plan scales to the number the header names
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 3
selector:
matchLabels:
app.kubernetes.io/name: web
template:
metadata:
labels:
app.kubernetes.io/name: web
spec:
containers:
- name: web
image: "registry.example.com/shop/web:1.8.2"
ports:
- containerPort: 8080
protocol: TCP
env:
- name: APP_ENV
valueFrom:
configMapKeyRef:
name: web-env
key: APP_ENV
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: web-secrets
key: DATABASE_URL
resources:
requests:
cpu: "500m"
memory: "1024Mi"
limits:
cpu: "500m"
memory: "1024Mi"
readinessProbe:
httpGet:
path: "/healthz"
port: 8080
periodSeconds: 30
timeoutSeconds: 6
successThreshold: 3
failureThreshold: 3The walk-through
Five steps from the Terraform files to a running workload. StackToIT performs the first two; everything after that is your team's work, with the files it wrote.
1 · Run the assessment
Run StackToIT in the folder that holds the Terraform. It reads the files, never connects to AWS and writes an output folder with the draft, the decisions and the execution plan.
2 · Read the classification
The cluster and the service are classed REPLATFORM, the surrounding network, secret, role, log group and load balancer RECONFIGURE. The task definition and the target group have no unit of their own: their content is folded into the service.
3 · Answer the decisions
Answer the open decisions in review/decisions.yaml (how the load balancer reaches the pods, whether the pods get network segmentation, which network holds the cluster and its consumers, the opt-in for the API calls) and set the node pool variables (machine type, minimum, maximum) in the target values file.
4 · Apply the cluster, then the manifests
Apply the Terraform for the SKE cluster first. The manifests are written with replicas set to 0, so nothing runs or connects to a data store before its data is in place.
5 · Scale up and switch traffic
After the data move, scale to the number of tasks the source states, run the smoke test of the checklist, move the load balancer or DNS record and keep ECS running until you accept the result.
What the run reports
The summary the command prints for the demo estate (console.txt in the output folder). The command was run from inside the input folder as stacktoit assess . <output folder> --offline --no-validate; --offline is the documented way to run without network requests, and --no-validate skipped the OpenTofu check of the source, which is why the summary says it was not run.
Source: input
Source files: 1
Resources scanned: 13
support resources: 1 (+0 Terraform helpers, out of scope; 1 + 0 = the 1 support node of summary.support.nodes in assessment.json)
Migration units: 10 (10 instances)
Unit dependencies: 14
Source checks: Built-in source checks passed — OpenTofu validation of the source was not run
STACKIT mapping
────────────────────────────────────────────
DIRECT 0
RECONFIGURE 8 ████████████████████████ 80%
REPLATFORM 2 ██████ 20%
CONTAINERIZE 0
REDESIGN 0
MANUAL_REVIEW 0
BLOCKED 0 (the mapping class BLOCKED, not the execution blockers, which Migration readiness counts)How the ECS settings land on Kubernetes
| ECS setting in the demo | Kubernetes result in the draft | What you check |
|---|---|---|
| desired_count = 3 | replicas: 0 in the file, scaled to 3 at the start step | The start step writes the number into the file too; applying the file again with 0 would hold the pods back again |
| cpu 512, memory 1024 on the task | requests and limits of 500m and 1024Mi on the container | Requests equal limits here; revisit them once you see real usage |
| secrets valueFrom a Secrets Manager secret | secretKeyRef to a Secret web-secrets, key DATABASE_URL | You create the Secret; the value never appears in the draft |
| environment APP_ENV | a ConfigMap web-env with an empty data block | Values are not copied on purpose, because a value can be a secret under any name |
| target group health check /healthz | readinessProbe httpGet on /healthz, port 8080 | The source sets only the path, so the draft writes the documented target-group defaults (interval 30, thresholds 3, timeout by protocol); the kubelet accepts any status from 200 to 399 |
| deployment minimum 100% and maximum 200% (the defaults, not set) | maxUnavailable 0, maxSurge 3 | StackToIT derives both from three replicas using the rounding rules of ECS |
| awslogs log driver | not carried | The container writes to stdout; you choose where the cluster ships the logs, for example STACKIT Logs |
| launch_type FARGATE, awsvpc network mode | not carried | Pods run on the node pool network; there is no per-task network interface and no security group per task |
The cluster and the load balancer
Fargate has no nodes, so the node pool size is a variable you set from the task sizes the report adds up (1.5 vCPU and 3 GiB at the floor and at the ceiling for this service). The load balancer reaches the pods through a NodePort Service on port 30000, with the node addresses as pool targets.
resource "stackit_ske_cluster" "shop" {
project_id = var.project_id
name = "shop"
node_pools = [{
name = "workloads"
machine_type = var.shop_machine_type
minimum = var.shop_nodes_min
maximum = var.shop_nodes_max
availability_zones = var.availability_zones
}]
} target_pools = [{
name = "web"
target_port = 30000
targets = [for ip in var.web_target_ips : { ip = ip }]
active_health_check = {
healthy_threshold = var.web_health_healthy_threshold
interval = var.web_health_interval
interval_jitter = var.alb_interval_jitter
timeout = var.web_health_timeout
unhealthy_threshold = var.web_health_unhealthy_threshold
http_health_checks = {
ok_status = var.web_health_ok_status
path = "/healthz"
}
}
}]apiVersion: v1
kind: Service
metadata:
name: web-nodeport
labels:
app.kubernetes.io/name: web
spec:
type: NodePort
selector:
app.kubernetes.io/name: web
ports:
- name: p8080-tcp
port: 8080
targetPort: 8080
protocol: TCP
nodePort: 30000
Decide how traffic reaches the pods
| Option the plan lists | What it means | What it costs you |
|---|---|---|
| NodePort behind the drafted load balancer (the draft) | A NodePort Service and a target pool of the node IPs | You keep the node addresses current as the pool scales; the load balancer and the nodes must share a network |
| An Ingress controller behind the load balancer | A controller such as ingress-nginx routes by Ingress rules to ClusterIP Services | You install and operate the controller; the draft writes neither it nor the Ingress rules |
| The SKE ALB Ingress extension | Enabled on the cluster through the SKE API; creating an IngressClass provisions a managed Application Load Balancer, and Ingress objects on that class define its routes | As of November 2026 STACKIT offers it only on request through STACKIT Support, as an opt-in feature that may change without notice and that STACKIT does not recommend for production use; the drafted load balancer block is not applied |
The checklist that keeps the workload closed
applications/web.json reports the workload as not ready and lists three blocking items (env value, secret, smoke test). The workload counts as ready only when your team marks them done in the decisions file; StackToIT deploys nothing and runs no test.
"readiness": "NOT_READY",
"readiness_words": "Checklist items are open: StackToIT lists the work and counts an item done only when the customer marks it in the decisions file (application_checklists); StackToIT never deploys the application.",What to change before you apply
The draft is a starting point. These are the edits and answers the demo run asks for.
Give the cluster a size
Set the machine type and the minimum and maximum node count. The maximum must be at least the number of zones; three zones are selected by default.
Place the load balancer
The source load balancer spans two subnets that are separate networks on STACKIT. Its targets are the cluster nodes, so the draft places it in the network SKE creates for the cluster; the provider exports neither that network nor the node addresses, so you apply the cluster first and then set the network id and the node IPs.
Upload the certificate
The HTTPS listener names a certificate ARN. The ARN is not converted: upload the certificate to STACKIT and pass its id in the listener variable.
Restrict who can connect
The source admits every IPv4 address on port 443 and the drafted load balancer does the same. Decide whether to write an allowed source range.
Write the network policy
The security group of the tasks is not translated to pods. The plan lists writing a NetworkPolicy, a security group on the nodes or accepting no segmentation as options.
Fill ConfigMap and Secret
Add APP_ENV to the ConfigMap, create web-secrets with DATABASE_URL and check that the nodes can reach the image registry.
Questions teams ask
Can I keep using Fargate-style task sizes on SKE?
There is no Fargate on STACKIT. The pods use the node pool, so the task sizes become container requests and limits and the node pool is sized from their sum plus system pods and rolling-update surge.
Does StackToIT apply the manifests to my cluster?
No. It writes the manifests, the Terraform and the execution plan. Your team applies them with the cluster kubeconfig after the decisions are answered.
What happens to the task role of my application?
The demo has only an execution role. The draft turns it into a STACKIT service account without permissions, because the run finds no STACKIT counterpart for the role of the platform agent that pulls images and injects secrets. Neither that role nor a task role reaches the pods: no ServiceAccount and no workload identity is drafted, so you decide how images are pulled and how the application authenticates.
How do I roll back after the cutover?
The manifest header describes it: the ECS service is left running, so you return the traffic to it. The pods may stay applied, and ECS is scaled down and deleted only after the pods have carried the load for the period you choose.
Sources
Your next move: STACKIT.
Tell us about your estate. We show you what StackToIT delivers for it: target architecture, Terraform artifacts and a structured migration plan.