Turn off AWS EC2 Windows Server based on active RDP connections. And save money.

2019-01-19Michal

This is one of those more self-explanatory titles, isn’t it? A little backstory for this is due, though.

Previously on “being stingy with Michał”

At idibu we support a fair amount of CRM system integrations – that means that both our developers and support crew need to be able to have client-side access to those. One of the most popular integrations is with a large Windows desktop app, served as a plugin package. That proved to be a bit problematic to support logistically, because:

  • it has to run with a local SQL Server installation
  • not everyone at idibu have Windows machines
  • updating local installations and maintaining them would be a pain in terms of both scheduling and amount of work

That’s why we’ve decided to run a Windows Server instance somewhere and connect to it via Remote Desktop. Azure was our first bet and unfortunately it turned out to be both slow and expensive as people (myself included) often forgot to turn it off after using. It ended up being about 50-70 GBP a month and the poor amount of IOPS made it so slow it was almost unusable for our purposes

Our ideal scenario was to have a single machine running somewhere, that we can connect remotely too, do our dev, demo and support stuff, then disconnect and forget about it. That’s why we’ve decided to run a remote Windows Server instance.

Azure was our first bet and unfortunately it turned out to be both slow and expensive as people (myself included) often forgot to turn it off after using. It ended up being about 50-70 GBP a month and the poor amount of IOPS made it so slow it was almost unusable for our purposes.

We’ve then switched to a (t2.medium) EC2 instance with Amazon Web Services, which was a lot faster (with the core system being ran from an SSD drive) and had a cost estimation of about 40-50 USD a month. I’ve tasked myself with minimizing that cost.

Phase 1: Analysis

So here are the most important facts:

  • about 98% of VM usage comes from our EMEA crew
  • they work from 9 to 5 BST
  • they all connect using Remote Desktop
  • the actual usage varies from 8 hours a day to none in some cases, 2 hours a day being the average value
  • the VM is only needed for a single desktop app

Knowing that is enough to come to a conclusion.

Phase 2: The plan

Let’s just stop the EC2 instance when it’s not used. By not used I mean “there’s no active RDP connections”. For that we’ll use CloudWatch with a custom metric and then add an alarm that triggers when there’s 0 users connected for 30 minutes and make it stop the instance.

We also need to allow people to start it again using some sort of graphical interface. We’ve already got a support portal written in PHP, so it would be the best if we could just set it up there – we can make PHP call a Python script. There’s already a PHP SDK for AWS, but for a couple of reasons I can’t use it – if you can, then you can just use pure PHP instead.

Phase 3: The recipe

You’ll need:

  • 1 AWS EC2 instance running Windows Server
  • 1 Elastic IP attached to that instance (so the host address doesn’t change and you can use the same .rdp files to connect)
  • AWS Cloudwatch (basic monitoring is fine)
  • Visual Studio for programming a Windows service uploading metrics

First you’ll need to write a Windows Service in Visual Studio, which will get the current number of active RDP connections and uploads it as a CloudWatch metric. So go ahead and create a new “Windows Service” project in Visual Studio:

Install two required NuGet packages: Cassia and AWSSDK.CloudWatch. Write the C# service code (in the Service1.cs file):

using System;
using System.Collections.Generic;
using System.Security.Principal;
using System.ServiceProcess;
using System.Timers;
using Amazon;
using Amazon.CloudWatch;
using Amazon.CloudWatch.Model;
using Cassia;

namespace Cloudwatch_metric_uploader
{
    public partial class Service1 : ServiceBase
    {
        // Change the following to your own values
        private string _accessKey = "ExampleAccessKey";
        private string _secretKey = "ExampleSecretKey";
        private string _instanceId = "ExampleEc2InstanceId"; 

        public Service1()
        {
            InitializeComponent();
        }

        protected override void OnStart(string[] args)
        {
            // Start a new timer with a "tick" every 5 mins
            var tim = new Timer(300000);

            // Subscribe to the "tick" event
            tim.Elapsed += Tim_Elapsed;

            // Make it so it starts over after every "tick"
            tim.AutoReset = true;

            // And start it
            tim.Enabled = true;
        }

        private void Tim_Elapsed(object sender, ElapsedEventArgs e)
        {
            try
            {
                // Get the list of currently active RDP connections
                var list = GetRdpAccountList();

                // And upload its count to Cloudwatch
                UploadRdpMetrics(list.Count);
            }
            catch (Exception)
            {
                //We don't want it to crash whenever there's a connection problem
            }
        }

        protected override void OnStop()
        {

        }

        public List<NTAccount> GetRdpAccountList()
        {
            List<NTAccount> accList = new List<NTAccount>();

            ITerminalServicesManager manager = new TerminalServicesManager();

            // Get the local terminal server data
            using (ITerminalServer server = manager.GetRemoteServer("localhost"))
            {
                server.Open();

                // List each terminal session
                foreach (ITerminalServicesSession session in server.GetSessions())
                {
                    NTAccount account = session.UserAccount;

                    // If a user is connected and the connection is currently active
                    if (account != null && session.ConnectionState == Cassia.ConnectionState.Active)
                    {
                        // Add it to the list of active connections
                        accList.Add(account);
                    }
                }
            }

            // Return the list of active connections
            return accList;
        }

        private void UploadRdpMetrics(int count)
        {
            // Create a new Cloudwatch client with pre-set access key, secret key and the right region - my EC2 was running in EUCentral1
            var cw = new AmazonCloudWatchClient(_accessKey, _secretKey, RegionEndpoint.EUCentral1);

            // Create a new metric called "RdpConnections"
            var mdList = new List<MetricDatum>
            {
                new MetricDatum
                {
                    MetricName = "RdpConnections",
                    // Add instance ID dimension to allow EC2 control from Cloudwatch alarms
                    Dimensions = new List<Dimension>{new Dimension{Name = "InstanceId", Value = _instanceId},
                    Unit = StandardUnit.Count,
                    Value = count
                }
            };

            // Put it in a "Windows VM custom metrics" Cloudwatch namespace
            var pmdr = new PutMetricDataRequest { MetricData = mdList, Namespace = "Windows VM custom metrics" };
            cw.PutMetricData(pmdr);
        }
    }
}

After that you need to add an installer to your service. Double-click on the Service1.cs file in your Solution Explorer, right-click anywhere in newly opened designer and select “Add installer”:

Left-click “serviceInstaller” and in the “Properties window” set DisplayName and ServiceName properties:

Left-click the “serviceProcessInstaller1” object in Designer, change the “Account” value to “LocalSystem”:

Compile the Release version, copy over the binary files to the EC2 VM. Log-in as administrator and install your service using the following command:

c:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe C:\path_to_service\service_name.exe

Press win+R, type “services.msc”, press Enter. Find your service on the list, right-click, go to “Properties” and change “Startup type” to “Automatic” – that will make sure it starts right when the EC2 instance starts and will be constantly uploading metrics.

Log-in to your AWS console, go to CloudWatch and see if your new metric pops up:

Let’s now set up a CloudWatch alarm that stops the EC2 instance. Click “Alams” on the left sidebar, then the blue “Create alarm” button. Select your new metric, put a nice name and description on enter your desired condition.

I wanted mine to turn off after 30 minutes of inactivity, i.e. with datapoints being uploaded every 5 minutes, that’s when 6 datapoints out of 6 in a row have values equal or less than zero.

I didn’t want it to be triggered when the VM is off, so I’ve set “Treat missing data as” to “missing” – that makes sure that if data is not there it’s not treated as 0 or any other value.

Finally, it’s time to click +EC2 Action to add an action with the following parameters:

Save the changes using the appropriate button.

Let’s disconnect from EC2’s remote desktop, go to Cloudwatch Alarm and see if it triggers after 30 mins and the instance is turned off:

It is! High-five yourself now, please.

Phase 4: Debriefing

Based on AWS cost calculator our t2.medium AWS instance would cost $670 running 24/7 for 12 months. With auto-shutdown that would be no more that $213 a year – so that’s over $400 in your or your employers pocket. Go buy yourself something nice for that. Like a beer. Or 200 beers, cause in the next episode we’ll do some more programming.

What’s next

In the next post we’ll make a website, so we (or other people) can start and stop your EC2 instance without having any AWS credentials. Stay tuned!

Leave a comment

Your email address will not be published. Required fields are marked *

Prev Post