AWS Elastic Beanstalk,运行cronjob

时间:2012-12-28 23:30:23

标签: amazon-web-services cron crontab elastic-beanstalk

我想知道是否有办法设置每分钟执行的cronjob /任务。目前,我的任何实例都应该能够运行此任务。

这是我在配置文件中尝试做的事情没有成功:

container_commands:
  01cronjobs:
    command: echo "*/1 * * * * root php /etc/httpd/myscript.php"

我不确定这是否是正确的方法

有什么想法吗?

18 个答案:

答案 0 :(得分:87)

这就是我向Elastic Beanstalk添加一个cron作业的方法:

在应用程序的根目录下创建一个名为.ebextensions的文件夹(如果该文件夹尚不存在)。然后在.ebextensions文件夹中创建一个配置文件。我将使用example.config进行说明。然后将其添加到example.config

container_commands:
  01_some_cron_job:
    command: "cat .ebextensions/some_cron_job.txt > /etc/cron.d/some_cron_job && chmod 644 /etc/cron.d/some_cron_job"
    leader_only: true

这是Elastic Beanstalk的YAML配置文件。确保将其复制到文本编辑器中时,文本编辑器使用空格而不是制表符。否则,当您将其推送到EB时,您将收到YAML错误。

这样做的是创建一个名为01_some_cron_job的命令。命令按字母顺序运行,因此01确保它作为第一个命令运行。

然后该命令获取名为some_cron_job.txt的文件的内容,并将其添加到/etc/cron.d中名为some_cron_job的文件中。

然后该命令更改/etc/cron.d/some_cron_job文件的权限。

leader_only键确保命令仅在被视为领导者的ec2实例上运行。而不是在每个ec2实例上运行,而不是在运行。

然后在.ebextensions文件夹中创建一个名为some_cron_job.txt的文件。您将把cron作业放在这个文件中。

例如:

# The newline at the end of this file is extremely important.  Cron won't run without it.
* * * * * root /usr/bin/php some-php-script-here > /dev/null

因此,这个cron作业将作为root用户以每天每小时的每分钟运行,并将输出丢弃到/ dev / null。 / usr / bin / php是php的路径。然后用你的php文件的路径替换some-php-script-here。这显然是假设您的cron作业需要运行PHP文件。

另外,请确保some_cron_job.txt文件在文件末尾有换行符,就像评论所说的那样。否则cron将无法运行。

<强>更新 当Elastic Beanstalk扩展您的实例时,此解决方案存在问题。例如,假设您有一个运行cron作业的实例。您的流量会增加,因此Elastic Beanstalk会将您扩展到两个实例。 leader_only将确保您只在两个实例之间运行一个cron作业。您的流量减少,Elastic Beanstalk将您缩小到一个实例。但是,Elastic Beanstalk不是终止第二个实例,而是终止作为领导者的第一个实例。您现在没有运行任何cron作业,因为它们仅在已终止的第一个实例上运行。 请参阅以下评论。

更新2: 从下面的评论中明确说明: AWS现在可以防止自动实例终止。只需在你的领导实例上启用它就可以了。 - NicolásArévalo2016年10月28日9:23

答案 1 :(得分:56)

这是现在的官方方式(2015年以上)。请先尝试一下,这是目前最简单的方法,也是最可靠的方法。

根据当前的文档,一个能够在所谓的worker tier上执行定期任务

引用文档:

  

AWS Elastic Beanstalk支持在运行预定义配置的环境中的工作线程环境层的定期任务,其中包含容器名称中包含“v1.2.0”的解决方案堆栈。您必须创建一个新环境。

关于 cron.yaml

的部分也很有趣
  

要调用定期任务,应用程序源包必须在根级别包含cron.yaml文件。该文件必须包含有关要安排的定期任务的信息。使用标准crontab语法指定此信息。

更新:我们能够完成这项工作。以下是我们的经验(Node.js平台)中的一些重要问题:

  • 使用 cron.yaml 文件时,请确保您拥有最新版本awsebcli,因为旧版本无法正常运行。
  • 创造新环境也至关重要(至少在我们的情况下是这样),而不仅仅是克隆旧环境。
  • 如果您想确保EC2工作器层实例支持CRON,请ssh到它(eb ssh),然后运行cat /var/log/aws-sqsd/default.log。它应报告为aws-sqsd 2.0 (2015-02-18)。如果您没有2.0版本,那么在创建环境时出现问题,您需要创建一个如上所述的新版本。

答案 2 :(得分:31)

关于jamieb的回复,正如alrdinleal所提到的,你可以使用&#39; leader_only&#39;确保只有一个EC2实例运行cron作业的属性。

引自http://docs.amazonwebservices.com/elasticbeanstalk/latest/dg/customize-containers-ec2.html

  

你可以使用leader_only。选择一个实例作为Auto Scaling组的领导者。如果leader_only值设置为true,则该命令仅在标记为leader的实例上运行。

我试图在我的eb上实现类似的东西,所以如果我解决它会更新我的帖子。

<强>更新

好的,我现在使用以下eb配置工作cronjobs:

files:
  "/tmp/cronjob" :
    mode: "000777"
    owner: ec2-user
    group: ec2-user
    content: |
      # clear expired baskets
      */10 * * * * /usr/bin/wget -o /dev/null http://blah.elasticbeanstalk.com/basket/purge > $HOME/basket_purge.log 2>&1
      # clean up files created by above cronjob
      30 23 * * * rm $HOME/purge*
    encoding: plain 
container_commands:
  purge_basket: 
    command: crontab /tmp/cronjob
    leader_only: true
commands:
  delete_cronjob_file: 
    command: rm /tmp/cronjob

基本上,我使用cronjobs创建一个临时文件,然后将crontab设置为从临时文件中读取,然后删除临时文件。希望这会有所帮助。

答案 3 :(得分:12)

如上所述,建立任何crontab配置的根本缺陷是它只在部署时发生。随着群集自动放大,然后退回,最好也是第一个关闭的服务器。此外,没有故障转移对我来说是至关重要的。

我做了一些研究,然后与我们的AWS账户专家交谈,以反映想法并确认我提出的解决方案。您可以使用OpsWorks完成此操作,尽管这有点像使用房子来杀死苍蝇。也可以使用Data Pipeline with Task Runner,但这在它可以执行的脚本中的能力有限,我需要能够运行PHP脚本,并可以访问整个代码库。您还可以在ElasticBeanstalk集群之外专用EC2实例,但之后不会再进行故障转移。

所以这就是我提出的内容,这显然是非常规的(正如AWS代表所评论的那样)并且可能被视为黑客攻击,但它可以运行并且可以通过故障转移实现。我选择了使用SDK的编码解决方案,我将在PHP中展示,尽管您可以使用您喜欢的任何语言执行相同的方法。

// contains the values for variables used (key, secret, env)
require_once('cron_config.inc'); 

// Load the AWS PHP SDK to connection to ElasticBeanstalk
use Aws\ElasticBeanstalk\ElasticBeanstalkClient;

$client = ElasticBeanstalkClient::factory(array(
    'key' => AWS_KEY,
    'secret' => AWS_SECRET,
    'profile' => 'your_profile',
    'region'  => 'us-east-1'
));

$result = $client->describeEnvironmentResources(array(
    'EnvironmentName' => AWS_ENV
));

if (php_uname('n') != $result['EnvironmentResources']['Instances'][0]['Id']) {
    die("Not the primary EC2 instance\n");
}

通过这个以及它如何运作......你可以像往常一样在每个EC2实例上调用crontab中的脚本。每个脚本在开头都包含它(或者每个脚本包含一个文件,因为我使用它),它建立一个ElasticBeanstalk对象并检索所有实例的列表。它仅使用列表中的第一个服务器,并检查它是否与自身匹配,如果它继续,则会死亡,然后关闭。我已经检查过并且返回的列表似乎是一致的,从技术上讲,它只需要一分钟左右一致,因为每个实例都执行预定的cron。如果确实发生了变化,那就无所谓了,因为它只与那个小窗口有关。

这无论如何都不优雅,但适合我们的特定需求 - 这不是为了增加额外服务的成本,或者必须拥有专用的EC2实例,并且在发生任何故障时都会进行故障转移。我们的cron脚本运行维护脚本,这些脚本放入SQS,集群中的每个服务器都有助于执行。如果符合您的需求,至少可以为您提供备用选项。

-Davey

答案 4 :(得分:8)

我与AWS支持代理商进行了交谈,这就是我们为此工作的方式。 2015解决方案:

使用your_file_name.config在.ebextensions目录中创建一个文件。 在配置文件输入中:

files:
  "/etc/cron.d/cron_example":
    mode: "000644"
    owner: root
    group: root
    content: |
      * * * * * root /usr/local/bin/cron_example.sh

  "/usr/local/bin/cron_example.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #!/bin/bash

      /usr/local/bin/test_cron.sh || exit
      echo "Cron running at " `date` >> /tmp/cron_example.log
      # Now do tasks that should only run on 1 instance ...

  "/usr/local/bin/test_cron.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #!/bin/bash

      METADATA=/opt/aws/bin/ec2-metadata
      INSTANCE_ID=`$METADATA -i | awk '{print $2}'`
      REGION=`$METADATA -z | awk '{print substr($2, 0, length($2)-1)}'`

      # Find our Auto Scaling Group name.
      ASG=`aws ec2 describe-tags --filters "Name=resource-id,Values=$INSTANCE_ID" \
        --region $REGION --output text | awk '/aws:autoscaling:groupName/ {print $5}'`

      # Find the first instance in the Group
      FIRST=`aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names $ASG \
        --region $REGION --output text | awk '/InService$/ {print $4}' | sort | head -1`

      # Test if they're the same.
      [ "$FIRST" = "$INSTANCE_ID" ]

commands:
  rm_old_cron:
    command: "rm *.bak"
    cwd: "/etc/cron.d"
    ignoreErrors: true

此解决方案有两个缺点:

  1. 在后续部署中,Beanstalk将现有的cron脚本重命名为.bak,但cron仍会运行它。你的Cron现在在同一台机器上执行两次。
  2. 如果您的环境扩展,您将获得多个实例,所有实例都在运行您的cron脚本。这意味着您的邮件被重复,或者您的数据库存档重复
  3. 解决方法:

    1. 确保创建cron的任何.ebextensions脚本也会在后续部署中删除.bak文件。
    2. 有一个帮助脚本,它执行以下操作: - 从元数据中获取当前的实例ID - 获取当前的Auto     从EC2标记缩放组名称 - 获取EC2列表     该组中的实例按字母顺序排序。 - 取得第一个     该列表中的实例。 - 比较步骤1中的实例ID     使用第4步中的第一个实例ID。 然后,您的cron脚本可以使用此帮助程序脚本来确定它们是否应该执行。
    3. 警告:

      • 用于Beanstalk实例的IAM角色需要ec2:DescribeTags和autoscaling:DescribeAutoScalingGroups权限
      • 从中选择的实例是通过Auto Scaling显示为InService的实例。这并不一定意味着它们已完全启动并准备运行您的cron。

      如果使用默认的beanstalk角色,则不必设置IAM角色。

答案 5 :(得分:7)

如果您使用的是Rails,则可以使用whenever-elasticbeanstalk gem。它允许您在所有实例上运行cron作业或只运行一个。它会检查每一分钟以确保只有一个“领导者”实例,并且如果没有,则会自动将一个服务器提升为“领导者”。这是必需的,因为Elastic Beanstalk在部署期间只具有leader的概念,并且可以在扩展时随时关闭任何实例。

<强>更新 我转而使用AWS OpsWorks,不再维护这个gem。如果您需要比Elastic Beanstalk基础知识更多的功能,我强烈建议您切换到OpsWorks。

答案 6 :(得分:6)

你真的不想在Elastic Beanstalk上运行cron作业。由于您将拥有多个应用程序实例,因此可能会导致竞争条件和其他奇怪问题。我实际上recently blogged about this(页面下方的第4或第5个提示)。简短版本:根据应用程序,使用SQS等作业队列或iron.io等第三方解决方案。

答案 7 :(得分:4)

使用files代替container_commands的更具可读性的解决方案:

files:
  "/etc/cron.d/my_cron":
    mode: "000644"
    owner: root
    group: root
    content: |
      # override default email address
      MAILTO="example@gmail.com"
      # run a Symfony command every five minutes (as ec2-user)
      */10 * * * * ec2-user /usr/bin/php /var/app/current/app/console do:something
    encoding: plain
commands:
  # delete backup file created by Elastic Beanstalk
  clear_cron_backup:
    command: rm -f /etc/cron.d/watson.bak

请注意,格式与通常的crontab格式不同,它指定用户运行命令。

答案 8 :(得分:2)

当新领导人出现时,有人想知道领导者只会出现自动缩放问题。我似乎无法弄清楚如何回复他们的评论,但请看这个链接:http://blog.paulopoiati.com/2013/08/25/running-cron-in-elastic-beanstalk-auto-scaling-environment/

答案 9 :(得分:1)

答案 10 :(得分:1)

2017:如果您使用的是Laravel5 +

您只需2分钟即可完成配置:

  • 创建工人层
  • 安装laravel-aws-worker

    composer require dusterio/laravel-aws-worker

  • 将cron.yaml添加到根文件夹:

  

将cron.yaml添加到应用程序的根文件夹中(这可以是   您的repo的一部分,或者您可以在部署之前添加此文件   EB - 重要的是这个文件出现在   部署):

version: 1
cron:
 - name: "schedule"
   url: "/worker/schedule"
   schedule: "* * * * *"

那就是它!

App\Console\Kernel中的所有任务现在都将执行

详细说明和解释:https://github.com/dusterio/laravel-aws-worker

如何在Laravel中编写任务:https://laravel.com/docs/5.4/scheduling

答案 11 :(得分:1)

我对2018年的1%贡献

这是正确的方法(使用django/pythondjango_crontab应用):

.ebextensions文件夹中创建一个类似98_cron.config的文件:

files:
  "/tmp/98_create_cron.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #!/bin/sh
      cd /
      sudo /opt/python/run/venv/bin/python /opt/python/current/app/manage.py crontab remove > /home/ec2-user/remove11.txt
      sudo /opt/python/run/venv/bin/python /opt/python/current/app/manage.py crontab add > /home/ec2-user/add11.txt 

container_commands:
    98crontab:
        command: "mv /tmp/98_create_cron.sh /opt/elasticbeanstalk/hooks/appdeploy/post && chmod 774 /opt/elasticbeanstalk/hooks/appdeploy/post/98_create_cron.sh"
        leader_only: true

它必须是container_commands而不是commands

答案 12 :(得分:0)

  

要控制Auto Scaling是否可以在扩展时终止特定实例,请使用实例保护。您可以在Auto Scaling组或单个Auto Scaling实例上启用实例保护设置。 Auto Scaling启动实例时,实例将继承Auto Scaling组的实例保护设置。您可以随时更改Auto Scaling组或Auto Scaling实例的实例保护设置。

http://docs.aws.amazon.com/autoscaling/latest/userguide/as-instance-termination.html#instance-protection

答案 13 :(得分:0)

如果需要通过cron运行php文件,如果你已经设置了任何NAT实例,那么我可以将另一个解决方案用于此,然后你可以将cronjob放在NAT实例上并通过wget运行php文件。

答案 14 :(得分:0)

这是一个修复程序,你想在PHP中执行此操作。你只需要在.ebextensions文件夹中使用cronjob.config就可以让它像这样工作。

files:
  "/etc/cron.d/my_cron":
    mode: "000644"
    owner: root
    group: root
    content: |
        empty stuff
    encoding: plain
commands:
  01_clear_cron_backup:
    command: "rm -f /etc/cron.d/*.bak"
  02_remove_content:
    command: "sudo sed -i 's/empty stuff//g' /etc/cron.d/my_cron"
container_commands:
  adding_cron:
    command: "echo '* * * * * ec2-user . /opt/elasticbeanstalk/support/envvars && /usr/bin/php /var/app/current/index.php cron sendemail > /tmp/sendemail.log 2>&1' > /etc/cron.d/my_cron"
    leader_only: true

envvars获取文件的环境变量。您可以像上面那样在tmp / sendemail.log上调试输出。

希望这对某些人有所帮助,因为它确实帮助了我们!

答案 15 :(得分:0)

所以我们一直在努力解决这个问题,在与AWS代表进行一些讨论后,我终于想出了我认为最好的解决方案。

使用带有cron.yaml的工作层绝对是最容易解决的问题。但是,文档没有说清楚的是,这会将作业放在您用来实际运行作业的SQS队列的 end 上。如果您的cron作业对时间敏感(尽可能多),则这是不可接受的,因为它取决于队列的大小。一种选择是使用一个完全独立的环境来运行cron作业,但我认为这太过分了。

其他一些选项,例如检查您是否是列表中的第一个实例,也不理想。如果当前的第一个实例正在关闭,该怎么办?

实例保护也可能带来问题 - 如果该实例被锁定/冻结该怎么办?

重要的是要了解AWS如何管理cron.yaml功能。有一个SQS守护进程使用Dynamo表来处理“领导者选举”。它经常写入此表,如果当前的领导者在短时间内没有写入,则下一个实例将接管为领导者。这是守护进程决定将作业触发到SQS队列的实例。

我们可以重新利用现有功能,而不是尝试重写我们自己的功能。您可以在此处查看完整解决方案:https://gist.github.com/dorner/4517fe2b8c79ccb3971084ec28267f27

这是Ruby中的,但您可以轻松地将其适用于具有AWS SDK的任何其他语言。从本质上讲,它检查当前的领导者,然后检查状态以确保它处于良好状态。它将循环直到当前领导者处于良好状态,如果当前实例是领导者,则执行该作业。

答案 16 :(得分:0)

根据user1599237的回答原则,您让cron作业在所有实例上运行,但随后在作业开始时确定是否应允许它们运行,我提出了另一种解决方案

我使用的是已经从所有实例连接到的MySQL数据库,而不是查看正在运行的实例(必须存储您的AWS密钥和机密)。

没有缺点,只有正面:

  • 没有额外的实例或费用
  • 坚实的解决方案-不可能双重执行
  • 可扩展-在实例按比例缩放时自动工作
  • 故障转移-在实例发生故障时自动运行

或者,您也可以使用公用共享文件系统(例如,通过NFS协议使用AWS EFS)而不是数据库。

以下解决方案是在PHP框架Yii中创建的,但是您可以轻松地使其适应其他框架和语言。另外,异常处理程序Yii::$app->system是我自己的模块。替换为您正在使用的任何内容。

/**
 * Obtain an exclusive lock to ensure only one instance or worker executes a job
 *
 * Examples:
 *
 * `php /var/app/current/yii process/lock 60 empty-trash php /var/app/current/yii maintenance/empty-trash`
 * `php /var/app/current/yii process/lock 60 empty-trash php /var/app/current/yii maintenance/empty-trash StdOUT./test.log`
 * `php /var/app/current/yii process/lock 60 "empty trash" php /var/app/current/yii maintenance/empty-trash StdOUT./test.log StdERR.ditto`
 * `php /var/app/current/yii process/lock 60 "empty trash" php /var/app/current/yii maintenance/empty-trash StdOUT./output.log StdERR./error.log`
 *
 * Arguments are understood as follows:
 * - First: Duration of the lock in minutes
 * - Second: Job name (surround with quotes if it contains spaces)
 * - The rest: Command to execute. Instead of writing `>` and `2>` for redirecting output you need to write `StdOUT` and `StdERR` respectively. To redirect stderr to stdout write `StdERR.ditto`.
 *
 * Command will be executed in the background. If determined that it should not be executed the script will terminate silently.
 */
public function actionLock() {
    $argsAll = $args = func_get_args();
    if (!is_numeric($args[0])) {
        \Yii::$app->system->error('Duration for obtaining process lock is not numeric.', ['Args' => $argsAll]);
    }
    if (!$args[1]) {
        \Yii::$app->system->error('Job name for obtaining process lock is missing.', ['Args' => $argsAll]);
    }

    $durationMins = $args[0];
    $jobName = $args[1];
    $instanceID = null;
    unset($args[0], $args[1]);

    $command = trim(implode(' ', $args));
    if (!$command) {
        \Yii::$app->system->error('Command to execute after obtaining process lock is missing.', ['Args' => $argsAll]);
    }

    // If using AWS Elastic Beanstalk retrieve the instance ID
    if (file_exists('/etc/elasticbeanstalk/.aws-eb-system-initialized')) {
        if ($awsEb = file_get_contents('/etc/elasticbeanstalk/.aws-eb-system-initialized')) {
            $awsEb = json_decode($awsEb);
            if (is_object($awsEb) && $awsEb->instance_id) {
                $instanceID = $awsEb->instance_id;
            }
        }
    }

    // Obtain lock
    $updateColumns = false;  //do nothing if record already exists
    $affectedRows = \Yii::$app->db->createCommand()->upsert('system_job_locks', [
        'job_name' => $jobName,
        'locked' => gmdate('Y-m-d H:i:s'),
        'duration' => $durationMins,
        'source' => $instanceID,
    ], $updateColumns)->execute();
    // The SQL generated: INSERT INTO system_job_locks (job_name, locked, duration, source) VALUES ('some-name', '2019-04-22 17:24:39', 60, 'i-HmkDAZ9S5G5G') ON DUPLICATE KEY UPDATE job_name = job_name

    if ($affectedRows == 0) {
        // record already exists, check if lock has expired
        $affectedRows = \Yii::$app->db->createCommand()->update('system_job_locks', [
                'locked' => gmdate('Y-m-d H:i:s'),
                'duration' => $durationMins,
                'source' => $instanceID,
            ],
            'job_name = :jobName AND DATE_ADD(locked, INTERVAL duration MINUTE) < NOW()', ['jobName' => $jobName]
        )->execute();
        // The SQL generated: UPDATE system_job_locks SET locked = '2019-04-22 17:24:39', duration = 60, source = 'i-HmkDAZ9S5G5G' WHERE job_name = 'clean-trash' AND DATE_ADD(locked, INTERVAL duration MINUTE) < NOW()

        if ($affectedRows == 0) {
            // We could not obtain a lock (since another process already has it) so do not execute the command
            exit;
        }
    }

    // Handle redirection of stdout and stderr
    $command = str_replace('StdOUT', '>', $command);
    $command = str_replace('StdERR.ditto', '2>&1', $command);
    $command = str_replace('StdERR', '2>', $command);

    // Execute the command as a background process so we can exit the current process
    $command .= ' &';

    $output = []; $exitcode = null;
    exec($command, $output, $exitcode);
    exit($exitcode);
}

这是我正在使用的数据库架构:

CREATE TABLE `system_job_locks` (
    `job_name` VARCHAR(50) NOT NULL,
    `locked` DATETIME NOT NULL COMMENT 'UTC',
    `duration` SMALLINT(5) UNSIGNED NOT NULL COMMENT 'Minutes',
    `source` VARCHAR(255) NULL DEFAULT NULL,
    PRIMARY KEY (`job_name`)
)

答案 17 :(得分:0)

来自Amazon的最新示例是最简单,最有效的(定期任务):

https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/using-features-managing-env-tiers.html

,在其中创建单独的工作层以执行任何cron作业。创建cron.yaml文件并将其放在您的根文件夹中。我遇到的一个问题(在cron似乎未执行之后)是发现我的CodePipeline没有权限执行动态修改。基于此,在IAM->角色-> yourpipeline下添加FullDynamoDB访问并重新部署(弹性beantalk)之后,它可以完美工作。