--- AWSTemplateFormatVersion: '2010-09-09' Description: | CloudFormation template that creates a Lambda function that sends a report of unused workspaces to an SNS topic. AWSTemplateFormatVersion: '2010-09-09' Description: | This Cloudformation template will deploy a Event Bridge Scheduled event that will trigger a Lambda function to detect unused Workspaces using the DescribeWorkspacesConnectionStatus API. #“The sample code; software libraries; command line tools; proofs of concept; templates; or other related technology (including any of the foregoing that are provided by our personnel) is provided to you as AWS Content under the AWS Customer Agreement, or the relevant written agreement between you and AWS (whichever applies). You should not use this AWS Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the AWS Content, such as sample code, as appropriate for production grade use based on your specific quality control practices and standards. Deploying AWS Content may incur AWS charges for creating or using AWS chargeable resources, such as running Amazon EC2 instances or using Amazon S3 storage.”. Parameters: UnusedDays: Type: Number Default: 30 MinValue: 7 MaxValue: 90 ConstraintDescription: Set a number of days to report unused WorkSpaces. Values between 7 and 90. Description: After how many days unused WorkSpaces will be reported?. Set the number of days between 7 and 90. ExecutionRate: Type: Number Default: 7 MinValue: 3 MaxValue: 30 ConstraintDescription: Set the number of days to run this solution. Values between 3 and 30. Description: Every how many days do you want to run this solution?. Set the Execution rate between 3 and 30. EmailAddress: ConstraintDescription: Set a valid Email address Description: Set an Email address to receive a report of the unused WorkSpaces AllowedPattern: "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$" Type: String Outputs: LambdaRoleARN: Description: Role for Lambda execution. Value: Fn::GetAtt: - LambdaRole - Arn LambdaFunctionName: Value: Ref: LambdaFunction LambdaFunctionARN: Description: Lambda function ARN. Value: Fn::GetAtt: - LambdaFunction - Arn MyTopicArn: Description: Arn of Created SNS Topic Value: Ref: MySNSTopic Resources: # Lambda role LambdaRole: Type: AWS::IAM::Role Properties: Description: "This role will be used by the Lambda Function to detect unused workspaces" AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: "/" Policies: - PolicyName: UnusedWorkspacesPolicy PolicyDocument: Version: '2012-10-17' Statement: - Sid: DescribeWorkspaces Effect: Allow Action: - workspaces:DescribeWorkspacesConnectionStatus - workspaces:DescribeWorkspaceDirectories Resource: - !Sub arn:aws:workspaces:${AWS::Region}:${AWS::AccountId}:workspace/* - Sid: SendEmail Effect: Allow Action: - sns:Publish Resource: !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:UnusedWorkspacesTopic-${AWS::AccountId}" - Sid: Logs Effect: Allow Action: - logs:CreateLogStream - logs:PutLogEvents Resource: - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/UnusedWorkspaces-${AWS::AccountId}:* - Sid: S3 Effect: Allow Action: - s3:PutObject - s3:PutObjectAcl Resource: - !Sub arn:aws:s3:::unusedworkspaces-${AWS::AccountId}/* # Lambda function LambdaFunction: Type: AWS::Lambda::Function Properties: Description: "Lambda function that sends a report of unused workspaces to an SNS topic." Runtime: python3.9 Environment: Variables: UNUSED_DAYS: Ref: UnusedDays SNS_TOPIC: !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:UnusedWorkspacesTopic-${AWS::AccountId}" BUCKET_NAME: !Sub "unusedworkspaces-${AWS::AccountId}" Role: Fn::GetAtt: - LambdaRole - Arn FunctionName: !Sub "UnusedWorkspaces-${AWS::AccountId}" Handler: index.lambda_handler MemorySize: 168 Timeout: 35 Code: ZipFile: | import boto3 import datetime import os import csv from typing import Any, Dict, Tuple ## Import clientes workspaces_client = boto3.client('workspaces') sns = boto3.client('sns') s3 = boto3.client('s3') ## Get Env variables UNUSED_DAYS = os.environ['UNUSED_DAYS'] SNS_TOPIC = os.environ['SNS_TOPIC'] BUCKET_NAME = os.environ['BUCKET_NAME'] ## Initialize variables ws_data = {} ws_data_unused = {} ct = datetime.datetime.now() def send_report_by_email(report: str, report_unused: str, UNUSED_DAYS: int) -> None: if not report and not report_unused: print ("No unused workspaces to report") return if not report: report = f"There are no unused workspaces in the last {UNUSED_DAYS} days" else: report = "WorkspaceId: Days unused\n"+report if not report_unused: report_unused = "No Workspaces with unknown last usage" else: report_unused = "Amazon Workspaces with unknown last usage\n"+report_unused email_subject = "Report: Unused Amazon Workspaces" email_footer = ( "Amazon workspaces with unknown usage have not been used by their users since they were created.\n" "Information obtained with API WorkspaceConnectionStatus.\n\n" "Regards,\n" "Amazon" ) email_data = ( f"Amazon Workspaces unused for {UNUSED_DAYS} days or more.\n\n" f"{report}\n\n" f"{report_unused}\n\n" f"{email_footer}" ) sns.publish(TopicArn=SNS_TOPIC, Message=email_data, Subject=email_subject) #def create_csv(ws_data_sorted,ws_data_unused): def generate_report_csv(workspaces: Dict[str, int], workspaces_unused: Dict[str, int]) -> None: with open("/tmp/csv_file.csv", "w+") as f: writer = csv.writer(f) writer.writerow(["WorkspaceId", "Days Unused"]) for workspace_id, days_unused in workspaces.items(): writer.writerow([workspace_id, str(days_unused)]) for workspace_id, _ in workspaces_unused.items(): writer.writerow([workspace_id, "not used"]) s3.upload_file("/tmp/csv_file.csv", BUCKET_NAME, f"unused_workspaces_report_{ct.strftime('%b-%d-%Y')}.csv") def generate_report(workspaces: Dict[str, int], workspaces_unused: Dict[str, int]) -> Tuple[str, str]: workspaces = {k: v for k, v in sorted(workspaces.items(), key=lambda item: item[1])} report = "" report_unused = "" for workspace_id, days_unused in workspaces.items(): report += f"{workspace_id}: {days_unused}\n" for workspace_id, _ in workspaces_unused.items(): report_unused += f"{workspace_id}\n" return report, report_unused def get_unused_workspaces(days: int) -> Tuple[Dict[str, int], Dict[str, int]]: workspaces = {} workspaces_unused = {} response = workspaces_client.describe_workspaces_connection_status() details = response["WorkspacesConnectionStatus"] while "NextToken" in response: response = workspaces_client.describe_workspaces_connection_status(NextToken=response["NextToken"]) details.extend(response["WorkspacesConnectionStatus"]) for workspace in details: try: diff = ct - workspace["LastKnownUserConnectionTimestamp"].replace(tzinfo=None) if diff.days >= days: workspaces[workspace["WorkspaceId"]] = diff.days except KeyError: workspaces_unused[workspace["WorkspaceId"]] = -1 return workspaces, workspaces_unused def lambda_handler(event, context) -> None: workspaces, workspaces_unused = get_unused_workspaces(int(UNUSED_DAYS)) report, report_unused = generate_report(workspaces, workspaces_unused) send_report_by_email(report, report_unused, UNUSED_DAYS) generate_report_csv(workspaces, workspaces_unused) Description: Invoke a function during stack creation. TracingConfig: Mode: Active # SNS Topic MySNSTopic: Type: AWS::SNS::Topic Properties: TopicName: !Sub "UnusedWorkspacesTopic-${AWS::AccountId}" DisplayName: Topic to report unused Workspaces Subscription: - Endpoint: Ref: EmailAddress Protocol: email - Endpoint: !GetAtt LambdaFunction.Arn Protocol: lambda # Log Group MyLambdaFunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: Fn::Join: - '' - - "/aws/lambda/" - Ref: LambdaFunction # S3 Bucket to save the Workspaces Report S3Bucket: Type: AWS::S3::Bucket DeletionPolicy: Retain UpdateReplacePolicy: Retain Properties: BucketName: !Sub unusedworkspaces-${AWS::AccountId} BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 # Event Bridge Event UnusedWorkspacesRule: Type: AWS::Events::Rule Properties: Description: "This Event Bridge rule will trigger the Lambda function." ScheduleExpression: !Sub "rate(${ExecutionRate} days)" Targets: - Id: LambdaFunction Arn: Fn::GetAtt: - LambdaFunction - Arn InvokeLambdaPermission: Type: AWS::Lambda::Permission Properties: FunctionName: Fn::GetAtt: - LambdaFunction - Arn Action: lambda:InvokeFunction Principal: events.amazonaws.com SourceArn: Fn::GetAtt: - UnusedWorkspacesRule - Arn