Hands On: AWS Savings with IoT Button

AWS IoT Environment Startup/Shutdown

AWS IoT Button

One of the easiest ways to save money on your AWS EC2 costs especially for development environments is to only run them when there is active development and then shut them down when they are not in use. There are a number of great articles on how to do automatic scheduling of EC2 startup/ shutdown. Be sure to check out the AWS Instance Scheduler

But what if you have an environment that you only need occassionally and there is no set schedule for it? For some Microsoft environments there is a need to startup and shutdown specific instances in a particular order. In order to do this, I divide the environment into tiers allowing some time in between starting or stopping of each tier.

Environment Setup

As an example, I will use this very simple environment but you could expand this to many different instances and tier levels.

  • Tier – 0 (Active Directory Controllers)
  • Tier – 1 (Remote Desktop Gateways)
  • Tier – 2 (SQL Servers, Certificate Servers, etc.)
Simple Tiered Environment

For each of the EC2 instances we will tag them with a tag key of ‘Tier’ and a value of Tier-0, Tier-1, etc. Now using the IoT Button and Lambda we can wire up the button to startup up the environment on a ‘single’ click and to shutdown the environment on a ‘double’ click. The python Lambda function will handle the startup/shutdown of the environment and send a SNS text notification when the environment is up and running.

The basic pseudocode for the Lambda function is this:

  1. Read the payload from the IoT Button and determine if it was a ‘single’ or ‘double’ click.
  2. Wait 1 minute between the startup or shutdown of each tier.
  3. If a ‘single’ click:
    loop through the tiers starting from the lowest to the highest and startup each EC2 instance in that tier.
  4. If a ‘double’ click:
    loop through the tiers starting from the highest to the lowest and shutdown each EC2 instance in that tier.
  5. Send a SNS notification when the environment is up or down.

Lambda Setup

Lambda Setup

For our Lambda function, we need to give it a role that has the rights to start/stop and read EC2 tags along with publishing the SNS notification.

Add the IoT Button as a trigger for the Lambda Function.

Lamdba Trigger Configuration

Be sure to set an appropriate timeout value for the Lambda function so that it has time to run through all the tiers of the environment.

Starting the Environment

On a ‘single’ click of the button, the environment will startup per tier.

CloudWatch Logs of environment startup
SNS Text after environment startup

Shutdown Environment

On a ‘double’ click of the IoT button the environment will shutdown per tier.

CloudWatch Logs of environment shutdown
SNS Text after environment shutdown

Python Code

Here is a rough first version of the Python code. I know I’m paying for some wasted sleep time on the function, but it is still less than what I would pay in EC2 hourly fees. Step Functions might be an answer in that respect, not sure at this point. I’m sure there are a number of tweaks to be done in the code. Always iterate.


The following JSON template shows what is sent as the payload:
    "serialNumber": "GXXXXXXXXXXXXXXXXX",
    "batteryVoltage": "xxmV",
    "clickType": "SINGLE" | "DOUBLE" | "LONG"

A "LONG" clickType is sent if the first press lasts longer than 1.5 seconds.
"SINGLE" and "DOUBLE" clickType payloads are sent for short clicks.

For more documentation, follow the link below.
from __future__ import print_function

import boto3
import json
import logging
import os
import time

logger = logging.getLogger()

# Global variables
sns = boto3.client('sns')
ec2 = boto3.client('ec2')
phone_number = os.environ.get("TO_NUMBER")
num_tiers = int(os.environ.get("NUM_TIERS"))
env_name = os.environ.get("ENV_NAME")

# Function to start/stop the instances
def dev_instances(tier, state_change):
    custom_filter = [{
        'Name': 'tag:Tier',
        'Values': [tier]
    response = ec2.describe_instances(Filters=custom_filter)
    instances = response['Reservations']
    instance_ids = []
    logger.info(f"Checking {tier} instances to a {state_change} state")
    # Build are list of instances at a given tier
    for x in range(len(instances)):
        instance_id = instances[x]['Instances'][0]['InstanceId']
        instance_state = instances[x]['Instances'][0]['State']['Name']
    # Determine if we should start or stop the instances
    if state_change == 'start':
        logger.info(f"Starting: {instance_ids}")
        logger.info(f"Stopping: {instance_ids}")

def lambda_handler(event, context):
    logger.info('Received event: ' + json.dumps(event))

    # A single click will start up the environment from Tier-0 to NUM_TIERS
    # Wait 1 minute after each tier starts up
    if event['clickType'] == 'SINGLE':
        for i in range(num_tiers):
           dev_instances(f'Tier-{i}', 'start')
        message = f'Environment {env_name} has been started. Be sure to shutdown'

    # A double-click will shutdown the environment in the reverse order
    elif event['clickType'] == 'DOUBLE':
        for i in range(num_tiers -1, -1, -1):
           dev_instances(f'Tier-{i}', 'stop')
        message = f'Environment {env_name} has been shutdown. Click to start'
        message = 'Hello from your IoT Button %s. Here is the full event: %s' % (event['serialNumber'], json.dumps(event))
    sns.publish(PhoneNumber=phone_number, Message=message)
    logger.info('SMS has been sent to ' + phone_number)

Never stop learning and Cloud On!

The cloud is an architect’s dream. Prior to the cloud if I screwed something up there was tangible evidence that had to be destroyed. Now it’s just a blip in the bill. – Mike Spence

Leave a Comment