AWSTemplateFormatVersion: '2010-09-09' Description: "Deploy single windows EC2 Instance and join domain with SSM Association" Parameters: DomainAdminPassword: 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]))^.* Description: Password for the domain admin user. Must be at least 8 characters, containing letters, numbers, and symbols. MaxLength: '32' MinLength: '8' NoEcho: 'true' Type: String DomainAdminUser: AllowedPattern: '[a-zA-Z0-9]*' Default: Admin Description: User name for the account that will be used as domain administrator. This is separate from the default "Administrator" account. MaxLength: '25' MinLength: '5' Type: String DomainDNSName: AllowedPattern: '[a-zA-Z0-9\-]+\..+' Default: example.com Description: Fully qualified domain name (FQDN). MaxLength: '255' MinLength: '2' Type: String DomainMemberSGID: Description: ID of the domain member security group (e.g., sg-7f16e910). Type: AWS::EC2::SecurityGroup::Id DomainNetBIOSName: AllowedPattern: '[a-zA-Z0-9\-]+' Default: EXAMPLE Description: NetBIOS name of the domain (up to 15 characters) for users of earlier versions of Windows. MaxLength: '15' MinLength: '1' Type: String EC2InstanceType: AllowedValues: - t3.nano - t3.micro - t3.small - t3.medium - t3.large - t3.xlarge - t3.2xlarge - m5.large - m5.xlarge - m5.2xlarge Default: m5.large Description: Amazon EC2 instance type Type: String LatestAmiId: Type: 'AWS::SSM::Parameter::Value' Default: "/aws/service/ami-windows-latest/Windows_Server-2019-English-Full-Base" SubnetID: Description: ID of a Subnet. Type: AWS::EC2::Subnet::Id Resources: DSCBucket: Type: AWS::S3::Bucket Properties: LifecycleConfiguration: Rules: - Id: DeleteAfter30Days ExpirationInDays: 30 Status: Enabled Prefix: 'logs/' DomainJoinSecrets: Type: AWS::SecretsManager::Secret Properties: Name: !Sub 'DomainJoinSecrets-${AWS::StackName}' Description: Secrets to join AD domain SecretString: !Sub '{"username":"${DomainAdminUser}","password":"${DomainAdminPassword}"}' LambdaSSMRole: Type: AWS::IAM::Role Properties: Policies: - PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - s3:PutObject Resource: - !Sub "${DSCBucket.Arn}" - !Sub "${DSCBucket.Arn}/*" PolicyName: write-mof-s3 Path: / AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' WriteScriptFunction: Type: AWS::Lambda::Function Properties: Code: ZipFile: > var AWS = require('aws-sdk'), s3 = new AWS.S3(); const response = require("cfn-response"); exports.handler = async (event, context) => { console.log(JSON.stringify(event)); if (event.RequestType === 'Delete') { await postResponse(event, context, response.SUCCESS, {}) return; } function postResponse(event, context, status, data){ return new Promise((resolve, reject) => { setTimeout(() => response.send(event, context, status, data), 5000) }); } await s3.putObject({ Body: event.ResourceProperties.Body, Bucket: event.ResourceProperties.Bucket, Key: event.ResourceProperties.Key }).promise(); await postResponse(event, context, response.SUCCESS, {}); }; Handler: index.handler Role: !GetAtt LambdaSSMRole.Arn Runtime: nodejs14.x Timeout: 10 WriteDomainJoinScript: Type: Custom::WriteScript Properties: ServiceToken: !GetAtt WriteScriptFunction.Arn Bucket: !Ref DSCBucket Key: "DomainJoin.ps1" Body: | [CmdletBinding()] # Incoming Parameters for Script, CloudFormation\SSM Parameters being passed in param( [Parameter(Mandatory=$true)] [string]$DomainNetBIOSName, [Parameter(Mandatory=$true)] [string]$DomainDNSName, [Parameter(Mandatory=$true)] [string]$AdminSecret ) # Formatting AD Admin User to proper format for JoinDomain DSC Resources in this Script $DomainAdmin = 'Domain\User' -replace 'Domain',$DomainNetBIOSName -replace 'User',$UserName $Admin = ConvertFrom-Json -InputObject (Get-SECSecretValue -SecretId $AdminSecret).SecretString $AdminUser = $DomainNetBIOSName + '\' + $Admin.UserName # Creating Credential Object for Administrator $Credentials = (New-Object PSCredential($AdminUser,(ConvertTo-SecureString $Admin.Password -AsPlainText -Force))) # Getting the DSC Cert Encryption Thumbprint to Secure the MOF File $DscCertThumbprint = (get-childitem -path cert:\LocalMachine\My | where { $_.subject -eq "CN=SampleDscEncryptCert" }).Thumbprint # Getting the Name Tag of the Instance $NameTag = (Get-EC2Tag -Filter @{ Name="resource-id";Values=(Invoke-RestMethod -Method Get -Uri http://169.254.169.254/latest/meta-data/instance-id)}| Where-Object { $_.Key -eq "Name" }) $NewName = $NameTag.Value # Creating Configuration Data Block that has the Certificate Information for DSC Configuration Processing $ConfigurationData = @{ AllNodes = @( @{ NodeName="*" CertificateFile = "C:\awssample\publickeys\SamplePublicKey.cer" Thumbprint = $DscCertThumbprint PSDscAllowDomainUser = $true }, @{ NodeName = 'localhost' } ) } Configuration DomainJoin { param( [PSCredential] $Credentials ) Import-Module -Name PSDesiredStateConfiguration Import-Module -Name ComputerManagementDsc Import-DscResource -Module PSDesiredStateConfiguration Import-DscResource -Module ComputerManagementDsc Node 'localhost' { Computer JoinDomain { Name = $NewName DomainName = $DomainDNSName Credential = $Credentials } } } DomainJoin -OutputPath 'C:\awssample\DomainJoin' -ConfigurationData $ConfigurationData -Credentials $Credentials WriteInstallModuleScript: Type: Custom::WriteScript Properties: ServiceToken: !GetAtt WriteScriptFunction.Arn Bucket: !Ref DSCBucket Key: "install-modules.ps1" Body: | [CmdletBinding()] param() "Setting up Powershell Gallery to Install DSC Modules" Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force Set-PSRepository -Name PSGallery -InstallationPolicy Trusted "Installing the needed Powershell DSC modules for this Quick Start" Install-Module -Name ComputerManagementDsc Install-Module -Name PSDscResources "Creating Directory for DSC Public Cert" New-Item -Path C:\awssample\publickeys -ItemType directory -Force "Setting up DSC Certificate to Encrypt Credentials in MOF File" $cert = New-SelfSignedCertificate -Type DocumentEncryptionCertLegacyCsp -DnsName 'SampleDscEncryptCert' -HashAlgorithm SHA256 # Exporting the public key certificate $cert | Export-Certificate -FilePath "C:\awssample\publickeys\SamplePublicKey.cer" -Force WriteLCMConfigScript: Type: Custom::WriteScript Properties: ServiceToken: !GetAtt WriteScriptFunction.Arn Bucket: !Ref DSCBucket Key: "LCM-Config.ps1" Body: | # This block sets the LCM configuration to what we need for QS [DSCLocalConfigurationManager()] configuration LCMConfig { Node 'localhost' { Settings { RefreshMode = 'Push' ActionAfterReboot = 'StopConfiguration' RebootNodeIfNeeded = $false CertificateId = $DscCertThumbprint } } } $DscCertThumbprint = [string](get-childitem -path cert:\LocalMachine\My | where { $_.subject -eq "CN=SampleDscEncryptCert" }).Thumbprint #Generates MOF File for LCM LCMConfig -OutputPath 'C:\awssample\LCMConfig' # Sets LCM Configuration to MOF generated in previous command Set-DscLocalConfigurationManager -Path 'C:\awssample\LCMConfig' DomainJoinAutomation: Type: AWS::SSM::Document Properties: DocumentType: Automation Content: schemaVersion: "0.3" description: "Join a Windows Domain" # Role that is utilized to perform the steps within the Automation Document. assumeRole: "{{AutomationAssumeRole}}" # Gathering parameters needed to configure DCs in the Quick Start parameters: InstanceId: description: "ID of the Instance." type: "StringList" DomainDNSName: default: "example.com" description: "Fully qualified domain name (FQDN) of the forest root domain e.g. example.com" type: "String" DomainNetBIOSName: default: "example" description: "NetBIOS name of the domain (up to 15 characters) for users of earlier versions of Windows e.g. EXAMPLE" type: "String" AdminSecrets: description: "AWS Secrets Parameter Name that has Password and User name for a domain administrator." type: "String" S3BucketName: description: "S3 bucket name for the Quick Start assets. Quick Start bucket name can include numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-)." type: "String" AutomationAssumeRole: default: "" description: "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf." type: "String" mainSteps: # This step Demonstrates how to run a local script on an Instance. It can be defined or pointed to a local script. - name: "InstallDSCModules" action: "aws:runCommand" inputs: DocumentName: "AWS-RunRemoteScript" InstanceIds: - "{{InstanceId}}" CloudWatchOutputConfig: CloudWatchOutputEnabled: "true" CloudWatchLogGroupName: !Sub '/ssm/${AWS::StackName}' Parameters: sourceType: "S3" sourceInfo: '{"path": "https://{{S3BucketName}}.s3.amazonaws.com/install-modules.ps1"}' commandLine: "./install-modules.ps1" - name: "ConfigureLCM" action: "aws:runCommand" inputs: DocumentName: "AWS-RunRemoteScript" InstanceIds: - "{{InstanceId}}" CloudWatchOutputConfig: CloudWatchOutputEnabled: "true" CloudWatchLogGroupName: !Sub '/ssm/${AWS::StackName}' Parameters: sourceType: "S3" sourceInfo: '{"path": "https://{{S3BucketName}}.s3.amazonaws.com/LCM-Config.ps1"}' commandLine: "./LCM-Config.ps1" - name: "GenerateDomainJoinMof" action: "aws:runCommand" inputs: DocumentName: "AWS-RunRemoteScript" InstanceIds: - "{{InstanceId}}" CloudWatchOutputConfig: CloudWatchOutputEnabled: "true" CloudWatchLogGroupName: !Sub '/ssm/${AWS::StackName}' Parameters: sourceType: "S3" sourceInfo: '{"path": "https://{{S3BucketName}}.s3.amazonaws.com/DomainJoin.ps1"}' commandLine: "./DomainJoin.ps1 -DomainNetBIOSName {{DomainNetBIOSName}} -DomainDNSName {{DomainDNSName}} -AdminSecret {{AdminSecrets}}" - name: "DomainJoin" action: aws:runCommand inputs: DocumentName: AWS-RunPowerShellScript InstanceIds: - "{{InstanceId}}" CloudWatchOutputConfig: CloudWatchOutputEnabled: "true" CloudWatchLogGroupName: !Sub '/ssm/${AWS::StackName}' Parameters: commands: - | function DscStatusCheck () { $LCMState = (Get-DscLocalConfigurationManager).LCMState if ($LCMState -eq 'PendingConfiguration' -Or $LCMState -eq 'PendingReboot') { 'returning 3010, should continue after reboot' exit 3010 } else { 'Completed' } } Start-DscConfiguration 'C:\awssample\DomainJoin' -Wait -Verbose -Force DscStatusCheck SSMExecutionRole: Type : AWS::IAM::Role Properties: Policies: - PolicyDocument: Version: '2012-10-17' Statement: - Action: - ssm:StartAssociationsOnce - ssm:CreateAssociation - ssm:CreateAssociationBatch - ssm:UpdateAssociation Resource: '*' Effect: Allow PolicyName: ssm-association Path: / ManagedPolicyArns: - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonSSMAutomationRole' AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "ec2.amazonaws.com" - "ssm.amazonaws.com" Action: "sts:AssumeRole" SSMInstanceRole: Type : AWS::IAM::Role Properties: Policies: - PolicyDocument: Version: '2012-10-17' Statement: - Action: - s3:GetObject Resource: - !Sub 'arn:aws:s3:::aws-ssm-${AWS::Region}/*' - !Sub 'arn:aws:s3:::aws-windows-downloads-${AWS::Region}/*' - !Sub 'arn:aws:s3:::amazon-ssm-${AWS::Region}/*' - !Sub 'arn:aws:s3:::amazon-ssm-packages-${AWS::Region}/*' - !Sub 'arn:aws:s3:::${AWS::Region}-birdwatcher-prod/*' - !Sub 'arn:aws:s3:::patch-baseline-snapshot-${AWS::Region}/*' Effect: Allow PolicyName: ssm-custom-s3-policy - PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - secretsmanager:GetSecretValue - secretsmanager:DescribeSecret Resource: - !Ref 'DomainJoinSecrets' PolicyName: ssm-secrets-policy - PolicyDocument: Version: '2012-10-17' Statement: - Action: - s3:GetObject - s3:PutObject - s3:PutObjectAcl - s3:ListBucket Resource: - !Sub 'arn:${AWS::Partition}:s3:::${DSCBucket}/*' - !Sub 'arn:${AWS::Partition}:s3:::${DSCBucket}' Effect: Allow PolicyName: s3-instance-bucket-policy Path: / ManagedPolicyArns: - !Sub 'arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore' - !Sub 'arn:${AWS::Partition}:iam::aws:policy/CloudWatchAgentServerPolicy' AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "ec2.amazonaws.com" - "ssm.amazonaws.com" Action: "sts:AssumeRole" SSMInstanceProfile: Type: "AWS::IAM::InstanceProfile" Properties: Roles: - !Ref SSMInstanceRole WINEC2Instance: Type: "AWS::EC2::Instance" Properties: ImageId: !Ref LatestAmiId InstanceType: !Ref EC2InstanceType IamInstanceProfile: !Ref SSMInstanceProfile NetworkInterfaces: - DeleteOnTermination: true DeviceIndex: '0' SubnetId: !Ref 'SubnetID' GroupSet: - !Ref DomainMemberSGID Tags: - Key: "Name" Value: "WindowsBox1" DomainAssociation: Type: AWS::SSM::Association Properties: AssociationName: DomainJoin # We are using the AWS-ApplyDSCMofs Document Name: !Ref DomainJoinAutomation WaitForSuccessTimeoutSeconds: 600 AutomationTargetParameterName: InstanceId Targets: - Key: ParameterValues Values: - !Ref WINEC2Instance OutputLocation: S3Location: OutputS3BucketName: !Ref DSCBucket OutputS3KeyPrefix: 'logs/' Parameters: DomainDNSName: - !Ref DomainDNSName DomainNetBIOSName: - !Ref DomainNetBIOSName AdminSecrets: - !Ref DomainJoinSecrets S3BucketName: - !Ref DSCBucket AutomationAssumeRole: - !GetAtt 'SSMExecutionRole.Arn'