Documentation

AWS ECS Fargate setup

Create a Fargate cluster, roles, CodeBuild, and load balancer in your account, then attach an IAM access key with aws-ecs-fargate permissions.

Attach an AWS access key under Organization or Project Cloud accounts. Agents, apps, and MCP servers all run on aws-ecs-fargate. Saving the key only proves STS sign-in. Create the resources below in the same account, then paste the key of an IAM user that has the policy at the end.

Replace YOUR_ACCOUNT_ID and YOUR_REGION everywhere. One cluster is enough for agents, apps, and MCP. Use a second cluster only if you want them isolated.

What you are creating

PieceAWS serviceWhy
ClusterECS Fargate (+ optional Fargate Spot)Runs the containers. Not created automatically on first deploy
BuildS3 + CodeBuild (privileged, so Docker can run)Source zip is uploaded, image is built and pushed to ECR
Image registryECRRepos can be created at deploy time if the key is allowed to CreateRepository
NetworkVPC, two public subnets, security groupsFargate tasks and the load balancer need them
HTTP front doorApplication Load Balancer (optional CloudFront in front)Apps and MCP get a hostname; deploys add target groups and listener rules
RolesExecution, task, infra, CodeBuildECS and CodeBuild assume these. Your access key only PassRole-s them

1. Create the ECS cluster

aws ecs create-cluster \
  --cluster-name fargate-cluster \
  --capacity-providers FARGATE FARGATE_SPOT \
  --default-capacity-provider-strategy \
    capacityProvider=FARGATE_SPOT,weight=4 \
    capacityProvider=FARGATE,weight=1

2. Create the four IAM roles

RoleTrusted servicePurpose
ecsTaskExecutionRoleecs-tasks.amazonaws.comPull images from ECR and write container logs
ecsTaskRoleecs-tasks.amazonaws.comWhat the running container can do in AWS (start with logs only)
ecsInfraRoleecs.amazonaws.comAttach the service to the load balancer and manage ENIs
codebuild-service-rolecodebuild.amazonaws.comRead the source bucket and push the image to ECR

Execution role:

aws iam create-role --role-name ecsTaskExecutionRole \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": { "Service": "ecs-tasks.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }]
  }'

aws iam attach-role-policy --role-name ecsTaskExecutionRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

Task role:

aws iam create-role --role-name ecsTaskRole \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": { "Service": "ecs-tasks.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }]
  }'

aws iam put-role-policy --role-name ecsTaskRole --policy-name TaskLogs --policy-document '{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["logs:CreateLogStream", "logs:PutLogEvents"],
    "Resource": "arn:aws:logs:*:*:log-group:/ecs/*"
  }]
}'

Infra role:

aws iam create-role --role-name ecsInfraRole \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": { "Service": "ecs.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }]
  }'

aws iam put-role-policy --role-name ecsInfraRole --policy-name AlbAndNetwork --policy-document '{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": [
      "ec2:DescribeSubnets",
      "ec2:DescribeSecurityGroups",
      "ec2:DescribeVpcs",
      "ec2:CreateNetworkInterface",
      "ec2:DeleteNetworkInterface",
      "ec2:DescribeNetworkInterfaces",
      "elasticloadbalancing:*",
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ],
    "Resource": "*"
  }]
}'

CodeBuild role (change the bucket name to match step 3):

aws iam create-role --role-name codebuild-service-role \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": { "Service": "codebuild.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }]
  }'

aws iam put-role-policy --role-name codebuild-service-role --policy-name CodeBuildDeploy --policy-document '{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
      "Resource": "arn:aws:logs:*:*:log-group:/aws/codebuild/image-builder*"
    },
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:GetObjectVersion", "s3:PutObject"],
      "Resource": "arn:aws:s3:::YOUR_ACCOUNT_ID-build-source/*"
    },
    {
      "Effect": "Allow",
      "Action": "ecr:GetAuthorizationToken",
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "ecr:PutImage",
        "ecr:InitiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload",
        "ecr:CreateRepository",
        "ecr:DescribeRepositories"
      ],
      "Resource": "arn:aws:ecr:*:*:repository/*"
    }
  ]
}'

3. Create the source bucket and CodeBuild project

Privileged mode must be on so the build can run Docker. BUILD_GENERAL1_SMALL is enough.

aws s3 mb s3://YOUR_ACCOUNT_ID-build-source --region YOUR_REGION

aws codebuild create-project --name image-builder \
  --source type=S3,location=YOUR_ACCOUNT_ID-build-source/ \
  --artifacts type=NO_ARTIFACTS \
  --environment type=LINUX_CONTAINER,image=aws/codebuild/standard:7.0,computeType=BUILD_GENERAL1_SMALL,privilegedMode=true \
  --service-role arn:aws:iam::YOUR_ACCOUNT_ID:role/codebuild-service-role

4. Network, security groups, and load balancer

Uses the account default VPC. The ALB needs two subnets in different availability zones. Keep the task security group closed except from the ALB. After this runs, save the cluster ARN, subnet IDs, task security group, ALB ARN, listener ARN, and the three ECS role ARNs in advanced deploy settings.

# Default VPC + two subnets in different AZs (required for an ALB)
VPC=$(aws ec2 describe-vpcs --filters Name=isDefault,Values=true --query "Vpcs[0].VpcId" --output text)
SUBNETS=$(aws ec2 describe-subnets --filters Name=vpc-id,Values=$VPC --query "Subnets[0:2].SubnetId" --output text)
SUBNET_A=$(echo $SUBNETS | awk '{print $1}')
SUBNET_B=$(echo $SUBNETS | awk '{print $2}')

ALB_SG=$(aws ec2 create-security-group --group-name alb-sg --description "ALB inbound 80/443" --vpc-id $VPC --query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id $ALB_SG --protocol tcp --port 80 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id $ALB_SG --protocol tcp --port 443 --cidr 0.0.0.0/0

TASK_SG=$(aws ec2 create-security-group --group-name task-sg --description "Tasks from ALB only" --vpc-id $VPC --query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id $TASK_SG --protocol tcp --port 80 --source-group $ALB_SG

ALB_ARN=$(aws elbv2 create-load-balancer --name deploy-alb --subnets $SUBNET_A $SUBNET_B --security-groups $ALB_SG --scheme internet-facing --type application --query "LoadBalancers[0].LoadBalancerArn" --output text)

aws elbv2 create-listener --load-balancer-arn $ALB_ARN --protocol HTTP --port 80 \
  --default-actions Type=fixed-response,FixedResponseConfig='{StatusCode=404,ContentType=text/plain,MessageBody=not found}'

Optional HTTPS: create a CloudFront distribution with the ALB as origin, HTTP only to the origin, viewer redirect to HTTPS, caching disabled, and all HTTP methods allowed. You can skip CloudFront and use the ALB DNS on port 80.

5. IAM on the access key you paste

Create an IAM user, attach this policy, then create an access key. Same policy covers agents, apps, and MCP. Replace YOUR_ACCOUNT_ID, YOUR_REGION, and the bucket / project names if you changed them.

AreaActionsUsed for
ECRGetAuthorizationToken, CreateRepository, push image, TagResourceCreate the repo if missing and push
S3Put/Get/Delete object, ListBucketUpload the build zip
CodeBuildStartBuild, BatchGetBuilds, StopBuildBuild the image
ECSTask definitions, services, tasks, List/Describe clustersRun Fargate
IAMPassRole on the three ECS rolesECS can assume execution, task, and infra
Logs + CloudWatchLog read/write; GetMetricData / ListMetricsContainer logs and console metrics
EC2Describe network resourcesPlace tasks and resolve public IP
ELBCreate/modify/delete target groups and listener rules; Describe*Hostname routing for apps, MCP, and agents when an ALB is used
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EcrAuth",
      "Effect": "Allow",
      "Action": "ecr:GetAuthorizationToken",
      "Resource": "*"
    },
    {
      "Sid": "EcrRepos",
      "Effect": "Allow",
      "Action": [
        "ecr:DescribeRepositories",
        "ecr:CreateRepository",
        "ecr:ListImages",
        "ecr:BatchCheckLayerAvailability",
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "ecr:PutImage",
        "ecr:InitiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload",
        "ecr:TagResource"
      ],
      "Resource": "arn:aws:ecr:YOUR_REGION:YOUR_ACCOUNT_ID:repository/*"
    },
    {
      "Sid": "S3",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:GetObjectVersion",
        "s3:DeleteObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::YOUR_ACCOUNT_ID-build-source",
        "arn:aws:s3:::YOUR_ACCOUNT_ID-build-source/*"
      ]
    },
    {
      "Sid": "CodeBuild",
      "Effect": "Allow",
      "Action": [
        "codebuild:StartBuild",
        "codebuild:BatchGetBuilds",
        "codebuild:StopBuild"
      ],
      "Resource": "arn:aws:codebuild:YOUR_REGION:YOUR_ACCOUNT_ID:project/image-builder"
    },
    {
      "Sid": "Logs",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents",
        "logs:GetLogEvents",
        "logs:FilterLogEvents",
        "logs:DescribeLogStreams"
      ],
      "Resource": "arn:aws:logs:YOUR_REGION:YOUR_ACCOUNT_ID:log-group:*"
    },
    {
      "Sid": "CloudWatchMetrics",
      "Effect": "Allow",
      "Action": [
        "cloudwatch:GetMetricData",
        "cloudwatch:GetMetricStatistics",
        "cloudwatch:ListMetrics"
      ],
      "Resource": "*"
    },
    {
      "Sid": "Ecs",
      "Effect": "Allow",
      "Action": [
        "ecs:RegisterTaskDefinition",
        "ecs:DeregisterTaskDefinition",
        "ecs:DescribeTaskDefinition",
        "ecs:CreateService",
        "ecs:UpdateService",
        "ecs:DeleteService",
        "ecs:DescribeServices",
        "ecs:ListClusters",
        "ecs:DescribeClusters",
        "ecs:ListTasks",
        "ecs:DescribeTasks",
        "ecs:StopTask",
        "ecs:RunTask",
        "ecs:TagResource"
      ],
      "Resource": "*"
    },
    {
      "Sid": "Ec2Describe",
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeNetworkInterfaces",
        "ec2:DescribeSubnets",
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeVpcs",
        "ec2:DescribeRouteTables"
      ],
      "Resource": "*"
    },
    {
      "Sid": "PassEcsRoles",
      "Effect": "Allow",
      "Action": "iam:PassRole",
      "Resource": [
        "arn:aws:iam::YOUR_ACCOUNT_ID:role/ecsTaskExecutionRole",
        "arn:aws:iam::YOUR_ACCOUNT_ID:role/ecsTaskRole",
        "arn:aws:iam::YOUR_ACCOUNT_ID:role/ecsInfraRole"
      ]
    },
    {
      "Sid": "Alb",
      "Effect": "Allow",
      "Action": [
        "elasticloadbalancing:CreateTargetGroup",
        "elasticloadbalancing:DeleteTargetGroup",
        "elasticloadbalancing:ModifyTargetGroup",
        "elasticloadbalancing:ModifyTargetGroupAttributes",
        "elasticloadbalancing:RegisterTargets",
        "elasticloadbalancing:DeregisterTargets",
        "elasticloadbalancing:AddTags",
        "elasticloadbalancing:CreateRule",
        "elasticloadbalancing:DeleteRule",
        "elasticloadbalancing:ModifyRule",
        "elasticloadbalancing:DescribeLoadBalancers",
        "elasticloadbalancing:DescribeListeners",
        "elasticloadbalancing:DescribeTargetGroups",
        "elasticloadbalancing:DescribeRules",
        "elasticloadbalancing:DescribeTargetHealth"
      ],
      "Resource": "*"
    }
  ]
}

6. Before you paste the keys

CheckWhy
aws sts get-caller-identity is the account that owns the clusterDeploys always use this account
Cluster, roles, CodeBuild, S3, subnets, and ALB existOnly the ECR repository is created automatically if IAM allows it
PassRole includes ecsInfraRoleLoad balancer attachment fails without it
Verified is greenOnly means STS succeeded, not that IAM or the cluster is ready
aws sts get-caller-identity