AWSTemplateFormatVersion: '2010-09-09' Description: Create parameters required for the AD domain join SSM Automation runbooks. Parameters: SSMAutomationDocumentName: Description: 'Automation runbook that will join or unjoin an EC2 Windows instance to Active Directory (AD). The name must be between 3 and 128 characters. Valid characters are a-z, A-Z, 0-9, and _, -, and . only. You can''t use the following strings as document name prefixes. These are reserved by AWS for use as document name prefixes: aws-,amazon, amzn.' Type: String AllowedPattern: ^[a-zA-Z0-9_\-.]{3,128}$ SSMStringADDomain: Description: The FQDN of the AD domain. Type: String Default: corp.example.com AllowedPattern: ^([a-zA-Z0-9]+[\.-])+([a-zA-Z0-9])+$ SSMStringADUser: Description: The AD username that has delegated rights to perform domain join activities. Enter the name in down-level logon name format, DOMAIN\Username. Type: String Default: CORP\Admin AllowedPattern: '[A-Z]+\\[\w.-]+' SSMSecureStringADPassword: Description: AD domain join password. Type: String NoEcho: true AllowedPattern: (?=^.{6,255}$)((?=.*\d)(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[^A-Za-z0-9])(?=.*[a-z])|(?=.*[^A-Za-z0-9])(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[A-Z])(?=.*[^A-Za-z0-9]))^.* MaxLength: '64' MinLength: '8' SSMStringADTargetOU: Description: Enter the Organizational Unit (OU) where your domain joined server will reside. Type: String Default: OU=Computers,OU=CORP,dc=corp,dc=example,dc=com Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: AWS Systems Manager Automation runbook and Parameter Store creation Parameters: - SSMAutomationDocumentName - SSMStringADDomain - SSMStringADUser - SSMSecureStringADPassword - SSMStringADTargetOU ParameterLabels: SSMAutomationDocumentName: default: Automation runbook name SSMStringADDomain: default: AD domain (FQDN) SSMStringADUser: default: AD domain user SSMSecureStringADPassword: default: AD domain user's password SSMStringADTargetOU: default: Target Organizational Unit (OU) Resources: SSMAutomationDocument: Type: AWS::SSM::Document Properties: DocumentType: Automation DocumentFormat: YAML Name: !Ref 'SSMAutomationDocumentName' Content: schemaVersion: '0.3' description: |- This document will join or unjoin an EC2 Windows instance to an Active Directory domain. assumeRole: '{{AutomationAssumeRole}}' parameters: AutomationAssumeRole: default: '' description: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf. type: String InstanceId: description: (Required) The Instance running Windows Server. type: String DomainJoinActivity: allowedValues: - Join - Unjoin - '' default: '' description: '(Required) Select which AD domain activity to perform, join an AD domain or unjoin an AD domain.' type: String mainSteps: - name: assertInstanceIsWindows action: 'aws:assertAwsResourceProperty' description: '' inputs: Service: ec2 PropertySelector: '$.Reservations[0].Instances[0].Platform' Api: DescribeInstances DesiredValues: - windows InstanceIds: - '{{InstanceId}}' timeoutSeconds: 10 nextStep: chooseDomainJoinActivity - name: chooseDomainJoinActivity action: aws:branch timeoutSeconds: 60 description: Determine the appropriate AD domain activity, join or unjoin. inputs: Choices: - NextStep: joinDomain StringEquals: Join Variable: '{{DomainJoinActivity}}' - NextStep: unjoinDomain StringEquals: Unjoin Variable: '{{DomainJoinActivity}}' isCritical: 'true' isEnd: false - name: joinDomain action: aws:runCommand description: Execute PowerShell locally on EC2 instance to join the AD domain. inputs: Parameters: commands: |- If ((Get-CimInstance -ClassName 'Win32_ComputerSystem' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty 'PartOfDomain') -eq $false) { Try { $targetOU = (Get-SSMParameterValue -Name 'defaultTargetOU' -ErrorAction Stop).Parameters[0].Value $domainName = (Get-SSMParameterValue -Name 'domainName' -ErrorAction Stop).Parameters[0].Value $domainJoinUserName = (Get-SSMParameterValue -Name 'domainJoinUserName' -ErrorAction Stop).Parameters[0].Value $domainJoinPassword = (Get-SSMParameterValue -Name 'domainJoinPassword' -WithDecryption:$true -ErrorAction Stop).Parameters[0].Value | ConvertTo-SecureString -AsPlainText -Force } Catch [System.Exception] { Write-Output " Failed to get SSM Parameter(s) $_" } $domainCredential = New-Object System.Management.Automation.PSCredential($domainJoinUserName, $domainJoinPassword) Try { Write-Output "Attempting to join $env:COMPUTERNAME to Active Directory domain: $domainName and moving $env:COMPUTERNAME to the following OU: $targetOU." Add-Computer -ComputerName $env:COMPUTERNAME -DomainName $domainName -Credential $domainCredential -OUPath $targetOU -Restart:$false -ErrorAction Stop } Catch [System.Exception] { Write-Output "Failed to add computer to the domain $_" Exit 1 } } Else { Write-Output "$env:COMPUTERNAME is already part of the Active Directory domain $domainName." Exit 0 } InstanceIds: - '{{InstanceId}}' DocumentName: AWS-RunPowerShellScript timeoutSeconds: 600 nextStep: joinADEC2Tag isEnd: false onFailure: step:failADEC2Tag - name: joinADEC2Tag action: aws:createTags description: Add the ADJoined EC2 tag to reflect joining to AD domain. inputs: ResourceIds: - '{{InstanceId}}' ResourceType: EC2 Tags: - Value: Join-complete Key: ADJoined isEnd: false nextStep: rebootServer - name: unjoinDomain action: aws:runCommand description: Execute PowerShell locally on EC2 instance to unjoin from the AD domain. inputs: Parameters: commands: |- If ((Get-CimInstance -ClassName 'Win32_ComputerSystem' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty 'PartOfDomain') -eq $true) { Try { $domainName = (Get-SSMParameterValue -Name 'domainName' -ErrorAction Stop).Parameters[0].Value $domainJoinUserName = (Get-SSMParameterValue -Name 'domainJoinUserName' -ErrorAction Stop).Parameters[0].Value $domainJoinPassword = (Get-SSMParameterValue -Name 'domainJoinPassword' -WithDecryption:$true -ErrorAction Stop).Parameters[0].Value | ConvertTo-SecureString -AsPlainText -Force } Catch [System.Exception] { Write-Output "Failed to get SSM Parameter(s) $_" } $domainCredential = New-Object System.Management.Automation.PSCredential($domainJoinUserName, $domainJoinPassword) If (-not (Get-WindowsFeature -Name 'RSAT-AD-Tools' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty 'Installed')) { Write-Output 'Installing RSAT AD Tools to allow domain joining' Try { $Null = Add-WindowsFeature -Name 'RSAT-AD-Tools' -ErrorAction Stop } Catch [System.Exception] { Write-Output "Failed to install RSAT AD Tools $_" Exit 1 } } $getADComputer = (Get-ADComputer -Identity $env:COMPUTERNAME -Credential $domainCredential) $distinguishedName = $getADComputer.DistinguishedName Try { Remove-Computer -ComputerName $env:COMPUTERNAME -UnjoinDomainCredential $domainCredential -Verbose -Force -Restart:$false -ErrorAction Stop Remove-ADComputer -Credential $domainCredential -Identity $distinguishedName -Server $domainName -Confirm:$False -Verbose -ErrorAction Stop } Catch [System.Exception] { Write-Output "Failed to remove $env:COMPUTERNAME from the $domainName domain and in a Windows Workgroup. $_" Exit 1 } } Else { Write-Output "$env:COMPUTERNAME is not part of the Active Directory domain $domainName and already part of a Windows Workgroup." Exit 0 } InstanceIds: - '{{InstanceId}}' DocumentName: AWS-RunPowerShellScript timeoutSeconds: 600 nextStep: unjoinADEC2Tag isEnd: false onFailure: step:failADEC2Tag - name: unjoinADEC2Tag action: aws:createTags description: Update the ADJoined EC2 tag to reflect removal from AD domain. inputs: ResourceIds: - '{{InstanceId}}' ResourceType: EC2 Tags: - Value: Unjoin-complete Key: ADJoined timeoutSeconds: 30 isEnd: false nextStep: stopServer - name: failADEC2Tag action: aws:createTags description: Update the ADJoined EC2 tag to reflect a failure in the AD domain join/unjoin process. inputs: ResourceIds: - '{{InstanceId}}' ResourceType: EC2 Tags: - Value: Failed Key: ADJoined timeoutSeconds: 30 isEnd: false nextStep: stopServer - name: rebootServer action: aws:executeAwsApi inputs: Service: ec2 Api: RebootInstances InstanceIds: - '{{InstanceId}}' isEnd: true - name: stopServer action: 'aws:executeAwsApi' inputs: Service: ec2 Api: StopInstances InstanceIds: - '{{InstanceId}}' isEnd: true SSMStringADDomainParam: Type: AWS::SSM::Parameter Properties: Name: domainName Type: String Value: !Ref SSMStringADDomain SSMStringADUserParam: Type: AWS::SSM::Parameter Properties: Name: domainJoinUserName Type: String Value: !Ref SSMStringADUser SSMStringADTargetOUParam: Type: AWS::SSM::Parameter Properties: Name: defaultTargetOU Type: String Value: !Ref SSMStringADTargetOU SSMSecureStringLambdaExecution: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: CustomSSMParameterStorePolicyCFN PolicyDocument: Version: '2012-10-17' Statement: - Sid: DelGetPutParameterPermissions Effect: Allow Action: - ssm:PutParameter - ssm:DeleteParameter - ssm:GetParameterHistory - ssm:GetParametersByPath - ssm:GetParameters - ssm:GetParameter - ssm:DeleteParameters Resource: - !Join - '' - - 'arn:' - !Ref 'AWS::Partition' - ':ssm:*:' - !Ref 'AWS::AccountId' - :parameter/domain* - Sid: DescParameterPermissions Effect: Allow Action: - ssm:DescribeParameters Resource: - !Join - '' - - 'arn:' - !Ref 'AWS::Partition' - ':ssm:*:' - !Ref 'AWS::AccountId' - :parameter/domain* Description: New IAM role configure Lambda execution and Amazon Systems Manager Parameter Store permissions. SSMSecureStringFunction: Type: AWS::Lambda::Function Properties: Code: ZipFile: | import boto3 import cfnresponse import json import logging import os import random import string import time import threading ssm_client = boto3.client('ssm') lambda_client = boto3.client('lambda') function_name = os.environ['AWS_LAMBDA_FUNCTION_NAME'] def update(list1,list2): lambda_client.update_function_configuration(FunctionName = function_name,Environment = {'Variables': {'cr_id' : list1,'delete': list2}}) def create_ssmsecurestring(ssm_secure_string,event): if event['RequestType'] == 'Update': ssm_client.put_parameter( Name = 'domainJoinPassword', Value = ssm_secure_string, Type = 'SecureString', DataType = 'text', Overwrite = True ) ssm_client.put_parameter( Name = 'domainJoinPassword', Value = ssm_secure_string, Type = 'SecureString', DataType = 'text', Overwrite = True ) def del_ssmsecurestring(ssm_secure_string): ssm_client.delete_parameters( Names=['domainJoinPassword',] ) def timeout(event,context): logging.error('Execution is about to time out, sending failure response to CloudFormation') cfnresponse.send(event, context, cfnresponse.FAILED, {}, None) def lambda_handler(event, context): timer = threading.Timer((context.get_remaining_time_in_millis() / 1000.00) - 0.5, timeout, args=[event, context]) timer.start() print('Received event: %s' % json.dumps(event)) status = cfnresponse.SUCCESS try: ssm_secure_string = event['ResourceProperties']['ssm_secure_string'] if event['RequestType'] == 'Delete': del_ssmsecurestring(ssm_secure_string) else: create_ssmsecurestring(ssm_secure_string,event) except Exception as e: logging.error('Exception: %s' % e, exc_info=True) status = cfnresponse.FAILED finally: timer.cancel() cfnresponse.send(event, context, status, {}) Runtime: python3.8 Role: !GetAtt SSMSecureStringLambdaExecution.Arn MemorySize: 256 Timeout: 300 Handler: index.lambda_handler ReservedConcurrentExecutions: 100 CustomLambdaPolicyResource: Type: AWS::IAM::Policy Properties: PolicyName: CustomLambdaPolicyCFN PolicyDocument: Version: '2012-10-17' Statement: - Sid: LambdaPermissions Effect: Allow Action: - lambda:UpdateFunctionCode - lambda:UpdateFunctionConfiguration - lambda:ListFunctions - lambda:GetFunction - lambda:GetFunctionConfiguration - lambda:DeleteFunction Resource: !GetAtt SSMSecureStringFunction.Arn Roles: - !Ref SSMSecureStringLambdaExecution InvokeSSMSecureStringFunction: Type: Custom::InvokeLambda Properties: ServiceToken: !GetAtt SSMSecureStringFunction.Arn ssm_secure_string: !Ref SSMSecureStringADPassword