From eb2878fa19436f6a4676cbe5efb06ca33ad48249 Mon Sep 17 00:00:00 2001 From: Jericho Tolentino Date: Fri, 19 Feb 2021 23:11:55 +0000 Subject: [PATCH] feat(deadline): add security group configuration for Repository and RenderQueue --- .../aws-rfdk/lib/core/lib/mongodb-instance.ts | 13 ++ .../lib/deadline/lib/database-connection.ts | 44 ++++++ .../lib/deadline/lib/render-queue-ref.ts | 20 +++ .../aws-rfdk/lib/deadline/lib/render-queue.ts | 14 ++ .../aws-rfdk/lib/deadline/lib/repository.ts | 19 +++ .../deadline/test/database-connection.test.ts | 52 +++++- .../lib/deadline/test/render-queue.test.ts | 97 ++++++++++++ .../lib/deadline/test/repository.test.ts | 149 ++++++++++++++++++ 8 files changed, 407 insertions(+), 1 deletion(-) diff --git a/packages/aws-rfdk/lib/core/lib/mongodb-instance.ts b/packages/aws-rfdk/lib/core/lib/mongodb-instance.ts index ecea8a4bd..830653cf8 100644 --- a/packages/aws-rfdk/lib/core/lib/mongodb-instance.ts +++ b/packages/aws-rfdk/lib/core/lib/mongodb-instance.ts @@ -286,6 +286,12 @@ export interface IMongoDb extends IConnectable, IConstruct { * The version of MongoDB that is running on this instance. */ readonly version: MongoDbVersion; + + /** + * Adds security groups to the database. + * @param securityGroups The security groups to add. + */ + addSecurityGroup(...securityGroups: ISecurityGroup[]): void; } /** @@ -487,6 +493,13 @@ export class MongoDbInstance extends Construct implements IMongoDb, IGrantable { tagConstruct(this); } + /** + * @inheritdoc + */ + public addSecurityGroup(...securityGroups: ISecurityGroup[]): void { + securityGroups?.forEach(securityGroup => this.server.autoscalingGroup.addSecurityGroup(securityGroup)); + } + /** * Adds UserData commands to install & configure the CloudWatch Agent onto the instance. * diff --git a/packages/aws-rfdk/lib/deadline/lib/database-connection.ts b/packages/aws-rfdk/lib/deadline/lib/database-connection.ts index 8cefbdcb8..0a222046d 100644 --- a/packages/aws-rfdk/lib/deadline/lib/database-connection.ts +++ b/packages/aws-rfdk/lib/deadline/lib/database-connection.ts @@ -7,10 +7,12 @@ import * as path from 'path'; import { CfnDBCluster, CfnDBInstance, + DatabaseCluster, IDatabaseCluster, } from '@aws-cdk/aws-docdb'; import { IConnectable, + ISecurityGroup, OperatingSystemType, Port, } from '@aws-cdk/aws-ec2'; @@ -155,6 +157,13 @@ export abstract class DatabaseConnection { * @param child The child to make dependent upon this database. */ public abstract addChildDependency(child: IConstruct): void; + + /** + * Adds a security group to the database. + * + * @param securityGroups The security group to add. + */ + public abstract addSecurityGroup(...securityGroups: ISecurityGroup[]): void; } /** @@ -267,6 +276,34 @@ class DocDBDatabaseConnection extends DatabaseConnection { } } + /** + * @inheritdoc + */ + public addSecurityGroup(...securityGroups: ISecurityGroup[]): void { + const added = false; + if (this.props.database instanceof DatabaseCluster) { + const resource = (this.props.database as DatabaseCluster).node.findChild('Resource'); + if (resource instanceof CfnDBCluster) { + const cfnCluster = resource as CfnDBCluster; + const securityGroupIds = securityGroups.map(sg => sg.securityGroupId); + + if (cfnCluster.vpcSecurityGroupIds === undefined) { + cfnCluster.vpcSecurityGroupIds = securityGroupIds; + } else { + cfnCluster.vpcSecurityGroupIds.push(...securityGroupIds); + } + } + } + + if (!added) { + Annotations.of(this.props.database).addWarning( + `Failed to add the following security groups to ${this.props.database.node.id}: ${securityGroups.map(sg => sg.securityGroupId).join(', ')}. ` + + 'This is because either the "database" property passed to this class is not an instance of AWS CDK\'s DocumentDB cluster construct or the ' + + 'internal implementation of AWS CDK\'s DocumentDB cluster construct has changed.', + ); + } + } + /** * Deadline is only compatible with MongoDB 3.6. This function attempts to determine whether * the given DocDB version is compatible. @@ -375,6 +412,13 @@ class MongoDbInstanceDatabaseConnection extends DatabaseConnection { } } + /** + * @inheritdoc + */ + public addSecurityGroup(...securityGroups: ISecurityGroup[]): void { + this.props.database.addSecurityGroup(...securityGroups); + } + /** * Download the client PKCS#12 certificate for authenticating to the MongoDB, and place it into * the path defined by: DB_CERT_LOCATION diff --git a/packages/aws-rfdk/lib/deadline/lib/render-queue-ref.ts b/packages/aws-rfdk/lib/deadline/lib/render-queue-ref.ts index 1f5db8ae6..71cf24a29 100644 --- a/packages/aws-rfdk/lib/deadline/lib/render-queue-ref.ts +++ b/packages/aws-rfdk/lib/deadline/lib/render-queue-ref.ts @@ -8,6 +8,7 @@ import { } from '@aws-cdk/aws-certificatemanager'; import { InstanceType, + ISecurityGroup, IVpc, SubnetSelection, } from '@aws-cdk/aws-ec2'; @@ -211,6 +212,20 @@ export interface RenderQueueAccessLogProps { readonly prefix?: string; } +/** + * Options for security groups of the `RenderQueue`. + */ +export interface RenderQueueSecurityGroupsOptions { + /** + * The `AutoScalingGroup` security groups. + */ + readonly autoScalingGroup?: ISecurityGroup[]; + /** + * The `LoadBalancer` security groups. + */ + readonly loadBalancer?: ISecurityGroup[]; +} + /** * Properties for the Render Queue */ @@ -312,6 +327,11 @@ export interface RenderQueueProps { * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/application-load-balancers.html#deletion-protection */ readonly deletionProtection?: boolean; + + /** + * Options to add additional security groups to the `RenderQueue`. + */ + readonly securityGroups?: RenderQueueSecurityGroupsOptions; } /** diff --git a/packages/aws-rfdk/lib/deadline/lib/render-queue.ts b/packages/aws-rfdk/lib/deadline/lib/render-queue.ts index 276a41bcf..b39c43707 100644 --- a/packages/aws-rfdk/lib/deadline/lib/render-queue.ts +++ b/packages/aws-rfdk/lib/deadline/lib/render-queue.ts @@ -61,6 +61,7 @@ import { IRepository, IVersion, RenderQueueProps, + RenderQueueSecurityGroupsOptions, VersionQuery, } from '.'; @@ -448,6 +449,10 @@ export class RenderQueue extends RenderQueueBase implements IGrantable { }); } + if (props.securityGroups) { + this.addSecurityGroups(props.securityGroups); + } + this.node.defaultChild = taskDefinition; // Tag deployed resources with RFDK meta-data @@ -519,6 +524,15 @@ export class RenderQueue extends RenderQueueBase implements IGrantable { child.node.addDependency(this.taskDefinition); } + public addSecurityGroups(securityGroups: RenderQueueSecurityGroupsOptions) { + securityGroups.autoScalingGroup?.forEach(securityGroup => { + this.asg.addSecurityGroup(securityGroup); + }); + securityGroups.loadBalancer?.forEach(securityGroup => { + this.loadBalancer.addSecurityGroup(securityGroup); + }); + } + private createTaskDefinition(props: { image: ContainerImage, portNumber: number, diff --git a/packages/aws-rfdk/lib/deadline/lib/repository.ts b/packages/aws-rfdk/lib/deadline/lib/repository.ts index 9785c1a19..45db507a5 100644 --- a/packages/aws-rfdk/lib/deadline/lib/repository.ts +++ b/packages/aws-rfdk/lib/deadline/lib/repository.ts @@ -24,6 +24,7 @@ import { InstanceClass, InstanceSize, InstanceType, + ISecurityGroup, IVpc, OperatingSystemType, SubnetSelection, @@ -339,6 +340,11 @@ export interface RepositoryProps { * @default Duration.days(15) for the database */ readonly backupOptions?: RepositoryBackupOptions; + + /** + * The security groups to add to the database. + */ + readonly securityGroups?: ISecurityGroup[]; } /** @@ -535,6 +541,10 @@ export class Repository extends Construct implements IRepository { }); } + if (props.securityGroups) { + this.addSecurityGroup(...props.securityGroups); + } + // Launching the instance which installs the deadline repository in the stack. this.installerGroup = new AutoScalingGroup(this, 'Installer', { instanceType: InstanceType.of(InstanceClass.T3, InstanceSize.LARGE), @@ -602,6 +612,15 @@ export class Repository extends Construct implements IRepository { return validationErrors; } + /** + * Add the security groups to the repository + * + * @param securityGroups: The security groups to add + */ + public addSecurityGroup(...securityGroups: ISecurityGroup[]): void { + this.databaseConnection.addSecurityGroup(...securityGroups); + } + /** * @inheritdoc */ diff --git a/packages/aws-rfdk/lib/deadline/test/database-connection.test.ts b/packages/aws-rfdk/lib/deadline/test/database-connection.test.ts index 113c18037..2dbce82ca 100644 --- a/packages/aws-rfdk/lib/deadline/test/database-connection.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/database-connection.test.ts @@ -11,9 +11,12 @@ import { } from '@aws-cdk/assert'; import { DatabaseCluster, + Endpoint, + IDatabaseCluster, } from '@aws-cdk/aws-docdb'; import { AmazonLinuxGeneration, + Connections, Instance, InstanceClass, InstanceSize, @@ -31,9 +34,14 @@ import { import { PrivateHostedZone, } from '@aws-cdk/aws-route53'; -import { Secret } from '@aws-cdk/aws-secretsmanager'; import { + Secret, + SecretAttachmentTargetProps, +} from '@aws-cdk/aws-secretsmanager'; +import { + Construct, Duration, + ResourceEnvironment, Stack, } from '@aws-cdk/core'; @@ -264,6 +272,48 @@ describe('DocumentDB', () => { }).toThrowError('Connecting to the Deadline Database is currently only supported for Linux.'); }); + test('adds warning annotation when a security group cannot be added', () => { + // GIVEN + class FakeDatabaseCluster extends Construct implements IDatabaseCluster { + public readonly clusterIdentifier: string = ''; + public readonly instanceIdentifiers: string[] = []; + public readonly clusterEndpoint: Endpoint = new Endpoint('address', 123); + public readonly clusterReadEndpoint: Endpoint = new Endpoint('readAddress', 123); + public readonly instanceEndpoints: Endpoint[] = []; + public readonly securityGroupId: string = ''; + public readonly connections: Connections = new Connections(); + + public readonly stack: Stack; + public readonly env: ResourceEnvironment; + + constructor(scope: Construct, id: string) { + super(scope, id); + this.stack = Stack.of(scope); + this.env = {account: this.stack.account, region: this.stack.region}; + } + + asSecretAttachmentTarget(): SecretAttachmentTargetProps { + throw new Error('Method not implemented.'); + } + } + const fakeDatabase = new FakeDatabaseCluster(stack, 'FakeDatabase'); + const securityGroup = new SecurityGroup(stack, 'NewSecurityGroup', { vpc }); + const connection = DatabaseConnection.forDocDB({database: fakeDatabase, login: database.secret!}); + + // WHEN + connection.addSecurityGroup(securityGroup); + + // THEN + expect(fakeDatabase.node.metadata).toEqual(expect.arrayContaining([ + expect.objectContaining({ + type: 'aws:cdk:warning', + data: expect.stringMatching(new RegExp(`Failed to add the following security groups to ${fakeDatabase.node.id}: .*\\. ` + + 'This is because either the \\"database\\" property passed to this class is not an instance of AWS CDK\'s DocumentDB cluster construct or the ' + + 'internal implementation of AWS CDK\'s DocumentDB cluster construct has changed.')), + }), + ])); + }); + }); describe('DocumentDB Version Checks', () => { 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 e7046138b..1686251e4 100644 --- a/packages/aws-rfdk/lib/deadline/test/render-queue.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/render-queue.test.ts @@ -26,6 +26,8 @@ import { InstanceSize, InstanceType, MachineImage, + Port, + SecurityGroup, Subnet, Vpc, WindowsVersion, @@ -65,6 +67,7 @@ import { RenderQueue, RenderQueueImages, RenderQueueProps, + RenderQueueSecurityGroupsOptions, Repository, VersionQuery, } from '../lib'; @@ -2271,6 +2274,100 @@ describe('RenderQueue', () => { }); }); + + describe('Security Groups', () => { + let renderQueueSecurityGroups: RenderQueueSecurityGroupsOptions; + + beforeEach(() => { + const asgSecurityGroup = new SecurityGroup(stack, 'ASGSecurityGroup', { vpc }); + const lbSecurityGroup = new SecurityGroup(stack, 'LBSecurityGroup', { vpc }); + renderQueueSecurityGroups = { + autoScalingGroup: [asgSecurityGroup], + loadBalancer: [lbSecurityGroup], + }; + }); + + test('adds security groups on construction', () => { + // WHEN + new RenderQueue(stack, 'RenderQueue', { + images, + repository, + version: renderQueueVersion, + vpc, + securityGroups: renderQueueSecurityGroups, + }); + + // THEN + assertSecurityGroupsWereAdded(renderQueueSecurityGroups); + }); + + test('adds security groups post-construction', () => { + // GIVEN + const renderQueue = new RenderQueue(stack, 'RenderQueue', { + images, + repository, + version: renderQueueVersion, + vpc, + }); + + // WHEN + renderQueue.addSecurityGroups(renderQueueSecurityGroups); + + // THEN + assertSecurityGroupsWereAdded(renderQueueSecurityGroups); + }); + + test('security groups added are not attached to Connections object', () => { + // GIVEN + const renderQueue = new RenderQueue(stack, 'RenderQueue', { + images, + repository, + version: renderQueueVersion, + vpc, + securityGroups: renderQueueSecurityGroups, + }); + const peerSecurityGroup = new SecurityGroup(stack, 'PeerSecurityGroup', { vpc }); + + // WHEN + renderQueue.connections.allowFrom(peerSecurityGroup, Port.tcp(22)); + + // THEN + // Existing LoadBalancer security groups shouldn't have the ingress rule added + renderQueueSecurityGroups.loadBalancer?.forEach(lbSecurityGroup => { + expectCDK(stack).notTo(haveResourceLike('AWS::EC2::SecurityGroupIngress', { + IpProtocol: 'tcp', + FromPort: 22, + ToPort: 22, + GroupId: stack.resolve(lbSecurityGroup.securityGroupId), + SourceSecurityGroupId: stack.resolve(peerSecurityGroup.securityGroupId), + })); + }); + // Existing AutoScalingGroup security groups shouldn't have the ingress rule added + renderQueueSecurityGroups.autoScalingGroup?.forEach(asgSecurityGroup => { + expectCDK(stack).notTo(haveResourceLike('AWS::EC2::SecurityGroupIngress', { + IpProtocol: 'tcp', + FromPort: 22, + ToPort: 22, + GroupId: stack.resolve(asgSecurityGroup.securityGroupId), + SourceSecurityGroupId: stack.resolve(peerSecurityGroup.securityGroupId), + })); + }); + }); + + function assertSecurityGroupsWereAdded(securityGroups: RenderQueueSecurityGroupsOptions) { + securityGroups.autoScalingGroup?.forEach(asgSecurityGroup => { + expectCDK(stack).to(haveResourceLike('AWS::AutoScaling::LaunchConfiguration', { + SecurityGroups: arrayWith(stack.resolve(asgSecurityGroup.securityGroupId)), + })); + }); + securityGroups.loadBalancer?.forEach(lbSecurityGroup => { + expectCDK(stack).to(haveResourceLike('AWS::ElasticLoadBalancingV2::LoadBalancer', { + SecurityGroups: arrayWith(stack.resolve(lbSecurityGroup.securityGroupId)), + })); + }); + } + }); + test('validates VersionQuery is not in a different stack', () => { // GIVEN const newStack = new Stack(app, 'NewStack'); diff --git a/packages/aws-rfdk/lib/deadline/test/repository.test.ts b/packages/aws-rfdk/lib/deadline/test/repository.test.ts index 0b930a501..508f0eeac 100644 --- a/packages/aws-rfdk/lib/deadline/test/repository.test.ts +++ b/packages/aws-rfdk/lib/deadline/test/repository.test.ts @@ -21,8 +21,10 @@ import { InstanceClass, InstanceSize, InstanceType, + ISecurityGroup, IVpc, MachineImage, + SecurityGroup, Subnet, SubnetType, Vpc, @@ -31,7 +33,9 @@ import { import { FileSystem as EfsFileSystem, } from '@aws-cdk/aws-efs'; +import { PrivateHostedZone } from '@aws-cdk/aws-route53'; import { Bucket } from '@aws-cdk/aws-s3'; +import { Secret } from '@aws-cdk/aws-secretsmanager'; import { App, CfnElement, @@ -41,7 +45,12 @@ import { } from '@aws-cdk/core'; import { + MongoDbInstance, + MongoDbSsplLicenseAcceptance, + MongoDbVersion, MountableEfs, + X509CertificatePem, + X509CertificatePkcs12, } from '../../core'; import { testConstructTags, @@ -1061,6 +1070,146 @@ describe('tagging', () => { }); }); +describe('Security Groups', () => { + let repositorySecurityGroup: ISecurityGroup; + + beforeEach(() => { + repositorySecurityGroup = new SecurityGroup(stack, 'AdditionalSecurityGroup', { vpc }); + }); + + describe('MongoDB', () => { + let databaseConnection: DatabaseConnection; + let originalSecurityGroups: ISecurityGroup[]; + + beforeEach(() => { + const dnsZone = new PrivateHostedZone(stack, 'PrivateHostedZone', { + vpc, + zoneName: 'zone', + }); + const signingCertificate = new X509CertificatePem(stack, 'MongoSigningCertificate', { + subject: { cn: 'cn1' }, + }); + const serverCertificate = new X509CertificatePem(stack, 'MongoServerCertificate', { + subject: { cn: 'cn2' }, + signingCertificate, + }); + const mongoDb = new MongoDbInstance(stack, 'MongoDb', { + vpc, + mongoDb: { + dnsZone, + serverCertificate, + hostname: 'mongodb', + version: MongoDbVersion.COMMUNITY_3_6, + userSsplAcceptance: MongoDbSsplLicenseAcceptance.USER_ACCEPTS_SSPL, + }, + }); + const clientCertificate = new X509CertificatePkcs12(stack, 'MongoClientCertificate', { + sourceCertificate: serverCertificate, + }); + databaseConnection = DatabaseConnection.forMongoDbInstance({ + database: mongoDb, + clientCertificate, + }); + + // Store a reference to the original security groups (which will include the one created for the MongoDB server) + // to ensure the AutoScaling LaunchConfiguration being asserted in these tests is the one for the MongoDB construct + // and not for the Repository installer ASG. + originalSecurityGroups = mongoDb.server.connections.securityGroups; + }); + + test('adds security groups on construction', () => { + // WHEN + new Repository(stack, 'Repository', { + version, + vpc, + database: databaseConnection, + securityGroups: [repositorySecurityGroup], + }); + + // THEN + assertSecurityGroupWasAdded(repositorySecurityGroup); + }); + + test('adds security groups post-construction', () => { + // GIVEN + const repository = new Repository(stack, 'Repository', { + version, + vpc, + database: databaseConnection, + }); + + // WHEN + repository.addSecurityGroup(repositorySecurityGroup); + + // THEN + assertSecurityGroupWasAdded(repositorySecurityGroup); + }); + + function assertSecurityGroupWasAdded(securityGroup: ISecurityGroup) { + const originalSecurityGroupIds = originalSecurityGroups.map(sg => stack.resolve(sg.securityGroupId)); + expectCDK(stack).to(haveResourceLike('AWS::AutoScaling::LaunchConfiguration', { + SecurityGroups: arrayWith( + ...originalSecurityGroupIds, + stack.resolve(securityGroup.securityGroupId), + ), + })); + } + }); + + describe ('DocDB', () => { + let databaseConnection: DatabaseConnection; + + beforeEach(() => { + const dbCluster = new DatabaseCluster(stack, 'DatabaseCluster', { + instanceProps: { + vpc, + instanceType: InstanceType.of(InstanceClass.C5, InstanceSize.LARGE), + }, + masterUser: { username: 'username' }, + }); + const dbConnectionSecret = new Secret(stack, 'DBConnectionSecret'); + databaseConnection = DatabaseConnection.forDocDB({ + database: dbCluster, + login: dbConnectionSecret, + }); + }); + + test('adds security groups on construction', () => { + // WHEN + new Repository(stack, 'Repository', { + version, + vpc, + database: databaseConnection, + securityGroups: [repositorySecurityGroup], + }); + + // THEN + assertSecurityGroupWasAdded(repositorySecurityGroup); + }); + + test('adds security groups post-construction', () => { + // GIVEN + const repository = new Repository(stack, 'Repository', { + version, + vpc, + database: databaseConnection, + }); + + // WHEN + repository.addSecurityGroup(repositorySecurityGroup); + + // THEN + assertSecurityGroupWasAdded(repositorySecurityGroup); + }); + + function assertSecurityGroupWasAdded(securityGroup: ISecurityGroup) { + expectCDK(stack).to(haveResourceLike('AWS::DocDB::DBCluster', { + VpcSecurityGroupIds: arrayWith(stack.resolve(securityGroup.securityGroupId)), + })); + } + }); +}); + test('validates VersionQuery is not in a different stack', () => { // GIVEN const newStack = new Stack(app, 'NewStack');