cloudformation/ad/ad-integration.yaml (903 lines of code) (raw):
Description: AWS ParallelCluster ActiveDirectory Database
Parameters:
DomainName:
Description: AD Domain Name.
Type: String
Default: corp.example.com
AllowedPattern: ^([a-zA-Z0-9]+[\\.-])+([a-zA-Z0-9])+$
AdminPassword:
Description: AD Admin Password.
Type: String
MinLength: 8
MaxLength: 64
AllowedPattern: (?=^.{8,64}$)((?=.*\d)(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[^A-Za-z0-9\s])(?=.*[a-z])|(?=.*[^A-Za-z0-9\s])(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[A-Z])(?=.*[^A-Za-z0-9\s]))^.*
NoEcho: true
ReadOnlyPassword:
Description: AD ReadOnlyUser Password.
Type: String
MinLength: 8
MaxLength: 64
AllowedPattern: (?=^.{8,64}$)((?=.*\d)(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[^A-Za-z0-9\s])(?=.*[a-z])|(?=.*[^A-Za-z0-9\s])(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[A-Z])(?=.*[^A-Za-z0-9\s]))^.*
NoEcho: true
UserNames:
Description: Comma separated cluster users to create in the Active Directory.
Type: String
Default: user000
MinLength: 3
UserPassword:
Description: Cluster user Password for all the users specified in 'Users'.
Type: String
MinLength: 8
MaxLength: 64
AllowedPattern: (?=^.{8,64}$)((?=.*\d)(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[^A-Za-z0-9\s])(?=.*[a-z])|(?=.*[^A-Za-z0-9\s])(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[A-Z])(?=.*[^A-Za-z0-9\s]))^.*
NoEcho: true
DirectoryType:
Description: Type of directory to use
Type: String
Default: MicrosoftAD
AllowedValues:
- MicrosoftAD
- SimpleAD
Keypair:
Description: EC2 Keypair to access management instance.
Type: AWS::EC2::KeyPair::KeyName
Vpc:
Description: VPC ID to create the Directory Service in (leave BLANK to create a new one.)
Type: String
# Type: AWS::EC2::VPC::Id
Default: ""
# Ad:
# Description: AD ID
# Type: String
# Default: ""
PrivateSubnetOne:
Description: Subnet ID of the first subnet. If the VPC is left blank, this will be created.
Type: String
# Type: AWS::EC2::Subnet::Id
Default: ""
PrivateSubnetTwo:
Description: Subnet ID of the second subnet. This subnet should be in another availability zone. . If the VPC is left blank, this will be created.
Type: String
# Type: AWS::EC2::Subnet::Id
Default: ""
AdminNodeAmiId:
Description: AMI for the Admin Node
Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
Default: '/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64'
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Directory
Parameters:
- DirectoryType
- DomainName
- Vpc
- PrivateSubnetOne
- PrivateSubnetTwo
- Label:
default: Users
Parameters:
- AdminPassword
- ReadOnlyPassword
- UserNames
- UserPassword
- Label:
default: Administrative Node
Parameters:
- AdminNodeAmiId
- Keypair
Transform: AWS::Serverless-2016-10-31
Conditions:
CreateVPC: !Equals [!Ref Vpc, '']
# CreateAD: !Equals [!Ref Ad, '']
CreateAD: !Equals ['', '']
UseMicrosoftAD:
!Equals [ !Ref DirectoryType, MicrosoftAD ]
UseSimpleAD:
!Equals [ !Ref DirectoryType, SimpleAD ]
Resources:
DisableImdsv1LaunchTemplate:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateData:
MetadataOptions:
HttpEndpoint: enabled
HttpPutResponseHopLimit: 4
HttpTokens: required
PrepRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Action:
- sts:AssumeRole
Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Policies:
- PolicyName: LogOutput
PolicyDocument:
Version: 2012-10-17
Statement:
- Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Effect: Allow
# Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${AWS::StackName}-Prep:*
Resource: '*'
- PolicyName: DescribeDirectory
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- ds:DescribeDirectories
# Resource: !Sub arn:${AWS::Partition}:ds:*:${AWS::AccountId}:directory/*
Resource: '*'
PclusterVpc:
Type: AWS::EC2::VPC
Condition: CreateVPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
InstanceTenancy: default
PclusterVpcIGW:
Type: AWS::EC2::InternetGateway
Condition: CreateVPC
PclusterVpcSubnet1Route:
Type: AWS::EC2::Route
Condition: CreateVPC
DependsOn:
- PclusterVpcGWA
Properties:
DestinationCidrBlock: 0.0.0.0/0
GatewayId:
Ref: PclusterVpcIGW
RouteTableId:
Ref: PclusterVpcSubnet1RouteTable
PclusterVpcSubnet1RouteTable:
Type: AWS::EC2::RouteTable
Condition: CreateVPC
Properties:
VpcId:
Ref: PclusterVpc
PclusterVpcSubnet1RTA:
Type: AWS::EC2::SubnetRouteTableAssociation
Condition: CreateVPC
Properties:
RouteTableId:
Ref: PclusterVpcSubnet1RouteTable
SubnetId:
Ref: PclusterVpcSubnet1
PclusterVpcSubnet1:
Type: AWS::EC2::Subnet
Condition: CreateVPC
Properties:
AvailabilityZone: !Sub ${AWS::Region}a
CidrBlock: 10.0.0.0/17
MapPublicIpOnLaunch: true
VpcId:
Ref: PclusterVpc
PclusterVpcSunet1Route:
Type: AWS::EC2::Route
Condition: CreateVPC
DependsOn:
- PclusterVpcGWA
Properties:
DestinationCidrBlock: 0.0.0.0/0
GatewayId:
Ref: PclusterVpcIGW
RouteTableId:
Ref: PclusterVpcSubnet2RouteTable
PclusterVpcSubnet2RouteTable:
Type: AWS::EC2::RouteTable
Condition: CreateVPC
Properties:
VpcId:
Ref: PclusterVpc
PclusterVpcSubnet2RTA:
Type: AWS::EC2::SubnetRouteTableAssociation
Condition: CreateVPC
Properties:
RouteTableId:
Ref: PclusterVpcSubnet2RouteTable
SubnetId:
Ref: PclusterVpcSubnet2
PclusterVpcSubnet2:
Type: AWS::EC2::Subnet
Condition: CreateVPC
Properties:
AvailabilityZone: !Sub ${AWS::Region}b
CidrBlock: 10.0.128.0/17
MapPublicIpOnLaunch: true
VpcId:
Ref: PclusterVpc
PclusterVpcGWA:
Type: AWS::EC2::VPCGatewayAttachment
Condition: CreateVPC
Properties:
InternetGatewayId:
Ref: PclusterVpcIGW
VpcId:
Ref: PclusterVpc
PrepLambda:
Type: AWS::Lambda::Function
Properties:
Description: !Sub "${AWS::StackName}: custom resource handler to prepare the stack."
Handler: index.handler
MemorySize: 128
Role: !GetAtt PrepRole.Arn
Runtime: python3.9
Timeout: 300
TracingConfig:
Mode: Active
Code:
ZipFile: |
import time
import cfnresponse
import boto3
import logging
import random
import string
logger = logging.getLogger()
logger.setLevel(logging.INFO)
ec2 = boto3.client("ec2")
ds = boto3.client("ds")
def create_physical_resource_id():
alnum = string.ascii_uppercase + string.ascii_lowercase + string.digits
return ''.join(random.choice(alnum) for _ in range(16))
def handler(event, context):
print(event)
print( 'boto version {}'.format(boto3.__version__))
domain = event['ResourceProperties']['DomainName']
vpc_id = event['ResourceProperties']['Vpc']
directory_id = event['ResourceProperties']['DirectoryId']
subnet1_id = event['ResourceProperties']['PrivateSubnetOne']
subnet2_id = event['ResourceProperties']['PrivateSubnetTwo']
directory = ds.describe_directories(DirectoryIds=[directory_id])['DirectoryDescriptions'][0]
dns_ip_addrs = directory['DnsIpAddrs']
response_data = {}
reason = None
response_status = cfnresponse.SUCCESS
stack_id_suffix = event['StackId'].split("/")[1]
if event['RequestType'] == 'Create':
response_data['Message'] = 'Resource creation successful!'
physical_resource_id = create_physical_resource_id()
# provide outputs
response_data['DirectoryId'] = directory_id
response_data['DomainName'] = domain
response_data['DomainShortName'] = domain.split(".")[0].upper()
response_data['VpcId'] = vpc_id
response_data['Subnet1Id'] = subnet1_id
response_data['Subnet2Id'] = subnet2_id
response_data['DnsIpAddresses'] = dns_ip_addrs
for i, addr in enumerate(dns_ip_addrs):
addr_index = i + 1
response_data[f'DnsIpAddress{addr_index}'] = addr
else:
physical_resource_id = event['PhysicalResourceId']
cfnresponse.send(event, context, response_status, response_data, physical_resource_id, reason)
MicrosoftADDirectory:
Type: AWS::DirectoryService::MicrosoftAD
Condition: UseMicrosoftAD
Properties:
Name: !Ref DomainName
Password: !Ref AdminPassword
Edition: Standard
VpcSettings:
SubnetIds:
- !If [CreateVPC, !Ref PclusterVpcSubnet1, !Ref PrivateSubnetOne ]
- !If [CreateVPC, !Ref PclusterVpcSubnet2, !Ref PrivateSubnetTwo ]
VpcId: !If [CreateVPC, !Ref PclusterVpc, !Ref Vpc ]
SimpleADDirectory:
Type: AWS::DirectoryService::SimpleAD
Condition: UseSimpleAD
Properties:
Name: !Ref DomainName
Password: !Ref AdminPassword
Size: Small
VpcSettings:
SubnetIds:
- !If [ CreateVPC, !Ref PclusterVpcSubnet1, !Ref PrivateSubnetOne ]
- !If [ CreateVPC, !Ref PclusterVpcSubnet2, !Ref PrivateSubnetTwo ]
VpcId: !If [ CreateVPC, !Ref PclusterVpc, !Ref Vpc ]
Prep:
Type: Custom::PrepLambda
Properties:
ServiceToken: !GetAtt PrepLambda.Arn
DomainName: !Ref DomainName
Vpc: !If [CreateVPC, !Ref PclusterVpc, !Ref Vpc ]
PrivateSubnetOne: !If [CreateVPC, !Ref PclusterVpcSubnet1, !Ref PrivateSubnetOne ]
PrivateSubnetTwo: !If [CreateVPC, !Ref PclusterVpcSubnet2, !Ref PrivateSubnetTwo ]
DirectoryId: !If [UseMicrosoftAD, !Ref MicrosoftADDirectory, !Ref SimpleADDirectory ]
AdDomainAdminNodeSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow SSH access
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
FromPort: -1
IpProtocol: "-1"
ToPort: -1
SecurityGroupIngress:
- CidrIp: 0.0.0.0/0
FromPort: 22
IpProtocol: tcp
ToPort: 22
VpcId: !If [CreateVPC, !Ref PclusterVpc, !Ref Vpc ]
JoinRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: !Sub ec2.${AWS::URLSuffix}
Version: "2012-10-17"
ManagedPolicyArns:
- !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore
- !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMDirectoryServiceAccess
Policies:
- PolicyDocument:
Statement:
- Action:
- ds:ResetUserPassword
- ds:DescribeDirectories
- ds:DescribeDomainControllers
Effect: Allow
Resource: !Sub
- arn:${AWS::Partition}:ds:${AWS::Region}:${AWS::AccountId}:directory/${DirectoryId}
# - { DirectoryId: !If [CreateAD, !Ref Directory, !Ref Ad ] }
- { DirectoryId: !If [UseMicrosoftAD, !Ref MicrosoftADDirectory, !Ref SimpleADDirectory ] }
PolicyName: ResetUserPassword
- PolicyDocument:
Statement:
- Action:
- secretsmanager:PutSecretValue
Effect: Allow
Resource:
- !Ref DomainCertificateSecret
- !Ref DomainPrivateKeySecret
PolicyName: PutDomainCertificateSecrets
JoinProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles:
- Ref: JoinRole
AdDomainAdminNodeWaitConditionHandle:
Type: AWS::CloudFormation::WaitConditionHandle
AdDomainAdminNodeWaitCondition:
Type: AWS::CloudFormation::WaitCondition
DependsOn:
- Prep
Properties:
Count: 1
Handle: !Ref AdDomainAdminNodeWaitConditionHandle
Timeout: 900
AdDomainAdminNode:
Type: AWS::EC2::Instance
Metadata:
"AWS::CloudFormation::Init":
configSets:
setup:
- install_dependencies
install_dependencies:
packages:
yum:
sssd: []
realmd: []
oddjob: []
oddjob-mkhomedir: []
adcli: []
samba-common: []
samba-common-tools: []
krb5-workstation: []
openldap-clients: []
openssl: []
Properties:
IamInstanceProfile:
Ref: JoinProfile
ImageId: !Ref AdminNodeAmiId
InstanceType: t3.micro
KeyName: !Ref Keypair
LaunchTemplate:
LaunchTemplateId: !Ref 'DisableImdsv1LaunchTemplate'
Version: !GetAtt 'DisableImdsv1LaunchTemplate.LatestVersionNumber'
SecurityGroupIds:
- Ref: AdDomainAdminNodeSecurityGroup
SubnetId: !If [CreateVPC, !Ref PclusterVpcSubnet1, !Ref PrivateSubnetOne ]
Tags:
- Key: "Name"
Value: !Sub [ "AdDomainAdminNode-${StackIdSuffix}", {StackIdSuffix: !Select [1, !Split ['/', !Ref 'AWS::StackId']]}]
UserData:
Fn::Base64:
!Sub
- |
#!/bin/bash -e
set -o pipefail
exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1
yum update -y aws-cfn-bootstrap
/opt/aws/bin/cfn-init -v --stack "${AWS::StackName}" --resource AdDomainAdminNode --configsets setup --region "${AWS::Region}"
echo "Directory Id: ${DirectoryId}"
echo "Domain Name: ${DirectoryDomain}"
echo "Domain DNS IP 1: ${DnsIp1}"
echo "Domain DNS IP 2: ${DnsIp2}"
echo "Domain Certificate Secret: ${DomainCertificateSecretArn}"
echo "Domain Private Key Secret: ${DomainPrivateKeySecretArn}"
echo "Describing directory..."
aws ds describe-directories --directory-id "${DirectoryId}" --region "${AWS::Region}"
echo "Describing domain controllers..."
aws ds describe-domain-controllers --directory-id "${DirectoryId}" --region "${AWS::Region}"
ADMIN_PW="${AdminPassword}"
USERNAMES="ReadOnlyUser,${UserNames}"
echo "Registering Users: $USERNAMES ..."
for username in $(echo $USERNAMES | sed "s/,/ /g")
do
attempt=0
max_attempts=5
until [ $attempt -ge $max_attempts ]; do
attempt=$((attempt+1))
echo "Registering user $username (attempt $attempt/$max_attempts) ..."
echo "$ADMIN_PW" | adcli create-user -v -x -U "${Admin}" --domain ${DirectoryDomain} --domain-controller="${DnsIp1}" --display-name="$username" "$username" && echo "User registered: $username" && break
echo "$ADMIN_PW" | adcli create-user -v -x -U "${Admin}" --domain ${DirectoryDomain} --domain-controller="${DnsIp2}" --display-name="$username" "$username" && echo "User registered: $username" && break
echo "User creation failed, describing directory and controllers for troubleshooting..."
aws ds describe-directories --directory-id "${DirectoryId}" --region "${AWS::Region}"
aws ds describe-domain-controllers --directory-id "${DirectoryId}" --region "${AWS::Region}"
sleep 10
done
done
echo "Creating domain certificate..."
PRIVATE_KEY="${DirectoryDomain}.key"
CERTIFICATE="${DirectoryDomain}.crt"
printf '.\n.\n.\n.\n.\n%s\n.\n' "${DirectoryDomain}" | openssl req -x509 -sha256 -nodes -newkey rsa:2048 -keyout "$PRIVATE_KEY" -days 365 -out "$CERTIFICATE"
echo "Storing domain private key to Secrets Manager..."
aws secretsmanager put-secret-value --secret-id "${DomainPrivateKeySecretArn}" --secret-string "file://$PRIVATE_KEY" --region "${AWS::Region}"
echo "Storing domain certificate to Secrets Manager..."
aws secretsmanager put-secret-value --secret-id "${DomainCertificateSecretArn}" --secret-string "file://$CERTIFICATE" --region "${AWS::Region}"
echo "Deleting private key and certificate from local file system..."
rm -rf "$PRIVATE_KEY" "$CERTIFICATE"
/opt/aws/bin/cfn-signal -e "$?" --stack "${AWS::StackName}" --region "${AWS::Region}" "${AdDomainAdminNodeWaitConditionHandle}"
- { DirectoryId: !GetAtt Prep.DirectoryId,
DirectoryDomain: !GetAtt Prep.DomainName,
AdminPassword: !Ref AdminPassword,
UserNames: !Ref UserNames,
DnsIp1: !GetAtt Prep.DnsIpAddress1,
DnsIp2: !GetAtt Prep.DnsIpAddress2,
DomainCertificateSecretArn: !Ref DomainCertificateSecret,
DomainPrivateKeySecretArn: !Ref DomainPrivateKeySecret,
Admin: !If [UseMicrosoftAD, "Admin", "Administrator" ],
AdDomainAdminNodeWaitConditionHandle: !Ref AdDomainAdminNodeWaitConditionHandle
}
PostRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Action:
- sts:AssumeRole
Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Policies:
- PolicyName: LogOutput
PolicyDocument:
Version: 2012-10-17
Statement:
- Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Effect: Allow
Resource: '*'
- PolicyName: ResetPassword
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- ds:ResetUserPassword
Resource: !Sub
- arn:${AWS::Partition}:ds:${AWS::Region}:${AWS::AccountId}:directory/${DirectoryId}
# - { DirectoryId: !If [CreateAD, !Ref Directory, !Ref Ad ] }
- { DirectoryId: !If [UseMicrosoftAD, !Ref MicrosoftADDirectory, !Ref SimpleADDirectory ] }
- PolicyName: StopInstances
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- ec2:StopInstances
Resource: !Sub
- arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/${InstanceId}
- { InstanceId: !Ref AdDomainAdminNode }
PostLambda:
Type: AWS::Lambda::Function
Properties:
Description: !Sub "${AWS::StackName}: custom resource handler to finish setting up stack after other resources have been created."
Handler: index.handler
MemorySize: 128
Role: !GetAtt PostRole.Arn
Runtime: python3.9
Timeout: 300
TracingConfig:
Mode: Active
Code:
ZipFile: |
import time
import cfnresponse
import boto3
import logging
import random
import string
logger = logging.getLogger()
logger.setLevel(logging.INFO)
ds = boto3.client("ds")
ec2 = boto3.client("ec2")
def create_physical_resource_id():
alnum = string.ascii_uppercase + string.ascii_lowercase + string.digits
return ''.join(random.choice(alnum) for _ in range(16))
def redact_keys(event: dict, redactions: set):
ret = {}
for k in event.keys():
if k in redactions:
ret[k] = "[REDACTED]"
else:
ret[k] = redact_keys(event[k], redactions) if type(event[k]) is dict else event[k] # handle nesting
return ret
def handler(event, context):
print(redact_keys(event, {"ReadOnlyPassword", "UserPassword", "AdminPassword"}))
print( 'boto version {}'.format(boto3.__version__))
directory_id = event['ResourceProperties']['DirectoryId']
instance_id = event['ResourceProperties']['AdminNodeInstanceId']
read_only_password = event['ResourceProperties']['ReadOnlyPassword']
user_names = event['ResourceProperties']['UserNames']
user_password = event['ResourceProperties']['UserPassword']
admin_password = event['ResourceProperties']['AdminPassword']
admin = event['ResourceProperties']['Admin']
response_data = {}
reason = None
response_status = cfnresponse.SUCCESS
if event['RequestType'] == 'Create':
response_data['Message'] = 'Resource creation successful!'
physical_resource_id = create_physical_resource_id()
ds.reset_user_password(DirectoryId=directory_id, UserName='ReadOnlyUser', NewPassword=read_only_password)
for name in user_names.split(","):
name = name.strip()
ds.reset_user_password(DirectoryId=directory_id, UserName=name, NewPassword=user_password)
ds.reset_user_password(DirectoryId=directory_id, UserName=admin, NewPassword=admin_password)
ec2.stop_instances(InstanceIds=[instance_id])
else:
physical_resource_id = event['PhysicalResourceId']
cfnresponse.send(event, context, response_status, response_data, physical_resource_id, reason)
Post:
Type: Custom::PostLambda
Properties:
ServiceToken: !GetAtt PostLambda.Arn
AdminNodeInstanceId: !Ref AdDomainAdminNode
# DirectoryId: !If [CreateAD, !Ref Directory, !Ref Ad ]
DirectoryId: !If [UseMicrosoftAD, !Ref MicrosoftADDirectory, !Ref SimpleADDirectory ]
UserNames: !Ref UserNames
UserPassword: !Ref UserPassword
AdminPassword: !Ref AdminPassword
ReadOnlyPassword: !Ref ReadOnlyPassword
Admin: !If [UseMicrosoftAD, "Admin", "Administrator" ]
PasswordSecret:
Type: AWS::SecretsManager::Secret
Properties:
Description: ReadOnly password for AD
Name: !Sub [ "PasswordSecret-${StackIdSuffix}", {StackIdSuffix: !Select [1, !Split ['/', !Ref 'AWS::StackId']]}]
SecretString: !Ref ReadOnlyPassword
UserPasswordSecret:
Type: AWS::SecretsManager::Secret
Properties:
Description: User password for AD
Name: !Sub [ "UserPasswordSecret-${StackIdSuffix}", { StackIdSuffix: !Select [ 1, !Split [ '/', !Ref 'AWS::StackId' ] ] } ]
SecretString: !Ref UserPassword
DomainCertificateSecret:
Type: AWS::SecretsManager::Secret
Properties:
Description: Domain certificate
Name: !Sub [ "DomainCertificateSecret-${StackIdSuffix}", { StackIdSuffix: !Select [ 1, !Split [ '/', !Ref 'AWS::StackId' ] ] } ]
DomainPrivateKeySecret:
Type: AWS::SecretsManager::Secret
Properties:
Description: Domain private key
Name: !Sub [ "DomainPrivateKeySecret-${StackIdSuffix}", { StackIdSuffix: !Select [ 1, !Split [ '/', !Ref 'AWS::StackId' ] ] } ]
NetworkLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Scheme: internal
Subnets:
- !If [CreateVPC, !Ref PclusterVpcSubnet1, !Ref PrivateSubnetOne ]
- !If [CreateVPC, !Ref PclusterVpcSubnet2, !Ref PrivateSubnetTwo ]
Type: network
NetworkLoadBalancerTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Port: 389
Protocol: TCP
VpcId: !If [CreateVPC, !Ref PclusterVpc, !Ref Vpc ]
HealthCheckEnabled: True
HealthCheckIntervalSeconds: 10
HealthCheckPort: 389
HealthCheckProtocol: TCP
HealthCheckTimeoutSeconds: 10
HealthyThresholdCount: 3
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: 60
Targets:
- Id: !Select [0, !GetAtt Prep.DnsIpAddresses]
Port: 389
- Id: !Select [1, !GetAtt Prep.DnsIpAddresses]
Port: 389
TargetType: ip
DNS:
Type: AWS::Route53::HostedZone
Properties:
Name: !Ref DomainName
VPCs:
- VPCId: !If [ CreateVPC, !Ref PclusterVpc, !Ref Vpc ]
VPCRegion: !Ref AWS::Region
DNSRecord:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref DNS
Name: !Ref DomainName
AliasTarget:
DNSName: !GetAtt NetworkLoadBalancer.DNSName
HostedZoneId: !GetAtt NetworkLoadBalancer.CanonicalHostedZoneID
Type: A
NetworkLoadBalancerListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- Type: forward
TargetGroupArn: !Ref NetworkLoadBalancerTargetGroup
LoadBalancerArn: !Ref NetworkLoadBalancer
Port: '636'
Protocol: TLS
SslPolicy: ELBSecurityPolicy-TLS-1-2-2017-01
Certificates:
- CertificateArn: !GetAtt DomainCertificateSetup.DomainCertificateArn
DomainCertificateSetupLambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Action:
- sts:AssumeRole
Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Policies:
- PolicyName: LogOutput
PolicyDocument:
Version: 2012-10-17
Statement:
- Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Effect: Allow
Resource: '*'
- PolicyName: ManageDomainCertificate
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- iam:UploadServerCertificate
- iam:TagServerCertificate
Resource: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:server-certificate/*
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
- !Ref DomainCertificateSecret
- !Ref DomainPrivateKeySecret
DomainCertificateSetupLambda:
Type: AWS::Lambda::Function
Properties:
Description: !Sub "${AWS::StackName}: custom resource handler to import the domain certificate into IAM."
Handler: index.handler
MemorySize: 128
Role: !GetAtt DomainCertificateSetupLambdaRole.Arn
Runtime: python3.9
Timeout: 300
TracingConfig:
Mode: Active
Code:
ZipFile: |
import time
import cfnresponse
import boto3
import logging
import random
import string
logger = logging.getLogger()
logger.setLevel(logging.INFO)
iam = boto3.client("iam")
sm = boto3.client("secretsmanager")
def create_physical_resource_id():
alnum = string.ascii_uppercase + string.ascii_lowercase + string.digits
return ''.join(random.choice(alnum) for _ in range(16))
def import_certificate(certificate_secret_arn, private_key_secret_arn, domain_name, tags):
logger.info('Reading secrets from Secrets Manager...')
domain_certificate = sm.get_secret_value(SecretId=certificate_secret_arn)["SecretString"]
domain_private_key = sm.get_secret_value(SecretId=private_key_secret_arn)["SecretString"]
logger.info('Importing certificate into IAM...')
certificate_arn = iam.upload_server_certificate(
ServerCertificateName=domain_name,
CertificateBody=domain_certificate,
PrivateKey=domain_private_key, Tags=tags
)["ServerCertificateMetadata"]["Arn"]
return certificate_arn
def handler(event, context):
logger.info(f"Context: {context}")
logger.info(f"Event: {event}")
logger.info(f"Boto version: {boto3.__version__}")
domain_name = event['ResourceProperties']['DomainName']
certificate_secret_arn = event['ResourceProperties']['DomainCertificateSecretArn']
private_key_secret_arn = event['ResourceProperties']['DomainPrivateKeySecretArn']
tags = [{ 'Key': 'StackId', 'Value': event['StackId']}]
response_data = {}
reason = None
response_status = cfnresponse.SUCCESS
physical_resource_id = event.get("PhysicalResourceId", create_physical_resource_id())
try:
if event['RequestType'] == 'Create':
certificate_arn = import_certificate(certificate_secret_arn, private_key_secret_arn, domain_name, tags)
time.sleep(60)
response_data['DomainCertificateArn'] = certificate_arn
response_data['Message'] = f"Resource creation successful! IAM certificate imported: {certificate_arn}"
except Exception as e:
response_status = cfnresponse.FAILED
reason = str(e)
cfnresponse.send(event, context, response_status, response_data, physical_resource_id, reason)
DomainCertificateSetup:
Type: Custom::DomainCertificateSetupLambda
DependsOn:
- Post
Properties:
ServiceToken: !GetAtt DomainCertificateSetupLambda.Arn
DomainName: !Ref DomainName
DomainCertificateSecretArn: !Ref DomainCertificateSecret
DomainPrivateKeySecretArn: !Ref DomainPrivateKeySecret
CleanupLambdaRole:
Type: AWS::IAM::Role
DependsOn:
- DomainCertificateSetup
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Action:
- sts:AssumeRole
Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Policies:
- PolicyName: LogOutput
PolicyDocument:
Version: 2012-10-17
Statement:
- Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Effect: Allow
Resource: '*'
- PolicyName: DeleteDomainCertificate
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- iam:DeleteServerCertificate
Resource: !GetAtt DomainCertificateSetup.DomainCertificateArn
CleanupLambda:
Type: AWS::Lambda::Function
Properties:
Description: !Sub "${AWS::StackName}: custom resource handler to cleanup resources."
Handler: index.handler
MemorySize: 128
Role: !GetAtt CleanupLambdaRole.Arn
Runtime: python3.9
Timeout: 900
TracingConfig:
Mode: Active
Code:
ZipFile: |
import time
import cfnresponse
import boto3
import logging
import random
import string
logger = logging.getLogger()
logger.setLevel(logging.INFO)
iam = boto3.client("iam")
def create_physical_resource_id():
alnum = string.ascii_uppercase + string.ascii_lowercase + string.digits
return ''.join(random.choice(alnum) for _ in range(16))
def delete_certificate(domain_name, certificate_arn):
logger.info(f"Deleting iam certificate {certificate_arn}...")
max_attempts = 10
sleep_time = 60
for attempt in range(1, max_attempts+1):
try:
iam.delete_server_certificate(ServerCertificateName=domain_name)
break
except iam.exceptions.DeleteConflictException as e:
logger.info(f"(Attempt {attempt}/{max_attempts}) Cannot delete IAM certificate because it is in use. Retrying in {sleep_time} seconds...")
if attempt == max_attempts:
raise Exception(f"Cannot delete certificate {certificate_arn}: {e}")
else:
time.sleep(sleep_time)
def handler(event, context):
logger.info(f"Context: {context}")
logger.info(f"Event: {event}")
logger.info(f"Boto version: {boto3.__version__}")
response_data = {}
reason = None
response_status = cfnresponse.SUCCESS
physical_resource_id = event.get("PhysicalResourceId", create_physical_resource_id())
try:
if event['RequestType'] == 'Delete':
certificate_arn = event['ResourceProperties']['DomainCertificateArn']
domain_name = event['ResourceProperties']['DomainName']
delete_certificate(domain_name, certificate_arn)
except Exception as e:
response_status = cfnresponse.FAILED
reason = str(e)
cfnresponse.send(event, context, response_status, response_data, physical_resource_id, reason)
Cleanup:
Type: Custom::CleanupLambda
DependsOn:
- DomainCertificateSetup
Properties:
ServiceToken: !GetAtt CleanupLambda.Arn
DomainName: !Ref DomainName
DomainCertificateArn: !GetAtt DomainCertificateSetup.DomainCertificateArn
DomainCertificateSecretReadPolicy:
Type: AWS::IAM::ManagedPolicy
DependsOn:
- DomainCertificateSecret
Properties:
ManagedPolicyName: !Sub [ "DomainCertificateSecretReadPolicy-${StackIdSuffix}", { StackIdSuffix: !Select [ 1, !Split [ '/', !Ref 'AWS::StackId' ] ] } ]
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
- !Ref DomainCertificateSecret
Outputs:
VpcId:
Value: !If [CreateVPC, !Ref PclusterVpc, !Ref Vpc ]
PrivateSubnetIds:
Value: !Join [",", [!If [CreateVPC, !Ref PclusterVpcSubnet1, !Ref PrivateSubnetOne ], !If [CreateVPC, !Ref PclusterVpcSubnet2, !Ref PrivateSubnetTwo ]]]
DomainName:
Value: !Ref DomainName
PasswordSecretArn:
Value: !Ref PasswordSecret
UserPasswordSecretArn:
Value: !Ref UserPasswordSecret
DomainCertificateArn:
Value: !GetAtt DomainCertificateSetup.DomainCertificateArn
DomainCertificateSecretArn:
Value: !Ref DomainCertificateSecret
DomainReadOnlyUser:
Value: !Sub
- cn=ReadOnlyUser,${type}=Users${ou}${dsn},dc=${dc}
- { type: !If [UseMicrosoftAD, "ou", "cn" ],
dc: !Join [",dc=", !Split [".", !Ref DomainName ]],
ou: !If [UseMicrosoftAD, ",ou=", "" ],
dsn: !If [UseMicrosoftAD, !GetAtt Prep.DomainShortName, "" ]
}
DomainAddrLdap:
Value: !Sub
- ldap://${address}
- address: !Join [",ldap://", !GetAtt Prep.DnsIpAddresses]
DomainAddrLdaps:
Value: !Sub ldaps://${DomainName}
DomainCertificateSecretReadPolicy:
Value: !Ref DomainCertificateSecretReadPolicy