Skip to content

Commit

Permalink
feat: add ability to use EFS access points
Browse files Browse the repository at this point in the history
- Allows instances to access EFS with a masqueraded UID/GID
- Instances that mount EFS do not need OS-level UID/GIDs synchronized with the file-system
  • Loading branch information
jusiskin committed Mar 10, 2021
1 parent f84097e commit 85a14a5
Show file tree
Hide file tree
Showing 16 changed files with 387 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def main():
# ------------------------------
service_props = service_tier.ServiceTierProps(
database=storage.database,
file_system=storage.file_system,
mountable_file_system=storage.mountable_file_system,
vpc=network.vpc,
ubl_certs_secret_arn=config.ubl_certificate_secret_arn,
ubl_licenses=config.ubl_licenses,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

from aws_rfdk import (
DistinguishedName,
IMountableLinuxFilesystem,
MountableEfs,
SessionManagerHelper,
X509CertificatePem
)
Expand Down Expand Up @@ -58,8 +58,8 @@ class ServiceTierProps(StackProps):
vpc: IVpc
# The database to connect to.
database: DatabaseConnection
# The file system to install Deadline Repository to.
file_system: IMountableLinuxFilesystem
# The file-system to install Deadline Repository to.
mountable_file_system: MountableEfs
# The ARN of the secret containing the UBL certificates .zip file (in binary form).
ubl_certs_secret_arn: typing.Optional[str]
# The UBL licenses to configure
Expand Down Expand Up @@ -109,9 +109,9 @@ def __init__(self, scope: Construct, stack_id: str, *, props: ServiceTierProps,
]
)

# Granting the bastion access to the file system mount for convenience.
# This can also safely be removed.
props.file_system.mount_to_linux_instance(
# Mounting the root of the EFS file-system to the bastion access for convenience.
# This can safely be removed.
MountableEfs(self, filesystem=props.mountable_file_system.file_system).mount_to_linux_instance(
self.bastion.instance,
location='/mnt/efs'
)
Expand All @@ -127,7 +127,7 @@ def __init__(self, scope: Construct, stack_id: str, *, props: ServiceTierProps,
'Repository',
vpc=props.vpc,
database=props.database,
file_system=props.file_system,
file_system=props.mountable_file_system,
repository_installation_timeout=Duration.minutes(20),
version=self.version
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
SubnetType
)
from aws_cdk.aws_efs import (
AccessPoint,
Acl,
FileSystem,
PosixUser
)
from aws_cdk.aws_route53 import (
IPrivateHostedZone
Expand Down Expand Up @@ -75,20 +78,60 @@ def __init__(self, scope: Construct, stack_id: str, *, props: StorageTierProps,
:param kwargs: Any kwargs that need to be passed on to the parent class.
"""
super().__init__(scope, stack_id, **kwargs)
# The file system to use (e.g. to install Deadline Repository onto).
self.file_system = MountableEfs(

# The file-system to use (e.g. to install Deadline Repository onto).
file_system = FileSystem(
self,
'EfsFileSystem',
vpc=props.vpc,
# TODO - Evaluate this removal policy for your own needs. This is set to DESTROY to
# cleanly remove everything when this stack is destroyed. If you would like to ensure
# that your data is not accidentally deleted, you should modify this value.
removal_policy=RemovalPolicy.DESTROY
)

# Create an EFS access point that is used to grant the Repository and RenderQueue with write access to the
# Deadline Repository directory in the EFS file-system.
access_point = AccessPoint(
self,
filesystem=FileSystem(
self,
'EfsFileSystem',
vpc=props.vpc,
# TODO - Evaluate this removal policy for your own needs. This is set to DESTROY to
# cleanly remove everything when this stack is destroyed. If you would like to ensure
# that your data is not accidentally deleted, you should modify this value.
removal_policy=RemovalPolicy.DESTROY
'AccessPoint',
file_system=file_system,

# The AccessPoint will create the directory (denoted by the path property below) if it doesn't exist with
# the owning UID/GID set as specified here. These should be set up to grant read and write access to the
# UID/GID configured in the "poxis_user" property below.
create_acl=Acl(
owner_uid='10000',
owner_gid='10000',
permissions='750',
),

# When you mount the EFS via the access point, the mount will be rooted at this path in the EFS file-system
path='/DeadlineRepository',

# TODO - When you mount the EFS via the access point, all file-system operations will be performed using
# these UID/GID values instead of those from the user on the system where the EFS is mounted.
#
# The values below are set as UID/GID 0 for RFDK users that are migrating from previous version of this
# All-in-AWS-infrastructure-Basic example CDK application. This grants root (unrestricted) access to any
# mount of the EFS access point. Users migrating from previous versions of the example are advised to
# instead connect to a bastion instance with the EFS mounted and change the permissions of the Deadline
# Repository directory and files to be owned by a different UID/GID and modify these values accordingly.
#
# Anyone building a new CDK application based on this example are encouraged to change these to match the
# "ownerGid" and "ownerUid" above as a reasonable starting point.
posix_user=PosixUser(
uid='0',
gid='0'
)
)

self.mountable_file_system = MountableEfs(
self,
filesystem=file_system,
access_point=access_point
)

# The database to connect Deadline to.
self.database: Optional[DatabaseConnection] = None

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ if (config.deployMongoDB) {
const service = new ServiceTier(app, 'ServiceTier', {
env,
database: storage.database,
fileSystem: storage.fileSystem,
mountableFileSystem: storage.mountableFileSystem,
vpc: network.vpc,
deadlineVersion: config.deadlineVersion,
ublCertsSecretArn: config.ublCertificatesSecretArn,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from '@aws-cdk/aws-route53';
import * as cdk from '@aws-cdk/core';
import {
IMountableLinuxFilesystem,
MountableEfs,
X509CertificatePem,
} from 'aws-rfdk';
import {
Expand All @@ -33,7 +33,6 @@ import {
import {
Secret,
} from '@aws-cdk/aws-secretsmanager';
import { Duration } from '@aws-cdk/core';
import { SessionManagerHelper } from 'aws-rfdk/lib/core';

/**
Expand All @@ -51,9 +50,9 @@ export interface ServiceTierProps extends cdk.StackProps {
readonly database: DatabaseConnection;

/**
* The file system to install Deadline Repository to.
* The file-system to install Deadline Repository to.
*/
readonly fileSystem: IMountableLinuxFilesystem;
readonly mountableFileSystem: MountableEfs;

/**
* Our self-signed root CA certificate for the internal endpoints in the farm.
Expand Down Expand Up @@ -136,11 +135,15 @@ export class ServiceTier extends cdk.Stack {
volume: BlockDeviceVolume.ebs(50, {
encrypted: true,
})},
]
],
});
// Granting the bastion access to the file system mount for convenience
props.database.allowConnectionsFrom(this.bastion);

// Granting the bastion access to the entire EFS file-system.
// This can also be safely removed
props.fileSystem.mountToLinuxInstance(this.bastion.instance, {
new MountableEfs(this, {
filesystem: props.mountableFileSystem.fileSystem,
}).mountToLinuxInstance(this.bastion.instance, {
location: '/mnt/efs',
});

Expand All @@ -152,8 +155,9 @@ export class ServiceTier extends cdk.Stack {
vpc: props.vpc,
version: this.version,
database: props.database,
fileSystem: props.fileSystem,
repositoryInstallationTimeout: Duration.minutes(20),
fileSystem: props.mountableFileSystem,
repositoryInstallationTimeout: cdk.Duration.minutes(20),
repositoryInstallationPrefix: "/",
});

const images = new ThinkboxDockerImages(this, 'Images', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import {
} from '@aws-cdk/aws-ec2';
import * as cdk from '@aws-cdk/core';
import { DatabaseCluster } from '@aws-cdk/aws-docdb';
import { FileSystem } from '@aws-cdk/aws-efs';
import {
AccessPoint,
FileSystem,
} from '@aws-cdk/aws-efs';
import { IPrivateHostedZone } from '@aws-cdk/aws-route53';
import { RemovalPolicy, Duration } from '@aws-cdk/core';
import {
IMountableLinuxFilesystem,
MongoDbInstance,
MongoDbPostInstallSetup,
MongoDbSsplLicenseAcceptance,
Expand Down Expand Up @@ -45,9 +47,9 @@ export interface StorageTierProps extends cdk.StackProps {
*/
export abstract class StorageTier extends cdk.Stack {
/**
* The file system to use (e.g. to install Deadline Repository onto).
* The mountable file-system to use for the Deadline Repository
*/
public readonly fileSystem: IMountableLinuxFilesystem;
public readonly mountableFileSystem: MountableEfs;

/**
* The database to connect Deadline to.
Expand All @@ -63,15 +65,51 @@ export abstract class StorageTier extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props: StorageTierProps) {
super(scope, id, props);

this.fileSystem = new MountableEfs(this, {
filesystem: new FileSystem(this, 'EfsFileSystem', {
vpc: props.vpc,
encrypted: true,
// TODO - Evaluate this removal policy for your own needs. This is set to DESTROY to
// cleanly remove everything when this stack is destroyed. If you would like to ensure
// that your data is not accidentally deleted, you should modify this value.
removalPolicy: RemovalPolicy.DESTROY,
}),
const fileSystem = new FileSystem(this, 'EfsFileSystem', {
vpc: props.vpc,
encrypted: true,
// TODO - Evaluate this removal policy for your own needs. This is set to DESTROY to
// cleanly remove everything when this stack is destroyed. If you would like to ensure
// that your data is not accidentally deleted, you should modify this value.
removalPolicy: RemovalPolicy.DESTROY,
});

// Create an EFS access point that is used to grant the Repository and RenderQueue with write access to the Deadline
// Repository directory in the EFS file-system.
const accessPoint = new AccessPoint(this, 'AccessPoint', {
fileSystem,

// The AccessPoint will create the directory (denoted by the "path" property below) if it doesn't exist with the
// owning UID/GID set as specified here. These should be set up to grant read and write access to the UID/GID
// configured in the "poxisUser" property below.
createAcl: {
ownerGid: '10000',
ownerUid: '10000',
permissions: '750',
},

// When you mount the EFS via the access point, the mount will be rooted at this path in the EFS file-system
path: '/DeadlineRepository',

// TODO - When you mount the EFS via the access point, all file-system operations will be performed using these
// UID/GID values instead of those from the user on the system where the EFS is mounted.
//
// The values below are set as UID/GID 0 for RFDK users that are migrating from previous version of this
// All-in-AWS-infrastructure-Basic example CDK application. This grants root (unrestricted) access to any mount of
// the EFS access point. Users migrating from previous versions of the example are advised to instead connect to a
// bastion instance with the EFS mounted and change the permissions of the Deadline repository directory and files
// to be owned by a different UID/GID and modify these values accordingly. Anyone building a new CDK application
// based on this example are encouraged to change these to match the "ownerGid" and "ownerUid" above as a
// reasonable starting point.
posixUser: {
uid: '0',
gid: '0',
},
});

this.mountableFileSystem = new MountableEfs(this, {
filesystem: fileSystem,
accessPoint,
});
}
}
Expand Down
17 changes: 17 additions & 0 deletions packages/aws-rfdk/lib/core/lib/mount-permissions-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,21 @@ export class MountPermissionsHelper {
}
throw new Error(`Unhandled MountPermission: ${permission}`);
}

/**
* Convert the given permission into the appropriate list of IAM actions allowed on the EFS FileSystem required for
* the mount.
*
* @param permission The permission to convert. Defaults to {@link MountPermissions.READWRITE} if not defined.
*/
public static toEfsIAMActions(permission?: MountPermissions): string[] {
permission = permission ?? MountPermissions.READWRITE;
const iamActions = [
'elasticfilesystem:ClientMount',
];
if (permission === MountPermissions.READWRITE) {
iamActions.push('elasticfilesystem:ClientWrite');
}
return iamActions;
}
}
44 changes: 42 additions & 2 deletions packages/aws-rfdk/lib/core/lib/mountable-efs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import {
Port,
} from '@aws-cdk/aws-ec2';
import * as efs from '@aws-cdk/aws-efs';
import {
PolicyStatement,
} from '@aws-cdk/aws-iam';
import {
Asset,
} from '@aws-cdk/aws-s3-assets';
Expand Down Expand Up @@ -37,6 +40,13 @@ export interface MountableEfsProps {
*/
readonly filesystem: efs.IFileSystem;

/**
* An optional access point to use for mounting the file-system
*
* @default no access point is used
*/
readonly accessPoint?: efs.IAccessPoint;

/**
* Extra NFSv4 mount options that will be added to /etc/fstab for the file system.
* See: {@link https://www.man7.org/linux/man-pages//man5/nfs.5.html}
Expand Down Expand Up @@ -65,7 +75,14 @@ export interface MountableEfsProps {
* @todo Add support for specifying an AccessPoint for the EFS filesystem to enforce user and group information for all file system requests.
*/
export class MountableEfs implements IMountableLinuxFilesystem {
constructor(protected readonly scope: Construct, protected readonly props: MountableEfsProps) {}
/**
* The underlying EFS filesystem that is mounted
*/
public readonly fileSystem: efs.IFileSystem;

constructor(protected readonly scope: Construct, protected readonly props: MountableEfsProps) {
this.fileSystem = props.filesystem;
}

/**
* @inheritdoc
Expand All @@ -75,6 +92,23 @@ export class MountableEfs implements IMountableLinuxFilesystem {
throw new Error('Target instance must be Linux.');
}

target.node.addDependency(this.props.filesystem.mountTargetsAvailable);

if (this.props.accessPoint) {
const grantActions = MountPermissionsHelper.toEfsIAMActions(mount?.permissions);
target.grantPrincipal.addToPrincipalPolicy(new PolicyStatement({
resources: [
(this.props.filesystem.node.defaultChild as efs.CfnFileSystem).attrArn,
],
actions: grantActions,
conditions: {
StringEquals: {
'elasticfilesystem:AccessPointArn': this.props.accessPoint.accessPointArn,
},
},
}));
}

target.connections.allowTo(this.props.filesystem, this.props.filesystem.connections.defaultPort as Port);

const mountScriptAsset = this.mountAssetSingleton();
Expand All @@ -86,8 +120,14 @@ export class MountableEfs implements IMountableLinuxFilesystem {

const mountDir: string = path.posix.normalize(mount.location);
const mountOptions: string[] = [ MountPermissionsHelper.toLinuxMountOption(mount.permissions) ];
if (this.props.accessPoint) {
mountOptions.push(
'iam',
`accesspoint=${this.props.accessPoint.accessPointId}`,
);
}
if (this.props.extraMountOptions) {
mountOptions.push( ...this.props.extraMountOptions);
mountOptions.push(...this.props.extraMountOptions);
}
const mountOptionsStr: string = mountOptions.join(',');

Expand Down
Loading

0 comments on commit 85a14a5

Please sign in to comment.