├── .gitignore ├── package.json ├── README.md └── lib └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "bugs": { 3 | "url": "" 4 | }, 5 | "description": "Run and configure Fargate tasks from within your serverless project", 6 | "devDependencies": {}, 7 | "homepage": "", 8 | "license": "MIT", 9 | "main": "lib/index.js", 10 | "name": "serverless-fargate-tasks", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/svdgraaf/serverless-fargate-tasks.git" 14 | }, 15 | "scripts": { 16 | }, 17 | "version": "0.4.1" 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Serverless Fargate Tasks 2 | ------------------------ 3 | This Serverless plugin will setup a Fargate cluster and setup services and tasks. 4 | With this plugin it's fairly easy to setup a (long running) task which would hook up to kinesis streams or SQS endpoints. 5 | 6 | The (minimal) config is as follows: 7 | 8 | ``` 9 | custom: 10 | fargate: 11 | vpc: 12 | subnets: 13 | - subnet-1234 14 | - subnet-5678 15 | 16 | tasks: 17 | my-task: 18 | image: 123456789369.dkr.ecr.eu-west-1.amazonaws.com/my-image 19 | ``` 20 | 21 | Of course, you can customize to your hearts desire, here are all the available options: 22 | 23 | ``` 24 | custom: 25 | fargate: 26 | # you can put global environment variables here, these will be added 27 | # to all tasks. Optional of course. 28 | environment: 29 | foo: bar 30 | 31 | # you can set the execution role that will be used, this will default to the default 32 | # role for your account 33 | role: arn:aws:iam::123456789369:role/ecsTaskExecutionRole 34 | 35 | vpc: 36 | public-ip: DISABLED # optional, defaults to disabled 37 | subnets: 38 | - subnet-1234 39 | - subnet-5678 40 | security-groups: # optional, defaults to vpc default 41 | - sg-123456678 42 | 43 | tasks: 44 | my-task: 45 | name: ${self:service}-${self:provider.stage}-my-task # default name is be the object key (here 'my-task') 46 | image: 123456789369.dkr.ecr.eu-west-1.amazonaws.com/my-image 47 | environment: # optional 48 | platypus: true 49 | # local variables will always override global ones 50 | foo: wut 51 | # you can also use cloudformation references with eg serverless-pseudo-parameters 52 | myArn: #{MyResource.Arn} 53 | cpu: 512 # optional, defaults to 25% -> 256, see cloudformation docs for valid values 54 | memory: 1GB # optional, defaults to 0.5GB 55 | ``` 56 | 57 | Advanced usage 58 | -------------- 59 | You can override the generated CF resource properties per task with the `override` properties: 60 | 61 | ``` 62 | custom: 63 | fargate: 64 | tasks: 65 | my-task: 66 | image: 123456789369.dkr.ecr.eu-west-1.amazonaws.com/my-image 67 | 68 | # these are all optional 69 | override: 70 | task: 71 | Foo: BAR 72 | container: 73 | Foo: Bar 74 | service: 75 | Foo: BAR 76 | vpc: 77 | Foo: BAR 78 | role: ARN 79 | ``` 80 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class ServerlessFargateTasks { 4 | constructor(serverless, options) { 5 | this.serverless = serverless; 6 | this.service = serverless.service; 7 | this.provider = serverless.getProvider('aws'); 8 | this.options = options || {}; 9 | this.debug = this.options.debug || process.env.SLS_DEBUG; 10 | this.colors = get(this.serverless, 'processedInput.options.color', true); 11 | this.hooks = { 12 | 'package:compileFunctions': this.compileTasks.bind(this) 13 | }; 14 | } 15 | 16 | compileTasks() { 17 | const template = this.serverless.service.provider.compiledCloudFormationTemplate; 18 | const colors = this.colors; 19 | const options = this.serverless.service.custom.fargate; 20 | const debug = this.debug; 21 | const consoleLog = this.serverless.cli.consoleLog; 22 | 23 | if (debug) consoleLog(yellow('Fargate Tasks Plugin')); 24 | 25 | // add the cluster 26 | template['Resources']['FargateTasksCluster'] = { 27 | "Type" : "AWS::ECS::Cluster", 28 | } 29 | 30 | // Create a loggroup for the logs 31 | template['Resources']['FargateTasksLogGroup'] = { 32 | "Type" : "AWS::Logs::LogGroup", 33 | } 34 | 35 | // for each defined task, we create a service and a task, and point it to 36 | // the created cluster 37 | Object.keys(options.tasks).forEach(identifier => { 38 | if (debug) consoleLog(yellow('Processing ' + identifier)); 39 | // consoleLog(options.tasks[identifier]); 40 | 41 | // get all override values, if they exists 42 | var override = options.tasks[identifier]['override'] || {} 43 | var container_override = override['container'] || {} 44 | var task_override = override['task'] || {} 45 | var service_override = override['service'] || {} 46 | var network_override = override['network'] || {} 47 | 48 | var name = options.tasks[identifier]['name'] || identifier 49 | var normalizedIdentifier = this.provider.naming.normalizeNameToAlphaNumericOnly(identifier) 50 | 51 | // consoleLog(override); 52 | if (!override.hasOwnProperty('role')) { 53 | // check if the default role can be assumed by ecs, if not, make it so 54 | if(template.Resources.IamRoleLambdaExecution.Properties.AssumeRolePolicyDocument.Statement[0].Principal.Service.indexOf('ecs-tasks.amazonaws.com') == -1) { 55 | template.Resources.IamRoleLambdaExecution.Properties.AssumeRolePolicyDocument.Statement[0].Principal.Service.push('ecs-tasks.amazonaws.com') 56 | 57 | // check if there already is a ManagedPolicyArns array, if not, create it 58 | if(!template.Resources.IamRoleLambdaExecution.Properties.hasOwnProperty('ManagedPolicyArns')) { 59 | template.Resources.IamRoleLambdaExecution.Properties['ManagedPolicyArns'] = []; 60 | } 61 | template.Resources.IamRoleLambdaExecution.Properties['ManagedPolicyArns'].push('arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy') 62 | } 63 | } 64 | 65 | // create a key/value list for the task environment 66 | let environment = [] 67 | if(options.tasks[identifier].hasOwnProperty('environment')) { 68 | 69 | // when a global environment is set, we need to extend it 70 | var target_environment = options['environment'] || {} 71 | target_environment = Object.assign(target_environment, options.tasks[identifier].environment) 72 | 73 | Object.keys(target_environment).forEach(function(key,index) { 74 | let value = target_environment[key]; 75 | environment.push({"Name": key, "Value": value}) 76 | }) 77 | } 78 | 79 | // create the container definition 80 | var definitions = Object.assign({ 81 | 'Name': name, 82 | 'Image': options.tasks[identifier]['image'], 83 | 'Environment': environment, 84 | 'LogConfiguration': { 85 | 'LogDriver': 'awslogs', 86 | 'Options': { 87 | 'awslogs-region':{"Fn::Sub": "${AWS::Region}"}, 88 | 'awslogs-group': {"Fn::Sub": "${FargateTasksLogGroup}"}, 89 | 'awslogs-stream-prefix': 'fargate' 90 | }, 91 | }, 92 | }, container_override) 93 | 94 | // create the task definition 95 | var task = { 96 | 'Type': 'AWS::ECS::TaskDefinition', 97 | 'Properties': Object.assign({ 98 | 'ContainerDefinitions': [definitions], 99 | 'Family': name, 100 | 'NetworkMode': 'awsvpc', 101 | 'ExecutionRoleArn': options['role'] || {"Fn::Sub": 'arn:aws:iam::${AWS::AccountId}:role/ecsTaskExecutionRole'}, 102 | 'TaskRoleArn': override['role'] || {"Fn::Sub": '${IamRoleLambdaExecution}'}, 103 | 'RequiresCompatibilities': ['FARGATE'], 104 | 'Memory': options.tasks[identifier]['memory'] || "0.5GB", 105 | 'Cpu': options.tasks[identifier]['cpu'] || 256, 106 | }, task_override) 107 | } 108 | template['Resources'][normalizedIdentifier + 'Task'] = task 109 | 110 | let desired = options.tasks[identifier]['desired'] 111 | 112 | // create the service definition 113 | var service = { 114 | 'Type': 'AWS::ECS::Service', 115 | 'Properties': Object.assign({ 116 | 'Cluster': {"Fn::Sub": '${FargateTasksCluster}'}, 117 | 'LaunchType': 'FARGATE', 118 | 'ServiceName': name, 119 | 'DesiredCount': desired == undefined ? 1 : desired, 120 | 'TaskDefinition': {"Fn::Sub": '${' + normalizedIdentifier + 'Task}'}, 121 | 'NetworkConfiguration': { 122 | 'AwsvpcConfiguration': Object.assign({ 123 | 'AssignPublicIp': options.vpc['public-ip'] || "DISABLED", 124 | 'SecurityGroups': options.vpc['security-groups'] || [], 125 | 'Subnets': options.vpc['subnets'] || [], 126 | }, network_override), 127 | } 128 | }, service_override) 129 | } 130 | template['Resources'][normalizedIdentifier + 'Service'] = service 131 | }); 132 | 133 | function yellow(str) { 134 | if (colors) return '\u001B[33m' + str + '\u001B[39m'; 135 | return str; 136 | } 137 | 138 | } 139 | } 140 | 141 | function get(obj, path, def) { 142 | return path.split('.').filter(Boolean).every(step => !(step && (obj = obj[step]) === undefined)) ? obj : def; 143 | } 144 | 145 | module.exports = ServerlessFargateTasks; 146 | --------------------------------------------------------------------------------