Automating installation and removal of MSP360 (CloudBerry) Drive for EC2 scaling
Problem
If you have an EC2 Auto Scaling group, want to use MSP360 (CloudBerry) Drive inside each managed instance, and have a license key for multiple CB Drive installations, you may want to automate CB Drive installation on newly created instances and properly reclaim the key when terminating them.
Suggestions and Resolution
The suggested approach is to use EC2 LifeCycle hooks that will execute routines on instances and wait for completion. Lifecycle hooks send an AWS SNS notification, and an AWS Lambda function is triggered to perform appropriate action. The Lambda function in turn executes a PowerShell script inside the instance. You can store your PowerShell scripts and CB Drive instance in any HTTP location. In our example they are stored in S3 https://s3.us-east-2.amazonaws.com/cb-drive/, please replace that path with your own location. CB Drive licensing information is stored as an AWS SSM parameter.
Preparing the environment
You need to have an AutoScaling group with 0 instances running. If you are going to add automation to an existing ASG, keep in mind that the drive will not be installed into running instances. You will need to create the ASG on your own; that topic is not covered in this article. In the code below, the ASG name is CBDriveASG. You will need to change it to your name.
Configuring the environment
Create an SNS topic named CBDrive:
aws sns create-topic --name CBDrive
The output should look something like this:
{
"TopicArn": "arn:aws:sns:us-east-2:1234567890:CBDrive"
}
Remember the ARN for the topic; you'll need it later.
Create the IAM role CBDriveHooksRole that will allow lifecycle hooks to publish messages to the SNS topic CBDrive:
- Create a file AutoscaleAssumeRole.json and enter the following content. This will allow Auto Scaling to assume IAM roles.
{
"Version": "2012-10-17",
"Statement": [ {
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "autoscaling.amazonaws.com"
},
"Action": "sts:AssumeRole"
} ]
}
- Then create a policy file CBDrive.json and allow publishing to the SNS topic CBDrive. You will need to change the ARN in the example to the ARN of your topic.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Resource": "*",
"Action": [
"sns:Publish"
]
}
]
}
- Finally, create the IAM role:
aws iam create-role \
--role-name CBDriveHooksRole \
--assume-role-policy-document file://AutoscaleAssumeRole.json
Ouput:
{
"Role": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "autoscaling.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
},
"RoleId": "AROAI16U3N2KRKUOCTJ4C",
"CreateDate": "2017-07-13T15:56:09.043Z",
"RoleName": "CBDriveHooksRole",
"Path": "/",
"Arn": "arn:aws:iam::1234567890:role/CBDriveHooksRole"
}
}
- And attach the inline policy
aws iam put-role-policy \
--role-name CBDriveHooksRole \
--policy-name AllowPublishCBDriveTopic \
--policy-document file://CBDrive.json
No output is expected.
Create IAM role that allow execution for Lambda functions
- Create a file LambdaRole.json
{
"Version": "2012-10-17",
"Statement": [ {
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
} ]
}
- And the policy file LambdaPolicy.json
{
"Version": "2012-10-17",
"Statement": [ {
"Effect": "Allow",
"Resource": "*",
"Action": [
"logs:*",
"ssm:*",
"autoscaling:CompleteLifecycleAction"
]
},
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*",
"Effect": "Allow"
} ]
}
- Create the IAM role
aws iam create-role \
--role-name LambdaCBDRole \
--assume-role-policy-document file://LambdaRole.json
Output:
{
"Role": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
},
"RoleId": "AROAIZWVMVZR36MN9OXZS",
"CreateDate": "2017-07-13T16:00:52.378Z",
"RoleName": "LambdaCBDRole",
"Path": "/",
"Arn": "arn:aws:iam::1234567890:role/LambdaCBDRole"
}
}
- And attach the policy
aws iam put-role-policy \
--role-name LambdaCBDRole \
--policy-name ExecuteCBDriveAutomation \
--policy-document file://LambdaPolicy.json
No output is expected.
Put lifecycle hooks
- For pushing CB Drive to a newly deployed EC2 instance:
aws autoscaling put-lifecycle-hook --lifecycle-hook-name "Install-CBDrive" \
--auto-scaling-group-name “CBDriveASG” \
--lifecycle-transition "autoscaling:EC2_INSTANCE_LAUNCHING" \
--role-arn arn:aws:iam::1234567890:role/CBDriveHooksRole \
--notification-target-arn arn:aws:sns:us-east-2:1234567890:CBDrive \
--heartbeat-timeout 1800 \
--default-result 'CONTINUE'
- And for reclaiming the license:
aws autoscaling put-lifecycle-hook --lifecycle-hook-name "Remove-CBDrive" \
--auto-scaling-group-name "CBDriveASG" \
--lifecycle-transition "autoscaling:EC2_INSTANCE_TERMINATING" \
--role-arn arn:aws:iam::1234567890:role/CBDriveHooksRole \
--notification-target-arn arn:aws:sns:us-east-2:1234567890:CBDrive \
--heartbeat-timeout 1800 --default-result 'CONTINUE'
Create the Lambda function
Here’s the code of our Lambda function:
import boto3
import time
import json
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def getParameter(param_name):
# Create the SSM Client
ssm = boto3.client('ssm')
# Get the requested parameter
response = ssm.get_parameters(
Names=[
param_name,
],
WithDecryption=False
)
# Store the credentials in a variable
credentials = response['Parameters'][0]['Value']
return credentials
def scaleUp(intInstanceId):
ssmClient = boto3.client('ssm')
license = dict(item.split("=") for item in getParameter('CBBDriveLicense').split(","))
ssmCommand = ssmClient.send_command(
InstanceIds = [
intInstanceId
],
DocumentName = 'AWS-RunPowerShellScript',
TimeoutSeconds = 240,
Comment = 'Install MSP360 (CloudBerry) Drive',
Parameters = {
'commands': [
'Invoke-WebRequest https://s3.us-east-2.amazonaws.com/cb-drive/installDrive.ps1 -outfile c:/installDrive.ps1','C:/installDrive.ps1 -license ' + license["license"] + ' -email ' + license["email"]
]
},
)
#poll SSM until EC2 Run Command completes
status = 'Pending'
while status == 'Pending' or status == 'InProgress':
time.sleep(3)
status = (ssmClient.list_commands(CommandId=ssmCommand['Command']['CommandId']))['Commands'][0]['Status']
if(status != 'Success'):
logger.info("Failed to install CB Dirve with status " + status)
else:
logger.info("CB Drive installed")
return
def scaleDown(intInstanceId):
ssmClient = boto3.client('ssm')
ssmCommand = ssmClient.send_command(
InstanceIds = [
intInstanceId
],
DocumentName = 'AWS-RunPowerShellScript',
TimeoutSeconds = 240,
Comment = 'Remove MSP360 (CloudBerry) Drive',
Parameters = {
'commands': [
'Invoke-WebRequest https://s3.us-east-2.amazonaws.com/cb-drive/removeDrive.ps1 -outfile c:/removeDrive.ps1','C:/removeDrive.ps1'
]
},
)
#poll SSM until EC2 Run Command completes
status = 'Pending'
while status == 'Pending' or status == 'InProgress':
time.sleep(3)
status = (ssmClient.list_commands(CommandId=ssmCommand['Command']['CommandId']))['Commands'][0]['Status']
if(status != 'Success'):
logger.info("Failed to remove CB Drive with status" + status)
else:
logger.info("CB Drive removed")
return
#The lambda_handler Python function gets called when you run your AWS Lambda function.
def lambda_handler(event, context):
s3Client = boto3.client('s3')
snsClient = boto3.client('sns')
asClient = boto3.client('autoscaling')
ecClient = boto3.client('ec2')
message = json.loads(event[u'Records'][0][u'Sns'][u'Message'])
logger.info(json.dumps(event))
if message['LifecycleTransition'] == 'autoscaling:EC2_INSTANCE_TERMINATING':
scaleDown(message['EC2InstanceId'])
elif message['LifecycleTransition'] == 'autoscaling:EC2_INSTANCE_LAUNCHING':
time.sleep(60)
scaleUp(message['EC2InstanceId'])
else:
logger.info("Unknow state " + eventType)
response = asClient.complete_lifecycle_action(
LifecycleHookName=message['LifecycleHookName'],
AutoScalingGroupName=message['AutoScalingGroupName'],
LifecycleActionToken=message['LifecycleActionToken'],
LifecycleActionResult='CONTINUE',
InstanceId=message['EC2InstanceId']
)
return
Save it to the ManageCBDrive.py file and compress it into the ManageCBDrive.zip archive. Then create the Lambda function in AWS:
aws lambda create-function \
--function-name ManageCBDrive \
--zip-file fileb://ManageCBDrive-1.0.zip \
--runtime python3.6 \
--role arn:aws:iam::1234567890:role/LambdaCBDRole \
--handler ManageCBDrive.lambda_handler \
--timeout 300
Output:
{
"TracingConfig": {
"Mode": "PassThrough"
},
"CodeSha256": "HuG0UIv3s2QzX4RB1Hokq5tn38EZV3LzVHCMyc+Xb9o=",
"FunctionName": "ManageCBDrive",
"CodeSize": 1349,
"MemorySize": 128,
"FunctionArn": "arn:aws:lambda:us-east-2:1234567890:function:ManageCBDrive",
"Version": "$LATEST",
"Role": "arn:aws:iam::1234567890:role/LambdaCBDRole",
"Timeout": 300,
"LastModified": "2017-07-13T16:56:07.747+0000",
"Handler": "ManageCBDrivelambda_handler",
"Runtime": "python3.6",
"Description": ""
}
Subscribe the Lambda function to the SNS topic
aws sns subscribe --protocol lambda \
--topic-arn arn:aws:sns:us-west-2:1234567890:CBDrive \
--notification-endpoint arn:aws:lambda:us-west-2:1234567890:function:ManageCBDrive
Output:
{
"SubscriptionArn": "arn:aws:sns:us-east-2:988585708477:CBDrive:7527c5b4-5282-47b7-bed3-38943a78f64c"
}
Grant permissions on the lambda function to the SNS topic:
aws lambda add-permission \
--function-name ManageCBDrive \
--statement-id 1 \
--action "lambda:InvokeFunction" \
--principal sns.amazonaws.com \
--source-arn arn:aws:sns:us-west-2:1234567890:CBDrive
Output:
{
"Statement": "{\"Sid\":\"1\",\"Resource\":\"arn:aws:lambda:us-east-2:1234567890:function:ManageCBDrive\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"sns.amazonaws.com\"},\"Action\":[\"lambda:InvokeFunction\"],\"Condition\":{\"ArnLike\":{\"AWS:SourceArn\":\"arn:aws:sns:us-east-2:1234567890:CBDrive\"}}}"
}
Create AWS SMS parameter and store licensing information there
aws ssm put-parameter --name CBBDriveLicense \
--type String \
--value "license=XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX,email=yourame@yourdomain.com"
Upload PowerShell script and CB Drive distro to your HTTP share
installDrive.ps1:
param (
[Parameter (Mandatory=$True)][string]$license,
[Parameter (Mandatory=$True)][string]$email
)
New-Item -ItemType Directory -Force -Path c:\bootstrap
$url="http://s3.amazonaws.com/cb_currentreleases/Drive/setup.exe"
$output="c:\bootstrap\setup.exe"
Invoke-WebRequest $url -outfile $output
$install_command=$output
$install_arguments= " /S"
start-process -FilePath $install_command -ArgumentList $install_arguments -Wait
$activate_command="C:\Program Files\CloudBerryLab\CloudBerry Drive\cbd.exe"
$activate_arguments= '/activatelicense' + ' -k "'+$license+'" -e "'+$email+'"'
Start-Process -FilePath $activate_command -ArgumentList $activate_arguments -Wait
Remove-Item -Path $output
removeDrive.ps1:
$release_command="c:\Program Files\CloudBerryLab\CloudBerry Drive\cbd.exe"
$release_arguments= " /silent /releaselicense"
start-process -FilePath $release_command -ArgumentList $release_arguments -Wait
$uninstall_command="c:\Program Files\CloudBerryLab\CloudBerry Drive\uninst.exe"
$uninstall_arguments=" /S"
Start-Process -FilePath $uninstall_command -ArgumentList $uninstall_arguments -Wait
Remove-Item -Path C:\removeDrive.ps1