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
| Piece | AWS service | Why |
|---|---|---|
| Cluster | ECS Fargate (+ optional Fargate Spot) | Runs the containers. Not created automatically on first deploy |
| Build | S3 + CodeBuild (privileged, so Docker can run) | Source zip is uploaded, image is built and pushed to ECR |
| Image registry | ECR | Repos can be created at deploy time if the key is allowed to CreateRepository |
| Network | VPC, two public subnets, security groups | Fargate tasks and the load balancer need them |
| HTTP front door | Application Load Balancer (optional CloudFront in front) | Apps and MCP get a hostname; deploys add target groups and listener rules |
| Roles | Execution, task, infra, CodeBuild | ECS 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=12. Create the four IAM roles
| Role | Trusted service | Purpose |
|---|---|---|
| ecsTaskExecutionRole | ecs-tasks.amazonaws.com | Pull images from ECR and write container logs |
| ecsTaskRole | ecs-tasks.amazonaws.com | What the running container can do in AWS (start with logs only) |
| ecsInfraRole | ecs.amazonaws.com | Attach the service to the load balancer and manage ENIs |
| codebuild-service-role | codebuild.amazonaws.com | Read 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/AmazonECSTaskExecutionRolePolicyTask 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-role4. 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.
| Area | Actions | Used for |
|---|---|---|
| ECR | GetAuthorizationToken, CreateRepository, push image, TagResource | Create the repo if missing and push |
| S3 | Put/Get/Delete object, ListBucket | Upload the build zip |
| CodeBuild | StartBuild, BatchGetBuilds, StopBuild | Build the image |
| ECS | Task definitions, services, tasks, List/Describe clusters | Run Fargate |
| IAM | PassRole on the three ECS roles | ECS can assume execution, task, and infra |
| Logs + CloudWatch | Log read/write; GetMetricData / ListMetrics | Container logs and console metrics |
| EC2 | Describe network resources | Place tasks and resolve public IP |
| ELB | Create/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
| Check | Why |
|---|---|
| aws sts get-caller-identity is the account that owns the cluster | Deploys always use this account |
| Cluster, roles, CodeBuild, S3, subnets, and ALB exist | Only the ECR repository is created automatically if IAM allows it |
| PassRole includes ecsInfraRole | Load balancer attachment fails without it |
| Verified is green | Only means STS succeeded, not that IAM or the cluster is ready |
aws sts get-caller-identity