CloudFormation has not yet caught up with the parameters needed to run a Fargate task as the direct target of a CloudWatch Events Rule. In the meantime, you can achieve the same result by having the rule target a Lambda function which runs the Fargate task.
For this to work the Events Rule will need lambda:InvokeFunction
permission on the Lambda function, and the Lambda function will need the ecs:RunTask
and iam:PassRole
permission on the appropriate resources (in addition to the usual logs permissions in AWSLambdaBasicExecutionRole).
Edit: Here is an example CF template that shows what I'm talking about. (It's pieced together and simplified from what we're using, so not tested, but hopefully illustrates the process.)
Parameters:
#ClusterName
#Subnets
#SecurityGroups
#CronExpression
#TaskDefinitionArn
#TaskRoleArn
#ExecutionRoleArn
Resources:
FargateLauncherRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${AWS::StackName}-FargateLauncher-${AWS::Region}
AssumeRolePolicyDocument:
Statement:
-
Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Path: /
FargateLauncherPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: !Sub ${AWS::StackName}-FargateLauncher-${AWS::Region}
PolicyDocument:
Version: 2012-10-17
Statement:
-
Sid: RunTaskAccess
Effect: Allow
Action:
- ecs:RunTask
Resource: '*'
-
Sid: PassRoleAccess
Effect: Allow
Action:
- iam:PassRole
Resource:
# whatever you have defined in your TaskDefinition, if any
- !Ref TaskRoleArn
- !Ref ExecutionRoleArn
Roles:
- !Ref FargateLauncherRole
FargateLauncher:
Type: AWS::Lambda::Function
DependsOn: FargateLauncherPolicy
Properties:
Environment:
Variables:
CLUSTER_NAME: !Ref ClusterName
SUBNETS: !Ref Subnets
SECURITY_GROUPS: !Ref SecurityGroups
Handler: index.handler
Role: !GetAtt FargateLauncherRole.Arn
Runtime: python3.6
Code:
ZipFile: |
from os import getenv
from boto3 import client
ecs = client('ecs')
def handler(event, context):
ecs.run_task(
cluster=getenv('CLUSTER_NAME'),
launchType='FARGATE',
taskDefinition=event.get('taskDefinition'),
count=1,
platformVersion='LATEST',
networkConfiguration={'awsvpcConfiguration': {
'subnets': getenv('SUBNETS').split(','),
'securityGroups': getenv('SECURITY_GROUPS').split(','),
'assignPublicIp': 'DISABLED'
}})
Schedule:
Type: AWS::Events::Rule
Properties:
ScheduleExpression: !Sub "cron(${CronExpression})"
State: ENABLED
Targets:
-
Id: fargate-launcher
Arn: !GetAtt FargateLauncher.Arn
Input: !Sub |
{
"taskDefinition": "${TaskDefinitionArn}"
}
InvokePermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref FargateLauncher
Action: lambda:InvokeFunction
Principal: events.amazonaws.com
SourceArn: !GetAtt Schedule.Arn
I define the Lambda function in my cluster stack, where I already have ClusterName
, Subnets
, and SecurityGroups
parameters, and can pass them directly to the Lambda environment. The schedule and invoke permission can then be defined in one or many separate stacks, passing in the TaskDefinition
for each task via the input to the Lambda function. That way you can have one Lambda per cluster but use as many different tasks as needed. You can also add a custom command string and/or other container overrides to the Lambda input which can be passed on via the overrides
param of run_task
.
Edit #2:
Here's an example Fargate TaskDefinition that can go in a CF template:
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Ref Family
Cpu: !Ref Cpu
Memory: !Ref Memory
NetworkMode: awsvpc
ExecutionRoleArn: !Ref ExecutionRoleArn
TaskRoleArn: !Ref TaskRoleArn
RequiresCompatibilities:
- FARGATE
ContainerDefinitions:
- Name: !Ref ContainerName
Essential: true
Image: !Ref Image
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: !Ref LogPrefix