diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/README.md b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/README.md index 114474602..49fa353ec 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/README.md +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/README.md @@ -32,8 +32,16 @@ These instructions assume that your working directory is `examples/deadline/All- popd pip install ../../../../dist/python/aws-rfdk-.tar.gz ``` -4. Change the value in the `deadline_client_linux_ami_map` variable in `package/config.py` to include the region + AMI ID mapping of your EC2 AMI(s) with Deadline Worker. +4. Change the value in the `deadline_client_linux_ami_map` variable in `package/config.py` to include the region + AMI ID mapping of your EC2 AMI(s) with Deadline Worker. You can use the following AWS CLI query to find AMI ID's: + ```bash + aws --region ec2 describe-images \ + --owners 357466774442 \ + --filters "Name=name,Values=*Worker*" "Name=name,Values=**" \ + --query 'Images[*].[ImageId, Name]' \ + --output text + ``` + And enter it into this section of `package/config.py`: ```python # For example, in the us-west-2 region self.deadline_client_linux_ami_map: Mapping[str, str] = { @@ -104,10 +112,7 @@ These instructions assume that your working directory is `examples/deadline/All- # Set this value to the version of AWS Thinkbox Deadline you'd like to deploy to your farm. Deadline 10.1.9 and up are supported. RFDK_DEADLINE_VERSION= - npx --package=aws-rfdk@${RFDK_VERSION} stage-deadline \ - --deadlineInstallerURI s3://thinkbox-installers/Deadline/${RFDK_DEADLINE_VERSION}/Linux/DeadlineClient-${RFDK_DEADLINE_VERSION}-linux-x64-installer.run \ - --dockerRecipesURI s3://thinkbox-installers/DeadlineDocker/${RFDK_DEADLINE_VERSION}/DeadlineDocker-${RFDK_DEADLINE_VERSION}.tar.gz \ - --output stage + npx --package=aws-rfdk@${RFDK_VERSION} stage-deadline ${RFDK_DEADLINE_VERSION} --output stage ``` 12. Deploy all the stacks in the sample app: diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/config.py b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/config.py index ed6f72d0a..6d8fba738 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/config.py +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/config.py @@ -18,8 +18,10 @@ class AppConfig: TODO: Fill these in with your own values. """ def __init__(self): - # A map of regions to Deadline Client Linux AMIs. - self.deadline_client_linux_ami_map: Mapping[str, str] = {'region': 'ami-id'} + # A map of regions to Deadline Client Linux AMIs.As an example, the Linux Deadline 10.1.12.1 AMI ID + # from us-west-2 is filled in. It can be used as-is, added to, or replaced. Ideally the version here + # should match the one used for staging the render queue and usage based licensing recipes. + self.deadline_client_linux_ami_map: Mapping[str, str] = {'us-west-2': 'ami-039f0c1faba28b015'} # A secret (in binary form) in SecretsManager that stores the UBL certificates in a .zip file. self.ubl_certificate_secret_arn: str =\ diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/lib/compute_tier.py b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/lib/compute_tier.py index a24b89d57..d0c349cdf 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/lib/compute_tier.py +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/lib/compute_tier.py @@ -24,6 +24,7 @@ from aws_rfdk import ( HealthMonitor, + SessionManagerHelper, ) from aws_rfdk.deadline import ( InstanceUserDataProvider, @@ -117,6 +118,11 @@ def __init__(self, scope: Construct, stack_id: str, *, props: ComputeTierProps, user_data_provider=UserDataProvider(self, 'UserDataProvider') ) + # This is an optional feature that will set up your EC2 instances to be enabled for use with + # the Session Manager. These worker fleet instances aren't available through a public subnet, + # so connecting to them directly through SSH isn't easy. + SessionManagerHelper.grant_permissions_to(self.worker_fleet) + if props.usage_based_licensing and props.licenses: props.usage_based_licensing.grant_port_access(self.worker_fleet, props.licenses) diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/lib/service_tier.py b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/lib/service_tier.py index 817b1e54b..334eddc7c 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/lib/service_tier.py +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/python/package/lib/service_tier.py @@ -31,6 +31,7 @@ from aws_rfdk import ( DistinguishedName, IMountableLinuxFilesystem, + SessionManagerHelper, X509CertificatePem ) from aws_rfdk.deadline import ( @@ -86,9 +87,10 @@ def __init__(self, scope: Construct, stack_id: str, *, props: ServiceTierProps, """ super().__init__(scope, stack_id, **kwargs) - # A bastion host to connect to the render farm with. - # The bastion host is for convenience (e.g. SSH into RenderQueue and WorkerFleet instances). - # This is not a critical component of the farm, so can safely be removed. + # Bastion instance for convenience (e.g. SSH into RenderQueue and WorkerFleet instances). + # Not a critical component of the farm, so this can be safely removed. An alternative way + # to access your hosts is also provided by the Session Manager, which is also configured + # later in this example. self.bastion = BastionHostLinux( self, 'Bastion', @@ -162,6 +164,14 @@ def __init__(self, scope: Construct, stack_id: str, *, props: ServiceTierProps, ) self.render_queue.connections.allow_default_port_from(self.bastion) + # This is an optional feature that will set up your EC2 instances to be enabled for use with + # the Session Manager. RFDK deploys EC2 instances that aren't available through a public subnet, + # so connecting to them by SSH isn't easy. This is an option to quickly access hosts without + # using a bastion instance. + # It's important to note that the permissions need to be granted to the render queue's ASG, + # rather than the render queue itself. + SessionManagerHelper.grant_permissions_to(self.render_queue.asg) + if props.ubl_licenses: if not props.ubl_certs_secret_arn: raise ValueError('UBL certificates secret ARN is required when using UBL but was not specified.') @@ -175,5 +185,10 @@ def __init__(self, scope: Construct, stack_id: str, *, props: ServiceTierProps, render_queue=self.render_queue, certificate_secret=ubl_cert_secret, ) + + # Another optional usage of the SessionManagerHelper that demonstrates how to configure the UBL + # construct's ASG for access. Note that this construct also requires you to apply the permissions + # to its ASG property. + SessionManagerHelper.grant_permissions_to(self.ubl_licensing.asg) else: self.ubl_licensing = None diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/README.md b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/README.md index 4b1d3cc5a..b8147e593 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/README.md +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/README.md @@ -18,8 +18,16 @@ These instructions assume that your working directory is `examples/deadline/All- ``` yarn install ``` -3. Change the value in the `deadlineClientLinuxAmiMap` variable in `bin/config.ts` to include the region + AMI ID mapping of your EC2 AMI(s) with Deadline Worker. +3. Change the value in the `deadlineClientLinuxAmiMap` variable in `bin/config.ts` to include the region + AMI ID mapping of your EC2 AMI(s) with Deadline Worker. You can use the following AWS CLI query to find AMI ID's: + ``` + aws --region ec2 describe-images \ + --owners 357466774442 \ + --filters "Name=name,Values=*Worker*" "Name=name,Values=**" \ + --query 'Images[*].[ImageId, Name]' \ + --output text + ``` + And enter it into this section of `bin/config.ts`: ```ts // For example, in the us-west-2 region public readonly deadlineClientLinuxAmiMap: Record = { diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/lib/compute-tier.ts b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/lib/compute-tier.ts index 1c3df9e36..97609cd0b 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/lib/compute-tier.ts +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/lib/compute-tier.ts @@ -12,7 +12,7 @@ import { import * as cdk from '@aws-cdk/core'; import { IHost, - InstanceUserDataProvider, + InstanceUserDataProvider, IRenderQueue, IWorkerFleet, UsageBasedLicense, @@ -22,6 +22,7 @@ import { import { HealthMonitor, IHealthMonitor, + SessionManagerHelper, } from 'aws-rfdk'; import { Asset } from '@aws-cdk/aws-s3-assets'; import * as path from 'path' @@ -72,7 +73,7 @@ class UserDataProvider extends InstanceUserDataProvider { } preRenderQueueConfiguration(host: IHost): void { host.userData.addCommands('echo preRenderQueueConfiguration'); - } + } preWorkerConfiguration(host: IHost): void { host.userData.addCommands('echo preWorkerConfiguration'); } @@ -80,7 +81,7 @@ class UserDataProvider extends InstanceUserDataProvider { host.userData.addCommands('echo postWorkerLaunch'); if (host.node.scope != undefined) { const testScript = new Asset( - host.node.scope as cdk.Construct, + host.node.scope as cdk.Construct, 'SampleAsset', {path: path.join(__dirname, '..', '..', 'scripts', 'configure_worker.sh')}, ); @@ -119,7 +120,7 @@ export class ComputeTier extends cdk.Stack { */ constructor(scope: cdk.Construct, id: string, props: ComputeTierProps) { super(scope, id, props); - + this.healthMonitor = new HealthMonitor(this, 'HealthMonitor', { vpc: props.vpc, // TODO - Evaluate deletion protection for your own needs. This is set to false to @@ -137,6 +138,11 @@ export class ComputeTier extends cdk.Stack { userDataProvider: new UserDataProvider(this, 'UserDataProvider'), }); + // This is an optional feature that will set up your EC2 instances to be enabled for use with + // the Session Manager. These worker fleet instances aren't available through a public subnet, + // so connecting to them directly through SSH isn't easy. + SessionManagerHelper.grantPermissionsTo(this.workerFleet); + if (props.usageBasedLicensing && props.licenses) { props.usageBasedLicensing.grantPortAccess(this.workerFleet, props.licenses); } diff --git a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/lib/service-tier.ts b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/lib/service-tier.ts index c3b97bc37..f004234b2 100644 --- a/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/lib/service-tier.ts +++ b/examples/deadline/All-In-AWS-Infrastructure-Basic/ts/lib/service-tier.ts @@ -22,7 +22,6 @@ import { } from 'aws-rfdk'; import { DatabaseConnection, - IRenderQueue, RenderQueue, Repository, Stage, @@ -34,6 +33,7 @@ import { Secret, } from '@aws-cdk/aws-secretsmanager'; import { Duration } from '@aws-cdk/core'; +import { SessionManagerHelper } from 'aws-rfdk/lib/core'; /** * Properties for {@link ServiceTier}. @@ -90,7 +90,7 @@ export class ServiceTier extends cdk.Stack { /** * The render queue. */ - public readonly renderQueue: IRenderQueue; + public readonly renderQueue: RenderQueue; /** * The UBL licensing construct. (License Forwarder) @@ -111,8 +111,10 @@ export class ServiceTier extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props: ServiceTierProps) { super(scope, id, props); - // Bastion instance for convenience (e.g. SSH into RenderQueue and WorkerFleet instances) - // Not a critical component of the farm, so this can be safely removed + // Bastion instance for convenience (e.g. SSH into RenderQueue and WorkerFleet instances). + // Not a critical component of the farm, so this can be safely removed. An alternative way + // to access your hosts is also provided by the Session Manager, which is also configured + // later in this example. this.bastion = new BastionHostLinux(this, 'Bastion', { vpc: props.vpc, subnetSelection: { @@ -173,11 +175,19 @@ export class ServiceTier extends cdk.Stack { }); this.renderQueue.connections.allowDefaultPortFrom(this.bastion); + // This is an optional feature that will set up your EC2 instances to be enabled for use with + // the Session Manager. RFDK deploys EC2 instances that aren't available through a public subnet, + // so connecting to them by SSH isn't easy. This is an option to quickly access hosts without + // using a bastion instance. + // It's important to note that the permissions need to be granted to the render queue's ASG, + // rather than the render queue itself. + SessionManagerHelper.grantPermissionsTo(this.renderQueue.asg); + if (props.ublLicenses) { if (!props.ublCertsSecretArn) { throw new Error('UBL licenses were set but no UBL Certs Secret Arn was set.'); } - const ublCertSecret = Secret.fromSecretArn(this, 'UBLCertsSecret', props.ublCertsSecretArn); + const ublCertSecret = Secret.fromSecretCompleteArn(this, 'UBLCertsSecret', props.ublCertsSecretArn); this.ublLicensing = new UsageBasedLicensing(this, 'UBLLicensing', { vpc: props.vpc, @@ -186,6 +196,11 @@ export class ServiceTier extends cdk.Stack { renderQueue: this.renderQueue, certificateSecret: ublCertSecret, }); + + // Another optional usage of the SessionManagerHelper that demonstrates how to configure the UBL + // construct's ASG for access. Note that this construct also requires you to apply the permissions + // to its ASG property. + SessionManagerHelper.grantPermissionsTo(this.ublLicensing.asg); } } } diff --git a/packages/aws-rfdk/lib/core/lib/index.ts b/packages/aws-rfdk/lib/core/lib/index.ts index 31b7b1430..c8138fc97 100644 --- a/packages/aws-rfdk/lib/core/lib/index.ts +++ b/packages/aws-rfdk/lib/core/lib/index.ts @@ -18,5 +18,6 @@ export * from './mountable-efs'; export * from './mountable-filesystem'; export { RFDK_VERSION } from './runtime-info'; export * from './script-assets'; +export * from './session-manager-helper'; export * from './staticip-server'; export * from './x509-certificate'; diff --git a/packages/aws-rfdk/lib/core/lib/session-manager-helper.ts b/packages/aws-rfdk/lib/core/lib/session-manager-helper.ts new file mode 100644 index 000000000..b205f5169 --- /dev/null +++ b/packages/aws-rfdk/lib/core/lib/session-manager-helper.ts @@ -0,0 +1,39 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + IGrantable, + PolicyStatement, +} from '@aws-cdk/aws-iam'; + +/** + * This is a helper class meant to make it easier to use the AWS Systems Manager Session Manager + * with any EC2 Instances or AutoScalingGroups. Once enabled, the Session Manager can be used to + * connect to an EC2 Instance through the AWS Console and open a shell session in the browser. + * + * Note that in order for the Session Manager to work, you will need an AMI that has the SSM-Agent + * installed and set to run at startup. The Amazon Linux 2 and Amazon provided Windows Server AMI's + * have this configured by default. + * + * More details about the AWS Systems Manager Session Manager can be found here: + * https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html + */ +export class SessionManagerHelper { + /** + * Grants the permissions required to enable Session Manager for the provided IGrantable. + */ + public static grantPermissionsTo(grantable: IGrantable): void { + grantable.grantPrincipal.addToPolicy(new PolicyStatement({ + actions: [ + 'ssmmessages:CreateControlChannel', + 'ssmmessages:CreateDataChannel', + 'ssmmessages:OpenControlChannel', + 'ssmmessages:OpenDataChannel', + 'ssm:UpdateInstanceInformation', + ], + resources: ['*'], + })); + } +} diff --git a/packages/aws-rfdk/lib/core/test/sessions-manager-helper.test.ts b/packages/aws-rfdk/lib/core/test/sessions-manager-helper.test.ts new file mode 100644 index 000000000..cb9d2eb6e --- /dev/null +++ b/packages/aws-rfdk/lib/core/test/sessions-manager-helper.test.ts @@ -0,0 +1,93 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + expect as expectCDK, + haveResourceLike, +} from '@aws-cdk/assert'; +import { + AutoScalingGroup, +} from '@aws-cdk/aws-autoscaling'; +import { + AmazonLinuxImage, + Instance, + InstanceClass, + InstanceSize, + InstanceType, + Vpc, +} from '@aws-cdk/aws-ec2'; +import { CfnElement, Stack } from '@aws-cdk/core'; + +import { SessionManagerHelper } from '../lib'; + +let stack: Stack; +let vpc: Vpc; +const instanceType = InstanceType.of(InstanceClass.T3, InstanceSize.MICRO); +const machineImage = new AmazonLinuxImage(); + +beforeEach(() => { + stack = new Stack(); + vpc = new Vpc(stack, 'VPC'); +}); + +test('Grant SSM permissions to Instance', () => { + const instance = new Instance(stack, 'Instance', { + vpc, + instanceType, + machineImage, + }); + SessionManagerHelper.grantPermissionsTo(instance); + + const instanceRole = stack.getLogicalId(instance.role.node.defaultChild as CfnElement); + + expectCDK(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'ssmmessages:CreateControlChannel', + 'ssmmessages:CreateDataChannel', + 'ssmmessages:OpenControlChannel', + 'ssmmessages:OpenDataChannel', + 'ssm:UpdateInstanceInformation', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + }, + Roles: [{ Ref: instanceRole }], + })); +}); + +test('Grant SSM permissions to ASG', () => { + const asg = new AutoScalingGroup(stack, 'ASG', { + vpc, + instanceType, + machineImage, + }); + SessionManagerHelper.grantPermissionsTo(asg); + + const asgRole = stack.getLogicalId(asg.role.node.defaultChild as CfnElement); + + expectCDK(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'ssmmessages:CreateControlChannel', + 'ssmmessages:CreateDataChannel', + 'ssmmessages:OpenControlChannel', + 'ssmmessages:OpenDataChannel', + 'ssm:UpdateInstanceInformation', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + }, + Roles: [{ Ref: asgRole }], + })); +});