Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): ssm policy helper #266

Merged
merged 1 commit into from
Dec 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 following AWS CLI query to find AMI ID's:
```bash
ddneilson marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -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',
Expand Down Expand Up @@ -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.')
Expand All @@ -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
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.
ryyakobe marked this conversation as resolved.
Show resolved Hide resolved
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 }],
}));
});