-
-
Save picadoh/815c11361d1a88419ea16b14fe044e85 to your computer and use it in GitHub Desktop.
### Cloudwatch Events ### | |
# Event rule: Runs at 8pm during working days | |
resource "aws_cloudwatch_event_rule" "start_instances_event_rule" { | |
name = "start_instances_event_rule" | |
description = "Starts stopped EC2 instances" | |
schedule_expression = "cron(0 8 ? * MON-FRI *)" | |
depends_on = ["aws_lambda_function.ec2_start_scheduler_lambda"] | |
} | |
# Runs at 8am during working days | |
resource "aws_cloudwatch_event_rule" "stop_instances_event_rule" { | |
name = "stop_instances_event_rule" | |
description = "Stops running EC2 instances" | |
schedule_expression = "cron(0 20 ? * MON-FRI *)" | |
depends_on = ["aws_lambda_function.ec2_stop_scheduler_lambda"] | |
} | |
# Event target: Associates a rule with a function to run | |
resource "aws_cloudwatch_event_target" "start_instances_event_target" { | |
target_id = "start_instances_lambda_target" | |
rule = "${aws_cloudwatch_event_rule.start_instances_event_rule.name}" | |
arn = "${aws_lambda_function.ec2_start_scheduler_lambda.arn}" | |
} | |
resource "aws_cloudwatch_event_target" "stop_instances_event_target" { | |
target_id = "stop_instances_lambda_target" | |
rule = "${aws_cloudwatch_event_rule.stop_instances_event_rule.name}" | |
arn = "${aws_lambda_function.ec2_stop_scheduler_lambda.arn}" | |
} | |
# AWS Lambda Permissions: Allow CloudWatch to execute the Lambda Functions | |
resource "aws_lambda_permission" "allow_cloudwatch_to_call_start_scheduler" { | |
statement_id = "AllowExecutionFromCloudWatch" | |
action = "lambda:InvokeFunction" | |
function_name = "${aws_lambda_function.ec2_start_scheduler_lambda.function_name}" | |
principal = "events.amazonaws.com" | |
source_arn = "${aws_cloudwatch_event_rule.start_instances_event_rule.arn}" | |
} | |
resource "aws_lambda_permission" "allow_cloudwatch_to_call_stop_scheduler" { | |
statement_id = "AllowExecutionFromCloudWatch" | |
action = "lambda:InvokeFunction" | |
function_name = "${aws_lambda_function.ec2_stop_scheduler_lambda.function_name}" | |
principal = "events.amazonaws.com" | |
source_arn = "${aws_cloudwatch_event_rule.stop_instances_event_rule.arn}" | |
} |
### IAM Role and Policy ### | |
# Allows Lambda function to describe, stop and start EC2 instances | |
resource "aws_iam_role" "ec2_start_stop_scheduler" { | |
name = "ec2_start_stop_scheduler" | |
assume_role_policy = <<EOF | |
{ | |
"Version": "2012-10-17", | |
"Statement": [ | |
{ | |
"Action": "sts:AssumeRole", | |
"Principal": { | |
"Service": "lambda.amazonaws.com" | |
}, | |
"Effect": "Allow", | |
"Sid": "" | |
} | |
] | |
} | |
EOF | |
} | |
data "aws_iam_policy_document" "ec2_start_stop_scheduler" { | |
statement = [ | |
{ | |
actions = [ | |
"logs:CreateLogGroup", | |
"logs:CreateLogStream", | |
"logs:PutLogEvents" | |
] | |
resources = [ | |
"arn:aws:logs:*:*:*", | |
] | |
}, | |
{ | |
actions = [ | |
"ec2:Describe*", | |
"ec2:Stop*", | |
"ec2:Start*" | |
] | |
resources = [ | |
"*", | |
] | |
} | |
] | |
} | |
resource "aws_iam_policy" "ec2_start_stop_scheduler" { | |
name = "ec2_access_scheduler" | |
path = "/" | |
policy = "${data.aws_iam_policy_document.ec2_start_stop_scheduler.json}" | |
} | |
resource "aws_iam_role_policy_attachment" "ec2_access_scheduler" { | |
role = "${aws_iam_role.ec2_start_stop_scheduler.name}" | |
policy_arn = "${aws_iam_policy.ec2_start_stop_scheduler.arn}" | |
} |
### AWS Lambda function ### | |
# AWS Lambda API requires a ZIP file with the execution code | |
data "archive_file" "start_scheduler" { | |
type = "zip" | |
source_file = "start_instances.py" | |
output_path = "start_instances.zip" | |
} | |
data "archive_file" "stop_scheduler" { | |
type = "zip" | |
source_file = "stop_instances.py" | |
output_path = "stop_instances.zip" | |
} | |
# Lambda defined that runs the Python code with the specified IAM role | |
resource "aws_lambda_function" "ec2_start_scheduler_lambda" { | |
filename = "${data.archive_file.start_scheduler.output_path}" | |
function_name = "start_instances" | |
role = "${aws_iam_role.ec2_start_stop_scheduler.arn}" | |
handler = "start_instances.lambda_handler" | |
runtime = "python2.7" | |
timeout = 300 | |
source_code_hash = "${data.archive_file.start_scheduler.output_base64sha256}" | |
} | |
resource "aws_lambda_function" "ec2_stop_scheduler_lambda" { | |
filename = "${data.archive_file.stop_scheduler.output_path}" | |
function_name = "stop_instances" | |
role = "${aws_iam_role.ec2_start_stop_scheduler.arn}" | |
handler = "stop_instances.lambda_handler" | |
runtime = "python2.7" | |
timeout = 300 | |
source_code_hash = "${data.archive_file.stop_scheduler.output_base64sha256}" | |
} |
import boto3 | |
# Boto Connection | |
ec2 = boto3.resource('ec2', 'eu-west-2') | |
def lambda_handler(event, context): | |
# Filters | |
filters = [{ | |
'Name': 'tag:AutoStop', | |
'Values': ['true'] | |
}, | |
{ | |
'Name': 'instance-state-name', | |
'Values': ['stopped'] | |
} | |
] | |
# Filter stopped instances that should start | |
instances = ec2.instances.filter(Filters=filters) | |
# Retrieve instance IDs | |
instance_ids = [instance.id for instance in instances] | |
# starting instances | |
starting_instances = ec2.instances.filter(Filters=[{'Name': 'instance-id', 'Values': instance_ids}]).start() |
import boto3 | |
# Boto Connection | |
ec2 = boto3.resource('ec2', 'eu-west-2') | |
def lambda_handler(event, context): | |
# Filters | |
filters = [{ | |
'Name': 'tag:AutoStop', | |
'Values': ['true'] | |
}, | |
{ | |
'Name': 'instance-state-name', | |
'Values': ['running'] | |
} | |
] | |
# Filter running instances that should stop | |
instances = ec2.instances.filter(Filters=filters) | |
# Retrieve instance IDs | |
instance_ids = [instance.id for instance in instances] | |
# stopping instances | |
stopping_instances = ec2.instances.filter(Filters=[{'Name': 'instance-id', 'Values': instance_ids}]).stop() |
Very nice. Thank you kindly for your example. As a heads up: in case the initial filter (running, tagged instances) does not match anything, an empty sequence will be passed into the subsequent filter (IntanceIds=[]
). This will cause boto3 to return all instances within the region and result in them being stopped.
This can arise, e.g., when the Cloudwatch event is fired manually ahead of the schedule. In that case, once the schedule triggers, there will be no running, tagged instances around, so that instance_ids
will be the empty list. Hence, we need an additional guard against this case.
Thanks, @mkrohmann, and @eigengrau. This was indeed problematic. I've changed the filter to use Filters=[{'Name': 'instance-id', 'Values': instance_ids}]
instead of InstanceIds=instance_ids
, which results it behaving as expected.
I'd like to use the scheduler code in a project that is planned to be opensource - would you be willing to add a license to this gist such as MIT?
i have tried same code and did not work for me,i mean could see aws cloudwatch and lambda function in place,
but its not stopping the ec2 instance.
when i try to run the terraform plan it gives me this error:
on iam.tf line 23, in data "aws_iam_policy_document" "ec2_start_stop_scheduler":
23: statement = [
An argument named "statement" is not expected here. Did you mean to define a
block of type "statement"?
i have tried same code and did not work for me,i mean could see aws cloudwatch and lambda function in place, but its not stopping the ec2 instance.
same issue, If you have resolved it kindly let me know.
i have tried same code and did not work for me,i mean could see aws cloudwatch and lambda function in place, but its not stopping the ec2 instance.
same issue, If you have resolved it kindly let me know.
Please use block
data "aws_iam_policy_document" "ec2_start_stop_scheduler" {
statement {
actions = ["logs:CreateLogGroup","logs:CreateLogStream","logs:PutLogEvents"]
resources = ["arn:aws:logs:::"]
effect = "Allow"
}
statement {
actions = ["ec2:Describe","ec2:Stop*","ec2:Start*"]
resources = ["*"]
effect = "Allow"
}
}
Thank you!
You'd better revise starting and stopping instances for the case when no instances match, since applying an empty sequence as an instance filter would return all instances unfiltered. Hence all your instances will be started or stopped.