AWS ECS Express Mode
Prerequisites
- formae CLI installed on your local machine - see local installation
- AWS CLI v2.34 or later installed and authenticated (
aws ecs create-express-gateway-servicerequires this version)
Set up the database
The agent requires a persistent database. Aurora Serverless v2 with the Data API works well here - ECS can reach it over HTTPS without VPC connectivity.
If you already have an Aurora cluster with the Data API enabled, create a database and skip to configure the agent.
Create a Secrets Manager secret for the database credentials:
aws secretsmanager create-secret \
--name formae-db-creds \
--secret-string '{"username":"postgres","password":"<password>"}'
Create an Aurora Serverless v2 PostgreSQL cluster:
aws rds create-db-cluster \
--db-cluster-identifier formae-db \
--engine aurora-postgresql \
--engine-version 16.4 \
--serverless-v2-scaling-configuration MinCapacity=0.5,MaxCapacity=2 \
--master-username postgres \
--master-user-password <password> \
--enable-http-endpoint
--enable-http-endpoint turns on the Data API. The agent connects over HTTPS - no VPC connectors or NAT gateways needed.
Add a Serverless v2 instance to the cluster:
aws rds create-db-instance \
--db-instance-identifier formae-db-instance \
--db-cluster-identifier formae-db \
--db-instance-class db.serverless \
--engine aurora-postgresql
Wait for both the cluster and the instance to become available before creating the database. The Data API requires a running instance, not just an available cluster:
aws rds wait db-cluster-available --db-cluster-identifier formae-db
aws rds wait db-instance-available --db-instance-identifier formae-db-instance
Then create the database:
CLUSTER_ARN=$(aws rds describe-db-clusters \
--db-cluster-identifier formae-db \
--query "DBClusters[0].DBClusterArn" --output text)
SECRET_ARN=$(aws secretsmanager describe-secret \
--secret-id formae-db-creds \
--query "ARN" --output text)
aws rds-data execute-statement \
--resource-arn $CLUSTER_ARN \
--secret-arn $SECRET_ARN \
--sql "CREATE DATABASE formae"
Configure the agent
The agent config is stored in Secrets Manager and injected as an environment variable at runtime. The start command writes it to a file before launching the agent.
Create a config file (formae.conf.pkl):
amends "formae:/Config.pkl"
agent {
datastore {
datastoreType = "auroradataapi"
auroraDataAPI {
clusterArn = "arn:aws:rds:<region>:<account-id>:cluster:formae-db"
secretArn = "arn:aws:secretsmanager:<region>:<account-id>:secret:formae-db-creds"
database = "formae"
region = "<region>"
}
}
}
Store the config in Secrets Manager:
aws secretsmanager create-secret \
--name formae-config \
--secret-string file://formae.conf.pkl
Create IAM roles
ECS Express Mode needs three roles: a task execution role to pull images and inject secrets, an infrastructure role to manage the load balancer and networking, and a task role for runtime permissions.
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 create-role --role-name ecsInfrastructureRoleForExpressServices \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "ecs.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
aws iam create-role --role-name formae-ecs-task-role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "ecs-tasks.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
Attach the managed policies:
aws iam attach-role-policy --role-name ecsTaskExecutionRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
aws iam attach-role-policy --role-name ecsInfrastructureRoleForExpressServices \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSInfrastructureRoleforExpressGatewayServices
The execution role needs access to the config secret:
aws iam put-role-policy --role-name ecsTaskExecutionRole \
--policy-name formae-secrets-access \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:<region>:<account-id>:secret:formae-config*"
}]
}'
The task role needs Aurora Data API access and permissions for the AWS plugin to manage infrastructure:
aws iam put-role-policy --role-name formae-ecs-task-role \
--policy-name formae-data-api \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"rds-data:ExecuteStatement",
"rds-data:BeginTransaction",
"rds-data:CommitTransaction",
"rds-data:RollbackTransaction"
],
"Resource": "arn:aws:rds:<region>:<account-id>:cluster:formae-db"
},
{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:<region>:<account-id>:secret:formae-db-creds*"
}
]
}'
aws iam attach-role-policy --role-name formae-ecs-task-role \
--policy-arn arn:aws:iam::aws:policy/PowerUserAccess
Deploy the agent
CONFIG_SECRET_ARN=$(aws secretsmanager describe-secret \
--secret-id formae-config \
--query "ARN" --output text)
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
aws ecs create-express-gateway-service \
--service-name formae-agent \
--execution-role-arn arn:aws:iam::$ACCOUNT_ID:role/ecsTaskExecutionRole \
--infrastructure-role-arn arn:aws:iam::$ACCOUNT_ID:role/ecsInfrastructureRoleForExpressServices \
--task-role-arn arn:aws:iam::$ACCOUNT_ID:role/formae-ecs-task-role \
--primary-container "{
\"image\": \"ghcr.io/platform-engineering-labs/formae:latest\",
\"containerPort\": 49684,
\"secrets\": [
{\"name\": \"FORMAE_CONFIG\", \"valueFrom\": \"$CONFIG_SECRET_ARN\"}
],
\"command\": [\"sh\", \"-c\", \"printenv FORMAE_CONFIG > /tmp/formae.conf.pkl && formae agent start --config /tmp/formae.conf.pkl\"]
}" \
--health-check-path "/api/v1/health" \
--cpu 1024 \
--memory 2048
Adjust --cpu and --memory based on the number of resources you plan to manage. See the agent sizing guide for recommendations.
ECS Express Mode auto-provisions the load balancer, TLS certificate, networking, and auto-scaling. The service URL is in the response under activeConfigurations[0].ingressPaths[0].endpoint.
Verify
curl -s -o /dev/null -w "%{http_code}" https://<service-url>/api/v1/health
# 200
Applying formas
The formae CLI runs on your local machine. It evaluates your Pkl files locally and sends the result to the agent API - no files need to be loaded into the container.
Update the CLI's agent URL in ~/.config/formae/formae.conf.pkl:
amends "formae:/Config.pkl"
cli {
api {
url = "https://<service-url>"
port = 443
}
}
Check connectivity:
formae status agent
Then apply a forma:
formae apply --mode reconcile your-forma.pkl
How authentication works
The formae AWS plugin uses the standard AWS credential chain. On ECS, the task role's credentials are available automatically - no env vars, no key files, no token refresh to manage.
See the AWS plugin docs for the full credential chain.
Next steps
Head to the quick start or see the configuration docs for available options.