├── Dockerfile ├── Gemfile ├── LICENSE ├── README.md ├── cron-entrypoint.sh ├── cron-stack-example.yml ├── crontab-example ├── run-task.rb ├── run-task.sh └── scheduler-service.yml /Dockerfile: -------------------------------------------------------------------------------- 1 | # tag as: scheduler 2 | FROM ruby:2.3 3 | 4 | MAINTAINER Hossam Hammady 5 | 6 | # install cron & curl 7 | RUN apt-get update && \ 8 | apt-get install -y \ 9 | cron rsyslog && \ 10 | apt-get clean && \ 11 | rm -rf /var/lib/apt/lists/* 12 | 13 | WORKDIR /root 14 | COPY Gemfile /root/Gemfile 15 | RUN bundle install 16 | 17 | RUN echo 'cron.* /var/log/cron.log' >> /etc/rsyslog.conf 18 | 19 | COPY run-task.sh /usr/bin/run-task 20 | COPY run-task.rb /root/run-task.rb 21 | COPY cron-entrypoint.sh /usr/bin/cron-entrypoint.sh 22 | ENTRYPOINT ["cron-entrypoint.sh"] 23 | 24 | CMD ["cron", "-f"] 25 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'docker-api' 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Rayyan QCRI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swarm-scheduler 2 | 3 | Many solutions out there explain how to run cron tasks using docker. 4 | However, none of them does that in a distributed manner and make use 5 | of the swarm mode orchestration. 6 | With swarm-scheduler, you can do that with additional benefits. 7 | 8 | Each time a task is run, it is scheduled on an arbitrary node. 9 | Moreover, resource reservations and/or limitations can be applied on tasks 10 | according to the needs. Tasks also run inside any container image, let it 11 | be ruby, python, php, ...etc or even an application specific image. 12 | No new syntax to learn, just use the [docker compose](https://docs.docker.com/compose/compose-file) file syntax. 13 | 14 | Here are the steps required to achieve this: 15 | 16 | ### 1. Deploy the cron stack 17 | 18 | First thing to do is to write a docker stack yaml file describing 19 | your cron tasks as explained in the introduction. 20 | An example file is located here with the name `cron-services-example.yml`. 21 | You can include any configuration as long as you abide with the following 22 | restrictions: 23 | 24 | 1. Set `command` to run the task command. 25 | 2. `replicas` should be set to 0, otherwise the task will run 26 | as soon as the service is deployed for the number of times you specified. 27 | 3. Set `restart_policy.condition` to `none`. If you set to `on-failure`, 28 | the task will restart automatically on failure. If set to `any` (default), 29 | it will continuously restart. Both cases are typically not needed in cron tasks. 30 | 31 | Once ready, deploy your cron stack on the swarm cluster: 32 | 33 | docker stack deploy -c cron-stack-example.yml cron 34 | 35 | ### 2. Deploy the crontab schedule 36 | 37 | A standard crontab file is used to schedule the tasks. 38 | The only requirement is to set the cron task command to launch the corresponding service. 39 | For example if you defined a service called `service1` in the cron stack, 40 | and you want to run it once every minute, your crontab file should look like: 41 | 42 | * * * * * root run-task cron_service1 43 | 44 | Once ready, deploy your crontab file as a docker config: 45 | 46 | docker config create crontab.v1 crontab-example 47 | 48 | ### 3. Deploy the scheduler 49 | 50 | With everything in place, it is time to deploy the scheduler itself 51 | and start the action: 52 | 53 | docker service create --name scheduler_manager \ 54 | --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \ 55 | --env DOCKER_URL=unix:///var/run/docker.sock \ 56 | --config source=crontab.v1,target=/crontab \ 57 | --constraint node.role==manager \ 58 | rayyanqcri/swarm-scheduler 59 | 60 | Alternatively, clone this repo then deploy the stack `scheduler-service.yml`: 61 | 62 | docker stack deploy -c scheduler-service.yml scheduler 63 | 64 | ### Watch the service logs 65 | 66 | One of the benefits of running cron tasks as docker services is that 67 | you can see the output of the tasks using docker logs. With our 68 | example, and the service name is service1: 69 | 70 | docker service logs cron_service1 71 | 72 | ### Updating the crontab or the cron stack 73 | 74 | If you need to modify your cron tasks defined in the cron stack above, 75 | use `docker service update ...` to update the existing cron services and 76 | `docker service create ...` to add new cron services. 77 | 78 | Alternatively, just remove the cron stack and add it again with the modified file: 79 | 80 | docker stack rm cron 81 | docker stack deploy -c modified-cron-stack.yml cron 82 | 83 | If what you need is just to change the cron schedule, create a newer version 84 | for the docker config. 85 | 86 | docker config create crontab.v2 crontab-example-modified && \ 87 | docker service update \ 88 | --config-add source=crontab.v2,target=crontab \ 89 | --config-rm crontab.v1 \ 90 | scheduler_manager && \ 91 | docker config rm crontab.v1 92 | -------------------------------------------------------------------------------- /cron-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cat /crontab >> /etc/crontab 4 | crontab /etc/crontab 5 | crontab -l 6 | 7 | # cron does not read env, save it here 8 | env > /root/env 9 | 10 | rsyslogd 11 | 12 | exec "$@" 13 | -------------------------------------------------------------------------------- /cron-stack-example.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | service1: 4 | image: busybox:latest 5 | command: date 6 | deploy: 7 | replicas: 0 8 | restart_policy: 9 | condition: none 10 | 11 | # docker service create --name bbls --restart-condition none --replicas 0 busybox date 12 | -------------------------------------------------------------------------------- /crontab-example: -------------------------------------------------------------------------------- 1 | * * * * * root run-task cron_service1 2 | -------------------------------------------------------------------------------- /run-task.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | service_name = ARGV[0] 4 | unless service_name 5 | $stderr.puts "USAGE: ./run-task.rb " 6 | exit 1 7 | end 8 | 9 | require "docker" 10 | 11 | class Docker::Service 12 | include Docker::Base 13 | 14 | def self.all(opts = {}, conn = Docker.connection) 15 | hashes = Docker::Util.parse_json(conn.get('/services', opts)) || [] 16 | hashes.map { |hash| new(conn, hash) } 17 | end 18 | 19 | def version 20 | self.info["Version"]["Index"] 21 | end 22 | 23 | def update(opts) 24 | connection.post("/services/#{self.id}/update", {version: version}, body: opts.to_json) 25 | end 26 | 27 | def scale(replicas) 28 | spec = self.info["Spec"] 29 | spec["Mode"]["Replicated"]["Replicas"] = replicas 30 | # by setting ForceUpdate to version, we are sure 31 | # it is changed every time, effectively forcing an update 32 | spec["TaskTemplate"]["ForceUpdate"] = version 33 | update(spec) 34 | end 35 | 36 | private_class_method :new 37 | end 38 | 39 | # do what the following command is doing: 40 | # docker service update --force --replicas 1 service1 41 | # because we don't have docker client installed 42 | Docker.validate_version! 43 | 44 | Docker::Service.all(filters: {name: [service_name]}.to_json)[0].scale 1 45 | puts "OK" 46 | -------------------------------------------------------------------------------- /run-task.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # import env vars that were written in entrypoint 4 | env - `cat /root/env` bundle exec /root/run-task.rb $1 5 | -------------------------------------------------------------------------------- /scheduler-service.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | manager: 4 | image: rayyanqcri/swarm-scheduler 5 | volumes: 6 | - /var/run/docker.sock:/var/run/docker.sock 7 | environment: 8 | - DOCKER_URL=unix:///var/run/docker.sock 9 | configs: 10 | - source: crontab.v1 11 | target: crontab 12 | deploy: 13 | placement: 14 | constraints: 15 | - node.role == manager 16 | configs: 17 | crontab.v1: 18 | external: true 19 | 20 | --------------------------------------------------------------------------------