AWS Feed
Managing domain membership of dynamic fleet of EC2 instances
This post is written by Alex Zarenin, Senior AWS Solution Architect, Microsoft Tech.
Updated: February 10, 2021
1. Introduction
For most companies, a move of Microsoft workloads to AWS starts with “lift and shift” where existing workloads are moved from the on-premises data centers to the cloud. These workloads may include WEB and API farms, and a fleet of processing nodes, which typically depend on AD Domain membership for access to shared resources, such as file shares and SQL Server databases.
When the farms and set of processing nodes are static, which is typical for on-premises deployments, managing domain membership is simple – new instances join the AD Domain and stay there. When some machines are periodically recycled, respective AD computer accounts are disabled or deleted, and new accounts are added when new machines are added to the domain. However, these changes are slow and can be easily managed.
When these workloads are moved to the cloud, it is natural to set up WEB and API farms as scalability groups to allow for scaling up and scaling down membership to optimize cost while meeting the performance requirements. Similarly, processing nodes could be combined into scalability groups or created on-demand as a set of Amazon EC2 Spot Instances.
In either case, the fleet becomes very dynamic, and can expand and shrink multiple times to match the load or in response to some events, which makes manual management of AD Domain membership impractical. This scenario requires automated solution for managing domain membership.
2. Challenges
This automated solution to manage domain membership of dynamic fleet of Amazon EC2 instances should provide for:
- Seamless AD Domain joining when the new instances join the fleet and it should work both for Managed and native ADs;
- Automatic unjoining from the AD Domain and removal from AD the respective computer account when the instance is stopped or terminated;
- Following the best practices for protecting sensitive information – the identity of the account that is used for joining domain or removing computer account from the domain.
- Extensive logging to facilitate troubleshooting if something does not work as expected.
3. Solution overview
Joining an AD domain, whether native or managed, could be achieved by placing a PowerShell script that performs domain joining into the User Data
section of the EC2 instance launch configuration.
It is much more difficult to implement domain unjoining and deleting computer account from AD upon instance termination as Windows does not support On-Shutdown
trigger in the Task Scheduler. However, it is possible to define On-Shutdown
script using the local Group Policy.
If defined, the On-Shutdown
script runs on EVERY shutdown. However, joining a domain REQUIRES reboot of the machine, so On-Shutdown
policy cannot be enabled on the first invocation of the User Data
script as it will be removed from the domain by the On-Shutdown
script right when it joins the domain. Thus, the User Data
script must have some logic to define whether it is the first invocation upon instance launch, or the subsequent one following the domain join reboot. The On-Shutdown
policy should be enabled only on the second start-up. This also necessitates to define the User Data
script as “persistent” by specifying <persist>true</persist>
in the User Data
section of the launch configuration.
Both domain join and domain unjoin scripts require security context that allows to perform these operations on the domain, which is usually achieved by providing credentials for a user account with corresponding rights. In the proposed implementation, both scripts obtain account credentials from the AWS Secrets Manager under protection of security policies and roles – no credentials are stored in the scripts.
Both scripts generate detailed log of their operation stored in the Amazon CloudWatch logs.
In this post, I demonstrate a solution based upon PowerShell script that is scheduled to perform Active Directory domain joining on the instance start-up through the EC2 launch User Data
script. I also show removal from the domain with the deletion of respective computer accounts from the domain upon instance shutdown using the script installed in the On-Shutdown
policy.
User Data
script overall logic:
- Initialize Logging
- Initialize
On-Shutdown
Policy - Read fields
UserID
,Password
, andDomain
fromprod/AD
secret - Verify machine’s domain membership
- If machine is already a member of the domain, then
- Enable
On-Shutdown
Policy - Install RSAT for AD PowerShell
- Enable
- Otherwise
- Create credentials from the secret
- Initiate domain join
- Request machine restart
On-Shutdown
script overall logic:
- Initialize Logging
- Check
cntrl
variable; Ifcntrl
variable is not set to value “run
”, exit script - Check whether machine is a member of the domain; if not, exit script
- Check if the RSAT for AD PowerShell installed; if not installed, exit the script
- Read fields
UserID
,Password
, andDomain
fromprod/AD
secret - Create credentials from the secret
- Identify domain controller
- Remove machine from the domain
- Delete machine account from domain controller
Now that I have reviewed the overall logic of the scripts, I can examine components of each script in more details.
4. Routines common to both UserData
and On-Shutdown
scripts
4.1. Processing configuration variables and parameters
UserData
script does not accept parameters and is being executed exactly as being provided in the UserData
section of the Launch configuration. However, at the beginning of the script a variable is specified that could be easily changed:
[string]$SecretAD = "prod/AD"
This variable provides the name of the secret defined in the Secrets Manager that contains UserID
, Password
, and Domain
.
The On-Shutdown
Group Policy invokes corresponding scrip with a parameter, which is stored in the registry as part of the policy set up. Thus, the first line of the On-Shutdown
script defines the variable for this parameter:
param([string]$cntrl = "NotSet")
The next line in the On-Shutdown
script provides the name of the secret – same as in the User Data
script. They are generated from the corresponding variables in the User Data
script.
4.2. The Logger class
Both scripts, UserData
and On-Shutdown
, use the same Logger
class and perform logging into the Amazon CloudWatch log group /ps/boot/configuration/
. If this log group does not exist, the script attempts to create respective log group. The name of the log group is stored in the Logger
class variable $this.cwlGroup
and can be changed if needed.
Each execution of either script creates a new log stream in the log group. The name of the log stream consists of three parts – machine name, script type, and date-time stamp. The script type is passed to the Logger
class in the constructor. Two script types are used in the script – UserData
for the script invoked through the UserData
section and UnJoin
for the script invoked through the On-Shutdown
policy. These log stream names may look like
EC2AMAZ-714VBCO/UserData/2020-10-06_05.40.02
EC2AMAZ-714VBCO/UnJoin/2020-10-06_05.48.43
5. The UserData
script
The following are the major components of the UserData
script.
5.1. Initializing the On-Shutdown policy
The SDManager
class wraps functionality necessary to create On-Shutdown policy. The policy requires certain registry entries and a script that executes when policy is invoked. This script must be placed in a well-defined folder on the file system.
The SDManager
constructor performs the following task:
- Verifies that the folder
C:WindowsSystem32GroupPolicyMachineScriptsShutdown
exists and, if necessary, creates it; - Updates
On-Shutdown
script stored as an array inSDManager
with the parameters provided to the constructor, and then saves adjusted script in the proper location; - Creates all registry entries required for
On-Shutdown
policy; - Sets the parameter that will be passed to the
On-Shutdown
script by the policy to a value that would precludeOn-Shutdown
script from removing machine from the domain.
SDManager
exposes two member functions, EnableUnJoin()
and DisableUnJoin()
. These functions update parameter passed to On-Shutdown
script to enable or disable removing machine from the domain, respectively.
5.2. Reading the “secret”
Using the value of configuration variable $SecretAD
, the following code example retrieves the secret value from AWS Secrets Manager and creates PowerShell credential to be used for the operations on the domain. The Domain
value from the secret is also used to verify that machine is a member of required domain.
Import-Module AWSPowerShell
try { $SecretObj = (Get-SECSecretValue -SecretId $SecretAD) }
catch
{
$log.WriteLine("Could not load secret <" + $SecretAD + "> - terminating execution")
return
}
[PSCustomObject]$Secret = ($SecretObj.SecretString | ConvertFrom-Json)
$log.WriteLine("Domain (from Secret): <" + $Secret.Domain + ">")
To get the secret from AWS Secrets Manager, you must use an AWS-specific cmdlet. To make it available, you must import the AWSPowerShell
module.
5.3. Checking for domain membership and enabling On-Shutdown policy
To check for domain membership, we use WMI Win32_ComputerObject
. While performing check for domain membership, we also validate that if the machine is a member of the domain, it is the domain specified in the secret.
If machine is already a member of the correct domain, the script proceeds with installing RSAT for AD PowerShell
, which is required for the On-Shutdown
script. It also enables the On-Shutdown
script. The following code example achieves this:
$compSys = Get-WmiObject -Class Win32_ComputerSystem
if ( ($compSys.PartOfDomain) -and ($compSys.Domain -eq $Secret.Domain))
{
$log.WriteLine("Already member of: <" + $compSys.Domain + "> - Verifying RSAT Status")
$RSAT = (Get-WindowsFeature RSAT-AD-PowerShell)
if ($RSAT -eq $null)
{
$log.WriteLine("<RSAT-AD-PowerShell> feature not found - terminating script")
return
}
$log.WriteLine("Enable OnShutdown task to un-join Domain")
$sdm.EnableUnJoin()
if ( (-Not $RSAT.Installed) -and ($RSAT.InstallState -eq "Available") )
{
$log.WriteLine("Installing <RSAT-AD-PowerShell> feature")
Install-WindowsFeature RSAT-AD-PowerShell
}
$log.WriteLine("Terminating script - ")
return
}
5.4. Joining Domain
If a machine is not a member of the domain or member of the wrong domain, the script creates credentials from the Secret and requests domain joining with subsequent restart of the machine. The following code example performs all these tasks:
$log.WriteLine("Domain Join required")
$log.WriteLine("Disable OnShutdown task to avoid reboot loop")
$sdm.DisableUnJoin()
$password = $Secret.Password | ConvertTo-SecureString -asPlainText -Force
$username = $Secret.UserID + "@" + $Secret.Domain
$credential = New-Object System.Management.Automation.PSCredential($username,$password)
$log.WriteLine("Attempting to join domain <" + $Secret.Domain + ">")
Add-Computer -DomainName $Secret.Domain -Credential $credential -Restart -Force
$log.WriteLine("Requesting restart...")
6. The On-Shutdown
script
Many components of the On-Shutdown
script, such as logging, working with the AWS Secrets Manager, and validating domain membership are either the same or very similar to respective components of the UserData
script.
One interesting difference is that the On-Shutdown
script accepts parameter from the respective policy. The value of this parameter is set by EnableUnJoin()
and DisableUnJoin()
functions in the User Data
script to control whether domain un-join will happen on a particular reboot – something that I discussed earlier. Thus, you have the following code example at the beginning of On-Shutdown
script:
if ($cntrl -ne "run")
{
$log.WriteLine("Script param <" + $cntrl + "> not set to <run> - script terminated")
return
}
By setting the On-Shutdown
policy parameter (a value in registry) to something other than “run
” we can stop On-Shutdown
script from executing – this is exactly what function DisableUnJoin()
does. Similarly, the function EnableUnJoin()
sets the value of this parameter to “run
” thus allowing the On-Shutdown
script to continue execution when invoked.
Another interesting problem with this script is how to implement removing a machine from the domain and deleting respective computer account from the Active Directory. If the script first removes machine from the domain, then it cannot find domain controller to delete computer account.
Alternatively, if the script first deletes computer account, and then tries to remove computer account by changing domain to a workgroup, this change would fail. The following code example represents how this issue was resolved in the script:
import-module ActiveDirectory
$DCHostName = (Get-ADDomainController -Discover).HostName
$log.WriteLine("Using Account <" + $username + ">")
$log.WriteLine("Using Domain Controller <" + $DCHostName + ">")
Remove-Computer -WorkgroupName "WORKGROUP" -UnjoinDomainCredential $credential -Force -Confirm:$false
Remove-ADComputer -Identity $MachineName -Credential $credential -Server "$DCHostName" -Confirm:$false
Before removing machine from the domain, the script obtains and stores in a local variable the name of one of the domain controllers. Then computer is switched from domain to the workgroup. As the last step, the respective computer account is being deleted from the AD using the host name of the domain controller obtained earlier.
7. Managing Script Credentials
Both User Data
and On-Shutdown
scripts obtain the domain name and user credentials to add or remove computers from the domain from AWS Secrets Manager secret
with the predefined name prod/AD
. This predefined name can be changed in the script.
Details on how to create a secret are available in AWS documentation. This secret should be defined as Other type of secrets and contain at least the following fields:
- UserID
- Password
- Domain
Fill in respective fields on the Secrets Manager configuration screen and chose Next as illustrated on the following screenshot:
Give the new secret the name prod/AD (this name is referred to in the script) and capture the secret’s ARN. The latter is required for creating a policy that allows access to this secret.
8. Creating AWS Policy and Role to access the Credential Secret
8.1. Creating IAM Policy
The next step is to use IAM to create a policy that would allow access to the secret; the policy statement will appear as following:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:us-east-1:NNNNNNNN7225:secret:prod/AD-??????"
}
]
}
Below is the AWS console screenshot with the policy statement filled in:
The resource in the policy is identified with the “wildcard” characters for the 6 random characters at the end of the ARN, which may change when the Secret is updated. Configuring policy with the wildcard allows to extend rights to ALL versions of the secret, which would allow for changing credential information without changing respective policy.
Let’s name this policy AdminReadDomainCreds
so that we may refer to it when creating an IAM Role.
8.2. Creating IAM Role
Now that I defined AdminReadDomainCreds
policy, you can create a role AdminDomainJoiner
that refers to this policy. On the Permission tab of the role creation dialog, attach standard SSM policy for EC2, AmazonEC2RoleforSSM
, policy that allows performing required CloudWatch logging operations, CloudWatchAgentAdminPolicy
, and, finally, the custom policy AdminReadDomainCreds
.
The Permission
tab of the role creation dialog with the respective roles attached is shown in the following screenshot:
This role should include our new policy, AdminReadDomainCreds
, in addition to standard SSM policy for EC2.
9. Launching the Instance
Now, you’re ready to launch the instance or create the launch configuration. When configuring the instance for launch, it’s important to assign the instance to the role AdminDomainJoiner, which you just created:
In the Advanced Details
section of the configuration screen, paste the script into the User Data
field:
If you named your secret differently than the name prod/AD
that I used, modify the script parameters SecretAD
to use the name of your secret.
10. Conclusion
That’s it! When you launch this instance, it will automatically join the domain. Upon Stop or Termination, the instance will remove itself from the domain.
For your convenience we provide the full text of the UserData
script:
<powershell>
# Script parameters
[string]$SecretAD = "prod/AD"
class Logger { #---------------------------------------------- [string] hidden $cwlGroup [string] hidden $cwlStream [string] hidden $sequenceToken #---------------------------------------------- # Log Initialization #---------------------------------------------- Logger([string] $Action) { $this.cwlGroup = "/ps/boot/configuration/" $this.cwlStream = "{0}/{1}/{2}" -f $env:COMPUTERNAME, $Action, (Get-Date -UFormat "%Y-%m-%d_%H.%M.%S") $this.sequenceToken = "" #------------------------------------------ if ( !(Get-CWLLogGroup -LogGroupNamePrefix $this.cwlGroup) ) { New-CWLLogGroup -LogGroupName $this.cwlGroup Write-CWLRetentionPolicy -LogGroupName $this.cwlGroup -RetentionInDays 3 } if ( !(Get-CWLLogStream -LogGroupName $this.cwlGroup -LogStreamNamePrefix $this.cwlStream) ) { New-CWLLogStream -LogGroupName $this.cwlGroup -LogStreamName $this.cwlStream } } #---------------------------------------- [void] WriteLine([string] $msg) { $logEntry = New-Object -TypeName "Amazon.CloudWatchLogs.Model.InputLogEvent" #----------------------------------------------------------- $logEntry.Message = $msg $logEntry.Timestamp = (Get-Date).ToUniversalTime() if ("" -eq $this.sequenceToken) { # First write into empty log... $this.sequenceToken = Write-CWLLogEvent -LogGroupName $this.cwlGroup ` -LogStreamName $this.cwlStream ` -LogEvent $logEntry } else { # Subsequent write into the log... $this.sequenceToken = Write-CWLLogEvent -LogGroupName $this.cwlGroup ` -LogStreamName $this.cwlStream ` -SequenceToken $this.sequenceToken ` -LogEvent $logEntry } }
}
[Logger]$log = [Logger]::new("UserData")
$log.WriteLine("------------------------------")
$log.WriteLine("Log Started - V4.0")
$RunUser = $env:username
$log.WriteLine("PowerShell session user: $RunUser")
class SDManager { #------------------------------------------------------------------- [Logger] hidden $SDLog [string] hidden $GPScrShd_0_0 = "HKLM:SOFTWAREMicrosoftWindowsCurrentVersionGroup PolicyScriptsShutdown " [string] hidden $GPMScrShd_0_0 = "HKLM:SOFTWAREMicrosoftWindowsCurrentVersionGroup PolicyStateMachineScriptsShutdown " #------------------------------------------------------------------- SDManager([Logger]$Log, [string]$RegFilePath, [string]$SecretName) { $this.SDLog = $Log #---------------------------------------------------------------- [string] $SecretLine = '[string]$SecretAD = "' + $SecretName + '"' #--------------- Local Variables ------------- [string] $GPRootPath = "C:WindowsSystem32GroupPolicy" [string] $GPMcnPath = "C:WindowsSystem32GroupPolicyMachine" [string] $GPScrPath = "C:WindowsSystem32GroupPolicyMachineScripts" [string] $GPSShdPath = "C:WindowsSystem32GroupPolicyMachineScriptsShutdown" [string] $ScriptFile = [System.IO.Path]::Combine($GPSShdPath, "Shutdown-UnJoin.ps1") #region Shutdown script (scheduled through Local Policy) $ScriptBody = @( 'param([string]$cntrl = "NotSet")', $SecretLine, '[string]$MachineName = $env:COMPUTERNAME', 'class Logger { ', ' #---------------------------------------------- ', ' [string] hidden $cwlGroup ', ' [string] hidden $cwlStream ', ' [string] hidden $sequenceToken ', ' #---------------------------------------------- ', ' # Log Initialization ', ' #---------------------------------------------- ', ' Logger([string] $Action) { ', ' $this.cwlGroup = "/ps/boot/configuration/" ', ' $this.cwlStream = "{0}/{1}/{2}" -f $env:COMPUTERNAME, $Action, ', ' (Get-Date -UFormat "%Y-%m-%d_%H.%M.%S") ', ' $this.sequenceToken = "" ', ' #------------------------------------------ ', ' if ( !(Get-CWLLogGroup -LogGroupNamePrefix $this.cwlGroup) ) { ', ' New-CWLLogGroup -LogGroupName $this.cwlGroup ', ' Write-CWLRetentionPolicy -LogGroupName $this.cwlGroup -RetentionInDays 3 ', ' } ', ' if ( !(Get-CWLLogStream -LogGroupName $this.cwlGroup -LogStreamNamePrefix $this.cwlStream) ) { ', ' New-CWLLogStream -LogGroupName $this.cwlGroup -LogStreamName $this.cwlStream ', ' } ', ' } ', ' #---------------------------------------- ', ' [void] WriteLine([string] $msg) { ', ' $logEntry = New-Object -TypeName "Amazon.CloudWatchLogs.Model.InputLogEvent" ', ' #----------------------------------------------------------- ', ' $logEntry.Message = $msg ', ' $logEntry.Timestamp = (Get-Date).ToUniversalTime() ', ' if ("" -eq $this.sequenceToken) { ', ' # First write into empty log... ', ' $this.sequenceToken = Write-CWLLogEvent -LogGroupName $this.cwlGroup `', ' -LogStreamName $this.cwlStream `', ' -LogEvent $logEntry ', ' } ', ' else { ', ' # Subsequent write into the log... ', ' $this.sequenceToken = Write-CWLLogEvent -LogGroupName $this.cwlGroup `', ' -LogStreamName $this.cwlStream `', ' -SequenceToken $this.sequenceToken `', ' -LogEvent $logEntry ', ' } ', ' } ', '} ', '[Logger]$log = [Logger]::new("UnJoin")', '$log.WriteLine("-----------------------------------------")', '$log.WriteLine("Log Started")', 'if ($cntrl -ne "run") ', ' { ', ' $log.WriteLine("Script param <" + $cntrl + "> not set to <run> - script terminated") ', ' return', ' }', '$compSys = Get-WmiObject -Class Win32_ComputerSystem', 'if ( -Not ($compSys.PartOfDomain))', ' {', ' $log.WriteLine("Not member of a domain - terminating script")', ' return', ' }', '$RSAT = (Get-WindowsFeature RSAT-AD-PowerShell)', 'if ( $RSAT -eq $null -or (-Not $RSAT.Installed) )', ' {', ' $log.WriteLine("<RSAT-AD-PowerShell> feature not found - terminating script")', ' return', ' }', '$log.WriteLine("Removing machine <" +$MachineName + "> from Domain <" + $compSys.Domain + ">")', '$log.WriteLine("Reading Secret <" + $SecretAD + ">")', 'Import-Module AWSPowerShell', 'try { $SecretObj = (Get-SECSecretValue -SecretId $SecretAD) }', 'catch ', ' { ', ' $log.WriteLine("Could not load secret <" + $SecretAD + "> - terminating execution")', ' return ', ' }', '[PSCustomObject]$Secret = ($SecretObj.SecretString | ConvertFrom-Json)', '$password = $Secret.Password | ConvertTo-SecureString -asPlainText -Force', '$username = $Secret.UserID + "@" + $Secret.Domain', '$credential = New-Object System.Management.Automation.PSCredential($username,$password)', 'import-module ActiveDirectory', '$DCHostName = (Get-ADDomainController -Discover).HostName', '$log.WriteLine("Using Account <" + $username + ">")', '$log.WriteLine("Using Domain Controller <" + $DCHostName + ">")', 'Remove-Computer -WorkgroupName "WORKGROUP" -UnjoinDomainCredential $credential -Force -Confirm:$false ', 'Remove-ADComputer -Identity $MachineName -Credential $credential -Server "$DCHostName" -Confirm:$false ', '$log.WriteLine("Machine <" +$MachineName + "> removed from Domain <" + $compSys.Domain + ">")' )
$this.SDLog.WriteLine("Constracting artifacts required for domain UnJoin") #---------------------------------------------------------------- try { if (!(Test-Path -Path $GPRootPath -pathType container)) { New-Item -ItemType directory -Path $GPRootPath } if (!(Test-Path -Path $GPMcnPath -pathType container)) { New-Item -ItemType directory -Path $GPMcnPath } if (!(Test-Path -Path $GPScrPath -pathType container)) { New-Item -ItemType directory -Path $GPScrPath } if (!(Test-Path -Path $GPSShdPath -pathType container)) { New-Item -ItemType directory -Path $GPSShdPath } } catch { $this.SDLog.WriteLine("Failure creating UnJoin script directory!" ) $this.SDLog.WriteLine($_) } #---------------------------------------- try { Set-Content $ScriptFile -Value $ScriptBody } catch { $this.SDLog.WriteLine("Failure saving UnJoin script!" ) $this.SDLog.WriteLine($_) } #---------------------------------------- $RegistryScript = @( 'Windows Registry Editor Version 5.00', '[HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsCurrentVersionGroup PolicyScripts]', '[HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsCurrentVersionGroup PolicyScriptsShutdown]', '[HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsCurrentVersionGroup PolicyScriptsShutdown ]', '"GPO-ID"="LocalGPO"', '"SOM-ID"="Local"', '"FileSysPath"="C:\Windows\System32\GroupPolicy\Machine"', '"DisplayName"="Local Group Policy"', '"GPOName"="Local Group Policy"', '"PSScriptOrder"=dword:00000001', '[HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsCurrentVersionGroup PolicyScriptsShutdown ]', '"Script"="Shutdown-UnJoin.ps1"', '"Parameters"=""', '"IsPowershell"=dword:00000001', '"ExecTime"=hex(b):00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00', '[HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsCurrentVersionGroup PolicyScriptsStartup]', '[HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsCurrentVersionGroup PolicyStateMachineScripts]', '[HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsCurrentVersionGroup PolicyStateMachineScriptsShutdown]', '[HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsCurrentVersionGroup PolicyStateMachineScriptsShutdown ]', '"GPO-ID"="LocalGPO"', '"SOM-ID"="Local"', '"FileSysPath"="C:\Windows\System32\GroupPolicy\Machine"', '"DisplayName"="Local Group Policy"', '"GPOName"="Local Group Policy"', '"PSScriptOrder"=dword:00000001', '[HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsCurrentVersionGroup PolicyStateMachineScriptsShutdown ]', '"Script"="Shutdown-UnJoin.ps1"', '"Parameters"=""', '"ExecTime"=hex(b):00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00', '[HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsCurrentVersionGroup PolicyStateMachineScriptsStartup]' ) try { [string] $RegistryFile = [System.IO.Path]::Combine($RegFilePath, "OnShutdown.reg") Set-Content $RegistryFile -Value $RegistryScript ®edit.exe /S "$RegistryFile" } catch { $this.SDLog.WriteLine("Failure creating policy entry in Registry!" ) $this.SDLog.WriteLine($_) } } #---------------------------------------- [void] DisableUnJoin() { try { Set-ItemProperty -Path $this.GPScrShd_0_0 -Name "Parameters" -Value "ignore" Set-ItemProperty -Path $this.GPMScrShd_0_0 -Name "Parameters" -Value "ignore" &gpupdate /Target:computer /Wait:0 } catch { $this.SDLog.WriteLine("Failure in <DisableUnjoin> function!" ) $this.SDLog.WriteLine($_) } } #---------------------------------------- [void] EnableUnJoin() { try { Set-ItemProperty -Path $this.GPScrShd_0_0 -Name "Parameters" -Value "run" Set-ItemProperty -Path $this.GPMScrShd_0_0 -Name "Parameters" -Value "run" &gpupdate /Target:computer /Wait:0 } catch { $this.SDLog.WriteLine("Failure in <EnableUnjoin> function!" ) $this.SDLog.WriteLine($_) } }
}
[SDManager]$sdm = [SDManager]::new($Log, "C:ProgramDataAmazonEC2-WindowsLaunchScripts", $SecretAD)
$log.WriteLine("Loading Secret <" + $SecretAD + ">")
Import-Module AWSPowerShell
try { $SecretObj = (Get-SECSecretValue -SecretId $SecretAD) }
catch { $log.WriteLine("Could not load secret <" + $SecretAD + "> - terminating execution") return
}
[PSCustomObject]$Secret = ($SecretObj.SecretString | ConvertFrom-Json)
$log.WriteLine("Domain (from Secret): <" + $Secret.Domain + ">")
# Verify domain membership
$compSys = Get-WmiObject -Class Win32_ComputerSystem
#------------------------------------------------------------------------------
if ( ($compSys.PartOfDomain) -and ($compSys.Domain -eq $Secret.Domain)) { $log.WriteLine("Already member of: <" + $compSys.Domain + "> - Verifying RSAT Status")
$RSAT = (Get-WindowsFeature RSAT-AD-PowerShell) if ($null -eq $RSAT) { $log.WriteLine("<RSAT-AD-PowerShell> feature not found - terminating script") return }
$log.WriteLine("Enable OnShutdown task to un-join Domain") $sdm.EnableUnJoin()
if ( (-Not $RSAT.Installed) -and ($RSAT.InstallState -eq "Available") ) { $log.WriteLine("Installing <RSAT-AD-PowerShell> feature") Install-WindowsFeature RSAT-AD-PowerShell }
$log.WriteLine("Terminating script - ") return
}
# Performing Domain Join
$log.WriteLine("Domain Join required")
$log.WriteLine("Disable OnShutdown task to avoid reboot loop")
$sdm.DisableUnJoin()
$password = $Secret.Password | ConvertTo-SecureString -asPlainText -Force
$username = $Secret.UserID + "@" + $Secret.Domain
$credential = New-Object System.Management.Automation.PSCredential($username, $password)
$log.WriteLine("Attempting to join domain <" + $Secret.Domain + ">")
Add-Computer -DomainName $Secret.Domain -Credential $credential -Restart -Force
$log.WriteLine("Requesting restart...")
#------------------------------------------------------------------------------
</powershell>
<persist>true</persist>