AWS ECSNext release
Introduction
The AWS ECS provider sets the desired count of ECS services to zero when their session expires, and back up when a request needs them. ECS then stops or starts the tasks. Sablier manages the services of one cluster and calls the ECS API with the credentials of the AWS SDK.
Install Sablier
Run Sablier as an ECS service in the cluster it manages. Create the cluster with a default Service Connect namespace, then deploy the CloudFormation template:
aws ecs create-cluster --cluster-name sablier --service-connect-defaults namespace=sablier
aws cloudformation deploy --stack-name sablier --template-file sablier.yaml \
--capabilities CAPABILITY_IAM \
--parameter-overrides Subnets=subnet-aaaa,subnet-bbbb SecurityGroup=sg-ccccThe template runs Sablier on Fargate. Its task definition selects the ECS provider and the cluster:
Command:
- start
- --provider.name=ecs
- !Sub --provider.ecs.cluster=${Cluster}
- !Sub --provider.ecs.region=${AWS::Region}Sablier gets its credentials from the task role. The role’s IAM policy allows the four ECS API actions Sablier calls:
TaskRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: sablier
PolicyDocument:
Version: "2012-10-17"
Statement:
# Check the cluster on startup.
- Effect: Allow
Action: ecs:DescribeClusters
Resource: !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/${Cluster}
# ListServices has no resource type: scope it with a condition.
- Effect: Allow
Action: ecs:ListServices
Resource: "*"
Condition:
ArnEquals:
ecs:cluster: !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/${Cluster}
# Read the services and their tags.
- Effect: Allow
Action: ecs:DescribeServices
Resource: !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:service/${Cluster}/*
# Change the desired count of opted-in services only.
- Effect: Allow
Action: ecs:UpdateService
Resource: !Sub arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:service/${Cluster}/*
Condition:
StringEquals:
aws:ResourceTag/sablier.enable: "true"ecs:ListServicesneeds"Resource": "*". The action has no resource type, so scope it to the cluster with theecs:clustercondition key.ecs:UpdateServicecan require thesablier.enabletag. Theaws:ResourceTagcondition stops Sablier from scaling services that did not opt in.
Reverse proxy plugins reach Sablier at http://sablier:10000. The service registers that name in the Service Connect namespace, so run your reverse proxy as an ECS service in the same namespace:
# Reverse proxies in the namespace reach Sablier at http://sablier:10000.
ServiceConnectConfiguration:
Enabled: true
Services:
- PortName: http
DiscoveryName: sablier
ClientAliases:
- DnsName: sablier
Port: 10000Run Sablier outside ECS
Sablier reads credentials the way every AWS SDK does, and uses the first source it finds:
- The
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYenvironment variables. - A web identity token, such as EKS IAM roles for service accounts.
- The shared
configandcredentialsfiles, withAWS_PROFILE. - The container credentials of an ECS task role or EKS Pod Identity.
- The instance role of an EC2 instance.
Grant those credentials the policy above. Set the region with --provider.ecs.region or AWS_REGION. Sablier does not read it from the EC2 instance metadata.
Usage example
Opt an ECS service in with the sablier.enable tag, and give it a group:
# An ECS service Sablier scales to zero when idle and back up on demand.
# Reverse proxies in the Service Connect namespace reach it at http://whoami.
AWSTemplateFormatVersion: "2010-09-09"
Description: whoami, a service managed by Sablier
Parameters:
Cluster:
Type: String
Default: sablier
Subnets:
Type: List<AWS::EC2::Subnet::Id>
SecurityGroup:
Type: AWS::EC2::SecurityGroup::Id
Description: Must allow inbound TCP 80 from your reverse proxy
Resources:
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: whoami
RequiresCompatibilities: [FARGATE]
NetworkMode: awsvpc
Cpu: "256"
Memory: "512"
ContainerDefinitions:
- Name: whoami
Image: sablierapp/mimic:v0.3.3
Command: ["-healthy=true"]
Essential: true
PortMappings:
- Name: http
ContainerPort: 80
AppProtocol: http
HealthCheck:
Command: ["CMD", "/mimic", "healthcheck"]
Interval: 5
Timeout: 2
Retries: 3
Service:
Type: AWS::ECS::Service
Properties:
ServiceName: whoami
Cluster: !Ref Cluster
TaskDefinition: !Ref TaskDefinition
DesiredCount: 0
LaunchType: FARGATE
NetworkConfiguration:
AwsvpcConfiguration:
Subnets: !Ref Subnets
SecurityGroups: [!Ref SecurityGroup]
AssignPublicIp: ENABLED
ServiceConnectConfiguration:
Enabled: true
Services:
- PortName: http
DiscoveryName: whoami
ClientAliases:
- DnsName: whoami
Port: 80
Tags:
- Key: sablier.enable
Value: "true"
- Key: sablier.group
Value: whoamiaws cloudformation deploy --stack-name whoami --template-file whoami.yaml \
--parameter-overrides Subnets=subnet-aaaa,subnet-bbbb SecurityGroup=sg-ccccThen configure your reverse proxy to call Sablier for the whoami group in front of http://whoami. On the first request, Sablier sets the desired count to 1 and holds the request, or shows a waiting page, until the task is running. When the session expires, Sablier sets it back to 0.
Provider reference
Configuration
| Option | Default | Description |
|---|---|---|
--provider.ecs.cluster | default | The name or ARN of the cluster that holds the services. Sablier fails to start if the cluster is missing or not ACTIVE. |
--provider.ecs.region | The AWS region of the cluster. When empty, Sablier reads AWS_REGION or the shared config profile. | |
--provider.ecs.endpoint | Replaces the ECS API endpoint URL, for example to target an emulator such as LocalStack. |
Every --provider.* option that is not provider-specific also applies; see the CLI reference.
Workloads
Sablier scales ECS services that use the REPLICA scheduling strategy. It calls UpdateService to change the desired count:
- Stop sets the desired count to
0, or toidle.replicaswhen set. - Start sets it to
active.replicaswhen set, or1.
ECS then starts or stops tasks from the service’s task definition. Sablier never changes the task definition.
Instance names
A reverse proxy can target one service by name instead of a group. The whoami service of the usage example is named:
whoamiThe instance name is the service name, without the cluster. Service names are only unique within a cluster, so one Sablier manages one cluster. A request can also name the service by its ARN: it resolves to the service name and shares its session.
Prefer groups: a name ties your proxy configuration to the service.
Labels
On ECS, Sablier settings are resource tags on the service. The keys and values are those of the label reference:
Service:
Type: AWS::ECS::Service
Properties:
ServiceName: team-api
Tags:
- Key: sablier.enable
Value: "true"
- Key: sablier.group
Value: team-a team-b # space separated: tag values cannot contain commas- Tag the service. Sablier reads the tags of the service only, not those of its task definition or tasks.
- Separate list values with spaces. Tag values can’t contain commas, so lists of groups and weekdays are space-separated.
- Keys and values are case-sensitive.
sablier.enablemust be exactlytrue.
To tag a service you did not create with CloudFormation, use the AWS CLI:
aws ecs tag-resource --resource-arn arn:aws:ecs:eu-west-1:123456789012:service/sablier/whoami \
--tags key=sablier.enable,value=true key=sablier.group,value=whoamiWorkload settings
Each excerpt below is the service of a complete template in examples/ecs.
EnableNext release
The sablier.enable tag set to true opts the service in. It is required, and any other value is ignored. See the usage example.
GroupNext release
sablier.group puts the service in one or more groups. A session for any of them starts it. Without it, the service is in the default group.
Separate the groups with spaces, because a tag value can’t contain a comma:
Service:
Type: AWS::ECS::Service
Properties:
ServiceName: team-api
Tags:
- Key: sablier.enable
Value: "true"
- Key: sablier.group
Value: team-a team-b # space separated: tag values cannot contain commasReady afterNext release
sablier.ready-after keeps reporting the service as not ready for a settling delay after its tasks first reach the RUNNING state. Use it when your application needs time to serve traffic after its task starts.
Service:
Type: AWS::ECS::Service
Properties:
ServiceName: slow-starter
Tags:
- Key: sablier.enable
Value: "true"
- Key: sablier.group
Value: slow-starter
- Key: sablier.ready-after
Value: 30sLearn more about delaying readiness.
Ready on startNext release
sablier.ready-on-start set to true reports the service ready as soon as Sablier raises its desired count, without waiting for its tasks.
Service:
Type: AWS::ECS::Service
Properties:
ServiceName: fast-starter
Tags:
- Key: sablier.enable
Value: "true"
- Key: sablier.group
Value: fast-starter
- Key: sablier.ready-on-start
Value: "true"Learn more about marking ready on start.
Running hoursNext release
sablier.running-hourskeeps the service running during a daily window.sablier.running-dayslimits that window to some weekdays. Separate the days with spaces.
Service:
Type: AWS::ECS::Service
Properties:
ServiceName: office-app
Tags:
- Key: sablier.enable
Value: "true"
- Key: sablier.group
Value: office-app
- Key: sablier.running-hours
Value: "09:00-18:00"
- Key: sablier.running-days
Value: Mon Tue Wed Thu Fri # space separatedThe window uses Sablier’s local time. Set the time zone with the TZ environment variable of Sablier’s container:
# sablier.running-hours windows use Sablier's local time.
Environment:
- Name: TZ
Value: Europe/ParisLearn more about running hours.
Anti-affinityNext release
sablier.anti-affinity lists groups the service backs off from: Sablier keeps it idle while any of them has an active session. Separate the groups with spaces.
Service:
Type: AWS::ECS::Service
Properties:
ServiceName: batch-worker
Tags:
- Key: sablier.enable
Value: "true"
- Key: sablier.group
Value: batch
- Key: sablier.anti-affinity
Value: streaming gaming # space separatedLearn more about anti-affinity.
Scale instead of stopNext release
Keep the service running when idle instead of scaling it to zero:
sablier.idle.replicasset to1or more keeps that many tasks when idle.sablier.active.replicassets the desired count on the next session.
Service:
Type: AWS::ECS::Service
Properties:
ServiceName: warm-api
Tags:
- Key: sablier.enable
Value: "true"
- Key: sablier.group
Value: warm-api
- Key: sablier.idle.replicas
Value: "1"
- Key: sablier.active.replicas
Value: "3"On ECS, only the task count changes. The CPU and memory of a task belong to its task definition, so Sablier ignores the CPU and memory tags and logs a warning.
Learn more about scaling instead of stopping.
Readiness
Sablier reads the service and its PRIMARY deployment, and reports it:
- stopped when its desired count is
0, even while tasks are still stopping. - error when the rollout state of the primary deployment is
FAILED. Sablier shows the reason ECS gives. - starting while fewer tasks are running than desired, or while the primary deployment is
IN_PROGRESS. - ready otherwise.
ECS counts a task as running once it reaches the RUNNING state. If your application needs more time after that, add sablier.ready-after.
A rollout fails when the deployment circuit breaker is on and the service can’t reach a steady state. Sablier reports the error until a new deployment replaces the failed one.
Limitations
- One Sablier manages one cluster. Run one Sablier per cluster.
- Sablier ignores services that use the
DAEMONscheduling strategy: they have no desired count to scale. - CPU, memory and block I/O throttling are not possible on ECS: they belong to the task definition.
- ECS publishes service changes to EventBridge only. Sablier polls the cluster every 10 seconds to detect services started, stopped, tagged or removed outside Sablier.
Examples
examples/ecs: every template on this page, with instructions to deploy them.