Skip to content

Commit

Permalink
feat(core): ssm policy helper
Browse files Browse the repository at this point in the history
Fixes #265
  • Loading branch information
horsmand committed Dec 17, 2020
1 parent 5db1f1f commit 556b104
Show file tree
Hide file tree
Showing 10 changed files with 206 additions and 17 deletions.
15 changes: 10 additions & 5 deletions examples/deadline/All-In-AWS-Infrastructure-Basic/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,16 @@ These instructions assume that your working directory is `examples/deadline/All-
popd
pip install ../../../../dist/python/aws-rfdk-<version>.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 follow AWS CLI query to find AMI ID's:
```bash
aws --region <region> ec2 describe-images \
--owners 357466774442 \
--filters "Name=name,Values=*Worker*" "Name=name,Values=*<version>*" \
--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] = {
Expand Down Expand Up @@ -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=<version_of_deadline>
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:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =\
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

from aws_rfdk import (
HealthMonitor,
SessionManagerHelper,
)
from aws_rfdk.deadline import (
InstanceUserDataProvider,
Expand Down Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from aws_rfdk import (
DistinguishedName,
IMountableLinuxFilesystem,
SessionManagerHelper,
X509CertificatePem
)
from aws_rfdk.deadline import (
Expand Down Expand Up @@ -162,6 +163,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.')
Expand All @@ -175,5 +184,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
10 changes: 9 additions & 1 deletion examples/deadline/All-In-AWS-Infrastructure-Basic/ts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <region> ec2 describe-images \
--owners 357466774442 \
--filters "Name=name,Values=*Worker*" "Name=name,Values=*<version>*" \
--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<string, string> = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import * as cdk from '@aws-cdk/core';
import {
IHost,
InstanceUserDataProvider,
InstanceUserDataProvider,
IRenderQueue,
IWorkerFleet,
UsageBasedLicense,
Expand All @@ -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'
Expand Down Expand Up @@ -72,15 +73,15 @@ class UserDataProvider extends InstanceUserDataProvider {
}
preRenderQueueConfiguration(host: IHost): void {
host.userData.addCommands('echo preRenderQueueConfiguration');
}
}
preWorkerConfiguration(host: IHost): void {
host.userData.addCommands('echo preWorkerConfiguration');
}
postWorkerLaunch(host: IHost): void {
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')},
);
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
} from 'aws-rfdk';
import {
DatabaseConnection,
IRenderQueue,
RenderQueue,
Repository,
Stage,
Expand All @@ -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}.
Expand Down Expand Up @@ -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)
Expand All @@ -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: {
Expand Down Expand Up @@ -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,
Expand All @@ -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);
}
}
}
1 change: 1 addition & 0 deletions packages/aws-rfdk/lib/core/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
39 changes: 39 additions & 0 deletions packages/aws-rfdk/lib/core/lib/session-manager-helper.ts
Original file line number Diff line number Diff line change
@@ -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: ['*'],
}));
}
}
93 changes: 93 additions & 0 deletions packages/aws-rfdk/lib/core/test/sessions-manager-helper.test.ts
Original file line number Diff line number Diff line change
@@ -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 }],
}));
});

0 comments on commit 556b104

Please sign in to comment.