AWSTemplateFormatVersion: 2010-09-09 Description: This template creates a cluster to provide high availability. (qs-1r2g4122s) Parameters: AgentAsgName: Type: String Default: '' Description: Name of agents autoscaling group ServerAsgName: Type: String Description: Name of servers autoscaling group AsRobotsAsgName: Type: String Description: Name of AS Robots autoscaling group GpuAgentInstanceId: Type: String Default: '' Description: Instance ID of the dedicated GPU node TMAgentInstanceId: Type: String Default: '' Description: Instance ID of the dedicated TM node InstanceAMIImageNameSSMParameter: Type: String Default: '' Description: Name of the SSM Parameter that holds the Image Name used at deploy time InstanceAMIIdSSMParameter: Type: String Default: '' Description: Name of the SSM Parameter that stores the AMI id InstanceIamRoleArn: Type: String Description: IAM Role ARN used by the EC2 instances ExistingIamRole: Type: String Description: Chose true to use an existing IAM role Default: 'false' AllowedValues: - 'true' - 'false' AgentLaunchTemplateName: Type: String Default: '' Description: Name of agent autoscaling group launch template ASRobotsLaunchTemplateName: Type: String Description: AS robots agent AutoScalingGroup Launch Template name ServerLaunchTemplateName: Type: String Description: Name of servers autoscaling group launch template UseExternalOrchestrator: Description: >- Choose true to connect AiCenter to an external Orchestrator Type: String Default: 'false' AllowedValues: - 'true' - 'false' QSS3BucketName: AllowedPattern: '^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$' ConstraintDescription: >- Quick Start bucket name can include numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-). Description: Name of the S3 bucket for your copy of the Quick Start assets. Do not modify. Type: String Default: uipath-s3-quickstart QSS3BucketRegion: Default: us-east-1 Description: >- AWS Region where the Quick Start S3 bucket (QSS3BucketName) is hosted. Do not modify. Type: String AllowedPattern: (us(-gov)?|ap|ca|cn|eu|sa)-(central|(north|south)?(east|west)?)-\d ConstraintDescription: Must be a valid AWS Region code. QSS3KeyPrefix: AllowedPattern: '^[0-9a-zA-Z-/]*/$' ConstraintDescription: >- The Quick Start S3 key prefix can include numbers, lowercase letters, uppercase letters, hyphens (-), and forward slashes (/). Default: aws-quickstart-sf/ Description: >- S3 key prefix that is used to simulate a directory for your copy of the Quick Start assets. Do not modify. Type: String Conditions: UsingDefaultBucket: !Equals - !Ref QSS3BucketName - uipath-s3-quickstart IsMultiNode: !Not [ !Equals [ !Ref AgentAsgName, '' ] ] UseExternalOrch: !Equals [ !Ref UseExternalOrchestrator, 'true' ] DeployIam: !Equals [!Ref ExistingIamRole, 'false'] DeployEventsBridgeRole: !And [!Condition IsMultiNode, !Condition DeployIam] FindingAmiAtDeploy: !Not [ !Equals [!Ref InstanceAMIImageNameSSMParameter, ''] ] Resources: # CloudWatch log group for logging events from the SSM automation LifecycleAutomationLogs: Type: AWS::Logs::LogGroup Condition: IsMultiNode Properties: LogGroupName: !Sub "/uipath/instance-lifecycle-ssm/${AWS::StackName}" RetentionInDays: 30 # CloudWatch log group for logging eventsfor cluster operations ClusterOperationsAutomationLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/uipath/cluster-operations-ssm/${AWS::StackName}" RetentionInDays: 30 AsRobotsAsgLifeCycleHookTerminating: Type: 'AWS::AutoScaling::LifecycleHook' Condition: IsMultiNode Properties: AutoScalingGroupName: !Ref AsRobotsAsgName LifecycleTransition: 'autoscaling:EC2_INSTANCE_TERMINATING' HeartbeatTimeout: 7200 DefaultResult: CONTINUE AgentAsgLifeCycleHookTerminating: Type: 'AWS::AutoScaling::LifecycleHook' Condition: IsMultiNode Properties: AutoScalingGroupName: !Ref AgentAsgName LifecycleTransition: 'autoscaling:EC2_INSTANCE_TERMINATING' HeartbeatTimeout: 7200 DefaultResult: CONTINUE ServerAsgLifeCycleHookTerminating: Type: 'AWS::AutoScaling::LifecycleHook' Condition: IsMultiNode Properties: AutoScalingGroupName: !Ref ServerAsgName LifecycleTransition: 'autoscaling:EC2_INSTANCE_TERMINATING' HeartbeatTimeout: 7200 DefaultResult: CONTINUE AutomationAssumeRole: Type: AWS::IAM::Role Condition: DeployIam Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - ssm.amazonaws.com - states.amazonaws.com Action: sts:AssumeRole Policies: - PolicyDocument: Version: '2012-10-17' Statement: - Action: - "autoscaling:CompleteLifecycleAction" - "autoscaling:RecordLifecycleActionHeartbeat" - "autoscaling:UpdateAutoScalingGroup" - "ssm:SendCommand" Resource: - !Sub "arn:${AWS::Partition}:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/${ServerAsgName}" - !Sub "arn:${AWS::Partition}:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/${AsRobotsAsgName}" - !If - IsMultiNode - !Sub "arn:${AWS::Partition}:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/${AgentAsgName}" - !Ref AWS::NoValue Effect: Allow PolicyName: allow-asg-instances-operations - PolicyDocument: Version: '2012-10-17' Statement: - Action: - "autoscaling:DescribeAutoScalingGroups" Resource: "*" Effect: Allow PolicyName: allow-asg-describe - PolicyDocument: Version: '2012-10-17' Statement: - Action: - "ssm:PutParameter" - "ssm:GetParameter" Resource: - !Sub "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${InstanceAMIIdSSMParameter}" - !If - FindingAmiAtDeploy - !Sub "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${InstanceAMIImageNameSSMParameter}" - !Ref AWS::NoValue Effect: Allow PolicyName: allow-parameter-store-update - PolicyDocument: Version: '2012-10-17' Statement: - Action: - logs:PutLogEvents - logs:DescribeLogStreams - logs:DescribeLogGroups - logs:CreateLogStream - logs:CreateLogGroup Resource: - !If - IsMultiNode - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:${LifecycleAutomationLogs}*" - !Ref AWS::NoValue - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:${ClusterOperationsAutomationLogs}*" Effect: Allow PolicyName: allow-logs-access - PolicyDocument: Version: '2012-10-17' Statement: - Action: - ec2:DescribeImages - ec2:DescribeLaunchTemplates - ec2:DescribeLaunchTemplateVersions Resource: "*" Effect: Allow PolicyName: allow-ec2-describe-actions - PolicyDocument: Version: '2012-10-17' Statement: - Action: - iam:PassRole Resource: !Ref InstanceIamRoleArn Effect: Allow PolicyName: allow-pass-as-iam-role - PolicyDocument: Version: '2012-10-17' Statement: - Action: - ec2:CreateLaunchTemplateVersion - ec2:RunInstances Resource: - !Sub "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:launch-template/${ServerLaunchTemplateName}" - !Sub "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:launch-template/${ASRobotsLaunchTemplateName}" - !If - IsMultiNode - !Sub "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:launch-template/${AgentLaunchTemplateName}" - !Ref AWS::NoValue Effect: Allow PolicyName: allow-launch-template-actions - PolicyDocument: Version: '2012-10-17' Statement: - Action: - states:StartExecution Resource: - !Ref OnDemandRestoreStateMachine Effect: Allow PolicyName: allow-step-function-execution - PolicyDocument: Version: '2012-10-17' Statement: - Action: - states:DescribeExecution Resource: - !Sub "arn:${AWS::Partition}:states:${AWS::Region}:${AWS::AccountId}:execution:${OnDemandRestoreStateMachine.Name}:*" Effect: Allow PolicyName: allow-step-function-read Path: / ManagedPolicyArns: - !Sub 'arn:${AWS::Partition}:iam::aws:policy/AmazonSSMFullAccess' StateMachinesAssumeRole: Type: AWS::IAM::Role Condition: DeployIam Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - states.amazonaws.com Action: sts:AssumeRole Path: / ManagedPolicyArns: - !Sub 'arn:${AWS::Partition}:iam::aws:policy/AmazonSSMFullAccess' EventsBridgeAssumeRole: Type: AWS::IAM::Role Condition: DeployEventsBridgeRole Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - events.amazonaws.com Action: sts:AssumeRole Policies: - PolicyDocument: Version: '2012-10-17' Statement: - Action: - "ssm:StartautomationExecution" Resource: - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${ServerRemoveInstanceDocument}* - !If - IsMultiNode - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${AgentRemoveInstanceDocument}* - !Ref AWS::NoValue Effect: Allow PolicyName: startSsmExecution - PolicyDocument: Version: '2012-10-17' Statement: - Action: - "iam:PassRole" Resource: !GetAtt AutomationAssumeRole.Arn Effect: Allow PolicyName: allow-all Path: / AgentRemoveInstanceDocument: Metadata: cfn-lint: config: ignore_checks: - E9101 ignore_reasons: E9101: Abort is a keyword Type: AWS::SSM::Document Condition: IsMultiNode Properties: DocumentType: Automation UpdateMethod: NewVersion Content: description: UiPath Automation Suite - remove agent instance from cluster schemaVersion: '0.3' assumeRole: !If [DeployIam, !GetAtt AutomationAssumeRole.Arn, !Ref InstanceIamRoleArn] parameters: InstanceId: type: String AutoScalingGroupName: type: String LifecycleHookName: type: String mainSteps: - name: "RemoveInstance" action: "aws:runCommand" onFailure: Abort inputs: DocumentName: "AWS-RunRemoteScript" InstanceIds: - "{{ InstanceId }}" CloudWatchOutputConfig: CloudWatchOutputEnabled: "true" CloudWatchLogGroupName: !Ref LifecycleAutomationLogs Parameters: sourceType: "S3" sourceInfo: !Sub - '{"path": "https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}scripts/remove-agent.sh"}' - S3Bucket: !If - UsingDefaultBucket - !Sub '${QSS3BucketName}-${AWS::Region}' - !Ref QSS3BucketName S3Region: !If - UsingDefaultBucket - !Ref AWS::Region - !Ref QSS3BucketRegion commandLine: "remove-agent.sh" - name: "TerminateTheInstance" action: "aws:executeAwsApi" inputs: LifecycleHookName: "{{ LifecycleHookName }}" InstanceId: "{{ InstanceId }}" AutoScalingGroupName: "{{ AutoScalingGroupName }}" Service: "autoscaling" Api: "CompleteLifecycleAction" LifecycleActionResult: "CONTINUE" ServerRemoveInstanceDocument: Metadata: cfn-lint: config: ignore_checks: - E9101 ignore_reasons: E9101: Abort is a keyword Type: AWS::SSM::Document Condition: IsMultiNode Properties: DocumentType: Automation UpdateMethod: NewVersion Content: description: UiPath Automation Suite - remove agent instance from cluster schemaVersion: '0.3' assumeRole: !If [DeployIam, !GetAtt AutomationAssumeRole.Arn, !Ref InstanceIamRoleArn] parameters: InstanceId: type: String AutoScalingGroupName: type: String LifecycleHookName: type: String mainSteps: - name: "RemoveInstance" action: "aws:runCommand" onFailure: Abort inputs: DocumentName: "AWS-RunRemoteScript" InstanceIds: - "{{ InstanceId }}" CloudWatchOutputConfig: CloudWatchOutputEnabled: "true" CloudWatchLogGroupName: !Ref LifecycleAutomationLogs Parameters: sourceType: "S3" sourceInfo: !Sub - '{"path": "https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}scripts/remove-server.sh"}' - S3Bucket: !If - UsingDefaultBucket - !Sub '${QSS3BucketName}-${AWS::Region}' - !Ref QSS3BucketName S3Region: !If - UsingDefaultBucket - !Ref AWS::Region - !Ref QSS3BucketRegion commandLine: "remove-server.sh" - name: "TerminateTheInstance" action: "aws:executeAwsApi" inputs: LifecycleHookName: "{{ LifecycleHookName }}" InstanceId: "{{ InstanceId }}" AutoScalingGroupName: "{{ AutoScalingGroupName }}" Service: "autoscaling" Api: "CompleteLifecycleAction" LifecycleActionResult: "CONTINUE" OnDemandRestoreStateMachine: Type: AWS::StepFunctions::StateMachine Properties: DefinitionS3Location: Bucket: !If - UsingDefaultBucket - !Sub '${QSS3BucketName}-${AWS::Region}' - !Ref QSS3BucketName Key: !Sub "${QSS3KeyPrefix}state_machines/on_demand_restore.asl.json" DefinitionSubstitutions: AutomationLogs: !Ref ClusterOperationsAutomationLogs S3ArtifactsPath: !Sub - "https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}" - S3Bucket: !If - UsingDefaultBucket - !Sub '${QSS3BucketName}-${AWS::Region}' - !Ref QSS3BucketName S3Region: !If - UsingDefaultBucket - !Ref AWS::Region - !Ref QSS3BucketRegion RoleArn: !If [DeployIam, !GetAtt StateMachinesAssumeRole.Arn, !Ref InstanceIamRoleArn] RegisterAiCenter: Metadata: cfn-lint: config: ignore_checks: - E9101 ignore_reasons: E9101: Abort is a keyword Type: AWS::SSM::Document Condition: UseExternalOrch Properties: DocumentType: Automation UpdateMethod: NewVersion Content: description: UiPath Automation Suite - register to Orchestrator schemaVersion: '0.3' assumeRole: !If [DeployIam, !GetAtt AutomationAssumeRole.Arn, !Ref InstanceIamRoleArn] parameters: IdentityToken: type: String description: 'Token used to communicate with Identity.' mainSteps: - name: "getFirstInstance" action: "aws:executeAwsApi" onFailure: Abort inputs: Service: autoscaling Api: DescribeAutoScalingGroups AutoScalingGroupNames: - !Sub "${ServerAsgName}" outputs: - Name: firstServerInstanceId Selector: $.AutoScalingGroups[0].Instances[0].InstanceId Type: String - name: "registerAiCenter" action: "aws:runCommand" onFailure: Abort inputs: DocumentName: "AWS-RunShellScript" InstanceIds: - "{{ getFirstInstance.firstServerInstanceId }}" CloudWatchOutputConfig: CloudWatchOutputEnabled: "true" CloudWatchLogGroupName: !Ref ClusterOperationsAutomationLogs Parameters: workingDirectory: '/root' executionTimeout: '43200' commands: - '/root/installer/configureUiPathAS.sh aicenter configure --installation-token "{{ IdentityToken }}"' OnDemandBackup: Metadata: cfn-lint: config: ignore_checks: - E9101 ignore_reasons: E9101: Abort is a keyword Type: AWS::SSM::Document Properties: DocumentType: Automation UpdateMethod: NewVersion Content: description: UiPath Automation Suite - create a snapshot of the Automation Suite cluster schemaVersion: '0.3' assumeRole: !If [DeployIam, !GetAtt AutomationAssumeRole.Arn, !Ref InstanceIamRoleArn] mainSteps: - name: "getFirstInstance" action: "aws:executeAwsApi" onFailure: Abort inputs: Service: autoscaling Api: DescribeAutoScalingGroups AutoScalingGroupNames: - !Sub "${ServerAsgName}" outputs: - Name: firstServerInstanceId Selector: $.AutoScalingGroups[0].Instances[0].InstanceId Type: String - name: "captureOnDemandBackup" action: "aws:runCommand" onFailure: Abort inputs: DocumentName: "AWS-RunShellScript" InstanceIds: - "{{ getFirstInstance.firstServerInstanceId }}" CloudWatchOutputConfig: CloudWatchOutputEnabled: "true" CloudWatchLogGroupName: !Ref ClusterOperationsAutomationLogs Parameters: workingDirectory: '/root' executionTimeout: '43200' commands: - 'timestamp=$(date +%Y%m%d%H%M%S)' - '/root/installer/configureUiPathAS.sh snapshot backup create "asbackup-ondemand-$timestamp" --wait' OnDemandRestoreDocument: Metadata: cfn-lint: config: ignore_checks: - E9101 ignore_reasons: E9101: Abort is a keyword Type: AWS::SSM::Document Properties: DocumentType: Automation UpdateMethod: NewVersion Content: description: UiPath Automation Suite - restores the Automation Suite cluster from a given snapshot schemaVersion: '0.3' assumeRole: !If [DeployIam, !GetAtt AutomationAssumeRole.Arn, !Ref InstanceIamRoleArn] parameters: SnapshotName: type: String description: 'Snapshot used for the restore.' mainSteps: - name: "computeRestoreStepInput" action: 'aws:executeScript' timeoutSeconds: 600 maxAttempts: 1 onFailure: Abort outputs: - Name: restoreInput Selector: $.Payload.restoreInput Type: String - Name: firstServer Selector: $.Payload.firstServer Type: String inputs: Runtime: python3.8 Handler: get_nodes_information InputPayload: ServerAsgName: !Sub '${ServerAsgName}' AgentAsgName: !Sub '${AgentAsgName}' AsRobotsAsgName: !Sub '${AsRobotsAsgName}' GpuNode: !Sub '${GpuAgentInstanceId}' TmNode: !Sub '${TMAgentInstanceId}' RegionName: !Sub '${AWS::Region}' SnapshotName: '{{ SnapshotName }}' Script: | from __future__ import print_function import json import boto3 def get_instance_ids_from_asg(asg_client, asg_name): if not asg_name: return [] asg_query = asg_client.describe_auto_scaling_groups(AutoScalingGroupNames=[asg_name]) if not len(asg_query['AutoScalingGroups']) > 0: raise Exception(f'ASG {asg_name} not found') return [instance['InstanceId'] for instance in asg_query['AutoScalingGroups'][0]['Instances']] def get_nodes_information(event, context): print("Received event: " + json.dumps(event, indent=2)) region = event["RegionName"] server_asg_name = event["ServerAsgName"] agent_asg_name = event["AgentAsgName"] as_robots_asg_name = event["AsRobotsAsgName"] gpu_node_id = event["GpuNode"] tm_node_id = event["TmNode"] snapshot_name = event["SnapshotName"] ec2 = boto3.client("ec2", region) asg = boto3.client("autoscaling", region) servers = get_instance_ids_from_asg(asg_client=asg, asg_name=server_asg_name) agents = get_instance_ids_from_asg(asg_client=asg, asg_name=agent_asg_name) as_robots = get_instance_ids_from_asg(asg_client=asg, asg_name=as_robots_asg_name) all_instances = list(servers) secondary_instances = [{'nodeType': 'server', 'instanceId': instanceId} for instanceId in servers[1:]] if len(agents) > 0: all_instances += agents secondary_instances += [{'nodeType': 'agent', 'instanceId': instanceId} for instanceId in agents] if len(as_robots) > 0: all_instances += as_robots secondary_instances += [{'nodeType': 'asrobots', 'instanceId': instanceId} for instanceId in as_robots] if gpu_node_id: all_instances.append(gpu_node_id) secondary_instances.append({'nodeType': 'gpu', 'instanceId': gpu_node_id}) if tm_node_id: all_instances.append(tm_node_id) secondary_instances.append({'nodeType': 'task-mining', 'instanceId': tm_node_id}) restore_input = { 'snapshotName': snapshot_name, 'allInstances': all_instances, 'firstServer': servers[0], 'secondaryServers': servers[1:], 'agents': agents, 'asRobots': as_robots, 'secondaryInstances': secondary_instances } return {'restoreInput': json.dumps(restore_input), 'firstServer': servers[0]} - name: "checkSnapshotExists" action: "aws:runCommand" onFailure: Abort inputs: DocumentName: "AWS-RunShellScript" InstanceIds: - "{{ computeRestoreStepInput.firstServer }}" CloudWatchOutputConfig: CloudWatchOutputEnabled: "true" CloudWatchLogGroupName: !Ref ClusterOperationsAutomationLogs Parameters: workingDirectory: '/root' executionTimeout: '43200' commands: - 'set -e' - 'echo "Checking if snapshot {{ SnapshotName }} exists..."' - '/root/installer/configureUiPathAS.sh snapshot list | grep -w "{{ SnapshotName }}"' - name: "runRestoreStepFunction" action: "aws:executeStateMachine" onFailure: Abort inputs: stateMachineArn: !Ref OnDemandRestoreStateMachine input: '{{ computeRestoreStepInput.restoreInput }}' GetBackupList: Metadata: cfn-lint: config: ignore_checks: - E9101 ignore_reasons: E9101: Abort is a keyword Type: AWS::SSM::Document Properties: DocumentType: Automation UpdateMethod: NewVersion Content: description: UiPath Automation Suite - get the list of avaliable snapshots of the Automation Suite cluster schemaVersion: '0.3' assumeRole: !If [DeployIam, !GetAtt AutomationAssumeRole.Arn, !Ref InstanceIamRoleArn] mainSteps: - name: "getFirstInstance" action: "aws:executeAwsApi" onFailure: Abort inputs: Service: autoscaling Api: DescribeAutoScalingGroups AutoScalingGroupNames: - !Sub "${ServerAsgName}" outputs: - Name: firstServerInstanceId Selector: $.AutoScalingGroups[0].Instances[0].InstanceId Type: String - name: "getSnapshotList" action: "aws:runCommand" onFailure: Abort inputs: DocumentName: "AWS-RunShellScript" InstanceIds: - "{{ getFirstInstance.firstServerInstanceId }}" CloudWatchOutputConfig: CloudWatchOutputEnabled: "true" CloudWatchLogGroupName: !Ref ClusterOperationsAutomationLogs Parameters: workingDirectory: '/root' executionTimeout: '43200' commands: - '/root/installer/configureUiPathAS.sh snapshot list' UpdateAMIDocument: Metadata: cfn-lint: config: ignore_checks: - E9101 ignore_reasons: E9101: Abort is a keyword Type: AWS::SSM::Document Properties: DocumentType: Automation UpdateMethod: NewVersion Content: description: UiPath Automation Suite - update AMI for the scalling groups schemaVersion: '0.3' assumeRole: !If [DeployIam, !GetAtt AutomationAssumeRole.Arn, !Ref InstanceIamRoleArn] parameters: ImageName: type: String description: | (Optional) The Image Name used for searching the new AMI. If you didn't use a custom AMI at deploy time you can leave the parameter empty and the Image Name stored in the Parameter Store will be used as default value. default: '' AmiId: type: String description: | (Optional) The AMI id that will be set on the new version of the ASG Launch Template. The AmiId takes precedence over the ImageName. default: '' mainSteps: - name: UpdateAmiId action: 'aws:executeScript' timeoutSeconds: 600 maxAttempts: 1 onFailure: Abort inputs: Runtime: python3.8 Handler: update_ami_id InputPayload: AmiId: '{{AmiId}}' ImageName: '{{ImageName}}' RegionName: !Sub '${AWS::Region}' Architecture: x86_64 VirtualizationType: hvm Owners: '309956199498' ServerAsgName: !Ref ServerAsgName AgentAsgName: !Ref AgentAsgName AsRobotsAsgName: !Ref AsRobotsAsgName InstanceAMIIdSSMParameter: !Ref InstanceAMIIdSSMParameter InstanceAMIImageNameSSMParameter: !Ref InstanceAMIImageNameSSMParameter Script: | from __future__ import print_function import json import boto3 def update_asg_launch_template(asg_name, ami_id, asg_client, ec2_client): asg_query = asg_client.describe_auto_scaling_groups(AutoScalingGroupNames=[asg_name]) if 'AutoScalingGroups' not in asg_query or not asg_query['AutoScalingGroups']: raise Exception('No ASG found matching the value you specified.') launch_template_specifications = asg_query['AutoScalingGroups'][0]["LaunchTemplate"] launch_template_name = launch_template_specifications["LaunchTemplateName"] launch_template_version = launch_template_specifications["Version"] new_launch_template_version = ec2_client.create_launch_template_version( LaunchTemplateName=launch_template_name, SourceVersion=launch_template_version, LaunchTemplateData={ 'ImageId': ami_id } ) asg_client.update_auto_scaling_group( AutoScalingGroupName=asg_name, LaunchTemplate={ 'LaunchTemplateName': launch_template_name, 'Version': str(new_launch_template_version["LaunchTemplateVersion"]["VersionNumber"]) } ) def update_ssm_parameter(parameter_name, parameter_value, ssm_client): ssm_client.put_parameter( Name=parameter_name, Value=parameter_value, Overwrite=True ) def update_ami_id(event, context): print("Received event: " + json.dumps(event, indent=2)) region = event["RegionName"] architecture = event["Architecture"] virtualization_type = event["VirtualizationType"] owners = event["Owners"] image_name = event["ImageName"] image_id = event["AmiId"] ec2 = boto3.client("ec2", region) asg = boto3.client("autoscaling", region) ssm = boto3.client("ssm", region) if not image_id and not image_name: if event["InstanceAMIImageNameSSMParameter"]: image_name = ssm.get_parameter(Name=event["InstanceAMIImageNameSSMParameter"])['Parameter']['Value'] else: raise Exception("Neither ImageName or AmiId have been provided") if image_id: image_querry = { "ExecutableUsers": ["all"], "Filters": [ {"Name": "image-id", "Values": [image_id]} ] } else: image_querry = { "ExecutableUsers": ["all"], "Filters": [ {"Name": "name", "Values": [image_name]}, {"Name": "state", "Values": ["available"]}, {"Name": "architecture", "Values": [architecture]}, {"Name": "virtualization-type", "Values": [virtualization_type]} ], "Owners": [owners] } images = ec2.describe_images(**image_querry)["Images"] if len(images) > 0: image_id = images[0]["ImageId"] else: raise Exception("Couldn't find an available image that satisfies the constraints") print(region, image_id) response = {} response["ImageId"] = image_id response["ImageName"] = image_name update_ssm_parameter(event["InstanceAMIIdSSMParameter"], image_id, ssm) if event["InstanceAMIImageNameSSMParameter"] and event["ImageName"]: update_ssm_parameter(event["InstanceAMIImageNameSSMParameter"], image_name, ssm) update_asg_launch_template(event["ServerAsgName"], image_id, asg, ec2) update_asg_launch_template(event["AsRobotsAsgName"], image_id, asg, ec2) if event["AgentAsgName"]: update_asg_launch_template(event["AgentAsgName"], image_id, asg, ec2) return response AsRobotsTerminateEventRule: Type: AWS::Events::Rule Condition: IsMultiNode Properties: EventPattern: source: - "aws.autoscaling" detail-type: - "EC2 Instance-terminate Lifecycle Action" detail: AutoScalingGroupName: - !Ref AsRobotsAsgName State: "ENABLED" Targets: - Arn: !Sub "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${AgentRemoveInstanceDocument}:$DEFAULT" Id: ASRobots-Termination RoleArn: !If [DeployEventsBridgeRole, !GetAtt EventsBridgeAssumeRole.Arn, !Ref InstanceIamRoleArn] InputTransformer: InputPathsMap: instance_id: $.detail.EC2InstanceId asg_name: "$.detail.AutoScalingGroupName" lch_name: "$.detail.LifecycleHookName" InputTemplate: '{"InstanceId":[],"AutoScalingGroupName":[],"LifecycleHookName":[]}' AgentTerminateEventRule: Type: AWS::Events::Rule Condition: IsMultiNode Properties: EventPattern: source: - "aws.autoscaling" detail-type: - "EC2 Instance-terminate Lifecycle Action" detail: AutoScalingGroupName: - !Ref AgentAsgName State: "ENABLED" Targets: - Arn: !Sub "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${AgentRemoveInstanceDocument}:$DEFAULT" Id: AS-Agent-Termination RoleArn: !If [DeployEventsBridgeRole, !GetAtt EventsBridgeAssumeRole.Arn, !Ref InstanceIamRoleArn] InputTransformer: InputPathsMap: instance_id: $.detail.EC2InstanceId asg_name: "$.detail.AutoScalingGroupName" lch_name: "$.detail.LifecycleHookName" InputTemplate: '{"InstanceId":[],"AutoScalingGroupName":[],"LifecycleHookName":[]}' ServerTerminateEventRule: Type: AWS::Events::Rule Condition: IsMultiNode Properties: EventPattern: source: - "aws.autoscaling" detail-type: - "EC2 Instance-terminate Lifecycle Action" detail: AutoScalingGroupName: - !Ref ServerAsgName State: "ENABLED" Targets: - Arn: !Sub "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${ServerRemoveInstanceDocument}:$DEFAULT" Id: AS-Server-Termination RoleArn: !If [DeployEventsBridgeRole, !GetAtt EventsBridgeAssumeRole.Arn, !Ref InstanceIamRoleArn] InputTransformer: InputPathsMap: instance_id: $.detail.EC2InstanceId asg_name: "$.detail.AutoScalingGroupName" lch_name: "$.detail.LifecycleHookName" InputTemplate: '{"InstanceId":[],"AutoScalingGroupName":[],"LifecycleHookName":[]}'