From bf5bbb99d195c5aebd4ba1a4ce2c42bc1436f905 Mon Sep 17 00:00:00 2001 From: Jericho Tolentino <68654047+jericht@users.noreply.github.com> Date: Wed, 28 Jul 2021 16:28:16 -0500 Subject: [PATCH] feat(core): add FSx for Lustre integration (#461) --- package.json | 1 + packages/aws-rfdk/lib/core/lib/index.ts | 1 + .../lib/core/lib/mountable-fsx-lustre.ts | 123 ++++++++ .../core/scripts/bash/installLustreClient.sh | 287 ++++++++++++++++++ .../lib/core/scripts/bash/mountFsxLustre.sh | 75 +++++ .../core/test/mountable-fsx-lustre.test.ts | 202 ++++++++++++ .../aws-rfdk/lib/deadline/lib/render-queue.ts | 11 + .../aws-rfdk/lib/deadline/lib/repository.ts | 21 +- .../scripts/bash/installDeadlineRepository.sh | 87 +++++- .../lib/deadline/test/render-queue.test.ts | 8 + .../lib/deadline/test/repository.test.ts | 14 + packages/aws-rfdk/package.json | 2 + yarn.lock | 11 + 13 files changed, 826 insertions(+), 17 deletions(-) create mode 100644 packages/aws-rfdk/lib/core/lib/mountable-fsx-lustre.ts create mode 100644 packages/aws-rfdk/lib/core/scripts/bash/installLustreClient.sh create mode 100644 packages/aws-rfdk/lib/core/scripts/bash/mountFsxLustre.sh create mode 100644 packages/aws-rfdk/lib/core/test/mountable-fsx-lustre.test.ts diff --git a/package.json b/package.json index 28aff0937..b99a433b6 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@aws-cdk/aws-elasticloadbalancingv2": "1.111.0", "@aws-cdk/aws-events": "1.111.0", "@aws-cdk/aws-events-targets": "1.111.0", + "@aws-cdk/aws-fsx": "1.111.0", "@aws-cdk/aws-globalaccelerator": "1.111.0", "@aws-cdk/aws-glue": "1.111.0", "@aws-cdk/aws-iam": "1.111.0", diff --git a/packages/aws-rfdk/lib/core/lib/index.ts b/packages/aws-rfdk/lib/core/lib/index.ts index 94ec110e8..e17167fad 100644 --- a/packages/aws-rfdk/lib/core/lib/index.ts +++ b/packages/aws-rfdk/lib/core/lib/index.ts @@ -15,6 +15,7 @@ export * from './mongodb-instance'; export * from './mongodb-post-install'; export * from './mountable-ebs'; export * from './mountable-efs'; +export * from './mountable-fsx-lustre'; export * from './mountable-filesystem'; export * from './pad-efs-storage'; export { RFDK_VERSION } from './runtime-info'; diff --git a/packages/aws-rfdk/lib/core/lib/mountable-fsx-lustre.ts b/packages/aws-rfdk/lib/core/lib/mountable-fsx-lustre.ts new file mode 100644 index 000000000..b9528b481 --- /dev/null +++ b/packages/aws-rfdk/lib/core/lib/mountable-fsx-lustre.ts @@ -0,0 +1,123 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as path from 'path'; +import { + OperatingSystemType, + Port, +} from '@aws-cdk/aws-ec2'; +import { + LustreFileSystem, +} from '@aws-cdk/aws-fsx'; +import { + Asset, +} from '@aws-cdk/aws-s3-assets'; +import { + Construct, + IConstruct, + Stack, +} from '@aws-cdk/core'; +import { + MountPermissionsHelper, +} from './mount-permissions-helper'; +import { + IMountableLinuxFilesystem, + IMountingInstance, + LinuxMountPointProps, +} from './mountable-filesystem'; + +/** + * Properties that are required to create a {@link MountableFsxLustre}. + */ +export interface MountableFsxLustreProps { + /** + * The {@link https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-fsx.LustreFileSystem.html|FSx for Lustre} + * filesystem that will be mounted by the object. + */ + readonly filesystem: LustreFileSystem; + + /** + * The fileset to mount. + * @see https://docs.aws.amazon.com/fsx/latest/LustreGuide/mounting-from-fileset.html + * @default Mounts the root of the filesystem. + */ + readonly fileset?: string; + + /** + * Extra Lustre mount options that will be added to /etc/fstab for the file system. + * See: {@link http://manpages.ubuntu.com/manpages/precise/man8/mount.lustre.8.html} + * + * The given values will be joined together into a single string by commas. + * ex: ['soft', 'rsize=4096'] will become 'soft,rsize=4096' + * + * @default No extra options. + */ + readonly extraMountOptions?: string[]; +} + +/** + * This class encapsulates scripting that can be used to mount an Amazon FSx for Lustre File System onto + * an instance. + * + * Security Considerations + * ------------------------ + * - Using this construct on an instance will result in that instance dynamically downloading and running scripts + * from your CDK bootstrap bucket when that instance is launched. You must limit write access to your CDK bootstrap + * bucket to prevent an attacker from modifying the actions performed by these scripts. We strongly recommend that + * you either enable Amazon S3 server access logging on your CDK bootstrap bucket, or enable AWS CloudTrail on your + * account to assist in post-incident analysis of compromised production environments. + */ +export class MountableFsxLustre implements IMountableLinuxFilesystem { + constructor(protected readonly scope: Construct, protected readonly props: MountableFsxLustreProps) {} + + /** + * @inheritdoc + */ + public mountToLinuxInstance(target: IMountingInstance, mount: LinuxMountPointProps): void { + if (target.osType !== OperatingSystemType.LINUX) { + throw new Error('Target instance must be Linux.'); + } + + target.connections.allowTo(this.props.filesystem, this.props.filesystem.connections.defaultPort as Port); + + const mountScriptAsset = this.mountAssetSingleton(target); + mountScriptAsset.grantRead(target.grantPrincipal); + const mountScript: string = target.userData.addS3DownloadCommand({ + bucket: mountScriptAsset.bucket, + bucketKey: mountScriptAsset.s3ObjectKey, + }); + + const mountDir: string = path.posix.normalize(mount.location); + const mountOptions: string[] = [ MountPermissionsHelper.toLinuxMountOption(mount.permissions) ]; + if (this.props.extraMountOptions) { + mountOptions.push(...this.props.extraMountOptions); + } + const mountOptionsStr: string = mountOptions.join(','); + const mountName = this.props.fileset ? path.posix.join(this.props.filesystem.mountName, this.props.fileset) : this.props.filesystem.mountName; + + target.userData.addCommands( + 'TMPDIR=$(mktemp -d)', + 'pushd "$TMPDIR"', + `unzip ${mountScript}`, + 'bash ./installLustreClient.sh', + `bash ./mountFsxLustre.sh ${this.props.filesystem.fileSystemId} ${mountDir} ${mountName} ${mountOptionsStr}`, + 'popd', + `rm -f ${mountScript}`, + ); + } + + /** + * Fetch the Asset singleton for the FSx for Lustre mounting scripts, or generate it if needed. + */ + protected mountAssetSingleton(scope: IConstruct): Asset { + const stack = Stack.of(scope); + const uuid = '0db888da-5901-4948-aaa5-e71c541c8060'; + const uniqueId = 'MountableFsxLustreAsset' + uuid.replace(/[-]/g, ''); + return (stack.node.tryFindChild(uniqueId) as Asset) ?? new Asset(stack, uniqueId, { + path: path.join(__dirname, '..', 'scripts', 'bash'), + exclude: [ '**/*', '!mountFsxLustre.sh', '!installLustreClient.sh', '!metadataUtilities.sh', '!ec2-certificates.crt' ], + }); + } +} diff --git a/packages/aws-rfdk/lib/core/scripts/bash/installLustreClient.sh b/packages/aws-rfdk/lib/core/scripts/bash/installLustreClient.sh new file mode 100644 index 000000000..e7aed9991 --- /dev/null +++ b/packages/aws-rfdk/lib/core/scripts/bash/installLustreClient.sh @@ -0,0 +1,287 @@ +#!/bin/bash + +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +# This script downloads and installs the Lustre client. +# See https://docs.aws.amazon.com/fsx/latest/LustreGuide/install-lustre-client.html + +set -xeu +shopt -s extglob + +trap exit_trap EXIT +function exit_trap() { + if [[ $? -ne 0 ]]; then + echo "ERROR: An error occurred while attempting to install the Lustre client on your device. \ + Please refer to the FSx for Lustre documentation: https://docs.aws.amazon.com/fsx/latest/LustreGuide/install-lustre-client.html" + fi +} + +function is_kernel_version_lower() { + local IFS='.-' + read -r -a lhs_array <<< "$1" + read -r -a rhs_array <<< "$2" + + # Calculate minimum length betweeen the two arrays + [[ ${#lhs_array[@]} -lt ${#rhs_array[@]} ]] && n=${#lhs_array[@]} || n=${#rhs_array[@]} + + unset IFS + for i in $( seq 0 $((n-1)) ); do + local lhs="${lhs_array[$i]}" + local rhs="${rhs_array[$i]}" + if [[ $lhs =~ ^[0-9]+$ && $rhs =~ ^[0-9]+$ && $lhs -lt $rhs ]]; then + return 0 + fi + done + return 1 +} + +function install_on_al2() { + architecture=$(uname -m) + case $architecture in + x86_64) min_version="4.14.104-95.84.amzn2.x86_64";; + aarch64) min_version="4.14.181-142.260.amzn2.aarch64";; + *) echo "ERROR: Unrecognized Amazon Linux 2 kernel version: $kernel_version"; exit 1;; + esac + + kernel_version=$(uname -r) + if is_kernel_version_lower "$kernel_version" "$min_version"; then + echo "ERROR: Kernel version ($kernel_version) is lower than the minimum required version: $min_version" + exit 1 + fi + + sudo amazon-linux-extras install -y lustre2.10 +} + +function install_on_al1() { + min_version="4.14.104-78.84.amzn1.x86_64" + + kernel_version=$(uname -r) + if is_kernel_version_lower "$kernel_version" "$min_version"; then + echo "ERROR: Kernel version ($kernel_version) is lower than the minimum required version: $min_version" + exit 1 + fi + + sudo yum install -y lustre-client +} + +function verify_lustre_kmod() { + if ! sudo modprobe -v --first-time lustre; then + echo "ERROR: Lustre client kernel modules were not installed successfully. See above logs for more information." + exit 1 + fi + + sudo lustre_rmmod +} + +function install_on_rhel() { + # Note: In addition to installing the lustre client packages, an additional "kmod-lustre-client" package also needs to be + # installed. This package contains kernel-specific modules used by the lustre client. + + # Create map of RHEL release to kernel version shell patterns. See https://access.redhat.com/articles/3078 + # This mapping is needed to determine which version of the Lustre client to install based on the instructions at + # https://docs.aws.amazon.com/fsx/latest/LustreGuide/install-lustre-client.html#lustre-client-rhel + declare -A rhel_kernel_versions + rhel_kernel_versions=( + [7.5]='3.10.0-862.*' + [7.6]='3.10.0-957.*' + [7.7]='3.10.0-1062.*' + [7.8]='3.10.0-1127.*' + [7.9]='3.10.0-1160.*' + [8.1]='4.18.0-147.*' + [8.2]='4.18.0-193.*' + [8.3]='4.18.0-240.*' + [8.4]='4.18.0-305.*' + ) + + kernel_version=$(uname -r) + + id=$(grep ^ID= /etc/os-release | awk -F "=" '{print $2}' | sed s/\"//g) + if [[ $id = "centos" ]]; then + # Map CentOS release to the RHEL release it was based on + # This information can be found by referring to the CentOS release announcements + # For example, see announcement for CentOS 7 (1804): https://lists.centos.org/pipermail/centos-announce/2018-May/022829.html + # + # The major and minor versions of CentOS releases match that of RHEL, + # so we can just extract those bits and treat them as the RHEL version + rhel_version=$(awk '{print $4}' /etc/centos-release | awk -F "." '{print $1"."$2}') + else + rhel_version=$(grep ^VERSION_ID= /etc/os-release | awk -F "=" '{print $2}' | sed s/\"//g) + fi + + case $rhel_version in + # RHEL 7.5 and 7.6 + # ---------------- + 7.5|7.6) + case $kernel_version in + ${rhel_kernel_versions[7.5]}) lustre_version="2.10.5";; + ${rhel_kernel_versions[7.6]}) lustre_version="2.10.8";; + *) echo "ERROR: Unsupported kernel version $kernel_version for RHEL $rhel_version"; exit 1;; + esac + sudo yum -y install "https://downloads.whamcloud.com/public/lustre/lustre-${lustre_version}/el7/client/RPMS/x86_64/kmod-lustre-client-${lustre_version}-1.el7.x86_64.rpm" + sudo yum -y install "https://downloads.whamcloud.com/public/lustre/lustre-${lustre_version}/el7/client/RPMS/x86_64/lustre-client-${lustre_version}-1.el7.x86_64.rpm" + + verify_lustre_kmod + ;; + + # RHEL 7.7, 7.8, and 7.9 + # ---------------------- + 7.7|7.8|7.9) + architecture=$(uname -m) + case $architecture in + x86_64) + distro_name="el" + case $kernel_version in + ${rhel_kernel_versions[7.7]}) replace_ver="7.7";; + ${rhel_kernel_versions[7.8]}) replace_ver="7.8";; + ${rhel_kernel_versions[7.9]});; # Do nothing + *) echo "ERROR: Unsupported kernel version $kernel_version for RHEL $rhel_version"; exit 1;; + esac + ;; + aarch64) + distro_name="centos" + case $kernel_version in + # RHEL 7.8 for ARM-based AWS-Graviton powered instances have the 8.1 kernel version (see AWS docs) + ${rhel_kernel_versions[8.1]}) replace_ver="7.8";; + ${rhel_kernel_versions[8.2]});; # Do nothing + *) echo "ERROR: Unsupported kernel version $kernel_version for RHEL $rhel_version"; exit 1;; + esac + ;; + *) echo "ERROR: Unrecognized architecture: $architecture"; exit 1;; + esac + + curl https://fsx-lustre-client-repo-public-keys.s3.amazonaws.com/fsx-rpm-public-key.asc -o /tmp/fsx-rpm-public-key.asc + sudo rpm --import /tmp/fsx-rpm-public-key.asc + sudo curl "https://fsx-lustre-client-repo.s3.amazonaws.com/${distro_name}/7/fsx-lustre-client.repo" -o /etc/yum.repos.d/aws-fsx.repo + if [[ -n "${replace_ver+x}" ]]; then + sudo sed -i "s#7#${replace_ver}#" /etc/yum.repos.d/aws-fsx.repo + fi + + sudo yum clean all + sudo yum install -y kmod-lustre-client lustre-client + + verify_lustre_kmod + ;; + + # RHEL 8.2 or newer + # ----------------- + 8.[2-9]|8.[1-9]+([0-9])) + case $kernel_version in + ${rhel_kernel_versions[8.2]}) replace_ver="8.2";; + ${rhel_kernel_versions[8.3]}) replace_ver="8.3";; + *) + if is_kernel_version_lower "$kernel_version" "${rhel_kernel_versions[8.2]}"; then + echo "ERROR: Kernel version ($kernel_version) is lower than the minimum required version: ${rhel_kernel_versions[8.2]}" + exit 1 + fi + ;; + esac + + curl https://fsx-lustre-client-repo-public-keys.s3.amazonaws.com/fsx-rpm-public-key.asc -o /tmp/fsx-rpm-public-key.asc + sudo rpm --import /tmp/fsx-rpm-public-key.asc + sudo curl https://fsx-lustre-client-repo.s3.amazonaws.com/el/8/fsx-lustre-client.repo -o /etc/yum.repos.d/aws-fsx.repo + if [[ -n "${replace_ver+x}" ]]; then + # Change the baseurl referenced in aws-fsx.repo so we pull the correct version of the lustre client for this kernel + sudo sed -i "s#8#${replace_ver}#" /etc/yum.repos.d/aws-fsx.repo + fi + + sudo yum clean all + sudo yum install -y kmod-lustre-client lustre-client + + verify_lustre_kmod + ;; + *) echo "ERROR: Unsupported CentOS/RHEL version: $rhel_version"; exit 1;; + esac +} + +function install_on_ubuntu() { + ubuntu_version=$(grep ^VERSION_ID= /etc/os-release | awk -F "=" '{print $2}' | sed s/\"//g) + kernel_version=$(uname -r) + architecture=$(uname -m) + + case $ubuntu_version in + 16.04) + code_name="xenial" + min_version="4.4.0-1092-aws" + ;; + 18.04) + code_name="bionic" + case $architecture in + x86_64) min_version="4.15.0-1054-aws";; + aarch64) min_version="5.3.0-1023-aws";; + *) echo "ERROR: Unrecognized architecture: $architecture"; exit 1;; + esac + ;; + 20.04) + code_name="focal" + case $architecture in + x86_64) min_version="5.4.0-1011-aws";; + aarch64) min_version="5.4.0-1015-aws";; + *) echo "ERROR: Unrecognized architecture: $architecture"; exit 1;; + esac + ;; + *) echo "ERROR: Unsupported Ubuntu version: $ubuntu_version"; exit 1;; + esac + + wget -O - https://fsx-lustre-client-repo-public-keys.s3.amazonaws.com/fsx-ubuntu-public-key.asc | sudo apt-key add - + sudo bash -c "echo \"deb https://fsx-lustre-client-repo.s3.amazonaws.com/ubuntu $code_name main\" > /etc/apt/sources.list.d/fsxlustreclientrepo.list && apt-get update" + + if is_kernel_version_lower "$kernel_version" "$min_version"; then + echo "ERROR: Kernel version ($kernel_version) is lower than the minimum required version: $min_version" + exit 1 + fi + + sudo apt install -y "lustre-client-modules-${kernel_version}" +} + +function install_on_suse() { + sudo wget https://fsx-lustre-client-repo-public-keys.s3.amazonaws.com/fsx-sles-public-key.asc + sudo rpm --import fsx-sles-public-key.asc + sudo wget https://fsx-lustre-client-repo.s3.amazonaws.com/suse/sles-12/SLES-12/fsx-lustre-client.repo + + sudo zypper -n ar --gpgcheck-strict fsx-lustre-client.repo + + suse_version=$(grep ^VERSION_ID= /etc/os-release | awk -F "=" '{print $2}' | sed s/\"//g) + case $suse_version in + 12.3) sudo sed -i 's#SLES-12#SP3#' /etc/zypp/repos.d/aws-fsx.repo;; + 12.4) sudo sed -i 's#SLES-12#SP4#' /etc/zypp/repos.d/aws-fsx.repo;; + 12.5);; # Do nothing + *) + echo "ERROR: Unsupported SUSE Linux version: $suse_version" + exit 1 + ;; + esac + + sudo zypper -n refresh + sudo zypper -n install lustre-client +} + +if lustre_version=$(lfs --version); then + echo "Lustre client already installed: $lustre_version" + exit 0 +fi + +echo "Installing Lustre client..." + +os_id=$(grep ^ID= /etc/os-release | awk -F "=" '{print $2}' | sed s/\"//g) +case $os_id in + rhel|centos) install_on_rhel;; + ubuntu) install_on_ubuntu;; + sles) install_on_suse;; + amzn) + amzn_version=$(uname -r | awk -F "." '{print $5}') + case $amzn_version in + amzn1) install_on_al1;; + amzn2?(int)) install_on_al2;; + *) + echo "WARNING: Unrecognized Amazon Linux version: $amzn_version. Assuming Amazon Linux 2..." + install_on_al2 + ;; + esac + ;; + *) + pretty_name=$(grep ^PRETTY_NAME= /etc/os-release | awk -F "=" '{print $2}' | sed s/\"//g) + echo "ERROR: Unsupported operating system: $os_id ($pretty_name)" + exit 1 + ;; +esac diff --git a/packages/aws-rfdk/lib/core/scripts/bash/mountFsxLustre.sh b/packages/aws-rfdk/lib/core/scripts/bash/mountFsxLustre.sh new file mode 100644 index 000000000..d89d68d03 --- /dev/null +++ b/packages/aws-rfdk/lib/core/scripts/bash/mountFsxLustre.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +# This script will mount an Amazon FSx for Lustre File System to a specified mount directory on this instance, +# and set up /etc/fstab so that the file system is re-mounted on a system reboot. +# +# This script requires the Lustre client to be already installed on the instance. +# See https://docs.aws.amazon.com/fsx/latest/LustreGuide/install-lustre-client.html +# +# Note: This script uses get_metadata_token and get_region from ./metadataUtilities.sh +# Thus, the system must have applications pre-installed as outlined in that file. +# +# Script arguments: +# $1 -- file system Identifier (ex: fs-00000000000) +# $2 -- Mount path; directory that we mount the file system to. +# $3 -- Mount name +# $4 -- (optional) Lustre mount options for the file system. + +set -xeu + +if test $# -lt 3 +then + echo "Usage: $0 []" + exit 1 +fi + +SCRIPT_DIR=$(dirname $0) +source "${SCRIPT_DIR}/metadataUtilities.sh" + +# Make sure that the EC2 instance identity document is authentic before we use it to fetch +# information about the instance we're running on. +authenticate_identity_document + +METADATA_TOKEN=$(get_metadata_token) +AWS_REGION=$(get_region "${METADATA_TOKEN}") + +FILESYSTEM_ID=$1 +MOUNT_PATH=$2 +MOUNT_NAME=$3 +MOUNT_OPTIONS="${4:-}" + +FILESYSTEM_DNS_NAME="${FILESYSTEM_ID}.fsx.${AWS_REGION}.amazonaws.com" +MOUNT_OPTIONS="defaults,noatime,flock,_netdev,${MOUNT_OPTIONS}" + +sudo mkdir -p "${MOUNT_PATH}" + +# Attempt to mount the FSx file system + +# fstab may be missing a newline at end of file. +if test $(tail -c 1 /etc/fstab | wc -l) -eq 0 +then + # Newline was missing, so add one. + echo "" | sudo tee -a /etc/fstab +fi + +# See https://docs.aws.amazon.com/fsx/latest/LustreGuide/mount-fs-auto-mount-onreboot.html +MOUNT_TYPE=lustre +echo "${FILESYSTEM_DNS_NAME}@tcp:/${MOUNT_NAME} ${MOUNT_PATH} ${MOUNT_TYPE} ${MOUNT_OPTIONS} 0 0" | sudo tee -a /etc/fstab + +# We can sometimes fail to mount the file system with a "Connection reset by host" error, or similar. +# To counteract this, as best we can, we try to mount the file system a handful of times and fail-out +# only if unable to mount it after that. +TRIES=0 +MAX_TRIES=20 +while test ${TRIES} -lt ${MAX_TRIES} && ! sudo mount -a -t ${MOUNT_TYPE} +do + let TRIES=TRIES+1 + sleep 2 +done + +# Check whether the drive as been mounted. Fail if not. +cat /proc/mounts | grep "${MOUNT_PATH}" +exit $? diff --git a/packages/aws-rfdk/lib/core/test/mountable-fsx-lustre.test.ts b/packages/aws-rfdk/lib/core/test/mountable-fsx-lustre.test.ts new file mode 100644 index 000000000..d8265b150 --- /dev/null +++ b/packages/aws-rfdk/lib/core/test/mountable-fsx-lustre.test.ts @@ -0,0 +1,202 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + expect as cdkExpect, + haveResourceLike, +} from '@aws-cdk/assert'; +import { + AmazonLinuxGeneration, + Instance, + InstanceType, + MachineImage, + SecurityGroup, + Vpc, + WindowsVersion, +} from '@aws-cdk/aws-ec2'; +import * as fsx from '@aws-cdk/aws-fsx'; +import { + App, + Stack, +} from '@aws-cdk/core'; + +import { + MountableFsxLustre, + MountPermissions, +} from '../lib'; + +import { + escapeTokenRegex, +} from './token-regex-helpers'; + +describe('MountableFsxLustre', () => { + let app: App; + let stack: Stack; + let vpc: Vpc; + let fs: fsx.LustreFileSystem; + let fsSecurityGroup: SecurityGroup; + let instance: Instance; + let instanceSecurityGroup: SecurityGroup; + + beforeEach(() => { + app = new App(); + stack = new Stack(app); + vpc = new Vpc(stack, 'Vpc'); + fsSecurityGroup = new SecurityGroup(stack, 'FSxLSecurityGroup', { + vpc, + }); + fs = new fsx.LustreFileSystem(stack, 'FSxL', { + vpc, + vpcSubnet: vpc.privateSubnets[0], + lustreConfiguration: { + deploymentType: fsx.LustreDeploymentType.SCRATCH_1, + }, + storageCapacityGiB: 1200, + securityGroup: fsSecurityGroup, + }); + instanceSecurityGroup = new SecurityGroup(stack, 'InstanceSecurityGroup', { + vpc, + }); + instance = new Instance(stack, 'Instance', { + vpc, + instanceType: new InstanceType('t3.small'), + machineImage: MachineImage.latestAmazonLinux({ generation: AmazonLinuxGeneration.AMAZON_LINUX_2 }), + securityGroup: instanceSecurityGroup, + }); + }); + + test('mounts with defaults', () => { + // GIVEN + const mount = new MountableFsxLustre(fs, { + filesystem: fs, + }); + + // WHEN + mount.mountToLinuxInstance(instance, { + location: '/mnt/fsx/fs1', + }); + const userData = instance.userData.render(); + + // THEN + // Make sure the instance has been granted ingress to the FSxL's security group + cdkExpect(stack).to(haveResourceLike('AWS::EC2::SecurityGroupIngress', { + IpProtocol: 'tcp', + FromPort: 988, + ToPort: 1023, + SourceSecurityGroupId: stack.resolve(instanceSecurityGroup.securityGroupId), + GroupId: stack.resolve(fsSecurityGroup.securityGroupId), + })); + // Make sure we download the mountFsxLustre script asset bundle + const s3Copy = 'aws s3 cp \'s3://${Token[TOKEN.\\d+]}/${Token[TOKEN.\\d+]}${Token[TOKEN.\\d+]}\' \'/tmp/${Token[TOKEN.\\d+]}${Token[TOKEN.\\d+]}\''; + expect(userData).toMatch(new RegExp(escapeTokenRegex(s3Copy))); + expect(userData).toMatch(new RegExp(escapeTokenRegex('unzip /tmp/${Token[TOKEN.\\d+]}${Token[TOKEN.\\d+]}'))); + // Make sure we install the Lustre client + expect(userData).toMatch('bash ./installLustreClient.sh'); + // Make sure we execute the script with the correct args + expect(userData).toMatch(new RegExp(escapeTokenRegex('bash ./mountFsxLustre.sh ${Token[TOKEN.\\d+]} /mnt/fsx/fs1 ${Token[TOKEN.\\d+]} rw'))); + }); + + test('assert Linux-only', () => { + // GIVEN + const windowsInstance = new Instance(stack, 'WindowsInstance', { + vpc, + instanceType: new InstanceType('t3.small'), + machineImage: MachineImage.latestWindows(WindowsVersion.WINDOWS_SERVER_2019_ENGLISH_FULL_SQL_2017_STANDARD), + }); + const mount = new MountableFsxLustre(fs, { + filesystem: fs, + }); + + // THEN + expect(() => { + mount.mountToLinuxInstance(windowsInstance, { + location: '/mnt/fsx/fs1', + permissions: MountPermissions.READONLY, + }); + }).toThrowError('Target instance must be Linux.'); + }); + + test('readonly mount', () => { + // GIVEN + const mount = new MountableFsxLustre(fs, { + filesystem: fs, + }); + + // WHEN + mount.mountToLinuxInstance(instance, { + location: '/mnt/fsx/fs1', + permissions: MountPermissions.READONLY, + }); + const userData = instance.userData.render(); + + // THEN + expect(userData).toMatch(new RegExp(escapeTokenRegex('mountFsxLustre.sh ${Token[TOKEN.\\d+]} /mnt/fsx/fs1 ${Token[TOKEN.\\d+]} r'))); + }); + + test('extra mount options', () => { + // GIVEN + const mount = new MountableFsxLustre(fs, { + filesystem: fs, + extraMountOptions: [ + 'option1', + 'option2', + ], + }); + + // WHEN + mount.mountToLinuxInstance(instance, { + location: '/mnt/fsx/fs1', + }); + const userData = instance.userData.render(); + + // THEN + expect(userData).toMatch(new RegExp(escapeTokenRegex('mountFsxLustre.sh ${Token[TOKEN.\\d+]} /mnt/fsx/fs1 ${Token[TOKEN.\\d+]} rw,option1,option2'))); + }); + + test('asset is singleton', () => { + // GIVEN + const mount1 = new MountableFsxLustre(fs, { + filesystem: fs, + }); + const mount2 = new MountableFsxLustre(fs, { + filesystem: fs, + }); + + // WHEN + mount1.mountToLinuxInstance(instance, { + location: '/mnt/fsx/fs1', + }); + mount2.mountToLinuxInstance(instance, { + location: '/mnt/fsx/fs1', + }); + const userData = instance.userData.render(); + const s3Copy = 'aws s3 cp \'s3://${Token[TOKEN.\\d+]}/${Token[TOKEN.\\d+]}${Token[TOKEN.\\d+]}\''; + const regex = new RegExp(escapeTokenRegex(s3Copy), 'g'); + const matches = userData.match(regex) ?? []; + + // THEN + // The source of the asset copy should be identical from mount1 & mount2 + expect(matches).toHaveLength(2); + expect(matches[0]).toBe(matches[1]); + }); + + test('applies Lustre fileset', () => { + // GIVEN + const fileset = 'fileset'; + const mount = new MountableFsxLustre(fs, { + filesystem: fs, + fileset, + }); + + // WHEN + mount.mountToLinuxInstance(instance, { + location: '/mnt/fsx/fs1', + }); + const userData = instance.userData.render(); + + // THEN + expect(userData).toMatch(new RegExp(escapeTokenRegex(`bash ./mountFsxLustre.sh \${Token[TOKEN.\\d+]} /mnt/fsx/fs1 \${Token[TOKEN.\\d+]}/${fileset} rw`))); + }); +}); diff --git a/packages/aws-rfdk/lib/deadline/lib/render-queue.ts b/packages/aws-rfdk/lib/deadline/lib/render-queue.ts index 6a15ec66d..4073ad72c 100644 --- a/packages/aws-rfdk/lib/deadline/lib/render-queue.ts +++ b/packages/aws-rfdk/lib/deadline/lib/render-queue.ts @@ -217,6 +217,11 @@ export class RenderQueue extends RenderQueueBase implements IGrantable { */ private static readonly RE_VALID_HOSTNAME = /^[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$/i; + /** + * UID/GID for the RCS user. + */ + private static readonly RCS_USER = { uid: 1000, gid: 1000 }; + /** * The principal to grant permissions to. */ @@ -410,6 +415,7 @@ export class RenderQueue extends RenderQueueBase implements IGrantable { portNumber: internalPortNumber, protocol: internalProtocol, repository: props.repository, + runAsUser: RenderQueue.RCS_USER, }); this.taskDefinition = taskDefinition; @@ -639,6 +645,7 @@ export class RenderQueue extends RenderQueueBase implements IGrantable { portNumber: number, protocol: ApplicationProtocol, repository: IRepository, + runAsUser?: { uid: number, gid?: number }, }) { const { image, portNumber, protocol, repository } = props; @@ -676,6 +683,9 @@ export class RenderQueue extends RenderQueueBase implements IGrantable { environment.RCS_TLS_REQUIRE_CLIENT_CERT = 'no'; } + // We can ignore this in test coverage because we always use RenderQueue.RCS_USER + /* istanbul ignore next */ + const user = props.runAsUser ? `${props.runAsUser.uid}:${props.runAsUser.gid}` : undefined; const containerDefinition = taskDefinition.addContainer('ContainerDefinition', { image, memoryReservationMiB: 2048, @@ -684,6 +694,7 @@ export class RenderQueue extends RenderQueueBase implements IGrantable { logGroup: this.logGroup, streamPrefix: 'RCS', }), + user, }); containerDefinition.addMountPoints(connection.readWriteMountPoint); diff --git a/packages/aws-rfdk/lib/deadline/lib/repository.ts b/packages/aws-rfdk/lib/deadline/lib/repository.ts index db7d4a24f..b8eabcd18 100644 --- a/packages/aws-rfdk/lib/deadline/lib/repository.ts +++ b/packages/aws-rfdk/lib/deadline/lib/repository.ts @@ -470,6 +470,11 @@ export class Repository extends Construct implements IRepository { */ private static DEFAULT_DATABASE_RETENTION_PERIOD: Duration = Duration.days(15); + /** + * The Repository owner is 1000:1000 (ec2-user on AL2). + */ + private static REPOSITORY_OWNER = { uid: 1000, gid: 1000 }; + /** * @inheritdoc */ @@ -669,6 +674,7 @@ export class Repository extends Construct implements IRepository { repositoryInstallationPath, props.version, props.repositorySettings, + Repository.REPOSITORY_OWNER, ); this.configureSelfTermination(); @@ -902,6 +908,7 @@ export class Repository extends Construct implements IRepository { installPath: string, version: IVersion, settings?: Asset, + owner?: { uid: number, gid: number }, ) { const installerScriptAsset = ScriptAsset.fromPathConvention(this, 'DeadlineRepositoryInstallerScript', { osType: installerGroup.osType, @@ -918,9 +925,9 @@ export class Repository extends Construct implements IRepository { version.linuxInstallers.repository.s3Bucket.grantRead(installerGroup, version.linuxInstallers.repository.objectKey); const installerArgs = [ - `"s3://${version.linuxInstallers.repository.s3Bucket.bucketName}/${version.linuxInstallers.repository.objectKey}"`, - `"${installPath}"`, - version.linuxFullVersionString(), + '-i', `"s3://${version.linuxInstallers.repository.s3Bucket.bucketName}/${version.linuxInstallers.repository.objectKey}"`, + '-p', `"${installPath}"`, + '-v', version.linuxFullVersionString(), ]; if (settings) { @@ -928,7 +935,13 @@ export class Repository extends Construct implements IRepository { bucket: settings.bucket, bucketKey: settings.s3ObjectKey, }); - installerArgs.push(repositorySettingsFilePath); + installerArgs.push('-s', repositorySettingsFilePath); + } + + // We can ignore this in test coverage because we always use Repository.REPOSITORY_OWNER + /* istanbul ignore next */ + if (owner) { + installerArgs.push('-o', `${owner.uid}:${owner.gid}`); } installerScriptAsset.executeOn({ diff --git a/packages/aws-rfdk/lib/deadline/scripts/bash/installDeadlineRepository.sh b/packages/aws-rfdk/lib/deadline/scripts/bash/installDeadlineRepository.sh index e6d0491fa..40de48e43 100644 --- a/packages/aws-rfdk/lib/deadline/scripts/bash/installDeadlineRepository.sh +++ b/packages/aws-rfdk/lib/deadline/scripts/bash/installDeadlineRepository.sh @@ -3,21 +3,48 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -# This script downloads the deadline repository installer and executes it. -# Arguments: -# $1: s3 path for the deadline repository installer. -# $2: Path where deadline repository needs to be installed. -# $3: Deadline Repository Version being installed. -# $4: (Optional) Deadline Repository settings file to import. - # exit when any command fails set -xeuo pipefail -S3PATH=$1 -PREFIX=$2 -DEADLINE_REPOSITORY_VERSION=$3 -DEADLINE_REPOSITORY_SETTINGS_FILE=${4:-} -shift;shift; +USAGE="Usage: $0 -i -p -v + +This script downloads the deadline repository installer and executes it. + +Required arguments: + -i s3 path for the deadline repository installer. + -p Path where deadline repository needs to be installed. + -v Deadline Repository Version being installed. + +Optional arguments + -s Deadline Repository settings file to import. + -o The UID[:GID] that this script will chown the Repository files for. If GID is not specified, it defults to be the same as UID." + +while getopts "i:p:v:s:o:" opt; do + case $opt in + i) S3PATH="$OPTARG" + ;; + p) PREFIX="$OPTARG" + ;; + v) DEADLINE_REPOSITORY_VERSION="$OPTARG" + ;; + s) DEADLINE_REPOSITORY_SETTINGS_FILE="$OPTARG" + ;; + o) DEADLINE_REPOSITORY_OWNER="$OPTARG" + ;; + /?) + echo "$USAGE" + exit 1 + ;; + esac +done + +if [ -z "${S3PATH+x}" ] || \ + [ -z "${PREFIX+x}" ] || \ + [ -z "${DEADLINE_REPOSITORY_VERSION+x}" ]; then + echo "ERROR: Required arguments are missing." + echo "$USAGE" + exit 1 +fi # check if repository is already installed at the given path REPOSITORY_FILE_PATH="$PREFIX/settings/repository.ini" @@ -73,7 +100,7 @@ INSTALLER_DB_ARGS_STRING='' for key in "${!INSTALLER_DB_ARGS[@]}"; do INSTALLER_DB_ARGS_STRING=$INSTALLER_DB_ARGS_STRING"${key} ${INSTALLER_DB_ARGS[$key]} "; done REPOSITORY_SETTINGS_ARG_STRING='' -if [ ! -z "$DEADLINE_REPOSITORY_SETTINGS_FILE" ]; then +if [ ! -z "${DEADLINE_REPOSITORY_SETTINGS_FILE+x}" ]; then if [ ! -f "$DEADLINE_REPOSITORY_SETTINGS_FILE" ]; then echo "ERROR: Repository settings file was specified but is not a file: $DEADLINE_REPOSITORY_SETTINGS_FILE." exit 1 @@ -82,8 +109,42 @@ if [ ! -z "$DEADLINE_REPOSITORY_SETTINGS_FILE" ]; then fi fi +if [[ -n "${DEADLINE_REPOSITORY_OWNER+x}" ]]; then + if [[ ! "$DEADLINE_REPOSITORY_OWNER" =~ ^[0-9]+(:[0-9]+)?$ ]]; then + echo "ERROR: Deadline Repository owner is invalid: ${DEADLINE_REPOSITORY_OWNER}" + exit 1 + fi + REPOSITORY_OWNER_UID="${DEADLINE_REPOSITORY_OWNER%:*}" + REPOSITORY_OWNER_GID="${DEADLINE_REPOSITORY_OWNER#*:}" + + if [[ -z $REPOSITORY_OWNER_GID ]]; then + echo "Repository owner GID not specified. Defaulting to UID $REPOSITORY_OWNER_UID" + REPOSITORY_OWNER_GID=$REPOSITORY_OWNER_UID + fi + + EXISTING_GROUP=$(id -g $REPOSITORY_OWNER_UID) + if [[ $? -eq 0 ]]; then + # UID already taken, make sure the GID matches + if [[ ! $EXISTING_GROUP -eq $REPOSITORY_OWNER_GID ]]; then + echo "ERROR: Deadline Repository owner UID $REPOSITORY_OWNER_UID is already in use and has incorrect GID. Got GID $EXISTING_GROUP, expected $REPOSITORY_OWNER_GID" + exit 1 + fi + else + # Create the group + groupadd deadline-rcs-user -g $REPOSITORY_OWNER_GID + + # Create the user + useradd deadline-rcs-user -u $REPOSITORY_OWNER_UID -g $REPOSITORY_OWNER_GID + fi +fi + $REPO_INSTALLER --mode unattended --setpermissions false --prefix "$PREFIX" --installmongodb false --backuprepo false ${INSTALLER_DB_ARGS_STRING} $REPOSITORY_SETTINGS_ARG_STRING +if [[ -n "${REPOSITORY_OWNER_UID+x}" ]]; then + echo "Changing ownership of Deadline Repository files to UID=$REPOSITORY_OWNER_UID GID=$REPOSITORY_OWNER_GID" + sudo chown -R "$REPOSITORY_OWNER_UID:$REPOSITORY_OWNER_GID" "$PREFIX" +fi + set -x echo "Script completed successfully." diff --git a/packages/aws-rfdk/lib/deadline/test/render-queue.test.ts b/packages/aws-rfdk/lib/deadline/test/render-queue.test.ts index 94065e887..2e8566b4b 100644 --- a/packages/aws-rfdk/lib/deadline/test/render-queue.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/render-queue.test.ts @@ -2723,4 +2723,12 @@ describe('RenderQueue', () => { })); }); + test('runs as RCS user', () => { + // THEN + expectCDK(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: arrayWith( + objectLike({ User: '1000:1000' }), + ), + })); + }); }); diff --git a/packages/aws-rfdk/lib/deadline/test/repository.test.ts b/packages/aws-rfdk/lib/deadline/test/repository.test.ts index 5bef7c81b..a78aaaf4f 100644 --- a/packages/aws-rfdk/lib/deadline/test/repository.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/repository.test.ts @@ -1224,3 +1224,17 @@ test('imports repository settings', () => { const installerGroup = repository.node.tryFindChild('Installer') as AutoScalingGroup; expect(installerGroup.userData.render()).toContain(`aws s3 cp '${repositorySettings.s3ObjectUrl}'`); }); + +test('changes ownership of repository files', () => { + // GIVEN + const repo = new Repository(stack, 'Repository', { + version, + vpc, + }); + + // WHEN + const script = (repo.node.defaultChild as AutoScalingGroup).userData.render(); + + // THEN + expect(script).toMatch('-o 1000:1000'); +}); diff --git a/packages/aws-rfdk/package.json b/packages/aws-rfdk/package.json index 5bd149e12..f57c54826 100644 --- a/packages/aws-rfdk/package.json +++ b/packages/aws-rfdk/package.json @@ -111,6 +111,7 @@ "@aws-cdk/aws-elasticloadbalancingv2": "1.111.0", "@aws-cdk/aws-events": "1.111.0", "@aws-cdk/aws-events-targets": "1.111.0", + "@aws-cdk/aws-fsx": "1.111.0", "@aws-cdk/aws-globalaccelerator": "1.111.0", "@aws-cdk/aws-glue": "1.111.0", "@aws-cdk/aws-iam": "1.111.0", @@ -177,6 +178,7 @@ "@aws-cdk/aws-elasticloadbalancingv2": "1.111.0", "@aws-cdk/aws-events": "1.111.0", "@aws-cdk/aws-events-targets": "1.111.0", + "@aws-cdk/aws-fsx": "1.111.0", "@aws-cdk/aws-globalaccelerator": "1.111.0", "@aws-cdk/aws-glue": "1.111.0", "@aws-cdk/aws-iam": "1.111.0", diff --git a/yarn.lock b/yarn.lock index 7e92d0178..3ec02826c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -487,6 +487,17 @@ "@aws-cdk/core" "1.111.0" constructs "^3.3.69" +"@aws-cdk/aws-fsx@1.111.0": + version "1.111.0" + resolved "https://registry.yarnpkg.com/@aws-cdk/aws-fsx/-/aws-fsx-1.111.0.tgz#a38c3093a0fdca4051273b7a6fd70bfda6a79bf8" + integrity sha512-ww7kNd/rEkCKtqc63siZ9ul47SWsA8P13046J6vec5F+q7sIhHz8EaaeyxHqZnPYooyS1RqaW67RovuDWpAdew== + dependencies: + "@aws-cdk/aws-ec2" "1.111.0" + "@aws-cdk/aws-iam" "1.111.0" + "@aws-cdk/aws-kms" "1.111.0" + "@aws-cdk/core" "1.111.0" + constructs "^3.3.69" + "@aws-cdk/aws-globalaccelerator@1.111.0": version "1.111.0" resolved "https://registry.yarnpkg.com/@aws-cdk/aws-globalaccelerator/-/aws-globalaccelerator-1.111.0.tgz#5890737f02cd4d98ca6d69421e6b285f2cd304fd"