feat: switch orchestrator from Fargate to EC2 Spot

Dispatcher now launches Spot instances instead of Fargate tasks:
- t3.small for go/node builds ($0.005/hr)
- t3.medium for docker/godot builds ($0.01/hr)
- t3.micro for deploy jobs ($0.004/hr)

Instances self-terminate via user-data trap on exit.
Cancel: ec2:TerminateInstances instead of ecs:StopTask.
Cleanup cron also sweeps orphan instances by tinqs-ci tag.

Pre-baked AMI with act_runner + tools = instant boot, no install.
This commit is contained in:
2026-05-22 19:24:33 +01:00
parent 537235c17b
commit 5f676dfb6b
3 changed files with 231 additions and 203 deletions
+22 -32
View File
@@ -1,6 +1,6 @@
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Tinqs CI Orchestrator — webhook dispatch, Fargate routing, cancel support
Description: Tinqs CI Orchestrator — Lambda dispatch + EC2 Spot runners
Parameters:
GiteaURL:
@@ -9,24 +9,18 @@ Parameters:
GiteaToken:
Type: String
NoEcho: true
ECSCluster:
Type: String
Default: tinqs-git
Subnets:
Type: CommaDelimitedList
Description: VPC subnet IDs for Fargate tasks
RunnerAMI:
Type: AWS::EC2::Image::Id
Description: Pre-baked AMI with Go, Node, Docker, AWS CLI, act_runner
Subnet:
Type: AWS::EC2::Subnet::Id
Description: Public subnet for spot instances
SecurityGroup:
Type: AWS::EC2::SecurityGroup::Id
Description: Security group for spot instances
InstanceProfileArn:
Type: String
Description: Security group for Fargate tasks
ECRBase:
Type: String
Default: 149751500842.dkr.ecr.eu-west-1.amazonaws.com
TaskRoleArn:
Type: String
Default: arn:aws:iam::149751500842:role/tinqs-git-task
ExecRoleArn:
Type: String
Default: arn:aws:iam::149751500842:role/ecsTaskExecutionRole
Description: IAM instance profile ARN for runners
Globals:
Function:
@@ -71,7 +65,7 @@ Resources:
FunctionName: tinqs-ci-dispatch
Handler: bootstrap
CodeUri: ../dispatch/
Description: Receives webhook, routes to Fargate or Lambda by runs-on label
Description: Receives webhook, starts Spot instances or invokes Lambda executor
Timeout: 60
MemorySize: 256
Environment:
@@ -79,13 +73,11 @@ Resources:
GITEA_URL: !Ref GiteaURL
GITEA_TOKEN: !Ref GiteaToken
EXECUTOR_FUNCTION_NAME: !Ref ExecFunction
ECS_CLUSTER: !Ref ECSCluster
SUBNETS: !Join [",", !Ref Subnets]
RUNNER_AMI: !Ref RunnerAMI
SUBNET: !Ref Subnet
SECURITY_GROUP: !Ref SecurityGroup
ECR_BASE: !Ref ECRBase
DDB_TABLE: !Ref RunsTable
TASK_ROLE_ARN: !Ref TaskRoleArn
EXEC_ROLE_ARN: !Ref ExecRoleArn
INSTANCE_PROFILE: !Ref InstanceProfileArn
Policies:
- LambdaInvokePolicy:
FunctionName: !Ref ExecFunction
@@ -95,16 +87,14 @@ Resources:
Statement:
- Effect: Allow
Action:
- ecs:RunTask
- ecs:StopTask
- ecs:RegisterTaskDefinition
- ecs:DescribeTasks
- ec2:RunInstances
- ec2:TerminateInstances
- ec2:DescribeInstances
- ec2:CreateTags
Resource: '*'
- Effect: Allow
Action: iam:PassRole
Resource:
- !Ref TaskRoleArn
- !Ref ExecRoleArn
Resource: !Ref InstanceProfileArn
Events:
Webhook:
Type: Api
@@ -172,12 +162,12 @@ Resources:
CILogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /ecs/tinqs-ci
LogGroupName: /tinqs/ci
RetentionInDays: 14
Outputs:
WebhookURL:
Description: Configure this as Gitea system webhook
Description: Configure as Gitea system webhook
Value: !Sub 'https://${WebhookApi}.execute-api.${AWS::Region}.amazonaws.com/prod/webhook'
DispatchArn:
Value: !GetAtt DispatchFunction.Arn