Hibernate Windows AWS EC2 instance using Lambda and System Manager

Posted: April 27, 2018 in Amazon Web Services (AWS), Windows Server

In previous post we configured EC2 instance for System Manager Service and executed command manually against EC2 instance.That’s nice, but we can schedule command execution using Lambda.In this example we’ll schedule powershell command which will check if instance is in idle mode (no RDP connection) and, if yes, instance will be stopped.

In that way we’ll save some money 🙂

First, install System Manager agent on your Windows instance, create IAM System Manager role and assign that role to your instances.Refer to my previous post for more info.

Then add 2 tags:Auto_Stop_Enabled-True and Instance_Used_As_Desktop (so we can filter instances this command will be run against)

2

You can skip these tags creations but also don’t remember to remove is references from Powershell script.

Creating Lambda Function

From AWS console click on Services-Lambda (or just type Lambda in search bar)

2

Click Create function:

2

Enter a name for function-from Role drop-down menu choose create custom role and click Create function

2.PNG

Type a name and click Edit

2

Delete current JSON code and put this one instead:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:DescribeTags",
"ec2:DescribeInstanceAttribute",
"ec2:DescribeInstanceStatus"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": [
"ssm:DescribeDocument",
"ssm:DescribeDocumentParameters",
"ssm:GetDocument",
"ssm:GetParameter"
],
"Resource": [
"arn:aws:ssm:*:*:document/*",
"arn:aws:ssm:*:*:parameter/*"
]
},
{
"Effect": "Allow",
"Action": [
"ssm:ListInventoryEntries",
"ssm:ListDocumentVersions",
"ssm:ListDocuments",
"ssm:SendCommand"
],
"Resource": "*"
}
]
}

After clicking Apply you’ll be redirected back to Create function-Click Create function

2.PNG

Now scroll down until Function code section-right click auto_stop-New folder-name it modules.

2.PNG

Now right click on modules folder-new-folder-give it name controls

2.PNG

Now, under controls folder create a file named index.js

2

Put following code into index.js Hibernation is Log name,AutoStopScript source name, If you don’t want EC2 tags, then remove

{
Name: "tag:Instance_Used_As",
Values: ["Desktop"]
},
{
Name: "tag:Auto_Stop_Enabled",
Values: ["True","true", "Yes", "yes"]
}
]

auto_stop/modules/control/index.js code:

// Import Dependencies
let AWS = require(‘aws-sdk’);

module.exports.getInstanceIds = () => {
return new Promise(
(resolve, reject) => {
let ec2 = new AWS.EC2();
let params = {
Filters: [
{ Name: “instance-state-name”,
Values: [“running”]
},
{
Name: “tag:Instance_Used_As”,
Values: [“Desktop”]
},
{
Name: “tag:Auto_Stop_Enabled”,
Values: [“True”,”true”, “Yes”, “yes”]
}
]
};

ec2.describeInstances(params, (err, data) => {
if (err) reject(err);

let instanceIds = [];
let reservations = “”;

try {
reservations = data.Reservations;
}
catch(err) {
reject(err);
}
if(Array.isArray(reservations)) {
reservations.forEach((reservation) => {
reservation.Instances.forEach((instance) =>{
instanceIds.push(instance.InstanceId);
});
});
if(instanceIds.length >= 1) {
resolve(
{
“InstanceIds”: instanceIds
}
);
}
else {
reject(new Error(“[Error] getInstanceIds: No instances found.”));
}
}
else {
reject(new Error(“[Error] getInstanceIds: Reservations is not an array.”));
}
});
}
);
};

module.exports.hibernateInstances = (controlObj) => {
return new Promise(
(resolve, reject) => {
let ec2 = new AWS.EC2();
let ssm = new AWS.SSM();

let instanceIds = controlObj.InstanceIds;

let params = {
InstanceIds: instanceIds
};

let ssmParams = {
InstanceIds: instanceIds,
DocumentName: “AWS-RunPowerShellScript”,
Parameters: {
“workingDirectory”:[“”],
“executionTimeout”:[“1200”],
“commands”:[
“########################”,
“#### VARIABLES #########”,
“########################”,
“$eventLogName = \”Hibernation\””,
“$eventSourceName = \”AutoStopScript\””,
“$eventLogIds = @{“,
” 1001 = \”Event log initialised.\”;”,
” 1002 = \”No active RDP sessions were found. Proceed to hibernate the instance.\”;”,
” 1003 = \”Active RDP session detected. Abort the hibernation attempt.\”;”,
” 1004 = \”Hibernation is enabled. Preparing for shutdown.\”;”,
” 3001 = \”CPU Usage is higher than the threshold. The hibernation is cancelled.\””,
” 4001 = \”Trouble accessing ‘qwinsta’ executable. Make sure ‘qwinsta’ is in the PATH.\”;”,
” 4002 = \”Trouble accessing ‘powercfg’. Make sure it is in PATH.\”;”,
” 4003 = \”Hibernation was not enabled successfully. Check C:\\ for enough drive space.\”;”,
” 4004 = \”Hibernation attempt failed. Please check if ‘shutdown’ is in PATH.\”;”,
“”,”}”,””,
“$mailMessages = @{“,
” hibernationEnableError = \”Error: Failed to enable hibernation.\”;”,
” hibernationStartError = \”Error: Failed to hibernate an instance.\”;”,”}”,”$minAverageCPU = 20 #If an instance has less than 20% average CPU usage in the period of 5 minutes, it will get shut down.”,””,”########################”,”#### INITIALIZATION ####”,”########################”,
“#### EVENT LOG INIT”,”Try {“,” $null = Get-EventLog -LogName $eventLogName -ErrorAction Stop”,”}”,”Catch {“,” Try {“,” New-EventLog -LogName $eventLogName -Source $eventSourceName -ErrorAction Stop”,” } “,” Catch {“,” #Noop”,” }”,” “,” Write-EventLog -LogName $eventLogName -Source $eventSourceName -EntryType Information -EventId 1001 -Category 1 -Message $eventLogIds.1001″,”}”,””,
“########################”,”#### FUNCTIONS #########”,”########################”,”# # Used to send notifications to the uses.”,”# Function Send-RESTMailMessage {“,”# Param (“,”# $Subject,”,”# $Message,”,”# $Recipient”,”# )”,”# $_headers = New-Object \”System.Collections.Generic.Dictionary[[String],[String]]\””,”# $_headers.Add(\”Content-Type\”, ‘application/json’)”,”# $_headers.Add(\”Cache-Control\”, ‘no-cache’)”,
“# $_headers.Add(\”x-api-key\”, ‘flREiNZkVK2lgGHWHUmxr1VPP8GIfLTz7uVH6eKz’)”,””,”# $body = @{“,”# subject=$Subject”,”# message=$Message”,”# auth_key=’sDfFXAk421412DSAkxKLaksdKASdFG'”,”# recipients=@($Recipient)”,”# }”,””,”# $body = $body | ConvertTo-JSON”,””,”# $response = Invoke-RestMethod -Uri \”https://ekiss3x6gl.execute-api.us-east-1.amazonaws.com/v1/notifications/system\” -Method Post -Headers $headers -Body $body”,”# return $response”,”# }”,
“”,”Function Get-ActiveRDPSessions {“,” # Check for any active RDP sessions.”,” # This function relies on \”qwinsta\” tool which comes bundled with Windows.p”,” # The function returns \”true\” if there are active RDP sessions or \”false\” if there aren’t any.”,” Param (“,” $EventLogName,”,” $EventSourceName,”,” $EventIds”,” )”,” Try {“,” $_allSessions = qwinsta”,” }”,” Catch {“,” Write-EventLog -LogName $EventLogName -Source $EventSourceName -EntryType Error -EventId 4001 -Category 4 -Message $EventIds.4001″,” return 1″,” }”,” “,” ForEach($_s in $_allSessions) {“,” If($_s -match \”rdp\” -and $_s -match \”Active\”) {“,” Write-EventLog -LogName $EventLogName -Source $eventSourceName -EntryType Information -EventId 1003 -Category 1 -Message $EventIds.1003″,” return \”ActiveSessionFound\””,” }”,” }”,” “,” Write-EventLog -LogName $EventLogName -Source $eventSourceName -EntryType Information -EventId 1002 -Category 1 -Message $EventIds.1002″,” return \”NoActiveSessionFound\””,”}”,””,”Function Enable-Hibernation {“,” Param (“,” $EventLogName,”,” $EventSourceName,”,” $EventIds”,” )”,””,” Try {“,” $_process = Start-Process powercfg -ArgumentList \”/h\”, \”on\” -PassThru -ErrorAction Stop”,” Start-Sleep -Seconds 3″,” }”,” Catch {“,” Write-EventLog -LogName $EventLogName -Source $EventSourceName -EntryType Error -EventId 4002 -Category 4 -Message $EventIds.4002″,” return 1″,” }”,””,” If ($_process.ExitCode -eq 0) {“,” Write-EventLog -LogName $EventLogName -Source $EventSourceName -EntryType Information -EventId 1004 -Category 1 -Message $EventIds.1004″,” return \”Enabled\””,””,” } “,” Else {“,” Write-EventLog -LogName $EventLogName -Source $EventSourceName -EntryType Error -EventId 4003 -Category 4 -Message $EventIds.4003″,” return 1″,” }”,”}”,””,”Function Start-Hibernation {“,” Param (“,” $EventLogName,”,” $EventSourceName,”,” $EventIds”,” )”,””,” Try {“,” $_process = Start-Process shutdown -ArgumentList \”/h\” -PassThru -ErrorAction Stop”,” }”,” Catch {“,” Write-EventLog -LogName $EventLogName -Source $EventSourceName -EntryType Error -EventId 4004 -Category 4 -Message $EventIds.4004″,” return 1″,” }”,”}”,””,”Function Check-CPUUsage {“,” $_samples = 5″,” $_intervalSeconds = 60″,” $_cpuLoadAverage = 0″,” For($i = 0; $i -ne $_samples; $i++) {“,” $_cpuLoadAverage += (Get-WmiObject win32_processor).LoadPercentage”,” Start-Sleep -Seconds $_intervalSeconds”,” }”,””,” return $_cpuLoadAverage / $_samples “,”}”,””,””,”########################”,”#### MAIN ##############”,”########################”,”$rdpSessionStatus = Get-ActiveRDPSessions -EventLogName $eventLogName -EventSourceName $eventSourceName -EventIds $eventLogIds”,””,”If ($rdpSessionStatus -eq \”NoActiveSessionFound\”) {“,” #Enable Hibernation”,” Enable-Hibernation -EventLogName $eventLogName -EventSourceName $eventSourceName -EventIds $eventLogIds”,” “,” If(Check-CPUUsage -lt $minAverageCPU) {“,””,””,” # TODO Notify User”,” # TODO Wait 10 minutes”,” # TODO Check for active RDP sessions again”,””,” #Hibernate”,” Start-Hibernation -EventLogName $eventLogName -EventSourceName $eventSourceName -EventIds $eventLogIds”,” }”,” Else {“,” Write-EventLog -LogName $EventLogName -Source $EventSourceName -EntryType Warning -EventId 3001 -Category 3 -Message $EventIds.3001″,” return 0″,” }”,”} “,”Else {“,” return 0″,”}”
]
},
MaxErrors: “0”,
TimeoutSeconds: 120
}

// Hibernate instances
ssm.sendCommand(ssmParams, function(err, data) {
if (err) reject(err);
resolve(data);
});
resolve(instanceIds);
console.log(instanceIds);
}
);
};

 

Now click on “parent” index.js (under auto_stop only)

 

2.PNG

paste following code (change AWS zone to fit your needs):

 

// Global Module Imports
let AWS = require(‘aws-sdk’);
AWS.config.region = “eu-west-1”;
let controls = require(“./modules/controls”);

// This is the function AWS Lambda will execute.
exports.handler = (event, context, callback) => {

// Execute the power control
controls.getInstanceIds()
.then(controls.hibernateInstances)
.catch((err) => {
callback(err);
});
};

At the end it should be like this:

 

2.PNG

 

Now, we need to schedule our function:

in Add trigger section click CloudWatch Events

 

2.PNG

Give it name and set schedule

 

You can use the following sample cron strings when creating a rule with schedule.

Minutes Hours Day of month Month Day of week Year Meaning
0 10 * * ? * Run at 10:00 am (UTC) every day
15 12 * * ? * Run at 12:15 pm (UTC) every day
0 18 ? * MON-FRI * Run at 6:00 pm (UTC) every Monday through Friday
0 8 1 * ? * Run at 8:00 am (UTC) every 1st day of the month
0/15 * * * ? * Run every 15 minutes
0/10 * ? * MON-FRI * Run every 10 minutes Monday through Friday
0/5 8-17 ? * MON-FRI * Run every 5 minutes Monday through Friday between 8:00 am and 5:55 pm (UTC)

 

In this example i set it to run every 20 minutes.

 

2.PNG

 

It should be something like this:

 

2

When you click on run_auto_stop_script you’ll get following picture:

 

2

 

If your instance is idle, it should be stopped. You can check it from AWS console-EC2-SYSTEM-MANAGER SHARED RESOURCES-Run Command

3.PNG

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s