AWSTemplateFormatVersion: '2010-09-09' Description: >- Creates a single Mac OS EC2 instance accoring to parameters provided. The instance is configured to join a Domain and mount an EFS file system. Metadata: cfn-lint: config: ignore_checks: - W9006 - E9101 Parameters: SubnetID: Description: ID of subnet to launch the instance in (e.g., subnet-a0246dcd) Type: AWS::EC2::Subnet::Id MacInstanceNetBIOSName: Description: NetBIOS Name of the instance Type: 'String' MacImageID: Description: AMI to use for the instance Type: AWS::EC2::Image::Id DomainDNSName: AllowedPattern: '[a-zA-Z0-9\-]+\..+' Description: Fully qualified domain name (FQDN) of the AD to be joined MaxLength: '255' MinLength: '2' Type: String DomainNetBIOSName: AllowedPattern: '[a-zA-Z0-9\-]+' Description: NetBIOS name of the AD domain to be joined (up to 15 characters) MaxLength: '15' MinLength: '1' Type: String KeyPairName: Description: Public/private key pairs allow you to securely connect to your instance after it launches Type: AWS::EC2::KeyPair::KeyName DomainJoinSecret: Description: ARN of the secret to join the domain Type: String MacHost: Description: ID of the dedicated bare metal mac host Type: String MacSG: Description: Security group ID of the Mac Instance Type: AWS::EC2::SecurityGroup::Id MacInstanceProfile: Description: IAM Instance Profile ID of the Mac Instance Type: String EFSID: Description: ID of the EFS Type: String Resources: MacInstance: Type: AWS::EC2::Instance CreationPolicy: ResourceSignal: Timeout: PT45M Count: 1 Properties: IamInstanceProfile: !Ref MacInstanceProfile HostId: !Ref MacHost SubnetId: !Ref SubnetID ImageId: !Ref MacImageID InstanceType: mac1.metal KeyName: !Ref KeyPairName SecurityGroupIds: - !Ref MacSG Tags: - Key: Name Value: !Ref 'MacInstanceNetBIOSName' - Key: Domain Value: !Ref 'DomainDNSName' UserData: Fn::Base64: !Sub | #!/bin/bash -e # Exit in case the userdata has been run already. if [[ -f /var/log/amazon/ec2/userdata.log ]]; then exit; fi # Log userdata output exec > /var/log/amazon/ec2/userdata.log 2>&1 # Install homebrew packages echo "Installing packages." su - ec2-user -c '/usr/local/bin/brew update' su - ec2-user -c '/usr/local/bin/brew install jq amazon-efs-utils' # Finalize install of amazon-efs-utils mkdir -p /Library/Filesystems/efs.fs/Contents/Resources ln -s /usr/local/bin/mount.efs /Library/Filesystems/efs.fs/Contents/Resources/mount_efs cp /usr/local/opt/amazon-efs-utils/libexec/amazon-efs-mount-watchdog.plist /Library/LaunchAgents # Install cfn-signal and setup the error handler /usr/bin/pip3 install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz function exit_handler { exit_status=$? SUCCESS=false if [[ $exit_status -eq 0 ]]; then SUCCESS=true fi # Signal success/failure back to CloudFormation wait condition /usr/local/bin/cfn-signal --success $SUCCESS --stack ${AWS::StackId} --resource MacInstance --region ${AWS::Region} } # Configure exit handler to signal script status back to CFN trap "exit_handler" EXIT # --------- Function definitions function to_lower { echo "$1" | tr '[:upper:]' '[:lower:]' } function convert_domain { dotted_domain=$1 dc_domain="" for dc in $(echo $dotted_domain |tr '.' ' '); do dc_domain="${!dc_domain}DC=$dc," done echo $dc_domain |rev |cut -c 2- |rev } function find_iface { ip=$1 ifaces=($(networksetup -listallhardwareports | grep "Device:" | grep -v "pci-" | awk '{print $2}' |tr '\n' ' ')) for i in ${!ifaces[*]}; do ifconfig $i | grep $ip &> /dev/null && echo $i && return done } # --------- Set the hostname echo "Setting machine host name." scutil --set HostName $(to_lower ${MacInstanceNetBIOSName}) scutil --set LocalHostName $(to_lower ${MacInstanceNetBIOSName}) scutil --set ComputerName $(to_lower ${MacInstanceNetBIOSName}) dscacheutil -flushcache # Find the interface on the local VPC subnet iface=$(find_iface $(curl -s http://169.254.169.254/latest/meta-data/local-ipv4)) # --------- Join the domain echo "Joining Domain ${DomainDNSName}." DOMAIN_JOIN_PW=$(aws secretsmanager get-secret-value --region ${AWS::Region} --secret-id ${DomainJoinSecret} --query SecretString --output text | jq -r .awsSeamlessDomainPassword) DOMAIN_JOIN_USER=$(aws secretsmanager get-secret-value --region ${AWS::Region} --secret-id ${DomainJoinSecret} --query SecretString --output text | jq -r .awsSeamlessDomainUsername) dsconfigad \ -add ${DomainDNSName} \ -computer $(to_lower ${MacInstanceNetBIOSName}) \ -user $DOMAIN_JOIN_USER \ -password $DOMAIN_JOIN_PW \ -ou "OU=Computers,OU=$(to_lower ${DomainNetBIOSName}),$(convert_domain ${DomainDNSName})" \ -useuncpath disable \ -shell "/bin/zsh" \ -sharepoint disable \ -restrictDDNS $iface \ -localhome enable # --------- Setup ssh access echo "Setting up SSH for AD users." mkdir -p /opt/authorized-keys-command cat <<'EOT' >> /opt/authorized-keys-command/ssh-ldap-wrappar-macos #!/bin/bash if [[ "Darwin" != "$( uname -s )" ]]; then exit 1 ;fi MKHOME_SCRIPT="/opt/authorized-keys-command/mkhome" username=$1 if [[ "${!username:-x}" = x ]]; then exit 1 ;fi key=$(/usr/bin/dscl /Search -read "/Users/$username" dsAttrTypeNative:altSecurityIdentities 2>/dev/null | /usr/bin/awk 'NR > 1' | awk '{$1=$1};1') if test -n "$key" && ! /sbin/mount |/usr/bin/grep '127.0.0.1:/ on /opt/nfs_share (nfs)' &>/dev/null ; then sudo /opt/mount-efs.sh fi if [[ -x "$MKHOME_SCRIPT" && -n "$key" ]]; then sudo "$MKHOME_SCRIPT" "$username" 2>/dev/null fi echo $key EOT cat <<'EOT' >> /opt/authorized-keys-command/mkhome #!/bin/bash if [ "Darwin" != "$( uname -s )" ]; then exit 1 ;fi username=$1 if [[ "${!username:-x}" = x ]]; then exit 1 ;fi userhomedir=/opt/nfs_share/$username if [[ ! -d "$userhomedir" || ! -L /Users/$username ]]; then mkdir -p "$userhomedir" ln -sfn "$userhomedir" /Users/$username chown -R "$(id -u $username):$(id -g $username)" "$userhomedir" /Users/$username fi EOT chmod -R 755 /opt/authorized-keys-command chown -R root:wheel /opt/authorized-keys-command echo "_sshd ALL=(ALL) NOPASSWD: /opt/authorized-keys-command/mkhome, /opt/mount-efs.sh" > /etc/sudoers.d/sshd echo "AuthorizedKeysCommand /opt/authorized-keys-command/ssh-ldap-wrappar-macos" >> /etc/ssh/sshd_config echo "AuthorizedKeysCommandUser _sshd" >> /etc/ssh/sshd_config launchctl stop com.openssh.sshd launchctl start com.openssh.sshd # --------- Configure EFS echo "Configuring EFS share." mkdir /opt/nfs_share cat <<'EOT' >> /opt/mount-efs.sh #!/bin/bash /usr/bin/syslog -s -l 5 'Mounting NFS share /opt/nfs_share' /bin/launchctl load /Library/LaunchAgents/amazon-efs-mount-watchdog.plist /sbin/mount -t efs ${EFSID}:/ /opt/nfs_share EOT chmod 755 /opt/mount-efs.sh chown root:wheel /opt/mount-efs.sh # --------- Signal success and reboot /usr/local/bin/cfn-signal --success true --stack ${AWS::StackId} --resource MacInstance --region ${AWS::Region} echo "Bootstrapping Successfull. Rebooting." reboot Outputs: MacInstancePrivateIp: Description: Private IP address of the Mac EC2 instance Value: !GetAtt MacInstance.PrivateIp