├── README.md ├── LICENSE └── 10_aws_eb_cron.config /README.md: -------------------------------------------------------------------------------- 1 | # aws-eb-cron 2 | Makes it easy & safe to run cron tasks on just the leading instance in an Elastic Beanstalk cluster. 3 | 4 | Usage: 5 | - Add the AmazonEC2ReadOnlyAccess policy to the aws-elasticbeanstalk-ec2-role (required so that instances can check their status) 6 | - Drop the `10_aws_eb_cron.config` file into your `.ebextensions` folder within your project. 7 | - Prefix cron tasks with `aws-eb-cron` when you only want them to run on the leading instance. 8 | - The script will periodically check if it is on the active leader, making this safe to use when scaling up and down. 9 | 10 | Example: Every 5 minutes, execute `php cron.php`, but only do so on one instance in the beanstalk environment. 11 | ``` 12 | */5 * * * * root aws-eb-cron php cron.php 13 | ``` 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Heath Dutton ☕ 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 | -------------------------------------------------------------------------------- /10_aws_eb_cron.config: -------------------------------------------------------------------------------- 1 | # Prevent cron tasks from being ran by multiple instances in Elastic Beanstalk. 2 | # 3 | # Features: 4 | # - Automatically adjusts to new "leading instance" when scaling up or down. 5 | # - Stores the result in an environment variable for other uses as AWS_EB_CRON 6 | # 7 | # Usage: 8 | # - Add the AmazonEC2ReadOnlyAccess policy to the aws-elasticbeanstalk-ec2-role (required so that instances can check their status) 9 | # - This must be ran as root. 10 | # - Anything after this file will be executed as webapp for security. 11 | # 12 | # Example Cron (should be created by .ebextensions): 13 | # # Every 5 minutes, execute cron.php, but only do so on one instance in the beanstalk environment. 14 | # */5 * * * * . aws-eb-cron php cron.php 15 | # 16 | # Alternatively, just use the environment variable AWS_EB_CRON in your code, just update the variable via cron. 17 | # */5 * * * * . aws-eb-cron 18 | # * * * * * php cron.php 19 | 20 | files: 21 | "/bin/aws-eb-cron": 22 | mode: "0000755" 23 | owner: root 24 | group: root 25 | content: | 26 | #!/usr/bin/env bash 27 | 28 | # Load environment variables if available. 29 | if [[ -f "/opt/elasticbeanstalk/support/envvars" ]] 30 | then 31 | . /opt/elasticbeanstalk/support/envvars 32 | fi 33 | 34 | # Store the previous value established (if available) 35 | if [[ -z "$AWS_EB_CRON" ]] 36 | then 37 | PREV_AWS_EB_CRON="$AWS_EB_CRON" 38 | else 39 | PREV_AWS_EB_CRON="NA" 40 | fi 41 | 42 | # Establish the frequency of lead checking. 43 | if [[ -z "$AWS_EB_CRON_FREQ" ]] 44 | then 45 | # By default, check every 5 minutes. 46 | # This is because it requires 4 curl actions, so you don't wanna run it ALL the time. 47 | AWS_EB_CRON_FREQ=5 48 | fi 49 | 50 | # Check if we've already established the leader recently. 51 | if test $( find /tmp/AWS_EB_CRON -mmin -"$AWS_EB_CRON_FREQ" ) 52 | then 53 | echo "We have already recently determined the leader." 54 | AWS_EB_CRON=$( cat /tmp/AWS_EB_CRON ) 55 | else 56 | echo "Determining leader..." 57 | 58 | # Get the current instance ID. 59 | EC2_INSTANCE_ID=$( curl -s -m 5 http://169.254.169.254/latest/meta-data/instance-id ) 60 | if [[ -z "$EC2_INSTANCE_ID" ]] 61 | then 62 | echo "Could not determine my EC2 instance. You may not be running this in an Amazon instance or something has gone terribly wrong. Aborting." 63 | exit 64 | fi 65 | echo "EC2 Instance ID: $EC2_INSTANCE_ID" 66 | 67 | # Get the current region. Note, we may have instances across availability zones, so we will not be using this variable further. 68 | EC2_AVAIL_ZONE=$( curl -s -m 5 http://169.254.169.254/latest/meta-data/placement/availability-zone ) 69 | echo "EC2 Availability Zone: $EC2_AVAIL_ZONE" 70 | 71 | EC2_REGION="`echo \"$EC2_AVAIL_ZONE\" | sed -e 's:\([0-9][0-9]*\)[a-z]*\$:\\1:'`" 72 | echo "EC2 Region: $EC2_REGION" 73 | 74 | # Get the elastic beanstalk environment id. 75 | EB_ENV_ID=$( aws ec2 describe-instances --region "$EC2_REGION" --instance-id "$EC2_INSTANCE_ID" --output text | grep "TAGS.*elasticbeanstalk:environment-id.*" | sed -e 's/TAGS\selasticbeanstalk:environment-id\s//' ) 76 | if [[ -z "$EB_ENV_ID" ]] 77 | then 78 | echo "Could not determine my EB Environment ID. Perhaps you are not running this in Elastic Beanstalk? Aborting." 79 | exit 80 | fi 81 | echo "Elastic Beanstalk Environment ID: $EB_ENV_ID" 82 | 83 | # Get all running instances with this tag, just the instance ID and the LaunchTime. 84 | EC2_INSTANCES=$( aws ec2 describe-instances --region "$EC2_REGION" --output text --query 'Reservations[*].Instances[*].[LaunchTime,InstanceId]' --filters "Name=tag-key,Values=elasticbeanstalk:environment-id" "Name=tag-value,Values=$EB_ENV_ID" "Name=instance-state-code,Values=16" ) 85 | 86 | if [[ -z "$EC2_INSTANCES" ]] 87 | then 88 | echo "No running sibling instances found, so I am the leader." 89 | AWS_EB_CRON=1 90 | else 91 | # The last instance in a sorted list will be the oldest, and will be our leader. 92 | EC2_INSTANCE_LEAD_ID=$( echo $EC2_INSTANCES | sort | tail -n 1 | sed -e 's/^.*\s//' ) 93 | if [[ "$EC2_INSTANCE_LEAD_ID" == "$EC2_INSTANCE_ID" ]] 94 | then 95 | echo "I am the oldest instance running, so I am the leader." 96 | AWS_EB_CRON=1 97 | else 98 | echo "I am a younger instance, so I will not lead." 99 | AWS_EB_CRON=0 100 | fi 101 | fi 102 | 103 | # Update the current instance environment variable "AWS_EB_CRON" if needed. 104 | if [[ "$AWS_EB_CRON" != "$PREV_AWS_EB_CRON" ]] 105 | then 106 | # This is a change from previously known status. 107 | sudo chmod 0646 /opt/elasticbeanstalk/support/envvars 108 | if [[ -z "$( grep 'export AWS_EB_CRON=' /opt/elasticbeanstalk/support/envvars )" ]] 109 | then 110 | echo "Adding a new variable." 111 | sudo echo "export AWS_EB_CRON=\"$AWS_EB_CRON\"" >> /opt/elasticbeanstalk/support/envvars 112 | else 113 | echo "Replacing previous environment variable." 114 | sudo sed -i "/export AWS_EB_CRON=/c\export AWS_EB_CRON=\"$AWS_EB_CRON\"" /opt/elasticbeanstalk/support/envvars 115 | fi 116 | sudo chmod 0644 /opt/elasticbeanstalk/support/envvars 117 | fi 118 | 119 | # Store the value in a file in /tmp for quicker lookup during the check_frequency. 120 | echo "$AWS_EB_CRON" > /tmp/AWS_EB_CRON 121 | fi 122 | 123 | # Run commands following this script as parameters if we are the leader. 124 | if [[ "$AWS_EB_CRON" == "1" ]] 125 | then 126 | if [[ ! -z "$@" ]] 127 | then 128 | cmdstring=". /opt/elasticbeanstalk/support/envvars ; $@" 129 | echo "Executing: sudo -u webapp bash -c \"$cmdstring\"" 130 | sudo -u webapp bash -c "$cmdstring" 131 | fi 132 | fi 133 | --------------------------------------------------------------------------------