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).

Example input · task definitionmain.tf
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"
        }
      }
    }
  ])
}
Example input · servicemain.tf
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
  }
}
Kubernetes draft by StackToITterraform/kubernetes/web/manifests.yaml
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: 3

The 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. 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. 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. 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. 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. 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.

Run summaryconsole.txt
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 demoKubernetes result in the draftWhat you check
desired_count = 3replicas: 0 in the file, scaled to 3 at the start stepThe 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 taskrequests and limits of 500m and 1024Mi on the containerRequests equal limits here; revisit them once you see real usage
secrets valueFrom a Secrets Manager secretsecretKeyRef to a Secret web-secrets, key DATABASE_URLYou create the Secret; the value never appears in the draft
environment APP_ENVa ConfigMap web-env with an empty data blockValues are not copied on purpose, because a value can be a secret under any name
target group health check /healthzreadinessProbe httpGet on /healthz, port 8080The 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 3StackToIT derives both from three replicas using the rounding rules of ECS
awslogs log drivernot carriedThe container writes to stdout; you choose where the cluster ships the logs, for example STACKIT Logs
launch_type FARGATE, awsvpc network modenot carriedPods 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.

STACKIT Terraform by StackToITterraform/kubernetes.tf
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
  }]
}
STACKIT Terraform by StackToITterraform/loadbalancing.tf (target pool)
  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"
      }
    }
  }]
Kubernetes draft by StackToITterraform/kubernetes/web/manifests.yaml (Service)
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 listsWhat it meansWhat it costs you
NodePort behind the drafted load balancer (the draft)A NodePort Service and a target pool of the node IPsYou 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 balancerA controller such as ingress-nginx routes by Ingress rules to ClusterIP ServicesYou install and operate the controller; the draft writes neither it nor the Ingress rules
The SKE ALB Ingress extensionEnabled on the cluster through the SKE API; creating an IngressClass provisions a managed Application Load Balancer, and Ingress objects on that class define its routesAs 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.

Application checklistterraform/applications/web.json (readiness)
  "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.