Stop EC2 instances automatically with Terraform

January 4, 2024 
On/off switch

Introduction

AWS EC2 is a very convenient place for experimenting with and testing things on virtual machines (or "EC2 instances"). However, it is also very easy to forget such EC2 instances running for no reason, which can become very costly very quickly. Fortunately In AWS computing (e.g. CPU, memory and I/O usage) and storage (disk) usage are billed separately. In other words: if your EC2 instance is stopped, you only pay for its storage. This cuts down the costs significantly, especially if your are using powerful EC2 instance types. So, you definitely should to stop EC2 instances automatically, say, every evening, if you don't need them to be up. There is no built-in way in to do, though, so this article shows a couple ways it can be done.

Details of a typical implementation

Most solutions I found stop EC2 instances using Lambda functions that are triggered by an event on a schedule. The event source can be AWS Eventbridge for example. Most solutions only stop (or start) EC2 instances that have a special tag set. That is a good safeguard against accidentally shutting down all of production, for example.

The official way: the Instance Scheduler Cloudformation template

AWS provides a supported Cloudformation template for scheduling EC2 instances. By "scheduling" we mean starting and stopping EC2 instance on schedule. It is fairly complex setup but it seems to deploy fine and "should work"(tm) because it is officially supported by AWS.

If you're using Terraform or Opentofu like us, you should probably stay away from Cloudformation templates which won't integrate well with your existing Infrastructure as Code codebase. I tried converting this particular Cloudformation template to Terraform code using cf2tf ("Cloudformation to Terraform"), but at least with Opentofu 1.6.0 the end result looked quite complex and did not work out of the box, so I gave up.

Terraform: lambda-scheduler-stop-start

If you use Terraform or Opentofu, you best bet seems to be the lambda-scheduler-stop-start Terraform module. Its GitHub statistics show that it is quite well-known and well-maintained. It also supports stopping and starting a lot more than just EC2 instances:

  • EC2 instances
  • ECS services
  • RDS services
  • RDS clusters
  • Redshift clusters
  • DocumentDB clusters
  • Autoscaling
  • Cloudwatch alarms

You can find quite a few other Terraform-based solutions if you search a bit, but they seem hacky, badly documented, outdated, limited or any any combination thereof.

Configuring IAM permissions for lambda-scheduler-stop-start

You may need to grant Terraform some additional IAM permissions so that it can configure lambda-scheduler-start-stop. Trying to be really granularwith IAM permissions generally bites you in the ass in the long term, so here's a set of rather brutal IAM permissions that allow Terraform to set up the scheduler:

AmazonEC2FullAccess
AmazonEventBridgeFullAccess
AWSLambda_FullAccess
CloudWatchFullAccessV2
IAMFullAccess

You should of course balance ease of use against the potential security implications of Terraform having excessively wide IAM permissions.

Stop EC2 instances automatically with lambda-scheduler-stop-start

You can configure the lambda-scheduler-stop-start easily using this snippet borrowed from its official README.md:

module "stop_ec2_instance" {
  source                         = "diodonfrost/lambda-scheduler-stop-start/aws"
  name                           = "ec2_stop"
  cloudwatch_schedule_expression = "cron(0 0 ? * FRI *)"
  schedule_action                = "stop"
  autoscaling_schedule           = "false"
  documentdb_schedule            = "false"
  ec2_schedule                   = "true"
  ecs_schedule                   = "false"
  rds_schedule                   = "false"
  redshift_schedule              = "false"
  cloudwatch_alarm_schedule      = "false"
  scheduler_tag                  = {
    key   = "tostop"
    value = "true"
  }
}

module "start_ec2_instance" {
  source                         = "diodonfrost/lambda-scheduler-stop-start/aws"
  name                           = "ec2_start"
  cloudwatch_schedule_expression = "cron(0 8 ? * MON *)"
  schedule_action                = "start"
  autoscaling_schedule           = "false"
  documentdb_schedule            = "false"
  ec2_schedule                   = "true"
  ecs_schedule                   = "false"
  rds_schedule                   = "false"
  redshift_schedule              = "false"
  cloudwatch_alarm_schedule      = "false"
  scheduler_tag                  = {
    key   = "tostop"
    value = "true"
  }
}

The resources (e.g. EC2 instances) you want to automatically stop and/or start should have a tag called "tostop" with value "true". If you only want to stop EC2 instances then omit the "start_ec2_instance" module call altogether. The schedule (cron) format is documented here. All times should be in UTC timezone.

To test the setup create an EC2 instance with appropriate tags, set the schedule to something in the next few minutes (in UTC time) and apply with Terraform. If your EC2 instance stops, then the lambda function, scheduler and event bridge are doing what they're supposed to. You can then adjust the schedule to match your actual requirements.

Samuli Seppänen
Samuli Seppänen
Author archive
menucross-circle