When to choose ECS
ECS is a strong fit when:- Your operations team already runs other workloads on AWS and you don’t have a Kubernetes platform in the same region.
- You want the smallest possible operational footprint — Fargate eliminates the node-management burden entirely.
- You need tight IAM and VPC integration without bridging EKS IRSA, AWS Load Balancer Controller, or external-secrets.io.
- TLS termination at the load balancer is acceptable (ACM-issued certs on the ALB).
- You need workload portability across clouds (use Kubernetes or OpenShift instead).
- You require custom scheduler behaviour, CRDs, or operators (use EKS or OpenShift).
- Your release pipeline is already Helm-/Kustomize-based and you don’t want to rewrite it for
aws ecs update-service.
Fargate vs. EC2 launch type
Images run unprivileged with a read-only root filesystem and writable
/tmp. Both launch types satisfy those requirements without modification.
Prerequisites
- AWS account with permissions to create ECS clusters, IAM roles, ALBs, target groups, and (optionally) RDS instances.
- VPC with at least two private subnets in distinct Availability Zones for the tasks, and two public subnets for the ALB. The default VPC works for evaluation; production deployments should use a dedicated VPC.
- Route 53 hosted zone (or another DNS provider) for the public hostname that points to the ALB.
- ACM certificate in the same region as the ALB, covering the public hostname.
- Container registry credentials — your private container registry requires an API token; create a Secret in AWS Secrets Manager with the docker-config JSON (see below) and reference it from the task execution role.
- RDS PostgreSQL instance in the same VPC (Single-AZ for evaluation; Multi-AZ for production). Encryption at rest enabled; auto-minor-version upgrade enabled.
- AWS CLI v2 with credentials that can call
ecs,iam,logs,secretsmanager, andelbv2.
Architecture at a glance
Step 1 — IAM roles
ECS uses two IAM roles per task: the task execution role (used by ECS to pull the image and write logs) and the task role (used by your application code to call AWS APIs).Task execution role
Attach the AWS-managed policyAmazonECSTaskExecutionRolePolicy and a custom policy granting access to the registry credentials and any Secrets Manager / SSM parameters referenced in the task definition:
Task role
For Aletyx Enterprise Build of Kogito and Drools workloads that do not integrate with AWS services beyond Secrets Manager (the common case), the task role can stay empty — the application reads its secrets via the task definition’ssecrets block, which is evaluated by the agent under the execution role. Add permissions to the task role only when the application code itself calls AWS APIs (uncommon).
Step 2 — Registry credentials
Your private container registry requires an API token. Create a Secrets Manager secret in the docker-config JSON format expected by ECS:repositoryCredentials.credentialsParameter. ECS rotates the in-flight pull on the next task launch, so token rotation is a no-downtime operation as long as the new value is in place before the next deployment.
Step 3 — Application secrets
Create one Secrets Manager secret per sensitive value (database password, OIDC client secret, etc.). Per-secret entries keep the IAM scope small and let you rotate one value at a time:String parameters or hard-code the values in the task definition environment block — both are equivalent at runtime.
Step 4 — Task definition
The task definition below assumes a Decision Control workload. The same shape applies to Decision Control Tower and the Tower Sidecar — swap the image, port, and env block accordingly.networkMode: awsvpc— required for Fargate; recommended for EC2. Each task gets its own ENI, which simplifies security groups and gives the task an IP that the ALB can target directly (iptarget type).requiresCompatibilities: ["FARGATE"]— for EC2 launch type, swap to["EC2"]and removecpu/memoryfrom the task level (move them to the container definition’scpu/memoryReservation).readonlyRootFilesystem: truewith atmpfsmount at/tmp— matches the hardening posture documented in the Kubernetes deployment guide.user: "1001:1001"— runs as a non-root UID baked into the image.healthCheck— both the container health check (here) and the ALB target group health check (next step) are valuable; the container check restarts the task locally, the ALB check reroutes traffic.
EC2 launch type variant
For EC2, replace the launch type and provide instance-side resource hints:Step 5 — ALB + target group
The Application Load Balancer terminates TLS, routes by host header, and integrates with the ECS service via a target group that uses theip target type (each task ENI is registered directly).
30s / 2 healthy) is recommended so a failed deployment is caught quickly. The path /actuator/health is the same endpoint used by the container health check and is unauthenticated by design.
Step 6 — ECS service
ECS runs a rolling update — new tasks are started, the ALB waits for them to be healthy, then old tasks drain. For blue-green deployments, switch to CODE_DEPLOY and define a CodeDeploy application + deployment group.
Auto Scaling
Decision Control is mostly request-bound, so target tracking against requests-per-target or CPU works well:Step 7 — Security groups
Two security groups are typically enough:- ALB SG: ingress
443/tcpfrom0.0.0.0/0(public hostname). - Task SG: ingress
8080/tcpfrom the ALB SG only; egress to RDS port5432/tcpand HTTPS443/tcp(for the OIDC provider, image pulls, and Secrets Manager).
5432/tcp from the Task SG only. Never expose RDS publicly.
OIDC redirect URI
The OIDC provider’s registered redirect URI must exactly match${APP_HOST}/auth/callback. With the ALB terminating TLS, APP_HOST=https://<your-public-hostname>. ECS sets X-Forwarded-Proto automatically; Aletyx Enterprise Build of Kogito and Drools respects it when FORWARD_HEADERS_STRATEGY=framework is set (the default in 1.6.0+).
Common pitfall: the registered redirect URI in Entra / Keycloak still pointing at the old ALB DNS name from a previous test environment. Update the registration when promoting to a new ALB / hostname.
Logging and observability
Theawslogs driver in the task definition streams stdout/stderr to a CloudWatch Logs group (/<your-namespace-prefix>/<your-app-name> in the example). Set a retention policy on the log group to control cost:
/actuator/prometheus endpoint are available. Wire the latter via a CloudWatch agent sidecar or an OTel collector if you already operate one — Aletyx Enterprise Build of Kogito and Drools workloads don’t ship a metrics-export sidecar by default.
Database connectivity
RDS in the same VPC is the production-recommended path. The task accesses the RDS endpoint via the private route. Useiam database authentication if your operating model already standardises on IAM credentials; otherwise the password-in-Secrets-Manager pattern shown above is the path of least resistance.
For evaluation only, RDS Single-AZ + Burstable instance is sufficient. Production should run Multi-AZ + Provisioned IOPS or gp3 storage.
Operational tasks
Deploying a new image
Rotating a secret
Update the secret value in Secrets Manager. Existing tasks continue to hold the old value in memory — they pick up the new one on the next start. Force a rotation byaws ecs update-service --force-new-deployment, which restarts tasks without changing the task definition.
Scaling manually
Troubleshooting
Task stops immediately with ResourceInitializationError
The execution role can’t fetch the registry credentials or one of the Secrets Manager values. Check CloudTrail for GetSecretValue denials and confirm the role has access to every secret referenced in the task definition.
Task stops with Essential container exited after a few seconds
Usually a startup failure inside the app. The first place to look is CloudWatch Logs at /<your-namespace-prefix>/<your-app-name>/<stream> — kubectl logs --previous equivalent. The two common causes are a stale schema (HIBERNATE_DDL_AUTO=validate against a DB without the latest migrations) and an Unable to acquire JDBC Connection from a misconfigured SPRING_DATASOURCE_URL or a Security Group that denies port 5432.
ALB target stuck unhealthy
If the task is Running but the ALB target is unhealthy:
- Confirm the target group port matches the container port (8080 by default).
- Confirm the security group on the task allows ingress from the ALB SG on port 8080.
- Curl the health endpoint from a bastion or another task in the same SG:
curl -v http://<task-private-ip>:8080/actuator/health.
health-check-grace-period-seconds to come healthy will be terminated and replaced — increase the grace period to 90–120 seconds for cold JVM starts.
OIDC redirect_uri_mismatch
The OIDC provider’s registered redirect URI does not exactly equal ${APP_HOST}/auth/callback. Common causes: missing /auth/callback path, http:// vs. https://, or a stale ALB DNS name still registered with the provider.