11

I'm trying to create a scheduled task (CloudWatch Events Rule) in my CloudFormation Template that would have the following EcsParameters:

EcsParameters:
        LaunchType: FARGATE
        NetworkConfiguration: 
          AwsVpcConfiguration:
            AssignPublicIp: !Ref PublicIpAssignment
            SecurityGroups:
              - !Ref EcsSecurityGroups
            Subnets:
              - !Ref SubnetName
        TaskCount: 1
        TaskDefinitionArn: !Ref TaskDefinitionOne

My ECS CLuster is launched on Fargate and not EC2, and I do NOT have a service running (use case doesn't need a long running process, directly scheduling tasks from events rules.)

Whenever I run this template (with LaunchType and NetworkConfiguration) the stack creation fails, with this error:

Encountered unsupported property NetworkConfiguration


As an alternative, I also tried launching the scheduled task from AWS CLI, but it seems like the network config and launch type options are not available there either:

Parameter validation failed: Unknown parameter in Targets[0].EcsParameters: "LaunchType", must be one of: TaskDefinitionArn, TaskCount


According to this page on the AWS Documentation itself, I should be able to specify LaunchType and NetworkConfiguration in my EcsParameters section in Targets in Properties of the AWS::Events::Rule resource.

Is there anything I can try that might work?

tanvi
  • 568
  • 2
  • 11
  • 32

3 Answers3

4

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
A developer
  • 290
  • 3
  • 11
  • Since i'm running scheduled tasks, I don't need to manually set up how to run the task. Cloudwatch Rules take care of that part; what i was stuck on is how to get the fargate task definition up on ecs itself in the first place with awsvpc config – tanvi Oct 01 '18 at 16:08
  • 2
    I'm suggesting to use a CloudWatch Events Rule (schedule) which triggers the Lambda function to launch the ECS task. Nothing is manual, and everything remains under CloudFormation control. There is no additional separate cli step needed to add a Fargate event target as in the other proposed answer. I agree with @gcv that it is best to avoid adding/modifying/deleting things outside of CloudFormation that are supposed to remain CF-managed. – chadawagner Oct 01 '18 at 22:18
  • Could you tell me where you've created the task definition? Is that set up manually through the ECS dashboard or did you use a CF template that seets up the task definition? – tanvi Oct 03 '18 at 16:07
  • Also- since you've used a lambda function, have you ever encountered trouble with the lambda function running more than once because of the _at least once_ SLA? Asking because I want to know if this could cause the task to be run more than once sometimes. – tanvi Oct 03 '18 at 16:14
  • My task definition is in another CF template, or you could put it in the same one. There were a couple properties required for Fargate, I'll add to the answer. Re: the lambda function running more than once: yes I take it that can happen, tho I have not noticed it yet. You should ensure that the task being run is idempotent. Idk if this is also an issue for regular EC2 tasks as event targets. I have had issues with them _not_ running e.g. if server resources are not available, but have not noticed them running multiple times. – chadawagner Oct 03 '18 at 16:50
  • Great, thanks for the updated answer. I've never had it not run, that's strange. I have a couple ecs tasks that sometimes run more than once, even without the lambda function (directly scheduled from cloudwatch rules). Simplest solution seems to edit tasks to be idempotent like you mentioned – tanvi Oct 12 '18 at 20:30
  • Thanks for the Answer ! I don't know if I will end up using a lambda, but at least knowing that it's not a fault of mine not being able to re-create what I successfully did manually makes me a little happier. Is anyone aware of any timeline for bringing this to CloudFormation ? – Ing. Luca Stucchi Feb 11 '19 at 10:22
  • They don't usually comment on timelines, but you could keep an eye on this page for the necessary parameters to appear: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-events-rule-ecsparameters.html – chadawagner Feb 12 '19 at 20:11
  • This approach worked for me, with the slight change that I had to set `'assignPublicIp': 'ENABLED'` in the lambda code, since my subnets were public facing. Otherwise I got the `CannotPullContainerError: API error (500)` – Tom Greenwood Jun 10 '19 at 13:51
4

Even though AWS hasn't updated the documentation by today (Jul.15, 2019) it's working as described by the initial poster.

Sebastian Annies
  • 2,438
  • 1
  • 20
  • 38
  • Awesome, you're right! It works even though it is undocumented. Hopefully it eventually makes it to the documentation as is. I did notice that it should indeed be `AwsVpcConfiguration` as the initial poster has it. It should NOT be `AwsvpcConfiguration` (with a lowercased "v"), even though that is how it appears under the NetworkConfiguration of the AWS::ECS::Service (https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-service-networkconfiguration.html). – Joseph Shih Jul 24 '19 at 01:11
2

After a day of research, it looks like AWS still hasn't released support for this though CloudFormation. However, here is an alternative that did work through the aws events put-targets command on the cli.

This method fails for older versions of the cli. run this to update: pip install awscli --upgrade --user This is the version i am on now: aws-cli/1.16.9 Python/2.7.15 Darwin/17.7.0 botocore/1.11.9

Use the aws events put-targets --rule <value> --targets <value> command. Make sure that you have a rule already defined on your cluster. If not, you can do that with the aws events put-rule cmd. Refer to the AWS docs for put-rule, and for put-targets.

An example of a rule from the documentation is given below:

aws events put-rule --name "DailyLambdaFunction" --schedule-expression "cron(0 9 * * ? *)"

The put-targets command that worked for me is this:

aws events put-targets --rule cli-RS-rule --targets '{"Arn": "arn:aws:ecs:1234/cluster/clustername","EcsParameters": {"LaunchType": "FARGATE","NetworkConfiguration": {"awsvpcConfiguration": {"AssignPublicIp": "ENABLED", "SecurityGroups": [ "sg-id1233" ], "Subnets": [ "subnet-1234" ] }},"TaskCount": 1,"TaskDefinitionArn": "arn:aws:ecs:1234:task-definition/taskdef"},"Id": "sampleID111","RoleArn": "arn:aws:iam:1234:role/eventrole"}'
tanvi
  • 568
  • 2
  • 11
  • 32
  • I ran into the same problem. But I think your workaround will mess up update operations on CloudFormation stacks. – gcv Sep 09 '18 at 23:50
  • @gcv I'm relatively new to CloudFormation, can you explain where this might cause trouble with the CF stack? – tanvi Sep 10 '18 at 13:51
  • When you create something in AWS using CloudFormation, modifying it afterwards _from outside CF_ usually messes it up from CF's perspective. It typically doesn't error out, but it stops applying updates you have made from CF on the same object. – gcv Sep 11 '18 at 21:41