Serverless Agent Pool on AWS Fargate
Deploy a serverless agent pool using Amazon API Gateway, AWS Lambda, and Amazon ECS on AWS Fargate. API Gateway receives the webhook from Scalr. Lambda requests a Fargate task, and the task runs one Scalr Agent.
This guide walks through a manual setup in the AWS and Scalr consoles. For automated provisioning with Terraform or OpenTofu, use the AWS configuration in terraform-scalr-serverless.
Prerequisites
- Permission to create and manage Scalr agent pools and assign a pool to a workspace.
- An AWS account with permission to manage IAM roles and policies, Secrets Manager secrets, CloudWatch log groups, ECS resources, Lambda functions, and API Gateway REST APIs.
- A VPC and subnets for the Fargate tasks, or permission to create them. Tasks need outbound access to Scalr, Docker Hub, AWS services, and the services used by your OpenTofu or Terraform configuration.
- Credentials for the infrastructure managed by your runs. Configure them through provider configurations or your existing workspace configuration.
Use one AWS Region for the resources in this guide. Replace placeholders in braces, such as {region} and {account_id}, with your values. Keep the resource names shown below, or update every reference when changing a name.
Step 1: Create the pool and store its token
- In Scalr, open Agent Pools at the account scope and create a pool with the Runs agent type. Leave Enable serverless execution off until the webhook URL is available.
- On the pool's Tokens tab, choose Generate token and copy the token.
- In AWS Secrets Manager, choose Store a new secret, then Other type of secret.
- Switch to the Plaintext tab, delete its contents, and paste only the token. Do not add a key on the Key/value tab.
- On the next page, name the secret
scalr/api/tokenand store it. - Copy the complete secret ARN from the secret details page, including its generated suffix. Use this value for
{secret_arn}throughout the guide.
On the stored secret, the Key/value tab shows "The secret value can't be converted to key name and value pairs." This is expected for a plaintext secret.
For example, a complete ARN looks like this:
arn:aws:secretsmanager:{region}:{account_id}:secret:scalr/api/token-AbCdEfThe task definition below injects the entire secret as
SCALR_AGENT_TOKEN. Store the token as plaintext. A key-value secret such as{"token":"..."}would inject a JSON object and cause authentication to fail. For an existing JSON secret, see Token authentication fails.
Step 2: Prepare networking and logs
Choose the task subnets
Use subnets in one VPC. Choose public or private subnets based on how tasks reach the internet:
| Subnet type | Required route | Lambda setting in Step 5 |
|---|---|---|
| Public | A route to an internet gateway. Each task needs a public IP address. | ECS_ASSIGN_PUBLIC_IP=ENABLED |
| Private | A route through a NAT gateway for outbound internet access. | ECS_ASSIGN_PUBLIC_IP=DISABLED |
You can provide multiple subnets in different Availability Zones. Record their IDs for Step 5. See Fargate networking for other network configurations.
Create a security group named scalr-ecs-sg in the same VPC. No inbound rules are needed for the agent. Allow outbound traffic to the required endpoints, AWS Secrets Manager, and CloudWatch Logs. Record the security group ID.
The Lambda function in this guide uses AWS service APIs and does not need a VPC attachment. The subnet and security group settings apply to the Fargate tasks it launches.
Create the log group
In CloudWatch, create the log group /ecs/scalr-agent-pool-cluster and choose a retention period. ECS will send the agent's logs to this group.
Step 3: Configure the Fargate task
Create the task execution role
ECS uses a task execution role to retrieve the secret and send container logs to CloudWatch. The container does not need a separate task role to receive a secret through its environment. See ECS task execution roles.
- In IAM, create a role named
ScalrECSExecutionRole. Choose the AWS service use case Elastic Container Service Task, which trustsecs-tasks.amazonaws.com. - Attach the AWS managed policy
AmazonECSTaskExecutionRolePolicy. - Add an inline policy named
ScalrAgentTokenAccesswith the following JSON. Replace{secret_arn}with the complete ARN from Step 1.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "{secret_arn}"
}
]
}If the secret uses a customer managed KMS key, also grant this role kms:Decrypt on that key and allow the role in the key policy.
If your OpenTofu or Terraform configuration uses credentials from an ECS task role, add that role separately as taskRoleArn in the task definition and allow Lambda to pass it in Step 4.
Create the cluster and task definition
- In ECS, create a cluster named
scalr-agent-pool-clusterfor Fargate tasks. - Under Task definitions, choose Create new task definition with JSON.
- Paste the JSON below and replace the placeholders. Use the complete secret ARN for
valueFrom, without a wildcard. - Create the task definition and copy its full ARN, including the revision number, for Step 5.
{
"family": "scalr-agent-run",
"executionRoleArn": "arn:aws:iam::{account_id}:role/ScalrECSExecutionRole",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "2048",
"memory": "4096",
"containerDefinitions": [
{
"name": "scalr-agent-run",
"image": "scalr/agent:latest",
"essential": true,
"environment": [
{
"name": "SCALR_AGENT_SINGLE",
"value": "true"
},
{
"name": "SCALR_AGENT_LOG_FORMAT",
"value": "json"
}
],
"secrets": [
{
"name": "SCALR_AGENT_TOKEN",
"valueFrom": "{secret_arn}"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/scalr-agent-pool-cluster",
"awslogs-region": "{region}",
"awslogs-stream-prefix": "ecs"
}
},
"stopTimeout": 120
}
]
}This example runs the scalr/agent image with 2 vCPUs and 4 GiB of memory per agent container. To choose when agents receive updates, replace latest with a tested agent version. If your runs need additional tools, use a custom agent image.
Lambda will launch standalone ECS tasks. You do not need an ECS service with a desired task count.
Step 4: Create the Lambda execution role
Lambda needs permission to run the task definition in your cluster and pass its execution role to ECS.
- In IAM, create a role named
ScalrLambdaExecutionRolewith the AWS service use case Lambda. - Attach the AWS managed policy
AWSLambdaBasicExecutionRolefor Lambda logs. - Add an inline policy named
ScalrLambdaExecutionPolicywith the following JSON. Replace{region}and{account_id}.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "ecs:RunTask",
"Resource": "arn:aws:ecs:{region}:{account_id}:task-definition/scalr-agent-run:*",
"Condition": {
"ArnEquals": {
"ecs:cluster": "arn:aws:ecs:{region}:{account_id}:cluster/scalr-agent-pool-cluster"
}
}
},
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::{account_id}:role/ScalrECSExecutionRole",
"Condition": {
"StringEquals": {
"iam:PassedToService": "ecs-tasks.amazonaws.com"
}
}
}
]
}If you added a task role in Step 3, include its ARN in the iam:PassRole resources as well.
Step 5: Create the Lambda handler
- In Lambda, create a function named
ScalrServerlesswith the Python 3.13 runtime. - Select Use an existing role and choose
ScalrLambdaExecutionRole. - Set the function timeout to 15 seconds.
- Add the following environment variables under Configuration > Environment variables.
| Variable | Value |
|---|---|
ECS_CLUSTER | scalr-agent-pool-cluster |
ECS_TASK_DEFINITION | The full task definition ARN from Step 3, including its revision. |
ECS_SUBNETS | Subnet IDs from Step 2, separated by commas. |
ECS_SECURITY_GROUPS | The security group ID from Step 2. Separate multiple IDs with commas. |
ECS_ASSIGN_PUBLIC_IP | ENABLED for public subnets, or DISABLED for private subnets with NAT. |
Replace the contents of lambda_function.py with the following code and choose Deploy. Keep the handler set to lambda_function.lambda_handler.
import hashlib
import json
import logging
import os
import boto3
from botocore.config import Config
from botocore.exceptions import BotoCoreError, ClientError
logger = logging.getLogger()
logger.setLevel(logging.INFO)
ecs = boto3.client(
"ecs",
config=Config(
connect_timeout=2,
read_timeout=5,
retries={"total_max_attempts": 1},
),
)
def response(status_code, body):
return {
"statusCode": status_code,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(body),
}
def lambda_handler(event, context):
headers = {
name.lower(): value
for name, value in (event.get("headers") or {}).items()
}
request_key = headers.get("idempotency-key")
if not request_key:
return response(400, {"error": "Idempotency-Key header is required"})
client_token = hashlib.sha256(request_key.encode()).hexdigest()
try:
result = ecs.run_task(
cluster=os.environ["ECS_CLUSTER"],
taskDefinition=os.environ["ECS_TASK_DEFINITION"],
launchType="FARGATE",
count=1,
clientToken=client_token,
networkConfiguration={
"awsvpcConfiguration": {
"subnets": [
subnet.strip() for subnet in os.environ["ECS_SUBNETS"].split(",")
],
"securityGroups": [
group.strip() for group in os.environ["ECS_SECURITY_GROUPS"].split(",")
],
"assignPublicIp": os.environ["ECS_ASSIGN_PUBLIC_IP"],
}
},
)
except (BotoCoreError, ClientError):
logger.exception("ECS RunTask request failed")
return response(502, {"error": "ECS RunTask request failed"})
if result.get("failures") or not result.get("tasks"):
logger.error("ECS could not launch the task: %s", result.get("failures"))
return response(502, {"error": "ECS could not launch the task"})
task_arn = result["tasks"][0]["taskArn"]
logger.info("ECS task %s requested for webhook %s", task_arn, request_key)
return response(202, {"taskArn": task_arn})The handler uses Scalr's Idempotency-Key to derive an ECS clientToken. After ECS accepts a launch, retries with the same key and launch parameters return the original task instead of creating another one, within the ECS idempotency window. Keep the task definition revision and other launch settings stable while requests are being retried.
The handler returns after ECS accepts the request. It does not wait for the container to start or for the agent to execute a task. See webhook response and retry behavior.
Step 6: Create the webhook API
Use an API Gateway REST API for this example. Its API key and usage plan configuration is not supported by HTTP APIs. See REST and HTTP API differences.
API Gateway keys identify usage-plan clients. AWS does not treat them as authentication or authorization. For production access control, configure an authorization mechanism such as a Lambda authorizer that validates a separate credential sent in a Scalr custom header. See AWS guidance on API keys.
Create the route
- In API Gateway, create a REST API named
scalr-serverless-apiwith a Regional endpoint. - Create a resource with the path
/trigger. - Add a
POSTmethod to/triggerwith a Lambda integration targetingScalrServerless. Enable Lambda proxy integration so the function receives request headers. - Grant API Gateway permission to invoke the Lambda function when prompted.
- In the method request settings, enable API key required and save.
Deploy the API
Deploy the API to a stage named prod. Copy the stage's invoke URL and append /trigger to form the full webhook URL:
https://{api_id}.execute-api.{region}.amazonaws.com/prod/triggerUse this full URL in Scalr and in test requests. Redeploy the API to prod after changing its method settings.
Add the API key and usage plan
- Under API keys, create an enabled key named
scalr-webhook-keyand let API Gateway generate its value. Copy the key value for Step 7. - Under Usage plans, create a plan named
scalr-webhook-plan. When setting throttling and quotas, allow for up to 3 webhook requests per agent task. - Associate the API's
prodstage with the usage plan. - Add
scalr-webhook-keyto the plan.
Allow a few minutes for the association to take effect. For the detailed AWS procedure, see Set up API keys.
Step 7: Enable serverless execution in Scalr
- Open the agent pool created in Step 1, go to Settings, and turn on Enable serverless execution.
- Set API Gateway URL to the full URL from Step 6, ending in
/prod/trigger. - Under Headers, choose Add header. Enter
x-api-keyas the name and the API key value as the value. If you configured a Lambda authorizer, add the separate header and credential it requires. - Choose Save changes.
The agent pool token stays in Secrets Manager. The API key is a separate value used by API Gateway.
Step 8: Verify the deployment
Test the webhook
This request tests whether the webhook can launch a Fargate task. An agent that cannot acquire its task exits after the acquisition timeout.
In a shell, set the values below and send a test request. If your endpoint uses an authorizer, include its required header as well.
WEBHOOK_URL='https://{api_id}.execute-api.{region}.amazonaws.com/prod/trigger'
WEBHOOK_API_KEY='{api_key}'
REQUEST_ID="$(uuidgen)"
curl --silent --show-error --fail-with-body --request POST "$WEBHOOK_URL" \
--header 'Content-Type: application/json' \
--header "x-api-key: $WEBHOOK_API_KEY" \
--header "Idempotency-Key: $REQUEST_ID" \
--data '{}'A successful response has HTTP status 202 and a body containing the ECS task ARN:
{
"taskArn": "arn:aws:ecs:{region}:{account_id}:task/scalr-agent-pool-cluster/{task_id}"
}Repeat the curl command with the same REQUEST_ID. Within the ECS idempotency window, it should return the same task ARN. Use a new request ID only when you intend to launch another task.
Verify a Scalr run
- On the agent pool, choose Link to workspaces and select a workspace, or assign the pool another way. Start a run in that workspace.
- In ECS, open the
scalr-agent-pool-clustercluster and check that a task starts. - In CloudWatch, open
/ecs/scalr-agent-pool-cluster. Look forConnected to agent pool, followed by task log entries withtask_idandtask_name. - In Scalr, confirm that the run stage completes. In ECS, confirm that its container stops.
- If the run needs approval, approve it when ready. The apply stage should start another agent container.
For log fields and the relationship between tasks and containers, see Agent lifecycle and Find the task a container runs.
Troubleshooting
Lambda logs are in /aws/lambda/ScalrServerless. Agent logs are in /ecs/scalr-agent-pool-cluster. For a task that stops before agent logs appear, inspect the stopped reason in ECS.
The webhook request fails
The pool's Webhook deliveries tab in Scalr lists each delivery with its HTTP status.
| Symptom | What to check |
|---|---|
Forbidden | Send the API key value in x-api-key. Verify that the key is enabled and associated with the usage plan and prod stage. Check that the URL includes the stage, /prod/trigger, not only /trigger. If you configured a Lambda authorizer, include its required credential header as well. |
Missing Authentication Token | Use POST and the full URL ending in /prod/trigger. Verify that the route is deployed to prod. |
Idempotency-Key header is required | Include the header in manual tests. Check that Lambda proxy integration is enabled. Scalr sends this header automatically. |
502 or Internal Server Error | Check Lambda logs for missing environment variables, invalid launch parameters, or denied ecs:RunTask or iam:PassRole permissions. |
ECS could not launch the task in Lambda logs | Read the reason in the logged failures list to identify why ECS rejected the launch. |
ConflictException in Lambda logs | A request reused a client token with different launch parameters. Restore the original parameters for the retry. |
The container does not start or connect
Scalr does not resend a webhook that returned 2xx. After you fix the cause, cancel the affected run and start a new one.
| Symptom | What to check |
|---|---|
ResourceInitializationError | Inspect the ECS stopped reason. Check the full secret ARN, the task execution role's Secrets Manager permissions, the log group, and network access to AWS services. |
CannotPullContainerError | Check the image name, Docker Hub access and rate limits, subnet routes, and public IP or NAT configuration. ECR permissions do not grant access to Docker Hub. |
| Agent cannot reach Scalr | Check that the task's security group allows outbound HTTPS and that its subnet has the internet gateway or NAT route from Step 2. If you set SCALR_URL, verify that it matches your Scalr account URL. |
| Agent exits before acquiring its task | Check whether the run was canceled or duplicate webhook requests launched more than one container for the task. See task acquisition timeout. |
Token authentication fails
If the agent log shows Unable to connect to Scalr platform with Status 401: Unauthorized, verify that the secret contains a valid token for this agent pool. With the task definition shown above, the secret must contain only the raw token. If the Key/value tab of the secret shows a key and value, the secret is JSON.
To keep a JSON secret, set valueFrom to the complete secret ARN followed by :{key}::, where {key} is the JSON key that holds the token:
arn:aws:secretsmanager:{region}:{account_id}:secret:scalr/api/token-AbCdEf:{key}::Selecting a JSON key requires Linux Fargate platform version 1.4.0 or later. Register a new task definition revision and update ECS_TASK_DEFINITION in Lambda. See Secrets Manager values in ECS environment variables.
If you change only the secret value, newly launched tasks read the updated value. Existing containers keep the value injected at startup.
Updated about 4 hours ago
