11
votes

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?

3

3 Answers

4
votes

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
3
votes

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

2
votes

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"}'